Browse Source

add stream selector to playback troubleshooting (#2474)

pull/2475/head
Jason Dove 8 months ago committed by GitHub
parent
commit
18905a79dc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      CHANGELOG.md
  2. 8
      ErsatzTV.Application/Troubleshooting/Commands/ArchiveTroubleshootingResultsHandler.cs
  3. 1
      ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlayback.cs
  4. 60
      ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlaybackHandler.cs
  5. 1
      ErsatzTV.Application/Troubleshooting/Commands/StartTroubleshootingPlayback.cs
  6. 12
      ErsatzTV.Application/Troubleshooting/Commands/StartTroubleshootingPlaybackHandler.cs
  7. 12
      ErsatzTV/Controllers/Api/TroubleshootController.cs
  8. 65
      ErsatzTV/Pages/PlaybackTroubleshooting.razor

3
CHANGELOG.md

@ -62,6 +62,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -62,6 +62,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Preview button will be red when preview is disabled due to browser incompatibility
- Add AV1 encoding support with NVIDIA, VAAPI and QSV acceleration
- This also requires `HLS Segmenter (fmp4)`
- Add `Stream Selector` option to playback troubleshooting tool
- This can be helpful for validating stream selector behavior with specific content
- Manual subtitle selection will be disabled when using a stream selector
### Fixed
- Fix green output when libplacebo tonemapping is used with NVIDIA acceleration and 10-bit output in FFmpeg Profile

8
ErsatzTV.Application/Troubleshooting/Commands/ArchiveTroubleshootingResultsHandler.cs

@ -22,14 +22,22 @@ public class ArchiveTroubleshootingResultsHandler(ILocalFileSystem localFileSyst @@ -22,14 +22,22 @@ public class ArchiveTroubleshootingResultsHandler(ILocalFileSystem localFileSyst
{
hasReport = true;
zipArchive.CreateEntryFromFile(file, fileName);
continue;
}
if (Path.GetExtension(file).Equals(".json", StringComparison.OrdinalIgnoreCase))
{
zipArchive.CreateEntryFromFile(file, fileName);
continue;
}
if (fileName.Contains("capabilities", StringComparison.OrdinalIgnoreCase))
{
zipArchive.CreateEntryFromFile(file, fileName);
continue;
}
if (fileName.Contains("stream-selector", StringComparison.OrdinalIgnoreCase))
{
zipArchive.CreateEntryFromFile(file, fileName);
}

1
ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlayback.cs

@ -8,6 +8,7 @@ public record PrepareTroubleshootingPlayback( @@ -8,6 +8,7 @@ public record PrepareTroubleshootingPlayback(
StreamingMode StreamingMode,
int MediaItemId,
int FFmpegProfileId,
string StreamSelector,
List<int> WatermarkIds,
List<int> GraphicsElementIds,
int? SubtitleId,

60
ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlaybackHandler.cs

@ -108,6 +108,12 @@ public class PrepareTroubleshootingPlaybackHandler( @@ -108,6 +108,12 @@ public class PrepareTroubleshootingPlaybackHandler(
//SongVideoMode = ChannelSongVideoMode.WithProgress
};
if (!string.IsNullOrEmpty(request.StreamSelector))
{
channel.StreamSelectorMode = ChannelStreamSelectorMode.Custom;
channel.StreamSelector = request.StreamSelector;
}
List<WatermarkOptions> watermarks = [];
if (request.WatermarkIds.Count > 0)
{
@ -211,7 +217,7 @@ public class PrepareTroubleshootingPlaybackHandler( @@ -211,7 +217,7 @@ public class PrepareTroubleshootingPlaybackHandler(
new MediaItemAudioVersion(mediaItem, version),
videoPath,
mediaPath,
_ => GetSelectedSubtitle(mediaItem, request),
_ => GetSubtitles(mediaItem, request),
string.Empty,
string.Empty,
string.Empty,
@ -240,35 +246,33 @@ public class PrepareTroubleshootingPlaybackHandler( @@ -240,35 +246,33 @@ public class PrepareTroubleshootingPlaybackHandler(
return playoutItemResult;
}
private static async Task<List<Subtitle>> GetSelectedSubtitle(
MediaItem mediaItem,
PrepareTroubleshootingPlayback request)
private static async Task<List<Subtitle>> GetSubtitles(MediaItem mediaItem, PrepareTroubleshootingPlayback request)
{
if (request.SubtitleId is not null)
List<Subtitle> allSubtitles = mediaItem switch
{
List<Subtitle> allSubtitles = mediaItem switch
{
Episode episode => await Optional(episode.EpisodeMetadata).Flatten().HeadOrNone()
.Map(mm => mm.Subtitles ?? [])
.IfNoneAsync([]),
Movie movie => await Optional(movie.MovieMetadata).Flatten().HeadOrNone()
.Map(mm => mm.Subtitles ?? [])
.IfNoneAsync([]),
OtherVideo otherVideo => await Optional(otherVideo.OtherVideoMetadata).Flatten().HeadOrNone()
.Map(mm => mm.Subtitles ?? [])
.IfNoneAsync([]),
_ => []
};
bool isMediaServer = mediaItem is PlexMovie or PlexEpisode or
JellyfinMovie or JellyfinEpisode or EmbyMovie or EmbyEpisode;
if (isMediaServer)
{
// closed captions are currently unsupported
allSubtitles.RemoveAll(s => s.Codec == "eia_608");
}
Episode episode => await Optional(episode.EpisodeMetadata).Flatten().HeadOrNone()
.Map(mm => mm.Subtitles ?? [])
.IfNoneAsync([]),
Movie movie => await Optional(movie.MovieMetadata).Flatten().HeadOrNone()
.Map(mm => mm.Subtitles ?? [])
.IfNoneAsync([]),
OtherVideo otherVideo => await Optional(otherVideo.OtherVideoMetadata).Flatten().HeadOrNone()
.Map(mm => mm.Subtitles ?? [])
.IfNoneAsync([]),
_ => []
};
bool isMediaServer = mediaItem is PlexMovie or PlexEpisode or
JellyfinMovie or JellyfinEpisode or EmbyMovie or EmbyEpisode;
if (isMediaServer)
{
// closed captions are currently unsupported
allSubtitles.RemoveAll(s => s.Codec == "eia_608");
}
if (request.SubtitleId is not null)
{
allSubtitles.RemoveAll(s => s.Id != request.SubtitleId.Value);
foreach (Subtitle subtitle in allSubtitles)
@ -279,7 +283,7 @@ public class PrepareTroubleshootingPlaybackHandler( @@ -279,7 +283,7 @@ public class PrepareTroubleshootingPlaybackHandler(
}
}
return [];
return allSubtitles;
}
private static async Task<Validation<BaseError, Tuple<MediaItem, string, string, FFmpegProfile>>> Validate(

1
ErsatzTV.Application/Troubleshooting/Commands/StartTroubleshootingPlayback.cs

@ -5,6 +5,7 @@ namespace ErsatzTV.Application.Troubleshooting; @@ -5,6 +5,7 @@ namespace ErsatzTV.Application.Troubleshooting;
public record StartTroubleshootingPlayback(
Guid SessionId,
string StreamSelector,
PlayoutItemResult PlayoutItemResult,
MediaItemInfo MediaItemInfo,
TroubleshootingInfo TroubleshootingInfo) : IRequest, IFFmpegWorkerRequest;

12
ErsatzTV.Application/Troubleshooting/Commands/StartTroubleshootingPlaybackHandler.cs

@ -61,6 +61,18 @@ public class StartTroubleshootingPlaybackHandler( @@ -61,6 +61,18 @@ public class StartTroubleshootingPlaybackHandler(
troubleshootingInfoJson,
cancellationToken);
// write stream selector
if (!string.IsNullOrWhiteSpace(request.StreamSelector))
{
string fullPath = Path.Combine(FileSystemLayout.ChannelStreamSelectorsFolder, request.StreamSelector);
if (File.Exists(fullPath))
{
File.Copy(
fullPath,
Path.Combine(FileSystemLayout.TranscodeTroubleshootingFolder, "stream-selector.yml"));
}
}
HardwareAccelerationKind hwAccel = request.TroubleshootingInfo.FFmpegProfiles.Head().HardwareAcceleration;
if (hwAccel is HardwareAccelerationKind.Qsv)
{

12
ErsatzTV/Controllers/Api/TroubleshootController.cs

@ -34,6 +34,8 @@ public class TroubleshootController( @@ -34,6 +34,8 @@ public class TroubleshootController(
[FromQuery]
List<int> graphicsElement,
[FromQuery]
string streamSelector,
[FromQuery]
int? subtitleId,
[FromQuery]
int seekSeconds,
@ -48,6 +50,7 @@ public class TroubleshootController( @@ -48,6 +50,7 @@ public class TroubleshootController(
streamingMode,
mediaItem,
ffmpegProfile,
streamSelector,
watermark,
graphicsElement,
subtitleId,
@ -82,6 +85,7 @@ public class TroubleshootController( @@ -82,6 +85,7 @@ public class TroubleshootController(
await channelWriter.WriteAsync(
new StartTroubleshootingPlayback(
sessionId,
streamSelector,
playoutItemResult,
mediaInfo,
troubleshootingInfo),
@ -141,7 +145,13 @@ public class TroubleshootController( @@ -141,7 +145,13 @@ public class TroubleshootController(
Option<int> ss = seekSeconds > 0 ? seekSeconds : Option<int>.None;
Option<string> maybeArchivePath = await mediator.Send(
new ArchiveTroubleshootingResults(mediaItem, ffmpegProfile, streamingMode, watermark, graphicsElement, ss),
new ArchiveTroubleshootingResults(
mediaItem,
ffmpegProfile,
streamingMode,
watermark,
graphicsElement,
ss),
cancellationToken);
foreach (string archivePath in maybeArchivePath)

65
ErsatzTV/Pages/PlaybackTroubleshooting.razor

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
@page "/system/troubleshooting/playback"
@using ErsatzTV.Application.Channels
@using ErsatzTV.Application.FFmpegProfiles
@using ErsatzTV.Application.Graphics
@using ErsatzTV.Application.MediaItems
@ -7,6 +8,7 @@ @@ -7,6 +8,7 @@
@using ErsatzTV.Application.Watermarks
@using ErsatzTV.Core.Notifications
@using MediatR.Courier
@using Microsoft.AspNetCore.WebUtilities
@implements IDisposable
@inject IMediator Mediator
@inject NavigationManager NavigationManager
@ -55,6 +57,17 @@ @@ -55,6 +57,17 @@
}
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Stream Selector</MudText>
</div>
<MudSelect @bind-Value="_streamSelector" For="@(() => _streamSelector)" Clearable="true">
@foreach (string selector in _streamSelectors)
{
<MudSelectItem T="string" Value="@selector">@selector</MudSelectItem>
}
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Streaming Mode</MudText>
@ -68,7 +81,7 @@ @@ -68,7 +81,7 @@
<div class="d-flex">
<MudText>Subtitle</MudText>
</div>
<MudSelect @bind-Value="_subtitleId" For="@(() => _subtitleId)" Clearable="true">
<MudSelect @bind-Value="_subtitleId" For="@(() => _subtitleId)" Clearable="true" Disabled="@(!string.IsNullOrWhiteSpace(_streamSelector))">
<MudSelectItem T="int?" Value="@((int?)null)">(none)</MudSelectItem>
@foreach (SubtitleViewModel subtitleStream in _subtitleStreams)
{
@ -149,12 +162,14 @@ @@ -149,12 +162,14 @@
private CancellationTokenSource _cts;
private readonly List<FFmpegProfileViewModel> _ffmpegProfiles = [];
private readonly List<string> _streamSelectors = [];
private readonly List<WatermarkViewModel> _watermarks = [];
private readonly List<SubtitleViewModel> _subtitleStreams = [];
private readonly List<GraphicsElementViewModel> _graphicsElements = [];
private MediaItemInfo _info;
private StreamingMode _streamingMode = StreamingMode.HttpLiveStreamingSegmenter;
private int _ffmpegProfileId;
private string _streamSelector;
private IEnumerable<string> _watermarkNames = new System.Collections.Generic.HashSet<string>();
private IEnumerable<string> _graphicsElementNames = new System.Collections.Generic.HashSet<string>();
private bool _startFromBeginning;
@ -193,6 +208,10 @@ @@ -193,6 +208,10 @@
_ffmpegProfileId = _ffmpegProfiles.Map(f => f.Id).Head();
}
_streamSelectors.Clear();
_streamSelectors.AddRange(await Mediator.Send(new GetChannelStreamSelectors(), token));
_streamSelector = null;
_watermarks.Clear();
_watermarks.AddRange(await Mediator.Send(new GetAllWatermarks(), token));
@ -221,14 +240,21 @@ @@ -221,14 +240,21 @@
private async Task PreviewChannel()
{
var uri = new UriBuilder(NavigationManager.ToAbsoluteUri(NavigationManager.Uri));
uri.Path = uri.Path.Replace("/system/troubleshooting/playback", "/api/troubleshoot/playback.m3u8");
uri.Query = $"?mediaItem={MediaItemId}&ffmpegProfile={_ffmpegProfileId}&streamingMode={(int)_streamingMode}&seekSeconds={_seekSeconds}";
var baseUri = NavigationManager.ToAbsoluteUri(NavigationManager.Uri).ToString();
string apiUri = baseUri.Replace("/system/troubleshooting/playback", "/api/troubleshoot/playback.m3u8");
var queryString = new Dictionary<string, string>
{
["mediaItem"] = (MediaItemId ?? 0).ToString(),
["ffmpegProfile"] = _ffmpegProfileId.ToString(),
["streamingMode"] = ((int)_streamingMode).ToString(),
["seekSeconds"] = _seekSeconds.ToString()
};
foreach (string watermarkName in _watermarkNames)
{
foreach (WatermarkViewModel watermark in _watermarks.Where(wm => wm.Name == watermarkName))
{
uri.Query += $"&watermark={watermark.Id}";
queryString.Add("watermark", watermark.Id.ToString());
}
}
@ -236,16 +262,21 @@ @@ -236,16 +262,21 @@
{
foreach (GraphicsElementViewModel graphicsElement in _graphicsElements.Where(ge => ge.Name == graphicsElementName))
{
uri.Query += $"&graphicsElement={graphicsElement.Id}";
queryString.Add("graphicsElement", graphicsElement.Id.ToString());
}
}
if (_subtitleId is not null)
if (!string.IsNullOrWhiteSpace(_streamSelector))
{
uri.Query += $"&subtitleId={_subtitleId.Value}";
queryString.Add("streamSelector", _streamSelector);
}
else if (_subtitleId is not null)
{
queryString.Add("subtitleId", _subtitleId.Value.ToString());
}
await JsRuntime.InvokeVoidAsync("previewChannel", uri.ToString());
string uriWithQuery = QueryHelpers.AddQueryString(apiUri, queryString);
await JsRuntime.InvokeVoidAsync("previewChannel", uriWithQuery);
await Task.Delay(TimeSpan.FromSeconds(1));
@ -281,13 +312,20 @@ @@ -281,13 +312,20 @@
private async Task DownloadResults()
{
var uri = $"api/troubleshoot/playback/archive?mediaItem={MediaItemId ?? 0}&ffmpegProfile={_ffmpegProfileId}&streamingMode={(int)_streamingMode}&seekSeconds={_seekSeconds}";
string apiUri = "api/troubleshoot/playback/archive";
var queryString = new Dictionary<string, string>
{
["mediaItem"] = (MediaItemId ?? 0).ToString(),
["ffmpegProfile"] = _ffmpegProfileId.ToString(),
["streamingMode"] = ((int)_streamingMode).ToString(),
["seekSeconds"] = _seekSeconds.ToString()
};
foreach (string watermarkName in _watermarkNames)
{
foreach (WatermarkViewModel watermark in _watermarks.Where(wm => wm.Name == watermarkName))
{
uri += $"&watermark={watermark.Id}";
queryString.Add("watermark", watermark.Id.ToString());
}
}
@ -295,11 +333,12 @@ @@ -295,11 +333,12 @@
{
foreach (GraphicsElementViewModel graphicsElement in _graphicsElements.Where(ge => ge.Name == graphicsElementName))
{
uri += $"&graphicsElement={graphicsElement.Id}";
queryString.Add("graphicsElement", graphicsElement.Id.ToString());
}
}
await JsRuntime.InvokeVoidAsync("window.open", uri);
string uriWithQuery = QueryHelpers.AddQueryString(apiUri, queryString);
await JsRuntime.InvokeVoidAsync("window.open", uriWithQuery);
}
private void HandleTroubleshootingCompleted(PlaybackTroubleshootingCompletedNotification result)

Loading…
Cancel
Save