private IList <string> Arguments() { var audioLabel = "0:a"; var videoLabel = "0:v"; string watermarkLabel; string subtitleLabel; var result = new List <string>(); string audioFilterComplex = string.Empty; string videoFilterComplex = string.Empty; string watermarkFilterComplex = string.Empty; string watermarkOverlayFilterComplex = string.Empty; string subtitleFilterComplex = string.Empty; string subtitleOverlayFilterComplex = string.Empty; var distinctPaths = new List <string>(); foreach ((string path, _) in _maybeVideoInputFile) { if (!distinctPaths.Contains(path)) { distinctPaths.Add(path); } } foreach ((string path, _) in _maybeAudioInputFile) { if (!distinctPaths.Contains(path)) { distinctPaths.Add(path); } } foreach ((string path, _) in _maybeWatermarkInputFile) { if (!distinctPaths.Contains(path)) { distinctPaths.Add(path); } } foreach ((string path, _) in _maybeSubtitleInputFile) { if (!distinctPaths.Contains(path)) { distinctPaths.Add(path); } } foreach (VideoInputFile videoInputFile in _maybeVideoInputFile) { int inputIndex = distinctPaths.IndexOf(videoInputFile.Path); foreach ((int index, _, _) in videoInputFile.Streams) { videoLabel = $"{inputIndex}:{index}"; if (videoInputFile.FilterSteps.Any(f => !string.IsNullOrWhiteSpace(f.Filter))) { videoFilterComplex += $"[{inputIndex}:{index}]"; videoFilterComplex += string.Join( ",", videoInputFile.FilterSteps.Select(f => f.Filter).Filter(s => !string.IsNullOrWhiteSpace(s))); videoLabel = "[v]"; videoFilterComplex += videoLabel; } } } foreach (AudioInputFile audioInputFile in _maybeAudioInputFile) { int inputIndex = distinctPaths.IndexOf(audioInputFile.Path); foreach ((int index, _, _) in audioInputFile.Streams) { audioLabel = $"{inputIndex}:{index}"; if (audioInputFile.FilterSteps.Any(f => !string.IsNullOrWhiteSpace(f.Filter))) { audioFilterComplex += $"[{inputIndex}:{index}]"; audioFilterComplex += string.Join( ",", audioInputFile.FilterSteps.Select(f => f.Filter).Filter(s => !string.IsNullOrWhiteSpace(s))); audioLabel = "[a]"; audioFilterComplex += audioLabel; } } } foreach (WatermarkInputFile watermarkInputFile in _maybeWatermarkInputFile) { int inputIndex = distinctPaths.IndexOf(watermarkInputFile.Path); foreach ((int index, _, _) in watermarkInputFile.Streams) { watermarkLabel = $"{inputIndex}:{index}"; if (watermarkInputFile.FilterSteps.Any(f => !string.IsNullOrWhiteSpace(f.Filter))) { watermarkFilterComplex += $"[{inputIndex}:{index}]"; watermarkFilterComplex += string.Join( ",", watermarkInputFile.FilterSteps.Select(f => f.Filter) .Filter(s => !string.IsNullOrWhiteSpace(s))); watermarkLabel = "[wm]"; watermarkFilterComplex += watermarkLabel; } else { watermarkLabel = $"[{watermarkLabel}]"; } IPipelineFilterStep overlayFilter = AvailableWatermarkOverlayFilters.ForAcceleration( _ffmpegState.HardwareAccelerationMode, _currentState, watermarkInputFile.DesiredState, _resolution); if (overlayFilter.Filter != string.Empty) { string tempVideoLabel = string.IsNullOrWhiteSpace(videoFilterComplex) ? $"[{videoLabel}]" : videoLabel; // vaapi uses software overlay and needs to upload // videotoolbox seems to require a hwupload for hevc // also wait to upload if a subtitle overlay is coming string uploadDownloadFilter = string.Empty; if (_maybeSubtitleInputFile.IsNone && (_ffmpegState.HardwareAccelerationMode == HardwareAccelerationMode.Vaapi || _ffmpegState.HardwareAccelerationMode == HardwareAccelerationMode.VideoToolbox && _currentState.VideoFormat == VideoFormat.Hevc)) { uploadDownloadFilter = new HardwareUploadFilter(_ffmpegState).Filter; } if (_maybeSubtitleInputFile.Map(s => !s.IsImageBased).IfNone(false) && _ffmpegState.HardwareAccelerationMode != HardwareAccelerationMode.Vaapi && _ffmpegState.HardwareAccelerationMode != HardwareAccelerationMode.VideoToolbox) { uploadDownloadFilter = new HardwareDownloadFilter(_currentState).Filter; } if (!string.IsNullOrWhiteSpace(uploadDownloadFilter)) { uploadDownloadFilter = "," + uploadDownloadFilter; } watermarkOverlayFilterComplex = $"{tempVideoLabel}{watermarkLabel}{overlayFilter.Filter}{uploadDownloadFilter}[vf]"; // change the mapped label videoLabel = "[vf]"; } } } foreach (SubtitleInputFile subtitleInputFile in _maybeSubtitleInputFile.Filter(s => !s.Copy)) { int inputIndex = distinctPaths.IndexOf(subtitleInputFile.Path); foreach ((int index, _, _) in subtitleInputFile.Streams) { subtitleLabel = $"{inputIndex}:{index}"; if (subtitleInputFile.FilterSteps.Any(f => !string.IsNullOrWhiteSpace(f.Filter))) { subtitleFilterComplex += $"[{inputIndex}:{index}]"; subtitleFilterComplex += string.Join( ",", subtitleInputFile.FilterSteps.Select(f => f.Filter).Filter(s => !string.IsNullOrWhiteSpace(s))); subtitleLabel = "[st]"; subtitleFilterComplex += subtitleLabel; } else { subtitleLabel = $"[{subtitleLabel}]"; } string filter; if (subtitleInputFile.IsImageBased) { IPipelineFilterStep overlayFilter = AvailableSubtitleOverlayFilters.ForAcceleration( _ffmpegState.HardwareAccelerationMode, _currentState); filter = overlayFilter.Filter; } else { subtitleLabel = string.Empty; var subtitlesFilter = new SubtitlesFilter(_fontsDir, subtitleInputFile); filter = subtitlesFilter.Filter; } if (filter != string.Empty) { string tempVideoLabel = string.IsNullOrWhiteSpace(videoFilterComplex) && string.IsNullOrWhiteSpace(watermarkFilterComplex) ? $"[{videoLabel}]" : videoLabel; // vaapi uses software overlay and needs to upload // videotoolbox seems to require a hwupload for hevc string uploadFilter = string.Empty; if (_ffmpegState.HardwareAccelerationMode == HardwareAccelerationMode.Vaapi || _ffmpegState.HardwareAccelerationMode == HardwareAccelerationMode.VideoToolbox && _currentState.VideoFormat == VideoFormat.Hevc) { uploadFilter = new HardwareUploadFilter(_ffmpegState).Filter; } if (!string.IsNullOrWhiteSpace(uploadFilter)) { uploadFilter = "," + uploadFilter; } subtitleOverlayFilterComplex = $"{tempVideoLabel}{subtitleLabel}{filter}{uploadFilter}[vst]"; // change the mapped label videoLabel = "[vst]"; } } } if (!string.IsNullOrWhiteSpace(audioFilterComplex) || !string.IsNullOrWhiteSpace(videoFilterComplex)) { var filterComplex = string.Join( ";", new[] { audioFilterComplex, videoFilterComplex, watermarkFilterComplex, subtitleFilterComplex, watermarkOverlayFilterComplex, subtitleOverlayFilterComplex }.Where( s => !string.IsNullOrWhiteSpace(s))); result.AddRange(new[] { "-filter_complex", filterComplex }); } result.AddRange(new[] { "-map", audioLabel, "-map", videoLabel }); foreach (SubtitleInputFile subtitleInputFile in _maybeSubtitleInputFile.Filter(s => s.Copy)) { int inputIndex = distinctPaths.IndexOf(subtitleInputFile.Path); foreach ((int index, _, _) in subtitleInputFile.Streams) { subtitleLabel = $"{inputIndex}:{index}"; result.AddRange(new[] { "-map", subtitleLabel }); } } return(result); }
public FFmpegPipeline Build(FFmpegState ffmpegState, FrameState desiredState) { var allVideoStreams = _videoInputFile.SelectMany(f => f.VideoStreams).ToList(); // -sc_threshold 0 is unsupported with mpeg2video _pipelineSteps.Add( allVideoStreams.All(s => s.Codec != VideoFormat.Mpeg2Video) && desiredState.VideoFormat != VideoFormat.Mpeg2Video ? new NoSceneDetectOutputOption(0) : new NoSceneDetectOutputOption(1_000_000_000)); if (ffmpegState.SaveReport) { _pipelineSteps.Add(new FFReportVariable(_reportsFolder, None)); } foreach (TimeSpan desiredStart in ffmpegState.Start.Filter(s => s > TimeSpan.Zero)) { var option = new StreamSeekInputOption(desiredStart); _audioInputFile.Iter(f => f.AddOption(option)); _videoInputFile.Iter(f => f.AddOption(option)); // need to seek text subtitle files if (_subtitleInputFile.Map(s => !s.IsImageBased).IfNone(false)) { _pipelineSteps.Add(new StreamSeekFilterOption(desiredStart)); } } foreach (TimeSpan desiredFinish in ffmpegState.Finish) { _pipelineSteps.Add(new TimeLimitOutputOption(desiredFinish)); } foreach (VideoStream videoStream in allVideoStreams) { bool hasOverlay = _watermarkInputFile.IsSome || _subtitleInputFile.Map(s => s.IsImageBased && !s.Copy).IfNone(false); Option <int> initialFrameRate = Option <int> .None; foreach (string frameRateString in videoStream.FrameRate) { if (int.TryParse(frameRateString, out int parsedFrameRate)) { initialFrameRate = parsedFrameRate; } } var currentState = new FrameState( false, // realtime false, // infinite loop videoStream.Codec, videoStream.PixelFormat, videoStream.FrameSize, videoStream.FrameSize, initialFrameRate, Option <int> .None, Option <int> .None, Option <int> .None, false); // deinterlace IEncoder encoder; if (IsDesiredVideoState(currentState, desiredState)) { encoder = new EncoderCopyVideo(); _pipelineSteps.Add(encoder); } else { Option <IPipelineStep> maybeAccel = AvailableHardwareAccelerationOptions.ForMode( ffmpegState.HardwareAccelerationMode, ffmpegState.VaapiDevice, _logger); if (maybeAccel.IsNone) { ffmpegState = ffmpegState with { // disable hw accel if we don't match anything HardwareAccelerationMode = HardwareAccelerationMode.None }; } foreach (IPipelineStep accel in maybeAccel) { currentState = accel.NextState(currentState); _pipelineSteps.Add(accel); } // nvenc requires yuv420p background with yuva420p overlay if (ffmpegState.HardwareAccelerationMode == HardwareAccelerationMode.Nvenc && hasOverlay) { desiredState = desiredState with { PixelFormat = new PixelFormatYuv420P() }; } // qsv should stay nv12 if (ffmpegState.HardwareAccelerationMode == HardwareAccelerationMode.Qsv && hasOverlay) { IPixelFormat pixelFormat = desiredState.PixelFormat.IfNone(new PixelFormatYuv420P()); desiredState = desiredState with { PixelFormat = new PixelFormatNv12(pixelFormat.Name) }; } foreach (string desiredVaapiDriver in ffmpegState.VaapiDriver) { IPipelineStep step = new LibvaDriverNameVariable(desiredVaapiDriver); currentState = step.NextState(currentState); _pipelineSteps.Add(step); } foreach (IDecoder decoder in AvailableDecoders.ForVideoFormat( ffmpegState, currentState, desiredState, _watermarkInputFile, _subtitleInputFile, _logger)) { foreach (VideoInputFile videoInputFile in _videoInputFile) { videoInputFile.AddOption(decoder); currentState = decoder.NextState(currentState); } } } if (_subtitleInputFile.Map(s => s.Copy) == Some(true)) { _pipelineSteps.Add(new EncoderCopySubtitle()); } if (videoStream.StillImage) { var option = new InfiniteLoopInputOption(ffmpegState.HardwareAccelerationMode); _videoInputFile.Iter(f => f.AddOption(option)); } if (!IsDesiredVideoState(currentState, desiredState)) { if (desiredState.Realtime) { var option = new RealtimeInputOption(); _audioInputFile.Iter(f => f.AddOption(option)); _videoInputFile.Iter(f => f.AddOption(option)); } if (desiredState.InfiniteLoop) { var option = new InfiniteLoopInputOption(ffmpegState.HardwareAccelerationMode); _audioInputFile.Iter(f => f.AddOption(option)); _videoInputFile.Iter(f => f.AddOption(option)); } foreach (int desiredFrameRate in desiredState.FrameRate) { if (currentState.FrameRate != desiredFrameRate) { IPipelineStep step = new FrameRateOutputOption(desiredFrameRate); currentState = step.NextState(currentState); _pipelineSteps.Add(step); } } foreach (int desiredTimeScale in desiredState.VideoTrackTimeScale) { if (currentState.VideoTrackTimeScale != desiredTimeScale) { IPipelineStep step = new VideoTrackTimescaleOutputOption(desiredTimeScale); currentState = step.NextState(currentState); _pipelineSteps.Add(step); } } foreach (int desiredBitrate in desiredState.VideoBitrate) { if (currentState.VideoBitrate != desiredBitrate) { IPipelineStep step = new VideoBitrateOutputOption(desiredBitrate); currentState = step.NextState(currentState); _pipelineSteps.Add(step); } } foreach (int desiredBufferSize in desiredState.VideoBufferSize) { if (currentState.VideoBufferSize != desiredBufferSize) { IPipelineStep step = new VideoBufferSizeOutputOption(desiredBufferSize); currentState = step.NextState(currentState); _pipelineSteps.Add(step); } } if (desiredState.Deinterlaced && !currentState.Deinterlaced) { IPipelineFilterStep step = AvailableDeinterlaceFilters.ForAcceleration( ffmpegState.HardwareAccelerationMode, currentState, desiredState, _watermarkInputFile, _subtitleInputFile); currentState = step.NextState(currentState); _videoInputFile.Iter(f => f.FilterSteps.Add(step)); } // TODO: this is a software-only flow, will need to be different for hardware accel if (ffmpegState.HardwareAccelerationMode == HardwareAccelerationMode.None) { if (currentState.ScaledSize != desiredState.ScaledSize || currentState.PaddedSize != desiredState.PaddedSize) { IPipelineFilterStep scaleStep = new ScaleFilter( currentState, desiredState.ScaledSize, desiredState.PaddedSize); currentState = scaleStep.NextState(currentState); _videoInputFile.Iter(f => f.FilterSteps.Add(scaleStep)); // TODO: padding might not be needed, can we optimize this out? IPipelineFilterStep padStep = new PadFilter(currentState, desiredState.PaddedSize); currentState = padStep.NextState(currentState); _videoInputFile.Iter(f => f.FilterSteps.Add(padStep)); IPipelineFilterStep sarStep = new SetSarFilter(); currentState = sarStep.NextState(currentState); _videoInputFile.Iter(f => f.FilterSteps.Add(sarStep)); } } else if (currentState.ScaledSize != desiredState.ScaledSize) { IPipelineFilterStep scaleFilter = AvailableScaleFilters.ForAcceleration( ffmpegState.HardwareAccelerationMode, currentState, desiredState.ScaledSize, desiredState.PaddedSize); currentState = scaleFilter.NextState(currentState); _videoInputFile.Iter(f => f.FilterSteps.Add(scaleFilter)); // TODO: padding might not be needed, can we optimize this out? if (currentState.PaddedSize != desiredState.PaddedSize) { IPipelineFilterStep padStep = new PadFilter(currentState, desiredState.PaddedSize); currentState = padStep.NextState(currentState); _videoInputFile.Iter(f => f.FilterSteps.Add(padStep)); } IPipelineFilterStep sarStep = new SetSarFilter(); currentState = sarStep.NextState(currentState); _videoInputFile.Iter(f => f.FilterSteps.Add(sarStep)); } else if (currentState.PaddedSize != desiredState.PaddedSize) { IPipelineFilterStep scaleFilter = AvailableScaleFilters.ForAcceleration( ffmpegState.HardwareAccelerationMode, currentState, desiredState.ScaledSize, desiredState.PaddedSize); currentState = scaleFilter.NextState(currentState); _videoInputFile.Iter(f => f.FilterSteps.Add(scaleFilter)); if (currentState.PaddedSize != desiredState.PaddedSize) { IPipelineFilterStep padStep = new PadFilter(currentState, desiredState.PaddedSize); currentState = padStep.NextState(currentState); _videoInputFile.Iter(f => f.FilterSteps.Add(padStep)); } IPipelineFilterStep sarStep = new SetSarFilter(); currentState = sarStep.NextState(currentState); _videoInputFile.Iter(f => f.FilterSteps.Add(sarStep)); } if (hasOverlay && currentState.PixelFormat.Map(pf => pf.FFmpegName) != desiredState.PixelFormat.Map(pf => pf.FFmpegName)) { // this should only happen with nvenc? // use scale filter to fix pixel format foreach (IPixelFormat pixelFormat in desiredState.PixelFormat) { if (currentState.FrameDataLocation == FrameDataLocation.Software) { IPipelineFilterStep formatFilter = new PixelFormatFilter(pixelFormat); currentState = formatFilter.NextState(currentState); _videoInputFile.Iter(f => f.FilterSteps.Add(formatFilter)); switch (ffmpegState.HardwareAccelerationMode) { case HardwareAccelerationMode.Nvenc: var uploadFilter = new HardwareUploadFilter(ffmpegState); currentState = uploadFilter.NextState(currentState); _videoInputFile.Iter(f => f.FilterSteps.Add(uploadFilter)); break; } } else { if (ffmpegState.HardwareAccelerationMode != HardwareAccelerationMode.Qsv) { // the filter re-applies the current pixel format, so we have to set it first currentState = currentState with { PixelFormat = desiredState.PixelFormat }; IPipelineFilterStep scaleFilter = AvailableScaleFilters.ForAcceleration( ffmpegState.HardwareAccelerationMode, currentState, desiredState.ScaledSize, desiredState.PaddedSize); currentState = scaleFilter.NextState(currentState); _videoInputFile.Iter(f => f.FilterSteps.Add(scaleFilter)); } } } } // nvenc custom logic if (ffmpegState.HardwareAccelerationMode == HardwareAccelerationMode.Nvenc) { foreach (VideoInputFile videoInputFile in _videoInputFile) { // if we only deinterlace, we need to set pixel format again (using scale_cuda) bool onlyYadif = videoInputFile.FilterSteps.Count == 1 && videoInputFile.FilterSteps.Any(fs => fs is YadifCudaFilter); // if we have no filters and an overlay, we need to set pixel format bool unfilteredWithOverlay = videoInputFile.FilterSteps.Count == 0 && hasOverlay; if (onlyYadif || unfilteredWithOverlay) { // the filter re-applies the current pixel format, so we have to set it first currentState = currentState with { PixelFormat = desiredState.PixelFormat }; IPipelineFilterStep scaleFilter = AvailableScaleFilters.ForAcceleration( ffmpegState.HardwareAccelerationMode, currentState, desiredState.ScaledSize, desiredState.PaddedSize); currentState = scaleFilter.NextState(currentState); videoInputFile.FilterSteps.Add(scaleFilter); } } } if (ffmpegState.PtsOffset > 0) { foreach (int videoTrackTimeScale in desiredState.VideoTrackTimeScale) { IPipelineStep step = new OutputTsOffsetOption( ffmpegState.PtsOffset, videoTrackTimeScale); currentState = step.NextState(currentState); _pipelineSteps.Add(step); } } foreach (IPixelFormat desiredPixelFormat in desiredState.PixelFormat) { if (currentState.PixelFormat.Map(pf => pf.FFmpegName) != desiredPixelFormat.FFmpegName) { // qsv doesn't seem to like this if (ffmpegState.HardwareAccelerationMode != HardwareAccelerationMode.Qsv) { IPipelineStep step = new PixelFormatOutputOption(desiredPixelFormat); currentState = step.NextState(currentState); _pipelineSteps.Add(step); } } } } // TODO: if all video filters are software, use software pixel format for hwaccel output // might be able to skip scale_cuda=format=whatever,hwdownload,format=whatever foreach (AudioInputFile audioInputFile in _audioInputFile) { // always need to specify audio codec so ffmpeg doesn't default to a codec we don't want foreach (IEncoder step in AvailableEncoders.ForAudioFormat(audioInputFile.DesiredState, _logger)) { currentState = step.NextState(currentState); _pipelineSteps.Add(step); } foreach (int desiredAudioChannels in audioInputFile.DesiredState.AudioChannels) { _pipelineSteps.Add(new AudioChannelsOutputOption(desiredAudioChannels)); } foreach (int desiredBitrate in audioInputFile.DesiredState.AudioBitrate) { _pipelineSteps.Add(new AudioBitrateOutputOption(desiredBitrate)); } foreach (int desiredBufferSize in audioInputFile.DesiredState.AudioBufferSize) { _pipelineSteps.Add(new AudioBufferSizeOutputOption(desiredBufferSize)); } foreach (int desiredSampleRate in audioInputFile.DesiredState.AudioSampleRate) { _pipelineSteps.Add(new AudioSampleRateOutputOption(desiredSampleRate)); } if (audioInputFile.DesiredState.NormalizeLoudness) { _audioInputFile.Iter(f => f.FilterSteps.Add(new NormalizeLoudnessFilter())); } foreach (TimeSpan desiredDuration in audioInputFile.DesiredState.AudioDuration) { _audioInputFile.Iter(f => f.FilterSteps.Add(new AudioPadFilter(desiredDuration))); } } foreach (SubtitleInputFile subtitleInputFile in _subtitleInputFile) { if (subtitleInputFile.IsImageBased) { // vaapi and videotoolbox use a software overlay, so we need to ensure the background is already in software // though videotoolbox uses software decoders, so no need to download for that if (ffmpegState.HardwareAccelerationMode == HardwareAccelerationMode.Vaapi) { var downloadFilter = new HardwareDownloadFilter(currentState); currentState = downloadFilter.NextState(currentState); _videoInputFile.Iter(f => f.FilterSteps.Add(downloadFilter)); } subtitleInputFile.FilterSteps.Add(new SubtitlePixelFormatFilter(ffmpegState)); subtitleInputFile.FilterSteps.Add(new SubtitleHardwareUploadFilter(currentState, ffmpegState)); } else { _videoInputFile.Iter(f => f.AddOption(new CopyTimestampInputOption())); // text-based subtitles are always added in software, so always try to download the background // nvidia needs some extra format help if the only filter will be the download filter if (ffmpegState.HardwareAccelerationMode == HardwareAccelerationMode.Nvenc && _videoInputFile.Map(f => f.FilterSteps.Count).IfNone(1) == 0) { IPipelineFilterStep scaleFilter = AvailableScaleFilters.ForAcceleration( ffmpegState.HardwareAccelerationMode, currentState, desiredState.ScaledSize, desiredState.PaddedSize); currentState = scaleFilter.NextState(currentState); _videoInputFile.Iter(f => f.FilterSteps.Add(scaleFilter)); } var downloadFilter = new HardwareDownloadFilter(currentState); currentState = downloadFilter.NextState(currentState); _videoInputFile.Iter(f => f.FilterSteps.Add(downloadFilter)); } } foreach (WatermarkInputFile watermarkInputFile in _watermarkInputFile) { // vaapi and videotoolbox use a software overlay, so we need to ensure the background is already in software // though videotoolbox uses software decoders, so no need to download for that if (ffmpegState.HardwareAccelerationMode == HardwareAccelerationMode.Vaapi) { var downloadFilter = new HardwareDownloadFilter(currentState); currentState = downloadFilter.NextState(currentState); _videoInputFile.Iter(f => f.FilterSteps.Add(downloadFilter)); } watermarkInputFile.FilterSteps.Add( new WatermarkPixelFormatFilter(ffmpegState, watermarkInputFile.DesiredState)); foreach (VideoStream watermarkStream in watermarkInputFile.VideoStreams) { if (watermarkStream.StillImage == false) { watermarkInputFile.AddOption(new DoNotIgnoreLoopInputOption()); } else if (watermarkInputFile.DesiredState.MaybeFadePoints.Map(fp => fp.Count > 0).IfNone(false)) { // looping is required to fade a static image in and out watermarkInputFile.AddOption(new InfiniteLoopInputOption(ffmpegState.HardwareAccelerationMode)); } } if (watermarkInputFile.DesiredState.Size == WatermarkSize.Scaled) { watermarkInputFile.FilterSteps.Add( new WatermarkScaleFilter(watermarkInputFile.DesiredState, currentState.PaddedSize)); } if (watermarkInputFile.DesiredState.Opacity != 100) { watermarkInputFile.FilterSteps.Add(new WatermarkOpacityFilter(watermarkInputFile.DesiredState)); } foreach (List <WatermarkFadePoint> fadePoints in watermarkInputFile.DesiredState.MaybeFadePoints) { watermarkInputFile.FilterSteps.AddRange(fadePoints.Map(fp => new WatermarkFadeFilter(fp))); } watermarkInputFile.FilterSteps.Add(new WatermarkHardwareUploadFilter(currentState, ffmpegState)); } // after everything else is done, apply the encoder if (_pipelineSteps.OfType <IEncoder>().All(e => e.Kind != StreamKind.Video)) { foreach (IEncoder e in AvailableEncoders.ForVideoFormat( ffmpegState, currentState, desiredState, _watermarkInputFile, _subtitleInputFile, _logger)) { encoder = e; _pipelineSteps.Add(encoder); _videoInputFile.Iter(f => f.FilterSteps.Add(encoder)); currentState = encoder.NextState(currentState); } } if (ffmpegState.DoNotMapMetadata) { _pipelineSteps.Add(new DoNotMapMetadataOutputOption()); } foreach (string desiredServiceProvider in ffmpegState.MetadataServiceProvider) { _pipelineSteps.Add(new MetadataServiceProviderOutputOption(desiredServiceProvider)); } foreach (string desiredServiceName in ffmpegState.MetadataServiceName) { _pipelineSteps.Add(new MetadataServiceNameOutputOption(desiredServiceName)); } foreach (string desiredAudioLanguage in ffmpegState.MetadataAudioLanguage) { _pipelineSteps.Add(new MetadataAudioLanguageOutputOption(desiredAudioLanguage)); } switch (ffmpegState.OutputFormat) { case OutputFormatKind.MpegTs: _pipelineSteps.Add(new OutputFormatMpegTs()); _pipelineSteps.Add(new PipeProtocol()); // currentState = currentState with { OutputFormat = OutputFormatKind.MpegTs }; break; case OutputFormatKind.Hls: foreach (string playlistPath in ffmpegState.HlsPlaylistPath) { foreach (string segmentTemplate in ffmpegState.HlsSegmentTemplate) { var step = new OutputFormatHls( desiredState, videoStream.FrameRate, segmentTemplate, playlistPath); currentState = step.NextState(currentState); _pipelineSteps.Add(step); } } break; } var complexFilter = new ComplexFilter( currentState, ffmpegState, _videoInputFile, _audioInputFile, _watermarkInputFile, _subtitleInputFile, currentState.PaddedSize, _fontsFolder); _pipelineSteps.Add(complexFilter); } return(new FFmpegPipeline(_pipelineSteps)); }