Ejemplo n.º 1
0
    public FFmpegPipeline Concat(ConcatInputFile concatInputFile, FFmpegState ffmpegState)
    {
        concatInputFile.AddOption(new ConcatInputFormat());
        concatInputFile.AddOption(new RealtimeInputOption());
        concatInputFile.AddOption(new InfiniteLoopInputOption(HardwareAccelerationMode.None));

        _pipelineSteps.Add(new NoSceneDetectOutputOption(0));
        _pipelineSteps.Add(new EncoderCopyAll());

        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));
        }

        _pipelineSteps.Add(new OutputFormatMpegTs());
        _pipelineSteps.Add(new PipeProtocol());

        if (ffmpegState.SaveReport)
        {
            _pipelineSteps.Add(new FFReportVariable(_reportsFolder, concatInputFile));
        }

        return(new FFmpegPipeline(_pipelineSteps));
    }
Ejemplo n.º 2
0
 public static Option <IDecoder> ForVideoFormat(
     FFmpegState ffmpegState,
     FrameState currentState,
     FrameState desiredState,
     Option <WatermarkInputFile> watermarkInputFile,
     Option <SubtitleInputFile> subtitleInputFile,
     ILogger logger) =>
 (ffmpegState.HardwareAccelerationMode, currentState.VideoFormat,
Ejemplo n.º 3
0
 public ComplexFilter(
     FrameState currentState,
     FFmpegState ffmpegState,
     Option <VideoInputFile> maybeVideoInputFile,
     Option <AudioInputFile> maybeAudioInputFile,
     Option <WatermarkInputFile> maybeWatermarkInputFile,
     Option <SubtitleInputFile> maybeSubtitleInputFile,
     FrameSize resolution,
     string fontsDir)
 {
     _currentState            = currentState;
     _ffmpegState             = ffmpegState;
     _maybeVideoInputFile     = maybeVideoInputFile;
     _maybeAudioInputFile     = maybeAudioInputFile;
     _maybeWatermarkInputFile = maybeWatermarkInputFile;
     _maybeSubtitleInputFile  = maybeSubtitleInputFile;
     _resolution = resolution;
     _fontsDir   = fontsDir;
 }
Ejemplo n.º 4
0
    public Command ConcatChannel(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host)
    {
        var resolution = new FrameSize(channel.FFmpegProfile.Resolution.Width, channel.FFmpegProfile.Resolution.Height);

        var concatInputFile = new ConcatInputFile(
            $"http://localhost:{Settings.ListenPort}/ffmpeg/concat/{channel.Number}",
            resolution);

        var pipelineBuilder = new PipelineBuilder(
            None,
            None,
            None,
            None,
            FileSystemLayout.FFmpegReportsFolder,
            FileSystemLayout.FontsCacheFolder,
            _logger);

        FFmpegPipeline pipeline = pipelineBuilder.Concat(
            concatInputFile,
            FFmpegState.Concat(saveReports, channel.Name));

        return GetCommand(ffmpegPath, None, None, None, concatInputFile, pipeline);
    }
Ejemplo n.º 5
0
 public SubtitleHardwareUploadFilter(FrameState currentState, FFmpegState ffmpegState)
 {
     _currentState = currentState;
     _ffmpegState  = ffmpegState;
 }
 public WatermarkHardwareUploadFilter(FrameState currentState, FFmpegState ffmpegState)
 {
     _currentState = currentState;
     _ffmpegState  = ffmpegState;
 }
Ejemplo n.º 7
0
 public HardwareUploadFilter(FFmpegState ffmpegState) => _ffmpegState = ffmpegState;
Ejemplo n.º 8
0
 public SubtitlePixelFormatFilter(FFmpegState ffmpegState) => _ffmpegState = ffmpegState;
 public WatermarkPixelFormatFilter(FFmpegState ffmpegState, WatermarkState watermarkState)
 {
     _ffmpegState    = ffmpegState;
     _watermarkState = watermarkState;
 }
Ejemplo n.º 10
0
    public async Task<Command> ForPlayoutItem(
        string ffmpegPath,
        string ffprobePath,
        bool saveReports,
        Channel channel,
        MediaVersion videoVersion,
        MediaVersion audioVersion,
        string videoPath,
        string audioPath,
        List<Subtitle> subtitles,
        string preferredAudioLanguage,
        string preferredSubtitleLanguage,
        ChannelSubtitleMode subtitleMode,
        DateTimeOffset start,
        DateTimeOffset finish,
        DateTimeOffset now,
        Option<ChannelWatermark> playoutItemWatermark,
        Option<ChannelWatermark> globalWatermark,
        VaapiDriver vaapiDriver,
        string vaapiDevice,
        bool hlsRealtime,
        FillerKind fillerKind,
        TimeSpan inPoint,
        TimeSpan outPoint,
        long ptsOffset,
        Option<int> targetFramerate)
    {
        MediaStream videoStream = await _ffmpegStreamSelector.SelectVideoStream(videoVersion);
        Option<MediaStream> maybeAudioStream =
            await _ffmpegStreamSelector.SelectAudioStream(
                audioVersion,
                channel.StreamingMode,
                channel.Number,
                preferredAudioLanguage);
        Option<Subtitle> maybeSubtitle =
            await _ffmpegStreamSelector.SelectSubtitleStream(
                videoVersion,
                subtitles,
                channel.StreamingMode,
                channel.Number,
                preferredSubtitleLanguage,
                subtitleMode);

        FFmpegPlaybackSettings playbackSettings = _playbackSettingsCalculator.CalculateSettings(
            channel.StreamingMode,
            channel.FFmpegProfile,
            videoVersion,
            videoStream,
            maybeAudioStream,
            start,
            now,
            inPoint,
            outPoint,
            hlsRealtime,
            targetFramerate);

        Option<WatermarkOptions> watermarkOptions =
            await _ffmpegProcessService.GetWatermarkOptions(
                ffprobePath,
                channel,
                playoutItemWatermark,
                globalWatermark,
                videoVersion,
                None,
                None);

        Option<List<FadePoint>> maybeFadePoints = watermarkOptions
            .Map(o => o.Watermark)
            .Flatten()
            .Where(wm => wm.Mode == ChannelWatermarkMode.Intermittent)
            .Map(
                wm =>
                    WatermarkCalculator.CalculateFadePoints(
                        start,
                        inPoint,
                        outPoint,
                        playbackSettings.StreamSeek,
                        wm.FrequencyMinutes,
                        wm.DurationSeconds));

        string audioFormat = playbackSettings.AudioFormat switch
        {
            FFmpegProfileAudioFormat.Aac => AudioFormat.Aac,
            FFmpegProfileAudioFormat.Ac3 => AudioFormat.Ac3,
            FFmpegProfileAudioFormat.Copy => AudioFormat.Copy,
            _ => throw new ArgumentOutOfRangeException($"unexpected audio format {playbackSettings.VideoFormat}")
        };

        var audioState = new AudioState(
            audioFormat,
            playbackSettings.AudioChannels,
            playbackSettings.AudioBitrate,
            playbackSettings.AudioBufferSize,
            playbackSettings.AudioSampleRate,
            videoPath == audioPath ? playbackSettings.AudioDuration : Option<TimeSpan>.None,
            playbackSettings.NormalizeLoudness);

        var ffmpegVideoStream = new VideoStream(
            videoStream.Index,
            videoStream.Codec,
            AvailablePixelFormats.ForPixelFormat(videoStream.PixelFormat, _logger),
            new FrameSize(videoVersion.Width, videoVersion.Height),
            videoVersion.RFrameRate,
            videoPath != audioPath); // still image when paths are different

        var videoInputFile = new VideoInputFile(videoPath, new List<VideoStream> { ffmpegVideoStream });

        Option<AudioInputFile> audioInputFile = maybeAudioStream.Map(
            audioStream =>
            {
                var ffmpegAudioStream = new AudioStream(audioStream.Index, audioStream.Codec, audioStream.Channels);
                return new AudioInputFile(audioPath, new List<AudioStream> { ffmpegAudioStream }, audioState);
            });

        Option<SubtitleInputFile> subtitleInputFile = maybeSubtitle.Map<Option<SubtitleInputFile>>(
            subtitle =>
            {
                if (!subtitle.IsImage && subtitle.SubtitleKind == SubtitleKind.Embedded && !subtitle.IsExtracted)
                {
                    _logger.LogWarning("Subtitles are not yet available for this item");
                    return None;
                }

                var ffmpegSubtitleStream = new ErsatzTV.FFmpeg.MediaStream(
                    subtitle.IsImage ? subtitle.StreamIndex : 0,
                    subtitle.Codec,
                    StreamKind.Video);

                string path = subtitle.IsImage
                    ? videoPath
                    : Path.Combine(FileSystemLayout.SubtitleCacheFolder, subtitle.Path);

                return new SubtitleInputFile(
                    path,
                    new List<ErsatzTV.FFmpeg.MediaStream> { ffmpegSubtitleStream },
                    false);

                // TODO: figure out HLS direct
                // channel.StreamingMode == StreamingMode.HttpLiveStreamingDirect);
            }).Flatten();

        Option<WatermarkInputFile> watermarkInputFile = GetWatermarkInputFile(watermarkOptions, maybeFadePoints);

        string videoFormat = playbackSettings.VideoFormat switch
        {
            FFmpegProfileVideoFormat.Hevc => VideoFormat.Hevc,
            FFmpegProfileVideoFormat.H264 => VideoFormat.H264,
            FFmpegProfileVideoFormat.Mpeg2Video => VideoFormat.Mpeg2Video,
            FFmpegProfileVideoFormat.Copy => VideoFormat.Copy,
            _ => throw new ArgumentOutOfRangeException($"unexpected video format {playbackSettings.VideoFormat}")
        };

        HardwareAccelerationMode hwAccel = playbackSettings.HardwareAcceleration switch
        {
            HardwareAccelerationKind.Nvenc => HardwareAccelerationMode.Nvenc,
            HardwareAccelerationKind.Qsv => HardwareAccelerationMode.Qsv,
            HardwareAccelerationKind.Vaapi => HardwareAccelerationMode.Vaapi,
            HardwareAccelerationKind.VideoToolbox => HardwareAccelerationMode.VideoToolbox,
            _ => HardwareAccelerationMode.None
        };

        OutputFormatKind outputFormat = channel.StreamingMode == StreamingMode.HttpLiveStreamingSegmenter
            ? OutputFormatKind.Hls
            : OutputFormatKind.MpegTs;

        Option<string> hlsPlaylistPath = outputFormat == OutputFormatKind.Hls
            ? Path.Combine(FileSystemLayout.TranscodeFolder, channel.Number, "live.m3u8")
            : Option<string>.None;

        Option<string> hlsSegmentTemplate = outputFormat == OutputFormatKind.Hls
            ? Path.Combine(FileSystemLayout.TranscodeFolder, channel.Number, "live%06d.ts")
            : Option<string>.None;

        // normalize songs to yuv420p
        Option<IPixelFormat> desiredPixelFormat =
            videoPath == audioPath ? ffmpegVideoStream.PixelFormat : new PixelFormatYuv420P();

        var desiredState = new FrameState(
            playbackSettings.RealtimeOutput,
            false, // TODO: fallback filler needs to loop
            videoFormat,
            desiredPixelFormat,
            await playbackSettings.ScaledSize.Map(ss => new FrameSize(ss.Width, ss.Height))
                .IfNoneAsync(new FrameSize(videoVersion.Width, videoVersion.Height)),
            new FrameSize(channel.FFmpegProfile.Resolution.Width, channel.FFmpegProfile.Resolution.Height),
            playbackSettings.FrameRate,
            playbackSettings.VideoBitrate,
            playbackSettings.VideoBufferSize,
            playbackSettings.VideoTrackTimeScale,
            playbackSettings.Deinterlace);

        var ffmpegState = new FFmpegState(
            saveReports,
            hwAccel,
            VaapiDriverName(hwAccel, vaapiDriver),
            VaapiDeviceName(hwAccel, vaapiDevice),
            playbackSettings.StreamSeek,
            finish - now,
            channel.StreamingMode != StreamingMode.HttpLiveStreamingDirect,
            "ErsatzTV",
            channel.Name,
            maybeAudioStream.Map(s => Optional(s.Language)).Flatten(),
            outputFormat,
            hlsPlaylistPath,
            hlsSegmentTemplate,
            ptsOffset);

        _logger.LogDebug("FFmpeg desired state {FrameState}", desiredState);

        var pipelineBuilder = new PipelineBuilder(
            videoInputFile,
            audioInputFile,
            watermarkInputFile,
            subtitleInputFile,
            FileSystemLayout.FFmpegReportsFolder,
            FileSystemLayout.FontsCacheFolder,
            _logger);

        FFmpegPipeline pipeline = pipelineBuilder.Build(ffmpegState, desiredState);

        return GetCommand(ffmpegPath, videoInputFile, audioInputFile, watermarkInputFile, None, pipeline);
    }
Ejemplo n.º 11
0
    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));
    }