private Mp4BoxRenderedCommand GenerateDashManifest(DashConfig config, IEnumerable <StreamVideoFile> videoFiles, IEnumerable <StreamAudioFile> audioFiles, CancellationToken cancel) { string mpdOutputPath = Path.Combine(config.OutputDirectory, config.OutputFileName) + ".mpd"; var mp4boxCommand = Mp4BoxCommandBuilder.BuildMp4boxMpdCommand( videoFiles: videoFiles, audioFiles: audioFiles, mpdOutputPath: mpdOutputPath, keyInterval: (config.KeyframeInterval / config.Framerate) * 1000, additionalFlags: config.Options.AdditionalMP4BoxFlags); // Generate DASH files. ExecutionResult mpdResult; stderrLog.Invoke($"Running MP4Box with arguments: {mp4boxCommand.RenderedCommand}"); try { mpdResult = ManagedExecution.Start(BoxPath, mp4boxCommand.RenderedCommand, stdoutLog, stderrLog, cancel); // Dash Failed TODO: Add in Progress report behavior that was excluded from this // Detect error in MP4Box process and cleanup, then return null. if (mpdResult.ExitCode != 0) { MPD mpdFile = MPD.LoadFromFile(mpdOutputPath); var filePaths = mpdFile.GetFileNames().Select(x => Path.Combine(config.OutputDirectory, x)); stderrLog.Invoke($"ERROR: MP4Box returned code {mpdResult.ExitCode}. File: {config.InputFilePath}"); CleanOutputFiles(filePaths); CleanOutputFiles(mpdResult.Output); return(null); } } catch (Exception ex) { if (ex is OperationCanceledException) { throw new OperationCanceledException($"Exception running MP4box on {config.InputFilePath}", ex); } else { throw new Exception($"Exception running MP4box on {config.InputFilePath}", ex); } } finally { CleanOutputFiles(videoFiles.Select(x => x.Path)); CleanOutputFiles(audioFiles.Select(x => x.Path)); } return(mp4boxCommand); }
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); }
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> /// 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); } }
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); }
/// <summary> /// This method takes configuration, and a set of video and audio streams, and assemb /// </summary> /// <param name="config">The config to use to generate the MP4Box command.</param> /// <param name="videoFiles">A set of video files to include in the DASH process and manifest.</param> /// <param name="audioFiles">A set of audio files to include in the DASH process and manifest.</param> /// <param name="cancel">A cancel token to pass to the process.</param> /// <param name="originalFFmpegCommand">The ffmpeg command used to create the input files. This is for exception logging only, and may be left null.</param> protected virtual Mp4BoxCommand GenerateDashManifest(DashConfig config, IEnumerable <VideoStreamCommand> videoFiles, IEnumerable <AudioStreamCommand> audioFiles, CancellationToken cancel, FFmpegCommand originalFFmpegCommand = null) { Mp4BoxCommand mp4boxCommand = null; ExecutionResult mpdResult; var log = new StringBuilder(); try { mp4boxCommand = Mp4BoxCommandGeneratorMethod(config, videoFiles, audioFiles); mpdResult = ManagedExecution.Start(Mp4BoxPath, mp4boxCommand.RenderedCommand, (x) => { log.AppendLine(x); stdoutLog.Invoke(x); }, (x) => { log.AppendLine(x); stderrLog.Invoke(x); }, cancel); if (mpdResult.ExitCode != 0) { try { // Error in MP4Box. if (File.Exists(mp4boxCommand.MpdPath)) { MPD mpdFile = MPD.LoadFromFile(mp4boxCommand.MpdPath); var filePaths = mpdFile.GetFileNames().Select(x => Path.Combine(config.OutputDirectory, x)); CleanFiles(filePaths); CleanFiles(mpdResult.Output); } } catch (Exception ex) { throw new Mp4boxFailedException(originalFFmpegCommand, mp4boxCommand, log, $"MP4Box returned code {mpdResult.ExitCode}.", ex); } throw new Mp4boxFailedException(originalFFmpegCommand, mp4boxCommand, log, $"MP4Box returned code {mpdResult.ExitCode}."); } else if (!File.Exists(mp4boxCommand.MpdPath)) { throw new Mp4boxFailedException(originalFFmpegCommand, mp4boxCommand, log, $"MP4Box appeared to succeed, but no MPD file was created."); } } catch (Exception ex) { if (ex is Mp4boxFailedException) { throw; } throw new Mp4boxFailedException(originalFFmpegCommand, mp4boxCommand, log, ex.Message, ex); } finally { CleanFiles(videoFiles.Select(x => x.Path)); CleanFiles(audioFiles.Select(x => x.Path)); } return(mp4boxCommand); }
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); }