/// <summary> /// Create a typical instance of DashEncodeResult. /// </summary> /// <param name="mpdPath">The exact path to the mpd file.</param> /// <param name="mpdContent">The exact mpd content deserialized from XML.</param> /// <param name="ffmpegCommand">The generated ffmpeg command used when </param> /// <param name="inputMetadata">Metadata about the DASHed input file.</param> public DashEncodeResult(string mpdPath, MPD mpdContent, FFmpegCommand ffmpegCommand, MediaMetadata inputMetadata) { DashFileContent = mpdContent; DashFilePath = mpdPath; FFmpegCommand = ffmpegCommand; InputMetadata = inputMetadata; }
/// <summary> /// Converts the input file into an MPEG DASH representations. /// This includes multiple bitrates, subtitle tracks, audio tracks, and an MPD manifest. /// </summary> /// <param name="config">A configuration specifying how DASHing should be performed.</param> /// <param name="probedInputData">The output from running <see cref="ProbeFile">ProbeFile</see> on the input file.</param> /// <param name="progress">Gives progress through the ffmpeg process, which takes the longest of all the parts of DASHing.</param> /// <param name="cancel">Allows the process to be ended part way through.</param> /// <returns>A value containing metadata about the artifacts of the DASHing process.</returns> /// <exception cref="ArgumentNullException">The probe data parameter is null.</exception> /// <exception cref="FFMpegFailedException">The ffmpeg process returned an error code other than 0 or threw an inner exception such as <see cref="OperationCanceledException"/>.</exception> /// <exception cref="Mp4boxFailedException">The MP4Box process returned an error code other than 0, threw an inner exception such as <see cref="OperationCanceledException"/>, or did not generate an MPD file.</exception> /// <exception cref="DashManifestNotCreatedException">Everything seemed to go okay until the final step with MP4Box, where an MPD file was not found.</exception> /// <exception cref="OperationCanceledException">The cancellation token was triggered.</exception> public DashEncodeResult GenerateDash(DashConfig config, MediaMetadata probedInputData, IProgress <double> progress = null, CancellationToken cancel = default) { cancel.ThrowIfCancellationRequested(); if (probedInputData == null) { throw new ArgumentNullException(nameof(probedInputData), "Probe data cannot be null. Get this parameter from calling ProbeFile."); } //Field declarations IQuality compareQuality; bool enableStreamCopy = false; config.Qualities = QualityCrusher.CrushQualities(config.Qualities, probedInputData.KBitrate, config.QualityCrushTolerance); compareQuality = config.Qualities.First(); if (config.EnableStreamCopying && compareQuality.Bitrate == 0) { enableStreamCopy = Copyable264Infer.DetermineCopyCanBeDone(compareQuality.PixelFormat, compareQuality.Level, compareQuality.Profile.ToString(), probedInputData.VideoStreams); } // Set the framerate interval to match input if user has not already set if (config.Framerate <= 0) { config.Framerate = (int)Math.Round(probedInputData.Framerate); } // Set the keyframe interval to match input if user has not already set if (config.KeyframeInterval <= 0) { config.KeyframeInterval = config.Framerate * 3; } cancel.ThrowIfCancellationRequested(); FFmpegCommand ffmpegCommand = EncodeVideo(config, probedInputData, progress, cancel); Mp4BoxCommand mp4BoxCommand = GenerateDashManifest(config, ffmpegCommand.VideoCommands, ffmpegCommand.AudioCommands, cancel, ffmpegCommand); if (File.Exists(mp4BoxCommand.MpdPath)) { int maxFileIndex = ffmpegCommand.AllStreamCommands.Max(x => x.Index); IEnumerable <SubtitleStreamCommand> allSubtitles = ProcessSubtitles(config, ffmpegCommand.SubtitleCommands, maxFileIndex + 1); MPD mpd = PostProcessMpdFile(mp4BoxCommand.MpdPath, allSubtitles); return(new DashEncodeResult(mp4BoxCommand.MpdPath, mpd, ffmpegCommand, probedInputData)); } throw new DashManifestNotCreatedException(mp4BoxCommand.MpdPath, ffmpegCommand, mp4BoxCommand, $"MP4Box did not produce the expected mpd file at path {mp4BoxCommand.MpdPath}. File: {config.InputFilePath}"); }
public FFmpegCommand EncodeVideo(DashConfig config, MediaMetadata inputStats, IProgress <double> progress = null, CancellationToken cancel = default) { FFmpegCommand ffmpegCommand = null; var log = new StringBuilder(); try { ffmpegCommand = FFmpegCommandGeneratorMethod(config, inputStats); ExecutionResult ffResult; ffResult = ManagedExecution.Start(FFmpegPath, ffmpegCommand.RenderedCommand, (x) => { log.AppendLine(x); stdoutLog.Invoke(x); }, (x) => { log.AppendLine(x); FFmpegProgressShim(x, inputStats.Duration, progress); }, cancel); // Detect error in ffmpeg process and cleanup, then return null. if (ffResult.ExitCode != 0) { throw new FFMpegFailedException(ffmpegCommand, log, $"ERROR: ffmpeg returned code {ffResult.ExitCode}. File: {config.InputFilePath}"); } } catch (Exception ex) { try { CleanFiles(ffmpegCommand.AllStreamCommands.Select(x => x.Path)); } catch (Exception) { } if (ex is FFMpegFailedException) { throw; } throw new FFMpegFailedException(ffmpegCommand, log, ex.Message, ex); } finally { } return(ffmpegCommand); }
/// <summary> /// The default function for generating an ffmpeg command. /// </summary> public static FFmpegCommand GenerateFFmpegCommand(DashConfig config, MediaMetadata inputStats) { return(new FFmpegCommandBuilder ( inPath: config.InputFilePath, outDirectory: config.OutputDirectory, outBaseFilename: config.OutputFileName, options: config.Options, enableStreamCopying: config.EnableStreamCopying ) .WithVideoCommands(inputStats.VideoStreams, config.Qualities, config.Framerate, config.KeyframeInterval, inputStats.KBitrate) .WithAudioCommands(inputStats.AudioStreams, config.AudioConfig) .WithSubtitleCommands(inputStats.SubtitleStreams) .Build()); }
private FfmpegRenderedCommand EncodeVideo(DashConfig config, MediaMetadata inputStats, int inputBitrate, bool enableStreamCopying, Action <string> progressCallback, CancellationToken cancel) { FfmpegRenderedCommand ffmpegCommand = FFmpegCommandBuilder .Initilize( inPath: config.InputFilePath, outDirectory: config.OutputDirectory, outBaseFilename: config.OutputFileName, options: config.Options, enableStreamCopying: enableStreamCopying ) .WithVideoCommands(inputStats.VideoStreams, config.Qualities, config.Framerate, config.KeyframeInterval, inputBitrate) .WithAudioCommands(inputStats.AudioStreams) .WithSubtitleCommands(inputStats.SubtitleStreams) .Build(); // Generate intermediates try { ExecutionResult ffResult; stderrLog.Invoke($"Running ffmpeg with arguments: {ffmpegCommand.RenderedCommand}"); ffResult = ManagedExecution.Start(FFmpegPath, ffmpegCommand.RenderedCommand, stdoutLog, progressCallback, cancel); //TODO: Use a better log/error callback mechanism? Also use a better progress mechanism // Detect error in ffmpeg process and cleanup, then return null. if (ffResult.ExitCode != 0) { stderrLog.Invoke($"ERROR: ffmpeg returned code {ffResult.ExitCode}. File: {config.InputFilePath}"); CleanOutputFiles(ffmpegCommand.AllPieces.Select(x => x.Path)); return(null); } } catch (Exception ex) { CleanOutputFiles(ffmpegCommand.AllPieces.Select(x => x.Path)); if (ex is OperationCanceledException) { throw new OperationCanceledException($"Exception running ffmpeg on {config.InputFilePath}", ex); } else { throw new Exception($"Exception running ffmpeg on {config.InputFilePath}", ex); } } return(ffmpegCommand); }
/// <summary> /// Builds the arguments for the CLI version of ffmpeg. /// </summary> /// <param name="inPath">The source file to encode.</param> /// <param name="outDirectory">The directory to place output file in.</param> /// <param name="outFilename">The base filename to use when naming output files (format is [outFilename]_[qualityBitrate].mp4).</param> /// <param name="options">Flags to pass to the encoder.</param> /// <param name="framerate">The output framerate.</param> /// <param name="keyframeInterval">The output key interval. For best results should be a multiple of framerate.</param> /// <param name="metadata">Metadata on the encoded stream.</param> /// <param name="qualities">A collection of qualities to encode to. Entries in the collection must have a distinct bitrate, otherwise behavior is undefined.</param> /// <param name="defaultBitrate">The bitrate to use for the copy quality. Typically the input file bitrate.</param> /// <param name="enableStreamCopying">Set true to enable -vcodec copy for the copy quality.</param> internal static CommandBuildResult BuildFfmpegCommand( string inPath, string outDirectory, string outFilename, IEncodeOptions options, IEnumerable <IQuality> qualities, int framerate, int keyframeInterval, MediaMetadata metadata, int defaultBitrate, bool enableStreamCopying) { var videoCommand = BuildVideoCommands(metadata.VideoStreams, qualities, options.AdditionalVideoFlags, framerate, keyframeInterval, defaultBitrate, enableStreamCopying, outDirectory, outFilename); var audioCommand = BuildAudioCommands(metadata.AudioStreams, options.AdditionalAudioFlags, outDirectory, outFilename); var subtitleCommand = BuildSubtitleCommands(metadata.SubtitleStreams, outDirectory, outFilename); var allCommands = videoCommand.Concat(audioCommand).Concat(subtitleCommand); var additionalFlags = options.AdditionalFlags ?? new List <string>(); string parameters = $"-i \"{inPath}\" -y -hide_banner {string.Join(" ", additionalFlags.Concat(allCommands.Select(x => x.Argument)))}"; return(new CommandBuildResult(parameters, allCommands)); }
private MediaMetadata ProbeFile(string inFile) { string args = $"-print_format xml=fully_qualified=1 -show_format -show_streams -- \"{inFile}\""; var exResult = ManagedExecution.Start(FFprobePath, args); string xmlData = string.Join("\n", exResult.Output); if (FFprobeData.Deserialize(xmlData, out FFprobeData t)) { List <MediaStream> audioStreams = new List <MediaStream>(); List <MediaStream> videoStreams = new List <MediaStream>(); List <MediaStream> subtitleStreams = new List <MediaStream>(); foreach (var s in t.streams) { switch (s.codec_type) { case "audio": audioStreams.Add(s); break; case "video": videoStreams.Add(s); break; case "subtitle": subtitleStreams.Add(s); break; default: break; } } var metadata = new Dictionary <string, string>(); if (t.format.tag != null) { foreach (var item in t.format.tag) { if (!metadata.ContainsKey(item.key)) { metadata.Add(item.key.ToLower(System.Globalization.CultureInfo.InvariantCulture), item.value); } } } var firstVideoStream = videoStreams.FirstOrDefault(x => CommandBuilder.SupportedCodecs.ContainsKey(x.codec_name)); var firstAudioStream = audioStreams.FirstOrDefault(x => CommandBuilder.SupportedCodecs.ContainsKey(x.codec_name)); if (!decimal.TryParse(firstVideoStream?.r_frame_rate, out decimal framerate)) { framerate = 24; } float duration = t.format != null ? t.format.duration : 0; var meta = new MediaMetadata(videoStreams, audioStreams, subtitleStreams, metadata, firstVideoStream?.bit_rate ?? t.format.bit_rate, framerate, duration); return(meta); } return(null); }
public MediaMetadata ProbeFile(string inFile, out FFprobeData rawProbe) { string args = $"-print_format xml=fully_qualified=1 -show_format -show_streams -- \"{inFile}\""; var exResult = ManagedExecution.Start(FFprobePath, args); string xmlData = string.Join("\n", exResult.Output); rawProbe = FFprobeData.Deserialize(xmlData); List <MediaStream> audioStreams = new List <MediaStream>(); List <MediaStream> videoStreams = new List <MediaStream>(); List <MediaStream> subtitleStreams = new List <MediaStream>(); foreach (var s in rawProbe.streams) { switch (s.codec_type) { case "audio": audioStreams.Add(s); break; case "video": videoStreams.Add(s); break; case "subtitle": subtitleStreams.Add(s); break; default: break; } } var metadata = new Dictionary <string, string>(); if (rawProbe.format.tag != null) { foreach (var item in rawProbe.format.tag) { if (!metadata.ContainsKey(item.key)) { metadata.Add(item.key.ToLower(System.Globalization.CultureInfo.InvariantCulture), item.value); } } } var firstVideoStream = videoStreams.FirstOrDefault(x => Constants.SupportedInputCodecs.ContainsKey(x.codec_name)) ?? videoStreams.FirstOrDefault(); decimal framerate = 0; long bitrate = 0; if (firstVideoStream == null) { // Leave them as zero. } else { if (decimal.TryParse(firstVideoStream.r_frame_rate, out framerate)) { } else if (firstVideoStream.r_frame_rate.Contains("/")) { try { framerate = firstVideoStream.r_frame_rate .Split('/') .Select(component => decimal.Parse(component)) .Aggregate((dividend, divisor) => dividend / divisor); } catch (Exception) { // Leave it as zero. } } bitrate = firstVideoStream.bit_rate != 0 ? firstVideoStream.bit_rate : (rawProbe.format?.bit_rate ?? 0); } float duration = rawProbe.format != null ? rawProbe.format.duration : 0; var meta = new MediaMetadata(inFile, videoStreams, audioStreams, subtitleStreams, metadata, bitrate, framerate, duration); return(meta); }