Compare commits

...

90 commits

Author SHA1 Message Date
Malte Rosenbjerg
f31dc2b1cd
Merge branch 'main' into bugfix/ensure-ffmpeg-not-found-throws-ffmpegexception 2024-12-04 20:58:27 +01:00
Malte Rosenbjerg
10bc004888
Merge pull request #546 from brett-baker/main
Add Copy option to Audio Codec.  Add Crop option to Arguments
2024-12-04 20:56:58 +01:00
Malte Rosenbjerg
d43ea98e0e
Merge branch 'main' into bugfix/ensure-ffmpeg-not-found-throws-ffmpegexception 2024-12-04 20:55:04 +01:00
Malte Rosenbjerg
0a3be1e78c
Merge pull request #510 from Hagfjall/hagfjall/fix-509-snapshot-rotation
fix: Snapshots from rotated videos should have correct width/height
2024-12-04 20:53:22 +01:00
Malte Rosenbjerg
5d654af5f8 dotnet format 2024-12-04 21:51:43 +02:00
Malte Rosenbjerg
5bddf383bb
Merge pull request #523 from BenediktBertsch/main
Feat: add av1 support for smaller snapshots and videos
2024-12-04 20:50:26 +01:00
Malte Rosenbjerg
a4b9a885d3
Merge pull request #527 from alahane-techtel/main
>24hr Duration handling added in FFMpegArgumentProcessor
2024-12-04 20:49:59 +01:00
Malte Rosenbjerg
4eb515de56
Merge pull request #542 from Kaaybi/add-video-stream-level-to-ffprobe-analysis
Add video-stream level to FFProbe analysis
2024-12-04 20:49:32 +01:00
Malte Rosenbjerg
9b6626f54b
Merge branch 'main' into hagfjall/fix-509-snapshot-rotation 2024-12-04 20:47:41 +01:00
Malte Rosenbjerg
045068c70e
Merge branch 'main' into main 2024-12-04 20:46:04 +01:00
Malte Rosenbjerg
4a6abef172
Merge branch 'main' into add-video-stream-level-to-ffprobe-analysis 2024-12-04 20:45:03 +01:00
Malte Rosenbjerg
e8ef5804e8
Merge branch 'main' into main 2024-12-04 20:44:31 +01:00
Malte Rosenbjerg
a41ec3adae
Merge branch 'main' into main 2024-12-04 20:43:12 +01:00
Malte Rosenbjerg
71981ad3a0
Merge pull request #543 from Kaaybi/bump-system-text-json-v8.0.4
Bump system.text.json
2024-12-04 20:42:55 +01:00
Malte Rosenbjerg
a3a3144aac Bump codecov/codecov-action and set CODECOV_TOKEN 2024-12-04 21:38:41 +02:00
Malte Rosenbjerg
cf8f7cc674 Call MoveNext before accessing Current 2024-12-04 21:29:37 +02:00
Malte Rosenbjerg
0cd4e076ff Bump test packages 2024-12-04 20:55:31 +02:00
Malte Rosenbjerg
d4dcf86a36 Use macos-13 runner 2024-12-04 20:51:12 +02:00
Malte Rosenbjerg
dc165f9eae Bump workflow actions 2024-12-04 20:48:59 +02:00
Malte Rosenbjerg
63fe3a6e3d Set ProduceReferenceAssembly to true 2024-12-04 20:46:29 +02:00
Malte Rosenbjerg
79ea2a9797 Bump System.Text.Json to 9.0.0 2024-12-04 20:44:05 +02:00
Malte Rosenbjerg
410e940b7e Use .NET 8 for Examples and Test 2024-12-04 20:43:47 +02:00
Malte Rosenbjerg
cf513aac0f Use .NET 8 in workflows 2024-12-04 20:43:32 +02:00
Malte Rosenbjerg
8636817fe1
Merge branch 'main' into hagfjall/fix-509-snapshot-rotation 2024-12-04 19:34:42 +01:00
Malte Rosenbjerg
b1d908971c
Merge branch 'main' into bump-system-text-json-v8.0.4 2024-12-04 19:32:13 +01:00
Malte Rosenbjerg
9942e54762
Merge branch 'main' into main 2024-12-04 19:29:59 +01:00
Malte Rosenbjerg
cf9111881d
Merge branch 'main' into add-video-stream-level-to-ffprobe-analysis 2024-12-04 19:29:31 +01:00
Malte Rosenbjerg
cefc8efe95
Merge pull request #498 from Tomiscout/main
Add HDR color properties support in FFProbe analysis
2024-12-04 19:28:52 +01:00
Brett Baker
ff42378834 null check on title to not fail Media Analysis
If the title is missing from the analysis metadata, the analysis fails.  Added null check and default value of "TitleValueNotSet".
2024-11-13 08:45:00 -05:00
Brett Baker
72eddfca6d Add CropArgument
Add the option to crop the video to a given size.
2024-11-02 07:34:21 -04:00
Brett Baker
c4232b7ca0 Added Copy option to Audio 2024-11-01 09:13:29 -04:00
Kaaybi
d5a2e5cbe6 chore: bump system.text.json to v8.0.4 2024-09-18 12:13:25 +02:00
Kaaybi
f86d999035 feat: add video-stream level to ffprobe analysis 2024-09-18 12:08:16 +02:00
Ashish
8d7d37a308 removed unnecessary tests 2024-07-01 19:26:00 +10:00
Ashish
745fe2a8cc removed unnecessary usings 2024-07-01 19:24:06 +10:00
Ashish
06b9667991 >24hr Duration handling added in FFMpegArgumentProcessor 2024-07-01 19:18:34 +10:00
Benedikt Bertsch
9007883d76
Feat: add av1 support for smaller snapshots and videos 2024-06-08 14:58:18 +02:00
Fredrik Hagfjäll
fe4b79e24c fix linting 2024-03-30 09:31:03 +01:00
Fredrik Hagfjäll
7697e2767d made test resize the output 2024-03-30 09:29:25 +01:00
Fredrik Hagfjäll
82cc9715dc fix: Snapshots from rotated videos should have correct width/height
Some video-files have their metadata as -90 instead of 90. This PR handles correct aspect-ratio and rotation when taking snapshots.

The sample video provided in the PR is my own recording. A bit too big maybe? I couldn't find any command to set the metadata to `-90` on existing video. Would be happy to get it working on already existing test-data to reduce size.

