Decompiled source of FFMpegCore v5.1.1
BepInEx/core/FFMpegCore/netstandard2.0/FFMpegCore.dll
Decompiled 6 months ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.Globalization; using System.IO; using System.IO.Pipes; using System.Linq; using System.Reflection; using System.Resources; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Text; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using FFMpegCore.Arguments; using FFMpegCore.Builders.MetaData; using FFMpegCore.Enums; using FFMpegCore.Exceptions; using FFMpegCore.Extend; using FFMpegCore.Helpers; using FFMpegCore.Pipes; using Instances; using Microsoft.CodeAnalysis; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: InternalsVisibleTo("FFMpegCore.Test")] [assembly: TargetFramework(".NETStandard,Version=v2.0", FrameworkDisplayName = ".NET Standard 2.0")] [assembly: AssemblyCompany("Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyDescription("A .NET Standard FFMpeg/FFProbe wrapper for easily integrating media analysis and conversion into your .NET applications")] [assembly: AssemblyFileVersion("5.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: AssemblyProduct("FFMpegCore")] [assembly: AssemblyTitle("FFMpegCore")] [assembly: AssemblyMetadata("RepositoryUrl", "https://github.com/rosenbjerg/FFMpegCore")] [assembly: NeutralResourcesLanguage("en")] [assembly: AssemblyVersion("5.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace FFMpegCore { public static class FFMpeg { public static bool Snapshot(string input, string output, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0) { if (Path.GetExtension(output) != FileExtension.Png) { output = Path.Combine(Path.GetDirectoryName(output), Path.GetFileNameWithoutExtension(output) + FileExtension.Png); } IMediaAnalysis source = FFProbe.Analyse(input); var (fFMpegArguments, addArguments) = SnapshotArgumentBuilder.BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex); return fFMpegArguments.OutputToFile(output, overwrite: true, addArguments).ProcessSynchronously(); } public static async Task<bool> SnapshotAsync(string input, string output, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0) { if (Path.GetExtension(output) != FileExtension.Png) { output = Path.Combine(Path.GetDirectoryName(output), Path.GetFileNameWithoutExtension(output) + FileExtension.Png); } var (fFMpegArguments, addArguments) = SnapshotArgumentBuilder.BuildSnapshotArguments(input, await FFProbe.AnalyseAsync(input).ConfigureAwait(continueOnCapturedContext: false), size, captureTime, streamIndex, inputFileIndex); return await fFMpegArguments.OutputToFile(output, overwrite: true, addArguments).ProcessAsynchronously(); } public static bool GifSnapshot(string input, string output, Size? size = null, TimeSpan? captureTime = null, TimeSpan? duration = null, int? streamIndex = null) { if (Path.GetExtension(output)?.ToLower() != FileExtension.Gif) { output = Path.Combine(Path.GetDirectoryName(output), Path.GetFileNameWithoutExtension(output) + FileExtension.Gif); } IMediaAnalysis source = FFProbe.Analyse(input); var (fFMpegArguments, addArguments) = SnapshotArgumentBuilder.BuildGifSnapshotArguments(input, source, size, captureTime, duration, streamIndex); return fFMpegArguments.OutputToFile(output, overwrite: true, addArguments).ProcessSynchronously(); } public static async Task<bool> GifSnapshotAsync(string input, string output, Size? size = null, TimeSpan? captureTime = null, TimeSpan? duration = null, int? streamIndex = null) { if (Path.GetExtension(output)?.ToLower() != FileExtension.Gif) { output = Path.Combine(Path.GetDirectoryName(output), Path.GetFileNameWithoutExtension(output) + FileExtension.Gif); } var (fFMpegArguments, addArguments) = SnapshotArgumentBuilder.BuildGifSnapshotArguments(input, await FFProbe.AnalyseAsync(input).ConfigureAwait(continueOnCapturedContext: false), size, captureTime, duration, streamIndex); return await fFMpegArguments.OutputToFile(output, overwrite: true, addArguments).ProcessAsynchronously(); } public static bool JoinImageSequence(string output, double frameRate = 30.0, params string[] images) { string?[] array = images.Select(Path.GetExtension).Distinct().ToArray(); if (array.Length != 1) { throw new ArgumentException("All images must have the same extension", "images"); } string text = array[0].ToLowerInvariant(); int? width = null; int? height = null; string text2 = Path.Combine(GlobalFFOptions.Current.TemporaryFilesFolder, Guid.NewGuid().ToString()); Directory.CreateDirectory(text2); try { int num = 0; foreach (string obj in images) { IMediaAnalysis mediaAnalysis = FFProbe.Analyse(obj); FFMpegHelper.ConversionSizeExceptionCheck(mediaAnalysis.PrimaryVideoStream.Width, mediaAnalysis.PrimaryVideoStream.Height); int valueOrDefault = width.GetValueOrDefault(); if (!width.HasValue) { valueOrDefault = mediaAnalysis.PrimaryVideoStream.Width; width = valueOrDefault; } valueOrDefault = height.GetValueOrDefault(); if (!height.HasValue) { valueOrDefault = mediaAnalysis.PrimaryVideoStream.Height; height = valueOrDefault; } string destFileName = Path.Combine(text2, num++.ToString().PadLeft(9, '0') + text); File.Copy(obj, destFileName); } return FFMpegArguments.FromFileInput(Path.Combine(text2, "%09d" + text), verifyExists: false).OutputToFile(output, overwrite: true, delegate(FFMpegArgumentOptions options) { options.ForcePixelFormat("yuv420p").Resize(width.Value, height.Value).WithFramerate(frameRate); }).ProcessSynchronously(); } finally { Directory.Delete(text2, recursive: true); } } public static bool PosterWithAudio(string image, string audio, string output) { FFMpegHelper.ExtensionExceptionCheck(output, FileExtension.Mp4); IMediaAnalysis mediaAnalysis = FFProbe.Analyse(image); FFMpegHelper.ConversionSizeExceptionCheck(mediaAnalysis.PrimaryVideoStream.Width, mediaAnalysis.PrimaryVideoStream.Height); return FFMpegArguments.FromFileInput(image, verifyExists: false, delegate(FFMpegArgumentOptions options) { options.Loop(1).ForceFormat("image2"); }).AddFileInput(audio).OutputToFile(output, overwrite: true, delegate(FFMpegArgumentOptions options) { options.ForcePixelFormat("yuv420p").WithVideoCodec(VideoCodec.LibX264).WithConstantRateFactor(21) .WithAudioBitrate(AudioQuality.Normal) .UsingShortest(); }) .ProcessSynchronously(); } public static bool Convert(string input, string output, ContainerFormat format, Speed speed = Speed.SuperFast, VideoSize size = VideoSize.Original, AudioQuality audioQuality = AudioQuality.Normal, bool multithreaded = false) { FFMpegHelper.ExtensionExceptionCheck(output, format.Extension); IMediaAnalysis mediaAnalysis = FFProbe.Analyse(input); FFMpegHelper.ConversionSizeExceptionCheck(mediaAnalysis); double num = ((VideoSize.Original == size) ? 1.0 : ((double)mediaAnalysis.PrimaryVideoStream.Height / (double)size)); Size outputSize = new Size((int)((double)mediaAnalysis.PrimaryVideoStream.Width / num), (int)((double)mediaAnalysis.PrimaryVideoStream.Height / num)); if (outputSize.Width % 2 != 0) { outputSize.Width++; } return format.Name switch { "mp4" => FFMpegArguments.FromFileInput(input).OutputToFile(output, overwrite: true, delegate(FFMpegArgumentOptions options) { options.UsingMultithreading(multithreaded).WithVideoCodec(VideoCodec.LibX264).WithVideoBitrate(2400) .WithVideoFilters(delegate(VideoFilterOptions filterOptions) { filterOptions.Scale(outputSize); }) .WithSpeedPreset(speed) .WithAudioCodec(AudioCodec.Aac) .WithAudioBitrate(audioQuality); }).ProcessSynchronously(), "ogv" => FFMpegArguments.FromFileInput(input).OutputToFile(output, overwrite: true, delegate(FFMpegArgumentOptions options) { options.UsingMultithreading(multithreaded).WithVideoCodec(VideoCodec.LibTheora).WithVideoBitrate(2400) .WithVideoFilters(delegate(VideoFilterOptions filterOptions) { filterOptions.Scale(outputSize); }) .WithSpeedPreset(speed) .WithAudioCodec(AudioCodec.LibVorbis) .WithAudioBitrate(audioQuality); }).ProcessSynchronously(), "mpegts" => FFMpegArguments.FromFileInput(input).OutputToFile(output, overwrite: true, delegate(FFMpegArgumentOptions options) { options.CopyChannel().WithBitStreamFilter(Channel.Video, Filter.H264_Mp4ToAnnexB).ForceFormat(VideoType.Ts); }).ProcessSynchronously(), "webm" => FFMpegArguments.FromFileInput(input).OutputToFile(output, overwrite: true, delegate(FFMpegArgumentOptions options) { options.UsingMultithreading(multithreaded).WithVideoCodec(VideoCodec.LibVpx).WithVideoBitrate(2400) .WithVideoFilters(delegate(VideoFilterOptions filterOptions) { filterOptions.Scale(outputSize); }) .WithSpeedPreset(speed) .WithAudioCodec(AudioCodec.LibVorbis) .WithAudioBitrate(audioQuality); }).ProcessSynchronously(), _ => throw new ArgumentOutOfRangeException("format"), }; } public static bool Join(string output, params string[] videos) { string[] array = videos.Select(delegate(string videoPath) { FFMpegHelper.ConversionSizeExceptionCheck(FFProbe.Analyse(videoPath)); string text = Path.Combine(GlobalFFOptions.Current.TemporaryFilesFolder, Path.GetFileNameWithoutExtension(videoPath) + FileExtension.Ts); Directory.CreateDirectory(GlobalFFOptions.Current.TemporaryFilesFolder); Convert(videoPath, text, VideoType.Ts); return text; }).ToArray(); try { return FFMpegArguments.FromConcatInput(array).OutputToFile(output, overwrite: true, delegate(FFMpegArgumentOptions options) { options.CopyChannel().WithBitStreamFilter(Channel.Audio, Filter.Aac_AdtstoAsc); }).ProcessSynchronously(); } finally { Cleanup(array); } } private static FFMpegArgumentProcessor BaseSubVideo(string input, string output, TimeSpan startTime, TimeSpan endTime) { if (Path.GetExtension(input) != Path.GetExtension(output)) { output = Path.Combine(Path.GetDirectoryName(output), Path.GetFileNameWithoutExtension(output), Path.GetExtension(input)); } return FFMpegArguments.FromFileInput(input, verifyExists: true, delegate(FFMpegArgumentOptions options) { options.Seek(startTime).EndSeek(endTime); }).OutputToFile(output, overwrite: true, delegate(FFMpegArgumentOptions options) { options.CopyChannel(); }); } public static bool SubVideo(string input, string output, TimeSpan startTime, TimeSpan endTime) { return BaseSubVideo(input, output, startTime, endTime).ProcessSynchronously(); } public static async Task<bool> SubVideoAsync(string input, string output, TimeSpan startTime, TimeSpan endTime) { return await BaseSubVideo(input, output, startTime, endTime).ProcessAsynchronously(); } public static bool SaveM3U8Stream(Uri uri, string output) { FFMpegHelper.ExtensionExceptionCheck(output, FileExtension.Mp4); if (uri.Scheme != "http" && uri.Scheme != "https") { throw new ArgumentException("Uri: " + uri.AbsoluteUri + ", does not point to a valid http(s) stream."); } return FFMpegArguments.FromUrlInput(uri).OutputToFile(output).ProcessSynchronously(); } public static bool Mute(string input, string output) { FFMpegHelper.ConversionSizeExceptionCheck(FFProbe.Analyse(input)); return FFMpegArguments.FromFileInput(input).OutputToFile(output, overwrite: true, delegate(FFMpegArgumentOptions options) { options.CopyChannel(Channel.Video).DisableChannel(Channel.Audio); }).ProcessSynchronously(); } public static bool ExtractAudio(string input, string output) { FFMpegHelper.ExtensionExceptionCheck(output, FileExtension.Mp3); return FFMpegArguments.FromFileInput(input).OutputToFile(output, overwrite: true, delegate(FFMpegArgumentOptions options) { options.DisableChannel(Channel.Video); }).ProcessSynchronously(); } public static bool ReplaceAudio(string input, string inputAudio, string output, bool stopAtShortest = false) { FFMpegHelper.ConversionSizeExceptionCheck(FFProbe.Analyse(input)); return FFMpegArguments.FromFileInput(input).AddFileInput(inputAudio).OutputToFile(output, overwrite: true, delegate(FFMpegArgumentOptions options) { options.CopyChannel().WithAudioCodec(AudioCodec.Aac).WithAudioBitrate(AudioQuality.Good) .UsingShortest(stopAtShortest); }) .ProcessSynchronously(); } internal static IReadOnlyList<PixelFormat> GetPixelFormatsInternal() { //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Expected O, but got Unknown FFMpegHelper.RootExceptionCheck(); List<PixelFormat> list = new List<PixelFormat>(); ProcessArguments val = new ProcessArguments(GlobalFFOptions.GetFFMpegBinaryPath(), "-pix_fmts"); val.OutputDataReceived += delegate(object e, string data) { if (PixelFormat.TryParse(data, out PixelFormat fmt)) { list.Add(fmt); } }; IProcessResult val2 = ProcessArgumentsExtensions.StartAndWaitForExit(val); if (val2.ExitCode != 0) { throw new FFMpegException(FFMpegExceptionType.Process, string.Join("\r\n", val2.OutputData)); } return list.AsReadOnly(); } public static IReadOnlyList<PixelFormat> GetPixelFormats() { if (!GlobalFFOptions.Current.UseCache) { return GetPixelFormatsInternal(); } return FFMpegCache.PixelFormats.Values.ToList().AsReadOnly(); } public static bool TryGetPixelFormat(string name, out PixelFormat format) { string name2 = name; if (!GlobalFFOptions.Current.UseCache) { format = GetPixelFormatsInternal().FirstOrDefault((PixelFormat x) => x.Name == name2.ToLowerInvariant().Trim()); return format != null; } return FFMpegCache.PixelFormats.TryGetValue(name2, out format); } public static PixelFormat GetPixelFormat(string name) { if (TryGetPixelFormat(name, out PixelFormat format)) { return format; } throw new FFMpegException(FFMpegExceptionType.Operation, "Pixel format \"" + name + "\" not supported"); } private static void ParsePartOfCodecs(Dictionary<string, Codec> codecs, string arguments, Func<string, Codec?> parser) { //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Expected O, but got Unknown Func<string, Codec?> parser2 = parser; Dictionary<string, Codec> codecs2 = codecs; FFMpegHelper.RootExceptionCheck(); ProcessArguments val = new ProcessArguments(GlobalFFOptions.GetFFMpegBinaryPath(), arguments); val.OutputDataReceived += delegate(object e, string data) { Codec codec = parser2(data); if (codec != null) { if (codecs2.TryGetValue(codec.Name, out Codec value)) { value.Merge(codec); } else { codecs2.Add(codec.Name, codec); } } }; IProcessResult val2 = ProcessArgumentsExtensions.StartAndWaitForExit(val); if (val2.ExitCode != 0) { throw new FFMpegException(FFMpegExceptionType.Process, string.Join("\r\n", val2.OutputData)); } } internal static Dictionary<string, Codec> GetCodecsInternal() { Dictionary<string, Codec> dictionary = new Dictionary<string, Codec>(); ParsePartOfCodecs(dictionary, "-codecs", (string s) => Codec.TryParseFromCodecs(s, out Codec codec3) ? codec3 : null); ParsePartOfCodecs(dictionary, "-encoders", (string s) => Codec.TryParseFromEncodersDecoders(s, out Codec codec2, isEncoder: true) ? codec2 : null); ParsePartOfCodecs(dictionary, "-decoders", (string s) => Codec.TryParseFromEncodersDecoders(s, out Codec codec, isEncoder: false) ? codec : null); return dictionary; } public static IReadOnlyList<Codec> GetCodecs() { if (!GlobalFFOptions.Current.UseCache) { return GetCodecsInternal().Values.ToList().AsReadOnly(); } return FFMpegCache.Codecs.Values.ToList().AsReadOnly(); } public static IReadOnlyList<Codec> GetCodecs(CodecType type) { if (!GlobalFFOptions.Current.UseCache) { return GetCodecsInternal().Values.Where((Codec x) => x.Type == type).ToList().AsReadOnly(); } return FFMpegCache.Codecs.Values.Where((Codec x) => x.Type == type).ToList().AsReadOnly(); } public static IReadOnlyList<Codec> GetVideoCodecs() { return GetCodecs(CodecType.Video); } public static IReadOnlyList<Codec> GetAudioCodecs() { return GetCodecs(CodecType.Audio); } public static IReadOnlyList<Codec> GetSubtitleCodecs() { return GetCodecs(CodecType.Subtitle); } public static IReadOnlyList<Codec> GetDataCodecs() { return GetCodecs(CodecType.Data); } public static bool TryGetCodec(string name, out Codec codec) { string name2 = name; if (!GlobalFFOptions.Current.UseCache) { codec = GetCodecsInternal().Values.FirstOrDefault((Codec x) => x.Name == name2.ToLowerInvariant().Trim()); return codec != null; } return FFMpegCache.Codecs.TryGetValue(name2, out codec); } public static Codec GetCodec(string name) { if (TryGetCodec(name, out Codec codec) && codec != null) { return codec; } throw new FFMpegException(FFMpegExceptionType.Operation, "Codec \"" + name + "\" not supported"); } internal static IReadOnlyList<ContainerFormat> GetContainersFormatsInternal() { //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Expected O, but got Unknown FFMpegHelper.RootExceptionCheck(); List<ContainerFormat> list = new List<ContainerFormat>(); ProcessArguments val = new ProcessArguments(GlobalFFOptions.GetFFMpegBinaryPath(), "-formats"); val.OutputDataReceived += delegate(object e, string data) { if (ContainerFormat.TryParse(data, out ContainerFormat fmt)) { list.Add(fmt); } }; IProcessResult val2 = ProcessArgumentsExtensions.StartAndWaitForExit(val); if (val2.ExitCode != 0) { throw new FFMpegException(FFMpegExceptionType.Process, string.Join("\r\n", val2.OutputData)); } return list.AsReadOnly(); } public static IReadOnlyList<ContainerFormat> GetContainerFormats() { if (!GlobalFFOptions.Current.UseCache) { return GetContainersFormatsInternal(); } return FFMpegCache.ContainerFormats.Values.ToList().AsReadOnly(); } public static bool TryGetContainerFormat(string name, out ContainerFormat fmt) { string name2 = name; if (!GlobalFFOptions.Current.UseCache) { fmt = GetContainersFormatsInternal().FirstOrDefault((ContainerFormat x) => x.Name == name2.ToLowerInvariant().Trim()); return fmt != null; } return FFMpegCache.ContainerFormats.TryGetValue(name2, out fmt); } public static ContainerFormat GetContainerFormat(string name) { if (TryGetContainerFormat(name, out ContainerFormat fmt)) { return fmt; } throw new FFMpegException(FFMpegExceptionType.Operation, "Container format \"" + name + "\" not supported"); } private static void Cleanup(IEnumerable<string> pathList) { foreach (string path in pathList) { if (File.Exists(path)) { File.Delete(path); } } } } public class FFMpegArgumentOptions : FFMpegArgumentsBase { internal FFMpegArgumentOptions() { } public FFMpegArgumentOptions WithAudioCodec(Codec audioCodec) { return WithArgument(new AudioCodecArgument(audioCodec)); } public FFMpegArgumentOptions WithAudioCodec(string audioCodec) { return WithArgument(new AudioCodecArgument(audioCodec)); } public FFMpegArgumentOptions WithAudioBitrate(AudioQuality audioQuality) { return WithArgument(new AudioBitrateArgument(audioQuality)); } public FFMpegArgumentOptions WithAudioBitrate(int bitrate) { return WithArgument(new AudioBitrateArgument(bitrate)); } public FFMpegArgumentOptions WithAudioSamplingRate(int samplingRate = 48000) { return WithArgument(new AudioSamplingRateArgument(samplingRate)); } public FFMpegArgumentOptions WithVariableBitrate(int vbr) { return WithArgument(new VariableBitRateArgument(vbr)); } public FFMpegArgumentOptions Resize(int width, int height) { return WithArgument(new SizeArgument(width, height)); } public FFMpegArgumentOptions Resize(Size? size) { return WithArgument(new SizeArgument(size)); } public FFMpegArgumentOptions WithBitStreamFilter(Channel channel, Filter filter) { return WithArgument(new BitStreamFilterArgument(channel, filter)); } public FFMpegArgumentOptions WithConstantRateFactor(int crf) { return WithArgument(new ConstantRateFactorArgument(crf)); } public FFMpegArgumentOptions CopyChannel(Channel channel = Channel.Both) { return WithArgument(new CopyArgument(channel)); } public FFMpegArgumentOptions DisableChannel(Channel channel) { return WithArgument(new DisableChannelArgument(channel)); } public FFMpegArgumentOptions WithDuration(TimeSpan? duration) { return WithArgument(new DurationArgument(duration)); } public FFMpegArgumentOptions WithFastStart() { return WithArgument(new FaststartArgument()); } public FFMpegArgumentOptions WithFrameOutputCount(int frames) { return WithArgument(new FrameOutputCountArgument(frames)); } public FFMpegArgumentOptions WithHardwareAcceleration(HardwareAccelerationDevice hardwareAccelerationDevice = HardwareAccelerationDevice.Auto) { return WithArgument(new HardwareAccelerationArgument(hardwareAccelerationDevice)); } public FFMpegArgumentOptions UsingShortest(bool shortest = true) { return WithArgument(new ShortestArgument(shortest)); } public FFMpegArgumentOptions UsingMultithreading(bool multithread) { return WithArgument(new ThreadsArgument(multithread)); } public FFMpegArgumentOptions UsingThreads(int threads) { return WithArgument(new ThreadsArgument(threads)); } public FFMpegArgumentOptions WithVideoCodec(Codec videoCodec) { return WithArgument(new VideoCodecArgument(videoCodec)); } public FFMpegArgumentOptions WithVideoCodec(string videoCodec) { return WithArgument(new VideoCodecArgument(videoCodec)); } public FFMpegArgumentOptions WithVideoBitrate(int bitrate) { return WithArgument(new VideoBitrateArgument(bitrate)); } public FFMpegArgumentOptions WithVideoFilters(Action<VideoFilterOptions> videoFilterOptions) { VideoFilterOptions videoFilterOptions2 = new VideoFilterOptions(); videoFilterOptions(videoFilterOptions2); return WithArgument(new VideoFiltersArgument(videoFilterOptions2)); } public FFMpegArgumentOptions WithAudioFilters(Action<AudioFilterOptions> audioFilterOptions) { AudioFilterOptions audioFilterOptions2 = new AudioFilterOptions(); audioFilterOptions(audioFilterOptions2); return WithArgument(new AudioFiltersArgument(audioFilterOptions2)); } public FFMpegArgumentOptions WithFramerate(double framerate) { return WithArgument(new FrameRateArgument(framerate)); } public FFMpegArgumentOptions WithoutMetadata() { return WithArgument(new RemoveMetadataArgument()); } public FFMpegArgumentOptions WithSpeedPreset(Speed speed) { return WithArgument(new SpeedPresetArgument(speed)); } public FFMpegArgumentOptions WithStartNumber(int startNumber) { return WithArgument(new StartNumberArgument(startNumber)); } public FFMpegArgumentOptions WithCustomArgument(string argument) { return WithArgument(new CustomArgument(argument)); } public FFMpegArgumentOptions Seek(TimeSpan? seekTo) { return WithArgument(new SeekArgument(seekTo)); } public FFMpegArgumentOptions EndSeek(TimeSpan? seekTo) { return WithArgument(new EndSeekArgument(seekTo)); } public FFMpegArgumentOptions Loop(int times) { return WithArgument(new LoopArgument(times)); } public FFMpegArgumentOptions OverwriteExisting() { return WithArgument(new OverwriteArgument()); } public FFMpegArgumentOptions SelectStream(int streamIndex, int inputFileIndex = 0, Channel channel = Channel.All) { return WithArgument(new MapStreamArgument(streamIndex, inputFileIndex, channel)); } public FFMpegArgumentOptions SelectStreams(IEnumerable<int> streamIndices, int inputFileIndex = 0, Channel channel = Channel.All) { return streamIndices.Aggregate(this, (FFMpegArgumentOptions options, int streamIndex) => options.SelectStream(streamIndex, inputFileIndex, channel)); } public FFMpegArgumentOptions DeselectStream(int streamIndex, int inputFileIndex = 0, Channel channel = Channel.All) { return WithArgument(new MapStreamArgument(streamIndex, inputFileIndex, channel, negativeMap: true)); } public FFMpegArgumentOptions DeselectStreams(IEnumerable<int> streamIndices, int inputFileIndex = 0, Channel channel = Channel.All) { return streamIndices.Aggregate(this, (FFMpegArgumentOptions options, int streamIndex) => options.DeselectStream(streamIndex, inputFileIndex, channel)); } public FFMpegArgumentOptions ForceFormat(ContainerFormat format) { return WithArgument(new ForceFormatArgument(format)); } public FFMpegArgumentOptions ForceFormat(string format) { return WithArgument(new ForceFormatArgument(format)); } public FFMpegArgumentOptions ForcePixelFormat(string pixelFormat) { return WithArgument(new ForcePixelFormat(pixelFormat)); } public FFMpegArgumentOptions ForcePixelFormat(PixelFormat pixelFormat) { return WithArgument(new ForcePixelFormat(pixelFormat)); } public FFMpegArgumentOptions WithAudibleEncryptionKeys(string key, string iv) { return WithArgument(new AudibleEncryptionKeyArgument(key, iv)); } public FFMpegArgumentOptions WithAudibleActivationBytes(string activationBytes) { return WithArgument(new AudibleEncryptionKeyArgument(activationBytes)); } public FFMpegArgumentOptions WithTagVersion(int id3v2Version = 3) { return WithArgument(new ID3V2VersionArgument(id3v2Version)); } public FFMpegArgumentOptions WithGifPaletteArgument(int streamIndex, Size? size, int fps = 12) { return WithArgument(new GifPaletteArgument(streamIndex, fps, size)); } public FFMpegArgumentOptions WithArgument(IArgument argument) { Arguments.Add(argument); return this; } } public class FFMpegArgumentProcessor { private static readonly Regex ProgressRegex = new Regex("time=(\\d\\d:\\d\\d:\\d\\d.\\d\\d?)", RegexOptions.Compiled); private readonly List<Action<FFOptions>> _configurations; private readonly FFMpegArguments _ffMpegArguments; private Action<double>? _onPercentageProgress; private Action<TimeSpan>? _onTimeProgress; private Action<string>? _onOutput; private Action<string>? _onError; private TimeSpan? _totalTimespan; private FFMpegLogLevel? _logLevel; public string Arguments => _ffMpegArguments.Text; private event EventHandler<int> CancelEvent; internal FFMpegArgumentProcessor(FFMpegArguments ffMpegArguments) { _configurations = new List<Action<FFOptions>>(); _ffMpegArguments = ffMpegArguments; } public FFMpegArgumentProcessor NotifyOnProgress(Action<double> onPercentageProgress, TimeSpan totalTimeSpan) { _totalTimespan = totalTimeSpan; _onPercentageProgress = onPercentageProgress; return this; } public FFMpegArgumentProcessor NotifyOnProgress(Action<TimeSpan> onTimeProgress) { _onTimeProgress = onTimeProgress; return this; } public FFMpegArgumentProcessor NotifyOnOutput(Action<string> onOutput) { _onOutput = onOutput; return this; } public FFMpegArgumentProcessor NotifyOnError(Action<string> onError) { _onError = onError; return this; } public FFMpegArgumentProcessor CancellableThrough(out Action cancel, int timeout = 0) { cancel = delegate { this.CancelEvent?.Invoke(this, timeout); }; return this; } public FFMpegArgumentProcessor CancellableThrough(CancellationToken token, int timeout = 0) { token.Register(delegate { this.CancelEvent?.Invoke(this, timeout); }); return this; } public FFMpegArgumentProcessor Configure(Action<FFOptions> configureOptions) { _configurations.Add(configureOptions); return this; } public FFMpegArgumentProcessor WithLogLevel(FFMpegLogLevel logLevel) { _logLevel = logLevel; return this; } public bool ProcessSynchronously(bool throwOnError = true, FFOptions? ffMpegOptions = null) { FFOptions configuredOptions = GetConfiguredOptions(ffMpegOptions); CancellationTokenSource cancellationTokenSource; ProcessArguments processArguments = PrepareProcessArguments(configuredOptions, out cancellationTokenSource); IProcessResult val = null; try { val = Process(processArguments, cancellationTokenSource).ConfigureAwait(continueOnCapturedContext: false).GetAwaiter().GetResult(); } catch (OperationCanceledException) { if (throwOnError) { throw; } } return HandleCompletion(throwOnError, (val != null) ? val.ExitCode : (-1), ((val != null) ? val.ErrorData : null) ?? Array.Empty<string>()); } public async Task<bool> ProcessAsynchronously(bool throwOnError = true, FFOptions? ffMpegOptions = null) { FFOptions configuredOptions = GetConfiguredOptions(ffMpegOptions); CancellationTokenSource cancellationTokenSource; ProcessArguments processArguments = PrepareProcessArguments(configuredOptions, out cancellationTokenSource); IProcessResult processResult = null; try { processResult = await Process(processArguments, cancellationTokenSource).ConfigureAwait(continueOnCapturedContext: false); } catch (OperationCanceledException) { if (throwOnError) { throw; } } FFMpegArgumentProcessor fFMpegArgumentProcessor = this; IProcessResult obj = processResult; int exitCode = ((obj != null) ? obj.ExitCode : (-1)); IProcessResult obj2 = processResult; return fFMpegArgumentProcessor.HandleCompletion(throwOnError, exitCode, ((obj2 != null) ? obj2.ErrorData : null) ?? Array.Empty<string>()); } private async Task<IProcessResult> Process(ProcessArguments processArguments, CancellationTokenSource cancellationTokenSource) { CancellationTokenSource cancellationTokenSource2 = cancellationTokenSource; IProcessResult processResult = null; _ffMpegArguments.Pre(); IProcessInstance instance = processArguments.Start(); bool cancelled; try { cancelled = false; CancelEvent += OnCancelEvent; try { await Task.WhenAll(instance.WaitForExitAsync(default(CancellationToken)).ContinueWith(delegate(Task<IProcessResult> t) { processResult = t.Result; cancellationTokenSource2.Cancel(); _ffMpegArguments.Post(); }), _ffMpegArguments.During(cancellationTokenSource2.Token)).ConfigureAwait(continueOnCapturedContext: false); if (cancelled) { throw new OperationCanceledException("ffmpeg processing was cancelled"); } return processResult; } finally { CancelEvent -= OnCancelEvent; } } finally { if (instance != null) { ((IDisposable)instance).Dispose(); } } void OnCancelEvent(object sender, int timeout) { cancelled = true; instance.SendInput("q"); if (!cancellationTokenSource2.Token.WaitHandle.WaitOne(timeout, exitContext: true)) { cancellationTokenSource2.Cancel(); instance.Kill(); } } } private bool HandleCompletion(bool throwOnError, int exitCode, IReadOnlyList<string> errorData) { if (throwOnError && exitCode != 0) { throw new FFMpegException(FFMpegExceptionType.Process, string.Format("ffmpeg exited with non-zero exit-code ({0} - {1})", exitCode, string.Join("\n", errorData)), null, string.Join("\n", errorData)); } _onPercentageProgress?.Invoke(100.0); if (_totalTimespan.HasValue) { _onTimeProgress?.Invoke(_totalTimespan.Value); } return exitCode == 0; } internal FFOptions GetConfiguredOptions(FFOptions? ffOptions) { FFOptions fFOptions = ffOptions ?? GlobalFFOptions.Current.Clone(); foreach (Action<FFOptions> configuration in _configurations) { configuration(fFOptions); } return fFOptions; } private ProcessArguments PrepareProcessArguments(FFOptions ffOptions, out CancellationTokenSource cancellationTokenSource) { //IL_009d: Unknown result type (might be due to invalid IL or missing references) //IL_00a3: Expected O, but got Unknown FFMpegHelper.RootExceptionCheck(); FFMpegHelper.VerifyFFMpegExists(ffOptions); string text = _ffMpegArguments.Text; if (!_logLevel.HasValue) { _logLevel = ffOptions.LogLevel; } if (_logLevel.HasValue) { string text2 = _logLevel.ToString().ToLower(); text = text + " -v " + text2; } ProcessArguments val = new ProcessArguments(new ProcessStartInfo { FileName = GlobalFFOptions.GetFFMpegBinaryPath(ffOptions), Arguments = text, StandardOutputEncoding = ffOptions.Encoding, StandardErrorEncoding = ffOptions.Encoding, WorkingDirectory = ffOptions.WorkingDirectory }); cancellationTokenSource = new CancellationTokenSource(); if (_onOutput != null) { val.OutputDataReceived += OutputData; } if (_onError != null || _onTimeProgress != null || (_onPercentageProgress != null && _totalTimespan.HasValue)) { val.ErrorDataReceived += ErrorData; } return val; } private void ErrorData(object sender, string msg) { _onError?.Invoke(msg); Match match = ProgressRegex.Match(msg); if (match.Success) { TimeSpan obj = TimeSpan.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); _onTimeProgress?.Invoke(obj); if (_onPercentageProgress != null && _totalTimespan.HasValue) { double obj2 = Math.Round(obj.TotalSeconds / _totalTimespan.Value.TotalSeconds * 100.0, 2); _onPercentageProgress(obj2); } } } private void OutputData(object sender, string msg) { _onOutput?.Invoke(msg); } } public sealed class FFMpegArguments : FFMpegArgumentsBase { private readonly FFMpegGlobalArguments _globalArguments = new FFMpegGlobalArguments(); public string Text => GetText(); private FFMpegArguments() { } private string GetText() { IArgument[] allArguments = _globalArguments.Arguments.Concat(Arguments).ToArray(); return string.Join(" ", allArguments.Select((IArgument arg) => (!(arg is IDynamicArgument dynamicArgument)) ? arg.Text : dynamicArgument.GetText(allArguments))); } public static FFMpegArguments FromConcatInput(IEnumerable<string> filePaths, Action<FFMpegArgumentOptions>? addArguments = null) { return new FFMpegArguments().WithInput(new ConcatArgument(filePaths), addArguments); } public static FFMpegArguments FromDemuxConcatInput(IEnumerable<string> filePaths, Action<FFMpegArgumentOptions>? addArguments = null) { return new FFMpegArguments().WithInput(new DemuxConcatArgument(filePaths), addArguments); } public static FFMpegArguments FromFileInput(string filePath, bool verifyExists = true, Action<FFMpegArgumentOptions>? addArguments = null) { return new FFMpegArguments().WithInput(new InputArgument(verifyExists, filePath), addArguments); } public static FFMpegArguments FromFileInput(FileInfo fileInfo, Action<FFMpegArgumentOptions>? addArguments = null) { return new FFMpegArguments().WithInput(new InputArgument(fileInfo.FullName, verifyExists: false), addArguments); } public static FFMpegArguments FromUrlInput(Uri uri, Action<FFMpegArgumentOptions>? addArguments = null) { return new FFMpegArguments().WithInput(new InputArgument(uri.AbsoluteUri, verifyExists: false), addArguments); } public static FFMpegArguments FromDeviceInput(string device, Action<FFMpegArgumentOptions>? addArguments = null) { return new FFMpegArguments().WithInput(new InputDeviceArgument(device), addArguments); } public static FFMpegArguments FromPipeInput(IPipeSource sourcePipe, Action<FFMpegArgumentOptions>? addArguments = null) { return new FFMpegArguments().WithInput(new InputPipeArgument(sourcePipe), addArguments); } public FFMpegArguments WithGlobalOptions(Action<FFMpegGlobalArguments> configureOptions) { configureOptions(_globalArguments); return this; } public FFMpegArguments AddConcatInput(IEnumerable<string> filePaths, Action<FFMpegArgumentOptions>? addArguments = null) { return WithInput(new ConcatArgument(filePaths), addArguments); } public FFMpegArguments AddDemuxConcatInput(IEnumerable<string> filePaths, Action<FFMpegArgumentOptions>? addArguments = null) { return WithInput(new DemuxConcatArgument(filePaths), addArguments); } public FFMpegArguments AddFileInput(string filePath, bool verifyExists = true, Action<FFMpegArgumentOptions>? addArguments = null) { return WithInput(new InputArgument(verifyExists, filePath), addArguments); } public FFMpegArguments AddFileInput(FileInfo fileInfo, Action<FFMpegArgumentOptions>? addArguments = null) { return WithInput(new InputArgument(fileInfo.FullName, verifyExists: false), addArguments); } public FFMpegArguments AddUrlInput(Uri uri, Action<FFMpegArgumentOptions>? addArguments = null) { return WithInput(new InputArgument(uri.AbsoluteUri, verifyExists: false), addArguments); } public FFMpegArguments AddDeviceInput(string device, Action<FFMpegArgumentOptions>? addArguments = null) { return WithInput(new InputDeviceArgument(device), addArguments); } public FFMpegArguments AddPipeInput(IPipeSource sourcePipe, Action<FFMpegArgumentOptions>? addArguments = null) { return WithInput(new InputPipeArgument(sourcePipe), addArguments); } public FFMpegArguments AddMetaData(string content, Action<FFMpegArgumentOptions>? addArguments = null) { return WithInput(new MetaDataArgument(content), addArguments); } public FFMpegArguments AddMetaData(IReadOnlyMetaData metaData, Action<FFMpegArgumentOptions>? addArguments = null) { return WithInput(new MetaDataArgument(MetaDataSerializer.Instance.Serialize(metaData)), addArguments); } public FFMpegArguments MapMetaData(int? inputIndex = null, Action<FFMpegArgumentOptions>? addArguments = null) { return WithInput(new MapMetadataArgument(inputIndex), addArguments); } private FFMpegArguments WithInput(IInputArgument inputArgument, Action<FFMpegArgumentOptions>? addArguments) { FFMpegArgumentOptions fFMpegArgumentOptions = new FFMpegArgumentOptions(); addArguments?.Invoke(fFMpegArgumentOptions); Arguments.AddRange(fFMpegArgumentOptions.Arguments); Arguments.Add(inputArgument); return this; } public FFMpegArgumentProcessor OutputToFile(string file, bool overwrite = true, Action<FFMpegArgumentOptions>? addArguments = null) { return ToProcessor(new OutputArgument(file, overwrite), addArguments); } public FFMpegArgumentProcessor OutputToUrl(string uri, Action<FFMpegArgumentOptions>? addArguments = null) { return ToProcessor(new OutputUrlArgument(uri), addArguments); } public FFMpegArgumentProcessor OutputToUrl(Uri uri, Action<FFMpegArgumentOptions>? addArguments = null) { return ToProcessor(new OutputUrlArgument(uri.ToString()), addArguments); } public FFMpegArgumentProcessor OutputToPipe(IPipeSink reader, Action<FFMpegArgumentOptions>? addArguments = null) { return ToProcessor(new OutputPipeArgument(reader), addArguments); } private FFMpegArgumentProcessor ToProcessor(IOutputArgument argument, Action<FFMpegArgumentOptions>? addArguments) { FFMpegArgumentOptions fFMpegArgumentOptions = new FFMpegArgumentOptions(); addArguments?.Invoke(fFMpegArgumentOptions); Arguments.AddRange(fFMpegArgumentOptions.Arguments); Arguments.Add(argument); return new FFMpegArgumentProcessor(this); } internal void Pre() { foreach (IInputOutputArgument item in Arguments.OfType<IInputOutputArgument>()) { item.Pre(); } } internal async Task During(CancellationToken cancellationToken = default(CancellationToken)) { await Task.WhenAll(from io in Arguments.OfType<IInputOutputArgument>() select io.During(cancellationToken)).ConfigureAwait(continueOnCapturedContext: false); } internal void Post() { foreach (IInputOutputArgument item in Arguments.OfType<IInputOutputArgument>()) { item.Post(); } } } public abstract class FFMpegArgumentsBase { internal readonly List<IArgument> Arguments = new List<IArgument>(); } internal static class FFMpegCache { private static readonly object _syncObject = new object(); private static Dictionary<string, PixelFormat>? _pixelFormats; private static Dictionary<string, Codec>? _codecs; private static Dictionary<string, ContainerFormat>? _containers; public static IReadOnlyDictionary<string, PixelFormat> PixelFormats { get { if (_pixelFormats == null) { lock (_syncObject) { if (_pixelFormats == null) { _pixelFormats = FFMpeg.GetPixelFormatsInternal().ToDictionary((PixelFormat x) => x.Name); } } } return _pixelFormats; } } public static IReadOnlyDictionary<string, Codec> Codecs { get { if (_codecs == null) { lock (_syncObject) { if (_codecs == null) { _codecs = FFMpeg.GetCodecsInternal(); } } } return _codecs; } } public static IReadOnlyDictionary<string, ContainerFormat> ContainerFormats { get { if (_containers == null) { lock (_syncObject) { if (_containers == null) { _containers = FFMpeg.GetContainersFormatsInternal().ToDictionary((ContainerFormat x) => x.Name); } } } return _containers; } } } public sealed class FFMpegGlobalArguments : FFMpegArgumentsBase { internal FFMpegGlobalArguments() { } public FFMpegGlobalArguments WithVerbosityLevel(VerbosityLevel verbosityLevel = VerbosityLevel.Error) { return WithOption(new VerbosityLevelArgument(verbosityLevel)); } private FFMpegGlobalArguments WithOption(IArgument argument) { Arguments.Add(argument); return this; } } public static class SnapshotArgumentBuilder { public static (FFMpegArguments, Action<FFMpegArgumentOptions> outputOptions) BuildSnapshotArguments(string input, IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0) { TimeSpan valueOrDefault = captureTime.GetValueOrDefault(); if (!captureTime.HasValue) { valueOrDefault = TimeSpan.FromSeconds(source.Duration.TotalSeconds / 3.0); captureTime = valueOrDefault; } size = PrepareSnapshotSize(source, size); int valueOrDefault2 = streamIndex.GetValueOrDefault(); if (!streamIndex.HasValue) { valueOrDefault2 = source.PrimaryVideoStream?.Index ?? source.VideoStreams.FirstOrDefault()?.Index ?? 0; streamIndex = valueOrDefault2; } return (FFMpegArguments.FromFileInput(input, verifyExists: false, delegate(FFMpegArgumentOptions options) { options.Seek(captureTime); }), delegate(FFMpegArgumentOptions options) { options.SelectStream(streamIndex.Value, inputFileIndex).WithVideoCodec(VideoCodec.Png).WithFrameOutputCount(1) .Resize(size); }); } public static (FFMpegArguments, Action<FFMpegArgumentOptions> outputOptions) BuildGifSnapshotArguments(string input, IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null, TimeSpan? duration = null, int? streamIndex = null, int fps = 12) { Size size2 = new Size(480, -1); TimeSpan valueOrDefault = captureTime.GetValueOrDefault(); if (!captureTime.HasValue) { valueOrDefault = TimeSpan.FromSeconds(source.Duration.TotalSeconds / 3.0); captureTime = valueOrDefault; } size = PrepareSnapshotSize(source, size) ?? size2; int valueOrDefault2 = streamIndex.GetValueOrDefault(); if (!streamIndex.HasValue) { valueOrDefault2 = source.PrimaryVideoStream?.Index ?? source.VideoStreams.FirstOrDefault()?.Index ?? 0; streamIndex = valueOrDefault2; } return (FFMpegArguments.FromFileInput(input, verifyExists: false, delegate(FFMpegArgumentOptions options) { options.Seek(captureTime).WithDuration(duration); }), delegate(FFMpegArgumentOptions options) { options.WithGifPaletteArgument(streamIndex.Value, size, fps); }); } private static Size? PrepareSnapshotSize(IMediaAnalysis source, Size? wantedSize) { if (!wantedSize.HasValue || (wantedSize.Value.Height <= 0 && wantedSize.Value.Width <= 0) || source.PrimaryVideoStream == null) { return null; } Size size = new Size(source.PrimaryVideoStream.Width, source.PrimaryVideoStream.Height); if (source.PrimaryVideoStream.Rotation == 90 || source.PrimaryVideoStream.Rotation == 180) { size = new Size(source.PrimaryVideoStream.Height, source.PrimaryVideoStream.Width); } if (wantedSize.Value.Width != size.Width || wantedSize.Value.Height != size.Height) { if (wantedSize.Value.Width <= 0 && wantedSize.Value.Height > 0) { double num = (double)wantedSize.Value.Height / (double)size.Height; return new Size((int)((double)size.Width * num), (int)((double)size.Height * num)); } if (wantedSize.Value.Height <= 0 && wantedSize.Value.Width > 0) { double num2 = (double)wantedSize.Value.Width / (double)size.Width; return new Size((int)((double)size.Width * num2), (int)((double)size.Height * num2)); } return wantedSize; } return null; } } public class FFOptions : ICloneable { public string WorkingDirectory { get; set; } = string.Empty; public string BinaryFolder { get; set; } = string.Empty; public string TemporaryFilesFolder { get; set; } = Path.GetTempPath(); public Encoding Encoding { get; set; } = System.Text.Encoding.Default; public FFMpegLogLevel? LogLevel { get; set; } public Dictionary<string, string> ExtensionOverrides { get; set; } = new Dictionary<string, string> { { "mpegts", ".ts" } }; public bool UseCache { get; set; } = true; object ICloneable.Clone() { return Clone(); } public FFOptions Clone() { return (FFOptions)MemberwiseClone(); } } public class AudioStream : MediaStream { public int Channels { get; set; } public string ChannelLayout { get; set; } public int SampleRateHz { get; set; } public string Profile { get; set; } } public static class FFProbe { public static IMediaAnalysis Analyse(string filePath, FFOptions? ffOptions = null) { ThrowIfInputFileDoesNotExist(filePath); IProcessResult obj = PrepareStreamAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current).StartAndWaitForExit(); ThrowIfExitCodeNotZero(obj); return ParseOutput(obj); } public static FFProbeFrames GetFrames(string filePath, FFOptions? ffOptions = null) { ThrowIfInputFileDoesNotExist(filePath); IProcessResult obj = PrepareFrameAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current).StartAndWaitForExit(); ThrowIfExitCodeNotZero(obj); return ParseFramesOutput(obj); } public static FFProbePackets GetPackets(string filePath, FFOptions? ffOptions = null) { ThrowIfInputFileDoesNotExist(filePath); IProcessResult obj = PreparePacketAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current).StartAndWaitForExit(); ThrowIfExitCodeNotZero(obj); return ParsePacketsOutput(obj); } public static IMediaAnalysis Analyse(Uri uri, FFOptions? ffOptions = null) { IProcessResult obj = PrepareStreamAnalysisInstance(uri.AbsoluteUri, ffOptions ?? GlobalFFOptions.Current).StartAndWaitForExit(); ThrowIfExitCodeNotZero(obj); return ParseOutput(obj); } public static IMediaAnalysis Analyse(Stream stream, FFOptions? ffOptions = null) { InputPipeArgument inputPipeArgument = new InputPipeArgument(new StreamPipeSource(stream)); ProcessArguments processArguments = PrepareStreamAnalysisInstance(inputPipeArgument.PipePath, ffOptions ?? GlobalFFOptions.Current); inputPipeArgument.Pre(); Task<IProcessResult> task = processArguments.StartAndWaitForExitAsync(); try { inputPipeArgument.During().ConfigureAwait(continueOnCapturedContext: false).GetAwaiter() .GetResult(); } catch (IOException) { } finally { inputPipeArgument.Post(); } IProcessResult result = task.ConfigureAwait(continueOnCapturedContext: false).GetAwaiter().GetResult(); ThrowIfExitCodeNotZero(result); return ParseOutput(result); } public static async Task<IMediaAnalysis> AnalyseAsync(string filePath, FFOptions? ffOptions = null, CancellationToken cancellationToken = default(CancellationToken)) { ThrowIfInputFileDoesNotExist(filePath); IProcessResult obj = await PrepareStreamAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current).StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false); ThrowIfExitCodeNotZero(obj); return ParseOutput(obj); } public static FFProbeFrames GetFrames(Uri uri, FFOptions? ffOptions = null) { IProcessResult obj = PrepareFrameAnalysisInstance(uri.AbsoluteUri, ffOptions ?? GlobalFFOptions.Current).StartAndWaitForExit(); ThrowIfExitCodeNotZero(obj); return ParseFramesOutput(obj); } public static async Task<FFProbeFrames> GetFramesAsync(string filePath, FFOptions? ffOptions = null, CancellationToken cancellationToken = default(CancellationToken)) { ThrowIfInputFileDoesNotExist(filePath); return ParseFramesOutput(await PrepareFrameAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current).StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false)); } public static async Task<FFProbePackets> GetPacketsAsync(string filePath, FFOptions? ffOptions = null, CancellationToken cancellationToken = default(CancellationToken)) { ThrowIfInputFileDoesNotExist(filePath); return ParsePacketsOutput(await PreparePacketAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current).StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false)); } public static async Task<IMediaAnalysis> AnalyseAsync(Uri uri, FFOptions? ffOptions = null, CancellationToken cancellationToken = default(CancellationToken)) { IProcessResult obj = await PrepareStreamAnalysisInstance(uri.AbsoluteUri, ffOptions ?? GlobalFFOptions.Current).StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false); ThrowIfExitCodeNotZero(obj); return ParseOutput(obj); } public static async Task<IMediaAnalysis> AnalyseAsync(Stream stream, FFOptions? ffOptions = null, CancellationToken cancellationToken = default(CancellationToken)) { StreamPipeSource writer = new StreamPipeSource(stream); InputPipeArgument pipeArgument = new InputPipeArgument(writer); ProcessArguments processArguments = PrepareStreamAnalysisInstance(pipeArgument.PipePath, ffOptions ?? GlobalFFOptions.Current); pipeArgument.Pre(); Task<IProcessResult> task = processArguments.StartAndWaitForExitAsync(cancellationToken); try { await pipeArgument.During(cancellationToken).ConfigureAwait(continueOnCapturedContext: false); } catch (IOException) { } finally { pipeArgument.Post(); } IProcessResult obj = await task.ConfigureAwait(continueOnCapturedContext: false); ThrowIfExitCodeNotZero(obj); pipeArgument.Post(); return ParseOutput(obj); } public static async Task<FFProbeFrames> GetFramesAsync(Uri uri, FFOptions? ffOptions = null, CancellationToken cancellationToken = default(CancellationToken)) { return ParseFramesOutput(await PrepareFrameAnalysisInstance(uri.AbsoluteUri, ffOptions ?? GlobalFFOptions.Current).StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false)); } private static IMediaAnalysis ParseOutput(IProcessResult instance) { FFProbeAnalysis? fFProbeAnalysis = JsonSerializer.Deserialize<FFProbeAnalysis>(string.Join(string.Empty, instance.OutputData), new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); if (fFProbeAnalysis?.Format == null) { throw new FormatNullException(); } fFProbeAnalysis.ErrorData = instance.ErrorData; return new MediaAnalysis(fFProbeAnalysis); } private static FFProbeFrames ParseFramesOutput(IProcessResult instance) { return JsonSerializer.Deserialize<FFProbeFrames>(string.Join(string.Empty, instance.OutputData), new JsonSerializerOptions { PropertyNameCaseInsensitive = true, NumberHandling = (JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString) }); } private static FFProbePackets ParsePacketsOutput(IProcessResult instance) { return JsonSerializer.Deserialize<FFProbePackets>(string.Join(string.Empty, instance.OutputData), new JsonSerializerOptions { PropertyNameCaseInsensitive = true, NumberHandling = (JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString) }); } private static void ThrowIfInputFileDoesNotExist(string filePath) { if (!File.Exists(filePath)) { throw new FFMpegException(FFMpegExceptionType.File, "No file found at '" + filePath + "'"); } } private static void ThrowIfExitCodeNotZero(IProcessResult result) { if (result.ExitCode != 0) { string message = string.Format("ffprobe exited with non-zero exit-code ({0} - {1})", result.ExitCode, string.Join("\n", result.ErrorData)); throw new FFMpegException(FFMpegExceptionType.Process, message, null, string.Join("\n", result.ErrorData)); } } private static ProcessArguments PrepareStreamAnalysisInstance(string filePath, FFOptions ffOptions) { return PrepareInstance("-loglevel error -print_format json -show_format -sexagesimal -show_streams \"" + filePath + "\"", ffOptions); } private static ProcessArguments PrepareFrameAnalysisInstance(string filePath, FFOptions ffOptions) { return PrepareInstance("-loglevel error -print_format json -show_frames -v quiet -sexagesimal \"" + filePath + "\"", ffOptions); } private static ProcessArguments PreparePacketAnalysisInstance(string filePath, FFOptions ffOptions) { return PrepareInstance("-loglevel error -print_format json -show_packets -v quiet -sexagesimal \"" + filePath + "\"", ffOptions); } private static ProcessArguments PrepareInstance(string arguments, FFOptions ffOptions) { //IL_003b: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Expected O, but got Unknown FFProbeHelper.RootExceptionCheck(); FFProbeHelper.VerifyFFProbeExists(ffOptions); return new ProcessArguments(new ProcessStartInfo(GlobalFFOptions.GetFFProbeBinaryPath(ffOptions), arguments) { StandardOutputEncoding = ffOptions.Encoding, StandardErrorEncoding = ffOptions.Encoding, WorkingDirectory = ffOptions.WorkingDirectory }); } } public class FFProbeAnalysis { [JsonPropertyName("streams")] public List<FFProbeStream> Streams { get; set; } [JsonPropertyName("format")] public Format Format { get; set; } [JsonIgnore] public IReadOnlyList<string> ErrorData { get; set; } = new List<string>(); } public class FFProbeStream : ITagsContainer, IDispositionContainer { [JsonPropertyName("index")] public int Index { get; set; } [JsonPropertyName("avg_frame_rate")] public string AvgFrameRate { get; set; } [JsonPropertyName("bits_per_raw_sample")] public string BitsPerRawSample { get; set; } [JsonPropertyName("bits_per_sample")] public int BitsPerSample { get; set; } [JsonPropertyName("bit_rate")] public string BitRate { get; set; } [JsonPropertyName("channels")] public int? Channels { get; set; } [JsonPropertyName("channel_layout")] public string ChannelLayout { get; set; } [JsonPropertyName("codec_type")] public string CodecType { get; set; } [JsonPropertyName("codec_name")] public string CodecName { get; set; } [JsonPropertyName("codec_long_name")] public string CodecLongName { get; set; } [JsonPropertyName("codec_tag")] public string CodecTag { get; set; } [JsonPropertyName("codec_tag_string")] public string CodecTagString { get; set; } [JsonPropertyName("display_aspect_ratio")] public string DisplayAspectRatio { get; set; } [JsonPropertyName("sample_aspect_ratio")] public string SampleAspectRatio { get; set; } [JsonPropertyName("start_time")] public string StartTime { get; set; } [JsonPropertyName("duration")] public string Duration { get; set; } [JsonPropertyName("profile")] public string Profile { get; set; } [JsonPropertyName("width")] public int? Width { get; set; } [JsonPropertyName("height")] public int? Height { get; set; } [JsonPropertyName("r_frame_rate")] public string FrameRate { get; set; } [JsonPropertyName("pix_fmt")] public string PixelFormat { get; set; } [JsonPropertyName("sample_rate")] public string SampleRate { get; set; } [JsonPropertyName("disposition")] public Dictionary<string, int> Disposition { get; set; } [JsonPropertyName("tags")] public Dictionary<string, string> Tags { get; set; } [JsonPropertyName("side_data_list")] public List<Dictionary<string, JsonValue>> SideData { get; set; } } public class Format : ITagsContainer { [JsonPropertyName("filename")] public string Filename { get; set; } [JsonPropertyName("nb_streams")] public int NbStreams { get; set; } [JsonPropertyName("nb_programs")] public int NbPrograms { get; set; } [JsonPropertyName("format_name")] public string FormatName { get; set; } [JsonPropertyName("format_long_name")] public string FormatLongName { get; set; } [JsonPropertyName("start_time")] public string StartTime { get; set; } [JsonPropertyName("duration")] public string Duration { get; set; } [JsonPropertyName("size")] public string Size { get; set; } [JsonPropertyName("bit_rate")] public string? BitRate { get; set; } [JsonPropertyName("probe_score")] public int ProbeScore { get; set; } [JsonPropertyName("tags")] public Dictionary<string, string> Tags { get; set; } } public interface IDispositionContainer { Dictionary<string, int> Disposition { get; set; } } public interface ITagsContainer { Dictionary<string, string> Tags { get; set; } } public static class TagExtensions { private static string? TryGetTagValue(ITagsContainer tagsContainer, string key) { if (tagsContainer.Tags != null && tagsContainer.Tags.TryGetValue(key, out string value)) { return value; } return null; } public static string? GetLanguage(this ITagsContainer tagsContainer) { return TryGetTagValue(tagsContainer, "language"); } public static string? GetCreationTime(this ITagsContainer tagsContainer) { return TryGetTagValue(tagsContainer, "creation_time "); } public static string? GetRotate(this ITagsContainer tagsContainer) { return TryGetTagValue(tagsContainer, "rotate"); } public static string? GetDuration(this ITagsContainer tagsContainer) { return TryGetTagValue(tagsContainer, "duration"); } } public static class DispositionExtensions { private static int? TryGetDispositionValue(IDispositionContainer dispositionContainer, string key) { if (dispositionContainer.Disposition != null && dispositionContainer.Disposition.TryGetValue(key, out var value)) { return value; } return null; } public static int? GetDefault(this IDispositionContainer tagsContainer) { return TryGetDispositionValue(tagsContainer, "default"); } public static int? GetForced(this IDispositionContainer tagsContainer) { return TryGetDispositionValue(tagsContainer, "forced"); } } public class FFProbeFrameAnalysis { [JsonPropertyName("media_type")] public string MediaType { get; set; } [JsonPropertyName("stream_index")] public int StreamIndex { get; set; } [JsonPropertyName("key_frame")] public int KeyFrame { get; set; } [JsonPropertyName("pkt_pts")] public long PacketPts { get; set; } [JsonPropertyName("pkt_pts_time")] public string PacketPtsTime { get; set; } [JsonPropertyName("pkt_dts")] public long PacketDts { get; set; } [JsonPropertyName("pkt_dts_time")] public string PacketDtsTime { get; set; } [JsonPropertyName("best_effort_timestamp")] public long BestEffortTimestamp { get; set; } [JsonPropertyName("best_effort_timestamp_time")] public string BestEffortTimestampTime { get; set; } [JsonPropertyName("pkt_duration")] public int PacketDuration { get; set; } [JsonPropertyName("pkt_duration_time")] public string PacketDurationTime { get; set; } [JsonPropertyName("pkt_pos")] public long PacketPos { get; set; } [JsonPropertyName("pkt_size")] public int PacketSize { get; set; } [JsonPropertyName("width")] public long Width { get; set; } [JsonPropertyName("height")] public long Height { get; set; } [JsonPropertyName("pix_fmt")] public string PixelFormat { get; set; } [JsonPropertyName("pict_type")] public string PictureType { get; set; } [JsonPropertyName("coded_picture_number")] public long CodedPictureNumber { get; set; } [JsonPropertyName("display_picture_number")] public long DisplayPictureNumber { get; set; } [JsonPropertyName("interlaced_frame")] public int InterlacedFrame { get; set; } [JsonPropertyName("top_field_first")] public int TopFieldFirst { get; set; } [JsonPropertyName("repeat_pict")] public int RepeatPicture { get; set; } [JsonPropertyName("chroma_location")] public string ChromaLocation { get; set; } } public class FFProbeFrames { [JsonPropertyName("frames")] public List<FFProbeFrameAnalysis> Frames { get; set; } } public interface IMediaAnalysis { TimeSpan Duration { get; } MediaFormat Format { get; } AudioStream? PrimaryAudioStream { get; } VideoStream? PrimaryVideoStream { get; } SubtitleStream? PrimarySubtitleStream { get; } List<VideoStream> VideoStreams { get; } List<AudioStream> AudioStreams { get; } List<SubtitleStream> SubtitleStreams { get; } IReadOnlyList<string> ErrorData { get; } } internal class MediaAnalysis : IMediaAnalysis { public TimeSpan Duration => new TimeSpan[3] { Format.Duration, PrimaryVideoStream?.Duration ?? TimeSpan.Zero, PrimaryAudioStream?.Duration ?? TimeSpan.Zero }.Max(); public MediaFormat Format { get; } public AudioStream? PrimaryAudioStream => AudioStreams.OrderBy((AudioStream stream) => stream.Index).FirstOrDefault(); public VideoStream? PrimaryVideoStream => VideoStreams.OrderBy((VideoStream stream) => stream.Index).FirstOrDefault(); public SubtitleStream? PrimarySubtitleStream => SubtitleStreams.OrderBy((SubtitleStream stream) => stream.Index).FirstOrDefault(); public List<VideoStream> VideoStreams { get; } public List<AudioStream> AudioStreams { get; } public List<SubtitleStream> SubtitleStreams { get; } public IReadOnlyList<string> ErrorData { get; } internal MediaAnalysis(FFProbeAnalysis analysis) { Format = ParseFormat(analysis.Format); VideoStreams = analysis.Streams.Where((FFProbeStream stream) => stream.CodecType == "video").Select(ParseVideoStream).ToList(); AudioStreams = analysis.Streams.Where((FFProbeStream stream) => stream.CodecType == "audio").Select(ParseAudioStream).ToList(); SubtitleStreams = analysis.Streams.Where((FFProbeStream stream) => stream.CodecType == "subtitle").Select(ParseSubtitleStream).ToList(); ErrorData = analysis.ErrorData; } private MediaFormat ParseFormat(Format analysisFormat) { return new MediaFormat { Duration = MediaAnalysisUtils.ParseDuration(analysisFormat.Duration), StartTime = MediaAnalysisUtils.ParseDuration(analysisFormat.StartTime), FormatName = analysisFormat.FormatName, FormatLongName = analysisFormat.FormatLongName, StreamCount = analysisFormat.NbStreams, ProbeScore = analysisFormat.ProbeScore, BitRate = long.Parse(analysisFormat.BitRate ?? "0"), Tags = analysisFormat.Tags.ToCaseInsensitive() }; } private int? GetBitDepth(FFProbeStream stream) { int result; int num = (int.TryParse(stream.BitsPerRawSample, out result) ? result : stream.BitsPerSample); if (num != 0) { return num; } return null; } private VideoStream ParseVideoStream(FFProbeStream stream) { return new VideoStream { Index = stream.Index, AvgFrameRate = MediaAnalysisUtils.DivideRatio(MediaAnalysisUtils.ParseRatioDouble(stream.AvgFrameRate, '/')), BitRate = ((!string.IsNullOrEmpty(stream.BitRate)) ? MediaAnalysisUtils.ParseLongInvariant(stream.BitRate) : 0), BitsPerRawSample = ((!string.IsNullOrEmpty(stream.BitsPerRawSample)) ? MediaAnalysisUtils.ParseIntInvariant(stream.BitsPerRawSample) : 0), CodecName = stream.CodecName, CodecLongName = stream.CodecLongName, CodecTag = stream.CodecTag, CodecTagString = stream.CodecTagString, DisplayAspectRatio = MediaAnalysisUtils.ParseRatioInt(stream.DisplayAspectRatio, ':'), SampleAspectRatio = MediaAnalysisUtils.ParseRatioInt(stream.SampleAspectRatio, ':'), Duration = MediaAnalysisUtils.ParseDuration(stream.Duration), StartTime = MediaAnalysisUtils.ParseDuration(stream.StartTime), FrameRate = MediaAnalysisUtils.DivideRatio(MediaAnalysisUtils.ParseRatioDouble(stream.FrameRate, '/')), Height = stream.Height.GetValueOrDefault(), Width = stream.Width.GetValueOrDefault(), Profile = stream.Profile, PixelFormat = stream.PixelFormat, Rotation = MediaAnalysisUtils.ParseRotation(stream), Language = stream.GetLanguage(), Disposition = MediaAnalysisUtils.FormatDisposition(stream.Disposition), Tags = stream.Tags.ToCaseInsensitive(), BitDepth = GetBitDepth(stream) }; } private AudioStream ParseAudioStream(FFProbeStream stream) { return new AudioStream { Index = stream.Index, BitRate = ((!string.IsNullOrEmpty(stream.BitRate)) ? MediaAnalysisUtils.ParseLongInvariant(stream.BitRate) : 0), CodecName = stream.CodecName, CodecLongName = stream.CodecLongName, CodecTag = stream.CodecTag, CodecTagString = stream.CodecTagString, Channels = stream.Channels.GetValueOrDefault(), ChannelLayout = stream.ChannelLayout, Duration = MediaAnalysisUtils.ParseDuration(stream.Duration), StartTime = MediaAnalysisUtils.ParseDuration(stream.StartTime), SampleRateHz = ((!string.IsNullOrEmpty(stream.SampleRate)) ? MediaAnalysisUtils.ParseIntInvariant(stream.SampleRate) : 0), Profile = stream.Profile, Language = stream.GetLanguage(), Disposition = MediaAnalysisUtils.FormatDisposition(stream.Disposition), Tags = stream.Tags.ToCaseInsensitive(), BitDepth = GetBitDepth(stream) }; } private SubtitleStream ParseSubtitleStream(FFProbeStream stream) { return new SubtitleStream { Index = stream.Index, BitRate = ((!string.IsNullOrEmpty(stream.BitRate)) ? MediaAnalysisUtils.ParseLongInvariant(stream.BitRate) : 0), CodecName = stream.CodecName, CodecLongName = stream.CodecLongName, Duration = MediaAnalysisUtils.ParseDuration(stream.Duration), StartTime = MediaAnalysisUtils.ParseDuration(stream.StartTime), Language = stream.GetLanguage(), Disposition = MediaAnalysisUtils.FormatDisposition(stream.Disposition), Tags = stream.Tags.ToCaseInsensitive() }; } } public static class MediaAnalysisUtils { private static readonly Regex DurationRegex = new Regex("^(\\d+):(\\d{1,2}):(\\d{1,2})\\.(\\d{1,3})", RegexOptions.Compiled); internal static Dictionary<string, string> ToCaseInsensitive(this Dictionary<string, string>? dictionary) { return dictionary?.ToDictionary<KeyValuePair<string, string>, string, string>((KeyValuePair<string, string> tag) => tag.Key, (KeyValuePair<string, string> tag) => tag.Value, StringComparer.OrdinalIgnoreCase) ?? new Dictionary<string, string>(); } public static double DivideRatio((double, double) ratio) { return ratio.Item1 / ratio.Item2; } public static (int, int) ParseRatioInt(string input, char separator) { if (string.IsNullOrEmpty(input)) { return (0, 0); } string[] array = input.Split(new char[1] { separator }); return (ParseIntInvariant(array[0]), ParseIntInvariant(array[1])); } public static (double, double) ParseRatioDouble(string input, char separator) { if (string.IsNullOrEmpty(input)) { return (0.0, 0.0); } string[] array = input.Split(new char[1] { separator }); return ((array.Length != 0) ? ParseDoubleInvariant(array[0]) : 0.0, (array.Length > 1) ? ParseDoubleInvariant(array[1]) : 0.0); } public static double ParseDoubleInvariant(string line) { return double.Parse(line, NumberStyles.Any, CultureInfo.InvariantCulture); } public static int ParseIntInvariant(string line) { return int.Parse(line, NumberStyles.Any, CultureInfo.InvariantCulture); } public static long ParseLongInvariant(string line) { return long.Parse(line, NumberStyles.Any, CultureInfo.InvariantCulture); } public static TimeSpan ParseDuration(string duration) { if (!string.IsNullOrEmpty(duration)) { Match match = DurationRegex.Match(duration); if (match.Success) { string text = match.Groups[4].Value; if (text.Length < 3) { text = text.PadRight(3, '0'); } int hours = int.Parse(match.Groups[1].Value); int minutes = int.Parse(match.Groups[2].Value); int seconds = int.Parse(match.Groups[3].Value); int milliseconds = int.Parse(text); return new TimeSpan(0, hours, minutes, seconds, milliseconds); } return TimeSpan.Zero; } return TimeSpan.Zero; } public static int ParseRotation(FFProbeStream fFProbeStream) { JsonValue value2; Dictionary<string, JsonValue>? obj = fFProbeStream.SideData?.Find((Dictionary<string, JsonValue> item) => item.TryGetValue("side_data_type", out value2) && value2.ToString() == "Display Matrix"); if (obj != null && obj.TryGetValue("rotation", out JsonValue value)) { return (int)float.Parse(value.ToString()); } return (int)float.Parse(fFProbeStream.GetRotate() ?? "0"); } public static Dictionary<string, bool>? FormatDisposition(Dictionary<string, int>? disposition) { if (disposition == null) { return null; } Dictionary<string, bool> dictionary = new Dictionary<string, bool>(disposition.Count, StringComparer.Ordinal); foreach (KeyValuePair<string, int> item in disposition) { dictionary.Add(item.Key, ToBool(item.Value)); } return dictionary; static bool ToBool(int value) { return value switch { 0 => false, 1 => true, _ => throw new ArgumentOutOfRangeException("value", $"Not expected disposition state value: {value}"), }; } } } public class MediaFormat { public TimeSpan Duration { get; set; } public TimeSpan StartTime { get; set; } public string FormatName { get; set; } public string FormatLongName { get; set; } public int StreamCount { get; set; } public double ProbeScore { get; set; } public double BitRate { get; set; } public Dictionary<string, string>? Tags { get; set; } } public abstract class MediaStream { public int Index { get; set; } public string CodecName { get; set; } public string CodecLongName { get; set; } public string CodecTagString { get; set; } public string CodecTag { get; set; } public long BitRate { get; set; } public TimeSpan StartTime { get; set; } public TimeSpan Duration { get; set; } public string? Language { get; set; } public Dictionary<string, bool>? Disposition { get; set; } public Dictionary<string, string>? Tags { get; set; } public int? BitDepth { get; set; } public Codec GetCodecInfo() { return FFMpeg.GetCodec(CodecName); } } public class FFProbePacketAnalysis { [JsonPropertyName("codec_type")] public string CodecType { get; set; } [JsonPropertyName("stream_index")] public int StreamIndex { get; set; } [JsonPropertyName("pts")] public long Pts { get; set; } [JsonPropertyName("pts_time")] public string PtsTime { get; set; } [JsonPropertyName("dts")] public long Dts { get; set; } [JsonPropertyName("dts_time")] public string DtsTime { get; set; } [JsonPropertyName("duration")] public int Duration { get; set; } [JsonPropertyName("duration_time")] public string DurationTime { get; set; } [JsonPropertyName("size")] public int Size { get; set; } [JsonPropertyName("pos")] public long Pos { get; set; } [JsonPropertyName("flags")] public string Flags { get; set; } } public class FFProbePackets { [JsonPropertyName("packets")] public List<FFProbePacketAnalysis> Packets { get; set; } } public static class ProcessArgumentsExtensions { public static IProcessResult StartAndWaitForExit(this ProcessArguments processArguments) { IProcessInstance val = processArguments.Start(); try { return val.WaitForExit(); } finally { ((IDisposable)val)?.Dispose(); } } public static async Task<IProcessResult> StartAndWaitForExitAsync(this ProcessArguments processArguments, CancellationToken cancellationToken = default(CancellationToken)) { IProcessInstance instance = processArguments.Start(); try { return await instance.WaitForExitAsync(cancellationToken); } finally { ((IDisposable)instance)?.Dispose(); } } } public class SubtitleStream : MediaStream { } public class VideoStream : MediaStream { public double AvgFrameRate { get; set; } public int BitsPerRawSample { get; set; } public (int Width, int Height) DisplayAspectRatio { get; set; } public (int Width, int Height) SampleAspectRatio { get; set; } public string Profile { get; set; } public int Width { get; set; } public int Height { get; set; } public double FrameRate { get; set; } public string PixelFormat { get; set; } public int Rotation { get; set; } public double AverageFrameRate { get; set; } public PixelFormat GetPixelFormatInfo() { return FFMpeg.GetPixelFormat(PixelFormat); } } public static class GlobalFFOptions { private const string ConfigFile = "ffmpeg.config.json"; private static FFOptions? _current; public static FFOptions Current => _current ?? (_current = LoadFFOptions()); public static void Configure(Action<FFOptions> optionsAction) { optionsAction(Current); } public static void Configure(FFOptions ffOptions) { _current = ffOptions ?? throw new ArgumentNullException("ffOptions"); } public static string GetFFMpegBinaryPath(FFOptions? ffOptions = null) { return GetFFBinaryPath("FFMpeg", ffOptions ?? Current); } public static string GetFFProbeBinaryPath(FFOptions? ffOptions = null) { return GetFFBinaryPath("FFProbe", ffOptions ?? Current); } private static string GetFFBinaryPath(string name, FFOptions ffOptions) { string text = name.ToLowerInvariant(); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { text += ".exe"; } string text2 = (Environment.Is64BitProcess ? "x64" : "x86"); if (Directory.Exists(Path.Combine(ffOptions.BinaryFolder, text2))) { text = Path.Combine(text2, text); } return Path.Combine(ffOptions.BinaryFolder, text); } private static FFOptions LoadFFOptions() { if (!File.Exists("ffmpeg.config.json")) { return new FFOptions(); } return JsonSerializer.Deserialize<FFOptions>(File.ReadAllText("ffmpeg.config.json")); } } } namespace FFMpegCore.Helpers { public static class FFMpegHelper { private static bool _ffmpegVerified; public static void ConversionSizeExceptionCheck(IMediaAnalysis info) { ConversionSizeExceptionCheck(info.PrimaryVideoStream.Width, info.PrimaryVideoStream.Height); } public static void ConversionSizeExceptionCheck(int width, int height) { if (height % 2 != 0 || width % 2 != 0) { throw new ArgumentException("FFMpeg yuv420p encoding requires the width and height to be a multiple of 2!"); } } public static void ExtensionExceptionCheck(string filename, string extension) { if (!extension.Equals(Path.GetExtension(filename), StringComparison.OrdinalIgnoreCase)) { throw new FFMpegException(FFMpegExceptionType.File, "Invalid output file. File extension should be '" + extension + "' required."); } } public static void RootExceptionCheck() { if (GlobalFFOptions.Current.BinaryFolder == null) { throw new FFOptionsException("FFMpeg root is not configured in app config. Missing key 'BinaryFolder'."); } } public static void VerifyFFMpegExists(FFOptions ffMpegOptions) { if (!_ffmpegVerified) { _ffmpegVerified = Instance.Finish(GlobalFFOptions.GetFFMpegBinaryPath(ffMpegOptions), "-version", (EventHandler<string>)null, (EventHandler<string>)null).ExitCode == 0; if (!_ffmpegVerified) { throw new FFMpegException(FFMpegExceptionType.Operation, "ffmpeg was not found on your system"); } } } } public static class FFProbeHelper { private static bool _ffprobeVerified; public static void RootExceptionCheck() { if (GlobalFFOptions.Current.BinaryFolder == null) { throw new FFOptionsException("FFProbe root is not configured in app config. Missing key 'BinaryFolder'."); } } public static void VerifyFFProbeExists(FFOptions ffMpegOptions) { if (!_ffprobeVerified) { _ffprobeVerified = Instance.Finish(GlobalFFOptions.GetFFProbeBinaryPath(ffMpegOptions), "-version", (EventHandler<string>)null, (EventHandler<string>)null).ExitCode == 0; if (!_ffprobeVerified) { throw new FFProbeException("ffprobe was not found on your system"); } } } } } namespace FFMpegCore.Pipes { public interface IAudioSample { void Serialize(Stream stream); Task SerializeAsync(Stream stream, CancellationToken token); } public interface IPipeSink { Task ReadAsync(Stream inputStream, CancellationToken cancellationToken); string GetFormat(); } public interface IPipeSource { string GetStreamArguments(); Task WriteAsync(Stream outputStream, CancellationToken cancellationToken); } public interface IVideoFrame { int Width { get; } int Height { get; } string Format { get; } void Serialize(Stream pipe); Task SerializeAsync(Stream pipe, CancellationToken token); } internal static class PipeHelpers { public static string GetUnqiuePipeName() { return "FFMpegCore_" + Guid.NewGuid().ToString("N").Substring(0, 5); } public static string GetPipePath(string pipeName) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { return "\\\\.\\pipe\\" + pipeName; } return "unix:" + Path.Combine(Path.GetTempPath(), "CoreFxPipe_" + pipeName); } } public class RawAudioPipeSource : IPipeSource { private readonly IEnumerator<IAudioSample> _sampleEnumerator; public string Format { get; set; } = "s16le"; public uint SampleRate { get; set; } = 8000u; public uint Channels { get; set; } = 1u; public RawAudioPipeSource(IEnumerator<IAudioSample> sampleEnumerator) { _sampleEnumerator = sampleEnumerator; } public RawAudioPipeSource(IEnumerable<IAudioSample> sampleEnumerator) : this(sampleEnumerator.GetEnumerator()) { } public string GetStreamArguments() { return $"-f {Format} -ar {SampleRate} -ac {Channels}"; } public async Task WriteAsync(Stream outputStream, CancellationToken cancellationToken) { if (_sampleEnumerator.Current != null) { await _sampleEnumerator.Current.SerializeAsync(outputStream, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); } while (_sampleEnumerator.MoveNext()) { await _sampleEnumerator.Current.SerializeAsync(outputStream, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); } } } public class RawVideoPipeSource : IPipeSource { private bool _formatInitialized; private readonly IEnumerator<IVideoFrame> _framesEnumerator; public string StreamFormat { get; private set; } public int Width { get; private set; } public int Height { get; private set; } public double FrameRate { get; set; } = 25.0; public RawVideoPipeSource(IEnumerable<IVideoFrame> framesEnumerator) { _framesEnumerator = framesEnumerator.GetEnumerator(); } public string GetStreamArguments() { if (!_formatInitialized) { if (_framesEnumerator.Current == null && !_framesEnumerator.MoveNext()) { throw new InvalidOperationException("Enumerator is empty, unable to get frame"); } StreamFormat = _framesEnumerator.Current.Format; Width = _framesEnumerator.Current.Width; Height = _framesEnumerator.Current.Height; _formatInitialized = true; } return $"-f rawvideo -r {FrameRate.ToString(CultureInfo.InvariantCulture)} -pix_fmt {StreamFormat} -s {Width}x{Height}"; } public async Task WriteAsync(Stream outputStream, CancellationToken cancellationToken) { if (_framesEnumerator.Current != null) { CheckFrameAndThrow(_framesEnumerator.Current); await _framesEnumerator.Current.SerializeAsync(outputStream, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); } while (_framesEnumerator.MoveNext()) { CheckFrameAndThrow(_framesEnumerator.Current); await _framesEnumerator.Current.SerializeAsync(outputStream, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); } } private void CheckFrameAndThrow(IVideoFrame frame) { if (frame.Width != Width || frame.Height != Height || frame.Format != StreamFormat) { throw new FFMpegStreamFormatException(FFMpegExceptionType.Operation, "Video frame is not the same format as created raw video stream\r\n" + $"Frame format: {frame.Width}x{frame.Height} pix_fmt: {frame.Format}\r\n" + $"Stream format: {Width}x{Height} pix_fmt: {StreamFormat}"); } } } public class StreamPipeSink : IPipeSink { public Func<Stream, CancellationToken, Task> Writer { get; } public int BlockSize { get; set; } = 4096; public string Format { get; set; } = string.Empty; public StreamPipeSink(Func<Stream, CancellationToken, Task> writer) { Writer = writer; } public StreamPipeSink(Stream destination) { Stream destination2 = destination; base..ctor(); StreamPipeSink streamPipeSink = this; Writer = (Stream inputStream, CancellationToken cancellationToken) => inputStream.CopyToAsync(destination2, streamPipeSink.BlockSize, cancellationToken); } public async Task ReadAsync(Stream inputStream, CancellationToken cancellationToken) { await Writer(inputStream, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); } public string GetFormat() { return Format; } } public class StreamPipeSource : IPipeSource { public Stream Source { get; } public int BlockSize { get; } = 4096; public string StreamFormat { get; } = string.Empty; public StreamPipeSource(Stream source) { Source = source; } public string GetStreamArguments() { return StreamFormat; } public Task WriteAsync(Stream outputStream, CancellationToken cancellationToken) { return Source.CopyToAsync(outputStream, BlockSize, cancellationToken); } } } namespace FFMpegCore.Exceptions { public enum FFMpegExceptionType { Conversion, File, Operation, Process } public class FFMpegException : Exception { public FFMpegExceptionType Type { get; } public string FFMpegErrorOutput { get; } public FFMpegException(FFMpegExceptionType type, string message, Exception? innerException = null, string ffMpegErrorOutput = "") : base(message, innerException) { FFMpegErrorOutput = ffMpegErrorOutput; Type = type; } public FFMpegException(FFMpegExceptionType type, string message, string ffMpegErrorOutput = "") : base(message) { FFMpegErrorOutput = ffMpegErrorOutput; Type = type; } public FFMpegException(FFMpegExceptionType type, string message) : base(message) { FFMpegErrorOutput = string.Empty; Type = type; } } public class FFOptionsException : Exception { public FFOptionsException(string message, Exception? innerException = null) : base(message, innerException) { } } public class FFMpegArgumentException : Exception { public FFMpegArgumentException(string? message = null, Exception? innerException = null) : base(message, innerException) { } } public class FFMpegStreamFormatException : FFMpegException { public FFMpegStreamFormatException(FFMpegExceptionType type, string message, Exception? innerException = null) : base(type, message, innerException) { } } public class FFProbeException : Exception { public FFProbeException(string message, Exception? inner = null) : base(message, inner) { } } public class FFProbeProcessException : FFProbeException { public IReadOnlyCollection<string> ProcessErrors { get; } public FFProbeProcessException(string message, IReadOnlyCollection<string> processErrors, Exception? inner = null) : base(message, inner) { ProcessErrors = processErrors; } } public class FormatNullException : FFProbeException { public FormatNullException() : base("Format not specified") { } } } namespace FFMpegCore.Enums { public enum AudioQuality { Ultra = 384, VeryHigh = 256, Good = 192, Normal = 128, BelowNormal = 96, Low = 64 } public enum FeatureStatus { Unknown, NotSupported, Supported } public class Codec { public class FeatureLevel { public bool IsExperimental { get; internal set; } public FeatureStatus SupportsFrameLevelMultithreading { get; internal set; } public FeatureStatus SupportsSliceLevelMultithreading { get; internal set; } public FeatureStatus SupportsDrawHorizBand { get; internal set; } public FeatureStatus SupportsDirectRendering { get; internal set; } internal void Merge(FeatureLevel other) { IsExperimental |= other.IsExperimental; SupportsFrameLevelMultithreading = (FeatureStatus)Math.Max((int)SupportsFrameLevelMultithreading, (int)other.SupportsFrameLevelMultithreading); SupportsSliceLevelMultithreading = (FeatureStatus)Math.Max((int)SupportsSliceLevelMultithreading, (int)other.SupportsSliceLevelMultithreading); SupportsDrawHorizBand = (FeatureStatus)Math.Max((int)SupportsDrawHorizBand, (int)other.SupportsDrawHorizBand); SupportsDirectRendering = (FeatureStatus)Math.Max((int)SupportsDirectRendering, (int)other.SupportsDirectRendering); } } private static readonly Regex _codecsFormatRegex = new Regex("([D\\.])([E\\.])([VASD\\.])([I\\.])([L\\.])([S\\.])\\s+([a-z0-9_-]+)\\s+(.+)"); private static readonly Regex _decodersEncodersFormatRegex = new Regex("([VASD\\.])([F\\.])([S\\.])([X\\.])([B\\.])([D\\.])\\s+([a-z0-9_-]+)\\s+(.+)"); public string Name { get; private set; } public CodecType Type { get; private set; } public bool DecodingSupported { get; private set; } public bool EncodingSupported { get; private set; } public bool IsIntraFrameOnly { get; private set; } public bool IsLossy { get; private set; } public bool IsLossless { get; private set; } public string Description { get; private set; } public FeatureLevel EncoderFeatureLevel { get; private set; } public FeatureLevel DecoderFeatureLevel { get; private set; } internal Codec(string name, CodecType type) { EncoderFeatureLevel = new FeatureLevel(); DecoderFeatureLevel = new FeatureLevel(); Name = name; Type = type; } internal static bool TryParseFromCodecs(string line, out Codec codec) { Match match = _codecsFormatRegex.Match(line); if (!match.Success) { codec = null; return false; } string value = match.Groups[7].Value; CodecType codecType = match.Groups[3].Value switch { "V" => CodecType.Video, "A" => CodecType.Audio, "D" => CodecType.Data, "S" => CodecType.Subtitle, _ => CodecType.Unknown, }; if (codecType == CodecType.Unknown) { codec = null; return false; } codec = new Codec(value, codecType); codec.DecodingSupported = match.Groups[1].Value != "."; codec.EncodingSupported = match.Groups[2].Value != "."; codec.IsIntraFrameOnly = match.Groups[4].Value != "."; codec.IsLossy = match.Groups[5].Value != "."; codec.IsLossless = match.Groups[6].Value != "."; codec.Description = match.Groups[8].Value; return true; } internal static bool TryParseFromEncodersDecoders(string line, out Codec codec, bool isEncoder) { Match match = _decodersEncodersFormatRegex.Match(line); if (!match.Success) { codec = null; return false; } string value = match.Groups[7].Value; CodecType codecType = match.Groups[1].Value switch { "V" => CodecType.Video, "A" => CodecType.Audio, "D" => CodecType.Data, "S" => CodecType.Subtitle, _ => CodecType.Unknown, }; if (codecType == CodecType.Unknown) { codec = null; return false; } codec = new Codec(value, codecType); FeatureLevel obj = (isEncoder ? codec.EncoderFeatureLevel : codec.DecoderFeatureLevel); codec.DecodingSupported = !isEncoder; codec.EncodingSupported = isEncoder; obj.SupportsFrameLevelMultithreading = ((!(match.Groups[2].Value != ".")) ? FeatureStatus.NotSupported : FeatureStatus.Supported); obj.SupportsSliceLevelMultithreading = ((!(match.Groups[3].Value != ".")) ? FeatureStatus.NotSupported : FeatureStatus.Supported); obj.IsExperimental = match.Groups[4].Value != "."; obj.SupportsDrawHorizBand = ((!(match.Groups[5].Value != ".")) ? FeatureStatus.NotSupported : FeatureStatus.Supported); obj.SupportsDirectRendering = ((!(match.Groups[6].Value != ".")) ? FeatureStatus.NotSupported : FeatureStatus.Supported); codec.Description = match.Groups[8].Value; return true; } internal void Merge(Codec other) { if (Name != other.Name) { throw new FFMpegException(FFMpegExceptionType.Operation, "different codecs enable to merge"); } Type |= other.Type; DecodingSupported |= other.DecodingSupported; EncodingSupported |= other.EncodingSupported; IsIntraFrameOnly |= other.IsIntraFrameOnly; IsLossy |= other.IsLossy; IsLossless |= other.IsLossless; EncoderFeatureLevel.Merge(other.EncoderFeatureLevel); DecoderFeatureLevel.Merge(other.DecoderFeatureLevel); if (Description != other.Description) { Description = Description + "\r\n" + other.Description; } } } public class ContainerFormat { private static readonly Regex FormatRegex = new Regex("([D ])([E ])\\s+([a-z0-9_]+)\\s+(.+)"); public string Name { get; private set; } public bool DemuxingSupported { get; private set; } public bool MuxingSupported { get; private set; } public string Description { get; private set; } public string Extension { get { if (GlobalFFOptions.Current.ExtensionOverrides.ContainsKey(Name)) { return GlobalFFOptions.Current.ExtensionOverrides[Name]; } return "." + Name; } } internal ContainerFormat(string name) { Name = name; } internal static bool TryParse(string line, out ContainerFormat fmt) { Match match = FormatRegex.Match(line); if (!match.Success) { fmt = null; return false; } fmt = new ContainerFormat(match.Groups[3].Value) { DemuxingSupported = (match.Groups[1].Value != " "), MuxingSupported = (match.Groups[2].Value != " "), Description = match.Groups[4].Value }; return true; } } public enum CodecType { Unknown = 0, Video = 2, Audio = 4, Subtitle = 8, Data = 0x10 } public static class VideoCodec { public static Codec LibX264 => FFMpeg.GetCodec("libx264"); public static Codec LibX265 => FFMpeg.GetCodec("libx265"); public static Codec LibVpx => FFMpeg.GetCodec("libvpx"); public static Codec LibTheora => FFMpeg.GetCodec("libtheora"); public static Codec Png => FFMpeg.GetCodec("png"); public static Codec MpegTs => FFMpeg.GetCodec("mpegts"); } public static class AudioCodec { public static Codec Aac => FFMpeg.GetCodec("aac"); public static Codec LibVorbis => FFMpeg.GetCodec("libvorbis"); public static Codec LibFdk_Aac => FFMpeg.GetCodec("libfdk_aac"); public static Codec Ac3 => FFMpeg.GetCodec("ac3"); public static Codec Eac3 => FFMpeg.GetCodec("eac3"); public static Codec LibMp3Lame => FFMpeg.GetCodec("libmp3lame"); } public static class VideoType { public static ContainerFormat MpegTs => FFMpeg.GetContainerFormat("mpegts"); public static ContainerFormat Ts => FFMpeg.GetContainerFormat("mpegts"); public static ContainerFormat Mp4 => FFMpeg.GetContainerFormat("mp4"); public static ContainerFormat Mov => FFMpeg.GetContainerFormat("mov"); public static ContainerFormat Avi => FFMpeg.GetContainerFormat("avi"); public static ContainerFormat Ogv => FFMpeg.GetContainerFormat("ogv"); public static ContainerFormat WebM => FFMpeg.GetContainerFormat("webm"); } public enum Filter { H264_Mp4ToAnnexB, Aac_AdtstoAsc } public enum Channel { Audio, Video, Both, VideoNoAttachedPic, Subtitle, Data, Attachments, All } internal static class ChannelMethods { public static string StreamType(this Channel channel) { return channel switch { Channel.Audio => ":a", Channel.Video => ":v", Channel.VideoNoAttachedPic => ":V", Channel.Subtitle => ":s", Channel.Data => ":d", Channel.Attachments => ":t", _ => string.Empty, }; } } public enum FFMpegLogLevel { Quiet, Panic, Fatal, Error, Warning, Info, Verbose, Debug, Trace } public static class FileExtension { public static readonly string Mp4 = VideoType.Mp4.Extension; public static readonly string Ts = VideoType.MpegTs.Extension; public static readonly string Ogv = VideoType.Ogv.Extension; public static readonly string WebM = VideoType.WebM.Extension; public static readonly string Png = ".png"; public static readonly string Mp3 = ".mp3"; public static readonly string Gif = ".gif"; public static string Extension(this Codec type) { return type.Name switch { "libx264" => Mp4, "libxvpx" => WebM, "libxtheora" => Ogv, "mpegts" => Ts, "png" => Png, _ => throw new Exception("The extension for this video type is not defined."), }; } } public enum HardwareAccelerationDevice { Auto, D3D11VA, DXVA2, QSV, CUVID, CUDA, VDPAU, VAAPI, LibMFX } public enum Mirroring { Vertical, Horizontal } public class PixelFormat { private static readonly Regex _formatRegex = new Regex("([I\\.])([O\\.])([H\\.])([P\\.])([B\\.])\\s+(\\S+)\\s+([0-9]+)\\s+([0-9]+)"); public bool InputConversionSupported { get; private set; } public bool OutputConversionSupported { get; private set; } public bool HardwareAccelerationSupported { get; private set; } public bool IsPaletted { get; private set; } public bool IsBitstream { get; private set; } public string Name { get; private set; } public int Components { get; private set; } public int BitsPerPixel { get; private set; } public bool CanConvertTo(PixelFormat other) { if (InputConversionSupported) { return other.OutputConversionSupported; } return false; } internal PixelFormat(string name) { Name = name; } internal static bool TryParse(string line, out PixelFormat fmt) { Match match = _formatRegex.Match(line); if (!match.Success) { fmt = null; return false; } fmt = new PixelFormat(match.Groups[6].Value); fmt.InputConversionSupported = match.Groups[1].Value != "."; fmt.OutputConversionSupported = match.Groups[2].Value != "."; fmt.HardwareAccelerationSupported = match.Groups[3].Value != "."; fmt.IsPaletted = match.Groups[4].Value != "."; fmt.IsBitstream = match.Groups[5].Value != "."; if (!int.TryParse(match.Groups[7].Value, out var result)) { return false; } fmt.Components = result; if (!int.TryParse(match.Groups[8].Value, out var result2)) { return false; } fmt.BitsPerPixel = result2; return true; } } public enum Speed { VerySlow, Slower, Slow, Medium, Fast, Faster, VeryFast, SuperFast, UltraFast } public enum Transposition { CounterClockwise90VerticalFlip, Clockwise90, CounterClockwise90, Clockwise90VerticalFlip } public enum VideoSize { FullHd = 1080, Hd = 720, Ed = 480, Ld = 360, Original = -1 } } namespace FFMpegCore.Builders.MetaData { public class ChapterData { public string Title { get; private set; } public TimeSpan Start { get; private set; } public TimeSpan End { get; private set; } public ChapterData(string title, TimeSpan start, TimeSpan end) { Title = title; Start = start; End = end; } } public interface IReadOnlyMetaData { IReadOnlyList<ChapterData> Chapters { get; } IReadOnlyDictionary<string, string> Entries { get; } } public class MetaData : IReadOnlyMetaData { public Dictionary<string, string> Entries { get; private set; } public List<ChapterData> Chapters { get; private set; } IReadOnlyList<ChapterData> IReadOnlyMetaData.Chapters => Chapters; IReadOnlyDictionary<string, string> IReadOnlyMetaData.Entries => Entries; public MetaData() { Entries = new Dictionary<string, string>(); Chapters = new List<ChapterData>(); } public MetaData(MetaData cloneSource) { Entries = new Dictionary<string, string>(cloneSource.Entries); Chapters = cloneSource.Chapters.Select(delegate(ChapterData x) { TimeSpan start = x.Start; TimeSpan end = x.End; return new ChapterData(x.Title, start, end); }).ToList(); } } public class MetaDataBuilder { private readonly MetaData _metaData = new MetaData(); public MetaDataBuilder WithEntry(string key, string entry) { if (_metaData.Entries.TryGetValue(key, out string value) && !string.IsNullOrWhiteSpace(value)) { entry = value + "; " + entry; } _metaData.Entries[key] = entry; return this; } public MetaDataBuilder WithEntry(string key, params string[] values) { return WithEntry(key, string.Join("; ", values)); } public MetaDataBuilder WithEntry(string key, IEnumerable<string> values) { return WithEntry(key, string.Join("; ", values)); } public MetaDataBuilder AddChapter(ChapterData chapterData) { _metaData.Chapters.Add(chapterData); return this; } public MetaDataBuilder AddChapters<T>(IEnumerable<T> values, Func<T, (TimeSpan duration, string title)> chapterGetter) { foreach (T value in values) { var (duration, title) = chapterGetter(value); AddChapter(duration, title); } return this; } public MetaDataBuilder AddChapter(TimeSpan duration, string? title = null) { TimeSpan timeSpan = _metaData.Chapters.LastOrDefault()?.End ?? TimeSpan.Zero; TimeSpan timeSpan2 = timeSpan + duration; title = (string.IsNullOrEmpty(title) ? $"Chapter {_metaData.Chapters.Count + 1}" : title); _metaData.Chapters.Add(new ChapterData(start: timeSpan, end: timeSpan2, title: title ?? string.Empty)); return this; } public MetaDataBuilder WithMajorBrand(string value) { return WithEntry("major_brand", value); } public MetaDataBuilder WithMinorVersion(string value) { return WithEntry("minor_version", value); } public MetaDataBuilder WithCompatibleBrands(string value) { return WithEntry("compatible_brands", value); } public MetaDataBuilder WithCopyright(string value) { return WithEntry("copyright", value); } public MetaDataBuilder WithTitle(string value) { return WithEntry("