Skip to content

Commit

Permalink
Merge pull request #527 from huynhsontung/improve-subtitle-picking
Browse files Browse the repository at this point in the history
feat: improve subtitle picking logic - persist subtitle language selection
  • Loading branch information
huynhsontung authored Jan 3, 2025
2 parents 4d85aa9 + 2569537 commit 43b1e97
Show file tree
Hide file tree
Showing 12 changed files with 159 additions and 24 deletions.
2 changes: 1 addition & 1 deletion Screenbox.Core/Common/ServiceHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public static void PopulateCoreServices(ServiceCollection services)
services.AddTransient<PlayerElementViewModel>();
services.AddTransient<PropertyViewModel>();
services.AddTransient<ChapterViewModel>();
services.AddTransient<AudioTrackSubtitleViewModel>();
services.AddTransient<CompositeTrackPickerViewModel>();
services.AddTransient<SeekBarViewModel>();
services.AddTransient<VideosPageViewModel>();
services.AddTransient<NetworkPageViewModel>();
Expand Down
8 changes: 7 additions & 1 deletion Screenbox.Core/Helpers/LanguageHelper.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Windows.ApplicationModel.Resources;
using System.Linq;
using Windows.ApplicationModel.Resources;

namespace Screenbox.Core.Helpers;
internal static class LanguageHelper
Expand All @@ -12,4 +13,9 @@ public static bool TryConvertISO6392ToISO6391(string threeLetterTag, out string
twoLetterTag = Loader.GetString(threeLetterTag);
return !string.IsNullOrEmpty(twoLetterTag);
}

public static string GetPreferredLanguage()
{
return Windows.System.UserProfile.GlobalizationPreferences.Languages.FirstOrDefault() ?? string.Empty;
}
}
46 changes: 41 additions & 5 deletions Screenbox.Core/Helpers/VlcMediaExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,32 @@ 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)
if (media.CheckParsed() && 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)
if (media.CheckParsed() && media.ParsedStatus != MediaParsedStatus.Skipped)
return;

// Wait for the ParsedStatus to change again.
TaskCompletionSource<MediaParsedStatus> tsc = new();
Task task = tsc.Task;

media.ParsedChanged += MediaOnParsedChanged;

if (await Task.WhenAny(tsc.Task, Task.Delay(timeout, cancellationToken)) != task)
try
{
if (await Task.WhenAny(tsc.Task, Task.Delay(timeout, cancellationToken)) != task)
{
tsc.SetCanceled();
}
}
finally
{
tsc.SetCanceled();
media.ParsedChanged -= MediaOnParsedChanged;
}

return;
Expand All @@ -38,4 +44,34 @@ void MediaOnParsedChanged(object sender, MediaParsedChangedEventArgs e)
tsc.TrySetResult(e.ParsedStatus);
}
}

public static async Task WaitForParsed(this Media media, TimeSpan timeout, CancellationToken cancellationToken = default)
{
if (media.CheckParsed()) return;
TaskCompletionSource<bool> tcs = new();
Task task = tcs.Task;

media.ParsedChanged += OnMediaOnParsedChanged;
try
{
if (await Task.WhenAny(tcs.Task, Task.Delay(timeout, cancellationToken)) != task)
{
tcs.SetCanceled();
}
}
finally
{
media.ParsedChanged -= OnMediaOnParsedChanged;
}

return;

void OnMediaOnParsedChanged(object sender, MediaParsedChangedEventArgs args)
{
tcs.TrySetResult(args.ParsedStatus == MediaParsedStatus.Done);
}
}

public static bool CheckParsed(this Media media) =>
media.IsParsed || media.ParsedStatus != 0 || media.State == VLCState.Playing;
}
2 changes: 2 additions & 0 deletions Screenbox.Core/Playback/MediaTrack.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ public abstract class MediaTrack : IMediaTrack

public string Language => _language?.DisplayName ?? _languageStr;

public string LanguageTag => _language?.LanguageTag ?? string.Empty;

private readonly Language? _language;
private readonly string _languageStr;

Expand Down
2 changes: 1 addition & 1 deletion Screenbox.Core/Screenbox.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@
<Compile Include="ViewModels\ArtistSearchResultPageViewModel.cs" />
<Compile Include="ViewModels\ArtistsPageViewModel.cs" />
<Compile Include="ViewModels\ArtistViewModel.cs" />
<Compile Include="ViewModels\AudioTrackSubtitleViewModel.cs" />
<Compile Include="ViewModels\CompositeTrackPickerViewModel.cs" />
<Compile Include="ViewModels\BaseMusicContentViewModel.cs" />
<Compile Include="ViewModels\CastControlViewModel.cs" />
<Compile Include="ViewModels\ChapterViewModel.cs" />
Expand Down
1 change: 1 addition & 0 deletions Screenbox.Core/Services/ISettingsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public interface ISettingsService
bool PlayerTapGesture { get; set; }
bool PlayerShowControls { get; set; }
int PersistentVolume { get; set; }
string PersistentSubtitleLanguage { get; set; }
bool ShowRecent { get; set; }
bool EnqueueAllFilesInFolder { get; set; }
bool SearchRemovableStorage { get; set; }
Expand Down
7 changes: 7 additions & 0 deletions Screenbox.Core/Services/SettingsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public sealed class SettingsService : ISettingsService
private const string PersistentVolumeKey = "Values/Volume";
private const string MaxVolumeKey = "Values/MaxVolume";
private const string PersistentRepeatModeKey = "Values/RepeatMode";
private const string PersistentSubtitleLanguageKey = "Values/SubtitleLanguage";