Fixes #509
2024-03-30 09:10:13 +01:00
Tomas Gužauskas
ef08fd93a9 Add HDR color properties support in FFProbe analysis
Add hdr video test
2024-01-30 00:52:25 +02:00
Malte Rosenbjerg
eb221c3e49
Merge pull request #478 from vortex852456/main
Fix: Changed "Chapter" Modell
2023-10-25 19:03:15 +02:00
Vortex
df123bb191 Fix: Changed "Chapter" Modell since "Id", "Start" and "End" returned by ffprobe can contain values larger than Int32.MaxValue 2023-10-16 03:20:31 +02:00
Malte Rosenbjerg
d90b482213
Merge pull request #473 from duggaraju/main
Add support for multiple outputs and tee muxer.
2023-10-05 12:17:06 +02:00
Malte Rosenbjerg
bc6defe535
Merge branch 'main' into main 2023-10-05 12:06:43 +02:00
Malte Rosenbjerg
ed2b95038c
Merge pull request #431 from vfrz/feature/custom-ffprob-arguments
Feature: custom ffprob arguments
2023-10-05 12:06:31 +02:00
Malte Rosenbjerg
f8407bce24 Merge branch 'main' into pr/431 2023-10-05 12:00:06 +02:00
Malte Rosenbjerg
20a5b156e9
Merge pull request #416 from phillipfisher/main
Add chapters to FFProbe
2023-10-05 11:56:58 +02:00
Malte Rosenbjerg
21c02ba2a4
Merge branch 'main' into main 2023-10-05 09:30:44 +02:00
Malte Rosenbjerg
d30629fb8e
Merge pull request #443 from devedse/main
Fix issue where ffmpeg can't be found if x64/x86 folders exist withithout ffmpeg in there
2023-10-05 09:27:36 +02:00
Malte Rosenbjerg
ed30b14687 Align with main branch 2023-10-05 09:24:44 +02:00
Malte Rosenbjerg
d2296496bf
Merge branch 'main' into main 2023-10-05 09:22:13 +02:00
Malte Rosenbjerg
4a39e234ea
Merge branch 'main' into main 2023-10-05 09:21:59 +02:00
Malte Rosenbjerg
54e5d0860c
Merge branch 'main' into feature/custom-ffprob-arguments 2023-10-05 09:21:21 +02:00
Malte Rosenbjerg
7f283e9113
Merge branch 'main' into main 2023-10-05 09:21:13 +02:00
Malte Rosenbjerg
3085fa4209
Merge pull request #477 from rosenbjerg/bugfix/fix-null-ref-exception-with-tags-container
Bugfix/fix null ref exception with tags container
2023-10-05 09:20:51 +02:00
Malte Rosenbjerg
55b1946976 Update FFProbeTests.cs 2023-10-05 09:14:47 +02:00
Malte Rosenbjerg
5fbe71bc39 Update dependencies 2023-10-05 08:58:57 +02:00
Malte Rosenbjerg
b31c8da7ae Fix tags container null ref exception 2023-10-05 08:58:50 +02:00
Prakash Duggaraju
42f9005d59 Add support for multiple outputs and tee muxer.
A single input can be encoded simultaneously to multiple oputs or muxed in multiple formats
2023-09-08 13:22:09 -07:00
Malte Rosenbjerg
4be90f025b
Merge branch 'main' into main 2023-08-24 22:53:11 +02:00
Malte Rosenbjerg
71a55530d3
Merge branch 'main' into feature/custom-ffprob-arguments 2023-08-24 22:38:52 +02:00
Malte Rosenbjerg
78ca5dd02f
Merge branch 'main' into main 2023-08-23 18:57:12 +02:00
Malte Rosenbjerg
6df9495e9f
Merge pull request #445 from rpaschoal/feature/codec-copy
Update SaveM3U8Stream method to use "-codec copy" argument
2023-05-21 11:37:03 +02:00
Rafael Carvalho
6c2311c869 Update "SaveM3U8Stream" method to use "WithCopyCodec" option 2023-05-17 11:41:13 +12:00
Rafael Carvalho
643952db7b Add "WithCopyCodec" option 2023-05-17 11:40:40 +12:00
Rafael Carvalho
69b01c91c6 Add CopyCodecArgument 2023-05-17 11:40:20 +12:00
Malte Rosenbjerg
97b9fa6db9
Merge pull request #433 from NaBian/patch-1
fix: readme minor mistakes.
2023-05-06 09:43:35 +02:00
Devedse
a920ab2bc1 Fix issue where ffmpeg can't be found if x64/x86 folders exist without ffmpeg in there 2023-05-05 12:42:27 +02:00
NaBian
d9c4595eec
fix: readme minor mistakes. 2023-04-21 11:07:56 +08:00
vfrz
bf06a8059b Add test again 2023-04-12 22:14:23 +02:00
vfrz
338274b500 Try fix encoding 2 2023-04-12 22:10:37 +02:00
vfrz
66c166f2e8 Try fix encoding 2023-04-12 22:07:22 +02:00
vfrz
4b0cd9239e Add test 2023-04-12 21:57:20 +02:00
vfrz
b2488303cf feature: custom ffprobe arguments 2023-04-12 21:47:36 +02:00
Malte Rosenbjerg
0ac351493c Verify Chapters property is initialized to non-null in test 2023-04-12 16:31:26 +02:00
Malte Rosenbjerg
ba78512cb9 ChapterData.Duration as computed property 2023-04-12 16:30:46 +02:00
Malte Rosenbjerg
943662aa15 Bump nuget version 2023-03-15 13:26:11 +01:00
Malte Rosenbjerg
4cf777882f
Merge pull request #419 from rpaschoal/feature/output-gif
Add a helper method to generate GIF Snapshots
2023-03-07 10:34:17 +01:00
Rafael Carvalho
3dc2fff0ac Fix whitespace formatting linting error 2023-03-07 17:04:23 +13:00
Rafael Carvalho
7569669f52 Fix Palette typos 2023-03-07 16:55:11 +13:00
Rafael Carvalho
62a9ed6281 Update README to add information about GIF Snapshots 2023-03-07 16:49:18 +13:00
Rafael Carvalho
c218b3592b Add unit tests 2023-03-07 16:44:33 +13:00
Rafael Carvalho
a90918eac6 Add "GifSnapshot" and "GifSnapshotAsync" methods 2023-03-07 16:41:52 +13:00
Rafael Carvalho
d14ef2268f Add "BuildGifSnapshotArguments" method 2023-03-07 16:31:45 +13:00
Rafael Carvalho
19c177a248 Receive stream index as an argument for generating GIFs 2023-03-07 16:19:03 +13:00
Rafael Carvalho
b7fd9890da Add GifPallet argument builder 2023-03-07 16:07:30 +13:00
Rafael Carvalho
4dbbf345d4 Add "GifPalettArgument" for outputting GIFs 2023-03-06 17:25:08 +13:00
Rafael Carvalho
718371505c Add .gif file extension 2023-03-06 17:22:08 +13:00
Phillip Fisher
38789162eb Add chapters to FFProbe 2023-02-25 11:29:41 -06:00
37 changed files with 599 additions and 77 deletions

View file

@ -22,23 +22,23 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [windows-latest, ubuntu-latest, macos-latest]
os: [windows-latest, ubuntu-latest, macos-13]
timeout-minutes: 7
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Prepare .NET
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
with:
dotnet-version: '7.0.x'
dotnet-version: '8.0.x'
- name: Lint with dotnet
run: dotnet format FFMpegCore.sln --severity warn --verify-no-changes
- name: Prepare FFMpeg
uses: FedericoCarboni/setup-ffmpeg@v2
uses: FedericoCarboni/setup-ffmpeg@v3
with:
token: ${{ secrets.GITHUB_TOKEN }}
@ -47,7 +47,8 @@ jobs:
- if: matrix.os == 'windows-latest'
name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v5
with:
directory: FFMpegCore.Test/TestResults
fail_ci_if_error: true
directory: FFMpegCore.Test/TestResults
token: ${{ secrets.CODECOV_TOKEN }}

View file

@ -8,12 +8,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Prepare .NET
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
with:
dotnet-version: '7.0.x'
dotnet-version: '8.0.x'
- name: Build solution
run: dotnet pack FFMpegCore.sln -c Release

View file

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>

View file

@ -9,11 +9,12 @@
</PackageReleaseNotes>
<PackageTags>ffmpeg ffprobe convert video audio mediafile resize analyze muxing skiasharp</PackageTags>
<Authors>Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev, Dimitri Vranken</Authors>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SkiaSharp" Version="2.88.3" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="2.88.3" />
<PackageReference Include="SkiaSharp" Version="2.88.6" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="2.88.6" />
</ItemGroup>
<ItemGroup>

View file

@ -9,6 +9,7 @@
</PackageReleaseNotes>
<PackageTags>ffmpeg ffprobe convert video audio mediafile resize analyze muxing</PackageTags>
<Authors>Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev</Authors>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
</PropertyGroup>
<ItemGroup>

View file

