Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Windows.Input;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Media;

namespace JellyBox.Controls;

Expand Down Expand Up @@ -117,6 +118,16 @@ public double PlaybackSpeed
set => SetValue(PlaybackSpeedProperty, value);
}

public static readonly DependencyProperty StretchModeProperty = DependencyProperty.Register(
nameof(StretchMode), typeof(Stretch), typeof(CustomMediaTransportControls),
new PropertyMetadata(Stretch.Uniform, OnStretchModeChanged));

public Stretch StretchMode
{
get => (Stretch)GetValue(StretchModeProperty);
set => SetValue(StretchModeProperty, value);
}

#endregion

#region Command Dependency Properties
Expand Down Expand Up @@ -265,5 +276,8 @@ private static void OnSelectedSubtitleIndexChanged(DependencyObject d, Dependenc
private static void OnPlaybackSpeedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
=> ((CustomMediaTransportControls)d).UpdatePlaybackSpeedCheckedState();

private static void OnStretchModeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
=> ((CustomMediaTransportControls)d).UpdateStretchModeCheckedState();

#endregion
}
78 changes: 40 additions & 38 deletions src/JellyBox/Controls/CustomMediaTransportControls.Flyouts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ namespace JellyBox.Controls;
/// <summary>
/// Represents a selectable audio or subtitle track.
/// </summary>
internal sealed record TrackInfo(int Index, string DisplayName);
/// <param name="JellyfinIndex">The Jellyfin stream index.</param>
/// <param name="UwpTrackIndex">The UWP MediaPlayer track index, or null if not directly playable.</param>
/// <param name="DisplayName">The display name for the track.</param>
internal sealed record TrackInfo(int JellyfinIndex, int? UwpTrackIndex, string DisplayName);

internal sealed partial class CustomMediaTransportControls
{
Expand Down Expand Up @@ -41,12 +44,7 @@ private void RebuildAudioTracksFlyout()
_audioTracksFlyout = new() { Placement = FlyoutPlacementMode.Top };
foreach (TrackInfo track in AudioTracks)
{
ToggleMenuFlyoutItem item = new()
{
Text = track.DisplayName,
Tag = track.Index,
IsChecked = track.Index == SelectedAudioIndex
};
ToggleMenuFlyoutItem item = CreateFlyoutMenuItem(track, SelectedAudioIndex);
item.Click += OnAudioTrackItemClicked;
_audioTracksFlyout.Items.Add(item);
}
Expand All @@ -63,18 +61,18 @@ private void UpdateAudioTracksCheckedState()

foreach (MenuFlyoutItemBase menuItem in _audioTracksFlyout.Items)
{
if (menuItem is ToggleMenuFlyoutItem toggleItem)
if (menuItem is ToggleMenuFlyoutItem toggleItem && toggleItem.Tag is TrackInfo track)
{
toggleItem.IsChecked = toggleItem.Tag is int index && index == SelectedAudioIndex;
toggleItem.IsChecked = track.JellyfinIndex == SelectedAudioIndex;
}
}
}

private void OnAudioTrackItemClicked(object sender, RoutedEventArgs e)
{
if (sender is ToggleMenuFlyoutItem item && item.Tag is int index)
if (sender is ToggleMenuFlyoutItem item && item.Tag is TrackInfo track)
{
SelectAudioTrackCommand?.Execute(index);
SelectAudioTrackCommand?.Execute(track);
}
}

Expand All @@ -100,25 +98,17 @@ private void RebuildSubtitlesFlyout()
_subtitlesFlyout = new() { Placement = FlyoutPlacementMode.Top };

// Add "Off" option at the top
ToggleMenuFlyoutItem offItem = new()
{
Text = "Off",
Tag = -1,
IsChecked = SelectedSubtitleIndex == -1
};
TrackInfo offTrack = new(-1, UwpTrackIndex: null, "Off");
ToggleMenuFlyoutItem offItem = CreateFlyoutMenuItem(offTrack, SelectedSubtitleIndex);

offItem.Click += OnSubtitleTrackItemClicked;
_subtitlesFlyout.Items.Add(offItem);

_subtitlesFlyout.Items.Add(new MenuFlyoutSeparator());

foreach (TrackInfo track in SubtitleTracks)
{
ToggleMenuFlyoutItem item = new()
{
Text = track.DisplayName,
Tag = track.Index,
IsChecked = track.Index == SelectedSubtitleIndex
};
ToggleMenuFlyoutItem item = CreateFlyoutMenuItem(track, SelectedSubtitleIndex);
item.Click += OnSubtitleTrackItemClicked;
_subtitlesFlyout.Items.Add(item);
}
Expand All @@ -135,18 +125,18 @@ private void UpdateSubtitlesCheckedState()

