Compare commits

...

112 commits

Author SHA1 Message Date
Malte Rosenbjerg
6f1a8d77d7
Merge pull request #563 from rosenbjerg/main
Some checks failed
NuGet release / release (push) Has been cancelled
V.5.2.0
2025-03-05 15:35:45 +01:00
Malte Rosenbjerg
37973c3daf Use different ffmpeg version 2025-02-18 08:55:41 +01:00
Malte Rosenbjerg
0e9ceee8d9 Use different ffmpeg version 2025-02-18 08:50:36 +01:00
Malte Rosenbjerg
df08334765 Specify ffmpeg version used for CI 2025-02-18 08:45:58 +01:00
Malte Rosenbjerg
19c83b8661 Update property for Prepare FFMpeg CI step 2025-02-16 22:29:29 +01:00
Malte Rosenbjerg
cc1d9d4966 Only run CI for PRs 2025-02-16 22:29:15 +01:00
Malte Rosenbjerg
f5a8cf5de7 v 5.2.0 2025-02-16 22:17:13 +01:00
Malte Rosenbjerg
1c4333ee4c
Merge pull request #552 from rosenbjerg/bump-dependencies--instances
Some checks failed
CI / ci (macos-13) (push) Has been cancelled
CI / ci (ubuntu-latest) (push) Has been cancelled
CI / ci (windows-latest) (push) Has been cancelled
Bump Instances
2024-12-05 10:05:06 +01:00
Malte Rosenbjerg
855e6ece30 Bump Instances 2024-12-05 10:59:59 +02:00
Malte Rosenbjerg
6836b143c7
Merge pull request #551 from rosenbjerg/bump-dependencies
Some checks are pending
CI / ci (macos-13) (push) Waiting to run
CI / ci (ubuntu-latest) (push) Waiting to run
CI / ci (windows-latest) (push) Waiting to run
Bump packages
2024-12-04 23:07:53 +01:00
Malte Rosenbjerg
82387dfded
Merge branch 'main' into bump-dependencies 2024-12-04 21:54:58 +01:00
Malte Rosenbjerg
8656a86c47
Merge pull request #409 from rosenbjerg/chore/deterministic-dotnet-builds
Deterministic .NET builds
2024-12-04 21:54:39 +01:00
Malte Rosenbjerg
9d0f8893a5 Use codecov/codecov-action@v4 instead of 5 2024-12-04 22:25:36 +02:00
Malte Rosenbjerg
c7f8c19be7 Specify codecov os 2024-12-04 22:19:57 +02:00
Malte Rosenbjerg
217c1d99e2 Bump extension package nuget version 2024-12-04 22:16:22 +02:00
Malte Rosenbjerg
dedd913682 Bump packages 2024-12-04 22:15:02 +02:00
Malte Rosenbjerg
09faf340fb
Merge branch 'main' into chore/deterministic-dotnet-builds 2024-12-04 20:59:32 +01:00
Malte Rosenbjerg
71e36847a9
Merge pull request #505 from AddyMills/main
Add Multiple Input files
2024-12-04 20:59:10 +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
69535d6fdc dotnet format 2024-12-04 21:54:06 +02: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
40a3609d5d
Merge branch 'main' into main 2024-12-04 20:43:51 +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
AddyMills
8309951519 Reapply "Update JSON Reference"
This reverts commit aabd5cc3a8.
2024-11-16 20:09:24 -06:00
AddyMills
aabd5cc3a8 Revert "Update JSON Reference"
This reverts commit f3be9f2b77.
2024-11-16 20:07:57 -06:00
AddyMills
f3be9f2b77 Update JSON Reference 2024-11-16 19:50:29 -06: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
AddyMills
49c2941474 Remove JSON vulnerability Reference 2024-08-31 20:30:37 -06: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
Vlad Jerca
5e62d9ba36
Merge branch 'main' into chore/deterministic-dotnet-builds 2024-06-28 13:52:40 +02: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
AddyMills
94db493d19 Add IEnumerable tests for inputs 2024-03-03 18:56:06 -06:00
AddyMills
31b117d186 Remove whitespace 2024-03-03 13:09:52 -06:00
AddyMills
bb341e6dd7 Update one more instance of string[] to IEnumerable 2024-03-03 13:02:50 -06:00
AddyMills
622db9600c Add MultiInput Test 2024-03-03 12:49:34 -06:00
AddyMills
d836501681 Change string array to IEnumerable 2024-03-03 11:42:52 -06:00
AddyMills
8f6d1aa8e9 Update MultiInput to IEnumerable 2024-03-01 07:13:55 -06:00
AddyMills
8a764eccc7 Add MultiInputArgument 2024-03-01 07:08:16 -06: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
Phillip Fisher
38789162eb Add chapters to FFProbe 2023-02-25 11:29:41 -06:00
Malte Rosenbjerg
e9264a3c2a Update ci.yml 2023-02-22 17:52:56 +01:00
Malte Rosenbjerg
06f5bfa598 Update CI paths 2023-02-22 17:52:29 +01:00
Malte Rosenbjerg
cc2d9890f9 CI yaml fixes 2023-02-22 17:50:35 +01:00
Malte Rosenbjerg
86a500309d Set ContinuousIntegrationBuild to true 2023-02-22 17:49:09 +01:00
37 changed files with 574 additions and 96 deletions

