Compare commits

..

4 commits

Author SHA1 Message Date
Sergey Nechaev
65cbc5552c Update the ThrowIfExitCodeNotZero() to check the exit code before handling cancellation.
This preserves the original semantics and contract (throw only if the ffprobe exits with a non-zero code).
2025-10-24 18:06:55 +02:00
Sergey Nechaev
8f9930ad2a Additional test to verify that FFProbeHelper still throws FFMpegException when FFProbe exits with non-zero code and no cancellation was requested.
Ref.: #594
2025-10-22 21:24:32 +02:00
Sergey Nechaev
c3e80a7af6 FFProbe: Do not throw FFMpegException if cancellation was requested.
Throw OperationCancelledException in this case to provide more uniform and expected behavior.

Fixes #594
2025-10-22 20:44:50 +02:00
Sergey Nechaev
5f906af57f Add test to verify unexpected exception on FFProbe operations cancellation.
Ref.: #594
2025-10-22 20:44:50 +02:00
4 changed files with 7 additions and 108 deletions

View file

@ -82,40 +82,6 @@ public class VideoTest
Assert.IsTrue(success); Assert.IsTrue(success);
} }
[TestMethod]
[Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)]
public async Task Video_MetadataBuilder()
{
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
await FFMpegArguments
.FromFileInput(TestResources.WebmVideo)
.AddMetaData(FFMetadataBuilder.Empty()
.WithTag("title", "noname")
.WithTag("artist", "unknown")
.WithChapter("Chapter 1", 1.1)
.WithChapter("Chapter 2", 1.23))
.OutputToFile(outputFile, false, opt => opt
.WithVideoCodec(VideoCodec.LibX264))
.CancellableThrough(TestContext.CancellationToken)
.ProcessAsynchronously();
var analysis = await FFProbe.AnalyseAsync(outputFile, cancellationToken: TestContext.CancellationToken);
Assert.IsTrue(analysis.Format.Tags!.TryGetValue("title", out var title));
Assert.IsTrue(analysis.Format.Tags!.TryGetValue("artist", out var artist));
Assert.AreEqual("noname", title);
Assert.AreEqual("unknown", artist);
Assert.HasCount(2, analysis.Chapters);
Assert.AreEqual("Chapter 1", analysis.Chapters.First().Title);
Assert.AreEqual(1.1, analysis.Chapters.First().Duration.TotalSeconds);
Assert.AreEqual(1.1, analysis.Chapters.First().End.TotalSeconds);
Assert.AreEqual("Chapter 2", analysis.Chapters.Last().Title);
Assert.AreEqual(1.23, analysis.Chapters.Last().Duration.TotalSeconds);
Assert.AreEqual(1.1 + 1.23, analysis.Chapters.Last().End.TotalSeconds);
}
[TestMethod] [TestMethod]
[Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)]
public void Video_ToH265_MKV_Args() public void Video_ToH265_MKV_Args()

View file

@ -1,62 +0,0 @@
using System.Text;
namespace FFMpegCore;
public class FFMetadataBuilder
{
private Dictionary<string, string> Tags { get; } = new();
private List<FFMetadataChapter> Chapters { get; } = [];
public static FFMetadataBuilder Empty()
{
return new FFMetadataBuilder();
}
public FFMetadataBuilder WithTag(string key, string value)
{
Tags.Add(key, value);
return this;
}
public FFMetadataBuilder WithChapter(string title, long durationMs)
{
Chapters.Add(new FFMetadataChapter(title, durationMs));
return this;
}
public FFMetadataBuilder WithChapter(string title, double durationSeconds)
{
Chapters.Add(new FFMetadataChapter(title, Convert.ToInt64(durationSeconds * 1000)));
return this;
}
public string GetMetadataFileContent()
{
var sb = new StringBuilder();
sb.AppendLine(";FFMETADATA1");
foreach (var tag in Tags)
{
sb.AppendLine($"{tag.Key}={tag.Value}");
}
long totalDurationMs = 0;
foreach (var chapter in Chapters)
{
sb.AppendLine("[CHAPTER]");
sb.AppendLine("TIMEBASE=1/1000");
sb.AppendLine($"START={totalDurationMs}");
sb.AppendLine($"END={totalDurationMs + chapter.DurationMs}");
sb.AppendLine($"title={chapter.Title}");
totalDurationMs += chapter.DurationMs;
}
return sb.ToString();
}
private class FFMetadataChapter(string title, long durationMs)
{
public string Title { get; } = title;
public long DurationMs { get; } = durationMs;
}
}