foreach (MenuFlyoutItemBase menuItem in _subtitlesFlyout.Items)
{
if (menuItem is ToggleMenuFlyoutItem toggleItem)
if (menuItem is ToggleMenuFlyoutItem toggleItem && toggleItem.Tag is TrackInfo track)
{
toggleItem.IsChecked = toggleItem.Tag is int index && index == SelectedSubtitleIndex;
toggleItem.IsChecked = track.JellyfinIndex == SelectedSubtitleIndex;
}
}
}

private void OnSubtitleTrackItemClicked(object sender, RoutedEventArgs e)
{
if (sender is ToggleMenuFlyoutItem item && item.Tag is int index)
if (sender is ToggleMenuFlyoutItem item && item.Tag is TrackInfo track)
{
SelectSubtitleTrackCommand?.Execute(index);
SelectSubtitleTrackCommand?.Execute(track);
}
}

Expand Down Expand Up @@ -236,22 +226,26 @@ private void OnPlaybackSpeedItemClicked(object sender, RoutedEventArgs e)
}
}

private void OnAspectRatioItemClicked(object sender, RoutedEventArgs e)
private void UpdateStretchModeCheckedState()
{
if (sender is ToggleMenuFlyoutItem clickedItem && clickedItem.Tag is Stretch stretch)
if (_aspectRatioSubItem is null)
{
// Update checked state for all aspect items
if (_aspectRatioSubItem is not null)
return;
}

foreach (MenuFlyoutItemBase subMenuItem in _aspectRatioSubItem.Items)
{
if (subMenuItem is ToggleMenuFlyoutItem toggleItem)
{
foreach (MenuFlyoutItemBase subMenuItem in _aspectRatioSubItem.Items)
{
if (subMenuItem is ToggleMenuFlyoutItem toggleItem)
{
toggleItem.IsChecked = toggleItem.Tag is Stretch itemStretch && itemStretch == stretch;
}
}
toggleItem.IsChecked = toggleItem.Tag is Stretch itemStretch && itemStretch == StretchMode;
}
}
}

private void OnAspectRatioItemClicked(object sender, RoutedEventArgs e)
{
if (sender is ToggleMenuFlyoutItem clickedItem && clickedItem.Tag is Stretch stretch)
{
ChangeStretchModeCommand?.Execute(stretch);
}
}
Expand All @@ -262,4 +256,12 @@ private void OnPlaybackInfoClicked(object sender, RoutedEventArgs e)
}

#endregion

private static ToggleMenuFlyoutItem CreateFlyoutMenuItem(TrackInfo track, int selectedIndex)
=> new()
{
Text = track.DisplayName,
Tag = track,
IsChecked = track.JellyfinIndex == selectedIndex
};
}
87 changes: 35 additions & 52 deletions src/JellyBox/Services/DeviceProfileManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,29 @@ namespace JellyBox.Services;
internal sealed class DeviceProfileManager
#pragma warning disable CA1812 // Avoid uninstantiated internal classes
{
// Supported embedded subtitle formats for UWP MediaPlayer
public static readonly HashSet<string> SupportedEmbeddedSubtitleFormats = new(StringComparer.OrdinalIgnoreCase)
{
"subrip",
"srt",
"vtt",
"webvtt",
"mov_text", // MP4 timed text
"ass", // Basic support (styling stripped). TODO: Can we render it manually?
"ssa", // Basic support (styling stripped). TODO: Can we render it manually?
};

public static readonly HashSet<string> SupportedExternalSubtitleFormats = new(StringComparer.OrdinalIgnoreCase)
{
// UWP TimedTextSource supports SRT and VTT natively.
"subrip",
"srt",
"vtt",
"webvtt",

// "pgssub" // PGS subtitles are not supported natively. TODO: Can we render it manually?
};

public DeviceProfile Profile { get; private set; } = null!; // TODO

// This logic is adapted from the web client's browserDeviceProfile.js
Expand Down Expand Up @@ -1070,64 +1093,24 @@ public async Task InitializeAsync()
}

// Subtitle profiles
// External vtt or burn in
string subtitleBurninSetting = ""; // TODO: appSettings.get("subtitleburnin");
#pragma warning disable CA1508 // Avoid dead conditional code
if (subtitleBurninSetting != "all")
#pragma warning restore CA1508 // Avoid dead conditional code
foreach (string subtitleFormat in SupportedEmbeddedSubtitleFormats)
{
profile.SubtitleProfiles.Add(
new SubtitleProfile
{
Format = "subrip",
Method = SubtitleProfile_Method.External,
Format = subtitleFormat,
Method = SubtitleProfile_Method.Embed,
});
}