View file

@ -1,53 +1,52 @@
name: CI
on:
push:
branches:
- master
paths:
- .github/workflows/ci.yml
- FFMpegCore/**
- FFMpegCore.Test/**
pull_request:
branches:
- main
- release
paths:
- .github/workflows/ci.yml
- FFMpegCore/**
- FFMpegCore.Test/**
- .github/workflows/ci.yml
- Directory.Build.props
- FFMpegCore/**
- FFMpegCore.Test/**
- FFMpegCore.Extensions.SkiaSharp/**
- FFMpegCore.Extensions.System.Drawing.Common/**
jobs:
ci:
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 }}
ffmpeg-version: 6.0.1
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Test with dotnet
run: dotnet test FFMpegCore.sln --collect "XPlat Code Coverage" --logger GitHubActions
- if: matrix.os == 'windows-latest'
name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
with:
fail_ci_if_error: true
directory: FFMpegCore.Test/TestResults
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}
os: windows

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

@ -13,5 +13,14 @@
<PackageProjectUrl>https://github.com/rosenbjerg/FFMpegCore</PackageProjectUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<NeutralLanguage>en</NeutralLanguage>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
</PropertyGroup>
<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>
</Project>

View file

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

View file

@ -3,17 +3,17 @@
<PropertyGroup>
<IsPackable>true</IsPackable>
<Description>Image extension for FFMpegCore using SkiaSharp</Description>
<PackageVersion>5.0.0</PackageVersion>
<PackageVersion>5.0.2</PackageVersion>
<PackageOutputPath>../nupkg</PackageOutputPath>
<PackageReleaseNotes>
</PackageReleaseNotes>
<PackageReleaseNotes>Bump dependencies</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="3.116.1" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="3.116.1" />
</ItemGroup>
<ItemGroup>

View file

@ -3,16 +3,16 @@
<PropertyGroup>
<IsPackable>true</IsPackable>
<Description>Image extension for FFMpegCore using System.Common.Drawing</Description>
<PackageVersion>5.0.0</PackageVersion>
<PackageVersion>5.0.2</PackageVersion>
<PackageOutputPath>../nupkg</PackageOutputPath>
<PackageReleaseNotes>
</PackageReleaseNotes>
<PackageReleaseNotes>Bump dependencies</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>
<PackageReference Include="System.Drawing.Common" Version="7.0.0"/>
<PackageReference Include="System.Drawing.Common" Version="9.0.2" />
</ItemGroup>
<ItemGroup>

View file

@ -9,6 +9,7 @@ namespace FFMpegCore.Test
public class ArgumentBuilderTest
{
private readonly string[] _concatFiles = { "1.mp4", "2.mp4", "3.mp4", "4.mp4" };
private readonly string[] _multiFiles = { "1.mp3", "2.mp3", "3.mp3", "4.mp3" };
[TestMethod]
public void Builder_BuildString_IO_1()
@ -571,5 +572,126 @@ namespace FFMpegCore.Test
-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);
}
[TestMethod]
public void Builder_BuildString_MultiInput()
{
var audioStreams = string.Join("", _multiFiles.Select((item, index) => $"[{index}:0]"));
var mixFilter = $"{audioStreams}amix=inputs={_multiFiles.Length}:duration=longest:dropout_transition=1:normalize=0[final]";
var ffmpegArgs = $"-filter_complex \"{mixFilter}\" -map \"[final]\"";
var str = FFMpegArguments
.FromFileInput(_multiFiles)
.OutputToFile("output.mp3", overwrite: true, options => options
.WithCustomArgument(ffmpegArgs)
.WithAudioCodec(AudioCodec.LibMp3Lame) // Set the audio codec to MP3
.WithAudioBitrate(128) // Set the bitrate to 128kbps
.WithAudioSamplingRate(48000) // Set the sample rate to 48kHz
.WithoutMetadata() // Remove metadata
.WithCustomArgument("-ac 2 -write_xing 0 -id3v2_version 0")) // Force 2 Channels
.Arguments;
Assert.AreEqual($"-i \"1.mp3\" -i \"2.mp3\" -i \"3.mp3\" -i \"4.mp3\" -filter_complex \"[0:0][1:0][2:0][3:0]amix=inputs=4:duration=longest:dropout_transition=1:normalize=0[final]\" -map \"[final]\" -c:a libmp3lame -b:a 128k -ar 48000 -map_metadata -1 -ac 2 -write_xing 0 -id3v2_version 0 \"output.mp3\" -y", str);
}
[TestMethod]
public void Pre_VerifyExists_AllFilesExist()
{
// Arrange
var filePaths = new List<string>
{
Path.GetTempFileName(),
Path.GetTempFileName(),
Path.GetTempFileName()
};
var argument = new MultiInputArgument(true, filePaths);
try
{
// Act & Assert
argument.Pre(); // No exception should be thrown
}
finally
{
// Cleanup
foreach (var filePath in filePaths)
{
File.Delete(filePath);
}
}
}
[TestMethod]
public void Pre_VerifyExists_SomeFilesNotExist()
{
// Arrange
var filePaths = new List<string>
{
Path.GetTempFileName(),
"file2.mp4",
"file3.mp4"
};
var argument = new MultiInputArgument(true, filePaths);
try
{
// Act & Assert
Assert.ThrowsException<FileNotFoundException>(() => argument.Pre());
}
finally
{
// Cleanup
File.Delete(filePaths[0]);
}
}
[TestMethod]
public void Pre_VerifyExists_NoFilesExist()
{
// Arrange
var filePaths = new List<string>
{
"file1.mp4",
"file2.mp4",
"file3.mp4"
};
var argument = new MultiInputArgument(true, filePaths);
// Act & Assert
Assert.ThrowsException<FileNotFoundException>(() => argument.Pre());
}
}
}

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.4">
<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="8.0.1" />
<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.13.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.8.0" />
<PackageReference Include="MSTest.TestFramework" Version="3.8.0" />
<PackageReference Include="SkiaSharp" Version="3.116.1" />
</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 @@ namespace FFMpegCore.Test
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 @@ namespace FFMpegCore.Test
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 @@ namespace FFMpegCore.Test
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 @@ namespace FFMpegCore.Test
{
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 @@ namespace FFMpegCore.Test
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 @@ namespace FFMpegCore.Test
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 @@ namespace FFMpegCore.Test
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 @@ namespace FFMpegCore.Test
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 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

@ -480,6 +480,21 @@ namespace FFMpegCore.Test
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()
{

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,47 @@
namespace FFMpegCore.Arguments
{
/// <summary>
/// Represents input parameters for multiple files
/// </summary>
public class MultiInputArgument : IInputArgument
{
public readonly bool VerifyExists;
public readonly IEnumerable<string> FilePaths;
public MultiInputArgument(bool verifyExists, IEnumerable<string> filePaths)
{
VerifyExists = verifyExists;
FilePaths = filePaths;
}
public MultiInputArgument(IEnumerable<string> filePaths, bool verifyExists) : this(verifyExists, filePaths) { }
public void Pre()
{
if (VerifyExists)
{
var missingFiles = new List<string>();
foreach (var filePath in FilePaths)
{
if (!File.Exists(filePath))
{
missingFiles.Add(filePath);
}
}
if (missingFiles.Any())
{
throw new FileNotFoundException($"The following input files were not found: {string.Join(", ", missingFiles)}");
}
}
}
public Task During(CancellationToken cancellationToken = default) => Task.CompletedTask;
public void Post() { }
/// <summary>
/// Generates a combined input argument text for all file paths
/// </summary>
public string Text => string.Join(" ", FilePaths.Select(filePath => $"-i \"{filePath}\""));
}
}

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 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 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 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

@ -333,7 +333,10 @@ namespace FFMpegCore
}
return FFMpegArguments
.FromUrlInput(uri)
.FromUrlInput(uri, options =>
{
options.WithCopyCodec();
})
.OutputToFile(output)
.ProcessSynchronously();
}

View file

@ -16,7 +16,10 @@ namespace FFMpegCore
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));
@ -77,6 +80,7 @@ namespace FFMpegCore
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 @@ namespace FFMpegCore
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

@ -21,6 +21,7 @@ namespace FFMpegCore
public static FFMpegArguments FromConcatInput(IEnumerable<string> filePaths, Action<FFMpegArgumentOptions>? addArguments = null) => new FFMpegArguments().WithInput(new ConcatArgument(filePaths), addArguments);
public static FFMpegArguments FromDemuxConcatInput(IEnumerable<string> filePaths, Action<FFMpegArgumentOptions>? addArguments = null) => new FFMpegArguments().WithInput(new DemuxConcatArgument(filePaths), addArguments);
public static FFMpegArguments FromFileInput(string filePath, bool verifyExists = true, Action<FFMpegArgumentOptions>? addArguments = null) => new FFMpegArguments().WithInput(new InputArgument(verifyExists, filePath), addArguments);
public static FFMpegArguments FromFileInput(IEnumerable<string> filePath, bool verifyExists = true, Action<FFMpegArgumentOptions>? addArguments = null) => new FFMpegArguments().WithInput(new MultiInputArgument(verifyExists, filePath), addArguments);
public static FFMpegArguments FromFileInput(FileInfo fileInfo, Action<FFMpegArgumentOptions>? addArguments = null) => new FFMpegArguments().WithInput(new InputArgument(fileInfo.FullName, false), addArguments);
public static FFMpegArguments FromUrlInput(Uri uri, Action<FFMpegArgumentOptions>? addArguments = null) => new FFMpegArguments().WithInput(new InputArgument(uri.AbsoluteUri, false), addArguments);
public static FFMpegArguments FromDeviceInput(string device, Action<FFMpegArgumentOptions>? addArguments = null) => new FFMpegArguments().WithInput(new InputDeviceArgument(device), addArguments);
@ -35,6 +36,7 @@ namespace FFMpegCore
public FFMpegArguments AddConcatInput(IEnumerable<string> filePaths, Action<FFMpegArgumentOptions>? addArguments = null) => WithInput(new ConcatArgument(filePaths), addArguments);
public FFMpegArguments AddDemuxConcatInput(IEnumerable<string> filePaths, Action<FFMpegArgumentOptions>? addArguments = null) => WithInput(new DemuxConcatArgument(filePaths), addArguments);
public FFMpegArguments AddFileInput(string filePath, bool verifyExists = true, Action<FFMpegArgumentOptions>? addArguments = null) => WithInput(new InputArgument(verifyExists, filePath), addArguments);
public FFMpegArguments AddFileInput(IEnumerable<string> filePath, bool verifyExists = true, Action<FFMpegArgumentOptions>? addArguments = null) => WithInput(new MultiInputArgument(verifyExists, filePath), addArguments);
public FFMpegArguments AddFileInput(FileInfo fileInfo, Action<FFMpegArgumentOptions>? addArguments = null) => WithInput(new InputArgument(fileInfo.FullName, false), addArguments);
public FFMpegArguments AddUrlInput(Uri uri, Action<FFMpegArgumentOptions>? addArguments = null) => WithInput(new InputArgument(uri.AbsoluteUri, false), addArguments);
public FFMpegArguments AddDeviceInput(string device, Action<FFMpegArgumentOptions>? addArguments = null) => WithInput(new InputDeviceArgument(device), addArguments);
@ -71,6 +73,21 @@ namespace FFMpegCore
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 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

@ -64,7 +64,7 @@ public static class SnapshotArgumentBuilder
}
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);
}
@ -88,4 +88,10 @@ public static class SnapshotArgumentBuilder
return null;
}
private static bool IsRotated(int rotation)
{
var absRotation = Math.Abs(rotation);
return absRotation == 90 || absRotation == 180;
}
}

View file

@ -3,10 +3,15 @@
<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.1.0</PackageVersion>
<PackageVersion>5.2.0</PackageVersion>
<PackageOutputPath>../nupkg</PackageOutputPath>
<PackageReleaseNotes>
</PackageReleaseNotes>
<PackageReleaseNotes>- **Instances and Packages Updates**: Updates to various instances and packages by rosenbjerg.
- **Audio and Video Enhancements**: Additions include a Copy option to Audio Codec and a Crop option to Arguments by brett-baker; video-stream level added to FFProbe analysis by Kaaybi; AV1 support for smaller snapshots and videos by BenediktBertsch; multiple input files support by AddyMills; HDR color properties support added to FFProbe analysis by Tomiscout.
- **System.Text.Json Bump**: Update by Kaaybi.
- **FFMpeg Processors and Utilities**: Modification for handling durations over 24 hours in `FFMpegArgumentProcessor` by alahane-techtel; fix for snapshots with correct width/height from rotated videos by Hagfjall.
- **Feature Additions and Fixes**: Support for multiple outputs and tee muxer by duggaraju; custom ffprob arguments by vfrz; fix for null reference exception with tags container by rosenbjerg; Chapter Modell change by vortex852456; codec copy added to the SaveM3U8Stream method by rpaschoal.
- **Closed and Non-merged Contributions**: Notable closed contributions include JSON source generators usage by onionware-github; Snapshot overload by 3UR; FromRawInput method by pedoc; runtime ffmpeg suite installation by yuqian5; and support for scale_npp by vicwilliam.
- **Miscellaneous Fixes**: Minor readme corrections by NaBian; fix for ffmpeg path issue by devedse.</PackageReleaseNotes>
<PackageTags>ffmpeg ffprobe convert video audio mediafile resize analyze muxing</PackageTags>
<Authors>Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev</Authors>
<PackageReadmeFile>README.md</PackageReadmeFile>
@ -17,8 +22,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Instances" Version="3.0.0" />
<PackageReference Include="System.Text.Json" Version="7.0.2" />
<PackageReference Include="Instances" Version="3.0.1" />
<PackageReference Include="System.Text.Json" Version="9.0.2" />
</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 @@ namespace FFMpegCore
/// </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 @@ namespace FFMpegCore
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 @@ namespace FFMpegCore
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 @@ namespace FFMpegCore
}
}
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 @@ namespace FFMpegCore
[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 @@ namespace FFMpegCore
[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 @@ namespace FFMpegCore
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 @@ namespace FFMpegCore
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 @@ namespace FFMpegCore
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 @@ namespace FFMpegCore
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 @@ namespace FFMpegCore
};
}
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 @@ namespace FFMpegCore
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 @@ namespace FFMpegCore
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 @@ namespace FFMpegCore
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 @@ namespace FFMpegCore
}
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

@ -87,7 +87,7 @@ FFMpeg.Join(@"..\joined_video.mp4",
``` csharp
FFMpeg.SubVideo(inputPath,
outputPath,
TimeSpan.FromSeconds(0)
TimeSpan.FromSeconds(0),
TimeSpan.FromSeconds(30)
);
```