Compare commits

...

7 commits

Author SHA1 Message Date
tqk2811
076f754267
Merge 89d4235f7a into cc75e03ec9 2025-11-01 04:11:56 +01:00
Malte Rosenbjerg
cc75e03ec9
Merge pull request #602 from rosenbjerg/add-more-missing-tests
Add more missing tests
2025-10-29 14:28:08 +01:00
Malte Rosenbjerg
5356536f37 Fix linting 2025-10-28 12:21:09 +01:00
Malte Rosenbjerg
f0f60c8bd8 Add more missing tests 2025-10-28 12:19:21 +01:00
tqk2811
89d4235f7a FFProbe: add GetAnalysis and GetAnalysisAsync #589 2025-10-18 17:36:53 +07:00
tqk2811
07bfc46178 remove JsonIgnore from FFProbeAnalysis.ErrorData #589 2025-10-18 17:15:57 +07:00
tqk2811
b83e7a4fff make MediaAnalysis public #589 2025-10-18 17:13:31 +07:00
5 changed files with 131 additions and 11 deletions

View file

@ -93,6 +93,14 @@ public class ArgumentBuilderTest
Assert.AreEqual("-i \"concat:1.mp4|2.mp4|3.mp4|4.mp4\" \"output.mp4\"", str);
}
[TestMethod]
public void Builder_BuildString_DemuxConcat()
{
var str = FFMpegArguments.FromDemuxConcatInput(_concatFiles).OutputToFile("output.mp4", false).Arguments;
Assert.Contains("-f concat -safe 0 -i", str);
Assert.Contains("\"output.mp4\"", str);
}
[TestMethod]
public void Builder_BuildString_Copy_Audio()
{

View file

@ -589,6 +589,19 @@ public class VideoTest
Assert.AreEqual(bitmap.RawFormat, ImageFormat.Png);
}
[SupportedOSPlatform("windows")]
[OsSpecificTestMethod(OsPlatforms.Windows)]
[Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)]
public async Task Video_SnapshotAsync_InMemory_SystemDrawingCommon()
{
using var bitmap = await FFMpegImage.SnapshotAsync(TestResources.Mp4Video, cancellationToken: TestContext.CancellationToken);
var input = await FFProbe.AnalyseAsync(TestResources.Mp4Video, cancellationToken: TestContext.CancellationToken);
Assert.AreEqual(input.PrimaryVideoStream!.Width, bitmap.Width);
Assert.AreEqual(input.PrimaryVideoStream.Height, bitmap.Height);
Assert.AreEqual(bitmap.RawFormat, ImageFormat.Png);
}
[TestMethod]
[Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)]
public void Video_Snapshot_InMemory_SkiaSharp()
@ -602,6 +615,19 @@ public class VideoTest
// e.g. Bgra8888 on Windows and Rgba8888 on macOS.
}
[TestMethod]
[Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)]
public async Task Video_SnapshotAsync_InMemory_SkiaSharp()
{
using var bitmap = await Extensions.SkiaSharp.FFMpegImage.SnapshotAsync(TestResources.Mp4Video, cancellationToken: TestContext.CancellationToken);
var input = await FFProbe.AnalyseAsync(TestResources.Mp4Video, cancellationToken: TestContext.CancellationToken);
Assert.AreEqual(input.PrimaryVideoStream!.Width, bitmap.Width);
Assert.AreEqual(input.PrimaryVideoStream.Height, bitmap.Height);
// Note: The resulting ColorType is dependent on the execution environment and therefore not assessed,
// e.g. Bgra8888 on Windows and Rgba8888 on macOS.
}
[TestMethod]
[Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)]
public void Video_Snapshot_Png_PersistSnapshot()
@ -617,6 +643,21 @@ public class VideoTest
Assert.AreEqual("png", analysis.PrimaryVideoStream!.CodecName);
}
[TestMethod]
[Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)]
public async Task Video_SnapshotAsync_Png_PersistSnapshot()
{
using var outputPath = new TemporaryFile("out.png");
var input = await FFProbe.AnalyseAsync(TestResources.Mp4Video, cancellationToken: TestContext.CancellationToken);
await FFMpeg.SnapshotAsync(TestResources.Mp4Video, outputPath, cancellationToken: TestContext.CancellationToken);
var analysis = FFProbe.Analyse(outputPath);
Assert.AreEqual(input.PrimaryVideoStream!.Width, analysis.PrimaryVideoStream!.Width);
Assert.AreEqual(input.PrimaryVideoStream.Height, analysis.PrimaryVideoStream!.Height);
Assert.AreEqual("png", analysis.PrimaryVideoStream!.CodecName);
}
[TestMethod]
[Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)]
public void Video_Snapshot_Jpg_PersistSnapshot()
@ -781,6 +822,46 @@ public class VideoTest
Assert.AreEqual(input.PrimaryVideoStream.Width, result.PrimaryVideoStream.Width);
}
[TestMethod]
[Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)]
public void Video_Convert_Webm()
{
using var outputPath = new TemporaryFile("out.webm");
var success = FFMpeg.Convert(TestResources.Mp4Video, outputPath, VideoType.WebM);
Assert.IsTrue(success);
Assert.IsTrue(File.Exists(outputPath));
var input = FFProbe.Analyse(TestResources.Mp4Video);
var result = FFProbe.Analyse(outputPath);
Assert.AreEqual(input.Duration.Days, result.Duration.Days);
Assert.AreEqual(input.Duration.Hours, result.Duration.Hours);
Assert.AreEqual(input.Duration.Minutes, result.Duration.Minutes);
Assert.AreEqual(input.Duration.Seconds, result.Duration.Seconds);
Assert.AreEqual(input.PrimaryVideoStream!.Height, result.PrimaryVideoStream!.Height);
Assert.AreEqual(input.PrimaryVideoStream.Width, result.PrimaryVideoStream.Width);
}
[TestMethod]
[Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)]
public void Video_Convert_Ogv()
{
using var outputPath = new TemporaryFile("out.ogv");
var success = FFMpeg.Convert(TestResources.Mp4Video, outputPath, VideoType.Ogv);
Assert.IsTrue(success);
Assert.IsTrue(File.Exists(outputPath));
var input = FFProbe.Analyse(TestResources.Mp4Video);
var result = FFProbe.Analyse(outputPath);
Assert.AreEqual(input.Duration.Days, result.Duration.Days);
Assert.AreEqual(input.Duration.Hours, result.Duration.Hours);
Assert.AreEqual(input.Duration.Minutes, result.Duration.Minutes);
Assert.AreEqual(input.Duration.Seconds, result.Duration.Seconds);
Assert.AreEqual(input.PrimaryVideoStream!.Height, result.PrimaryVideoStream!.Height);
Assert.AreEqual(input.PrimaryVideoStream.Width, result.PrimaryVideoStream.Width);
}
[TestMethod]
[Timeout(2 * BaseTimeoutMilliseconds, CooperativeCancellation = true)]
public void Video_Join_Image_Sequence()