View file

@ -109,11 +109,6 @@ public sealed class FFMpegArguments : FFMpegArgumentsBase
return WithInput(new MetaDataArgument(content), addArguments); return WithInput(new MetaDataArgument(content), addArguments);
} }
public FFMpegArguments AddMetaData(FFMetadataBuilder metaDataBuilder, Action<FFMpegArgumentOptions>? addArguments = null)
{
return WithInput(new MetaDataArgument(metaDataBuilder.GetMetadataFileContent()), addArguments);
}
public FFMpegArguments AddMetaData(IReadOnlyMetaData metaData, Action<FFMpegArgumentOptions>? addArguments = null) public FFMpegArguments AddMetaData(IReadOnlyMetaData metaData, Action<FFMpegArgumentOptions>? addArguments = null)
{ {
return WithInput(new MetaDataArgument(MetaDataSerializer.Instance.Serialize(metaData)), addArguments); return WithInput(new MetaDataArgument(MetaDataSerializer.Instance.Serialize(metaData)), addArguments);

View file

@ -84,8 +84,7 @@ public static class FFProbe
var instance = PrepareStreamAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current, customArguments); var instance = PrepareStreamAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current, customArguments);
var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false); var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested(); ThrowIfExitCodeNotZero(result, cancellationToken);
ThrowIfExitCodeNotZero(result);
return ParseOutput(result); return ParseOutput(result);
} }
@ -124,8 +123,7 @@ public static class FFProbe
{ {
var instance = PrepareStreamAnalysisInstance(uri.AbsoluteUri, ffOptions ?? GlobalFFOptions.Current, customArguments); var instance = PrepareStreamAnalysisInstance(uri.AbsoluteUri, ffOptions ?? GlobalFFOptions.Current, customArguments);
var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false); var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested(); ThrowIfExitCodeNotZero(result, cancellationToken);
ThrowIfExitCodeNotZero(result);
return ParseOutput(result); return ParseOutput(result);
} }
@ -152,8 +150,7 @@ public static class FFProbe
} }
var result = await task.ConfigureAwait(false); var result = await task.ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested(); ThrowIfExitCodeNotZero(result, cancellationToken);
ThrowIfExitCodeNotZero(result);
pipeArgument.Post(); pipeArgument.Post();
return ParseOutput(result); return ParseOutput(result);
@ -215,10 +212,13 @@ public static class FFProbe
} }
} }
private static void ThrowIfExitCodeNotZero(IProcessResult result) private static void ThrowIfExitCodeNotZero(IProcessResult result, CancellationToken cancellationToken = default)
{ {
if (result.ExitCode != 0) if (result.ExitCode != 0)
{ {
// if cancellation requested, then we are not interested in the exit code, just throw the cancellation exception
// to get consistent and expected behavior.
cancellationToken.ThrowIfCancellationRequested();
var message = $"ffprobe exited with non-zero exit-code ({result.ExitCode} - {string.Join("\n", result.ErrorData)})"; var message = $"ffprobe exited with non-zero exit-code ({result.ExitCode} - {string.Join("\n", result.ErrorData)})";
throw new FFMpegException(FFMpegExceptionType.Process, message, null, string.Join("\n", result.ErrorData)); throw new FFMpegException(FFMpegExceptionType.Process, message, null, string.Join("\n", result.ErrorData));
} }