diff --git a/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.Control.cs b/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.Control.cs index f31f0f217..6867442c6 100644 --- a/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.Control.cs +++ b/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.Control.cs @@ -344,11 +344,29 @@ private static bool SaveMediaPosition(object? source, TimeSpan timeSpan) timeSpan = timeSpan.Negate(); } - ref TimeSpan span = ref CollectionsMarshal.GetValueRefOrAddDefault(SharedLastMediaPosition, hashCode, out _); - span = timeSpan; + SharedLastMediaPosition[hashCode] = timeSpan; + + TrimSharedLastMediaPositionIfNeeded(); return true; } + private static void TrimSharedLastMediaPositionIfNeeded() + { + if (SharedLastMediaPosition.Count <= MaxSharedLastMediaPositionEntries) + return; + + int excess = SharedLastMediaPosition.Count - MaxSharedLastMediaPositionEntries; + foreach (var entry in SharedLastMediaPosition) + { + if (!SharedLastMediaPosition.TryRemove(entry.Key, out _)) + continue; + + excess--; + if (excess <= 0) + break; + } + } + #endregion } diff --git a/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameInitializer.cs b/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameInitializer.cs index 74cbf9097..a8b1a5a52 100644 --- a/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameInitializer.cs +++ b/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameInitializer.cs @@ -156,22 +156,32 @@ private unsafe void InitializeRenderTarget() #region Disposers + private void DetachVideoPlayerEvents(MediaPlayer player) + { + player.MediaOpened -= InitializeVideoFrameOnMediaOpened; + player.VideoFrameAvailable -= NotifyVideoLoaded; + player.VideoFrameAvailable -= VideoPlayer_VideoFrameAvailableUnsafe; + player.VideoFrameAvailable -= VideoPlayer_VideoFrameAvailableSafe; + player.PlaybackSession.PositionChanged -= MediaDurationPosition_OnChangedBridge; + } + private void DisposeVideoPlayer(bool disposeRenderImageSource = true) { try { - if (_videoPlayer.IsObjectDisposed()) + MediaPlayer? player = _videoPlayer; + if (player is null || player.IsObjectDisposed()) { return; } - _videoPlayer.MediaOpened -= InitializeVideoFrameOnMediaOpened; - _videoPlayer.Pause(); + DetachVideoPlayerEvents(player); + player.Pause(); // Save last video player duration for later - if (_videoPlayer.CanSeek) + if (player.CanSeek) { - _ = SaveMediaPosition(BackgroundSource, _videoPlayer.Position); + _ = SaveMediaPosition(BackgroundSource, player.Position); } if (!_useSafeFrameRenderer) @@ -180,8 +190,7 @@ private void DisposeVideoPlayer(bool disposeRenderImageSource = true) } Interlocked.Exchange(ref _videoFfmpegMediaSource, null)?.Dispose(); - // ReSharper disable once ConstantConditionalAccessQualifier - Interlocked.Exchange(ref _videoPlayer, null!)?.Dispose(); + Interlocked.Exchange(ref _videoPlayer, null)?.Dispose(); DisposeRenderTarget(disposeRenderImageSource); } diff --git a/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameRenderer.cs b/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameRenderer.cs index 6a3c6394e..38850acee 100644 --- a/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameRenderer.cs +++ b/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameRenderer.cs @@ -10,7 +10,7 @@ using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Media; using System; -using System.Collections.Generic; +using System.Collections.Concurrent; using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -55,8 +55,9 @@ private static ref readonly Guid IMediaPlayer5_IID #endregion #region Fields + private const int MaxSharedLastMediaPositionEntries = 32; - private static readonly Dictionary SharedLastMediaPosition = new(); + private static readonly ConcurrentDictionary SharedLastMediaPosition = new(); private CanvasRenderTarget? _canvasRenderTarget; private nint _canvasRenderTargetNativePtr; @@ -462,7 +463,8 @@ public void DisposeAndPauseVideoView(Action? actionOnPause DispatcherQueue?.TryEnqueue(() => SetValue(IsVideoPlayProperty, false)); actionOnPause?.Invoke(); - if (_videoPlayer == null!) + MediaPlayer? playerAtDisposeStart = _videoPlayer; + if (playerAtDisposeStart is null) { actionAfterPause?.Invoke(); return; @@ -471,14 +473,12 @@ public void DisposeAndPauseVideoView(Action? actionOnPause if (disposeVideoPlayer) { // Unsubscribe early to avoid wasted skipped frames. - _videoPlayer.VideoFrameAvailable -= !_useSafeFrameRenderer - ? VideoPlayer_VideoFrameAvailableUnsafe - : VideoPlayer_VideoFrameAvailableSafe; - - _videoPlayer.PlaybackSession.PositionChanged -= MediaDurationPosition_OnChangedBridge; + DetachVideoPlayerEvents(playerAtDisposeStart); } // Interlocked.Exchange(ref _isBlockVideoFrameDraw, 1); // Blocks early + // Guard: skip deferred dispose if the player was already swapped out by a + // concurrent source switch (the old player was disposed synchronously). PauseVideoView(Impl, volumeFadeDurationMs, volumeFadeResolutionMs, token); return; @@ -486,11 +486,14 @@ void Impl() { try { - if (_videoPlayer != null! && DispatcherQueue != null!) + if (_videoPlayer != null! && + ReferenceEquals(_videoPlayer, playerAtDisposeStart) && + DispatcherQueue != null!) { DispatcherQueue.TryEnqueue(DispatcherQueuePriority.High, () => { - if (disposeVideoPlayer) + if (disposeVideoPlayer && + ReferenceEquals(_videoPlayer, playerAtDisposeStart)) DisposeVideoPlayer(disposeRenderImageSource); }); } diff --git a/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.Loaders.cs b/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.Loaders.cs index bc32cf909..fd13a34dc 100644 --- a/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.Loaders.cs +++ b/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.Loaders.cs @@ -219,8 +219,16 @@ private void LoadFromSourceAsyncDetached( if (lastMediaType == MediaSourceType.Video) { - // Dispose and Invalidate Video Player - DisposeAndPauseVideoView(); + // Synchronously dispose the old player before loading the new source, + // cancelling any in-flight fade-out that would otherwise dispose it later. + CancellationTokenSource? fadeCts = Interlocked.Exchange(ref _videoPlayerFadeCts, null); + fadeCts?.Cancel(); + fadeCts?.Dispose(); + SetValue(IsVideoPlayProperty, false); + if (_videoPlayer != null!) + { + DisposeVideoPlayer(); + } } _ = InnerLoadDetached().ConfigureAwait(false); @@ -379,6 +387,11 @@ private static async Task LoadVideoFromSourceAsync( VideoOutputAllowBgra8 = true, VideoOutputAllowNv12 = true, VideoDecoderMode = instance.FfmpegDecoderMode + }, + General = + { + ReadAheadBufferEnabled = true, + ReadAheadBufferDuration = TimeSpan.FromSeconds(15) } }; @@ -423,6 +436,8 @@ private static async Task LoadVideoFromSourceAsync( : instance.VideoPlayer_VideoFrameAvailableSafe; ffmpegMediaSource.Dispose(); + sourceStreamRandom?.Dispose(); + sourceStreamRandom = null; if (loadFfmpegRetry <= 0) {