diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a8a689fb..cebffd6d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Changed - Use ffmpeg 7 in all docker images - Show health checks at top of home page; scroll release notes if needed +- Improve `HLS Segmenter V2` compliance by serving fmp4 segments when `hevc` video format is selected + - > 1.5. The container format for HEVC video MUST be fMP4. ## [0.8.6-beta] - 2024-04-03 ### Added diff --git a/ErsatzTV.Application/Streaming/Commands/StartFFmpegSessionHandler.cs b/ErsatzTV.Application/Streaming/Commands/StartFFmpegSessionHandler.cs index 8e1ab175c..a6f0b3b9c 100644 --- a/ErsatzTV.Application/Streaming/Commands/StartFFmpegSessionHandler.cs +++ b/ErsatzTV.Application/Streaming/Commands/StartFFmpegSessionHandler.cs @@ -107,7 +107,6 @@ public class StartFFmpegSessionHandler : IRequestHandler new HlsSessionWorkerV2( _serviceScopeFactory, - _configElementRepository, _localFileSystem, _sessionWorkerV2Logger, targetFramerate, diff --git a/ErsatzTV.Application/Streaming/HlsSessionWorkerV2.cs b/ErsatzTV.Application/Streaming/HlsSessionWorkerV2.cs index d75dee762..b0257cdd7 100644 --- a/ErsatzTV.Application/Streaming/HlsSessionWorkerV2.cs +++ b/ErsatzTV.Application/Streaming/HlsSessionWorkerV2.cs @@ -5,11 +5,9 @@ using System.Timers; using CliWrap; using CliWrap.Buffered; using ErsatzTV.Core; -using ErsatzTV.Core.Domain; using ErsatzTV.Core.FFmpeg; using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.Metadata; -using ErsatzTV.Core.Interfaces.Repositories; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Timer = System.Timers.Timer; @@ -21,7 +19,6 @@ public class HlsSessionWorkerV2 : IHlsSessionWorker private readonly SemaphoreSlim _slim = new(1, 1); //private static int _workAheadCount; - private readonly IConfigElementRepository _configElementRepository; private readonly string _host; private readonly ILocalFileSystem _localFileSystem; private readonly ILogger _logger; @@ -32,7 +29,6 @@ public class HlsSessionWorkerV2 : IHlsSessionWorker private CancellationTokenSource _cancellationTokenSource; private string _channelNumber; private bool _disposedValue; - private bool _hasWrittenSegments; private DateTimeOffset _lastAccess; private Option _lastProcessModel; private IServiceScope _serviceScope; @@ -42,7 +38,6 @@ public class HlsSessionWorkerV2 : IHlsSessionWorker public HlsSessionWorkerV2( IServiceScopeFactory serviceScopeFactory, - IConfigElementRepository configElementRepository, ILocalFileSystem localFileSystem, ILogger logger, Option targetFramerate, @@ -51,7 +46,6 @@ public class HlsSessionWorkerV2 : IHlsSessionWorker { _serviceScope = serviceScopeFactory.CreateScope(); _mediator = _serviceScope.ServiceProvider.GetRequiredService(); - _configElementRepository = configElementRepository; _localFileSystem = localFileSystem; _logger = logger; _targetFramerate = targetFramerate; @@ -298,8 +292,6 @@ public class HlsSessionWorkerV2 : IHlsSessionWorker foreach (PlayoutItemProcessModel processModel in result.RightToSeq()) { - _hasWrittenSegments = true; - _logger.LogDebug("Next playout item process will transcode until {Until}", processModel.Until); _transcodedUntil = processModel.Until; @@ -366,45 +358,4 @@ public class HlsSessionWorkerV2 : IHlsSessionWorker return result; } - - private async Task GetPtsOffset(string channelNumber, CancellationToken cancellationToken) - { - await _slim.WaitAsync(cancellationToken); - try - { - long result = 0; - - // if we haven't yet written any segments, start at zero - if (!_hasWrittenSegments) - { - return result; - } - - Either queryResult = await _mediator.Send( - new GetLastPtsDuration(channelNumber), - cancellationToken); - - foreach (BaseError error in queryResult.LeftToSeq()) - { - _logger.LogWarning("Unable to determine last pts offset - {Error}", error.ToString()); - } - - foreach ((long pts, long duration) in queryResult.RightToSeq()) - { - result = pts + duration + 1; - } - - return result; - } - finally - { - _slim.Release(); - } - } - - private async Task GetWorkAheadLimit() => - await _configElementRepository.GetValue(ConfigElementKey.FFmpegWorkAheadSegmenters) - .Map(maybeCount => maybeCount.Match(identity, () => 1)); - - private sealed record Segment(string File, int SequenceNumber); } diff --git a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs index 74ccd4e3e..ec035e39a 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs @@ -651,10 +651,14 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService Option hlsPlaylistPath = Path.Combine(FileSystemLayout.TranscodeFolder, channel.Number, "live.m3u8"); - Option hlsSegmentTemplate = Path.Combine( - FileSystemLayout.TranscodeFolder, - channel.Number, - "live%06d.ts"); + Option hlsSegmentTemplate = videoFormat switch + { + // hls/hevc needs mp4 + VideoFormat.Hevc => Path.Combine(FileSystemLayout.TranscodeFolder, channel.Number, "live%06d.m4s"), + + // hls is otherwise fine with ts + _ => Path.Combine(FileSystemLayout.TranscodeFolder, channel.Number, "live%06d.ts") + }; var desiredState = new FrameState( playbackSettings.RealtimeOutput, diff --git a/ErsatzTV.FFmpeg/OutputFormat/OutputFormatConcatHls.cs b/ErsatzTV.FFmpeg/OutputFormat/OutputFormatConcatHls.cs index d63ed4610..2b73a8484 100644 --- a/ErsatzTV.FFmpeg/OutputFormat/OutputFormatConcatHls.cs +++ b/ErsatzTV.FFmpeg/OutputFormat/OutputFormatConcatHls.cs @@ -18,21 +18,38 @@ public class OutputFormatConcatHls : IPipelineStep public string[] InputOptions(InputFile inputFile) => Array.Empty(); public string[] FilterOptions => Array.Empty(); - public string[] OutputOptions => - [ - //"-g", $"{gop}", - //"-keyint_min", $"{FRAME_RATE * OutputFormatHls.SegmentSeconds}", - "-force_key_frames", $"expr:gte(t,n_forced*{OutputFormatHls.SegmentSeconds}/2)", - "-f", "hls", - //"-hls_segment_type", "fmp4", - //"-hls_init_time", "2", - "-hls_time", $"{OutputFormatHls.SegmentSeconds}", - "-hls_list_size", "25", // burst of 45 means ~12 segments, so allow that plus a handful - "-segment_list_flags", "+live", - "-hls_segment_filename", _segmentTemplate, - "-hls_flags", "delete_segments+program_date_time+omit_endlist+discont_start+independent_segments", - _playlistPath - ]; + public string[] OutputOptions + { + get + { + string segmentType = "mpegts"; + string hlsFlags = "delete_segments+program_date_time+omit_endlist+discont_start+independent_segments"; + + // check for fmp4 output + if (_segmentTemplate.Contains("m4s")) + { + segmentType = "fmp4"; + hlsFlags = "delete_segments+program_date_time+omit_endlist"; + } + + return + [ + //"-g", $"{gop}", + //"-keyint_min", $"{FRAME_RATE * OutputFormatHls.SegmentSeconds}", + "-force_key_frames", $"expr:gte(t,n_forced*{OutputFormatHls.SegmentSeconds}/2)", + "-f", "hls", + "-hls_segment_type", segmentType, + //"-hls_init_time", "2", + "-hls_playlist_type", "event", + "-hls_time", $"{OutputFormatHls.SegmentSeconds}", + "-hls_list_size", "25", // burst of 45 means ~12 segments, so allow that plus a handful + "-segment_list_flags", "+live", + "-hls_segment_filename", _segmentTemplate, + "-hls_flags", hlsFlags, + _playlistPath + ]; + } + } public FrameState NextState(FrameState currentState) => currentState; } diff --git a/ErsatzTV/Startup.cs b/ErsatzTV/Startup.cs index 537ad695e..53b1201ed 100644 --- a/ErsatzTV/Startup.cs +++ b/ErsatzTV/Startup.cs @@ -529,9 +529,9 @@ public class Startup ChannelWriter writer = app.ApplicationServices .GetRequiredService>(); writer.TryWrite(new TouchFFmpegSession(ctx.File.PhysicalPath)); - } + }, // to serve m4s - // ServeUnknownFileTypes = true + ServeUnknownFileTypes = true }); app.MapWhen(