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