bool supportsTextTracks = false; // TODO: Check
if (supportsTextTracks)
{
profile.SubtitleProfiles.Add(
new SubtitleProfile
{
Format = "vtt",
Method = SubtitleProfile_Method.External,
});
}

bool enableSsaRender = false; // TODO: Check settings
#pragma warning disable CA1508 // Avoid dead conditional code
if (!enableSsaRender && subtitleBurninSetting != "allcomplexformats")
#pragma warning restore CA1508 // Avoid dead conditional code
{
profile.SubtitleProfiles.Add(
new SubtitleProfile
{
Format = "ass",
Method = SubtitleProfile_Method.External
});
profile.SubtitleProfiles.Add(
new SubtitleProfile
{
Format = "ssa",
Method = SubtitleProfile_Method.External
});
}

// TODO: Can we overlay an image on top?
/*
if (options.enablePgsRender !== false
&& appSettings.get("subtitlerenderpgs") == "true"
&& subtitleBurninSetting != "allcomplexformats"
&& subtitleBurninSetting != "onlyimageformats")
{
profile.SubtitleProfiles.Add(
new SubtitleProfile
{
Format = "pgssub",
Method = SubtitleProfile_Method.External
});
}
*/
foreach (string subtitleFormat in SupportedExternalSubtitleFormats)
{
profile.SubtitleProfiles.Add(
new SubtitleProfile
{
Format = subtitleFormat,
Method = SubtitleProfile_Method.External,
});
}

Profile = profile;
Expand Down
19 changes: 11 additions & 8 deletions src/JellyBox/ViewModels/ItemDetailsViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace JellyBox.ViewModels;

internal sealed record MediaInfoItem(string Text);

internal sealed record MediaStreamOption(string DisplayText, int? Index)
internal sealed record MediaStreamOption(string DisplayText, int Index)
{
public static MediaStreamOption SubtitlesOff { get; } = new("Off", -1);
}
Expand Down Expand Up @@ -229,7 +229,7 @@ private void DetermineVideoOptions(MediaSourceInfo mediaSourceInfo)
.Where(s => s.Type == MediaStream_Type.Video)
.OrderBy(s => s, MediaStreamComparer.Instance)
.ToList();
int? selectedIndex = videoStreams.Count > 0 ? videoStreams[0].Index : -1;
int selectedIndex = videoStreams.Count > 0 ? videoStreams[0].Index.GetValueOrDefault() : -1;

MediaStreamOption? selectedOption = null;
List<MediaStreamOption> options = new(videoStreams.Count);
Expand All @@ -243,10 +243,11 @@ private void DetermineVideoOptions(MediaSourceInfo mediaSourceInfo)
displayTitle = "TODO";
}

MediaStreamOption option = new(displayTitle, videoStream.Index);
int index = videoStream.Index.GetValueOrDefault();
MediaStreamOption option = new(displayTitle, index);
options.Add(option);

if (selectedOption is null || videoStream.Index == selectedIndex)
if (selectedOption is null || index == selectedIndex)
{
selectedOption = option;
}
Expand All @@ -268,10 +269,11 @@ private void DetermineAudioOptions(MediaSourceInfo mediaSourceInfo)
List<MediaStreamOption> options = new(audioStreams.Count);
foreach (MediaStream audioStream in audioStreams)
{
MediaStreamOption option = new(audioStream.DisplayTitle!, audioStream.Index);
int index = audioStream.Index.GetValueOrDefault();
MediaStreamOption option = new(audioStream.DisplayTitle!, index);
options.Add(option);

if (selectedOption is null || audioStream.Index == selectedIndex)
if (selectedOption is null || index == selectedIndex)
{
selectedOption = option;
}
Expand Down Expand Up @@ -305,10 +307,11 @@ private void DetermineSubtitleOptions(MediaSourceInfo mediaSourceInfo)

foreach (MediaStream subtitleStream in subtitleStreams)
{
MediaStreamOption option = new(subtitleStream.DisplayTitle!, subtitleStream.Index);
int index = subtitleStream.Index.GetValueOrDefault();
MediaStreamOption option = new(subtitleStream.DisplayTitle!, index);
options.Add(option);

if (selectedOption is null || subtitleStream.Index == selectedIndex)
if (selectedOption is null || index == selectedIndex)
{
selectedOption = option;
}
Expand Down
Loading