@ -1,4 +1,5 @@
using FFMpegCore.Arguments;
using System.Drawing;
using FFMpegCore.Arguments;
using FFMpegCore.Enums;
using Microsoft.VisualStudio.TestTools.UnitTesting;
@ -537,5 +538,78 @@ public void Builder_BuildString_PadFilter_Alt()
"-i \"input.mp4\" -vf \"pad=aspect=4/3:x=(ow-iw)/2:y=(oh-ih)/2:color=violet:eval=frame\" \"output.mp4\"",
str);
}
[TestMethod]
public void Builder_BuildString_GifPalette()
{
var streamIndex = 0;
var size = new Size(640, 480);
var str = FFMpegArguments
.FromFileInput("input.mp4")
.OutputToFile("output.gif", false, opt => opt
.WithGifPaletteArgument(streamIndex, size))
.Arguments;
Assert.AreEqual($"""
-i "input.mp4" -filter_complex "[0:v] fps=12,scale=w={size.Width}:h={size.Height},split [a][b];[a] palettegen=max_colors=32 [p];[b][p] paletteuse=dither=bayer" "output.gif"
""", str);
}
[TestMethod]
public void Builder_BuildString_GifPalette_NullSize_FpsSupplied()
{
var streamIndex = 1;
var str = FFMpegArguments
.FromFileInput("input.mp4")
.OutputToFile("output.gif", false, opt => opt
.WithGifPaletteArgument(streamIndex, null, 10))
.Arguments;
Assert.AreEqual($"""
-i "input.mp4" -filter_complex "[{streamIndex}:v] fps=10,split [a][b];[a] palettegen=max_colors=32 [p];[b][p] paletteuse=dither=bayer" "output.gif"
""", str);
}
[TestMethod]
public void Builder_BuildString_MultiOutput()
{
var str = FFMpegArguments.FromFileInput("input.mp4")
.MultiOutput(args => args
.OutputToFile("output.mp4", overwrite: true, args => args.CopyChannel())
.OutputToFile("output.ts", overwrite: false, args => args.CopyChannel().ForceFormat("mpegts"))
.OutputToUrl("http://server/path", options => options.ForceFormat("webm")))
.Arguments;
Assert.AreEqual($"""
-i "input.mp4" -c:a copy -c:v copy "output.mp4" -y -c:a copy -c:v copy -f mpegts "output.ts" -f webm http://server/path
""", str);
}
[TestMethod]
public void Builder_BuildString_MBROutput()
{
var str = FFMpegArguments.FromFileInput("input.mp4")
.MultiOutput(args => args
.OutputToFile("sd.mp4", overwrite: true, args => args.Resize(1200, 720))
.OutputToFile("hd.mp4", overwrite: false, args => args.Resize(1920, 1080)))
.Arguments;
Assert.AreEqual($"""
-i "input.mp4" -s 1200x720 "sd.mp4" -y -s 1920x1080 "hd.mp4"
""", str);
}
[TestMethod]
public void Builder_BuildString_TeeOutput()
{
var str = FFMpegArguments.FromFileInput("input.mp4")
.OutputToTee(args => args
.OutputToFile("output.mp4", overwrite: false, args => args.WithFastStart())
.OutputToUrl("http://server/path", options => options.ForceFormat("mpegts").SelectStream(0, channel: Channel.Video)))
.Arguments;
Assert.AreEqual($"""
-i "input.mp4" -f tee "[movflags=faststart]output.mp4|[f=mpegts:select=\'0:v:0\']http://server/path"
""", str);
}
}
}

View file

@ -1,26 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
<Nullable>disable</Nullable>
<LangVersion>default</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="3.2.0">
<PackageReference Include="coverlet.collector" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" Version="6.10.0" />
<PackageReference Include="GitHubActionsTestLogger" Version="2.0.1">
<PackageReference Include="FluentAssertions" Version="7.0.0" />
<PackageReference Include="GitHubActionsTestLogger" Version="2.4.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.0.2" />
<PackageReference Include="MSTest.TestFramework" Version="3.0.2" />
<PackageReference Include="SkiaSharp" Version="2.88.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.6.4" />
<PackageReference Include="MSTest.TestFramework" Version="3.6.4" />
<PackageReference Include="SkiaSharp" Version="2.88.6" />
</ItemGroup>
<ItemGroup>
@ -51,9 +51,15 @@
<None Update="Resources\input_3sec_rotation_90deg.mp4">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Resources\input_3sec_rotation_negative_90deg.mp4">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Resources\input_audio_only_10sec.mp4">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Resources\input_hdr.mov">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Resources\input_video_only_3sec.mp4">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>

View file

@ -46,7 +46,7 @@ public async Task PacketAnalysis_Async()
var packets = packetAnalysis.Packets;
Assert.AreEqual(96, packets.Count);
Assert.IsTrue(packets.All(f => f.CodecType == "video"));
Assert.AreEqual("K_", packets[0].Flags);
Assert.IsTrue(packets[0].Flags.StartsWith("K_"));
Assert.AreEqual(1362, packets.Last().Size);
}
@ -57,7 +57,7 @@ public void PacketAnalysis_Sync()
Assert.AreEqual(96, packets.Count);
Assert.IsTrue(packets.All(f => f.CodecType == "video"));
Assert.AreEqual("K_", packets[0].Flags);
Assert.IsTrue(packets[0].Flags.StartsWith("K_"));
Assert.AreEqual(1362, packets.Last().Size);
}
@ -70,7 +70,7 @@ public void PacketAnalysisAudioVideo_Sync()
var actual = packets.Select(f => f.CodecType).Distinct().ToList();
var expected = new List<string> { "audio", "video" };
CollectionAssert.AreEquivalent(expected, actual);
Assert.IsTrue(packets.Where(t => t.CodecType == "audio").All(f => f.Flags == "K_"));
Assert.IsTrue(packets.Where(t => t.CodecType == "audio").All(f => f.Flags.StartsWith("K_")));
Assert.AreEqual(75, packets.Count(t => t.CodecType == "video"));
Assert.AreEqual(141, packets.Count(t => t.CodecType == "audio"));
}
@ -105,6 +105,7 @@ public void Probe_Success()
{
var info = FFProbe.Analyse(TestResources.Mp4Video);
Assert.AreEqual(3, info.Duration.Seconds);
Assert.AreEqual(0, info.Chapters.Count);
Assert.AreEqual("5.1", info.PrimaryAudioStream!.ChannelLayout);
Assert.AreEqual(6, info.PrimaryAudioStream.Channels);
@ -122,6 +123,7 @@ public void Probe_Success()
Assert.AreEqual(1, info.PrimaryVideoStream.SampleAspectRatio.Width);
Assert.AreEqual(1, info.PrimaryVideoStream.SampleAspectRatio.Height);
Assert.AreEqual("yuv420p", info.PrimaryVideoStream.PixelFormat);
Assert.AreEqual(31, info.PrimaryVideoStream.Level);
Assert.AreEqual(1280, info.PrimaryVideoStream.Width);
Assert.AreEqual(720, info.PrimaryVideoStream.Height);
Assert.AreEqual(25, info.PrimaryVideoStream.AvgFrameRate);
@ -144,6 +146,13 @@ public void Probe_Rotation()
Assert.AreEqual(90, info.PrimaryVideoStream.Rotation);
}
[TestMethod]
public void Probe_Rotation_Negative_Value()
{
var info = FFProbe.Analyse(TestResources.Mp4VideoRotationNegative);
Assert.AreEqual(-90, info.PrimaryVideoStream.Rotation);
}
[TestMethod, Timeout(10000)]
public async Task Probe_Async_Success()
{
@ -172,6 +181,18 @@ public async Task Probe_Success_FromStream_Async()
Assert.AreEqual(3, info.Duration.Seconds);
}
[TestMethod, Timeout(10000)]
public void Probe_HDR()
{
var info = FFProbe.Analyse(TestResources.HdrVideo);
Assert.IsNotNull(info.PrimaryVideoStream);
Assert.AreEqual("tv", info.PrimaryVideoStream.ColorRange);
Assert.AreEqual("bt2020nc", info.PrimaryVideoStream.ColorSpace);
Assert.AreEqual("arib-std-b67", info.PrimaryVideoStream.ColorTransfer);
Assert.AreEqual("bt2020", info.PrimaryVideoStream.ColorPrimaries);
}
[TestMethod, Timeout(10000)]
public async Task Probe_Success_Subtitle_Async()
{
@ -235,5 +256,12 @@ public async Task Probe_Success_32BitWavBitDepth_Async()
Assert.IsNotNull(info.PrimaryAudioStream);
Assert.AreEqual(32, info.PrimaryAudioStream.BitDepth);
}
[TestMethod]
public void Probe_Success_Custom_Arguments()
{
var info = FFProbe.Analyse(TestResources.Mp4Video, customArguments: "-headers \"Hello: World\"");
Assert.AreEqual(3, info.Duration.Seconds);
}
}
}

