diff --git a/CollapseLauncher/Classes/EventsManagement/BackgroundActivityManager.cs b/CollapseLauncher/Classes/EventsManagement/BackgroundActivityManager.cs index 2ec01077c..d1d83fc86 100644 --- a/CollapseLauncher/Classes/EventsManagement/BackgroundActivityManager.cs +++ b/CollapseLauncher/Classes/EventsManagement/BackgroundActivityManager.cs @@ -259,6 +259,12 @@ private static void AttachEventToNotification(PresetConfig presetConfig, IBackgr activity!.ProgressChanged += progressChangedEventHandler; activity!.StatusChanged += statusChangedEventHandler; + // The notification can be attached after the operation has already + // started. Hydrate it immediately instead of waiting for another + // progress/status event and leaving the placeholder text visible. + progressChangedEventHandler(activity, activity.Progress); + statusChangedEventHandler(activity, activity.Status); + activity.FlushingTrigger += (_, _) => { activity.ProgressChanged -= progressChangedEventHandler; diff --git a/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.CodecDetect.cs b/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.CodecDetect.cs index ed4a97354..1156e2df3 100644 --- a/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.CodecDetect.cs +++ b/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.CodecDetect.cs @@ -49,9 +49,8 @@ public partial class ImageBackgroundManager // -- Check using FFmpeg if available MediaSupport codecInfo = FFmpegPInvoke.GetMediaSupport(filePath); - PixelColorModel pixelFormat = codecInfo.VideoPixelFormatInfo.ColorModel; if ((GlobalIsUseFFmpeg && GlobalIsFFmpegAvailable) || - (codecInfo.Video.DecoderAvailable && pixelFormat.HasFlag(PixelColorModel.Rgb))) + RequiresFFmpegDecoder(codecInfo)) { return (true, true, true); } @@ -77,6 +76,14 @@ public partial class ImageBackgroundManager #endregion + private static bool RequiresFFmpegDecoder(MediaSupport codecInfo) + { + // Windows' H.264 decoder does not support the High 10 profile used by some backgrounds. + return codecInfo.Video.DecoderAvailable && + (codecInfo.VideoPixelFormatInfo.ColorModel.HasFlag(PixelColorModel.Rgb) || + codecInfo.VideoPixelFormatInfo.Format is AVPixelFormat.AV_PIX_FMT_YUV420P10LE or AVPixelFormat.AV_PIX_FMT_YUV420P10BE); + } + private static bool IsMediaFileExtensionSupported(ReadOnlySpan filePath) { return IsImageMediaFileExtensionSupported(filePath) || diff --git a/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.Loaders.cs b/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.Loaders.cs index 01fa62784..003848bca 100644 --- a/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.Loaders.cs +++ b/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.Loaders.cs @@ -1,4 +1,4 @@ -using CollapseLauncher.Extension; +using CollapseLauncher.Extension; using CollapseLauncher.Helper; using CollapseLauncher.Helper.Background; using CollapseLauncher.Helper.Image; @@ -154,8 +154,7 @@ public void PreloadBackground(PresetConfig? preset, Grid? presenterGrid) bool isVideo = IsVideoMediaFileExtensionSupported(primaryBg ?? staticBgUrl!); MediaSupport codecInfo = isVideo ? FFmpegPInvoke.GetMediaSupport(localBackground) : default; - PixelColorModel pixelFormat = codecInfo.VideoPixelFormatInfo.ColorModel; - if (isUseFFmpeg || (codecInfo.Video.DecoderAvailable && pixelFormat.HasFlag(PixelColorModel.Rgb))) + if (isUseFFmpeg || RequiresFFmpegDecoder(codecInfo)) { isUseFFmpeg = true; } @@ -367,6 +366,7 @@ private void SpawnImageLayer(Uri? overlayFilePath, if (!forceReload && CurrentBackgroundElement != null && + CurrentBackgroundElement.UseFfmpegDecoder == (forceFFmpeg || (GlobalIsUseFFmpeg && GlobalIsFFmpegAvailable)) && _displayedContext != null) { if (context.Equals(_displayedContext)) @@ -448,9 +448,10 @@ private LayeredBackgroundImage CreateLayerElement(Uri? { LayeredBackgroundImage layerElement = new() { + Opacity = 0, BackgroundSource = backgroundFilePath, BackgroundStaticSource = backgroundStaticFilePath, - ForegroundSource = overlayFilePath, + ForegroundSource = isVideo ? overlayFilePath : null, UseFfmpegDecoder = forceFFmpeg || (GlobalIsUseFFmpeg && GlobalIsFFmpegAvailable), Tag = context, ParallaxResetOnUnfocused = false, @@ -568,6 +569,8 @@ private void LayerElementOnLoaded(LayeredBackgroundImage layerElement) PresenterGrid.Background = null; } + layerElement.Opacity = 1; + if (CurrentIsEnableBackgroundAutoPlay && WindowUtility.CurrentWindowIsVisible) { Play(false); @@ -676,4 +679,4 @@ private async Task GetMediaAccentColor(object? context) SentryHelper.ExceptionHandler(ex); } } -} \ No newline at end of file +} diff --git a/CollapseLauncher/Classes/Interfaces/Class/ProgressBase.cs b/CollapseLauncher/Classes/Interfaces/Class/ProgressBase.cs index 9ca682bfa..88f3d842a 100644 --- a/CollapseLauncher/Classes/Interfaces/Class/ProgressBase.cs +++ b/CollapseLauncher/Classes/Interfaces/Class/ProgressBase.cs @@ -467,6 +467,13 @@ protected virtual void UpdateProgressCopyStream(long currentPosition, int read, protected double CalculateSpeed(long receivedBytes) => CalculateSpeed(receivedBytes, ref _scLastSpeed, ref _scLastReceivedBytes, ref _scLastTick); + protected void ResetSpeedCalculator() + { + Interlocked.Exchange(ref _scLastReceivedBytes, 0); + Interlocked.Exchange(ref _scLastTick, Environment.TickCount64); + _scLastSpeed = 0; + } + protected static double CalculateSpeed(long receivedBytes, ref double lastSpeedToUse, ref long lastReceivedBytesToUse, ref long lastTickToUse) { long currentTick = Environment.TickCount64 - lastTickToUse + 1; diff --git a/CollapseLauncher/Classes/Interfaces/IBackgroundActivity.cs b/CollapseLauncher/Classes/Interfaces/IBackgroundActivity.cs index 3eeda615b..ba378bf23 100644 --- a/CollapseLauncher/Classes/Interfaces/IBackgroundActivity.cs +++ b/CollapseLauncher/Classes/Interfaces/IBackgroundActivity.cs @@ -11,6 +11,8 @@ internal interface IBackgroundActivity event EventHandler StatusChanged; event EventHandler FlushingTrigger; + TotalPerFileProgress Progress { get; } + TotalPerFileStatus Status { get; } bool IsRunning { get; } UIElement ParentUI { get; } void CancelRoutine(); diff --git a/CollapseLauncher/Classes/Plugins/PluginGameInstallWrapper.cs b/CollapseLauncher/Classes/Plugins/PluginGameInstallWrapper.cs index 3bfeb2aae..30e89db4b 100644 --- a/CollapseLauncher/Classes/Plugins/PluginGameInstallWrapper.cs +++ b/CollapseLauncher/Classes/Plugins/PluginGameInstallWrapper.cs @@ -73,6 +73,7 @@ public override string GamePath private InstallProgressState _currentInstallState = InstallProgressState.Idle; private string _lastActivityStatus = string.Empty; private float _lastLoggedPercentage = -1f; + private bool _resetSpeedBaseline = true; private PerFileProgressCallbackNative? _perFileProgressDelegate; private GCHandle _perFileProgressGcHandle; @@ -558,7 +559,18 @@ private void UpdateProgressCallback(in InstallProgress delegateProgress) long downloadedBytes = delegateProgress.DownloadedBytes; long downloadedBytesTotal = delegateProgress.TotalBytesToDownload; - long readDownload = delegateProgress.DownloadedBytes - _updateProgressProperty.LastDownloaded; + long readDownload = 0; + if (_resetSpeedBaseline) + { + _resetSpeedBaseline = false; + _updateProgressProperty.LastDownloaded = downloadedBytes; + } + else if (downloadedBytes >= _updateProgressProperty.LastDownloaded) + { + readDownload = downloadedBytes - _updateProgressProperty.LastDownloaded; + _updateProgressProperty.LastDownloaded = downloadedBytes; + } + double currentSpeed = CalculateSpeed(readDownload); Progress.ProgressAllSizeCurrent = downloadedBytes; @@ -605,8 +617,6 @@ or InstallProgressState.Verify : 0; } - _updateProgressProperty.LastDownloaded = downloadedBytes; - PublishProgressUi(updateProgressBar: true); } } @@ -623,7 +633,26 @@ private void UpdateStatusCallback(InstallProgressState delegateState) { using (_updateStatusLock.EnterScope()) { - _currentInstallState = delegateState; + if (_currentInstallState != delegateState) + { + _currentInstallState = delegateState; + _resetSpeedBaseline = true; + ResetSpeedCalculator(); + Progress.ProgressAllSpeed = 0; + } + + if (delegateState == InstallProgressState.Completed) + { + Progress.ProgressAllTimeLeft = TimeSpan.Zero; + if (Progress.ProgressPerFileSizeTotal > 0) + { + Progress.ProgressPerFilePercentage = Math.Min(100d, + ConverterTool.ToPercentage( + Progress.ProgressPerFileSizeTotal, + Progress.ProgressPerFileSizeCurrent)); + } + } + ApplyActivityStatusFromProperty(); PublishProgressUi(updateProgressBar: true); } diff --git a/CollapseLauncher/Classes/Plugins/PluginPresetConfigWrapper.cs b/CollapseLauncher/Classes/Plugins/PluginPresetConfigWrapper.cs index bd0578c38..6c8a77d52 100644 --- a/CollapseLauncher/Classes/Plugins/PluginPresetConfigWrapper.cs +++ b/CollapseLauncher/Classes/Plugins/PluginPresetConfigWrapper.cs @@ -25,6 +25,7 @@ public partial class PluginPresetConfigWrapper : PresetConfig, IDisposable { public DiscordPresenceExtension.DiscordPresenceContext DiscordPresenceContext { get; } public GameManagerExtension.RunGameFromGameManagerContext RunGameContext { get; } + public GameSettingsExtension.GameSettingsContext GameSettingsContext { get; } public readonly PluginInfo PluginInfo; public readonly IPlugin Plugin; private readonly IPluginPresetConfig _config; @@ -50,6 +51,7 @@ private unsafe PluginPresetConfigWrapper(PluginInfo pluginInfo, IPluginPresetCon }; DiscordPresenceContext = new DiscordPresenceExtension.DiscordPresenceContext(pluginInfo.Handle, config); + GameSettingsContext = new GameSettingsExtension.GameSettingsContext(pluginInfo.Handle, config); } public unsafe GameManagerExtension.RunGameFromGameManagerContext UseToggledGameLaunchContext() diff --git a/CollapseLauncher/XAMLs/MainApp/MainPage.Navigation.cs b/CollapseLauncher/XAMLs/MainApp/MainPage.Navigation.cs index fb8913904..3cfcba9d9 100644 --- a/CollapseLauncher/XAMLs/MainApp/MainPage.Navigation.cs +++ b/CollapseLauncher/XAMLs/MainApp/MainPage.Navigation.cs @@ -203,11 +203,13 @@ void Impl() GameNameType.StarRail => typeof(StarRailGameSettingsPage), GameNameType.Genshin => typeof(GenshinGameSettingsPage), GameNameType.Zenless => typeof(ZenlessGameSettingsPage), + GameNameType.Plugin when presetConfig is PluginPresetConfigWrapper + { GameSettingsContext.HasPage: true } => typeof(PluginGameSettingsPage), _ => null }; NavigationViewItemsContext.GameSettingsPage.Item.Tag = gspPageType; - NavigationViewItemsContext.GameSettingsPage.Item.Visibility = isPluginGame ? Visibility.Collapsed : Visibility.Visible; + NavigationViewItemsContext.GameSettingsPage.Item.Visibility = gspPageType == null ? Visibility.Collapsed : Visibility.Visible; NavigationViewItemsContext.FileCleanupPage.Item.Visibility = isPluginGame ? Visibility.Collapsed : Visibility.Visible; } } @@ -378,10 +380,17 @@ typeOfPageObj is Type currentPageType && { case Type pageType: { - if (pageType.Name.EndsWith("GameSettingsPage") && - !IsGameInstalled()) + if (pageType.Name.EndsWith("GameSettingsPage")) { - pageType = typeof(NotInstalledPage); + if (!IsLoadRegionComplete) + { + return false; + } + + if (!IsGameInstalled()) + { + pageType = typeof(NotInstalledPage); + } } LauncherFrame.Navigate(pageType, null, transitionInfo ?? new DrillInNavigationTransitionInfo()); diff --git a/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.cs b/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.cs new file mode 100644 index 000000000..459db06e2 --- /dev/null +++ b/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.cs @@ -0,0 +1,418 @@ +using CollapseLauncher.DiscordPresence; +using CollapseLauncher.Extension; +using CollapseLauncher.GameManagement.ImageBackground; +using CollapseLauncher.Helper; +using CollapseLauncher.Helper.Animation; +using CollapseLauncher.Plugins; +using Hi3Helper; +using Hi3Helper.Plugin.Core.UI.Settings; +using Hi3Helper.Plugin.Core.Utility; +using Hi3Helper.Shared.ClassStruct; +using Microsoft.UI; +using Microsoft.UI.Text; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Automation; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; +using System; +using System.Globalization; +using System.Numerics; +using static CollapseLauncher.Statics.GamePropertyVault; + +#nullable enable +namespace CollapseLauncher.Pages; + +/// +/// Renders the declarative game settings page exposed by a v0.1.6 plugin. +/// +public sealed partial class PluginGameSettingsPage +{ + private readonly GameSettingsExtension.GameSettingsContext _context; + private readonly TextBlock _statusText = new() + { + Margin = new Thickness(16, 0, 0, 0), + VerticalAlignment = VerticalAlignment.Center, + TextWrapping = TextWrapping.Wrap + }; + + public PluginGameSettingsPage() : base(null!, null!) + { + InitializeComponent(); + + ApplyButton.Translation = new Vector3(0, 0, 32); + GameSettingsApplyGrid.Translation = new Vector3(0, 0, 64); + + SetApplyTextContainer(GameSettingsApplyGrid, gridColumn: 1); + + if (GetCurrentGameProperty().GamePreset is not PluginPresetConfigWrapper preset) + { + throw new InvalidOperationException("The current game preset is not provided by a plugin"); + } + + _context = preset.GameSettingsContext; + } + + private FrameworkElement? CreateContent(out Exception? error) + { + if (!_context.TryGetPage(out GameSettingsPage? page, out error) || page == null) + { + return null; + } + + Grid root = new(); + root.RowDefinitions.Add(new RowDefinition()); + root.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); + + StackPanel sectionsPanel = new() { Margin = new Thickness(32, 40, 32, 32), Spacing = 24 }; + if (!string.IsNullOrWhiteSpace(page.Title)) + { + sectionsPanel.Children.Add(new TextBlock + { + Text = page.Title, + Style = Application.Current.Resources["TitleLargeTextBlockStyle"] as Style, + TextWrapping = TextWrapping.Wrap + }); + } + + Grid sectionGrid = new() { ColumnSpacing = 32, RowSpacing = 28 }; + foreach (GameSettingsSection section in page.Sections) + { + // Informational sections, including plugin warnings, remain full width. + if (section.Entries.Count == 0) + sectionsPanel.Children.Add(CreateSection(section)); + else + sectionGrid.Children.Add(CreateSection(section)); + } + + sectionsPanel.Children.Add(sectionGrid); + sectionsPanel.Children.Add(CreateCustomArgumentsSection()); + sectionGrid.SizeChanged += (_, args) => ArrangeGrid(sectionGrid, args.NewSize.Width >= 900 ? 2 : 1); + ArrangeGrid(sectionGrid, 1); + + ScrollViewer scrollViewer = new() + { + Content = sectionsPanel, + VerticalScrollBarVisibility = ScrollBarVisibility.Auto + }; + root.Children.Add(scrollViewer); + + return root; + } + + private FrameworkElement CreateCustomArgumentsSection() + { + var settings = GetCurrentGameProperty().GameSettings; + StackPanel panel = new() { Spacing = 12 }; + panel.Children.Add(new TextBlock + { + Text = Locale.Current.Lang?._GameSettingsPage?.CustomArgs_Title ?? "Custom arguments", + Style = Application.Current.Resources["SubtitleTextBlockStyle"] as Style, + TextWrapping = TextWrapping.Wrap + }); + ToggleSwitch toggle = new() + { + Header = Locale.Current.Lang?._GameSettingsPage?.CustomArgs_Subtitle ?? "Use custom launch arguments", + IsOn = settings?.SettingsCollapseMisc.UseCustomArguments ?? false, + IsEnabled = settings != null + }; + TextBox arguments = new() + { + Text = settings?.SettingsCustomArgument.CustomArgumentValue ?? string.Empty, + IsEnabled = toggle.IsOn, + HorizontalAlignment = HorizontalAlignment.Stretch + }; + AutomationProperties.SetName(arguments, "Custom launch arguments"); + toggle.Toggled += (_, _) => + { + if (settings == null) return; + settings.SettingsCollapseMisc.UseCustomArguments = toggle.IsOn; + arguments.IsEnabled = toggle.IsOn; + }; + arguments.TextChanged += (_, _) => + { + if (settings != null) + settings.SettingsCustomArgument.CustomArgumentValue = arguments.Text; + }; + panel.Children.Add(toggle); + panel.Children.Add(arguments); + return panel; + } + private static void ArrangeGrid(Grid grid, int columns) + { + int rows = (grid.Children.Count + columns - 1) / columns; + if (grid.ColumnDefinitions.Count == columns && grid.RowDefinitions.Count == rows) + return; + + grid.ColumnDefinitions.Clear(); + grid.RowDefinitions.Clear(); + for (int column = 0; column < columns; column++) + grid.ColumnDefinitions.Add(new ColumnDefinition()); + for (int row = 0; row < rows; row++) + grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); + for (int index = 0; index < grid.Children.Count; index++) + { + FrameworkElement child = (FrameworkElement)grid.Children[index]; + Grid.SetColumn(child, index % columns); + Grid.SetRow(child, index / columns); + } + } + + private FrameworkElement CreateSection(GameSettingsSection section) + { + StackPanel panel = new() { Spacing = 12, VerticalAlignment = VerticalAlignment.Top }; + panel.Children.Add(new TextBlock + { + Text = section.Title, + Style = Application.Current.Resources["SubtitleTextBlockStyle"] as Style, + TextWrapping = TextWrapping.Wrap + }); + + if (!string.IsNullOrWhiteSpace(section.Description)) + { + panel.Children.Add(new TextBlock + { + Text = section.Description, + Opacity = 0.72, + TextWrapping = TextWrapping.Wrap + }); + } + + Grid entries = new() { ColumnSpacing = 16, RowSpacing = 16 }; + foreach (GameSettingEntry entry in section.Entries) + entries.Children.Add(CreateEntry(entry)); + + if (entries.Children.Count > 0) + { + entries.SizeChanged += (_, args) => + ArrangeGrid(entries, args.NewSize.Width >= 520 ? 3 : args.NewSize.Width >= 340 ? 2 : 1); + ArrangeGrid(entries, 1); + panel.Children.Add(entries); + } + return panel; + } + + private FrameworkElement CreateEntry(GameSettingEntry entry) + { + FrameworkElement editor = CreateEditor(entry); + editor.HorizontalAlignment = HorizontalAlignment.Stretch; + editor.VerticalAlignment = VerticalAlignment.Top; + AutomationProperties.SetName(editor, entry.Title); + + StackPanel panel = new() { Spacing = 6 }; + if (entry.Kind != GameSettingKind.Toggle) + { + panel.Children.Add(new TextBlock + { + Text = entry.Title, + FontWeight = FontWeights.SemiBold, + TextWrapping = TextWrapping.Wrap + }); + } + panel.Children.Add(editor); + if (!string.IsNullOrWhiteSpace(entry.Description)) + { + panel.Children.Add(new TextBlock + { + Text = entry.Description, + FontSize = 12, + Opacity = 0.72, + TextWrapping = TextWrapping.Wrap + }); + AutomationProperties.SetHelpText(editor, entry.Description); + } + return panel; + } + + private FrameworkElement CreateEditor(GameSettingEntry entry) => entry.Kind switch + { + GameSettingKind.Toggle => CreateToggle(entry), + GameSettingKind.Text => CreateText(entry), + GameSettingKind.Number => CreateNumber(entry), + GameSettingKind.Slider => CreateSlider(entry), + GameSettingKind.Choice => CreateChoice(entry), + _ => throw new ArgumentOutOfRangeException(nameof(entry.Kind)) + }; + + private CheckBox CreateToggle(GameSettingEntry entry) + { + CheckBox control = new() + { + Content = new TextBlock { Text = entry.Title, TextWrapping = TextWrapping.Wrap }, + IsChecked = bool.TryParse(entry.Value, out bool value) && value + }; + control.Checked += (_, _) => SetValue(entry.Key, bool.TrueString); + control.Unchecked += (_, _) => SetValue(entry.Key, bool.FalseString); + return control; + } + + private TextBox CreateText(GameSettingEntry entry) + { + TextBox control = new() { Text = entry.Value, PlaceholderText = entry.Placeholder }; + control.TextChanged += (_, _) => SetValue(entry.Key, control.Text); + return control; + } + + private NumberBox CreateNumber(GameSettingEntry entry) + { + NumberBox control = new() + { + Minimum = entry.Minimum, + Maximum = entry.Maximum, + SmallChange = entry.Step, + SpinButtonPlacementMode = NumberBoxSpinButtonPlacementMode.Compact, + Value = ParseNumber(entry.Value, entry.Minimum) + }; + control.ValueChanged += (_, args) => + { + if (!double.IsNaN(args.NewValue)) + { + SetValue(entry.Key, args.NewValue.ToString(CultureInfo.InvariantCulture)); + } + }; + return control; + } + + private Slider CreateSlider(GameSettingEntry entry) + { + Slider control = new() + { + Minimum = entry.Minimum, + Maximum = entry.Maximum, + StepFrequency = entry.Step, + Style = Application.Current.Resources["FatSliderStyle"] as Style, + TickFrequency = Math.Max(entry.Step, (entry.Maximum - entry.Minimum) / 10), + TickPlacement = Microsoft.UI.Xaml.Controls.Primitives.TickPlacement.Outside, + Value = ParseNumber(entry.Value, entry.Minimum) + }; + control.ValueChanged += (_, args) => + SetValue(entry.Key, args.NewValue.ToString(CultureInfo.InvariantCulture)); + return control; + } + + private ComboBox CreateChoice(GameSettingEntry entry) + { + ComboBox control = new() { CornerRadius = new CornerRadius(14) }; + foreach (GameSettingChoice choice in entry.Choices ?? []) + { + ComboBoxItem item = new() { Content = choice.Title, Tag = choice.Value }; + control.Items.Add(item); + if (string.Equals(choice.Value, entry.Value, StringComparison.Ordinal)) + { + control.SelectedItem = item; + } + } + + control.SelectionChanged += (_, _) => + { + if (control.SelectedItem is ComboBoxItem { Tag: string value }) + { + SetValue(entry.Key, value); + } + }; + return control; + } + + private static double ParseNumber(string value, double fallback) => + double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out double result) ? result : fallback; + + private void SetValue(string key, string value) + { + try + { + _context.SetValue(key, value); + SetStatus(null); + } + catch (Exception ex) + { + SetStatus(ex.Message, true); + } + } + + private void SetStatus(string? text, bool isError = false) + { + _statusText.Text = text ?? string.Empty; + _statusText.Foreground = isError ? new SolidColorBrush(Colors.IndianRed) : null; + } + + #region Overriden Methods from Game Settings Base Page + protected override async void OnLoaded(object? sender, RoutedEventArgs args) + { + try + { + GameInstallStateEnum gameStatus = await CurrentGameProperty.GameVersion!.GetGameState(); + + ImageBackgroundManager.Shared.IsBackgroundElevated = true; + ImageBackgroundManager.Shared.ForegroundOpacity = 0d; + ImageBackgroundManager.Shared.SmokeOpacity = 1d; + + if (CurrentGameProperty.IsGameRunning) + { +#if !GSPBYPASSGAMERUNNING + Overlay.Visibility = Visibility.Visible; + PageContent.Visibility = Visibility.Collapsed; + OverlayTitle.Text = Locale.Current.Lang?._GameSettingsPage?.OverlayGameRunningTitle; + OverlaySubtitle.Text = Locale.Current.Lang?._GameSettingsPage?.OverlayGameRunningSubtitle; +#endif + } + else if (gameStatus + is GameInstallStateEnum.NotInstalled + or GameInstallStateEnum.NeedsUpdate + or GameInstallStateEnum.InstalledHavePlugin + or GameInstallStateEnum.GameBroken) + { + Overlay.Visibility = Visibility.Visible; + PageContent.Visibility = Visibility.Collapsed; + OverlayTitle.Text = Locale.Current.Lang?._GameSettingsPage?.OverlayNotInstalledTitle; + OverlaySubtitle.Text = Locale.Current.Lang?._GameSettingsPage?.OverlayNotInstalledSubtitle; + } + else + { +#if !DISABLEDISCORD + InnerLauncherConfig.AppDiscordPresence.SetActivity(DiscordActivityType.GameSettings); +#endif + + FrameworkElement? elementContent = CreateContent(out Exception? createPageError); + if (elementContent != null) + { + ElementContent.AddElementToGridRowColumn(elementContent); + SettingsScrollViewer.EnableImplicitAnimation(true); + return; + } + + Overlay.Visibility = Visibility.Visible; + PageContent.Visibility = Visibility.Collapsed; + OverlayTitle.Text = "Game Settings feature is not available."; + OverlaySubtitle.Text = createPageError?.Message ?? "This plugin did not provide a game settings page."; + + if (createPageError != null) + { + Logger.LogWriteLine($"An error has occurred while trying to create plugin-based game settings page.\r\n{createPageError}", LogType.Error, true); + ErrorSender.SendException(createPageError); + } + } + } + catch (Exception ex) + { + Logger.LogWriteLine($"FATAL ERROR!!!\r\n{ex}", LogType.Error, true); + ErrorSender.SendException(ex); + } + } + + protected override void OnUnloaded(object? sender, RoutedEventArgs args) + { + } + + protected override void OnApplyButtonClick(object sender, RoutedEventArgs args) + { + try + { + _context.Apply(); + SetApplyTextStatus("Lang._StarRailGameSettingsPage.SettingsApplied"); + } + catch (Exception ex) + { + SetApplyTextStatus(ex.Message, true); + } + } + #endregion +} diff --git a/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.xaml b/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.xaml new file mode 100644 index 000000000..bb529ba3e --- /dev/null +++ b/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.xaml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameInitializer.cs b/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameInitializer.cs index ccbe668d6..2cc3d8172 100644 --- a/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameInitializer.cs +++ b/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameInitializer.cs @@ -18,6 +18,8 @@ namespace CollapseLauncher.XAMLs.Theme.CustomControls; public partial class LayeredBackgroundImage { + private MediaPlayer? _videoFramePlayer; + #region Initializers private void InitializeVideoPlayer() @@ -54,38 +56,51 @@ private void InitializeVideoPlayer() } } - private void InitializeRenderTargetSize(MediaPlaybackSession playbackSession) + private bool InitializeRenderTargetSize(MediaPlaybackSession playbackSession) { try { - double currentCanvasWidth = playbackSession.NaturalVideoWidth; - double currentCanvasHeight = playbackSession.NaturalVideoHeight; - - _canvasWidth = (int)currentCanvasWidth; - _canvasHeight = (int)currentCanvasHeight; - _canvasRenderSize = new Rect(0, 0, _canvasWidth, _canvasHeight); + int width = (int)playbackSession.NaturalVideoWidth; + int height = (int)playbackSession.NaturalVideoHeight; // In some occasion, MediaPlayer reportedly 0x0px size which causes E_INVALIDARG while rendering frame // if FFmpeg source is used. So, use size reported by FFmpeg instead. - if (_canvasRenderSize != default || _videoFfmpegMediaSource == null) return; + if ((width <= 0 || height <= 0) && _videoFfmpegMediaSource?.CurrentVideoStream is { } videoStream) + { + width = videoStream.PixelWidth; + height = videoStream.PixelHeight; + } + + if (width <= 0 || height <= 0) return false; - _canvasWidth = _videoFfmpegMediaSource.CurrentVideoStream.PixelWidth; - _canvasHeight = _videoFfmpegMediaSource.CurrentVideoStream.PixelHeight; + _canvasWidth = width; + _canvasHeight = height; _canvasRenderSize = new Rect(0, 0, _canvasWidth, _canvasHeight); + return true; + } + catch (COMException ex) when ((uint)ex.HResult == 0xC00D3E85u) + { + // A queued MediaOpened callback can outlive the playback session. + return false; } catch (Exception ex) { - Logger.LogWriteLine($"[LayeredBackgroundImage::DisposeVideoPlayer] {ex}", + Logger.LogWriteLine($"[LayeredBackgroundImage::InitializeRenderTargetSize] {ex}", LogType.Error, true); + return false; } } - private unsafe void InitializeRenderTarget() + private unsafe bool InitializeRenderTarget() { + bool initialized = false; + Interlocked.Exchange(ref _isBlockVideoFrameDraw, 1); try { - Interlocked.Exchange(ref _isBlockVideoFrameDraw, 1); // Block frame drawing routine + // Play() can arrive before MediaOpened. Read this player's size before creating any surfaces. + if (_videoPlayer == null || !InitializeRenderTargetSize(_videoPlayer.PlaybackSession)) return false; + DisposeRenderTarget(_canvasImageSource == null); // Always ensure the previous render target has been disposed _canvasDevice ??= CanvasDevice.GetSharedDevice(); @@ -102,33 +117,36 @@ private unsafe void InitializeRenderTarget() Interlocked.Exchange(ref _useSafeFrameRenderer, false); if (_useSafeFrameRenderer) { - return; + return initialized = true; } - _canvasRenderTargetNativePtr = ((IWinRTObject)_canvasRenderTarget).NativeObject.ThisPtr; _canvasImageSourceNativePtr = ((IWinRTObject)_canvasImageSource).NativeObject.ThisPtr; - - ((IWinRTObject)_canvasRenderTarget).NativeObject.TryAs(typeof(IDirect3DSurface).GUID, out _canvasRenderTargetAsSurfacePtr); try { + // DrawImageToRect requires ICanvasBitmap, not the render target's default ICanvasRenderTarget interface. + Guid canvasBitmapIid = new("C57532ED-709E-4AC2-86BE-A1EC3A7FA8FE"); + Marshal.ThrowExceptionForHR(((IWinRTObject)_canvasRenderTarget).NativeObject.TryAs(canvasBitmapIid, out _canvasRenderTargetNativePtr)); + Marshal.ThrowExceptionForHR(((IWinRTObject)_canvasRenderTarget).NativeObject.TryAs(typeof(IDirect3DSurface).GUID, out _canvasRenderTargetAsSurfacePtr)); + if (_functionTableBeginDraw == null! || _functionTableDrawImage == null! || _functionTableCopyFrameToVideoSurface == null! || _functionTableDispose == null!) { - SwapChainPanelHelper.GetDirectNativeDelegateForDrawRoutine(_canvasImageSourceNativePtr, - _canvasRenderTargetNativePtr, - _videoPlayerPtr, - out _functionTableBeginDraw, - out _functionTableDrawImage, - out _functionTableCopyFrameToVideoSurface, - out _functionTableDispose, - in _canvasRenderSize); + SwapChainPanelHelper.GetFuncForCanvasImageSource(_canvasImageSourceNativePtr, + _canvasRenderTargetNativePtr, + _videoPlayerPtr, + out _functionTableBeginDraw, + out _functionTableDrawImage, + out _functionTableCopyFrameToVideoSurface, + out _functionTableDispose, + in _canvasRenderSize); } } catch (Exception e) { + NullifyRenderTargetNativePointers(); Interlocked.Exchange(ref _useSafeFrameRenderer, true); // Fallback _functionTableBeginDraw = null; @@ -139,20 +157,22 @@ private unsafe void InitializeRenderTarget() LogType.Error, true); } + return initialized = true; } catch (Exception e) { Logger.LogWriteLine($"[LayeredBackgroundImage::InitializeRenderTarget] FATAL: {e}", LogType.Error, true); + return false; } finally { - if (_canvasImageSource != null) + if (initialized && _canvasImageSource != null) { SetRenderImageSource(_canvasImageSource.Source); } - Interlocked.Exchange(ref _isBlockVideoFrameDraw, 0); // Unblock frame drawing routine + Interlocked.Exchange(ref _isBlockVideoFrameDraw, initialized ? 0 : 1); } } @@ -162,11 +182,12 @@ private unsafe void InitializeRenderTarget() private void DetachVideoPlayerEvents(MediaPlayer player) { - player.MediaOpened -= InitializeVideoFrameOnMediaOpened; - player.VideoFrameAvailable -= NotifyVideoLoaded; - player.VideoFrameAvailable -= VideoPlayer_VideoFrameAvailableUnsafe; - player.VideoFrameAvailable -= VideoPlayer_VideoFrameAvailableSafe; - player.PlaybackSession.PositionChanged -= MediaDurationPosition_OnChangedBridge; + player.MediaOpened -= InitializeVideoFrameOnMediaOpened; + player.PlaybackSession.NaturalVideoSizeChanged -= InitializeVideoFrameOnSizeChanged; + player.VideoFrameAvailable -= NotifyVideoLoaded; + player.VideoFrameAvailable -= VideoPlayerUnsafe_OnVideoFrameAvailable; + player.VideoFrameAvailable -= VideoPlayerSafe_OnVideoFrameAvailable; + player.PlaybackSession.PositionChanged -= MediaDurationPosition_OnChangedBridge; } private void DisposeVideoPlayer(bool disposeRenderImageSource = true) @@ -206,6 +227,7 @@ private void DisposeVideoPlayer(bool disposeRenderImageSource = true) } finally { + _videoFramePlayer = null; Interlocked.Exchange(ref _isVideoInitialized, 0); } } @@ -244,11 +266,16 @@ private void CanvasDevice_OnDeviceLost() LogType.Warning, true); - // -- Nullify _canvasImageSource so CanvasDevice and other dependencies are reinitialized too + // Note from: @neon-nyan. + // -- Use atomic exchange instead of locking :) + if (Interlocked.Exchange(ref _isBlockVideoFrameDraw, 1) == 1) + return; + + NullifyRenderTargetNativePointers(); + // Drop the image source only after invalidating its borrowed native pointer. Interlocked.Exchange(ref _canvasImageSource, null!); InitializeRenderTarget(); - // Try to unlock video draw progress (if a throw happened inside frame drawing routine) Interlocked.Exchange(ref _isVideoFrameDrawInProgress, 0); } @@ -270,13 +297,15 @@ private void NullifyRenderTargetNativePointers() { // -- Note to myself @neon-nyan: // Release IDirect3DSurface reference first, then dispose the whole CanvasRenderTarget. - // This is necessary as we just cast/QueryInterface the _canvasRenderTargetNativePtr (which - // is obtained from IWinRTObject's direct pointer) into IDirect3DSurface. If not released, + // This is necessary as we queried the render target's native object for + // IDirect3DSurface. If not released, // the reference on the IWinRTObject will not be zeroed, causing leak. if (_canvasRenderTargetAsSurfacePtr != nint.Zero) Marshal.Release(Interlocked.Exchange(ref _canvasRenderTargetAsSurfacePtr, nint.Zero)); - // -- Nullify IWinRTObject direct pointers. - Interlocked.Exchange(ref _canvasRenderTargetNativePtr, nint.Zero); + // -- Release the ICanvasBitmap reference acquired for DrawImageToRect. + if (_canvasRenderTargetNativePtr != nint.Zero) Marshal.Release(Interlocked.Exchange(ref _canvasRenderTargetNativePtr, nint.Zero)); + + // -- Nullify the borrowed IWinRTObject pointer. Interlocked.Exchange(ref _canvasImageSourceNativePtr, nint.Zero); } diff --git a/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameRenderer.cs b/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameRenderer.cs index 6159befcd..fa38a953e 100644 --- a/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameRenderer.cs +++ b/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameRenderer.cs @@ -1,4 +1,4 @@ -using CollapseLauncher.Extension; +using CollapseLauncher.Extension; using FFmpegInteropX; using Hi3Helper; using Hi3Helper.Data; @@ -88,27 +88,22 @@ private static ref readonly Guid IMediaPlayer5_IID #region Video Frame Drawing - private unsafe void VideoPlayer_VideoFrameAvailableUnsafe(MediaPlayer sender, object args) + private unsafe void VideoPlayerUnsafe_OnVideoFrameAvailable(MediaPlayer sender, object args) { - nint drawingSessionPpv = nint.Zero; - try - { - if (Interlocked.Decrement(ref _videoToSkipFrames) > 0) - { - return; - } + if (_isBlockVideoFrameDraw == 1) + return; - if (_isBlockVideoFrameDraw == 1 || - _canvasImageSourceNativePtr == nint.Zero || - _canvasRenderTargetNativePtr == nint.Zero || - Interlocked.Exchange(ref _isVideoFrameDrawInProgress, 1) == 1) - { + if (Interlocked.Exchange(ref _isVideoFrameDrawInProgress, 1) > 0) + { #if DEBUG - Logger.LogWriteLine($@"Skipping frame at: {sender.Position:hh\:mm\:ss\.ffffff}"); + Logger.LogWriteLine($@"Skipping frame at: {sender.Position:hh\:mm\:ss\.ffffff}"); #endif - return; - } + return; + } + nint drawingSessionPpv = nint.Zero; + try + { _functionTableCopyFrameToVideoSurface(_videoPlayerPtr, _canvasRenderTargetAsSurfacePtr); drawingSessionPpv = SwapChainPanelHelper .CanvasSessionDrawUnsafe(_canvasImageSourceNativePtr, @@ -117,133 +112,125 @@ private unsafe void VideoPlayer_VideoFrameAvailableUnsafe(MediaPlayer sender, ob _functionTableDrawImage, in _canvasRenderSize); } - // Device lost error. If happened, reinitialize render target - catch (COMException comEx) when ((uint)comEx.HResult is 0x887A0005u or 0x802B0020u or 0x8899000Cu) + // Ignore DCOMPOSITION_ERROR_SURFACE_BEING_RENDERED + catch (COMException comEx) when (unchecked((uint)comEx.HResult) == 0x88980801u) + { + // ignored + } + // Trying to recreate the context instead of re-creating the entire canvas + catch (COMException comEx) when (unchecked((uint)comEx.HResult) == 0x802B0020u) { - DispatcherQueue.TryEnqueue(CanvasDevice_OnDeviceLost); + Logger.LogWriteLine($"[LayeredBackgroundImage::UnsafeOnVideoFrameAvailable] CanvasImageSource has lost its context. Re-creating the context...\r\n{comEx}", + LogType.Warning, + true); + + /* TODO: Re-create the context and start redrawing. + * Use it for CanvasImageSource later. Needs some fixes with how I can re-create the RenderTarget and the ImageSource without + * dealing with the lock and stuffs. + using CanvasLock? canvasLock = _canvasDevice?.Lock(); + _canvasImageSource?.Recreate(_canvasDevice); + Logger.LogWriteLine("[LayeredBackgroundImage::UnsafeOnVideoFrameAvailable] Trying to re-draw after re-creating the context...", + LogType.Warning, + true); + goto StartDraw; + */ + CanvasDevice_OnDeviceLost(); } - catch (COMException comEx) when ((uint)comEx.HResult is 0x88980801u) + // Device lost error. If happened, reinitialize render target + catch (COMException comEx) when (unchecked((uint)comEx.HResult) is 0x887A0005u or 0x8899000Cu) { - // Try to unlock if any error caused by DCOMPOSITION_ERROR_SURFACE_NOT_BEING_RENDERED - Interlocked.Exchange(ref _isVideoFrameDrawInProgress, 0); + Logger.LogWriteLine($"[LayeredBackgroundImage::UnsafeOnVideoFrameAvailable] Direct3D device is lost! Trying to reinitialize it...\r\n{comEx}", + LogType.Warning, + true); + CanvasDevice_OnDeviceLost(); } catch (Exception ex) { - Logger.LogWriteLine($"[LayeredBackgroundImage::VideoPlayer_VideoFrameAvailableUnsafe|OtherThread] {ex}", + Logger.LogWriteLine($"[LayeredBackgroundImage::UnsafeOnVideoFrameAvailable]" + + $"\r\n_videoPlayerPtr: 0x{_videoPlayerPtr:x8}" + + $"\r\n_canvasRenderTargetAsSurfacePtr: 0x{_canvasRenderTargetAsSurfacePtr:x8}" + + $"\r\n_canvasImageSourceNativePtr: 0x{_canvasImageSourceNativePtr:x8}" + + $"\r\n_canvasRenderTargetNativePtr: 0x{_canvasRenderTargetNativePtr:x8}" + + $"\r\n{ex}", LogType.Error, true); } finally { - if (drawingSessionPpv != nint.Zero) + DispatcherQueue.TryEnqueue(DispatcherQueuePriority.High, () => { - DispatcherQueue?.TryEnqueue(DispatcherQueuePriority.High, DrawFrame); - } - void DrawFrame() - { - try - { + if (drawingSessionPpv != nint.Zero) SwapChainPanelHelper.DrawingDisposeUnsafe(drawingSessionPpv, _functionTableDispose); - } - // Device lost error. If happened, reinitialize render target - catch (COMException comEx) when ((uint)comEx.HResult is 0x887A0005u or 0x802B0020u or 0x8899000Cu) - { - DispatcherQueue.TryEnqueue(CanvasDevice_OnDeviceLost); - } - catch (COMException comEx) when ((uint)comEx.HResult is 0x88980801u) - { - // Try to unlock if any error caused by DCOMPOSITION_ERROR_SURFACE_NOT_BEING_RENDERED - Interlocked.Exchange(ref _isVideoFrameDrawInProgress, 0); - } - catch (Exception ex) - { - Logger.LogWriteLine($"[LayeredBackgroundImage::VideoPlayer_VideoFrameAvailableUnsafe|UIThread] {ex}", - LogType.Error, - true); - } - finally - { - Interlocked.Exchange(ref _isVideoFrameDrawInProgress, 0); - } - } + + Interlocked.Exchange(ref _isVideoFrameDrawInProgress, 0); + }); } } - private void VideoPlayer_VideoFrameAvailableSafe(MediaPlayer sender, object args) + private void VideoPlayerSafe_OnVideoFrameAvailable(MediaPlayer sender, object args) { - Unsafe.SkipInit(out CanvasDrawingSession? ds); + if (_isBlockVideoFrameDraw == 1) + return; - try + if (Interlocked.Exchange(ref _isVideoFrameDrawInProgress, 1) == 1) { - if (Interlocked.Decrement(ref _videoToSkipFrames) > 0) - { - return; - } - - if (_isBlockVideoFrameDraw == 1 || - _canvasImageSource == null! || - _canvasRenderTarget == null! || - Interlocked.Exchange(ref _isVideoFrameDrawInProgress, 1) == 1) - { #if DEBUG - Logger.LogWriteLine($@"Skipping frame at: {sender.Position:hh\:mm\:ss\.ffffff}"); + Logger.LogWriteLine($@"Skipping frame at: {sender.Position:hh\:mm\:ss\.ffffff}"); #endif - return; - } + return; + } + Unsafe.SkipInit(out CanvasDrawingSession? drawingSession); + try + { _videoPlayer?.CopyFrameToVideoSurface(_canvasRenderTarget); - ds = _canvasImageSource?.CreateDrawingSession(default, _canvasRenderSize); - ds?.DrawImage(_canvasRenderTarget, _canvasRenderSize); + drawingSession = _canvasImageSource?.CreateDrawingSession(default, _canvasRenderSize); } - // Device lost error. If happened, reinitialize render target - catch (COMException comEx) when ((uint)comEx.HResult is 0x887A0005u or 0x802B0020u or 0x8899000Cu) + // Ignore DCOMPOSITION_ERROR_SURFACE_BEING_RENDERED + catch (COMException comEx) when (unchecked((uint)comEx.HResult) == 0x88980801u) { - DispatcherQueue.TryEnqueue(CanvasDevice_OnDeviceLost); + // ignored } - catch (COMException comEx) when ((uint)comEx.HResult is 0x88980801u) + // Trying to recreate the context instead of re-creating the entire canvas + catch (COMException comEx) when (unchecked((uint)comEx.HResult) == 0x802B0020u) { - // Try to unlock if any error caused by DCOMPOSITION_ERROR_SURFACE_NOT_BEING_RENDERED - Interlocked.Exchange(ref _isVideoFrameDrawInProgress, 0); + Logger.LogWriteLine($"[LayeredBackgroundImage::SafeOnVideoFrameAvailable] CanvasImageSource has lost its context. Re-creating the context...\r\n{comEx}", + LogType.Warning, + true); + + /* TODO: Re-create the context and start redrawing. + * Use it for CanvasImageSource later. Needs some fixes with how I can re-create the RenderTarget and the ImageSource without + * dealing with the lock and stuffs. + using CanvasLock? canvasLock = _canvasDevice?.Lock(); + _canvasImageSource?.Recreate(_canvasDevice); + Logger.LogWriteLine("[LayeredBackgroundImage::SafeOnVideoFrameAvailable] Trying to re-draw after re-creating the context...", + LogType.Warning, + true); + goto StartDraw; + */ + CanvasDevice_OnDeviceLost(); + } + // Device lost error. If happened, reinitialize render target + catch (COMException comEx) when (unchecked((uint)comEx.HResult) is 0x887A0005u or 0x8899000Cu) + { + Logger.LogWriteLine($"[LayeredBackgroundImage::SafeOnVideoFrameAvailable] Direct3D device is lost! Trying to reinitialize it...\r\n{comEx}", + LogType.Warning, + true); + CanvasDevice_OnDeviceLost(); } catch (Exception ex) { - Logger.LogWriteLine($"[LayeredBackgroundImage::VideoPlayer_VideoFrameAvailableSafe|OtherThread] {ex}", + Logger.LogWriteLine($"[LayeredBackgroundImage::SafeOnVideoFrameAvailable] {ex}", LogType.Error, true); } finally { - DispatcherQueue?.TryEnqueue(DispatcherQueuePriority.High, DrawFrame); - } - - return; - - void DrawFrame() - { - try - { - ds?.Dispose(); - } - // Device lost error. If happened, reinitialize render target - catch (COMException comEx) when ((uint)comEx.HResult is 0x887A0005u or 0x802B0020u or 0x8899000Cu) - { - CanvasDevice_OnDeviceLost(); - } - catch (COMException comEx) when ((uint)comEx.HResult is 0x88980801u) - { - // Try to unlock if any error caused by DCOMPOSITION_ERROR_SURFACE_NOT_BEING_RENDERED - Interlocked.Exchange(ref _isVideoFrameDrawInProgress, 0); - } - catch (Exception ex) - { - Logger.LogWriteLine($"[LayeredBackgroundImage::VideoPlayer_VideoFrameAvailableSafe|UIThread] {ex}", - LogType.Error, - true); - } - finally + DispatcherQueue.TryEnqueue(DispatcherQueuePriority.High, () => { + drawingSession?.Dispose(); Interlocked.Exchange(ref _isVideoFrameDrawInProgress, 0); - } + }); } } @@ -367,7 +354,11 @@ public void InitializeAndPlayVideoView(Action? actionOnPlay // Only initialize once. if (Interlocked.Exchange(ref _isVideoInitialized, 1) == 0) { - InitializeRenderTarget(); + if (!InitializeRenderTarget()) + { + Interlocked.Exchange(ref _isVideoInitialized, 0); + return; + } // Seek to last position if source was the same if (_videoPlayer.CanSeek && @@ -419,8 +410,8 @@ public void InitializeAndPlayVideoView(Action? actionOnPlay void ActionVideoAfterPlay() { _videoPlayer.VideoFrameAvailable += !_useSafeFrameRenderer - ? VideoPlayer_VideoFrameAvailableUnsafe - : VideoPlayer_VideoFrameAvailableSafe; + ? VideoPlayerUnsafe_OnVideoFrameAvailable + : VideoPlayerSafe_OnVideoFrameAvailable; } } else if (BackgroundSource != null) diff --git a/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.Loaders.cs b/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.Loaders.cs index 9d67ba424..bd16915d4 100644 --- a/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.Loaders.cs +++ b/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.Loaders.cs @@ -19,11 +19,10 @@ using Windows.Media.Playback; using Windows.Storage.Streams; // ReSharper disable IdentifierTypo -#pragma warning disable IDE0051 -#pragma warning disable CS8321 // Local function is declared but never used - // ReSharper disable StringLiteralTypo // ReSharper disable CommentTypo +#pragma warning disable IDE0051 +#pragma warning disable CS8321 // Local function is declared but never used #pragma warning disable CsWinRT1032 #nullable enable @@ -151,6 +150,8 @@ private static void BackgroundSource_OnChange(DependencyObject d, DependencyProp private static void BackgroundSource_UseStatic(LayeredBackgroundImage element) { Grid grid = element._backgroundGrid; + // Static artwork already includes the foreground used by the video. + element._foregroundGrid.Visibility = Visibility.Collapsed; element.LoadFromSourceAsyncDetached(BackgroundStaticSourceProperty, nameof(BackgroundStretch), @@ -166,6 +167,7 @@ private static void BackgroundSource_UseStatic(LayeredBackgroundImage element) private static void BackgroundSource_UseNormal(LayeredBackgroundImage element) { Grid grid = element._backgroundGrid; + element._foregroundGrid.Visibility = Visibility.Visible; element.LoadFromSourceAsyncDetached(BackgroundSourceProperty, nameof(BackgroundStretch), nameof(BackgroundHorizontalAlignment), @@ -435,8 +437,15 @@ private static async Task LoadVideoFromSourceAsync( return false; } - await ffmpegMediaSource.OpenWithMediaPlayerAsync(player); + if (player == null || !ReferenceEquals(player, instance._videoPlayer) || player.IsObjectDisposed()) + { + ffmpegMediaSource.Dispose(); + isError = true; + return false; + } + Interlocked.Exchange(ref instance._videoFfmpegMediaSource, ffmpegMediaSource); + await ffmpegMediaSource.OpenWithMediaPlayerAsync(player); // HACK: // Sometimes the media source isn't ready when the window just get restored from minimized state, @@ -449,8 +458,8 @@ private static async Task LoadVideoFromSourceAsync( // Unsubscribe frame renderer event to avoid double call, and then mark deinitialization. Interlocked.Exchange(ref instance._isVideoInitialized, 0); player?.VideoFrameAvailable -= !instance._useSafeFrameRenderer - ? instance.VideoPlayer_VideoFrameAvailableUnsafe - : instance.VideoPlayer_VideoFrameAvailableSafe; + ? instance.VideoPlayerUnsafe_OnVideoFrameAvailable + : instance.VideoPlayerSafe_OnVideoFrameAvailable; ffmpegMediaSource.Dispose(); sourceStreamRandom?.Dispose(); @@ -533,11 +542,19 @@ private void InitializeVideoFrameOnMediaOpened(MediaPlayer sender, object args) void Impl() { - if (!IsLoaded) + if (!IsLoaded || !ReferenceEquals(sender, _videoPlayer) || sender.IsObjectDisposed()) { return; } + if (ReferenceEquals(_videoFramePlayer, sender)) return; + + MediaPlaybackSession session = sender.PlaybackSession; + session.NaturalVideoSizeChanged -= InitializeVideoFrameOnSizeChanged; + session.NaturalVideoSizeChanged += InitializeVideoFrameOnSizeChanged; + if (!InitializeRenderTargetSize(session)) return; + session.NaturalVideoSizeChanged -= InitializeVideoFrameOnSizeChanged; + // Update Media Duration and Update Binding to it. SetValue(MediaDurationProperty, sender.NaturalDuration); SetValue(IsCurrentMediaSeekableProperty, sender.CanSeek); @@ -563,18 +580,29 @@ void Impl() VerticalAlignmentProperty, BindingMode.OneWay); - InitializeRenderTargetSize(sender.PlaybackSession); - // Register events image.Loaded += Image_VideoFrameOnLoaded; image.Unloaded += Image_VideoFrameOnUnloaded; // Add to children image.Transitions.Add(new ContentThemeTransition()); + _videoFramePlayer = sender; _backgroundGrid.Children.Add(image); } } + private void InitializeVideoFrameOnSizeChanged(MediaPlaybackSession sender, object args) + { + DispatcherQueue.TryEnqueue(() => + { + if (_videoPlayer is { } player && !player.IsObjectDisposed() && + ReferenceEquals(sender, player.PlaybackSession)) + { + InitializeVideoFrameOnMediaOpened(player, args); + } + }); + } + private static void Image_VideoFrameOnLoaded(object sender, RoutedEventArgs e) { if (sender is not Image { Tag: ValueTuple parentGrid }) @@ -582,7 +610,8 @@ private static void Image_VideoFrameOnLoaded(object sender, RoutedEventArgs e) return; } - if (!parentGrid.Item2.IsVideoAutoplay) return; + if (!parentGrid.Item2.IsVideoAutoplay && + parentGrid.Item2._videoState is not (VideoState.RequestedPlay or VideoState.Playing)) return; Interlocked.Exchange(ref parentGrid.Item2._videoState, VideoState.Playing); parentGrid.Item2.InitializeAndPlayVideoView(); diff --git a/ColorThief b/ColorThief index 5a04db74a..b568f2973 160000 --- a/ColorThief +++ b/ColorThief @@ -1 +1 @@ -Subproject commit 5a04db74a50573469e48d8ffae4ece35f8707308 +Subproject commit b568f2973aa050c4a5665362da665c71945da2bb diff --git a/H.NotifyIcon b/H.NotifyIcon index 7422382b4..8cadbd2a7 160000 --- a/H.NotifyIcon +++ b/H.NotifyIcon @@ -1 +1 @@ -Subproject commit 7422382b4da5a738f84b400b31d23df1c1c0c319 +Subproject commit 8cadbd2a7a5d06473083ec59174de0ca04427c15 diff --git a/Hi3Helper.Http b/Hi3Helper.Http index 887674ee6..2d4c209d4 160000 --- a/Hi3Helper.Http +++ b/Hi3Helper.Http @@ -1 +1 @@ -Subproject commit 887674ee6365c4907d4b45061b26857726d9df74 +Subproject commit 2d4c209d4d462ee94a5e6a13e3c45abcfbca848e diff --git a/Hi3Helper.Plugin.Core b/Hi3Helper.Plugin.Core index 55d05702c..14293ba31 160000 --- a/Hi3Helper.Plugin.Core +++ b/Hi3Helper.Plugin.Core @@ -1 +1 @@ -Subproject commit 55d05702ccc96eedb041c8d66f0bb3940b1ce08f +Subproject commit 14293ba31be02b513d3088d6866727504fa78da0 diff --git a/Hi3Helper.SharpDiscordRPC b/Hi3Helper.SharpDiscordRPC index a2d1cb7c6..e80e57c3e 160000 --- a/Hi3Helper.SharpDiscordRPC +++ b/Hi3Helper.SharpDiscordRPC @@ -1 +1 @@ -Subproject commit a2d1cb7c6d4d39660bc557bec7296aafb6d48ad3 +Subproject commit e80e57c3e758bc7a28f9c69fb266019e977d1a1a diff --git a/Hi3Helper.SimpleZipArchiveReader b/Hi3Helper.SimpleZipArchiveReader index 345d80576..2abcc93d8 160000 --- a/Hi3Helper.SimpleZipArchiveReader +++ b/Hi3Helper.SimpleZipArchiveReader @@ -1 +1 @@ -Subproject commit 345d8057652890010e7e3600849d91c0acddfcf0 +Subproject commit 2abcc93d837fd53b8893988e3eab85b89356f74c diff --git a/Hi3Helper.Sophon b/Hi3Helper.Sophon index 9189e990e..ea21b1883 160000 --- a/Hi3Helper.Sophon +++ b/Hi3Helper.Sophon @@ -1 +1 @@ -Subproject commit 9189e990e2d8ef6a9ee5b3dfd77b41e1874f9cac +Subproject commit ea21b18838579add755fe1edd709b2f3daf0fb7c diff --git a/Hi3Helper.TaskScheduler/packages.lock.json b/Hi3Helper.TaskScheduler/packages.lock.json index 0849386bc..9fed7d551 100644 --- a/Hi3Helper.TaskScheduler/packages.lock.json +++ b/Hi3Helper.TaskScheduler/packages.lock.json @@ -15,10 +15,105 @@ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12" } }, - "hi3helper.win32": { - "type": "Project", + "Fody": { + "type": "Direct", + "requested": "[6.9.3, )", + "resolved": "6.9.3", + "contentHash": "1CUGgFdyECDKgi5HaUBhdv6k+VG9Iy4OCforGfHyar3xQXAJypZkzymgKtWj/4SPd6nSG0Qi7NH71qHrDSZLaA==" + }, + "Microsoft.NETFramework.ReferenceAssemblies": { + "type": "Direct", + "requested": "[1.0.3, )", + "resolved": "1.0.3", + "contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==", + "dependencies": { + "Microsoft.NETFramework.ReferenceAssemblies.net462": "1.0.3" + } + }, + "System.Net.Http": { + "type": "Direct", + "requested": "[4.3.4, )", + "resolved": "4.3.4", + "contentHash": "aOa2d51SEbmM+H+Csw7yJOuNZoHkrP2XnAurye5HWYgGVVU54YZDvsLUYRv6h18X3sPnjNCANmN7ZhIPiqMcjA==", + "dependencies": { + "System.Security.Cryptography.X509Certificates": "4.3.0" + } + }, + "System.Text.RegularExpressions": { + "type": "Direct", + "requested": "[4.3.1, )", + "resolved": "4.3.1", + "contentHash": "N0kNRrWe4+nXOWlpLT4LAY5brb8caNFlUuIRpraCVMDLYutKkol1aV079rQjLuSxKMJT2SpBQsYX9xbcTMmzwg==" + }, + "TaskScheduler": { + "type": "Direct", + "requested": "[2.12.2, )", + "resolved": "2.12.2", + "contentHash": "glpAb3VrwfdAofp6PIyAzL0ZeTV7XUJ8muu0oZoTeyU5jtk2sMJ6QAMRRuFbovcaj+SBJiEUGklxIWOqQoxshA==" + }, + "Microsoft.NETFramework.ReferenceAssemblies.net462": { + "type": "Transitive", + "resolved": "1.0.3", + "contentHash": "IzAV30z22ESCeQfxP29oVf4qEo8fBGXLXSU6oacv/9Iqe6PzgHDKCaWfwMBak7bSJQM0F5boXWoZS+kChztRIQ==" + }, + "System.Security.Cryptography.Algorithms": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "W1kd2Y8mYSCgc3ULTAZ0hOP2dSdG5YauTb1089T0/kRcN2MpSAW1izOFROrJgxSlMn3ArsgHXagigyi+ibhevg==", + "dependencies": { + "System.Security.Cryptography.Primitives": "4.3.0" + } + }, + "System.Security.Cryptography.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "1DEWjZZly9ae9C79vFwqaO5kaOlI5q+3/55ohmq/7dpDyDfc8lYe7YVxJUZ5MF/NtbkRjwFRo14yM4OEo9EmDw==" + }, + "System.Security.Cryptography.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "7bDIyVFNL/xKeFHjhobUAQqSpJq9YTOpbEs6mR233Et01STBMXNAc/V+BM6dwYGc95gVh/Zf+iVXWzj3mE8DWg==" + }, + "System.Security.Cryptography.X509Certificates": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "t2Tmu6Y2NtJ2um0RtcuhP7ZdNNxXEgUm2JeoA/0NvlMjAhKCnM1NX07TDl3244mVp3QU6LPEhT3HTtH1uF7IYw==", + "dependencies": { + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0" + } + } + }, + ".NETFramework,Version=v4.6.2/win-x64": { + "System.Net.Http": { + "type": "Direct", + "requested": "[4.3.4, )", + "resolved": "4.3.4", + "contentHash": "aOa2d51SEbmM+H+Csw7yJOuNZoHkrP2XnAurye5HWYgGVVU54YZDvsLUYRv6h18X3sPnjNCANmN7ZhIPiqMcjA==", + "dependencies": { + "System.Security.Cryptography.X509Certificates": "4.3.0" + } + }, + "System.Security.Cryptography.Algorithms": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "W1kd2Y8mYSCgc3ULTAZ0hOP2dSdG5YauTb1089T0/kRcN2MpSAW1izOFROrJgxSlMn3ArsgHXagigyi+ibhevg==", + "dependencies": { + "System.Security.Cryptography.Primitives": "4.3.0" + } + }, + "System.Security.Cryptography.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "1DEWjZZly9ae9C79vFwqaO5kaOlI5q+3/55ohmq/7dpDyDfc8lYe7YVxJUZ5MF/NtbkRjwFRo14yM4OEo9EmDw==" + }, + "System.Security.Cryptography.X509Certificates": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "t2Tmu6Y2NtJ2um0RtcuhP7ZdNNxXEgUm2JeoA/0NvlMjAhKCnM1NX07TDl3244mVp3QU6LPEhT3HTtH1uF7IYw==", "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "[10.0.12, )" + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0" } } } diff --git a/Hi3Helper.Win32 b/Hi3Helper.Win32 index f5fa4f871..a44bcf38e 160000 --- a/Hi3Helper.Win32 +++ b/Hi3Helper.Win32 @@ -1 +1 @@ -Subproject commit f5fa4f871735e16bdea8f2e844e8a90be5bd936a +Subproject commit a44bcf38e78839e0ad6a9b3e4fbec316a7c9be4b diff --git a/ImageEx b/ImageEx index f129c5ef9..4027757a4 160000 --- a/ImageEx +++ b/ImageEx @@ -1 +1 @@ -Subproject commit f129c5ef97209c9f343291b09d06639f692e97eb +Subproject commit 4027757a4890e68fe1d32306cee95310a19d231e diff --git a/SevenZipExtractor b/SevenZipExtractor index eb15009af..5e0f4de32 160000 --- a/SevenZipExtractor +++ b/SevenZipExtractor @@ -1 +1 @@ -Subproject commit eb15009af23925d56453415d75e51e3192160a44 +Subproject commit 5e0f4de32a5da3ae441fdd5c95d3951ef703480e