diff --git a/CHANGELOG.md b/CHANGELOG.md index f86978b1f..10f791aa1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -107,11 +107,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Remote Stream library items consist of YAML (`.yml`) files with the following fields - `url`: the URL of the content that can be played directly by ffmpeg - `script`: the process name and arguments for a command that will output content to stdout - - `duration`: when the content is "live" and does not have duration metadata, this must be provided to allow scheduling - `is_live`: *required* property that indicates whether the remote stream contains live content - - When this is set to `true`, ETV cannot work ahead on transcoding this item, which is a necessary tradeoff for supporting live content - - When this is set to `false`, ETV will treat the stream as VOD and attempt to work ahead on transcoding like any other local item - - This *will* cause errors when the content is actually live, so it's important to configure this correctly + - When this is set to `true`, ETV cannot work ahead on transcoding this item, which is a necessary tradeoff for supporting live content + - When this is set to `false`, ETV will treat the stream as VOD and attempt to work ahead on transcoding like any other local item + - This *will* cause errors when the content is actually live, so it's important to configure this correctly + - `duration`: when the content is live and does not have duration metadata, this must be provided to allow scheduling - The remote stream definition (YAML file) may provide either a `url` or a `script` - If both are provided, `url` will be used diff --git a/ErsatzTV.Application/Troubleshooting/Commands/ArchiveTroubleshootingResults.cs b/ErsatzTV.Application/Troubleshooting/Commands/ArchiveTroubleshootingResults.cs index 53e3a2f70..1da40232e 100644 --- a/ErsatzTV.Application/Troubleshooting/Commands/ArchiveTroubleshootingResults.cs +++ b/ErsatzTV.Application/Troubleshooting/Commands/ArchiveTroubleshootingResults.cs @@ -1,4 +1,8 @@ namespace ErsatzTV.Application.Troubleshooting; -public record ArchiveTroubleshootingResults(int MediaItemId, int FFmpegProfileId, int WatermarkId) +public record ArchiveTroubleshootingResults( + int MediaItemId, + int FFmpegProfileId, + int WatermarkId, + bool StartFromBeginning) : IRequest>; diff --git a/ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlayback.cs b/ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlayback.cs index fda1ed85a..ab843ae80 100644 --- a/ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlayback.cs +++ b/ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlayback.cs @@ -3,5 +3,9 @@ using ErsatzTV.Core; namespace ErsatzTV.Application.Troubleshooting; -public record PrepareTroubleshootingPlayback(int MediaItemId, int FFmpegProfileId, int WatermarkId) +public record PrepareTroubleshootingPlayback( + int MediaItemId, + int FFmpegProfileId, + int WatermarkId, + bool StartFromBeginning) : IRequest>; diff --git a/ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlaybackHandler.cs b/ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlaybackHandler.cs index a096a6723..4b362d3b8 100644 --- a/ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlaybackHandler.cs +++ b/ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlaybackHandler.cs @@ -96,6 +96,15 @@ public class PrepareTroubleshootingPlaybackHandler( // we cannot burst live input bool hlsRealtime = mediaItem is RemoteStream { IsLive: true }; + TimeSpan inPoint = TimeSpan.Zero; + TimeSpan outPoint = duration; + if (!hlsRealtime && !request.StartFromBeginning) + { + inPoint = TimeSpan.FromSeconds(version.Duration.TotalSeconds / 2.0); + duration = TimeSpan.FromSeconds(duration.TotalSeconds / 2.0); + outPoint = inPoint + duration; + } + Command process = await ffmpegProcessService.ForPlayoutItem( ffmpegPath, ffprobePath, @@ -127,8 +136,8 @@ public class PrepareTroubleshootingPlaybackHandler( Option.None, hlsRealtime, FillerKind.None, - TimeSpan.Zero, - duration, + inPoint, + outPoint, 0, None, false, diff --git a/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs b/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs index 9fd5ad1c4..8876a4e75 100644 --- a/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs +++ b/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs @@ -145,23 +145,23 @@ public class TranscodingTests public static InputFormat[] InputFormats = [ // // // example format that requires colorspace filter - // new("libx264", "yuv420p", "tv", "smpte170m", "bt709", "smpte170m"), + new("libx264", "yuv420p", "tv", "smpte170m", "bt709", "smpte170m"), // // // // // // // example format that requires setparams filter - // new("libx264", "yuv420p", string.Empty, string.Empty, string.Empty, string.Empty), + new("libx264", "yuv420p", string.Empty, string.Empty, string.Empty, string.Empty), // // // // // // // new("libx264", "yuvj420p"), - // new("libx264", "yuv420p10le"), + new("libx264", "yuv420p10le"), // // // // new("libx264", "yuv444p10le"), // // // // // // // new("mpeg1video", "yuv420p"), // // // // - // new("mpeg2video", "yuv420p"), + new("mpeg2video", "yuv420p"), // // //new InputFormat("libx265", "yuv420p"), - new("libx265", "yuv420p10le") + new("libx265", "yuv420p10le"), // - // new("mpeg4", "yuv420p"), + new("mpeg4", "yuv420p"), // // new("libvpx-vp9", "yuv420p"), // new("libvpx-vp9", "yuv420p10le"), diff --git a/ErsatzTV/Controllers/Api/TroubleshootController.cs b/ErsatzTV/Controllers/Api/TroubleshootController.cs index 2f56515d2..aa54d5fb3 100644 --- a/ErsatzTV/Controllers/Api/TroubleshootController.cs +++ b/ErsatzTV/Controllers/Api/TroubleshootController.cs @@ -26,10 +26,12 @@ public class TroubleshootController( int ffmpegProfile, [FromQuery] int watermark, + [FromQuery] + bool startFromBeginning, CancellationToken cancellationToken) { Either result = await mediator.Send( - new PrepareTroubleshootingPlayback(mediaItem, ffmpegProfile, watermark), + new PrepareTroubleshootingPlayback(mediaItem, ffmpegProfile, watermark, startFromBeginning), cancellationToken); return await result.MatchAsync( @@ -83,10 +85,12 @@ public class TroubleshootController( int ffmpegProfile, [FromQuery] int watermark, + [FromQuery] + bool startFromBeginning, CancellationToken cancellationToken) { Option maybeArchivePath = await mediator.Send( - new ArchiveTroubleshootingResults(mediaItem, ffmpegProfile, watermark), + new ArchiveTroubleshootingResults(mediaItem, ffmpegProfile, watermark, startFromBeginning), cancellationToken); foreach (string archivePath in maybeArchivePath) diff --git a/ErsatzTV/Pages/PlaybackTroubleshooting.razor b/ErsatzTV/Pages/PlaybackTroubleshooting.razor index 090fe6688..0c43ede71 100644 --- a/ErsatzTV/Pages/PlaybackTroubleshooting.razor +++ b/ErsatzTV/Pages/PlaybackTroubleshooting.razor @@ -64,6 +64,12 @@ } + +
+ Start From Beginning +
+ +
Preview @@ -72,7 +78,7 @@ Color="Color.Primary" StartIcon="@Icons.Material.Filled.PlayCircle" Disabled="@(Locker.IsTroubleshootingPlaybackLocked() || _mediaItemId is null)" - OnClick="PreviewChannel"> + OnClick="@PreviewChannel"> Play @@ -105,6 +111,7 @@ private MediaItemInfo _info; private int _ffmpegProfileId; private int? _watermarkId; + private bool _startFromBeginning; private bool _hasPlayed; public void Dispose() @@ -136,7 +143,7 @@ { 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}&watermark={_watermarkId ?? 0}"; + uri.Query = $"?mediaItem={_mediaItemId}&ffmpegProfile={_ffmpegProfileId}&watermark={_watermarkId ?? 0}&startFromBeginning={_startFromBeginning}"; await JsRuntime.InvokeVoidAsync("previewChannel", uri.ToString()); await Task.Delay(TimeSpan.FromSeconds(1)); @@ -155,6 +162,7 @@ foreach (MediaItemInfo info in maybeInfo.RightToSeq()) { _info = info; + _startFromBeginning = string.Equals(info.Kind, "RemoteStream", StringComparison.OrdinalIgnoreCase); } if (maybeInfo.IsLeft) @@ -168,7 +176,7 @@ private async Task DownloadResults() { - await JsRuntime.InvokeVoidAsync("window.open", $"api/troubleshoot/playback/archive?mediaItem={_mediaItemId ?? 0}&ffmpegProfile={_ffmpegProfileId}&watermark={_watermarkId ?? 0}"); + await JsRuntime.InvokeVoidAsync("window.open", $"api/troubleshoot/playback/archive?mediaItem={_mediaItemId ?? 0}&ffmpegProfile={_ffmpegProfileId}&watermark={_watermarkId ?? 0}&startFromBeginning={_startFromBeginning}"); } private void HandleTroubleshootingCompleted(PlaybackTroubleshootingCompletedNotification result)