View file

@ -4,7 +4,9 @@ public static class TestResources
{
public static readonly string Mp4Video = "./Resources/input_3sec.mp4";
public static readonly string Mp4VideoRotation = "./Resources/input_3sec_rotation_90deg.mp4";
public static readonly string Mp4VideoRotationNegative = "./Resources/input_3sec_rotation_negative_90deg.mp4";
public static readonly string WebmVideo = "./Resources/input_3sec.webm";
public static readonly string HdrVideo = "./Resources/input_hdr.mov";
public static readonly string Mp4WithoutVideo = "./Resources/input_audio_only_10sec.mp4";
public static readonly string Mp4WithoutAudio = "./Resources/input_video_only_3sec.mp4";
public static readonly string RawAudio = "./Resources/audio.raw";

Binary file not shown.

View file

@ -1,4 +1,5 @@
using System.Drawing.Imaging;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.Versioning;
using System.Text;
using FFMpegCore.Arguments;
@ -479,6 +480,79 @@ public void Video_Snapshot_PersistSnapshot()
Assert.AreEqual("png", analysis.PrimaryVideoStream!.CodecName);
}
[TestMethod, Timeout(BaseTimeoutMilliseconds)]
public void Video_Snapshot_Rotated_PersistSnapshot()
{
using var outputPath = new TemporaryFile("out.png");
var size = new Size(360, 0); // half the size of original video, keeping height 0 for keeping aspect ratio
FFMpeg.Snapshot(TestResources.Mp4VideoRotationNegative, outputPath, size);
var analysis = FFProbe.Analyse(outputPath);
Assert.AreEqual(size.Width, analysis.PrimaryVideoStream!.Width);
Assert.AreEqual(1280 / 2, analysis.PrimaryVideoStream!.Height);
Assert.AreEqual(0, analysis.PrimaryVideoStream!.Rotation);
Assert.AreEqual("png", analysis.PrimaryVideoStream!.CodecName);
}
[TestMethod, Timeout(BaseTimeoutMilliseconds)]
public void Video_GifSnapshot_PersistSnapshot()
{
using var outputPath = new TemporaryFile("out.gif");
var input = FFProbe.Analyse(TestResources.Mp4Video);
FFMpeg.GifSnapshot(TestResources.Mp4Video, outputPath, captureTime: TimeSpan.FromSeconds(0));
var analysis = FFProbe.Analyse(outputPath);
Assert.AreNotEqual(input.PrimaryVideoStream!.Width, analysis.PrimaryVideoStream!.Width);
Assert.AreNotEqual(input.PrimaryVideoStream.Height, analysis.PrimaryVideoStream!.Height);
Assert.AreEqual("gif", analysis.PrimaryVideoStream!.CodecName);
}
[TestMethod, Timeout(BaseTimeoutMilliseconds)]
public void Video_GifSnapshot_PersistSnapshot_SizeSupplied()
{
using var outputPath = new TemporaryFile("out.gif");
var input = FFProbe.Analyse(TestResources.Mp4Video);
var desiredGifSize = new Size(320, 240);
FFMpeg.GifSnapshot(TestResources.Mp4Video, outputPath, desiredGifSize, captureTime: TimeSpan.FromSeconds(0));
var analysis = FFProbe.Analyse(outputPath);
Assert.AreNotEqual(input.PrimaryVideoStream!.Width, desiredGifSize.Width);
Assert.AreNotEqual(input.PrimaryVideoStream.Height, desiredGifSize.Height);
Assert.AreEqual("gif", analysis.PrimaryVideoStream!.CodecName);
}
[TestMethod, Timeout(BaseTimeoutMilliseconds)]
public async Task Video_GifSnapshot_PersistSnapshotAsync()
{
using var outputPath = new TemporaryFile("out.gif");
var input = FFProbe.Analyse(TestResources.Mp4Video);
await FFMpeg.GifSnapshotAsync(TestResources.Mp4Video, outputPath, captureTime: TimeSpan.FromSeconds(0));
var analysis = FFProbe.Analyse(outputPath);
Assert.AreNotEqual(input.PrimaryVideoStream!.Width, analysis.PrimaryVideoStream!.Width);
Assert.AreNotEqual(input.PrimaryVideoStream.Height, analysis.PrimaryVideoStream!.Height);
Assert.AreEqual("gif", analysis.PrimaryVideoStream!.CodecName);
}
[TestMethod, Timeout(BaseTimeoutMilliseconds)]
public async Task Video_GifSnapshot_PersistSnapshotAsync_SizeSupplied()
{
using var outputPath = new TemporaryFile("out.gif");
var input = FFProbe.Analyse(TestResources.Mp4Video);
var desiredGifSize = new Size(320, 240);
await FFMpeg.GifSnapshotAsync(TestResources.Mp4Video, outputPath, desiredGifSize, captureTime: TimeSpan.FromSeconds(0));
var analysis = FFProbe.Analyse(outputPath);
Assert.AreNotEqual(input.PrimaryVideoStream!.Width, desiredGifSize.Width);
Assert.AreNotEqual(input.PrimaryVideoStream.Height, desiredGifSize.Height);
Assert.AreEqual("gif", analysis.PrimaryVideoStream!.CodecName);
}
[TestMethod, Timeout(BaseTimeoutMilliseconds)]
public void Video_Join()
{

View file

@ -0,0 +1,10 @@
namespace FFMpegCore.Arguments
{
/// <summary>
/// Represents a copy codec parameter
/// </summary>
public class CopyCodecArgument : IArgument
{
public string Text => $"-codec copy";
}
}

View file

@ -0,0 +1,22 @@
using System.Drawing;
namespace FFMpegCore.Arguments
{
public class CropArgument : IArgument
{
public readonly Size? Size;
public readonly int Top;
public readonly int Left;
public CropArgument(Size? size, int top, int left)
{
Size = size;
Top = top;
Left = left;
}
public CropArgument(int width, int height, int top, int left) : this(new Size(width, height), top, left) { }
public string Text => Size == null ? string.Empty : $"-vf crop={Size.Value.Width}:{Size.Value.Height}:{Left}:{Top}";
}
}

View file

@ -0,0 +1,24 @@
using System.Drawing;
namespace FFMpegCore.Arguments
{
public class GifPaletteArgument : IArgument
{
private readonly int _streamIndex;
private readonly int _fps;
private readonly Size? _size;
public GifPaletteArgument(int streamIndex, int fps, Size? size)
{
_streamIndex = streamIndex;
_fps = fps;
_size = size;
}
private string ScaleText => _size.HasValue ? $"scale=w={_size.Value.Width}:h={_size.Value.Height}," : string.Empty;
public string Text => $"-filter_complex \"[{_streamIndex}:v] fps={_fps},{ScaleText}split [a][b];[a] palettegen=max_colors=32 [p];[b][p] paletteuse=dither=bayer\"";
}
}

View file

@ -0,0 +1,57 @@

namespace FFMpegCore.Arguments
{
internal class OutputTeeArgument : IOutputArgument
{
private readonly FFMpegMultiOutputOptions _options;
public OutputTeeArgument(FFMpegMultiOutputOptions options)
{
if (options.Outputs.Count == 0)
{
throw new ArgumentException("Atleast one output must be specified.", nameof(options));
}
_options = options;
}
public string Text => $"-f tee \"{string.Join("|", _options.Outputs.Select(MapOptions))}\"";
public Task During(CancellationToken cancellationToken = default) => Task.CompletedTask;
public void Post()
{
}
public void Pre()
{
}
private static string MapOptions(FFMpegArgumentOptions option)
{
var optionPrefix = string.Empty;
if (option.Arguments.Count > 1)
{
var options = option.Arguments.Take(option.Arguments.Count - 1);
optionPrefix = $"[{string.Join(":", options.Select(MapArgument))}]";
}
var output = option.Arguments.OfType<IOutputArgument>().Single();
return $"{optionPrefix}{output.Text.Trim('"')}";
}
private static string MapArgument(IArgument argument)
{
if (argument is MapStreamArgument map)
{
return map.Text.Replace("-map ", "select=\\'") + "\\'";
}
else if (argument is BitStreamFilterArgument bitstreamFilter)
{
return bitstreamFilter.Text.Replace("-bsf:", "bsfs/").Replace(' ', '=');
}
return argument.Text.TrimStart('-').Replace(' ', '=');
}
}
}

View file

@ -6,6 +6,8 @@ public class ChapterData
public TimeSpan Start { get; private set; }
public TimeSpan End { get; private set; }
public TimeSpan Duration => End - Start;
public ChapterData(string title, TimeSpan start, TimeSpan end)
{
Title = title;

View file

@ -17,6 +17,7 @@ public static class VideoCodec
public static Codec LibTheora => FFMpeg.GetCodec("libtheora");
public static Codec Png => FFMpeg.GetCodec("png");
public static Codec MpegTs => FFMpeg.GetCodec("mpegts");
public static Codec LibaomAv1 => FFMpeg.GetCodec("libaom-av1");
}
public static class AudioCodec
@ -27,6 +28,8 @@ public static class AudioCodec
public static Codec Ac3 => FFMpeg.GetCodec("ac3");
public static Codec Eac3 => FFMpeg.GetCodec("eac3");
public static Codec LibMp3Lame => FFMpeg.GetCodec("libmp3lame");
public static Codec Copy => new Codec("copy", CodecType.Audio);
}
public static class VideoType