public bool UseIndexer
{
Expand Down Expand Up @@ -67,6 +68,12 @@ public int PersistentVolume
set => SetValue(PersistentVolumeKey, value);
}

public string PersistentSubtitleLanguage
{
get => GetValue<string>(PersistentSubtitleLanguageKey) ?? string.Empty;
set => SetValue(PersistentSubtitleLanguageKey, value);
}

public int MaxVolume
{
get => GetValue<int>(MaxVolumeKey);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Windows.Storage;
using Windows.Storage.Search;
Expand All @@ -21,7 +22,7 @@

namespace Screenbox.Core.ViewModels
{
public sealed partial class AudioTrackSubtitleViewModel : ObservableRecipient,
public sealed partial class CompositeTrackPickerViewModel : ObservableRecipient,
IRecipient<PlaylistCurrentItemChangedMessage>,
IRecipient<MediaPlayerChangedMessage>
{
Expand All @@ -37,12 +38,16 @@ public sealed partial class AudioTrackSubtitleViewModel : ObservableRecipient,
[ObservableProperty] private int _audioTrackIndex;
private readonly IFilesService _filesService;
private readonly IResourceService _resourceService;
private readonly ISettingsService _settingsService;
private IMediaPlayer? _mediaPlayer;
private bool _flyoutOpened;
private CancellationTokenSource? _cts;

public AudioTrackSubtitleViewModel(IFilesService filesService, IResourceService resourceService)
public CompositeTrackPickerViewModel(IFilesService filesService, IResourceService resourceService, ISettingsService settingsService)
{
_filesService = filesService;
_resourceService = resourceService;
_settingsService = settingsService;
SubtitleTracks = new ObservableCollection<string>();
AudioTracks = new ObservableCollection<string>();
_mediaPlayer = Messenger.Send(new MediaPlayerRequestMessage()).Response;
Expand All @@ -60,17 +65,70 @@ public void Receive(MediaPlayerChangedMessage message)
/// </summary>
public async void Receive(PlaylistCurrentItemChangedMessage message)
{
_cts?.Cancel();
if (_mediaPlayer is not VlcMediaPlayer player) return;
if (message.Value is not { Source: StorageFile file, MediaType: MediaPlaybackType.Video } media)
return;

bool subtitleInitialized = false;
var playbackSubtitleTrackList = media.Item.Value?.SubtitleTracks;
if (playbackSubtitleTrackList == null) return;
if (playbackSubtitleTrackList.Count > 0) subtitleInitialized = true;
IReadOnlyList<StorageFile> subtitles = await GetSubtitlesForFile(file);

if (subtitles.Count <= 0) return;
foreach (StorageFile subtitleFile in subtitles)
{
// Preload subtitle but don't select it
media.Item.Value?.SubtitleTracks.AddExternalSubtitle(player, subtitleFile, false);
playbackSubtitleTrackList.AddExternalSubtitle(player, subtitleFile, false);
}

if (!subtitleInitialized && media.Item.Value is { } playbackItem)
{
try
{
using var cts = new CancellationTokenSource();
_cts = cts;
await playbackItem.Media.WaitForParsed(TimeSpan.FromSeconds(5), cts.Token);
}
catch (OperationCanceledException)
{
// pass
}
finally
{
_cts = null;
}
}

TrySetSubtitleFromLanguage(playbackSubtitleTrackList, _settingsService.PersistentSubtitleLanguage);
}

private static void TrySetSubtitleFromLanguage(PlaybackSubtitleTrackList subtitleTrackList, string persistentLanguage)
{
// Check persistent subtitle value to try and select a subtitle
if (!string.IsNullOrEmpty(persistentLanguage))
{
// If there is only one subtitle then select it
if (subtitleTrackList.Count == 1)
{
subtitleTrackList.SelectedIndex = 0;
return;
}

// Try to select the subtitle with the same language as the persistent value
var langPreferences = persistentLanguage.Split(',', StringSplitOptions.RemoveEmptyEntries);
foreach (string language in langPreferences)
{
for (int i = 0; i < subtitleTrackList.Count; i++)
{
var subtitleTrack = subtitleTrackList[i];
// Try to match language tag first, then language name
if (language == subtitleTrack.LanguageTag || language.Equals(subtitleTrack.Language, StringComparison.CurrentCultureIgnoreCase))
{
subtitleTrackList.SelectedIndex = i;
break;
}
}
}
}
}

Expand Down Expand Up @@ -114,7 +172,24 @@ private async Task<IReadOnlyList<StorageFile>> GetSubtitlesForFile(StorageFile s
partial void OnSubtitleTrackIndexChanged(int value)
{
if (ItemSubtitleTrackList != null && value >= 0 && value < SubtitleTracks.Count)
{
ItemSubtitleTrackList.SelectedIndex = value - 1;

if (_flyoutOpened)
{
if (value == 0)
{
_settingsService.PersistentSubtitleLanguage = string.Empty;
}
else
{
var subtitle = ItemSubtitleTrackList[ItemSubtitleTrackList.SelectedIndex];
_settingsService.PersistentSubtitleLanguage =
$"{subtitle.LanguageTag},{subtitle.Language},{LanguageHelper.GetPreferredLanguage().Substring(0, 2)}";
}
}
}

}

partial void OnAudioTrackIndexChanged(int value)
Expand Down Expand Up @@ -142,12 +217,19 @@ private async Task AddSubtitle()
}
}

public void OnAudioCaptionFlyoutOpening()
public void OnFlyoutOpening()
{
UpdateSubtitleTrackList();
UpdateAudioTrackList();
SubtitleTrackIndex = ItemSubtitleTrackList?.SelectedIndex + 1 ?? 0;
AudioTrackIndex = ItemAudioTrackList?.SelectedIndex ?? -1;

_flyoutOpened = true;
}

public void OnFlyoutClosed()
{
_flyoutOpened = false;
}

private void UpdateAudioTrackList()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<UserControl
x:Class="Screenbox.Controls.AudioTrackSubtitlePicker"
x:Class="Screenbox.Controls.CompositeTrackPicker"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:contract14Present="http://schemas.microsoft.com/winfx/2006/xaml/presentation?IsApiContractPresent(Windows.Foundation.UniversalApiContract,14)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,17 @@

namespace Screenbox.Controls
{
public sealed partial class AudioTrackSubtitlePicker : UserControl
public sealed partial class CompositeTrackPicker : UserControl
{
public IRelayCommand? ShowSubtitleOptionsCommand { get; set; }
public IRelayCommand? ShowAudioOptionsCommand { get; set; }

internal AudioTrackSubtitleViewModel ViewModel => (AudioTrackSubtitleViewModel)DataContext;
internal CompositeTrackPickerViewModel ViewModel => (CompositeTrackPickerViewModel)DataContext;

public AudioTrackSubtitlePicker()
public CompositeTrackPicker()
{
this.InitializeComponent();
DataContext = Ioc.Default.GetRequiredService<AudioTrackSubtitleViewModel>();
DataContext = Ioc.Default.GetRequiredService<CompositeTrackPickerViewModel>();
}
}
}
5 changes: 3 additions & 2 deletions Screenbox/Controls/PlayerControls.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -511,10 +511,11 @@
<Button.Flyout>
<Flyout
x:Name="AudioSubtitlePickerFlyout"
Closed="{x:Bind AudioTrackSubtitlePicker.ViewModel.OnFlyoutClosed}"
FlyoutPresenterStyle="{StaticResource FlyoutPresenterMenuFlyoutStyle}"
Opening="{x:Bind AudioTrackSubtitlePicker.ViewModel.OnAudioCaptionFlyoutOpening}"
Opening="{x:Bind AudioTrackSubtitlePicker.ViewModel.OnFlyoutOpening}"
ShouldConstrainToRootBounds="False">
<controls:AudioTrackSubtitlePicker x:Name="AudioTrackSubtitlePicker" IsAccessKeyScope="True" />
<controls:CompositeTrackPicker x:Name="AudioTrackSubtitlePicker" IsAccessKeyScope="True" />
</Flyout>
</Button.Flyout>
</Button>
Expand Down
6 changes: 3 additions & 3 deletions Screenbox/Screenbox.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,8 @@
<Compile Include="Commands\ShowPropertiesCommand.cs" />
<Compile Include="Controls\AcceleratorService.cs" />
<Compile Include="Controls\Animations\AnimatedPlayingVisualSource.cs" />
<Compile Include="Controls\AudioTrackSubtitlePicker.xaml.cs">
<DependentUpon>AudioTrackSubtitlePicker.xaml</DependentUpon>
<Compile Include="Controls\CompositeTrackPicker.xaml.cs">
<DependentUpon>CompositeTrackPicker.xaml</DependentUpon>
</Compile>
<Compile Include="Controls\CastControl.xaml.cs">
<DependentUpon>CastControl.xaml</DependentUpon>
Expand Down Expand Up @@ -514,7 +514,7 @@
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</ApplicationDefinition>
<Page Include="Controls\AudioTrackSubtitlePicker.xaml">
<Page Include="Controls\CompositeTrackPicker.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
Expand Down

0 comments on commit 43b1e97

Please sign in to comment.