Example #1
0
        private static IEnumerable <StreamFile> BuildAudioCommands(IEnumerable <MediaStream> streams, ICollection <string> additionalFlags, string outDirectory, string outFilename)
        {
            additionalFlags = additionalFlags ?? new List <string>();

            var output = new List <StreamFile>();

            foreach (var stream in streams)
            {
                bool   codecSupported = SupportedCodecs.ContainsKey(stream.codec_name);
                string language       = stream.tag.Where(x => x.key == "language").Select(x => x.value).FirstOrDefault() ?? ((stream.disposition.@default > 0) ? "default" : "und");
                string path           = Path.Combine(outDirectory, $"{outFilename}_audio_{language}_{stream.index}.mp4");
                string codec          = codecSupported ? "" : $"-c:a aac -b:a {stream.bit_rate * 1.1}";

                var command = new StreamFile
                {
                    Type     = StreamType.Audio,
                    Origin   = stream.index,
                    Name     = language,
                    Path     = path,
                    Argument = $"-map 0:{stream.index} " + string.Join(" ", additionalFlags.Concat(new string[]
                    {
                        codec,
                        '"' + path + '"'
                    }))
                };

                output.Add(command);
            }
            return(output);
        }
Example #2
0
        private static IEnumerable <StreamFile> BuildVideoCommands(IEnumerable <MediaStream> streams, IEnumerable <IQuality> qualities, ICollection <string> additionalFlags, int framerate, int keyframeInterval, int defaultBitrate, bool enableStreamCopying, string outDirectory, string outFilename)
        {
            additionalFlags = additionalFlags ?? new List <string>();

            var getSize         = new Func <IQuality, string>(x => { return((x.Width == 0 || x.Height == 0) ? "" : $"-s {x.Width}x{x.Height}"); });
            var getBitrate      = new Func <int, string>(x => { return((x == 0) ? "" : $"-b:v {x}k"); });
            var getPreset       = new Func <string, string>(x => { return((string.IsNullOrEmpty(x)) ? "" : $"-preset {x}"); });
            var getProfile      = new Func <string, string>(x => { return((string.IsNullOrEmpty(x)) ? "" : $"-profile:v {x}"); });
            var getProfileLevel = new Func <string, string>(x => { return((string.IsNullOrEmpty(x)) ? "" : $"-level {x}"); });
            var getPixelFormat  = new Func <string, string>(x => { return((string.IsNullOrEmpty(x)) ? "" : $"-pix_fmt {x}"); });
            var getFramerate    = new Func <int, string>(x => { return((x == 0) ? "" : $"-r {x}"); });
            var getFilename     = new Func <string, string, int, string>((path, filename, bitrate) => { return(Path.Combine(path, $"{filename}_{(bitrate == 0 ? "original" : bitrate.ToString())}.mp4")); });

            var getVideoCodec = new Func <string, bool, int, string>((sourceCodec, enableCopy, keyInterval) =>
            {
                string defaultCoding = $"-x264-params keyint={keyframeInterval}:scenecut=0";
                switch (sourceCodec)
                {
                case "h264":
                    return($"-vcodec {(enableCopy ? "copy" : "libx264")} {defaultCoding}");

                default:
                    return($"-vcodec libx264 {defaultCoding}");
                }
            });

            var output = new List <StreamFile>();

            foreach (var stream in streams)
            {
                foreach (var quality in qualities)
                {
                    string path           = getFilename(outDirectory, outFilename, quality.Bitrate);
                    bool   copyThisStream = enableStreamCopying && quality.Bitrate == 0;
                    var    command        = new StreamFile
                    {
                        Type     = StreamType.Video,
                        Origin   = stream.index,
                        Name     = quality.Bitrate.ToString(),
                        Path     = path,
                        Argument = $"-map 0:{stream.index} " + string.Join(" ", additionalFlags.Concat(new string[]
                        {
                            copyThisStream ? "" : getSize(quality),
                            copyThisStream ? "" : getBitrate(quality.Bitrate == 0 ? defaultBitrate : quality.Bitrate),
                            copyThisStream ? "" : getPreset(quality.Preset),
                            copyThisStream ? "" : getProfile(quality.Profile),
                            copyThisStream ? "" : getProfileLevel(quality.Level),
                            copyThisStream ? "" : getPixelFormat(quality.PixelFormat),
                            getFramerate(framerate),
                            getVideoCodec(stream.codec_name, copyThisStream, keyframeInterval),
                            '"' + path + '"'
                        }))
                    };

                    output.Add(command);
                }
            }
            return(output);
        }