View file

@ -20,5 +20,6 @@ public static string Extension(this Codec type)
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";
}
}

View file

@ -57,6 +57,36 @@ public static async Task<bool> SnapshotAsync(string input, string output, Size?
.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);
}
var source = FFProbe.Analyse(input);
var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildGifSnapshotArguments(input, source, size, captureTime, duration, streamIndex);
return arguments
.OutputToFile(output, true, outputOptions)
.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 source = await FFProbe.AnalyseAsync(input).ConfigureAwait(false);
var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildGifSnapshotArguments(input, source, size, captureTime, duration, streamIndex);
return await arguments
.OutputToFile(output, true, outputOptions)
.ProcessAsynchronously();
}
/// <summary>
/// Converts an image sequence to a video.
/// </summary>
@ -303,7 +333,10 @@ public static bool SaveM3U8Stream(Uri uri, string output)
}
return FFMpegArguments
.FromUrlInput(uri)
.FromUrlInput(uri, options =>
{
options.WithCopyCodec();
})
.OutputToFile(output)
.ProcessSynchronously();
}

View file

@ -16,7 +16,10 @@ 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 Crop(Size? size, int left, int top) => WithArgument(new CropArgument(size, top, left));
public FFMpegArgumentOptions Crop(int width, int height, int left, int top) => WithArgument(new CropArgument(new Size(width, height), top, left));
public FFMpegArgumentOptions Crop(Size? size) => WithArgument(new CropArgument(size, 0, 0));
public FFMpegArgumentOptions Crop(int width, int height) => WithArgument(new CropArgument(new Size(width, height), 0, 0));
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));
@ -76,6 +79,8 @@ public FFMpegArgumentOptions DeselectStreams(IEnumerable<int> streamIndices, int
public FFMpegArgumentOptions WithAudibleEncryptionKeys(string key, string iv) => WithArgument(new AudibleEncryptionKeyArgument(key, iv));
public FFMpegArgumentOptions WithAudibleActivationBytes(string activationBytes) => WithArgument(new AudibleEncryptionKeyArgument(activationBytes));
public FFMpegArgumentOptions WithTagVersion(int id3v2Version = 3) => WithArgument(new ID3V2VersionArgument(id3v2Version));
public FFMpegArgumentOptions WithGifPaletteArgument(int streamIndex, Size? size, int fps = 12) => WithArgument(new GifPaletteArgument(streamIndex, fps, size));
public FFMpegArgumentOptions WithCopyCodec() => WithArgument(new CopyCodecArgument());
public FFMpegArgumentOptions WithArgument(IArgument argument)
{

View file

@ -1,5 +1,4 @@
using System.Diagnostics;
using System.Globalization;
using System.Text.RegularExpressions;
using FFMpegCore.Enums;
using FFMpegCore.Exceptions;
@ -263,7 +262,7 @@ private void ErrorData(object sender, string msg)
return;
}
var processed = TimeSpan.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture);
var processed = MediaAnalysisUtils.ParseDuration(match.Groups[1].Value);
_onTimeProgress?.Invoke(processed);
if (_onPercentageProgress == null || _totalTimespan == null)

View file

