예제 #1
0
    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);
    }
예제 #2
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));
    }