using ErsatzTV.FFmpeg.Environment; using ErsatzTV.FFmpeg.Format; namespace ErsatzTV.FFmpeg.OutputFormat; public class OutputFormatHls : IPipelineStep { public const int SegmentSeconds = 4; public const int KeyframeIntervalSeconds = 2; private readonly FrameState _desiredState; private readonly bool _isFirstTranscode; private readonly bool _isTroubleshooting; private readonly Option _mediaFrameRate; private readonly OutputFormatKind _outputFormat; private readonly Option _segmentOptions; private readonly bool _oneSecondGop; private readonly string _playlistPath; private readonly string _segmentTemplate; private readonly Option _initTemplate; public OutputFormatHls( FrameState desiredState, Option mediaFrameRate, OutputFormatKind outputFormat, Option segmentOptions, string segmentTemplate, Option initTemplate, string playlistPath, bool isFirstTranscode, bool oneSecondGop, bool isTroubleshooting) { _desiredState = desiredState; _mediaFrameRate = mediaFrameRate; _outputFormat = outputFormat; _segmentOptions = segmentOptions; _segmentTemplate = segmentTemplate; _initTemplate = initTemplate; _playlistPath = playlistPath; _isFirstTranscode = isFirstTranscode; _oneSecondGop = oneSecondGop; _isTroubleshooting = isTroubleshooting; } public EnvironmentVariable[] EnvironmentVariables => []; public string[] GlobalOptions => []; public string[] InputOptions(InputFile inputFile) => []; public string[] FilterOptions => []; public string[] OutputOptions { get { FrameRate frameRate = _desiredState.FrameRate.IfNone(_mediaFrameRate.IfNone(FrameRate.DefaultFrameRate)); int gop = _oneSecondGop ? (int)Math.Round(frameRate.ParsedFrameRate) : (int)Math.Round(frameRate.ParsedFrameRate * KeyframeIntervalSeconds); List result = []; if (_desiredState.VideoFormat != VideoFormat.Copy) { result.AddRange( [ "-g", $"{gop}", "-keyint_min", $"{(int)Math.Round(frameRate.ParsedFrameRate * KeyframeIntervalSeconds)}", "-force_key_frames", $"expr:gte(t,n_forced*{KeyframeIntervalSeconds})" ]); } result.AddRange( [ "-f", "hls", "-hls_time", $"{SegmentSeconds}", "-hls_list_size", "0", "-segment_list_flags", "+live", "-hls_segment_filename", _segmentTemplate ]); var independentSegments = "+independent_segments"; switch (_outputFormat) { case OutputFormatKind.Hls: result.AddRange( [ "-hls_segment_type", "mpegts" ]); break; case OutputFormatKind.HlsMp4: result.AddRange( [ "-hls_segment_type", "fmp4", "-hls_fmp4_init_filename", _initTemplate.IfNone($"{DateTimeOffset.Now.ToUnixTimeSeconds()}_init.mp4") ]); break; } foreach (string options in _segmentOptions) { result.AddRange("-hls_segment_options", options); } string pdt = _isTroubleshooting ? string.Empty : "program_date_time+omit_endlist+"; if (_isFirstTranscode) { result.AddRange( [ "-hls_flags", $"{pdt}append_list{independentSegments}", _playlistPath ]); } else { switch (_outputFormat) { case OutputFormatKind.HlsMp4: result.AddRange( [ "-hls_flags", $"{pdt}append_list+discont_start{independentSegments}", _playlistPath ]); break; default: result.AddRange( [ "-hls_flags", $"{pdt}append_list+discont_start{independentSegments}", _playlistPath ]); break; } } return result.ToArray(); } } public FrameState NextState(FrameState currentState) => currentState; }