@ -71,6 +71,21 @@ private FFMpegArgumentProcessor ToProcessor(IOutputArgument argument, Action<FFM
return new FFMpegArgumentProcessor(this);
}
public FFMpegArgumentProcessor OutputToTee(Action<FFMpegMultiOutputOptions> addOutputs, Action<FFMpegArgumentOptions>? addArguments = null)
{
var outputs = new FFMpegMultiOutputOptions();
addOutputs(outputs);
return ToProcessor(new OutputTeeArgument(outputs), addArguments);
}
public FFMpegArgumentProcessor MultiOutput(Action<FFMpegMultiOutputOptions> addOutputs)
{
var args = new FFMpegMultiOutputOptions();
addOutputs(args);
Arguments.AddRange(args.Arguments);
return new FFMpegArgumentProcessor(this);
}
internal void Pre()
{
foreach (var argument in Arguments.OfType<IInputOutputArgument>())

View file

@ -0,0 +1,29 @@
using FFMpegCore.Arguments;
using FFMpegCore.Pipes;
namespace FFMpegCore
{
public class FFMpegMultiOutputOptions
{
internal readonly List<FFMpegArgumentOptions> Outputs = new();
public IEnumerable<IArgument> Arguments => Outputs.SelectMany(o => o.Arguments);
public FFMpegMultiOutputOptions OutputToFile(string file, bool overwrite = true, Action<FFMpegArgumentOptions>? addArguments = null) => AddOutput(new OutputArgument(file, overwrite), addArguments);
public FFMpegMultiOutputOptions OutputToUrl(string uri, Action<FFMpegArgumentOptions>? addArguments = null) => AddOutput(new OutputUrlArgument(uri), addArguments);
public FFMpegMultiOutputOptions OutputToUrl(Uri uri, Action<FFMpegArgumentOptions>? addArguments = null) => AddOutput(new OutputUrlArgument(uri.ToString()), addArguments);
public FFMpegMultiOutputOptions OutputToPipe(IPipeSink reader, Action<FFMpegArgumentOptions>? addArguments = null) => AddOutput(new OutputPipeArgument(reader), addArguments);
public FFMpegMultiOutputOptions AddOutput(IOutputArgument argument, Action<FFMpegArgumentOptions>? addArguments)
{
var args = new FFMpegArgumentOptions();
addArguments?.Invoke(args);
args.Arguments.Add(argument);
Outputs.Add(args);
return this;
}
}
}

View file

@ -27,7 +27,7 @@ public string GetStreamArguments()
public async Task WriteAsync(Stream outputStream, CancellationToken cancellationToken)
{
if (_sampleEnumerator.Current != null)
if (_sampleEnumerator.MoveNext() && _sampleEnumerator.Current != null)
{
await _sampleEnumerator.Current.SerializeAsync(outputStream, cancellationToken).ConfigureAwait(false);
}

View file

@ -31,6 +31,31 @@ public static (FFMpegArguments, Action<FFMpegArgumentOptions> outputOptions) Bui
.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)
{
var defaultGifOutputSize = new Size(480, -1);
captureTime ??= TimeSpan.FromSeconds(source.Duration.TotalSeconds / 3);
size = PrepareSnapshotSize(source, size) ?? defaultGifOutputSize;
streamIndex ??= source.PrimaryVideoStream?.Index
?? source.VideoStreams.FirstOrDefault()?.Index
?? 0;
return (FFMpegArguments
.FromFileInput(input, false, options => options
.Seek(captureTime)
.WithDuration(duration)),
options => options
.WithGifPaletteArgument((int)streamIndex, size, fps));
}
private static Size? PrepareSnapshotSize(IMediaAnalysis source, Size? wantedSize)
{
if (wantedSize == null || (wantedSize.Value.Height <= 0 && wantedSize.Value.Width <= 0) || source.PrimaryVideoStream == null)
@ -39,7 +64,7 @@ public static (FFMpegArguments, Action<FFMpegArgumentOptions> outputOptions) Bui
}
var currentSize = new Size(source.PrimaryVideoStream.Width, source.PrimaryVideoStream.Height);
if (source.PrimaryVideoStream.Rotation == 90 || source.PrimaryVideoStream.Rotation == 180)
if (IsRotated(source.PrimaryVideoStream.Rotation))
{
currentSize = new Size(source.PrimaryVideoStream.Height, source.PrimaryVideoStream.Width);
}
@ -63,4 +88,10 @@ public static (FFMpegArguments, Action<FFMpegArgumentOptions> outputOptions) Bui
return null;
}
private static bool IsRotated(int rotation)
{
var absRotation = Math.Abs(rotation);
return absRotation == 90 || absRotation == 180;
}
}

View file

@ -3,7 +3,7 @@
<PropertyGroup>
<IsPackable>true</IsPackable>
<Description>A .NET Standard FFMpeg/FFProbe wrapper for easily integrating media analysis and conversion into your .NET applications</Description>
<PackageVersion>5.0.2</PackageVersion>
<PackageVersion>5.1.0</PackageVersion>
<PackageOutputPath>../nupkg</PackageOutputPath>
<PackageReleaseNotes>
</PackageReleaseNotes>
@ -18,7 +18,7 @@
<ItemGroup>
<PackageReference Include="Instances" Version="3.0.0" />
<PackageReference Include="System.Text.Json" Version="7.0.2" />
<PackageReference Include="System.Text.Json" Version="9.0.0" />
</ItemGroup>
</Project>

View file

@ -1,4 +1,5 @@
using System.Text;
using System.Text.Json.Serialization;
using FFMpegCore.Enums;
namespace FFMpegCore
@ -20,10 +21,20 @@ public class FFOptions : ICloneable
/// </summary>
public string TemporaryFilesFolder { get; set; } = Path.GetTempPath();
/// <summary>
/// Encoding web name used to persist encoding <see cref="Encoding"/>
/// </summary>
public string EncodingWebName { get; set; } = Encoding.Default.WebName;
/// <summary>
/// Encoding used for parsing stdout/stderr on ffmpeg and ffprobe processes
/// </summary>
public Encoding Encoding { get; set; } = Encoding.Default;
[JsonIgnore]
public Encoding Encoding
{
get => Encoding.GetEncoding(EncodingWebName);
set => EncodingWebName = value?.WebName ?? Encoding.Default.WebName;
}
/// <summary>
/// The log level to use when calling of the ffmpeg executable.

View file

