From f6132e2eba6170635eec944bf26cf5c5cf1fdc0d Mon Sep 17 00:00:00 2001 From: Fedor Zhilkin Date: Thu, 17 Sep 2020 20:50:38 +0300 Subject: [PATCH 01/23] -map filtering Add map filtering to choose videostream Former-commit-id: e60fb71ff836e482915241309924e1b61f6b0802 --- .../FFMpeg/Arguments/MapStreamArgument.cs | 17 +++++++++++++++++ FFMpegCore/FFMpeg/FFMpegArguments.cs | 3 ++- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 FFMpegCore/FFMpeg/Arguments/MapStreamArgument.cs diff --git a/FFMpegCore/FFMpeg/Arguments/MapStreamArgument.cs b/FFMpegCore/FFMpeg/Arguments/MapStreamArgument.cs new file mode 100644 index 0000000..f6d9977 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/MapStreamArgument.cs @@ -0,0 +1,17 @@ +namespace FFMpegCore.Arguments +{ + /// + /// Represents choice of video stream, works with one input file + /// + public class MapStreamArgument : IArgument + { + public readonly int VideoStream; + + public MapStreamArgument(int videoStreamNum) + { + VideoStream = videoStreamNum; + } + + public string Text => $"-map 0:{VideoStream}"; + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/FFMpegArguments.cs b/FFMpegCore/FFMpeg/FFMpegArguments.cs index b3285f6..db129ee 100644 --- a/FFMpegCore/FFMpeg/FFMpegArguments.cs +++ b/FFMpegCore/FFMpeg/FFMpegArguments.cs @@ -59,7 +59,8 @@ private FFMpegArguments(IInputArgument inputArgument) public FFMpegArguments WithDuration(TimeSpan? duration) => WithArgument(new DurationArgument(duration)); public FFMpegArguments WithFastStart() => WithArgument(new FaststartArgument()); public FFMpegArguments WithFrameOutputCount(int frames) => WithArgument(new FrameOutputCountArgument(frames)); - + public FFMpegArguments WithVideoStream(int videoStreamNumber) => WithArgument(new MapStreamArgument(videoStreamNumber)); + public FFMpegArguments UsingShortest(bool shortest = true) => WithArgument(new ShortestArgument(shortest)); public FFMpegArguments UsingMultithreading(bool multithread) => WithArgument(new ThreadsArgument(multithread)); public FFMpegArguments UsingThreads(int threads) => WithArgument(new ThreadsArgument(threads)); From e977511b17af3578b62a99ea70bc44911bc71796 Mon Sep 17 00:00:00 2001 From: Fedor Zhilkin Date: Thu, 17 Sep 2020 20:56:28 +0300 Subject: [PATCH 02/23] Use map filtering Use map filtering doing snapshot Former-commit-id: 981b3294c5d43da7ecc0e5f04cc39e14641672a0 --- FFMpegCore/FFMpeg/FFMpeg.cs | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/FFMpegCore/FFMpeg/FFMpeg.cs b/FFMpegCore/FFMpeg/FFMpeg.cs index 7aad74f..0f516e8 100644 --- a/FFMpegCore/FFMpeg/FFMpeg.cs +++ b/FFMpegCore/FFMpeg/FFMpeg.cs @@ -20,13 +20,14 @@ public static class FFMpeg /// Output video file path /// Seek position where the thumbnail should be taken. /// Thumbnail size. If width or height equal 0, the other will be computed automatically. + /// Number of video stream in input file. Default it is 0. /// Bitmap with the requested snapshot. - public static bool Snapshot(IMediaAnalysis source, string output, Size? size = null, TimeSpan? captureTime = null) + public static bool Snapshot(IMediaAnalysis source, string output, Size? size = null, TimeSpan? captureTime = null, int videoStreamNumber = 0) { if (Path.GetExtension(output) != FileExtension.Png) output = Path.GetFileNameWithoutExtension(output) + FileExtension.Png; - var arguments = BuildSnapshotArguments(source, size, captureTime); + var arguments = BuildSnapshotArguments(source, size, captureTime, videoStreamNumber); return arguments .OutputToFile(output) @@ -39,13 +40,14 @@ public static bool Snapshot(IMediaAnalysis source, string output, Size? size = n /// Output video file path /// Seek position where the thumbnail should be taken. /// Thumbnail size. If width or height equal 0, the other will be computed automatically. + /// Number of video stream in input file. Default it is 0. /// Bitmap with the requested snapshot. - public static Task SnapshotAsync(IMediaAnalysis source, string output, Size? size = null, TimeSpan? captureTime = null) + public static Task SnapshotAsync(IMediaAnalysis source, string output, Size? size = null, TimeSpan? captureTime = null, int videoStreamNumber = 0) { if (Path.GetExtension(output) != FileExtension.Png) output = Path.GetFileNameWithoutExtension(output) + FileExtension.Png; - var arguments = BuildSnapshotArguments(source, size, captureTime); + var arguments = BuildSnapshotArguments(source, size, captureTime, videoStreamNumber); return arguments .OutputToFile(output) @@ -57,10 +59,11 @@ public static Task SnapshotAsync(IMediaAnalysis source, string output, Siz /// Source video file. /// Seek position where the thumbnail should be taken. /// Thumbnail size. If width or height equal 0, the other will be computed automatically. + /// Number of video stream in input file. Default it is 0. /// Bitmap with the requested snapshot. - public static Bitmap Snapshot(IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null) + public static Bitmap Snapshot(IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null, int videoStreamNumber = 0) { - var arguments = BuildSnapshotArguments(source, size, captureTime); + var arguments = BuildSnapshotArguments(source, size, captureTime, videoStreamNumber); using var ms = new MemoryStream(); arguments @@ -77,10 +80,11 @@ public static Bitmap Snapshot(IMediaAnalysis source, Size? size = null, TimeSpan /// Source video file. /// Seek position where the thumbnail should be taken. /// Thumbnail size. If width or height equal 0, the other will be computed automatically. + /// Number of video stream in input file. Default it is 0. /// Bitmap with the requested snapshot. - public static async Task SnapshotAsync(IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null) + public static async Task SnapshotAsync(IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null, int videoStreamNumber = 0) { - var arguments = BuildSnapshotArguments(source, size, captureTime); + var arguments = BuildSnapshotArguments(source, size, captureTime, videoStreamNumber); using var ms = new MemoryStream(); await arguments @@ -92,13 +96,14 @@ await arguments return new Bitmap(ms); } - private static FFMpegArguments BuildSnapshotArguments(IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null) + private static FFMpegArguments BuildSnapshotArguments(IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null, int videoStreamNumber = 0) { captureTime ??= TimeSpan.FromSeconds(source.Duration.TotalSeconds / 3); size = PrepareSnapshotSize(source, size); return FFMpegArguments .FromSeekedFiles((source.Path, captureTime ?? TimeSpan.Zero)) + .WithVideoStream(videoStreamNumber) .WithVideoCodec(VideoCodec.Png) .WithFrameOutputCount(1) .Resize(size); From 0a09cbce2bce91eddb7c4c9b1db2e09162c72753 Mon Sep 17 00:00:00 2001 From: Fedor Zhilkin Date: Thu, 17 Sep 2020 21:02:33 +0300 Subject: [PATCH 03/23] Add test for map filtering Former-commit-id: f2fca0c1b6afa70cd768161e41ea008decf46744 --- FFMpegCore.Test/ArgumentBuilderTest.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/FFMpegCore.Test/ArgumentBuilderTest.cs b/FFMpegCore.Test/ArgumentBuilderTest.cs index 7a058a2..ee954c5 100644 --- a/FFMpegCore.Test/ArgumentBuilderTest.cs +++ b/FFMpegCore.Test/ArgumentBuilderTest.cs @@ -180,6 +180,13 @@ public void Builder_BuildString_FrameOutputCount() Assert.AreEqual("-i \"input.mp4\" -vframes 50 \"output.mp4\"", str); } + [TestMethod] + public void Builder_BuildString_VideoStreamNumber() + { + var str = FFMpegArguments.FromInputFiles(true, "input.mp4").WithVideoStream(1).OutputToFile("output.mp4", false).Arguments; + Assert.AreEqual("-i \"input.mp4\" -map 0:1 \"output.mp4\"", str); + } + [TestMethod] public void Builder_BuildString_FrameRate() { From 4b088054673bbca798b4407e976db01f620d61c3 Mon Sep 17 00:00:00 2001 From: Fedor Zhilkin Date: Fri, 13 Nov 2020 02:09:52 +0300 Subject: [PATCH 04/23] Refactoring Former-commit-id: c47214b7aa10b1d8f2f23c0f52522da0a9cbd2f3 --- .../FFMpeg/Arguments/MapStreamArgument.cs | 8 +-- FFMpegCore/FFMpeg/FFMpeg.cs | 67 +++++++++++-------- 2 files changed, 42 insertions(+), 33 deletions(-) diff --git a/FFMpegCore/FFMpeg/Arguments/MapStreamArgument.cs b/FFMpegCore/FFMpeg/Arguments/MapStreamArgument.cs index f6d9977..01a537d 100644 --- a/FFMpegCore/FFMpeg/Arguments/MapStreamArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/MapStreamArgument.cs @@ -5,13 +5,13 @@ /// public class MapStreamArgument : IArgument { - public readonly int VideoStream; + private readonly int _streamIndex; - public MapStreamArgument(int videoStreamNum) + public MapStreamArgument(int index) { - VideoStream = videoStreamNum; + _streamIndex = index; } - public string Text => $"-map 0:{VideoStream}"; + public string Text => $"-map 0:{_streamIndex}"; } } \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/FFMpeg.cs b/FFMpegCore/FFMpeg/FFMpeg.cs index f252489..8a442ed 100644 --- a/FFMpegCore/FFMpeg/FFMpeg.cs +++ b/FFMpegCore/FFMpeg/FFMpeg.cs @@ -20,15 +20,15 @@ public static class FFMpeg /// Output video file path /// Seek position where the thumbnail should be taken. /// Thumbnail size. If width or height equal 0, the other will be computed automatically. - /// Number of video stream in input file. Default it is 0. + /// Index of video stream in input file. Default it is 0. /// Bitmap with the requested snapshot. - public static bool Snapshot(IMediaAnalysis source, string output, Size? size = null, TimeSpan? captureTime = null, int videoStreamNumber = 0) + public static bool Snapshot(IMediaAnalysis source, string output, Size? size = null, TimeSpan? captureTime = null, int streamIndex = 0) { if (Path.GetExtension(output) != FileExtension.Png) output = Path.GetFileNameWithoutExtension(output) + FileExtension.Png; - - var (arguments, outputOptions) = BuildSnapshotArguments(source, size, captureTime, videoStreamNumber); - + + var (arguments, outputOptions) = BuildSnapshotArguments(source, size, captureTime, streamIndex); + return arguments .OutputToFile(output, true, outputOptions) .ProcessSynchronously(); @@ -40,15 +40,15 @@ public static bool Snapshot(IMediaAnalysis source, string output, Size? size = n /// Output video file path /// Seek position where the thumbnail should be taken. /// Thumbnail size. If width or height equal 0, the other will be computed automatically. - /// Number of video stream in input file. Default it is 0. + /// Index of video stream in input file. Default it is 0. /// Bitmap with the requested snapshot. - public static Task SnapshotAsync(IMediaAnalysis source, string output, Size? size = null, TimeSpan? captureTime = null, int videoStreamNumber = 0) + public static Task SnapshotAsync(IMediaAnalysis source, string output, Size? size = null, TimeSpan? captureTime = null, int streamIndex = 0) { if (Path.GetExtension(output) != FileExtension.Png) output = Path.GetFileNameWithoutExtension(output) + FileExtension.Png; - - var (arguments, outputOptions) = BuildSnapshotArguments(source, size, captureTime, videoStreamNumber); - + + var (arguments, outputOptions) = BuildSnapshotArguments(source, size, captureTime, streamIndex); + return arguments .OutputToFile(output, true, outputOptions) .ProcessAsynchronously(); @@ -59,13 +59,13 @@ public static Task SnapshotAsync(IMediaAnalysis source, string output, Siz /// Source video file. /// Seek position where the thumbnail should be taken. /// Thumbnail size. If width or height equal 0, the other will be computed automatically. - /// Number of video stream in input file. Default it is 0. + /// Index of video stream in input file. Default it is 0. /// Bitmap with the requested snapshot. - public static Bitmap Snapshot(IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null, int videoStreamNumber = 0) - { - var (arguments, outputOptions) = BuildSnapshotArguments(source, size, captureTime, videoStreamNumber); + public static Bitmap Snapshot(IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null, int streamIndex = 0) + { + var (arguments, outputOptions) = BuildSnapshotArguments(source, size, captureTime, streamIndex); using var ms = new MemoryStream(); - + arguments .OutputToPipe(new StreamPipeSink(ms), options => outputOptions(options .ForceFormat("rawvideo"))) @@ -80,13 +80,13 @@ public static Bitmap Snapshot(IMediaAnalysis source, Size? size = null, TimeSpan /// Source video file. /// Seek position where the thumbnail should be taken. /// Thumbnail size. If width or height equal 0, the other will be computed automatically. - /// Number of video stream in input file. Default it is 0. + /// Index of video stream in input file. Default it is 0. /// Bitmap with the requested snapshot. - public static async Task SnapshotAsync(IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null, int videoStreamNumber = 0) + public static async Task SnapshotAsync(IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null, int streamIndex = 0) { - var (arguments, outputOptions) = BuildSnapshotArguments(source, size, captureTime, videoStreamNumber); + var (arguments, outputOptions) = BuildSnapshotArguments(source, size, captureTime, streamIndex); using var ms = new MemoryStream(); - + await arguments .OutputToPipe(new StreamPipeSink(ms), options => outputOptions(options .ForceFormat("rawvideo"))) @@ -95,17 +95,26 @@ await arguments ms.Position = 0; return new Bitmap(ms); } - - private static (FFMpegArguments, Action outputOptions) BuildSnapshotArguments(IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null, int videoStreamNumber = 0) + + private static (FFMpegArguments, Action outputOptions) BuildSnapshotArguments(IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null, int streamIndex = 0) { captureTime ??= TimeSpan.FromSeconds(source.Duration.TotalSeconds / 3); size = PrepareSnapshotSize(source, size); - + + // If user will know about numeration of streams (user passes index of necessary video stream) + int index = source.VideoStreams.Where(videoStream => videoStream.Index == streamIndex).FirstOrDefault().Index; + + // User passes number of video stream + // E.g: user can pass 0, but index of first video stream will be 1 + /*int index = 0; + try { index = source.VideoStreams[streamIndex].Index; } + catch { };*/ + return (FFMpegArguments .FromFileInput(source, options => options - .Seek(captureTime)), + .Seek(captureTime)), options => options - .SelectStream(videoStreamNumber) + .SelectStream(index) .WithVideoCodec(VideoCodec.Png) .WithFrameOutputCount(1) .Resize(size)); @@ -115,11 +124,11 @@ private static (FFMpegArguments, Action outputOptions) Bu { if (wantedSize == null || (wantedSize.Value.Height <= 0 && wantedSize.Value.Width <= 0)) return null; - + var currentSize = new Size(source.PrimaryVideoStream.Width, source.PrimaryVideoStream.Height); if (source.PrimaryVideoStream.Rotation == 90 || source.PrimaryVideoStream.Rotation == 180) currentSize = new Size(source.PrimaryVideoStream.Height, source.PrimaryVideoStream.Width); - + if (wantedSize.Value.Width != currentSize.Width || wantedSize.Value.Height != currentSize.Height) { if (wantedSize.Value.Width <= 0 && wantedSize.Value.Height > 0) @@ -327,7 +336,7 @@ public static bool SaveM3U8Stream(Uri uri, string output) 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) @@ -451,7 +460,7 @@ private static void ParsePartOfCodecs(Dictionary codecs, string a instance.DataReceived += (e, args) => { var codec = parser(args.Data); - if(codec != null) + if (codec != null) if (codecs.TryGetValue(codec.Name, out var parentCodec)) parentCodec.Merge(codec); else @@ -498,7 +507,7 @@ public static IReadOnlyList GetCodecs(CodecType type) { if (!FFMpegOptions.Options.UseCache) return GetCodecsInternal().Values.Where(x => x.Type == type).ToList().AsReadOnly(); - return FFMpegCache.Codecs.Values.Where(x=>x.Type == type).ToList().AsReadOnly(); + return FFMpegCache.Codecs.Values.Where(x => x.Type == type).ToList().AsReadOnly(); } public static IReadOnlyList GetVideoCodecs() => GetCodecs(CodecType.Video); From 3951eda615365a1b171349fe8b6a8f2045ff0f80 Mon Sep 17 00:00:00 2001 From: chaitanyabd Date: Sat, 31 Jul 2021 00:05:38 +0530 Subject: [PATCH 05/23] Update README.md Former-commit-id: a2b37cb920cf346847e556c5dbfd0965053e1ada --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 9dce345..298489e 100644 --- a/README.md +++ b/README.md @@ -186,6 +186,8 @@ The default value of an empty string (expecting ffmpeg to be found through PATH) GlobalFFOptions.Configure(new FFOptions { BinaryFolder = "./bin", TemporaryFilesFolder = "/tmp" }); // or GlobalFFOptions.Configure(options => options.BinaryFolder = "./bin"); +// on some systems the absolute path may be required, in which case +GlobalFFOptions.Configure(new FFOptions { BinaryFolder = Server.MapPath("./bin"), TemporaryFilesFolder = Server.MapPath("/tmp") }); // or individual, per-run options await FFMpegArguments From bd7dcf9edd9a5a5979bb7fb012aec3fdea653e35 Mon Sep 17 00:00:00 2001 From: alex6dj Date: Sat, 31 Jul 2021 15:34:47 -0400 Subject: [PATCH 06/23] Basic ffprobe subtitle support Former-commit-id: f80d1fa3a66b9779a5d4353a2f6a784cba117b7a --- FFMpegCore/FFProbe/IMediaAnalysis.cs | 2 ++ FFMpegCore/FFProbe/MediaAnalysis.cs | 21 ++++++++++++++++++--- FFMpegCore/FFProbe/SubtitleStream.cs | 7 +++++++ 3 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 FFMpegCore/FFProbe/SubtitleStream.cs diff --git a/FFMpegCore/FFProbe/IMediaAnalysis.cs b/FFMpegCore/FFProbe/IMediaAnalysis.cs index 4e67d4f..7be3b20 100644 --- a/FFMpegCore/FFProbe/IMediaAnalysis.cs +++ b/FFMpegCore/FFProbe/IMediaAnalysis.cs @@ -9,7 +9,9 @@ public interface IMediaAnalysis MediaFormat Format { get; } AudioStream? PrimaryAudioStream { get; } VideoStream? PrimaryVideoStream { get; } + SubtitleStream? PrimarySubtitleStream { get; } List VideoStreams { get; } List AudioStreams { get; } + List SubtitleStreams { get; } } } diff --git a/FFMpegCore/FFProbe/MediaAnalysis.cs b/FFMpegCore/FFProbe/MediaAnalysis.cs index 2602f86..d021813 100644 --- a/FFMpegCore/FFProbe/MediaAnalysis.cs +++ b/FFMpegCore/FFProbe/MediaAnalysis.cs @@ -12,8 +12,9 @@ internal MediaAnalysis(FFProbeAnalysis analysis) Format = ParseFormat(analysis.Format); VideoStreams = analysis.Streams.Where(stream => stream.CodecType == "video").Select(ParseVideoStream).ToList(); AudioStreams = analysis.Streams.Where(stream => stream.CodecType == "audio").Select(ParseAudioStream).ToList(); + SubtitleStreams = analysis.Streams.Where(stream => stream.CodecType == "subtitle").Select(ParseSubtitleStream).ToList(); } - + private MediaFormat ParseFormat(Format analysisFormat) { return new MediaFormat @@ -36,12 +37,14 @@ private MediaFormat ParseFormat(Format analysisFormat) }.Max(); public MediaFormat Format { get; } + public AudioStream? PrimaryAudioStream => AudioStreams.OrderBy(stream => stream.Index).FirstOrDefault(); - public VideoStream? PrimaryVideoStream => VideoStreams.OrderBy(stream => stream.Index).FirstOrDefault(); + public SubtitleStream? PrimarySubtitleStream => SubtitleStreams.OrderBy(stream => stream.Index).FirstOrDefault(); public List VideoStreams { get; } public List AudioStreams { get; } + public List SubtitleStreams { get; } private VideoStream ParseVideoStream(FFProbeStream stream) { @@ -84,7 +87,19 @@ private AudioStream ParseAudioStream(FFProbeStream stream) }; } - + private SubtitleStream ParseSubtitleStream(FFProbeStream stream) + { + return new SubtitleStream + { + Index = stream.Index, + BitRate = !string.IsNullOrEmpty(stream.BitRate) ? MediaAnalysisUtils.ParseIntInvariant(stream.BitRate) : default, + CodecName = stream.CodecName, + CodecLongName = stream.CodecLongName, + Duration = MediaAnalysisUtils.ParseDuration(stream), + Language = stream.GetLanguage(), + Tags = stream.Tags, + }; + } } public static class MediaAnalysisUtils diff --git a/FFMpegCore/FFProbe/SubtitleStream.cs b/FFMpegCore/FFProbe/SubtitleStream.cs new file mode 100644 index 0000000..80493f4 --- /dev/null +++ b/FFMpegCore/FFProbe/SubtitleStream.cs @@ -0,0 +1,7 @@ +namespace FFMpegCore +{ + public class SubtitleStream : MediaStream + { + + } +} \ No newline at end of file From 041d40effd780d5e39472344b19b0f100bdcd5af Mon Sep 17 00:00:00 2001 From: alex6dj Date: Sat, 31 Jul 2021 16:46:21 -0400 Subject: [PATCH 07/23] Basic ffprobe test Former-commit-id: 47916eac29fef9a7f86de79522e1eed0ae6d6a62 --- FFMpegCore.Test/FFMpegCore.Test.csproj | 3 +++ FFMpegCore.Test/FFProbeTests.cs | 10 ++++++++++ FFMpegCore.Test/Resources/TestResources.cs | 1 + FFMpegCore.Test/Resources/sample.srt | 12 ++++++++++++ 4 files changed, 26 insertions(+) create mode 100644 FFMpegCore.Test/Resources/sample.srt diff --git a/FFMpegCore.Test/FFMpegCore.Test.csproj b/FFMpegCore.Test/FFMpegCore.Test.csproj index 98c9274..2505545 100644 --- a/FFMpegCore.Test/FFMpegCore.Test.csproj +++ b/FFMpegCore.Test/FFMpegCore.Test.csproj @@ -83,6 +83,9 @@ Always + + PreserveNewest + diff --git a/FFMpegCore.Test/FFProbeTests.cs b/FFMpegCore.Test/FFProbeTests.cs index 5cabc4e..897848d 100644 --- a/FFMpegCore.Test/FFProbeTests.cs +++ b/FFMpegCore.Test/FFProbeTests.cs @@ -100,5 +100,15 @@ public async Task Probe_Success_FromStream_Async() var info = await FFProbe.AnalyseAsync(stream); Assert.AreEqual(3, info.Duration.Seconds); } + + [TestMethod, Timeout(10000)] + public async Task Probe_Success_Subtitle_Async() + { + var info = await FFProbe.AnalyseAsync(TestResources.SrtSubtitle); + Assert.IsNotNull(info.PrimarySubtitleStream); + Assert.AreEqual(1, info.SubtitleStreams.Count); + Assert.AreEqual(0, info.AudioStreams.Count); + Assert.AreEqual(0, info.VideoStreams.Count); + } } } \ No newline at end of file diff --git a/FFMpegCore.Test/Resources/TestResources.cs b/FFMpegCore.Test/Resources/TestResources.cs index 6277dd3..14f8abe 100644 --- a/FFMpegCore.Test/Resources/TestResources.cs +++ b/FFMpegCore.Test/Resources/TestResources.cs @@ -20,5 +20,6 @@ public static class TestResources public static readonly string Mp3Audio = "./Resources/audio.mp3"; public static readonly string PngImage = "./Resources/cover.png"; public static readonly string ImageCollection = "./Resources/images"; + public static readonly string SrtSubtitle = "./Resources/sample.srt"; } } diff --git a/FFMpegCore.Test/Resources/sample.srt b/FFMpegCore.Test/Resources/sample.srt new file mode 100644 index 0000000..b08f594 --- /dev/null +++ b/FFMpegCore.Test/Resources/sample.srt @@ -0,0 +1,12 @@ +1 +00:00:00,000 --> 00:00:01,500 +For www.forom.com + +2 +00:00:01,500 --> 00:00:02,500 +Tonight's the night. + +3 +00:00:03,000 --> 00:00:15,000 +And it's going to happen +again and again -- \ No newline at end of file From 074404f08c27b2fecce070f4e0c8b2fb7a3b2702 Mon Sep 17 00:00:00 2001 From: Fedor Zhilkin Date: Tue, 3 Aug 2021 21:58:40 +0300 Subject: [PATCH 08/23] Style guide fixes and minor refactoring Former-commit-id: c30cf4c7ad87da71dcf3820ee35f5dabdc3d98ec --- FFMpegCore/FFMpeg/FFMpeg.cs | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/FFMpegCore/FFMpeg/FFMpeg.cs b/FFMpegCore/FFMpeg/FFMpeg.cs index 42c344b..14556b4 100644 --- a/FFMpegCore/FFMpeg/FFMpeg.cs +++ b/FFMpegCore/FFMpeg/FFMpeg.cs @@ -20,14 +20,15 @@ public static class FFMpeg /// Output video file path /// Seek position where the thumbnail should be taken. /// Thumbnail size. If width or height equal 0, the other will be computed automatically. + /// Selected video stream index. /// Bitmap with the requested snapshot. - public static bool Snapshot(string input, string output, Size? size = null, TimeSpan? captureTime = null) + public static bool Snapshot(string input, string output, Size? size = null, TimeSpan? captureTime = null, int streamIndex = 0) { if (Path.GetExtension(output) != FileExtension.Png) output = Path.GetFileNameWithoutExtension(output) + FileExtension.Png; var source = FFProbe.Analyse(input); - var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime); + var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime, streamIndex); return arguments .OutputToFile(output, true, outputOptions) @@ -40,14 +41,15 @@ public static bool Snapshot(string input, string output, Size? size = null, Time /// Output video file path /// Seek position where the thumbnail should be taken. /// Thumbnail size. If width or height equal 0, the other will be computed automatically. + /// Selected video stream index. /// Bitmap with the requested snapshot. - public static async Task SnapshotAsync(string input, string output, Size? size = null, TimeSpan? captureTime = null) + public static async Task SnapshotAsync(string input, string output, Size? size = null, TimeSpan? captureTime = null, int streamIndex = 0) { if (Path.GetExtension(output) != FileExtension.Png) output = Path.GetFileNameWithoutExtension(output) + FileExtension.Png; var source = await FFProbe.AnalyseAsync(input); - var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime); + var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime, streamIndex); return await arguments .OutputToFile(output, true, outputOptions) @@ -60,11 +62,12 @@ public static async Task SnapshotAsync(string input, string output, Size? /// Source video file. /// Seek position where the thumbnail should be taken. /// Thumbnail size. If width or height equal 0, the other will be computed automatically. + /// Selected video stream index. /// Bitmap with the requested snapshot. - public static Bitmap Snapshot(string input, Size? size = null, TimeSpan? captureTime = null) + public static Bitmap Snapshot(string input, Size? size = null, TimeSpan? captureTime = null, int streamIndex = 0) { var source = FFProbe.Analyse(input); - var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime); + var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime, streamIndex); using var ms = new MemoryStream(); arguments @@ -82,11 +85,12 @@ public static Bitmap Snapshot(string input, Size? size = null, TimeSpan? capture /// Source video file. /// Seek position where the thumbnail should be taken. /// Thumbnail size. If width or height equal 0, the other will be computed automatically. + /// Selected video stream index. /// Bitmap with the requested snapshot. - public static async Task SnapshotAsync(string input, Size? size = null, TimeSpan? captureTime = null) + public static async Task SnapshotAsync(string input, Size? size = null, TimeSpan? captureTime = null, int streamIndex = 0) { var source = await FFProbe.AnalyseAsync(input); - var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime); + var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime, streamIndex); using var ms = new MemoryStream(); await arguments @@ -98,15 +102,17 @@ await arguments return new Bitmap(ms); } - private static (FFMpegArguments, Action outputOptions) BuildSnapshotArguments(string input, IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null) + private static (FFMpegArguments, Action outputOptions) BuildSnapshotArguments(string input, IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null, int streamIndex = 0) { captureTime ??= TimeSpan.FromSeconds(source.Duration.TotalSeconds / 3); size = PrepareSnapshotSize(source, size); + var index = source.VideoStreams.FirstOrDefault(videoStream => videoStream.Index == streamIndex)?.Index; return (FFMpegArguments .FromFileInput(input, false, options => options .Seek(captureTime)), options => options + .SelectStream(index ?? 0) .WithVideoCodec(VideoCodec.Png) .WithFrameOutputCount(1) .Resize(size)); From d755c1d5264520fa3ac95c054de738264ed7a0a1 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Thu, 5 Aug 2021 13:21:57 +0200 Subject: [PATCH 09/23] Fix PcmAudioSampleWrapper namespace Former-commit-id: fc23456eb10b1d82e3e6eef6f0713beb627c23c4 --- FFMpegCore.Test/AudioTest.cs | 1 + FFMpegCore/Extend/PcmAudioSampleWrapper.cs | 39 ++++++++++++---------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/FFMpegCore.Test/AudioTest.cs b/FFMpegCore.Test/AudioTest.cs index f1abb72..b6fde77 100644 --- a/FFMpegCore.Test/AudioTest.cs +++ b/FFMpegCore.Test/AudioTest.cs @@ -8,6 +8,7 @@ using System.IO; using System.Linq; using System.Threading.Tasks; +using FFMpegCore.Extend; namespace FFMpegCore.Test { diff --git a/FFMpegCore/Extend/PcmAudioSampleWrapper.cs b/FFMpegCore/Extend/PcmAudioSampleWrapper.cs index d67038b..503a23f 100644 --- a/FFMpegCore/Extend/PcmAudioSampleWrapper.cs +++ b/FFMpegCore/Extend/PcmAudioSampleWrapper.cs @@ -1,27 +1,30 @@ -using FFMpegCore.Pipes; -using System.IO; +using System.IO; using System.Threading; using System.Threading.Tasks; +using FFMpegCore.Pipes; -public class PcmAudioSampleWrapper : IAudioSample +namespace FFMpegCore.Extend { - //This could actually be short or int, but copies would be inefficient. - //Handling bytes lets the user decide on the conversion, and abstract the library - //from handling shorts, unsigned shorts, integers, unsigned integers and floats. - private readonly byte[] _sample; - - public PcmAudioSampleWrapper(byte[] sample) + public class PcmAudioSampleWrapper : IAudioSample { - _sample = sample; - } + //This could actually be short or int, but copies would be inefficient. + //Handling bytes lets the user decide on the conversion, and abstract the library + //from handling shorts, unsigned shorts, integers, unsigned integers and floats. + private readonly byte[] _sample; - public void Serialize(Stream stream) - { - stream.Write(_sample, 0, _sample.Length); - } + public PcmAudioSampleWrapper(byte[] sample) + { + _sample = sample; + } - public async Task SerializeAsync(Stream stream, CancellationToken token) - { - await stream.WriteAsync(_sample, 0, _sample.Length, token); + public void Serialize(Stream stream) + { + stream.Write(_sample, 0, _sample.Length); + } + + public async Task SerializeAsync(Stream stream, CancellationToken token) + { + await stream.WriteAsync(_sample, 0, _sample.Length, token); + } } } From 96ec0613d367a851386bd8409d540be35269924d Mon Sep 17 00:00:00 2001 From: alex6dj Date: Thu, 5 Aug 2021 14:37:32 -0400 Subject: [PATCH 10/23] Subtitle hard-burn implementation. Former-commit-id: 3a890623846eb5a0e978882afbda4c3e1d5b1508 --- FFMpegCore.Test/ArgumentBuilderTest.cs | 21 +++++ FFMpegCore/Extend/KeyValuePairExtensions.cs | 15 ++++ FFMpegCore/Extend/StringExtensions.cs | 10 +++ .../Arguments/SubtitleHardBurnArgument.cs | 89 +++++++++++++++++++ .../FFMpeg/Arguments/VideoFiltersArgument.cs | 1 + 5 files changed, 136 insertions(+) create mode 100644 FFMpegCore/Extend/KeyValuePairExtensions.cs create mode 100644 FFMpegCore/Extend/StringExtensions.cs create mode 100644 FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs diff --git a/FFMpegCore.Test/ArgumentBuilderTest.cs b/FFMpegCore.Test/ArgumentBuilderTest.cs index daa3eda..71bc90b 100644 --- a/FFMpegCore.Test/ArgumentBuilderTest.cs +++ b/FFMpegCore.Test/ArgumentBuilderTest.cs @@ -317,6 +317,27 @@ public void Builder_BuildString_DrawtextFilter_Alt() str); } + [TestMethod] + public void Builder_BuildString_SubtitleHardBurnFilter() + { + var str = FFMpegArguments + .FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt + .WithVideoFilters(filterOptions => filterOptions + .HardBurnSubtitle(SubtitleHardBurnOptions + .Create(subtitlePath: "sample.srt") + .SetCharacterEncoding("UTF-8") + .SetOriginalSize(1366,768) + .SetSubtitleIndex(0) + .WithStyle(StyleOptions.Create() + .WithParameter("FontName", "DejaVu Serif") + .WithParameter("PrimaryColour", "&HAA00FF00"))))) + .Arguments; + + Assert.AreEqual("-i \"input.mp4\" -vf \"subtitles=sample.srt:charenc=UTF-8:original_size=1366x768:si=0:force_style='FontName=DejaVu Serif\\,PrimaryColour=&HAA00FF00'\" \"output.mp4\"", + str); + } + [TestMethod] public void Builder_BuildString_StartNumber() { diff --git a/FFMpegCore/Extend/KeyValuePairExtensions.cs b/FFMpegCore/Extend/KeyValuePairExtensions.cs new file mode 100644 index 0000000..92dbf6d --- /dev/null +++ b/FFMpegCore/Extend/KeyValuePairExtensions.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace FFMpegCore.Extend +{ + internal static class KeyValuePairExtensions + { + public static string FormatArgumentPair(this KeyValuePair pair, bool enclose) + { + var key = pair.Key; + var value = enclose ? pair.Value.EncloseIfContainsSpace() : pair.Value; + + return $"{key}={value}"; + } + } +} \ No newline at end of file diff --git a/FFMpegCore/Extend/StringExtensions.cs b/FFMpegCore/Extend/StringExtensions.cs new file mode 100644 index 0000000..f4e0169 --- /dev/null +++ b/FFMpegCore/Extend/StringExtensions.cs @@ -0,0 +1,10 @@ +namespace FFMpegCore.Extend +{ + internal static class StringExtensions + { + public static string EncloseIfContainsSpace(this string input) + { + return input.Contains(" ") ? $"'{input}'" : input; + } + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs b/FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs new file mode 100644 index 0000000..552a87b --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs @@ -0,0 +1,89 @@ +using FFMpegCore.Extend; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; + +namespace FFMpegCore.Arguments +{ + public class SubtitleHardBurnArgument : IVideoFilterArgument + { + private readonly SubtitleHardBurnOptions _subtitleHardBurnOptions; + + public SubtitleHardBurnArgument(SubtitleHardBurnOptions subtitleHardBurnOptions) + { + _subtitleHardBurnOptions = subtitleHardBurnOptions; + } + + public string Key => "subtitles"; + + public string Value => _subtitleHardBurnOptions.TextInternal; + } + + public class SubtitleHardBurnOptions + { + private readonly string _subtitle; + + public readonly Dictionary Parameters = new Dictionary(); + + public static SubtitleHardBurnOptions Create(string subtitlePath) + { + return new SubtitleHardBurnOptions(subtitlePath); + } + + private SubtitleHardBurnOptions(string subtitle) + { + _subtitle = subtitle; + } + + public SubtitleHardBurnOptions SetOriginalSize(int width, int height) + { + return WithParameter("original_size", $"{width}x{height}"); + } + + public SubtitleHardBurnOptions SetOriginalSize(Size size) + { + return SetOriginalSize(size.Width, size.Height); + } + + public SubtitleHardBurnOptions SetSubtitleIndex(int index) + { + return WithParameter("si", index.ToString()); + } + + public SubtitleHardBurnOptions SetCharacterEncoding(string encode) + { + return WithParameter("charenc", encode); + } + + public SubtitleHardBurnOptions WithStyle(StyleOptions styleOptions) + { + return WithParameter("force_style", styleOptions.TextInternal); + } + + public SubtitleHardBurnOptions WithParameter(string key, string value) + { + Parameters.Add(key, value); + return this; + } + + internal string TextInternal => string.Join(":", new[] { _subtitle.EncloseIfContainsSpace() }.Concat(Parameters.Select(parameter => parameter.FormatArgumentPair(enclose: true)))); + } + + public class StyleOptions + { + public readonly Dictionary Parameters = new Dictionary(); + + public static StyleOptions Create() + { + return new StyleOptions(); + } + + public StyleOptions WithParameter(string key, string value) + { + Parameters.Add(key, value); + return this; + } + + internal string TextInternal => string.Join(",", Parameters.Select(parameter => parameter.FormatArgumentPair(enclose: false))); + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Arguments/VideoFiltersArgument.cs b/FFMpegCore/FFMpeg/Arguments/VideoFiltersArgument.cs index fa4ae1e..4d0dfde 100644 --- a/FFMpegCore/FFMpeg/Arguments/VideoFiltersArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/VideoFiltersArgument.cs @@ -50,6 +50,7 @@ public class VideoFilterOptions public VideoFilterOptions Transpose(Transposition transposition) => WithArgument(new TransposeArgument(transposition)); public VideoFilterOptions Mirror(Mirroring mirroring) => WithArgument(new SetMirroringArgument(mirroring)); public VideoFilterOptions DrawText(DrawTextOptions drawTextOptions) => WithArgument(new DrawTextArgument(drawTextOptions)); + public VideoFilterOptions HardBurnSubtitle(SubtitleHardBurnOptions subtitleHardBurnOptions) => WithArgument(new SubtitleHardBurnArgument(subtitleHardBurnOptions)); private VideoFilterOptions WithArgument(IVideoFilterArgument argument) { From 75386b6dacdd7a16492d4daa9598d6725b77f42f Mon Sep 17 00:00:00 2001 From: alex6dj Date: Thu, 5 Aug 2021 15:11:23 -0400 Subject: [PATCH 11/23] Document parameters Former-commit-id: 6247bf6ea4a0ce4bc74e1f93d4192419c27f3e7a --- FFMpegCore/Extend/KeyValuePairExtensions.cs | 9 ++++ FFMpegCore/Extend/StringExtensions.cs | 5 +++ .../Arguments/SubtitleHardBurnArgument.cs | 43 +++++++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/FFMpegCore/Extend/KeyValuePairExtensions.cs b/FFMpegCore/Extend/KeyValuePairExtensions.cs index 92dbf6d..28cc087 100644 --- a/FFMpegCore/Extend/KeyValuePairExtensions.cs +++ b/FFMpegCore/Extend/KeyValuePairExtensions.cs @@ -4,6 +4,15 @@ namespace FFMpegCore.Extend { internal static class KeyValuePairExtensions { + /// + /// Concat the two members of a + /// + /// Input object + /// + /// If true encloses the value part between quotes if contains an space character. If false use the + /// value unmodified + /// + /// The formatted string public static string FormatArgumentPair(this KeyValuePair pair, bool enclose) { var key = pair.Key; diff --git a/FFMpegCore/Extend/StringExtensions.cs b/FFMpegCore/Extend/StringExtensions.cs index f4e0169..ddcf54b 100644 --- a/FFMpegCore/Extend/StringExtensions.cs +++ b/FFMpegCore/Extend/StringExtensions.cs @@ -2,6 +2,11 @@ { internal static class StringExtensions { + /// + /// Enclose string between quotes if contains an space character + /// + /// The input + /// The enclosed string public static string EncloseIfContainsSpace(this string input) { return input.Contains(" ") ? $"'{input}'" : input; diff --git a/FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs b/FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs index 552a87b..1186ae2 100644 --- a/FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs @@ -25,6 +25,13 @@ public class SubtitleHardBurnOptions public readonly Dictionary Parameters = new Dictionary(); + /// + /// Create a new using a provided subtitle file or a video file + /// containing one. + /// + /// + /// + /// Only support .srt and .ass files, and subrip and ssa subtitle streams public static SubtitleHardBurnOptions Create(string subtitlePath) { return new SubtitleHardBurnOptions(subtitlePath); @@ -35,26 +42,56 @@ private SubtitleHardBurnOptions(string subtitle) _subtitle = subtitle; } + /// + /// Specify the size of the original video, the video for which the ASS file was composed. + /// + /// + /// + /// public SubtitleHardBurnOptions SetOriginalSize(int width, int height) { return WithParameter("original_size", $"{width}x{height}"); } + /// + /// Specify the size of the original video, the video for which the ASS file was composed. + /// + /// + /// public SubtitleHardBurnOptions SetOriginalSize(Size size) { return SetOriginalSize(size.Width, size.Height); } + /// + /// Set subtitles stream index. + /// + /// + /// + /// + /// Used when the provided subtitle is an stream of a video file (ex. .mkv) with multiple subtitles. + /// Represent the index of the subtitle not the stream, them the first subtitle index is 0 and second is 1 + /// public SubtitleHardBurnOptions SetSubtitleIndex(int index) { return WithParameter("si", index.ToString()); } + /// + /// Set subtitles input character encoding. Only useful if not UTF-8 + /// + /// Charset encoding + /// public SubtitleHardBurnOptions SetCharacterEncoding(string encode) { return WithParameter("charenc", encode); } + /// + /// Override default style or script info parameters of the subtitles + /// + /// + /// public SubtitleHardBurnOptions WithStyle(StyleOptions styleOptions) { return WithParameter("force_style", styleOptions.TextInternal); @@ -78,6 +115,12 @@ public static StyleOptions Create() return new StyleOptions(); } + /// + /// Used to override default style or script info parameters of the subtitles. It accepts ASS style format + /// + /// + /// + /// public StyleOptions WithParameter(string key, string value) { Parameters.Add(key, value); From 8633937f52b97fae96ef4ce666549adc7326309c Mon Sep 17 00:00:00 2001 From: alex6dj Date: Thu, 5 Aug 2021 15:14:37 -0400 Subject: [PATCH 12/23] Use long option for stream index for better clarity Former-commit-id: 14d457b94661bcc481f246c1f0472b30874cd12a --- FFMpegCore.Test/ArgumentBuilderTest.cs | 2 +- FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/FFMpegCore.Test/ArgumentBuilderTest.cs b/FFMpegCore.Test/ArgumentBuilderTest.cs index 71bc90b..6fd108d 100644 --- a/FFMpegCore.Test/ArgumentBuilderTest.cs +++ b/FFMpegCore.Test/ArgumentBuilderTest.cs @@ -334,7 +334,7 @@ public void Builder_BuildString_SubtitleHardBurnFilter() .WithParameter("PrimaryColour", "&HAA00FF00"))))) .Arguments; - Assert.AreEqual("-i \"input.mp4\" -vf \"subtitles=sample.srt:charenc=UTF-8:original_size=1366x768:si=0:force_style='FontName=DejaVu Serif\\,PrimaryColour=&HAA00FF00'\" \"output.mp4\"", + Assert.AreEqual("-i \"input.mp4\" -vf \"subtitles=sample.srt:charenc=UTF-8:original_size=1366x768:stream_index=0:force_style='FontName=DejaVu Serif\\,PrimaryColour=&HAA00FF00'\" \"output.mp4\"", str); } diff --git a/FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs b/FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs index 1186ae2..a48f845 100644 --- a/FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs @@ -74,7 +74,7 @@ public SubtitleHardBurnOptions SetOriginalSize(Size size) /// public SubtitleHardBurnOptions SetSubtitleIndex(int index) { - return WithParameter("si", index.ToString()); + return WithParameter("stream_index", index.ToString()); } /// From 8c67ddb769da0b92fe2d1e2571624eb86c6e1cf8 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Fri, 6 Aug 2021 12:13:35 +0200 Subject: [PATCH 13/23] Replace contributors block with image from contributors-img Former-commit-id: e13376f17531f2fcb920119aa2e31794f518585b --- README.md | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 298489e..1eb08a3 100644 --- a/README.md +++ b/README.md @@ -212,16 +212,9 @@ The root and temp directory for the ffmpeg binaries can be configured via the `f ## Contributors - - - - - - - - - - + + + ### License From 1f3d1ec429235fa20f29ce4f49b3536b3f1205c1 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Sat, 7 Aug 2021 14:40:58 +0200 Subject: [PATCH 14/23] Move Loop(1) to image FileInput. Fix for #206 Former-commit-id: 914003ee3252869d3f2846d2e0427f65fca68ff2 --- FFMpegCore/FFMpeg/FFMpeg.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/FFMpegCore/FFMpeg/FFMpeg.cs b/FFMpegCore/FFMpeg/FFMpeg.cs index 42c344b..2928e29 100644 --- a/FFMpegCore/FFMpeg/FFMpeg.cs +++ b/FFMpegCore/FFMpeg/FFMpeg.cs @@ -231,10 +231,10 @@ public static bool PosterWithAudio(string image, string audio, string output) FFMpegHelper.ConversionSizeExceptionCheck(Image.FromFile(image)); return FFMpegArguments - .FromFileInput(image) + .FromFileInput(image, false, options => options + .Loop(1)) .AddFileInput(audio) .OutputToFile(output, true, options => options - .Loop(1) .WithVideoCodec(VideoCodec.LibX264) .WithConstantRateFactor(21) .WithAudioBitrate(AudioQuality.Normal) From 3516440ca1e2cab5b38ba15e2c72e75ccc491455 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Sat, 7 Aug 2021 14:54:48 +0200 Subject: [PATCH 15/23] Specify README.md as nuget readme Former-commit-id: 0efaf686f36d08850f4e1a6bb7020048093e779a --- FFMpegCore/FFMpegCore.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/FFMpegCore/FFMpegCore.csproj b/FFMpegCore/FFMpegCore.csproj index dd96a3d..5f7b969 100644 --- a/FFMpegCore/FFMpegCore.csproj +++ b/FFMpegCore/FFMpegCore.csproj @@ -6,9 +6,8 @@ https://github.com/rosenbjerg/FFMpegCore A .NET Standard FFMpeg/FFProbe wrapper for easily integrating media analysis and conversion into your .NET applications - 3.0.0.0 - 3.0.0.0 - 3.0.0.0 + 4.0.0.0 + README.md - Cancellation token support (thanks patagonaa) - Support for setting stdout and stderr encoding for ffprobe (thanks CepheiSigma) - Improved ffprobe exceptions @@ -27,6 +26,7 @@ Always + From 1c8d0fcd4f86b6fb6a0eab924a9669f6e1c05167 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Sat, 7 Aug 2021 15:34:40 +0200 Subject: [PATCH 16/23] Update README.md Former-commit-id: 78c749c748e980fb3f0d53c98ea934b3e7f4a55a --- README.md | 121 +++++++++++++++++++++++++++--------------------------- 1 file changed, 60 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index 1eb08a3..3d19fa8 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,29 @@ -# FFMpegCore -[![CI](https://github.com/rosenbjerg/FFMpegCore/workflows/CI/badge.svg)](https://github.com/rosenbjerg/FFMpegCore/actions?query=workflow%3ACI) +# [FFMpegCore](https://www.nuget.org/packages/FFMpegCore/) [![NuGet Badge](https://buildstats.info/nuget/FFMpegCore)](https://www.nuget.org/packages/FFMpegCore/) [![GitHub issues](https://img.shields.io/github/issues/rosenbjerg/FFMpegCore)](https://github.com/rosenbjerg/FFMpegCore/issues) [![GitHub stars](https://img.shields.io/github/stars/rosenbjerg/FFMpegCore)](https://github.com/rosenbjerg/FFMpegCore/stargazers) [![GitHub](https://img.shields.io/github/license/rosenbjerg/FFMpegCore)](https://github.com/rosenbjerg/FFMpegCore/blob/master/LICENSE) +[![CI](https://github.com/rosenbjerg/FFMpegCore/workflows/CI/badge.svg)](https://github.com/rosenbjerg/FFMpegCore/actions?query=workflow%3ACI) -# Setup - -#### NuGet: - -``` -Install-Package FFMpegCore -``` - -A .NET Standard FFMpeg/FFProbe wrapper for easily integrating media analysis and conversion into your C# applications. Support both synchronous and asynchronous use +A .NET Standard FFMpeg/FFProbe wrapper for easily integrating media analysis and conversion into your .NET applications. Supports both synchronous and asynchronous calls # API ## FFProbe -FFProbe is used to gather media information: +Use FFProbe to analyze media files: ```csharp -var mediaInfo = FFProbe.Analyse(inputPath); +var mediaInfo = await FFProbe.AnalyseAsync(inputPath); ``` or ```csharp -var mediaInfo = await FFProbe.AnalyseAsync(inputPath); +var mediaInfo = FFProbe.Analyse(inputPath); ``` ## FFMpeg -FFMpeg is used for converting your media files to web ready formats. +Use FFMpeg to convert your media files. Easily build your FFMpeg arguments using the fluent argument builder: Convert input file to h264/aac scaled to 720p w/ faststart, for web playback @@ -49,15 +41,6 @@ FFMpegArguments .ProcessSynchronously(); ``` -Easily capture screens from your videos: -```csharp -// process the snapshot in-memory and use the Bitmap directly -var bitmap = FFMpeg.Snapshot(inputPath, new Size(200, 400), TimeSpan.FromMinutes(1)); - -// or persists the image on the drive -FFMpeg.Snapshot(inputPath, outputPath, new Size(200, 400), TimeSpan.FromMinutes(1)); -``` - Convert to and/or from streams ```csharp await FFMpegArguments @@ -68,7 +51,19 @@ await FFMpegArguments .ProcessAsynchronously(); ``` -Join video parts into one single file: +## Helper methods +The provided helper methods makes it simple to perform common operations. + +### Easily capture snapshots from a video file: +```csharp +// process the snapshot in-memory and use the Bitmap directly +var bitmap = FFMpeg.Snapshot(inputPath, new Size(200, 400), TimeSpan.FromMinutes(1)); + +// or persists the image on the drive +FFMpeg.Snapshot(inputPath, outputPath, new Size(200, 400), TimeSpan.FromMinutes(1)); +``` + +### Join video parts into one single file: ```csharp FFMpeg.Join(@"..\joined_video.mp4", @"..\part1.mp4", @@ -77,7 +72,7 @@ FFMpeg.Join(@"..\joined_video.mp4", ); ``` -Join images into a video: +### Join images into a video: ```csharp FFMpeg.JoinImageSequence(@"..\joined_video.mp4", frameRate: 1, ImageInfo.FromPath(@"..\1.png"), @@ -86,22 +81,22 @@ FFMpeg.JoinImageSequence(@"..\joined_video.mp4", frameRate: 1, ); ``` -Mute videos: +### Mute the audio of a video file: ```csharp FFMpeg.Mute(inputPath, outputPath); ``` -Save audio track from video: +### Extract the audio track from a video file: ```csharp FFMpeg.ExtractAudio(inputPath, outputPath); ``` -Add or replace audio track on video: +### Add or replace the audio track of a video file: ```csharp FFMpeg.ReplaceAudio(inputPath, inputAudioPath, outputPath); ``` -Add poster image to audio file (good for youtube videos): +### Combine an image with audio file, for youtube or similar platforms ```csharp FFMpeg.PosterWithAudio(inputPath, inputAudioPath, outputPath); // or @@ -111,26 +106,27 @@ image.AddAudio(inputAudioPath, outputPath); Other available arguments could be found in `FFMpegCore.Arguments` namespace. -### Input piping -With input piping it is possible to write video frames directly from program memory without saving them to jpeg or png and then passing path to input of ffmpeg. This feature also allows us to convert video on-the-fly while frames are being generated or received. +## Input piping +With input piping it is possible to write video frames directly from program memory without saving them to jpeg or png and then passing path to input of ffmpeg. This feature also allows for converting video on-the-fly while frames are being generated or received. -The `IPipeSource` interface is used as the source of data. It could be represented as encoded video stream or raw frames stream. Currently, the `IPipeSource` interface has single implementation, `RawVideoPipeSource` that is used for raw stream encoding. +An object implementing the `IPipeSource` interface is used as the source of data. Currently, the `IPipeSource` interface has two implementations; `StreamPipeSource` for streams, and `RawVideoPipeSource` for raw video frames. -For example: +### Working with raw video frames -Method that is generating bitmap frames: +Method for generating bitmap frames: ```csharp IEnumerable CreateFrames(int count) { for(int i = 0; i < count; i++) { - yield return GetNextFrame(); //method of generating new frames + yield return GetNextFrame(); //method that generates of receives the next frame } } ``` -Then create `ArgumentsContainer` with `InputPipeArgument` + +Then create a `RawVideoPipeSource` that utilises your video frame source ```csharp -var videoFramesSource = new RawVideoPipeSource(CreateFrames(64)) //pass IEnumerable or IEnumerator to constructor of RawVideoPipeSource +var videoFramesSource = new RawVideoPipeSource(CreateFrames(64)) { FrameRate = 30 //set source frame rate }; @@ -141,51 +137,43 @@ await FFMpegArguments .ProcessAsynchronously(); ``` -if you want to use `System.Drawing.Bitmap` as `IVideoFrame`, there is a `BitmapVideoFrameWrapper` wrapper class. +If you want to use `System.Drawing.Bitmap`s as `IVideoFrame`s, a `BitmapVideoFrameWrapper` wrapper class is provided. -## Binaries +# Binaries +## Installation If you prefer to manually download them, visit [ffbinaries](https://ffbinaries.com/downloads) or [zeranoe Windows builds](https://ffmpeg.zeranoe.com/builds/). -#### Windows - -command: `choco install ffmpeg -Y` +### Windows (using choco) +command: `choco install ffmpeg -y` location: `C:\ProgramData\chocolatey\lib\ffmpeg\tools\ffmpeg\bin` -#### Mac OSX - +### Mac OSX command: `brew install ffmpeg mono-libgdiplus` location: `/usr/local/bin` -#### Ubuntu - +### Ubuntu command: `sudo apt-get install -y ffmpeg libgdiplus` location: `/usr/bin` + ## Path Configuration -#### Behavior - -If you wish to support multiple client processor architectures, you can do so by creating a folder `x64` and `x86` in the `root` directory. -Both folders should contain the binaries (`ffmpeg.exe` and `ffprobe.exe`) for build for the respective architectures. - -By doing so, the library will attempt to use either `/root/{ARCH}/(ffmpeg|ffprobe).exe`. - -If these folders are not defined, it will try to find the binaries in `/root/(ffmpeg|ffprobe.exe)` - -#### Option 1 +### Option 1 The default value of an empty string (expecting ffmpeg to be found through PATH) can be overwritten via the `FFOptions` class: -```c# +```csharp // setting global options GlobalFFOptions.Configure(new FFOptions { BinaryFolder = "./bin", TemporaryFilesFolder = "/tmp" }); + // or GlobalFFOptions.Configure(options => options.BinaryFolder = "./bin"); + // on some systems the absolute path may be required, in which case GlobalFFOptions.Configure(new FFOptions { BinaryFolder = Server.MapPath("./bin"), TemporaryFilesFolder = Server.MapPath("/tmp") }); @@ -196,9 +184,9 @@ await FFMpegArguments .ProcessAsynchronously(true, new FFOptions { BinaryFolder = "./bin", TemporaryFilesFolder = "/tmp" }); ``` -#### Option 2 +### Option 2 -The root and temp directory for the ffmpeg binaries can be configured via the `ffmpeg.config.json` file. +The root and temp directory for the ffmpeg binaries can be configured via the `ffmpeg.config.json` file, which will be read on first use only. ```json { @@ -207,8 +195,19 @@ The root and temp directory for the ffmpeg binaries can be configured via the `f } ``` +### Supporting both 32 and 64 bit processes +If you wish to support multiple client processor architectures, you can do so by creating a folder `x64` and `x86` in the `root` directory. +Both folders should contain the binaries (`ffmpeg.exe` and `ffprobe.exe`) for build for the respective architectures. + +By doing so, the library will attempt to use either `/root/{ARCH}/(ffmpeg|ffprobe).exe`. + +If these folders are not defined, it will try to find the binaries in `/root/(ffmpeg|ffprobe.exe)`. + +(`.exe` is only appended on Windows) + + # Compatibility - Some versions of FFMPEG might not have the same argument schema. The lib has been tested with version `3.3` to `4.2` +Older versions of ffmpeg might not support all ffmpeg arguments available through this library. The library has been tested with version `3.3` to `4.2` ## Contributors From b466c1b649ff752eaf3674f8a1032ad9bc587f8a Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Sat, 7 Aug 2021 16:08:15 +0200 Subject: [PATCH 17/23] Add non-code contributors Former-commit-id: 2722431457baee63caadda56188e627cf7320dfe --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3d19fa8..d58e3fe 100644 --- a/README.md +++ b/README.md @@ -210,11 +210,15 @@ If these folders are not defined, it will try to find the binaries in `/root/(ff Older versions of ffmpeg might not support all ffmpeg arguments available through this library. The library has been tested with version `3.3` to `4.2` -## Contributors +## Code contributors +## Non-code contributors + + + ### License Copyright © 2021 From d73d7fa30e2ff65cb4dea3343080e807430d4e8a Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Sat, 7 Aug 2021 16:39:10 +0200 Subject: [PATCH 18/23] Load config file, if found, on first use Former-commit-id: e363440118e3f92bcdd3a37e1c5aa239d93a2f63 --- FFMpegCore/FFMpegCore.csproj | 2 +- FFMpegCore/GlobalFFOptions.cs | 29 +++++++++++++++++------------ 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/FFMpegCore/FFMpegCore.csproj b/FFMpegCore/FFMpegCore.csproj index 5f7b969..daf7901 100644 --- a/FFMpegCore/FFMpegCore.csproj +++ b/FFMpegCore/FFMpegCore.csproj @@ -26,7 +26,7 @@ Always - + diff --git a/FFMpegCore/GlobalFFOptions.cs b/FFMpegCore/GlobalFFOptions.cs index 358787a..37340cc 100644 --- a/FFMpegCore/GlobalFFOptions.cs +++ b/FFMpegCore/GlobalFFOptions.cs @@ -8,27 +8,20 @@ namespace FFMpegCore public static class GlobalFFOptions { private static readonly string ConfigFile = "ffmpeg.config.json"; + private static FFOptions? _current; - public static FFOptions Current { get; private set; } - static GlobalFFOptions() + public static FFOptions Current { - if (File.Exists(ConfigFile)) - { - Current = JsonSerializer.Deserialize(File.ReadAllText(ConfigFile))!; - } - else - { - Current = new FFOptions(); - } + get { return _current ??= LoadFFOptions(); } } - + public static void Configure(Action optionsAction) { optionsAction?.Invoke(Current); } public static void Configure(FFOptions ffOptions) { - Current = ffOptions ?? throw new ArgumentNullException(nameof(ffOptions)); + _current = ffOptions ?? throw new ArgumentNullException(nameof(ffOptions)); } @@ -48,5 +41,17 @@ private static string GetFFBinaryPath(string name, FFOptions ffOptions) return Path.Combine(ffOptions.BinaryFolder, ffName); } + + private static FFOptions LoadFFOptions() + { + if (File.Exists(ConfigFile)) + { + return JsonSerializer.Deserialize(File.ReadAllText(ConfigFile))!; + } + else + { + return new FFOptions(); + } + } } } From cbd33fd553e14950538f1e34d3c87f609d0acbee Mon Sep 17 00:00:00 2001 From: Warrick Wilson Date: Mon, 9 Aug 2021 14:52:39 -0500 Subject: [PATCH 19/23] Added CodecTag and CodecTagString properties to info derived from an FFProbe.Analyse() call. This allows for specific codecs to be identified to check compatibility with hardware limitations. Former-commit-id: ac2b358fadcb727ca76851a8bfe396c26b795f22 --- FFMpegCore.Test/FFProbeTests.cs | 4 ++++ FFMpegCore/FFProbe/FFProbeAnalysis.cs | 6 ++++++ FFMpegCore/FFProbe/MediaAnalysis.cs | 4 ++++ FFMpegCore/FFProbe/MediaStream.cs | 2 ++ 4 files changed, 16 insertions(+) diff --git a/FFMpegCore.Test/FFProbeTests.cs b/FFMpegCore.Test/FFProbeTests.cs index 897848d..7af92cd 100644 --- a/FFMpegCore.Test/FFProbeTests.cs +++ b/FFMpegCore.Test/FFProbeTests.cs @@ -63,6 +63,8 @@ public void Probe_Success() Assert.AreEqual("LC", info.PrimaryAudioStream.Profile); Assert.AreEqual(377351, info.PrimaryAudioStream.BitRate); Assert.AreEqual(48000, info.PrimaryAudioStream.SampleRateHz); + Assert.AreEqual("mp4a", info.PrimaryAudioStream.CodecTagString); + Assert.AreEqual("0x6134706d", info.PrimaryAudioStream.CodecTag); Assert.AreEqual(1471810, info.PrimaryVideoStream!.BitRate); Assert.AreEqual(16, info.PrimaryVideoStream.DisplayAspectRatio.Width); @@ -76,6 +78,8 @@ public void Probe_Success() Assert.AreEqual("h264", info.PrimaryVideoStream.CodecName); Assert.AreEqual(8, info.PrimaryVideoStream.BitsPerRawSample); Assert.AreEqual("Main", info.PrimaryVideoStream.Profile); + Assert.AreEqual("avc1", info.PrimaryVideoStream.CodecTagString); + Assert.AreEqual("0x31637661", info.PrimaryVideoStream.CodecTag); } [TestMethod, Timeout(10000)] diff --git a/FFMpegCore/FFProbe/FFProbeAnalysis.cs b/FFMpegCore/FFProbe/FFProbeAnalysis.cs index a0f2d41..1997cc3 100644 --- a/FFMpegCore/FFProbe/FFProbeAnalysis.cs +++ b/FFMpegCore/FFProbe/FFProbeAnalysis.cs @@ -41,6 +41,12 @@ public class FFProbeStream : ITagsContainer [JsonPropertyName("codec_long_name")] public string CodecLongName { get; set; } = null!; + [JsonPropertyName("codec_tag")] + public string CodecTag { get; set; } = null!; + + [JsonPropertyName("codec_tag_string")] + public string CodecTagString { get; set; } = null!; + [JsonPropertyName("display_aspect_ratio")] public string DisplayAspectRatio { get; set; } = null!; diff --git a/FFMpegCore/FFProbe/MediaAnalysis.cs b/FFMpegCore/FFProbe/MediaAnalysis.cs index d021813..aea714c 100644 --- a/FFMpegCore/FFProbe/MediaAnalysis.cs +++ b/FFMpegCore/FFProbe/MediaAnalysis.cs @@ -56,6 +56,8 @@ private VideoStream ParseVideoStream(FFProbeStream stream) BitsPerRawSample = !string.IsNullOrEmpty(stream.BitsPerRawSample) ? MediaAnalysisUtils.ParseIntInvariant(stream.BitsPerRawSample) : default, CodecName = stream.CodecName, CodecLongName = stream.CodecLongName, + CodecTag = stream.CodecTag, + CodecTagString = stream.CodecTagString, DisplayAspectRatio = MediaAnalysisUtils.ParseRatioInt(stream.DisplayAspectRatio, ':'), Duration = MediaAnalysisUtils.ParseDuration(stream), FrameRate = MediaAnalysisUtils.DivideRatio(MediaAnalysisUtils.ParseRatioDouble(stream.FrameRate, '/')), @@ -77,6 +79,8 @@ private AudioStream ParseAudioStream(FFProbeStream stream) BitRate = !string.IsNullOrEmpty(stream.BitRate) ? MediaAnalysisUtils.ParseIntInvariant(stream.BitRate) : default, CodecName = stream.CodecName, CodecLongName = stream.CodecLongName, + CodecTag = stream.CodecTag, + CodecTagString = stream.CodecTagString, Channels = stream.Channels ?? default, ChannelLayout = stream.ChannelLayout, Duration = MediaAnalysisUtils.ParseDuration(stream), diff --git a/FFMpegCore/FFProbe/MediaStream.cs b/FFMpegCore/FFProbe/MediaStream.cs index 0780c8e..22186c5 100644 --- a/FFMpegCore/FFProbe/MediaStream.cs +++ b/FFMpegCore/FFProbe/MediaStream.cs @@ -10,6 +10,8 @@ public class MediaStream public int Index { get; internal set; } public string CodecName { get; internal set; } = null!; public string CodecLongName { get; internal set; } = null!; + public string CodecTagString { get; set; } = null!; + public string CodecTag { get; set; } = null!; public int BitRate { get; internal set; } public TimeSpan Duration { get; internal set; } public string? Language { get; internal set; } From bc00b6786a744260848f918dceffe1ce0eb2a38e Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Tue, 10 Aug 2021 10:38:56 +0200 Subject: [PATCH 20/23] Increase timeout on failing unit test Former-commit-id: 04ebdb1907b9569bc43f7dc9744eb05f760f4cc4 --- FFMpegCore.Test/VideoTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index 45853ea..27ec79e 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -655,7 +655,7 @@ public async Task Video_Cancel_CancellationToken_Async_With_Timeout() .WithAudioCodec(AudioCodec.Aac) .WithVideoCodec(VideoCodec.LibX264) .WithSpeedPreset(Speed.VeryFast)) - .CancellableThrough(cts.Token, 5000) + .CancellableThrough(cts.Token, 8000) .ProcessAsynchronously(false); await Task.Delay(300); From 65ebc57b296e316c29d094045b05f316115279d4 Mon Sep 17 00:00:00 2001 From: Fedor Zhilkin Date: Wed, 11 Aug 2021 01:21:06 +0300 Subject: [PATCH 21/23] Nullable streamIndex and inputFileIndex Former-commit-id: 562a50d874d061f78c1c0386d3c3ab18fa98a5e9 --- FFMpegCore.Test/ArgumentBuilderTest.cs | 2 +- .../FFMpeg/Arguments/MapStreamArgument.cs | 10 ++-- FFMpegCore/FFMpeg/FFMpeg.cs | 46 +++++++++++-------- FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs | 4 +- 4 files changed, 37 insertions(+), 25 deletions(-) diff --git a/FFMpegCore.Test/ArgumentBuilderTest.cs b/FFMpegCore.Test/ArgumentBuilderTest.cs index 082d9bf..5da3e0b 100644 --- a/FFMpegCore.Test/ArgumentBuilderTest.cs +++ b/FFMpegCore.Test/ArgumentBuilderTest.cs @@ -234,7 +234,7 @@ public void Builder_BuildString_FrameOutputCount() [TestMethod] public void Builder_BuildString_VideoStreamNumber() { - var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.SelectStream(1)).Arguments; + var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.SelectStream(0,1)).Arguments; Assert.AreEqual("-i \"input.mp4\" -map 0:1 \"output.mp4\"", str); } diff --git a/FFMpegCore/FFMpeg/Arguments/MapStreamArgument.cs b/FFMpegCore/FFMpeg/Arguments/MapStreamArgument.cs index 01a537d..6ce1dbe 100644 --- a/FFMpegCore/FFMpeg/Arguments/MapStreamArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/MapStreamArgument.cs @@ -1,17 +1,19 @@ namespace FFMpegCore.Arguments { /// - /// Represents choice of video stream, works with one input file + /// Represents choice of video stream /// public class MapStreamArgument : IArgument { + private readonly int _inputFileIndex; private readonly int _streamIndex; - public MapStreamArgument(int index) + public MapStreamArgument(int inputFileIndex, int streamIndex) { - _streamIndex = index; + _inputFileIndex = inputFileIndex; + _streamIndex = streamIndex; } - public string Text => $"-map 0:{_streamIndex}"; + public string Text => $"-map {_inputFileIndex}:{_streamIndex}"; } } \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/FFMpeg.cs b/FFMpegCore/FFMpeg/FFMpeg.cs index 14556b4..bffbd1a 100644 --- a/FFMpegCore/FFMpeg/FFMpeg.cs +++ b/FFMpegCore/FFMpeg/FFMpeg.cs @@ -20,16 +20,17 @@ public static class FFMpeg /// Output video file path /// Seek position where the thumbnail should be taken. /// Thumbnail size. If width or height equal 0, the other will be computed automatically. + /// Input file index /// Selected video stream index. /// Bitmap with the requested snapshot. - public static bool Snapshot(string input, string output, Size? size = null, TimeSpan? captureTime = null, int streamIndex = 0) + public static bool Snapshot(string input, string output, Size? size = null, TimeSpan? captureTime = null, int inputFileIndex = 0, int? streamIndex = null) { if (Path.GetExtension(output) != FileExtension.Png) output = Path.GetFileNameWithoutExtension(output) + FileExtension.Png; var source = FFProbe.Analyse(input); - var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime, streamIndex); - + var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime, inputFileIndex, streamIndex); + return arguments .OutputToFile(output, true, outputOptions) .ProcessSynchronously(); @@ -41,16 +42,17 @@ public static bool Snapshot(string input, string output, Size? size = null, Time /// Output video file path /// Seek position where the thumbnail should be taken. /// Thumbnail size. If width or height equal 0, the other will be computed automatically. + /// Input file index /// Selected video stream index. /// Bitmap with the requested snapshot. - public static async Task SnapshotAsync(string input, string output, Size? size = null, TimeSpan? captureTime = null, int streamIndex = 0) + public static async Task SnapshotAsync(string input, string output, Size? size = null, TimeSpan? captureTime = null, int inputFileIndex = 0, int? streamIndex = null) { if (Path.GetExtension(output) != FileExtension.Png) output = Path.GetFileNameWithoutExtension(output) + FileExtension.Png; var source = await FFProbe.AnalyseAsync(input); - var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime, streamIndex); - + var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime, inputFileIndex, streamIndex); + return await arguments .OutputToFile(output, true, outputOptions) .ProcessAsynchronously(); @@ -62,14 +64,15 @@ public static async Task SnapshotAsync(string input, string output, Size? /// Source video file. /// Seek position where the thumbnail should be taken. /// Thumbnail size. If width or height equal 0, the other will be computed automatically. + /// Input file index /// Selected video stream index. /// Bitmap with the requested snapshot. - public static Bitmap Snapshot(string input, Size? size = null, TimeSpan? captureTime = null, int streamIndex = 0) + public static Bitmap Snapshot(string input, Size? size = null, TimeSpan? captureTime = null, int inputFileIndex = 0, int? streamIndex = null) { var source = FFProbe.Analyse(input); - var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime, streamIndex); + var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime, inputFileIndex, streamIndex); using var ms = new MemoryStream(); - + arguments .OutputToPipe(new StreamPipeSink(ms), options => outputOptions(options .ForceFormat("rawvideo"))) @@ -85,14 +88,15 @@ public static Bitmap Snapshot(string input, Size? size = null, TimeSpan? capture /// Source video file. /// Seek position where the thumbnail should be taken. /// Thumbnail size. If width or height equal 0, the other will be computed automatically. + /// Input file index /// Selected video stream index. /// Bitmap with the requested snapshot. - public static async Task SnapshotAsync(string input, Size? size = null, TimeSpan? captureTime = null, int streamIndex = 0) + public static async Task SnapshotAsync(string input, Size? size = null, TimeSpan? captureTime = null, int inputFileIndex = 0, int? streamIndex = null) { var source = await FFProbe.AnalyseAsync(input); - var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime, streamIndex); + var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime, inputFileIndex, streamIndex); using var ms = new MemoryStream(); - + await arguments .OutputToPipe(new StreamPipeSink(ms), options => outputOptions(options .ForceFormat("rawvideo"))) @@ -102,17 +106,23 @@ await arguments return new Bitmap(ms); } - private static (FFMpegArguments, Action outputOptions) BuildSnapshotArguments(string input, IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null, int streamIndex = 0) + private static (FFMpegArguments, Action outputOptions) BuildSnapshotArguments( + string input, + IMediaAnalysis source, + Size? size = null, + TimeSpan? captureTime = null, + int inputFileIndex = 0, + int? streamIndex = null) { captureTime ??= TimeSpan.FromSeconds(source.Duration.TotalSeconds / 3); size = PrepareSnapshotSize(source, size); - var index = source.VideoStreams.FirstOrDefault(videoStream => videoStream.Index == streamIndex)?.Index; + streamIndex = streamIndex == null ? 0 : source.VideoStreams.FirstOrDefault(videoStream => videoStream.Index == streamIndex).Index; return (FFMpegArguments .FromFileInput(input, false, options => options - .Seek(captureTime)), + .Seek(captureTime)), options => options - .SelectStream(index ?? 0) + .SelectStream((int)streamIndex, inputFileIndex) .WithVideoCodec(VideoCodec.Png) .WithFrameOutputCount(1) .Resize(size)); @@ -122,11 +132,11 @@ private static (FFMpegArguments, Action outputOptions) Bu { if (wantedSize == null || (wantedSize.Value.Height <= 0 && wantedSize.Value.Width <= 0) || source.PrimaryVideoStream == null) return null; - + var currentSize = new Size(source.PrimaryVideoStream.Width, source.PrimaryVideoStream.Height); if (source.PrimaryVideoStream.Rotation == 90 || source.PrimaryVideoStream.Rotation == 180) currentSize = new Size(source.PrimaryVideoStream.Height, source.PrimaryVideoStream.Width); - + if (wantedSize.Value.Width != currentSize.Width || wantedSize.Value.Height != currentSize.Height) { if (wantedSize.Value.Width <= 0 && wantedSize.Value.Height > 0) diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs b/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs index 94b1cb2..1126471 100644 --- a/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs +++ b/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs @@ -48,11 +48,11 @@ public FFMpegArgumentOptions WithVideoFilters(Action videoFi public FFMpegArgumentOptions WithSpeedPreset(Speed speed) => WithArgument(new SpeedPresetArgument(speed)); public FFMpegArgumentOptions WithStartNumber(int startNumber) => WithArgument(new StartNumberArgument(startNumber)); public FFMpegArgumentOptions WithCustomArgument(string argument) => WithArgument(new CustomArgument(argument)); - + public FFMpegArgumentOptions Seek(TimeSpan? seekTo) => WithArgument(new SeekArgument(seekTo)); public FFMpegArgumentOptions Loop(int times) => WithArgument(new LoopArgument(times)); public FFMpegArgumentOptions OverwriteExisting() => WithArgument(new OverwriteArgument()); - public FFMpegArgumentOptions SelectStream(int index) => WithArgument(new MapStreamArgument(index)); + public FFMpegArgumentOptions SelectStream(int streamIndex, int inputFileIndex = 0) => WithArgument(new MapStreamArgument(inputFileIndex, streamIndex)); public FFMpegArgumentOptions ForceFormat(ContainerFormat format) => WithArgument(new ForceFormatArgument(format)); public FFMpegArgumentOptions ForceFormat(string format) => WithArgument(new ForceFormatArgument(format)); From 847ed705221311d6758b9ae6df423a7c03bf5969 Mon Sep 17 00:00:00 2001 From: Fedor Zhilkin Date: Wed, 11 Aug 2021 15:54:58 +0300 Subject: [PATCH 22/23] Fix params order Former-commit-id: 86ea16c432a35e27e7c4e1986bd8a8e49549d4ce --- FFMpegCore.Test/ArgumentBuilderTest.cs | 2 +- .../FFMpeg/Arguments/MapStreamArgument.cs | 2 +- FFMpegCore/FFMpeg/FFMpeg.cs | 28 +++++++++---------- FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs | 12 ++++---- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/FFMpegCore.Test/ArgumentBuilderTest.cs b/FFMpegCore.Test/ArgumentBuilderTest.cs index 5da3e0b..082d9bf 100644 --- a/FFMpegCore.Test/ArgumentBuilderTest.cs +++ b/FFMpegCore.Test/ArgumentBuilderTest.cs @@ -234,7 +234,7 @@ public void Builder_BuildString_FrameOutputCount() [TestMethod] public void Builder_BuildString_VideoStreamNumber() { - var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.SelectStream(0,1)).Arguments; + var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.SelectStream(1)).Arguments; Assert.AreEqual("-i \"input.mp4\" -map 0:1 \"output.mp4\"", str); } diff --git a/FFMpegCore/FFMpeg/Arguments/MapStreamArgument.cs b/FFMpegCore/FFMpeg/Arguments/MapStreamArgument.cs index 6ce1dbe..b904be5 100644 --- a/FFMpegCore/FFMpeg/Arguments/MapStreamArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/MapStreamArgument.cs @@ -8,7 +8,7 @@ public class MapStreamArgument : IArgument private readonly int _inputFileIndex; private readonly int _streamIndex; - public MapStreamArgument(int inputFileIndex, int streamIndex) + public MapStreamArgument(int streamIndex, int inputFileIndex) { _inputFileIndex = inputFileIndex; _streamIndex = streamIndex; diff --git a/FFMpegCore/FFMpeg/FFMpeg.cs b/FFMpegCore/FFMpeg/FFMpeg.cs index bffbd1a..c46d864 100644 --- a/FFMpegCore/FFMpeg/FFMpeg.cs +++ b/FFMpegCore/FFMpeg/FFMpeg.cs @@ -20,16 +20,16 @@ public static class FFMpeg /// Output video file path /// Seek position where the thumbnail should be taken. /// Thumbnail size. If width or height equal 0, the other will be computed automatically. - /// Input file index /// Selected video stream index. + /// Input file index /// Bitmap with the requested snapshot. - public static bool Snapshot(string input, string output, Size? size = null, TimeSpan? captureTime = null, int inputFileIndex = 0, int? streamIndex = null) + 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.GetFileNameWithoutExtension(output) + FileExtension.Png; var source = FFProbe.Analyse(input); - var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime, inputFileIndex, streamIndex); + var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex); return arguments .OutputToFile(output, true, outputOptions) @@ -42,16 +42,16 @@ public static bool Snapshot(string input, string output, Size? size = null, Time /// Output video file path /// Seek position where the thumbnail should be taken. /// Thumbnail size. If width or height equal 0, the other will be computed automatically. - /// Input file index /// Selected video stream index. + /// Input file index /// Bitmap with the requested snapshot. - public static async Task SnapshotAsync(string input, string output, Size? size = null, TimeSpan? captureTime = null, int inputFileIndex = 0, int? streamIndex = null) + public static async Task 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.GetFileNameWithoutExtension(output) + FileExtension.Png; var source = await FFProbe.AnalyseAsync(input); - var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime, inputFileIndex, streamIndex); + var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex); return await arguments .OutputToFile(output, true, outputOptions) @@ -64,13 +64,13 @@ public static async Task SnapshotAsync(string input, string output, Size? /// Source video file. /// Seek position where the thumbnail should be taken. /// Thumbnail size. If width or height equal 0, the other will be computed automatically. - /// Input file index /// Selected video stream index. + /// Input file index /// Bitmap with the requested snapshot. - public static Bitmap Snapshot(string input, Size? size = null, TimeSpan? captureTime = null, int inputFileIndex = 0, int? streamIndex = null) + public static Bitmap Snapshot(string input, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0) { var source = FFProbe.Analyse(input); - var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime, inputFileIndex, streamIndex); + var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex); using var ms = new MemoryStream(); arguments @@ -88,13 +88,13 @@ public static Bitmap Snapshot(string input, Size? size = null, TimeSpan? capture /// Source video file. /// Seek position where the thumbnail should be taken. /// Thumbnail size. If width or height equal 0, the other will be computed automatically. - /// Input file index /// Selected video stream index. + /// Input file index /// Bitmap with the requested snapshot. - public static async Task SnapshotAsync(string input, Size? size = null, TimeSpan? captureTime = null, int inputFileIndex = 0, int? streamIndex = null) + public static async Task SnapshotAsync(string input, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0) { var source = await FFProbe.AnalyseAsync(input); - var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime, inputFileIndex, streamIndex); + var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex); using var ms = new MemoryStream(); await arguments @@ -111,8 +111,8 @@ private static (FFMpegArguments, Action outputOptions) Bu IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null, - int inputFileIndex = 0, - int? streamIndex = null) + int? streamIndex = null, + int inputFileIndex = 0) { captureTime ??= TimeSpan.FromSeconds(source.Duration.TotalSeconds / 3); size = PrepareSnapshotSize(source, size); diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs b/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs index 1126471..41ac38c 100644 --- a/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs +++ b/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs @@ -8,7 +8,7 @@ namespace FFMpegCore public class FFMpegArgumentOptions : FFMpegArgumentsBase { internal FFMpegArgumentOptions() { } - + public FFMpegArgumentOptions WithAudioCodec(Codec audioCodec) => WithArgument(new AudioCodecArgument(audioCodec)); public FFMpegArgumentOptions WithAudioCodec(string audioCodec) => WithArgument(new AudioCodecArgument(audioCodec)); public FFMpegArgumentOptions WithAudioBitrate(AudioQuality audioQuality) => WithArgument(new AudioBitrateArgument(audioQuality)); @@ -17,9 +17,9 @@ internal FFMpegArgumentOptions() { } public FFMpegArgumentOptions WithVariableBitrate(int vbr) => WithArgument(new VariableBitRateArgument(vbr)); public FFMpegArgumentOptions Resize(int width, int height) => WithArgument(new SizeArgument(width, height)); public FFMpegArgumentOptions Resize(Size? size) => WithArgument(new SizeArgument(size)); - - + + public FFMpegArgumentOptions WithBitStreamFilter(Channel channel, Filter filter) => WithArgument(new BitStreamFilterArgument(channel, filter)); public FFMpegArgumentOptions WithConstantRateFactor(int crf) => WithArgument(new ConstantRateFactorArgument(crf)); public FFMpegArgumentOptions CopyChannel(Channel channel = Channel.Both) => WithArgument(new CopyArgument(channel)); @@ -27,12 +27,12 @@ internal FFMpegArgumentOptions() { } public FFMpegArgumentOptions WithDuration(TimeSpan? duration) => WithArgument(new DurationArgument(duration)); public FFMpegArgumentOptions WithFastStart() => WithArgument(new FaststartArgument()); public FFMpegArgumentOptions WithFrameOutputCount(int frames) => WithArgument(new FrameOutputCountArgument(frames)); - public FFMpegArgumentOptions WithHardwareAcceleration(HardwareAccelerationDevice hardwareAccelerationDevice = HardwareAccelerationDevice.Auto) => WithArgument(new HardwareAccelerationArgument(hardwareAccelerationDevice)); + public FFMpegArgumentOptions WithHardwareAcceleration(HardwareAccelerationDevice hardwareAccelerationDevice = HardwareAccelerationDevice.Auto) => WithArgument(new HardwareAccelerationArgument(hardwareAccelerationDevice)); public FFMpegArgumentOptions UsingShortest(bool shortest = true) => WithArgument(new ShortestArgument(shortest)); public FFMpegArgumentOptions UsingMultithreading(bool multithread) => WithArgument(new ThreadsArgument(multithread)); public FFMpegArgumentOptions UsingThreads(int threads) => WithArgument(new ThreadsArgument(threads)); - + public FFMpegArgumentOptions WithVideoCodec(Codec videoCodec) => WithArgument(new VideoCodecArgument(videoCodec)); public FFMpegArgumentOptions WithVideoCodec(string videoCodec) => WithArgument(new VideoCodecArgument(videoCodec)); public FFMpegArgumentOptions WithVideoBitrate(int bitrate) => WithArgument(new VideoBitrateArgument(bitrate)); @@ -52,7 +52,7 @@ public FFMpegArgumentOptions WithVideoFilters(Action videoFi public FFMpegArgumentOptions Seek(TimeSpan? seekTo) => WithArgument(new SeekArgument(seekTo)); public FFMpegArgumentOptions Loop(int times) => WithArgument(new LoopArgument(times)); public FFMpegArgumentOptions OverwriteExisting() => WithArgument(new OverwriteArgument()); - public FFMpegArgumentOptions SelectStream(int streamIndex, int inputFileIndex = 0) => WithArgument(new MapStreamArgument(inputFileIndex, streamIndex)); + public FFMpegArgumentOptions SelectStream(int streamIndex, int inputFileIndex = 0) => WithArgument(new MapStreamArgument(streamIndex, inputFileIndex)); public FFMpegArgumentOptions ForceFormat(ContainerFormat format) => WithArgument(new ForceFormatArgument(format)); public FFMpegArgumentOptions ForceFormat(string format) => WithArgument(new ForceFormatArgument(format)); From db56dda75e85d2e7e8fa2eae8d4b7bcc37fb8a5b Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Thu, 12 Aug 2021 22:37:52 +0200 Subject: [PATCH 23/23] Update nuget details Former-commit-id: 607bd1837fef2d6d2bd2cbbf09995918041035bf --- FFMpegCore/FFMpegCore.csproj | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/FFMpegCore/FFMpegCore.csproj b/FFMpegCore/FFMpegCore.csproj index daf7901..231df98 100644 --- a/FFMpegCore/FFMpegCore.csproj +++ b/FFMpegCore/FFMpegCore.csproj @@ -8,11 +8,13 @@ A .NET Standard FFMpeg/FFProbe wrapper for easily integrating media analysis and conversion into your .NET applications 4.0.0.0 README.md - - Cancellation token support (thanks patagonaa) -- Support for setting stdout and stderr encoding for ffprobe (thanks CepheiSigma) -- Improved ffprobe exceptions + - Support for PCM audio samples (thanks to Namaneo) +- Support for subtitle streams in MediaAnalysis (thanks to alex6dj) +- Support for subtitle hard burning (thanks to alex6dj) +- Additional codec* properties on MediaAnalysis object (thanks to GuyWithDogs) +- SelectStream method for mapping/specifyíng specific streams from input files (thanks to Feodoros) 8 - 4.4.0 + 4.5.0 MIT Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev ffmpeg ffprobe convert video audio mediafile resize analyze muxing