Example #3
0
        private static IEnumerable <StreamFile> BuildSubtitleCommands(IEnumerable <MediaStream> streams, string outDirectory, string outFilename)
        {
            var supportedCodecs = new List <string>()
            {
                "webvtt",
                "ass",
                "mov_text",
                "subrip",
                "text"
            };

            var output = new List <StreamFile>();

            foreach (var stream in streams)
            {
                if (!supportedCodecs.Contains(stream.codec_name))
                {
                    continue;
                }
                string language = stream.tag.Where(x => x.key == "language").Select(x => x.value).FirstOrDefault() ?? "und";
                string path     = Path.Combine(outDirectory, $"{outFilename}_subtitle_{language}_{stream.index}.vtt");

                var command = new StreamFile
                {
                    Type     = StreamType.Subtitle,
                    Origin   = stream.index,
                    Name     = language,
                    Path     = path,
                    Argument = string.Join(" ", new string[]
                    {
                        $"-map 0:{stream.index}",
                        '"' + path + '"'
                    })
                };

                output.Add(command);
            }
            return(output);
        }
Example #4
0
        /// <summary>
        /// Converts the input file into an MPEG DASH representation with multiple bitrates.
        /// </summary>
        /// <param name="inFile">The video file to convert.</param>
        /// <param name="outFilename">The base filename to use for the output files. Files will be overwritten if they exist.</param>
        /// <param name="framerate">Output video stream framerate. Pass zero to make this automatic based on the input file.</param>
        /// <param name="keyframeInterval">Output video keyframe interval. Pass zero to make this automatically 3x the framerate.</param>
        /// <param name="qualities">Parameters to pass to ffmpeg when performing the preparation encoding. Bitrates must be distinct, an exception will be thrown if they are not.</param>
        /// <param name="options">Options for the ffmpeg encode.</param>
        /// <param name="outDirectory">The directory to place output files and intermediary files in.</param>
        /// <param name="progress">A callback for progress events. The collection will contain values with the Name property of "Encode", "DASHify", "Post Process"</param>
        /// <param name="cancel">Allows cancellation of the operation.</param>
        /// <returns>An object containing a representation of the generated MPD file, it's path, and the associated filenames, or null if no file generated.</returns>
        public DashEncodeResult GenerateDash(string inFile, string outFilename, int framerate, int keyframeInterval,
                                             IEnumerable <IQuality> qualities, IEncodeOptions options = null, string outDirectory = null, IProgress <IEnumerable <EncodeStageProgress> > progress = null, CancellationToken cancel = default(CancellationToken))
        {
            cancel.ThrowIfCancellationRequested();

            options      = options ?? new H264EncodeOptions();
            outDirectory = outDirectory ?? WorkingDirectory;

            // Input validation.
            if (inFile == null || !File.Exists(inFile))
            {
                throw new FileNotFoundException("Input path does not exist.");
            }
            if (!Directory.Exists(outDirectory))
            {
                throw new DirectoryNotFoundException("Output directory does not exist.");
            }
            if (string.IsNullOrEmpty(outFilename))
            {
                throw new ArgumentNullException("Output filename is null or empty.");
            }
            if (qualities == null || qualities.Count() == 0)
            {
                throw new ArgumentOutOfRangeException("No qualitied specified. At least one quality is required.");
            }

            // Check for invalid characters and remove them.
            outFilename = RemoveSymbols(outFilename, '#', '&', '*', '<', '>', '/', '?', ':', '"');
            // Another check to ensure we didn't remove all the characters.
            if (outFilename.Length == 0)
            {
                throw new ArgumentNullException("Output filename is null or empty.");
            }

            // Check bitrate distinction.
            if (qualities.GroupBy(x => x.Bitrate).Count() != qualities.Count())
            {
                throw new ArgumentOutOfRangeException("Duplicate bitrates found. Bitrates must be distinct.");
            }

            var inputStats = ProbeFile(inFile);

            if (inputStats == null)
            {
                throw new NullReferenceException("ffprobe query returned a null result.");
            }
            int inputBitrate = (int)(inputStats.Bitrate / 1024);

            if (!DisableQualityCrushing)
            {
                qualities = QualityCrusher.CrushQualities(qualities, inputBitrate);
            }
            var  compareQuality   = qualities.First();
            bool enableStreamCopy = EnableStreamCopying && compareQuality.Bitrate == 0 &&
                                    Copyable264Infer.DetermineCopyCanBeDone(compareQuality.PixelFormat, compareQuality.Level, compareQuality.Profile, inputStats.VideoStreams);

            var progressList = new List <EncodeStageProgress>()
            {
                new EncodeStageProgress("Encode", 0),
                new EncodeStageProgress("DASHify", 0),
                new EncodeStageProgress("Post Process", 0)
            };
            const int encodeStage = 0;
            const int dashStage   = 1;
            const int postStage   = 2;

            var stdErrShim = stderrLog;

            if (progress != null)
            {
                stdErrShim = new Action <string>(x =>
                {
                    stderrLog(x);
                    if (x != null)
                    {
                        var match = Encode.Regexes.ParseProgress.Match(x);
                        if (match.Success && TimeSpan.TryParse(match.Value, out TimeSpan p))
                        {
                            ReportProgress(progress, progressList, encodeStage, Math.Min(1, (float)(p.TotalMilliseconds / 1000) / inputStats.Duration));
                        }
                    }
                });
            }

            framerate        = framerate <= 0 ? (int)Math.Round(inputStats.Framerate) : framerate;
            keyframeInterval = keyframeInterval <= 0 ? framerate * 3 : keyframeInterval;

            // Build task definitions.
            var ffmpegCommand = CommandBuilder.BuildFfmpegCommand(
                inPath: inFile,
                outDirectory: WorkingDirectory,
                outFilename: outFilename,
                options: options,
                framerate: framerate,
                keyframeInterval: keyframeInterval,
                qualities: qualities.OrderByDescending(x => x.Bitrate),
                metadata: inputStats,
                defaultBitrate: inputBitrate,
                enableStreamCopying: enableStreamCopy);

            cancel.ThrowIfCancellationRequested();

            // Generate intermediates
            try
            {
                ExecutionResult ffResult;
                stderrLog.Invoke($"Running ffmpeg with arguments: {ffmpegCommand.RenderedCommand}");
                ffResult = ManagedExecution.Start(FFmpegPath, ffmpegCommand.RenderedCommand, stdoutLog, stdErrShim, cancel);

                // Detect error in ffmpeg process and cleanup, then return null.
                if (ffResult.ExitCode != 0)
                {
                    stderrLog.Invoke($"ERROR: ffmpeg returned code {ffResult.ExitCode}. File: {inFile}");
                    CleanOutputFiles(ffmpegCommand.CommandPieces.Select(x => x.Path));
                    return(null);
                }
            }
            catch (Exception ex)
            {
                CleanOutputFiles(ffmpegCommand.CommandPieces.Select(x => x.Path));

                if (ex is OperationCanceledException)
                {
                    throw new OperationCanceledException($"Exception running ffmpeg on {inFile}", ex);
                }
                else
                {
                    throw new Exception($"Exception running ffmpeg on {inFile}", ex);
                }
            }


            var audioVideoFiles = ffmpegCommand.CommandPieces.Where(x => x.Type == StreamType.Video || x.Type == StreamType.Audio);

            var mp4boxCommand = CommandBuilder.BuildMp4boxMpdCommand(
                inFiles: audioVideoFiles.Select(x => x.Path),
                outFilePath: Path.Combine(outDirectory, outFilename) + ".mpd",
                keyInterval: (keyframeInterval / framerate) * 1000);

            // Generate DASH files.
            ExecutionResult mpdResult;

            stderrLog.Invoke($"Running MP4Box with arguments: {mp4boxCommand.RenderedCommand}");
            try
            {
                mpdResult = ManagedExecution.Start(BoxPath, mp4boxCommand.RenderedCommand, stdoutLog, stderrLog, cancel);
            }
            catch (Exception ex)
            {
                CleanOutputFiles(audioVideoFiles.Select(x => x.Path));

                if (ex is OperationCanceledException)
                {
                    throw new OperationCanceledException($"Exception running MP4box on {inFile}", ex);
                }
                else
                {
                    throw new Exception($"Exception running MP4box on {inFile}", ex);
                }
            }

            // Report DASH complete.
            if (mpdResult.ExitCode == 0)
            {
                ReportProgress(progress, progressList, dashStage, 1);
            }

            // Cleanup intermediates.
            CleanOutputFiles(audioVideoFiles.Select(x => x.Path));

            ReportProgress(progress, progressList, postStage, 0.33);

            // Move subtitles found in media
            List <StreamFile> subtitles = new List <StreamFile>();

            foreach (var subFile in ffmpegCommand.CommandPieces.Where(x => x.Type == StreamType.Subtitle))
            {
                string oldPath = subFile.Path;
                subFile.Path = Path.Combine(outDirectory, Path.GetFileName(subFile.Path));
                subtitles.Add(subFile);
                if (oldPath != subFile.Path)
                {
                    if (File.Exists(subFile.Path))
                    {
                        File.Delete(subFile.Path);
                    }
                    File.Move(oldPath, subFile.Path);
                }
            }
            // Add external subtitles
            int    originIndex  = ffmpegCommand.CommandPieces.Max(x => x.Origin) + 1;
            string baseFilename = Path.GetFileNameWithoutExtension(inFile);

            foreach (var vttFile in Directory.EnumerateFiles(Path.GetDirectoryName(inFile), baseFilename + "*", SearchOption.TopDirectoryOnly))
            {
                if (vttFile.EndsWith(".vtt"))
                {
                    string vttFilename   = Path.GetFileName(vttFile);
                    string vttName       = GetSubtitleName(vttFilename);
                    string vttOutputPath = Path.Combine(outDirectory, $"{outFilename}_subtitle_{vttName}_{originIndex}.vtt");

                    var subFile = new StreamFile()
                    {
                        Type   = StreamType.Subtitle,
                        Origin = originIndex,
                        Path   = vttOutputPath,
                        Name   = $"{vttName}_{originIndex}"
                    };
                    originIndex++;
                    File.Copy(vttFile, vttOutputPath, true);
                    subtitles.Add(subFile);
                }
            }

            ReportProgress(progress, progressList, postStage, 0.66);

            try
            {
                string mpdFilepath = mp4boxCommand.CommandPieces.FirstOrDefault().Path;
                if (File.Exists(mpdFilepath))
                {
                    MPD mpd = PostProcessMpdFile(mpdFilepath, subtitles);

                    var result = new DashEncodeResult(mpd, inputStats.Metadata, TimeSpan.FromMilliseconds((inputStats.VideoStreams.FirstOrDefault()?.duration ?? 0) * 1000), mpdFilepath);

                    // Detect error in MP4Box process and cleanup, then return null.
                    if (mpdResult.ExitCode != 0)
                    {
                        stderrLog.Invoke($"ERROR: MP4Box returned code {mpdResult.ExitCode}. File: {inFile}");
                        CleanOutputFiles(result.MediaFiles.Select(x => Path.Combine(outDirectory, x)));
                        CleanOutputFiles(mpdResult.Output);

                        return(null);
                    }

                    // Success.
                    return(result);
                }

                stderrLog.Invoke($"ERROR: MP4Box did not produce the expected mpd file at path {mpdFilepath}. File: {inFile}");
                return(null);
            }
            finally
            {
                ReportProgress(progress, progressList, postStage, 1);
            }
        }