@ -10,52 +10,52 @@ namespace FFMpegCore
{
public static class FFProbe
{
public static IMediaAnalysis Analyse(string filePath, FFOptions? ffOptions = null)
public static IMediaAnalysis Analyse(string filePath, FFOptions? ffOptions = null, string? customArguments = null)
{
ThrowIfInputFileDoesNotExist(filePath);
var processArguments = PrepareStreamAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current);
var processArguments = PrepareStreamAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current, customArguments);
var result = processArguments.StartAndWaitForExit();
ThrowIfExitCodeNotZero(result);
return ParseOutput(result);
}
public static FFProbeFrames GetFrames(string filePath, FFOptions? ffOptions = null)
public static FFProbeFrames GetFrames(string filePath, FFOptions? ffOptions = null, string? customArguments = null)
{
ThrowIfInputFileDoesNotExist(filePath);
var instance = PrepareFrameAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current);
var instance = PrepareFrameAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current, customArguments);
var result = instance.StartAndWaitForExit();
ThrowIfExitCodeNotZero(result);
return ParseFramesOutput(result);
}
public static FFProbePackets GetPackets(string filePath, FFOptions? ffOptions = null)
public static FFProbePackets GetPackets(string filePath, FFOptions? ffOptions = null, string? customArguments = null)
{
ThrowIfInputFileDoesNotExist(filePath);
var instance = PreparePacketAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current);
var instance = PreparePacketAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current, customArguments);
var result = instance.StartAndWaitForExit();
ThrowIfExitCodeNotZero(result);
return ParsePacketsOutput(result);
}
public static IMediaAnalysis Analyse(Uri uri, FFOptions? ffOptions = null)
public static IMediaAnalysis Analyse(Uri uri, FFOptions? ffOptions = null, string? customArguments = null)
{
var instance = PrepareStreamAnalysisInstance(uri.AbsoluteUri, ffOptions ?? GlobalFFOptions.Current);
var instance = PrepareStreamAnalysisInstance(uri.AbsoluteUri, ffOptions ?? GlobalFFOptions.Current, customArguments);
var result = instance.StartAndWaitForExit();
ThrowIfExitCodeNotZero(result);
return ParseOutput(result);
}
public static IMediaAnalysis Analyse(Stream stream, FFOptions? ffOptions = null)
public static IMediaAnalysis Analyse(Stream stream, FFOptions? ffOptions = null, string? customArguments = null)
{
var streamPipeSource = new StreamPipeSource(stream);
var pipeArgument = new InputPipeArgument(streamPipeSource);
var instance = PrepareStreamAnalysisInstance(pipeArgument.PipePath, ffOptions ?? GlobalFFOptions.Current);
var instance = PrepareStreamAnalysisInstance(pipeArgument.PipePath, ffOptions ?? GlobalFFOptions.Current, customArguments);
pipeArgument.Pre();
var task = instance.StartAndWaitForExitAsync();
@ -75,57 +75,57 @@ public static IMediaAnalysis Analyse(Stream stream, FFOptions? ffOptions = null)
return ParseOutput(result);
}
public static async Task<IMediaAnalysis> AnalyseAsync(string filePath, FFOptions? ffOptions = null, CancellationToken cancellationToken = default)
public static async Task<IMediaAnalysis> AnalyseAsync(string filePath, FFOptions? ffOptions = null, CancellationToken cancellationToken = default, string? customArguments = null)
{
ThrowIfInputFileDoesNotExist(filePath);
var instance = PrepareStreamAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current);
var instance = PrepareStreamAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current, customArguments);
var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false);
ThrowIfExitCodeNotZero(result);
return ParseOutput(result);
}
public static FFProbeFrames GetFrames(Uri uri, FFOptions? ffOptions = null)
public static FFProbeFrames GetFrames(Uri uri, FFOptions? ffOptions = null, string? customArguments = null)
{
var instance = PrepareFrameAnalysisInstance(uri.AbsoluteUri, ffOptions ?? GlobalFFOptions.Current);
var instance = PrepareFrameAnalysisInstance(uri.AbsoluteUri, ffOptions ?? GlobalFFOptions.Current, customArguments);
var result = instance.StartAndWaitForExit();
ThrowIfExitCodeNotZero(result);
return ParseFramesOutput(result);
}
public static async Task<FFProbeFrames> GetFramesAsync(string filePath, FFOptions? ffOptions = null, CancellationToken cancellationToken = default)
public static async Task<FFProbeFrames> GetFramesAsync(string filePath, FFOptions? ffOptions = null, CancellationToken cancellationToken = default, string? customArguments = null)
{
ThrowIfInputFileDoesNotExist(filePath);
var instance = PrepareFrameAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current);
var instance = PrepareFrameAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current, customArguments);
var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false);
return ParseFramesOutput(result);
}
public static async Task<FFProbePackets> GetPacketsAsync(string filePath, FFOptions? ffOptions = null, CancellationToken cancellationToken = default)
public static async Task<FFProbePackets> GetPacketsAsync(string filePath, FFOptions? ffOptions = null, CancellationToken cancellationToken = default, string? customArguments = null)
{
ThrowIfInputFileDoesNotExist(filePath);
var instance = PreparePacketAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current);
var instance = PreparePacketAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current, customArguments);
var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false);
return ParsePacketsOutput(result);
}
public static async Task<IMediaAnalysis> AnalyseAsync(Uri uri, FFOptions? ffOptions = null, CancellationToken cancellationToken = default)
public static async Task<IMediaAnalysis> AnalyseAsync(Uri uri, FFOptions? ffOptions = null, CancellationToken cancellationToken = default, string? customArguments = null)
{
var instance = PrepareStreamAnalysisInstance(uri.AbsoluteUri, ffOptions ?? GlobalFFOptions.Current);
var instance = PrepareStreamAnalysisInstance(uri.AbsoluteUri, ffOptions ?? GlobalFFOptions.Current, customArguments);
var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false);
ThrowIfExitCodeNotZero(result);
return ParseOutput(result);
}
public static async Task<IMediaAnalysis> AnalyseAsync(Stream stream, FFOptions? ffOptions = null, CancellationToken cancellationToken = default)
public static async Task<IMediaAnalysis> AnalyseAsync(Stream stream, FFOptions? ffOptions = null, CancellationToken cancellationToken = default, string? customArguments = null)
{
var streamPipeSource = new StreamPipeSource(stream);
var pipeArgument = new InputPipeArgument(streamPipeSource);
var instance = PrepareStreamAnalysisInstance(pipeArgument.PipePath, ffOptions ?? GlobalFFOptions.Current);
var instance = PrepareStreamAnalysisInstance(pipeArgument.PipePath, ffOptions ?? GlobalFFOptions.Current, customArguments);
pipeArgument.Pre();
var task = instance.StartAndWaitForExitAsync(cancellationToken);
@ -148,9 +148,9 @@ public static async Task<IMediaAnalysis> AnalyseAsync(Stream stream, FFOptions?
return ParseOutput(result);
}
public static async Task<FFProbeFrames> GetFramesAsync(Uri uri, FFOptions? ffOptions = null, CancellationToken cancellationToken = default)
public static async Task<FFProbeFrames> GetFramesAsync(Uri uri, FFOptions? ffOptions = null, CancellationToken cancellationToken = default, string? customArguments = null)
{
var instance = PrepareFrameAnalysisInstance(uri.AbsoluteUri, ffOptions ?? GlobalFFOptions.Current);
var instance = PrepareFrameAnalysisInstance(uri.AbsoluteUri, ffOptions ?? GlobalFFOptions.Current, customArguments);
var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false);
return ParseFramesOutput(result);
}
@ -212,18 +212,18 @@ private static void ThrowIfExitCodeNotZero(IProcessResult result)
}
}
private static ProcessArguments PrepareStreamAnalysisInstance(string filePath, FFOptions ffOptions)
=> PrepareInstance($"-loglevel error -print_format json -show_format -sexagesimal -show_streams \"{filePath}\"", ffOptions);
private static ProcessArguments PrepareFrameAnalysisInstance(string filePath, FFOptions ffOptions)
=> PrepareInstance($"-loglevel error -print_format json -show_frames -v quiet -sexagesimal \"{filePath}\"", ffOptions);
private static ProcessArguments PreparePacketAnalysisInstance(string filePath, FFOptions ffOptions)
=> PrepareInstance($"-loglevel error -print_format json -show_packets -v quiet -sexagesimal \"{filePath}\"", ffOptions);
private static ProcessArguments PrepareStreamAnalysisInstance(string filePath, FFOptions ffOptions, string? customArguments)
=> PrepareInstance($"-loglevel error -print_format json -show_format -sexagesimal -show_streams -show_chapters \"{filePath}\"", ffOptions, customArguments);
private static ProcessArguments PrepareFrameAnalysisInstance(string filePath, FFOptions ffOptions, string? customArguments)
=> PrepareInstance($"-loglevel error -print_format json -show_frames -v quiet -sexagesimal \"{filePath}\"", ffOptions, customArguments);
private static ProcessArguments PreparePacketAnalysisInstance(string filePath, FFOptions ffOptions, string? customArguments)
=> PrepareInstance($"-loglevel error -print_format json -show_packets -v quiet -sexagesimal \"{filePath}\"", ffOptions, customArguments);
private static ProcessArguments PrepareInstance(string arguments, FFOptions ffOptions)
private static ProcessArguments PrepareInstance(string arguments, FFOptions ffOptions, string? customArguments)
{
FFProbeHelper.RootExceptionCheck();
FFProbeHelper.VerifyFFProbeExists(ffOptions);
var startInfo = new ProcessStartInfo(GlobalFFOptions.GetFFProbeBinaryPath(ffOptions), arguments)
var startInfo = new ProcessStartInfo(GlobalFFOptions.GetFFProbeBinaryPath(ffOptions), $"{arguments} {customArguments}")
{
StandardOutputEncoding = ffOptions.Encoding,
StandardErrorEncoding = ffOptions.Encoding,

View file

@ -11,6 +11,9 @@ public class FFProbeAnalysis
[JsonPropertyName("format")]
public Format Format { get; set; } = null!;
[JsonPropertyName("chapters")]
public List<Chapter> Chapters { get; set; } = null!;
[JsonIgnore]
public IReadOnlyList<string> ErrorData { get; set; } = new List<string>();
}
@ -80,6 +83,9 @@ public class FFProbeStream : ITagsContainer, IDispositionContainer
[JsonPropertyName("pix_fmt")]
public string PixelFormat { get; set; } = null!;
[JsonPropertyName("level")]
public int Level { get; set; }
[JsonPropertyName("sample_rate")]
public string SampleRate { get; set; } = null!;
@ -87,10 +93,22 @@ public class FFProbeStream : ITagsContainer, IDispositionContainer
public Dictionary<string, int> Disposition { get; set; } = null!;
[JsonPropertyName("tags")]
public Dictionary<string, string> Tags { get; set; } = null!;
public Dictionary<string, string>? Tags { get; set; }
[JsonPropertyName("side_data_list")]
public List<Dictionary<string, JsonValue>> SideData { get; set; } = null!;
[JsonPropertyName("color_range")]
public string ColorRange { get; set; } = null!;
[JsonPropertyName("color_space")]
public string ColorSpace { get; set; } = null!;
[JsonPropertyName("color_transfer")]
public string ColorTransfer { get; set; } = null!;
[JsonPropertyName("color_primaries")]
public string ColorPrimaries { get; set; } = null!;
}
public class Format : ITagsContainer
@ -126,7 +144,31 @@ public class Format : ITagsContainer
public int ProbeScore { get; set; }
[JsonPropertyName("tags")]
public Dictionary<string, string> Tags { get; set; } = null!;
public Dictionary<string, string>? Tags { get; set; }
}
public class Chapter : ITagsContainer
{
[JsonPropertyName("id")]
public long Id { get; set; }
[JsonPropertyName("time_base")]
public string TimeBase { get; set; } = null!;
[JsonPropertyName("start")]
public long Start { get; set; }
[JsonPropertyName("start_time")]
public string StartTime { get; set; } = null!;
[JsonPropertyName("end")]
public long End { get; set; }
[JsonPropertyName("end_time")]
public string EndTime { get; set; } = null!;
[JsonPropertyName("tags")]
public Dictionary<string, string>? Tags { get; set; }
}
public interface IDispositionContainer
@ -136,7 +178,7 @@ public interface IDispositionContainer
public interface ITagsContainer
{
Dictionary<string, string> Tags { get; set; }
Dictionary<string, string>? Tags { get; set; }
}
public static class TagExtensions