View file

@ -12,6 +12,11 @@ namespace FFMpegCore;
public static class FFProbe
{
public static IMediaAnalysis Analyse(string filePath, FFOptions? ffOptions = null, string? customArguments = null)
{
return new MediaAnalysis(GetAnalysis(filePath, ffOptions, customArguments));
}
public static FFProbeAnalysis GetAnalysis(string filePath, FFOptions? ffOptions = null, string? customArguments = null)
{
ThrowIfInputFileDoesNotExist(filePath);
@ -19,7 +24,7 @@ public static class FFProbe
var result = processArguments.StartAndWaitForExit();
ThrowIfExitCodeNotZero(result);
return ParseOutput(result);
return DeserializeOutput(result);
}
public static FFProbeFrames GetFrames(string filePath, FFOptions? ffOptions = null, string? customArguments = null)
@ -45,15 +50,25 @@ public static class FFProbe
}
public static IMediaAnalysis Analyse(Uri uri, FFOptions? ffOptions = null, string? customArguments = null)
{
return new MediaAnalysis(GetAnalysis(uri, ffOptions, customArguments));
}
public static FFProbeAnalysis GetAnalysis(Uri uri, FFOptions? ffOptions = null, string? customArguments = null)
{
var instance = PrepareStreamAnalysisInstance(uri.AbsoluteUri, ffOptions ?? GlobalFFOptions.Current, customArguments);
var result = instance.StartAndWaitForExit();
ThrowIfExitCodeNotZero(result);
return ParseOutput(result);
return DeserializeOutput(result);
}
public static IMediaAnalysis Analyse(Stream stream, FFOptions? ffOptions = null, string? customArguments = null)
{
return new MediaAnalysis(GetAnalysis(stream, ffOptions, customArguments));
}
public static FFProbeAnalysis GetAnalysis(Stream stream, FFOptions? ffOptions = null, string? customArguments = null)
{
var streamPipeSource = new StreamPipeSource(stream);
var pipeArgument = new InputPipeArgument(streamPipeSource);
@ -74,11 +89,17 @@ public static class FFProbe
var result = task.ConfigureAwait(false).GetAwaiter().GetResult();
ThrowIfExitCodeNotZero(result);
return ParseOutput(result);
return DeserializeOutput(result);
}
public static async Task<IMediaAnalysis> AnalyseAsync(string filePath, FFOptions? ffOptions = null, CancellationToken cancellationToken = default,
string? customArguments = null)
{
return new MediaAnalysis(await GetAnalysisAsync(filePath, ffOptions, cancellationToken, customArguments));
}
public static async Task<FFProbeAnalysis> GetAnalysisAsync(string filePath, FFOptions? ffOptions = null, CancellationToken cancellationToken = default,
string? customArguments = null)
{
ThrowIfInputFileDoesNotExist(filePath);
@ -87,7 +108,7 @@ public static class FFProbe
cancellationToken.ThrowIfCancellationRequested();
ThrowIfExitCodeNotZero(result);
return ParseOutput(result);
return DeserializeOutput(result);
}
public static FFProbeFrames GetFrames(Uri uri, FFOptions? ffOptions = null, string? customArguments = null)
@ -121,17 +142,27 @@ public static class FFProbe
public static async Task<IMediaAnalysis> AnalyseAsync(Uri uri, FFOptions? ffOptions = null, CancellationToken cancellationToken = default,
string? customArguments = null)
{
return new MediaAnalysis(await GetAnalysisAsync(uri, ffOptions, cancellationToken, customArguments));
}
public static async Task<FFProbeAnalysis> GetAnalysisAsync(Uri uri, FFOptions? ffOptions = null, CancellationToken cancellationToken = default,
string? customArguments = null)
{
var instance = PrepareStreamAnalysisInstance(uri.AbsoluteUri, ffOptions ?? GlobalFFOptions.Current, customArguments);
var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
ThrowIfExitCodeNotZero(result);
return ParseOutput(result);
return DeserializeOutput(result);
}
public static async Task<IMediaAnalysis> AnalyseAsync(Stream stream, FFOptions? ffOptions = null, CancellationToken cancellationToken = default,
string? customArguments = null)
{
return new MediaAnalysis(await GetAnalysisAsync(stream, ffOptions, cancellationToken, customArguments));
}
public static async Task<FFProbeAnalysis> GetAnalysisAsync(Stream stream, FFOptions? ffOptions = null, CancellationToken cancellationToken = default,
string? customArguments = null)
{
var streamPipeSource = new StreamPipeSource(stream);
var pipeArgument = new InputPipeArgument(streamPipeSource);
@ -156,7 +187,7 @@ public static class FFProbe
ThrowIfExitCodeNotZero(result);
pipeArgument.Post();
return ParseOutput(result);
return DeserializeOutput(result);
}
public static async Task<FFProbeFrames> GetFramesAsync(Uri uri, FFOptions? ffOptions = null, CancellationToken cancellationToken = default,
@ -167,7 +198,7 @@ public static class FFProbe
return ParseFramesOutput(result);
}
private static IMediaAnalysis ParseOutput(IProcessResult instance)
private static FFProbeAnalysis DeserializeOutput(IProcessResult instance)
{
var json = string.Join(string.Empty, instance.OutputData);
var ffprobeAnalysis = JsonSerializer.Deserialize<FFProbeAnalysis>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
@ -178,7 +209,7 @@ public static class FFProbe
}
ffprobeAnalysis.ErrorData = instance.ErrorData;
return new MediaAnalysis(ffprobeAnalysis);
return ffprobeAnalysis;
}
private static FFProbeFrames ParseFramesOutput(IProcessResult instance)

View file

@ -11,7 +11,7 @@ public class FFProbeAnalysis
[JsonPropertyName("chapters")] public List<Chapter> Chapters { get; set; } = null!;
[JsonIgnore] public IReadOnlyList<string> ErrorData { get; set; } = new List<string>();
public IReadOnlyList<string> ErrorData { get; set; } = new List<string>();
}
public class FFProbeStream : ITagsContainer, IDispositionContainer

View file

@ -4,9 +4,9 @@ using FFMpegCore.Builders.MetaData;
namespace FFMpegCore;
internal class MediaAnalysis : IMediaAnalysis
public class MediaAnalysis : IMediaAnalysis
{
internal MediaAnalysis(FFProbeAnalysis analysis)
public MediaAnalysis(FFProbeAnalysis analysis)
{
Format = ParseFormat(analysis.Format);
Chapters = analysis.Chapters.Select(c => ParseChapter(c)).ToList();