From 2b13df3966f30d97d7464e2eed98e2d1e76ffe98 Mon Sep 17 00:00:00 2001 From: Tung Huynh Date: Thu, 25 Apr 2024 21:48:57 -0700 Subject: [PATCH 1/4] better handle playlist parsing timeout --- Screenbox.Core/ViewModels/MediaListViewModel.cs | 2 +- Screenbox.Core/ViewModels/MediaViewModel.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Screenbox.Core/ViewModels/MediaListViewModel.cs b/Screenbox.Core/ViewModels/MediaListViewModel.cs index 7ae37b133..03da56675 100644 --- a/Screenbox.Core/ViewModels/MediaListViewModel.cs +++ b/Screenbox.Core/ViewModels/MediaListViewModel.cs @@ -708,7 +708,7 @@ private async Task> ParsePlaylistAsync(MediaViewModel sour MediaParsedStatus parsedStatus = media.ParsedStatus; if (!media.IsParsed) { - parsedStatus = await media.Parse(MediaParseOptions.ParseNetwork, 5000, cts.Token); + parsedStatus = await media.Parse(MediaParseOptions.ParseNetwork, 10000, cts.Token); } if (parsedStatus != MediaParsedStatus.Done) return Array.Empty(); diff --git a/Screenbox.Core/ViewModels/MediaViewModel.cs b/Screenbox.Core/ViewModels/MediaViewModel.cs index b63bcea87..afef29c4d 100644 --- a/Screenbox.Core/ViewModels/MediaViewModel.cs +++ b/Screenbox.Core/ViewModels/MediaViewModel.cs @@ -224,7 +224,7 @@ public async Task LoadDetailsAsync(IFilesService filesService) switch (MediaType) { - case MediaPlaybackType.Unknown when _item is { VideoTracks.Count: 0, Media.IsParsed: true }: + case MediaPlaybackType.Unknown when _item is { VideoTracks.Count: 0, Media.ParsedStatus: MediaParsedStatus.Done }: // Update media type when it was previously set Unknown. Usually when source is a URI. // We don't want to init PlaybackItem just for this. MediaInfo.MediaType = MediaPlaybackType.Music; From 2e8c84f722af2eb45d244dad3e30be8d71940fc2 Mon Sep 17 00:00:00 2001 From: Tung Huynh Date: Thu, 25 Apr 2024 22:58:33 -0700 Subject: [PATCH 2/4] fix uri media is not parsed properly when adding to play queue --- .../ViewModels/MediaListViewModel.cs | 143 ++++++++++-------- 1 file changed, 83 insertions(+), 60 deletions(-) diff --git a/Screenbox.Core/ViewModels/MediaListViewModel.cs b/Screenbox.Core/ViewModels/MediaListViewModel.cs index 03da56675..2b514fffb 100644 --- a/Screenbox.Core/ViewModels/MediaListViewModel.cs +++ b/Screenbox.Core/ViewModels/MediaListViewModel.cs @@ -66,12 +66,33 @@ public sealed partial class MediaListViewModel : ObservableRecipient, private const int MediaBufferCapacity = 5; - private sealed record ShuffleBackup(List OriginalPlaylist, List? Removals = null) + private sealed class ShuffleBackup { - public List OriginalPlaylist { get; } = OriginalPlaylist; + public List OriginalPlaylist { get; } // Needed due to how UI invokes CollectionChanged when moving items - public List Removals { get; } = Removals ?? new List(); + public List Removals { get; } + + public ShuffleBackup(List originalPlaylist, List? removals = null) + { + OriginalPlaylist = originalPlaylist; + Removals = removals ?? new List(); + } + } + + private sealed class PlaylistCreateResult + { + public MediaViewModel PlayNext { get; } + + public IList Playlist { get; } + + public PlaylistCreateResult(MediaViewModel playNext, IList playlist) + { + PlayNext = playNext; + Playlist = playlist; + } + + public PlaylistCreateResult(MediaViewModel playNext) : this(playNext, new[] { playNext }) { } } public MediaListViewModel(IFilesService filesService, ISettingsService settingsService, @@ -153,21 +174,25 @@ public void Receive(ClearPlaylistMessage message) ClearPlaylistAndNeighboringQuery(); } - public void Receive(QueuePlaylistMessage message) + public async void Receive(QueuePlaylistMessage message) { _lastUpdated = message.Value; bool canInsert = CurrentIndex + 1 < Items.Count; int counter = 0; foreach (MediaViewModel media in message.Value) { - if (message.AddNext && canInsert) - { - Items.Insert(CurrentIndex + 1 + counter, media); - counter++; - } - else + var result = await CreatePlaylistAsync(media); + foreach (MediaViewModel subMedia in result.Playlist) { - Items.Add(media); + if (message.AddNext && canInsert) + { + Items.Insert(CurrentIndex + 1 + counter, subMedia); + counter++; + } + else + { + Items.Add(subMedia); + } } } } @@ -426,20 +451,14 @@ private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs } } - public async Task EnqueueAsync(IReadOnlyList files) + private async Task CreatePlaylistAsync(IReadOnlyList files) { List queue = new(); foreach (IStorageItem item in files) { - // TODO: handle folders - //if (item is IStorageFolder folder) - //{ - // folder.GetFilesAsync() - //} - if (item is not StorageFile storageFile) continue; MediaViewModel vm = _mediaFactory.GetSingleton(storageFile); - if (storageFile.IsSupportedPlaylist() && await RecursiveParsePlaylistAsync(vm) is { Count: > 0 } playlist) + if (storageFile.IsSupportedPlaylist() && await ParseSubMediaRecursiveAsync(vm) is { Count: > 0 } playlist) { queue.AddRange(playlist); } @@ -449,6 +468,13 @@ private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs } } + return queue.Count > 0 ? new PlaylistCreateResult(queue[0], queue) : null; + } + + public async Task EnqueueAsync(IReadOnlyList files) + { + var result = await CreatePlaylistAsync(files); + var queue = result?.Playlist ?? Array.Empty(); Enqueue(queue); return queue.Count > 0 ? queue[0] : null; @@ -462,59 +488,49 @@ private void Enqueue(IEnumerable list) } } - private async Task EnqueueAsync(MediaViewModel media) + private async Task CreatePlaylistAsync(MediaViewModel media) { if (media.Item == null || media.Item.Media is { IsParsed: true, SubItems.Count: 0 } || (media.Source is StorageFile file && !file.IsSupportedPlaylist()) - || await RecursiveParsePlaylistAsync(media) is not { Count: > 0 } playlist) + || await ParseSubMediaRecursiveAsync(media) is not { Count: > 0 } playlist) { - Items.Add(media); - return media; + return new PlaylistCreateResult(media); } - Enqueue(playlist); - return playlist[0]; + return new PlaylistCreateResult(media, playlist); } - private async Task EnqueueAsync(StorageFile file) + private async Task CreatePlaylistAsync(StorageFile file) { MediaViewModel media = _mediaFactory.GetSingleton(file); - if (file.IsSupportedPlaylist() && await RecursiveParsePlaylistAsync(media) is { Count: > 0 } playlist) + if (file.IsSupportedPlaylist() && await ParseSubMediaRecursiveAsync(media) is { Count: > 0 } playlist) { media = playlist[0]; - Enqueue(playlist); - } - else - { - Items.Add(media); + return new PlaylistCreateResult(media, playlist); } - return media; + return new PlaylistCreateResult(media); } - private async Task EnqueueAsync(Uri uri) + private async Task CreatePlaylistAsync(Uri uri) { MediaViewModel media = _mediaFactory.GetTransient(uri); - if (await RecursiveParsePlaylistAsync(media) is { Count: > 0 } playlist) + if (await ParseSubMediaRecursiveAsync(media) is { Count: > 0 } playlist) { media = playlist[0]; - Enqueue(playlist); - } - else - { - Items.Add(media); + return new PlaylistCreateResult(media, playlist); } - return media; + return new PlaylistCreateResult(media); } - private async Task DispatchEnqueueAsync(object value) => value switch + private async Task CreatePlaylistAsync(object value) => value switch { - StorageFile file => await EnqueueAsync(file), - Uri uri => await EnqueueAsync(uri), - IReadOnlyList files => await EnqueueAsync(files), - MediaViewModel media => await EnqueueAsync(media), + StorageFile file => await CreatePlaylistAsync(file), + Uri uri => await CreatePlaylistAsync(uri), + IReadOnlyList files => await CreatePlaylistAsync(files), + MediaViewModel media => await CreatePlaylistAsync(media), _ => throw new ArgumentException("Unsupported media type", nameof(value)) }; @@ -522,9 +538,12 @@ private async void EnqueueAndPlay(object value) { try { - MediaViewModel? next = await DispatchEnqueueAsync(value); - if (next != null) - PlaySingle(next); + PlaylistCreateResult? result = await CreatePlaylistAsync(value); + if (result != null) + { + Enqueue(result.Playlist); + PlaySingle(result.PlayNext); + } } catch (OperationCanceledException) { @@ -592,7 +611,9 @@ private async Task NextAsync() if (nextFile != null) { ClearPlaylist(); - MediaViewModel next = await EnqueueAsync(nextFile); + var result = await CreatePlaylistAsync(nextFile); + Enqueue(result.Playlist); + MediaViewModel next = result.PlayNext; PlaySingle(next); } } @@ -630,7 +651,9 @@ private async Task PreviousAsync() if (previousFile != null) { ClearPlaylist(); - MediaViewModel prev = await EnqueueAsync(previousFile); + var result = await CreatePlaylistAsync(previousFile); + Enqueue(result.Playlist); + MediaViewModel prev = result.PlayNext; PlaySingle(prev); } else @@ -676,16 +699,16 @@ private void OnEndReached(IMediaPlayer sender, object? args) }); } - private async Task> RecursiveParsePlaylistAsync(MediaViewModel source) + private async Task> ParseSubMediaRecursiveAsync(MediaViewModel source) { - IList playlist = await ParsePlaylistAsync(source); + IList playlist = await ParseSubMediaAsync(source); if (playlist.Count > 0) { MediaViewModel nextItem = playlist[0]; - while (playlist.Count == 1 && await ParsePlaylistAsync(nextItem) is { Count: > 0 } nextPlaylist) + while (playlist.Count == 1 && await ParseSubMediaAsync(nextItem) is { Count: > 0 } nextSubItems) { - nextItem = nextPlaylist[0]; - playlist = nextPlaylist; + nextItem = nextSubItems[0]; + playlist = nextSubItems; } } @@ -693,11 +716,11 @@ private async Task> RecursiveParsePlaylistAsync(MediaViewM } - private async Task> ParsePlaylistAsync(MediaViewModel source) + private async Task> ParseSubMediaAsync(MediaViewModel source) { if (source.Item == null) return Array.Empty(); - // Load playlist is atomic + // Parsing sub items is atomic _cts?.Cancel(); using CancellationTokenSource cts = new(); @@ -712,8 +735,8 @@ private async Task> ParsePlaylistAsync(MediaViewModel sour } if (parsedStatus != MediaParsedStatus.Done) return Array.Empty(); - IEnumerable playlist = media.SubItems.Select(item => _mediaFactory.GetTransient(item)); - return playlist.ToList(); + IEnumerable subItems = media.SubItems.Select(item => _mediaFactory.GetTransient(item)); + return subItems.ToList(); } catch (OperationCanceledException) { From a932385252b050ca417da64ad33331403cb048d0 Mon Sep 17 00:00:00 2001 From: Tung Huynh Date: Thu, 25 Apr 2024 23:05:22 -0700 Subject: [PATCH 3/4] force media uri to be absoulte --- Screenbox.Core/ViewModels/MediaViewModel.cs | 2 ++ Screenbox/Controls/OpenUrlDialog.xaml.cs | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Screenbox.Core/ViewModels/MediaViewModel.cs b/Screenbox.Core/ViewModels/MediaViewModel.cs index afef29c4d..f9eb85d00 100644 --- a/Screenbox.Core/ViewModels/MediaViewModel.cs +++ b/Screenbox.Core/ViewModels/MediaViewModel.cs @@ -1,5 +1,6 @@ #nullable enable +using CommunityToolkit.Diagnostics; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Messaging; using LibVLCSharp.Shared; @@ -116,6 +117,7 @@ public MediaViewModel(LibVlcService libVlcService, StorageFile file) public MediaViewModel(LibVlcService libVlcService, Uri uri) : this(uri, new MediaInfo(MediaPlaybackType.Unknown), libVlcService) { + Guard.IsTrue(uri.IsAbsoluteUri); Location = uri.OriginalString; _name = uri.Segments.Length > 0 ? Uri.UnescapeDataString(uri.Segments.Last()) : string.Empty; } diff --git a/Screenbox/Controls/OpenUrlDialog.xaml.cs b/Screenbox/Controls/OpenUrlDialog.xaml.cs index fd1e33996..638f6707f 100644 --- a/Screenbox/Controls/OpenUrlDialog.xaml.cs +++ b/Screenbox/Controls/OpenUrlDialog.xaml.cs @@ -21,14 +21,14 @@ public OpenUrlDialog() OpenUrlDialog dialog = new(); ContentDialogResult result = await dialog.ShowAsync(); string url = dialog.UrlBox.Text; - return result == ContentDialogResult.Primary && Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out Uri uri) + return result == ContentDialogResult.Primary && Uri.TryCreate(url, UriKind.Absolute, out Uri uri) ? uri : null; } private bool CanOpen(string url) { - return Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out Uri _); + return Uri.TryCreate(url, UriKind.Absolute, out Uri _); } } } \ No newline at end of file From b6f23313f55865a0cff0460b684f3ad7622733e3 Mon Sep 17 00:00:00 2001 From: Tung Huynh Date: Sun, 28 Apr 2024 14:47:58 -0700 Subject: [PATCH 4/4] remove load time when opening an URL media will be parsed and play with a timeout but the UI feedback is instant --- Screenbox.Core/Helpers/VlcMediaExtensions.cs | 41 ++++++++++++++ Screenbox.Core/Screenbox.Core.csproj | 1 + .../ViewModels/MediaListViewModel.cs | 54 +++++++++++++------ 3 files changed, 79 insertions(+), 17 deletions(-) create mode 100644 Screenbox.Core/Helpers/VlcMediaExtensions.cs diff --git a/Screenbox.Core/Helpers/VlcMediaExtensions.cs b/Screenbox.Core/Helpers/VlcMediaExtensions.cs new file mode 100644 index 000000000..ed6beb049 --- /dev/null +++ b/Screenbox.Core/Helpers/VlcMediaExtensions.cs @@ -0,0 +1,41 @@ +using LibVLCSharp.Shared; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Screenbox.Core.Helpers; +internal static class VlcMediaExtensions +{ + public static async Task ParseAsync(this Media media, TimeSpan timeout, CancellationToken cancellationToken = default) + { + // Check if media is already parsed + if (media.IsParsed && media.ParsedStatus is MediaParsedStatus.Done or MediaParsedStatus.Failed) + return; + + await media.Parse(MediaParseOptions.ParseNetwork, (int)timeout.TotalMilliseconds, cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); + + // Media may not be parsed even after calling Parse() + // This can happen if the media is being open at the same time. + if (media.IsParsed && media.ParsedStatus != MediaParsedStatus.Skipped) + return; + + // Wait for the ParsedStatus to change again. + TaskCompletionSource tsc = new(); + Task task = tsc.Task; + + media.ParsedChanged += MediaOnParsedChanged; + + if (await Task.WhenAny(tsc.Task, Task.Delay(timeout, cancellationToken)) != task) + { + tsc.SetCanceled(); + } + + return; + + void MediaOnParsedChanged(object sender, MediaParsedChangedEventArgs e) + { + tsc.TrySetResult(e.ParsedStatus); + } + } +} diff --git a/Screenbox.Core/Screenbox.Core.csproj b/Screenbox.Core/Screenbox.Core.csproj index e44826aaf..eb090cdd4 100644 --- a/Screenbox.Core/Screenbox.Core.csproj +++ b/Screenbox.Core/Screenbox.Core.csproj @@ -151,6 +151,7 @@ + diff --git a/Screenbox.Core/ViewModels/MediaListViewModel.cs b/Screenbox.Core/ViewModels/MediaListViewModel.cs index 2b514fffb..9a5729008 100644 --- a/Screenbox.Core/ViewModels/MediaListViewModel.cs +++ b/Screenbox.Core/ViewModels/MediaListViewModel.cs @@ -128,11 +128,13 @@ public void Receive(MediaPlayerChangedMessage message) if (_delayPlay != null) { - _dispatcherQueue.TryEnqueue(() => + async void SetPlayQueue() { ClearPlaylist(); - EnqueueAndPlay(_delayPlay); - }); + await EnqueueAndPlay(_delayPlay); + } + + _dispatcherQueue.TryEnqueue(SetPlayQueue); } } @@ -202,7 +204,7 @@ public void Receive(PlaylistRequestMessage message) message.Reply(new PlaylistInfo(Items, CurrentItem, CurrentIndex, _lastUpdated, _neighboringFilesQuery)); } - public void Receive(PlayMediaMessage message) + public async void Receive(PlayMediaMessage message) { if (_mediaPlayer == null) { @@ -218,7 +220,7 @@ public void Receive(PlayMediaMessage message) { _lastUpdated = message.Value; ClearPlaylistAndNeighboringQuery(); - EnqueueAndPlay(message.Value); + await EnqueueAndPlay(message.Value); } } @@ -231,6 +233,7 @@ partial void OnCurrentItemChanging(MediaViewModel? value) if (CurrentItem != null) { + _cts?.Cancel(); CurrentItem.IsMediaActive = false; CurrentItem.IsPlaying = null; } @@ -270,9 +273,8 @@ async partial void OnCurrentItemChanged(MediaViewModel? value) } Messenger.Send(new PlaylistCurrentItemChangedMessage(value)); - await Task.WhenAll( - _transportControlsService.UpdateTransportControlsDisplayAsync(value), - UpdateMediaBufferAsync()); + await _transportControlsService.UpdateTransportControlsDisplayAsync(value); + await UpdateMediaBufferAsync(); Analytics.TrackEvent("PlaylistCurrentItemChanged", value != null ? new Dictionary { @@ -491,14 +493,14 @@ private void Enqueue(IEnumerable list) private async Task CreatePlaylistAsync(MediaViewModel media) { if (media.Item == null - || media.Item.Media is { IsParsed: true, SubItems.Count: 0 } + || media.Item.Media is { ParsedStatus: MediaParsedStatus.Done or MediaParsedStatus.Failed, SubItems.Count: 0 } || (media.Source is StorageFile file && !file.IsSupportedPlaylist()) || await ParseSubMediaRecursiveAsync(media) is not { Count: > 0 } playlist) { return new PlaylistCreateResult(media); } - return new PlaylistCreateResult(media, playlist); + return new PlaylistCreateResult(playlist[0], playlist); } private async Task CreatePlaylistAsync(StorageFile file) @@ -534,13 +536,29 @@ private async Task CreatePlaylistAsync(Uri uri) _ => throw new ArgumentException("Unsupported media type", nameof(value)) }; - private async void EnqueueAndPlay(object value) + private MediaViewModel? GetMedia(object value) => value switch + { + MediaViewModel media => media, + StorageFile file => _mediaFactory.GetSingleton(file), + Uri uri => _mediaFactory.GetTransient(uri), + _ => null + }; + + private async Task EnqueueAndPlay(object value) { try { - PlaylistCreateResult? result = await CreatePlaylistAsync(value); - if (result != null) + MediaViewModel? playNext = GetMedia(value); + if (playNext != null) { + Enqueue(new[] { playNext }); + PlaySingle(playNext); + } + + PlaylistCreateResult? result = await CreatePlaylistAsync(playNext ?? value); + if (result != null && !result.PlayNext.Source.Equals(playNext?.Source)) + { + ClearPlaylist(); Enqueue(result.Playlist); PlaySingle(result.PlayNext); } @@ -728,13 +746,15 @@ private async Task> ParseSubMediaAsync(MediaViewModel sour { _cts = cts; Media media = source.Item.Media; - MediaParsedStatus parsedStatus = media.ParsedStatus; - if (!media.IsParsed) + if (!media.IsParsed || media.ParsedStatus is MediaParsedStatus.Skipped) // Not yet parsed { - parsedStatus = await media.Parse(MediaParseOptions.ParseNetwork, 10000, cts.Token); + await media.ParseAsync(TimeSpan.FromSeconds(10), cts.Token); } - if (parsedStatus != MediaParsedStatus.Done) return Array.Empty(); + // If token is cancelled, it is likely that media is already disposed + // Must immediately throw + cts.Token.ThrowIfCancellationRequested(); // Important + IEnumerable subItems = media.SubItems.Select(item => _mediaFactory.GetTransient(item)); return subItems.ToList(); }