View file

@ -1,9 +1,12 @@
namespace FFMpegCore
using FFMpegCore.Builders.MetaData;
namespace FFMpegCore
{
public interface IMediaAnalysis
{
TimeSpan Duration { get; }
MediaFormat Format { get; }
List<ChapterData> Chapters { get; }
AudioStream? PrimaryAudioStream { get; }
VideoStream? PrimaryVideoStream { get; }
SubtitleStream? PrimarySubtitleStream { get; }

View file

@ -1,4 +1,5 @@
using System.Text.RegularExpressions;
using FFMpegCore.Builders.MetaData;
namespace FFMpegCore
{
@ -7,6 +8,7 @@ internal class MediaAnalysis : IMediaAnalysis
internal MediaAnalysis(FFProbeAnalysis analysis)
{
Format = ParseFormat(analysis.Format);
Chapters = analysis.Chapters.Select(c => ParseChapter(c)).ToList();
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();
@ -28,6 +30,18 @@ private MediaFormat ParseFormat(Format analysisFormat)
};
}
private string GetValue(string tagName, Dictionary<string, string>? tags, string defaultValue) =>
tags == null ? defaultValue : tags.TryGetValue(tagName, out var value) ? value : defaultValue;
private ChapterData ParseChapter(Chapter analysisChapter)
{
var title = GetValue("title", analysisChapter.Tags, "TitleValueNotSet");
var start = MediaAnalysisUtils.ParseDuration(analysisChapter.StartTime);
var end = MediaAnalysisUtils.ParseDuration(analysisChapter.EndTime);
return new ChapterData(title, start, end);
}
public TimeSpan Duration => new[]
{
Format.Duration,
@ -37,6 +51,8 @@ private MediaFormat ParseFormat(Format analysisFormat)
public MediaFormat Format { get; }
public List<ChapterData> Chapters { 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();
@ -74,6 +90,11 @@ private VideoStream ParseVideoStream(FFProbeStream stream)
Width = stream.Width ?? 0,
Profile = stream.Profile,
PixelFormat = stream.PixelFormat,
Level = stream.Level,
ColorRange = stream.ColorRange,
ColorSpace = stream.ColorSpace,
ColorTransfer = stream.ColorTransfer,
ColorPrimaries = stream.ColorPrimaries,
Rotation = MediaAnalysisUtils.ParseRotation(stream),
Language = stream.GetLanguage(),
Disposition = MediaAnalysisUtils.FormatDisposition(stream.Disposition),

View file

@ -1,6 +1,6 @@
namespace FFMpegCore
{
public class MediaFormat
public class MediaFormat : ITagsContainer
{
public TimeSpan Duration { get; set; }
public TimeSpan StartTime { get; set; }

View file

@ -2,7 +2,7 @@
namespace FFMpegCore
{
public abstract class MediaStream
public abstract class MediaStream : ITagsContainer
{
public int Index { get; set; }
public string CodecName { get; set; } = null!;

View file

@ -13,8 +13,13 @@ public class VideoStream : MediaStream
public int Height { get; set; }
public double FrameRate { get; set; }
public string PixelFormat { get; set; } = null!;
public int Level { get; set; }
public int Rotation { get; set; }
public double AverageFrameRate { get; set; }
public string ColorRange { get; set; } = null!;
public string ColorSpace { get; set; } = null!;
public string ColorTransfer { get; set; } = null!;
public string ColorPrimaries { get; set; } = null!;
public PixelFormat GetPixelFormatInfo() => FFMpeg.GetPixelFormat(PixelFormat);
}

View file

@ -30,12 +30,23 @@ private static string GetFFBinaryPath(string name, FFOptions ffOptions)
}
var target = Environment.Is64BitProcess ? "x64" : "x86";
if (Directory.Exists(Path.Combine(ffOptions.BinaryFolder, target)))
var possiblePaths = new List<string>()
{
ffName = Path.Combine(target, ffName);
Path.Combine(ffOptions.BinaryFolder, target),
ffOptions.BinaryFolder
};
foreach (var possiblePath in possiblePaths)
{
var possibleFFMpegPath = Path.Combine(possiblePath, ffName);
if (File.Exists(possibleFFMpegPath))
{
return possibleFFMpegPath;
}
}
return Path.Combine(ffOptions.BinaryFolder, ffName);
//Fall back to the assumption this tool exists in the PATH
return ffName;
}
private static FFOptions LoadFFOptions()

View file

@ -63,6 +63,17 @@ var bitmap = FFMpeg.Snapshot(inputPath, new Size(200, 400), TimeSpan.FromMinutes
FFMpeg.Snapshot(inputPath, outputPath, new Size(200, 400), TimeSpan.FromMinutes(1));
```
### You can also capture GIF snapshots from a video file:
```csharp
FFMpeg.GifSnapshot(inputPath, outputPath, new Size(200, 400), TimeSpan.FromSeconds(10));
// or async
await FFMpeg.GifSnapshotAsync(inputPath, outputPath, new Size(200, 400), TimeSpan.FromSeconds(10));
// you can also supply -1 to either one of Width/Height Size properties if you'd like FFMPEG to resize while maintaining the aspect ratio
await FFMpeg.GifSnapshotAsync(inputPath, outputPath, new Size(480, -1), TimeSpan.FromSeconds(10));
```
### Join video parts into one single file:
```csharp
FFMpeg.Join(@"..\joined_video.mp4",
@ -76,7 +87,7 @@ FFMpeg.Join(@"..\joined_video.mp4",
``` csharp
FFMpeg.SubVideo(inputPath,
outputPath,
TimeSpan.FromSeconds(0)
TimeSpan.FromSeconds(0),
TimeSpan.FromSeconds(30)
);
```