public void GenerateDash_MidLevelApiOptions_ProducesCorrectDashEncodeResult() { Encoder encoder = new Encoder(ffmpegPath, ffprobePath, mp4boxPath, ffmpegCommandGenerator: (dashConfig, mediaMetadata) => { DashConfig c = dashConfig; MediaMetadata m = mediaMetadata; FFmpegCommand r = Encoder.GenerateFFmpegCommand(c, m); return(r); }, mp4BoxCommandGenerator: (dashConfig, videoStreams, audioStreams) => { DashConfig c = dashConfig; IEnumerable <VideoStreamCommand> v = videoStreams; IEnumerable <AudioStreamCommand> a = audioStreams; Mp4BoxCommand r = Encoder.GenerateMp4BoxCommand(c, v, a); return(r); }); DashConfig config = new DashConfig(testFileName, RunPath, Qualities, "output"); encodeResult = encoder.GenerateDash(config, encoder.ProbeFile(config.InputFilePath, out _)); Assert.NotNull(encodeResult.DashFilePath); Assert.NotNull(encodeResult.DashFileContent); Assert.NotNull(encodeResult.MediaFiles); Assert.Equal(4, encodeResult.MediaFiles.Count()); }
public void GenerateDash_WithLiveStreamingProfile_ProducesCorrectDashEncodeResult() { string outputFilename = "outputlive"; var options = new H264EncodeOptions { AdditionalMP4BoxFlags = new List <string>() { "-profile \"dashavc264:live\"", "-bs-switching no" } }; Encoder encoder = new Encoder(ffmpegPath, ffprobePath, mp4boxPath); DashConfig config = new DashConfig("testfile.ogg", RunPath, SubtitleQualities, outputFilename) { Options = options }; try { encodeResult = encoder.GenerateDash(config, encoder.ProbeFile(config.InputFilePath, out _)); Assert.NotNull(encodeResult.DashFilePath); Assert.NotNull(encodeResult.DashFileContent); } finally { foreach (var file in Directory.EnumerateFiles(RunPath, $"{outputFilename}_*")) { File.Delete(Path.Combine(RunPath, file)); } } }
public void GenerateDash_Downmixing_ProducesCorrectDashEncodeResult() { Encoder encoder = new Encoder(ffmpegPath, ffprobePath, mp4boxPath); DashConfig config = new DashConfig(multiLanguageTestFileName, RunPath, Qualities, "output") { AudioConfig = new AudioConfig() { DownmixMode = DownmixMode.Default, MaxPerChannelBitrate = 1024 * 80 } }; encodeResult = encoder.GenerateDash(config, encoder.ProbeFile(config.InputFilePath, out _)); var scans = encodeResult.MediaFiles.Select((x) => encoder.ProbeFile(x, out _)); foreach (var outputStream in scans) { if (outputStream.VideoStreams.Count() > 0) { continue; } foreach (var aStream in outputStream.AudioStreams) { Assert.True(aStream.channels <= 2); Assert.True(aStream.max_bit_rate <= config.AudioConfig.MaxPerChannelBitrate * aStream.channels); } } }
public void Constructor_WithInvalidOutputCharacters_CleansCharacters() { string outputName = "testfile*&:\\"; DashConfig config = new DashConfig(testFileName, Environment.CurrentDirectory, Qualities, outputName); Assert.Equal("testfile", config.OutputFileName); }
public MainWindow() { InitializeComponent(); this.config = new DashConfig(); this.languages = config.getFeeds(); this.Title = Assembly.GetExecutingAssembly().GetName().Name; this.DataContext = this; }
public void GenerateDash_WithCancellationToken_ThrowsOperationCanceledException() { var tokenSource = new CancellationTokenSource(500); DEnc.Encoder encoder = new DEnc.Encoder(ffmpegPath, ffprobePath, mp4boxPath); DashConfig config = new DashConfig(testFileName, RunPath, Qualities, "output"); Assert.Throws <OperationCanceledException>(() => encodeResult = encoder.GenerateDash(config, cancel: tokenSource.Token)); }
public void GenerateDash_NormalEncode_ProducesCorrectDashEncodeResult() { Encoder encoder = new Encoder(ffmpegPath, ffprobePath, mp4boxPath); DashConfig config = new DashConfig(testFileName, RunPath, Qualities, "output"); encodeResult = encoder.GenerateDash(config, encoder.ProbeFile(config.InputFilePath, out _)); Assert.NotNull(encodeResult.DashFilePath); Assert.NotNull(encodeResult.DashFileContent); Assert.NotNull(encodeResult.MediaFiles); Assert.Equal(4, encodeResult.MediaFiles.Count()); }
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); }
/// <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}"); }
/// <summary> /// The default function for generating an MP4Box command. /// </summary> public static Mp4BoxCommand GenerateMp4BoxCommand(DashConfig config, IEnumerable <VideoStreamCommand> videoFiles, IEnumerable <AudioStreamCommand> audioFiles) { // Use a default key interval of 3s if a framerate or keyframe interval is not given. int keyInterval = (config.KeyframeInterval == 0 || config.Framerate == 0) ? 3000 : (config.KeyframeInterval / config.Framerate * 1000); string mpdOutputPath = Path.Combine(config.OutputDirectory, config.OutputFileName) + ".mpd"; var mp4boxCommand = Mp4BoxCommandBuilder.BuildMp4boxMpdCommand( videoFiles: videoFiles, audioFiles: audioFiles, mpdOutputPath: mpdOutputPath, keyInterval: keyInterval, additionalFlags: config.Options.AdditionalMP4BoxFlags); 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); }
public void GenerateDash_WithManySubtitleLanguages_ProducesSubtitleFiles() { DEnc.Encoder encoder = new DEnc.Encoder(ffmpegPath, ffprobePath, mp4boxPath); encoder.EnableStreamCopying = true; DashConfig config = new DashConfig(multiLanguageTestFileName, RunPath, MultiLanguageQualities, "outputlang"); encodeResult = encoder.GenerateDash(config); Assert.NotNull(encodeResult.DashFilePath); Assert.NotNull(encodeResult.DashFileContent); Assert.NotNull(encodeResult.MediaFiles); Assert.Equal("avc1.640028", encodeResult.DashFileContent.Period[0].AdaptationSet[0].Representation[0].Codecs); Assert.True(encodeResult.DashFileContent.Period[0].AdaptationSet.Where(x => x.Lang == "jpn" && x.MaxFrameRate == null).SingleOrDefault().Representation.Count() == 1); Assert.True(encodeResult.DashFileContent.Period[0].AdaptationSet.Where(x => x.Lang == "eng" && x.MaxFrameRate == null).Count() == 2); }
/// <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()); }
public void GenerateDash_WithManySubtitles_ProducesSubtitleFiles() { Encoder encoder = new Encoder(ffmpegPath, ffprobePath, mp4boxPath); DashConfig config = new DashConfig(subtitleTestFileName, RunPath, SubtitleQualities, "outputmulti"); encodeResult = encoder.GenerateDash(config, encoder.ProbeFile(config.InputFilePath, out _)); Assert.NotNull(encodeResult.DashFilePath); Assert.NotNull(encodeResult.DashFileContent); Assert.NotNull(encodeResult.MediaFiles); Assert.Equal(16, encodeResult.MediaFiles.Count()); Assert.Contains("outputmulti_audio_default_1_dashinit.mp4", encodeResult.MediaFiles); Assert.Contains("outputmulti_subtitle_eng_2.vtt", encodeResult.MediaFiles); Assert.Contains("outputmulti_subtitle_und_10.vtt", encodeResult.MediaFiles); Assert.Contains("outputmulti_subtitle_eng_12.vtt", encodeResult.MediaFiles); }
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> /// Processes the media subtitles and finds and handles external subtitle files /// </summary> /// <param name="config">The <see cref="DashConfig"/></param> /// <param name="subtitleFiles">The subtitle stream files</param> /// <param name="startFileIndex">The index additional subtitles need to start at. This should be the max index of the ffmpeg pieces +1</param> private List <StreamSubtitleFile> ProcessSubtitles(DashConfig config, IEnumerable <StreamSubtitleFile> subtitleFiles, int startFileIndex) { // Move subtitles found in media List <StreamSubtitleFile> subtitles = new List <StreamSubtitleFile>(); foreach (var subFile in subtitleFiles) { string oldPath = subFile.Path; subFile.Path = Path.Combine(config.OutputDirectory, 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 string baseFilename = Path.GetFileNameWithoutExtension(config.InputFilePath); foreach (var vttFile in Directory.EnumerateFiles(Path.GetDirectoryName(config.InputFilePath), baseFilename + "*", SearchOption.TopDirectoryOnly)) { if (vttFile.EndsWith(".vtt")) { string vttFilename = Path.GetFileName(vttFile); string vttName = GetSubtitleName(vttFilename); string vttOutputPath = Path.Combine(config.OutputDirectory, $"{config.OutputFileName}_subtitle_{vttName}_{startFileIndex}.vtt"); var subFile = new StreamSubtitleFile() { Type = StreamType.Subtitle, Index = startFileIndex, Path = vttOutputPath, Language = $"{vttName}_{startFileIndex}" }; startFileIndex++; File.Copy(vttFile, vttOutputPath, true); subtitles.Add(subFile); } } return(subtitles); }
public void GenerateDash_WithCancellationToken_ThrowsOperationCanceledException() { var tokenSource = new CancellationTokenSource(250); Encoder encoder = new Encoder(ffmpegPath, ffprobePath, mp4boxPath); DashConfig config = new DashConfig(testFileName, RunPath, Qualities, "output"); Exception thrown = null; try { encodeResult = encoder.GenerateDash(config, encoder.ProbeFile(config.InputFilePath, out _), cancel: tokenSource.Token); } catch (Exception ex) { thrown = ex; } Assert.NotNull(thrown); Assert.IsType <OperationCanceledException>(thrown.InnerException); }
protected override void Process(IConversionItem item) { if (item.CancellationToken.IsCancellationRequested) { HandleCancel(item); return; } var options = new H264EncodeOptions(); switch (item.Tune) { case Tune.Film: options.AdditionalVideoFlags.Add("-tune film"); break; case Tune.Grain: options.AdditionalVideoFlags.Add("-tune grain"); break; case Tune.Animation: options.AdditionalVideoFlags.Add("-tune animation"); break; } log.LogInformation($"Processing item {item.SourcePath}"); try { var probeData = encoder.ProbeFile(item.SourcePath, out _); var dashConfig = new DashConfig(item.SourcePath, item.OutputPath, item.Quality, item.OutputBaseFilename) { EnableStreamCopying = true, Framerate = item.Framerate, Options = options }; if (item.DownmixAudio) { dashConfig.AudioConfig = new AudioConfig() { DownmixMode = DownmixMode.Default }; } var dashResult = encoder.GenerateDash(dashConfig, probeData, progress: new NaiveProgress <double>((x) => { item.Progress = new[] { new DescribedProgress("Conversion", x) }; }), cancel: item.CancellationToken.Token); if (dashResult == null) { throw new Exception("Failed to convert item. Got null from generator. Check the ffmpeg/mp4box log."); } item.CompletionAction.Invoke(item, dashResult); completeItems.Add(item); } catch (FFMpegFailedException ex) when(ex is FFMpegFailedException || ex is Mp4boxFailedException || ex is DashManifestNotCreatedException) { if (ex.InnerException is OperationCanceledException) { log.LogInformation($"Task Cancelled: {item.SourcePath}"); HandleCancel(item); return; } var logItems = new List <string>() { $"Failed to convert {item.SourcePath}", $"ffmpeg command: {ex.FFmpegCommand?.RenderedCommand ?? "Unavailable"}" }; string failureStage = "Unknown"; switch (ex) { case DashManifestNotCreatedException dex: failureStage = "Manifest generation"; break; case Mp4boxFailedException mpex: failureStage = "DASHing/MP4Box"; logItems.Add($"MP4Box command: {mpex.MP4BoxCommand.RenderedCommand}"); break; case FFMpegFailedException ffex: failureStage = "Encoding/ffmpeg"; break; default: break; } logItems.Add($"Stack trace: {ex}"); if (ex.Log != null && ex.Log.Length > 0) { logItems.Add($"Process log: {ex.Log}"); } string fullLog = string.Join('\n', logItems); log.LogWarning(fullLog); item.ErrorReason = $"Failed at step: {failureStage}. Message: {ex.Message}"; item.ErrorDetail = fullLog; completeItems.Add(item); } catch (OperationCanceledException) { log.LogInformation($"Task Cancelled: {item.SourcePath}"); HandleCancel(item); } }
public void Constructor_WithNullOutputFileName_UsesInputName() { DashConfig config = new DashConfig(testFileName, Environment.CurrentDirectory, Qualities); Assert.Equal("testfile", config.OutputFileName); }
/// <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); }
/// <summary> /// oOnverts the input file into an MPEG DASH representations. /// This includes multiple bitrates, subtitle tracks, audio tracks, and an MPD manifest. /// </summary> /// <param name="config"></param> /// <param name="progress"></param> /// <param name="cancel"></param> /// <returns></returns> public DashEncodeResult GenerateDash(DashConfig config, IProgress <Dictionary <EncodingStage, double> > progress = null, CancellationToken cancel = default(CancellationToken)) { cancel.ThrowIfCancellationRequested(); if (!Directory.Exists(WorkingDirectory)) { throw new DirectoryNotFoundException("The given path for the working directory doesn't exist."); } //Field declarations MediaMetadata inputStats; IQuality compareQuality; int inputBitrate; bool enableStreamCopy = false; inputStats = ProbeFile(config.InputFilePath); if (inputStats == null) { throw new NullReferenceException("ffprobe query returned a null result."); } inputBitrate = (int)(inputStats.Bitrate / 1024); if (!DisableQualityCrushing) { config.Qualities = QualityCrusher.CrushQualities(config.Qualities, inputBitrate); } compareQuality = config.Qualities.First(); if (EnableStreamCopying && compareQuality.Bitrate == 0) { enableStreamCopy = Copyable264Infer.DetermineCopyCanBeDone(compareQuality.PixelFormat, compareQuality.Level, compareQuality.Profile.ToString(), inputStats.VideoStreams); } // Set the framerate interval to match input if user has not already set if (config.Framerate <= 0) { config.Framerate = (int)Math.Round(inputStats.Framerate); } // Set the keyframe interval to match input if user has not already set if (config.KeyframeInterval <= 0) { config.KeyframeInterval = config.Framerate * 3; } //This is not really the proper place to have this // Logging shim for ffmpeg to get progress info var ffmpegLogShim = new Action <string>(x => { if (x != null) { var match = Encode.Regexes.ParseProgress.Match(x); if (match.Success && TimeSpan.TryParse(match.Value, out TimeSpan p)) { stdoutLog(x); float progressFloat = Math.Min(1, (float)(p.TotalMilliseconds / 1000) / inputStats.Duration); if (progress != null) { ReportProgress(progress, EncodingStage.Encode, progressFloat); } } else { stderrLog(x); } } else { stderrLog(x); } }); cancel.ThrowIfCancellationRequested(); FfmpegRenderedCommand ffmpgCommand = EncodeVideo(config, inputStats, inputBitrate, enableStreamCopy, ffmpegLogShim, cancel); if (ffmpgCommand is null) { return(null); } Mp4BoxRenderedCommand mp4BoxCommand = GenerateDashManifest(config, ffmpgCommand.VideoPieces, ffmpgCommand.AudioPieces, cancel); if (mp4BoxCommand is null) { return(null); } ReportProgress(progress, EncodingStage.DASHify, 1); ReportProgress(progress, EncodingStage.PostProcess, 0.3); int maxFileIndex = ffmpgCommand.AllPieces.Max(x => x.Index); List <StreamSubtitleFile> allSubtitles = ProcessSubtitles(config, ffmpgCommand.SubtitlePieces, maxFileIndex + 1); ReportProgress(progress, EncodingStage.PostProcess, 0.66); try { string mpdFilepath = mp4BoxCommand.MpdPath; if (File.Exists(mpdFilepath)) { MPD mpd = PostProcessMpdFile(mpdFilepath, allSubtitles); var result = new DashEncodeResult(mpd, inputStats.Metadata, TimeSpan.FromMilliseconds((inputStats.VideoStreams.FirstOrDefault()?.duration ?? 0) * 1000), mpdFilepath); // Success. return(result); } stderrLog.Invoke($"ERROR: MP4Box did not produce the expected mpd file at path {mpdFilepath}. File: {config.InputFilePath}"); return(null); } finally { ReportProgress(progress, EncodingStage.PostProcess, 1); } }