Skip to content

Commit ed8f899

Browse files
authored
Merge pull request #598 from rosenbjerg/main
V.5.4.0
2 parents 4651252 + 3b1a143 commit ed8f899

File tree

16 files changed

+396
-94
lines changed

16 files changed

+396
-94
lines changed

FFMpegCore.Extensions.SkiaSharp/FFMpegImage.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,18 +39,21 @@ public static SKBitmap Snapshot(string input, Size? size = null, TimeSpan? captu
3939
/// <param name="size">Thumbnail size. If width or height equal 0, the other will be computed automatically.</param>
4040
/// <param name="streamIndex">Selected video stream index.</param>
4141
/// <param name="inputFileIndex">Input file index</param>
42+
/// <param name="cancellationToken">Cancellation token</param>
4243
/// <returns>Bitmap with the requested snapshot.</returns>
4344
public static async Task<SKBitmap> SnapshotAsync(string input, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null,
44-
int inputFileIndex = 0)
45+
int inputFileIndex = 0, CancellationToken cancellationToken = default)
4546
{
46-
var source = await FFProbe.AnalyseAsync(input).ConfigureAwait(false);
47+
var source = await FFProbe.AnalyseAsync(input, cancellationToken: cancellationToken).ConfigureAwait(false);
4748
var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex);
4849
using var ms = new MemoryStream();
4950

5051
await arguments
5152
.OutputToPipe(new StreamPipeSink(ms), options => outputOptions(options
5253
.ForceFormat("rawvideo")))
53-
.ProcessAsynchronously();
54+
.CancellableThrough(cancellationToken)
55+
.ProcessAsynchronously()
56+
.ConfigureAwait(false);
5457

5558
ms.Position = 0;
5659
return SKBitmap.Decode(ms);

FFMpegCore.Extensions.System.Drawing.Common/FFMpegImage.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,18 +38,21 @@ public static Bitmap Snapshot(string input, Size? size = null, TimeSpan? capture
3838
/// <param name="size">Thumbnail size. If width or height equal 0, the other will be computed automatically.</param>
3939
/// <param name="streamIndex">Selected video stream index.</param>
4040
/// <param name="inputFileIndex">Input file index</param>
41+
/// <param name="cancellationToken">Cancellation token</param>
4142
/// <returns>Bitmap with the requested snapshot.</returns>
4243
public static async Task<Bitmap> SnapshotAsync(string input, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null,
43-
int inputFileIndex = 0)
44+
int inputFileIndex = 0, CancellationToken cancellationToken = default)
4445
{
45-
var source = await FFProbe.AnalyseAsync(input).ConfigureAwait(false);
46+
var source = await FFProbe.AnalyseAsync(input, cancellationToken: cancellationToken).ConfigureAwait(false);
4647
var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex);
4748
using var ms = new MemoryStream();
4849

4950
await arguments
5051
.OutputToPipe(new StreamPipeSink(ms), options => outputOptions(options
5152
.ForceFormat("rawvideo")))
52-
.ProcessAsynchronously();
53+
.CancellableThrough(cancellationToken)
54+
.ProcessAsynchronously()
55+
.ConfigureAwait(false);
5356

5457
ms.Position = 0;
5558
return new Bitmap(ms);

FFMpegCore.Test/AudioTest.cs

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ namespace FFMpegCore.Test;
99
[TestClass]
1010
public class AudioTest
1111
{
12+
private const int BaseTimeoutMilliseconds = 30_000;
13+
14+
public TestContext TestContext { get; set; }
15+
1216
[TestMethod]
1317
public void Audio_Remove()
1418
{
@@ -41,6 +45,7 @@ public async Task Audio_FromRaw()
4145
await FFMpegArguments
4246
.FromPipeInput(new StreamPipeSource(file), options => options.ForceFormat("s16le"))
4347
.OutputToPipe(new StreamPipeSink(memoryStream), options => options.ForceFormat("mp3"))
48+
.CancellableThrough(TestContext.CancellationToken)
4449
.ProcessAsynchronously();
4550
}
4651

@@ -70,7 +75,7 @@ public void Image_AddAudio()
7075
}
7176

7277
[TestMethod]
73-
[Timeout(10000, CooperativeCancellation = true)]
78+
[Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)]
7479
public void Audio_ToAAC_Args_Pipe()
7580
{
7681
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
@@ -83,12 +88,13 @@ public void Audio_ToAAC_Args_Pipe()
8388
.FromPipeInput(audioSamplesSource)
8489
.OutputToFile(outputFile, false, opt => opt
8590
.WithAudioCodec(AudioCodec.Aac))
91+
.CancellableThrough(TestContext.CancellationToken)
8692
.ProcessSynchronously();
8793
Assert.IsTrue(success);
8894
}
8995

9096
[TestMethod]
91-
[Timeout(10000, CooperativeCancellation = true)]
97+
[Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)]
9298
public void Audio_ToLibVorbis_Args_Pipe()
9399
{
94100
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
@@ -101,12 +107,13 @@ public void Audio_ToLibVorbis_Args_Pipe()
101107
.FromPipeInput(audioSamplesSource)
102108
.OutputToFile(outputFile, false, opt => opt
103109
.WithAudioCodec(AudioCodec.LibVorbis))
110+
.CancellableThrough(TestContext.CancellationToken)
104111
.ProcessSynchronously();
105112
Assert.IsTrue(success);
106113
}
107114

108115
[TestMethod]
109-
[Timeout(10000, CooperativeCancellation = true)]
116+
[Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)]
110117
public async Task Audio_ToAAC_Args_Pipe_Async()
111118
{
112119
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
@@ -119,12 +126,13 @@ public async Task Audio_ToAAC_Args_Pipe_Async()
119126
.FromPipeInput(audioSamplesSource)
120127
.OutputToFile(outputFile, false, opt => opt
121128
.WithAudioCodec(AudioCodec.Aac))
129+
.CancellableThrough(TestContext.CancellationToken)
122130
.ProcessAsynchronously();
123131
Assert.IsTrue(success);
124132
}
125133

126134
[TestMethod]
127-
[Timeout(10000, CooperativeCancellation = true)]
135+
[Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)]
128136
public void Audio_ToAAC_Args_Pipe_ValidDefaultConfiguration()
129137
{
130138
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
@@ -137,12 +145,13 @@ public void Audio_ToAAC_Args_Pipe_ValidDefaultConfiguration()
137145
.FromPipeInput(audioSamplesSource)
138146
.OutputToFile(outputFile, false, opt => opt
139147
.WithAudioCodec(AudioCodec.Aac))
148+
.CancellableThrough(TestContext.CancellationToken)
140149
.ProcessSynchronously();
141150
Assert.IsTrue(success);
142151
}
143152

144153
[TestMethod]
145-
[Timeout(10000, CooperativeCancellation = true)]
154+
[Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)]
146155
public void Audio_ToAAC_Args_Pipe_InvalidChannels()
147156
{
148157
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
@@ -153,11 +162,12 @@ public void Audio_ToAAC_Args_Pipe_InvalidChannels()
153162
.FromPipeInput(audioSamplesSource)
154163
.OutputToFile(outputFile, false, opt => opt
155164
.WithAudioCodec(AudioCodec.Aac))
165+
.CancellableThrough(TestContext.CancellationToken)
156166
.ProcessSynchronously());
157167
}
158168

159169
[TestMethod]
160-
[Timeout(10000, CooperativeCancellation = true)]
170+
[Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)]
161171
public void Audio_ToAAC_Args_Pipe_InvalidFormat()
162172
{
163173
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
@@ -168,11 +178,12 @@ public void Audio_ToAAC_Args_Pipe_InvalidFormat()
168178
.FromPipeInput(audioSamplesSource)
169179
.OutputToFile(outputFile, false, opt => opt
170180
.WithAudioCodec(AudioCodec.Aac))
181+
.CancellableThrough(TestContext.CancellationToken)
171182
.ProcessSynchronously());
172183
}
173184

174185
[TestMethod]
175-
[Timeout(10000, CooperativeCancellation = true)]
186+
[Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)]
176187
public void Audio_ToAAC_Args_Pipe_InvalidSampleRate()
177188
{
178189
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
@@ -183,11 +194,12 @@ public void Audio_ToAAC_Args_Pipe_InvalidSampleRate()
183194
.FromPipeInput(audioSamplesSource)
184195
.OutputToFile(outputFile, false, opt => opt
185196
.WithAudioCodec(AudioCodec.Aac))
197+
.CancellableThrough(TestContext.CancellationToken)
186198
.ProcessSynchronously());
187199
}
188200

189201
[TestMethod]
190-
[Timeout(10000, CooperativeCancellation = true)]
202+
[Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)]
191203
public void Audio_Pan_ToMono()
192204
{
193205
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
@@ -196,6 +208,7 @@ public void Audio_Pan_ToMono()
196208
.OutputToFile(outputFile, true,
197209
argumentOptions => argumentOptions
198210
.WithAudioFilters(filter => filter.Pan(1, "c0 < 0.9 * c0 + 0.1 * c1")))
211+
.CancellableThrough(TestContext.CancellationToken)
199212
.ProcessSynchronously();
200213

201214
var mediaAnalysis = FFProbe.Analyse(outputFile);
@@ -206,7 +219,7 @@ public void Audio_Pan_ToMono()
206219
}
207220

208221
[TestMethod]
209-
[Timeout(10000, CooperativeCancellation = true)]
222+
[Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)]
210223
public void Audio_Pan_ToMonoNoDefinitions()
211224
{
212225
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
@@ -215,6 +228,7 @@ public void Audio_Pan_ToMonoNoDefinitions()
215228
.OutputToFile(outputFile, true,
216229
argumentOptions => argumentOptions
217230
.WithAudioFilters(filter => filter.Pan(1)))
231+
.CancellableThrough(TestContext.CancellationToken)
218232
.ProcessSynchronously();
219233

220234
var mediaAnalysis = FFProbe.Analyse(outputFile);
@@ -225,7 +239,7 @@ public void Audio_Pan_ToMonoNoDefinitions()
225239
}
226240

227241
[TestMethod]
228-
[Timeout(10000, CooperativeCancellation = true)]
242+
[Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)]
229243
public void Audio_Pan_ToMonoChannelsToOutputDefinitionsMismatch()
230244
{
231245
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
@@ -234,11 +248,12 @@ public void Audio_Pan_ToMonoChannelsToOutputDefinitionsMismatch()
234248
.OutputToFile(outputFile, true,
235249
argumentOptions => argumentOptions
236250
.WithAudioFilters(filter => filter.Pan(1, "c0=c0", "c1=c1")))
251+
.CancellableThrough(TestContext.CancellationToken)
237252
.ProcessSynchronously());
238253
}
239254

240255
[TestMethod]
241-
[Timeout(10000, CooperativeCancellation = true)]
256+
[Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)]
242257
public void Audio_Pan_ToMonoChannelsLayoutToOutputDefinitionsMismatch()
243258
{
244259
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
@@ -247,11 +262,12 @@ public void Audio_Pan_ToMonoChannelsLayoutToOutputDefinitionsMismatch()
247262
.OutputToFile(outputFile, true,
248263
argumentOptions => argumentOptions
249264
.WithAudioFilters(filter => filter.Pan("mono", "c0=c0", "c1=c1")))
265+
.CancellableThrough(TestContext.CancellationToken)
250266
.ProcessSynchronously());
251267
}
252268

253269
[TestMethod]
254-
[Timeout(10000, CooperativeCancellation = true)]
270+
[Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)]
255271
public void Audio_DynamicNormalizer_WithDefaultValues()
256272
{
257273
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
@@ -260,13 +276,14 @@ public void Audio_DynamicNormalizer_WithDefaultValues()
260276
.OutputToFile(outputFile, true,
261277
argumentOptions => argumentOptions
262278
.WithAudioFilters(filter => filter.DynamicNormalizer()))
279+
.CancellableThrough(TestContext.CancellationToken)
263280
.ProcessSynchronously();
264281

265282
Assert.IsTrue(success);
266283
}
267284

268285
[TestMethod]
269-
[Timeout(10000, CooperativeCancellation = true)]
286+
[Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)]
270287
public void Audio_DynamicNormalizer_WithNonDefaultValues()
271288
{
272289
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
@@ -275,13 +292,14 @@ public void Audio_DynamicNormalizer_WithNonDefaultValues()
275292
.OutputToFile(outputFile, true,
276293
argumentOptions => argumentOptions
277294
.WithAudioFilters(filter => filter.DynamicNormalizer(250, 7, 0.9, 2, 1, false, true, true, 0.5)))
295+
.CancellableThrough(TestContext.CancellationToken)
278296
.ProcessSynchronously();
279297

280298
Assert.IsTrue(success);
281299
}
282300

283301
[TestMethod]
284-
[Timeout(10000, CooperativeCancellation = true)]
302+
[Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)]
285303
[DataRow(2)]
286304
[DataRow(32)]
287305
[DataRow(8)]
@@ -294,6 +312,7 @@ public void Audio_DynamicNormalizer_FilterWindow(int filterWindow)
294312
.OutputToFile(outputFile, true,
295313
argumentOptions => argumentOptions
296314
.WithAudioFilters(filter => filter.DynamicNormalizer(filterWindow: filterWindow)))
315+
.CancellableThrough(TestContext.CancellationToken)
297316
.ProcessSynchronously());
298317
}
299318
}

FFMpegCore.Test/FFProbeTests.cs

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
using FFMpegCore.Test.Resources;
1+
using FFMpegCore.Exceptions;
2+
using FFMpegCore.Helpers;
3+
using FFMpegCore.Test.Resources;
24

35
namespace FFMpegCore.Test;
46

@@ -285,4 +287,68 @@ public void Probe_Success_Custom_Arguments()
285287
var info = FFProbe.Analyse(TestResources.Mp4Video, customArguments: "-headers \"Hello: World\"");
286288
Assert.AreEqual(3, info.Duration.Seconds);
287289
}
290+
291+
[TestMethod]
292+
[Timeout(10000, CooperativeCancellation = true)]
293+
public async Task Parallel_FFProbe_Cancellation_Should_Throw_Only_OperationCanceledException()
294+
{
295+
// Warm up FFMpegCore environment
296+
FFProbeHelper.VerifyFFProbeExists(GlobalFFOptions.Current);
297+
298+
var mp4 = TestResources.Mp4Video;
299+
if (!File.Exists(mp4))
300+
{
301+
Assert.Inconclusive($"Test video not found: {mp4}");
302+
return;
303+
}
304+
305+
using var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.CancellationToken);
306+
using var semaphore = new SemaphoreSlim(Environment.ProcessorCount, Environment.ProcessorCount);
307+
var tasks = Enumerable.Range(0, 50).Select(x => Task.Run(async () =>
308+
{
309+
await semaphore.WaitAsync(cts.Token);
310+
try
311+
{
312+
var analysis = await FFProbe.AnalyseAsync(mp4, cancellationToken: cts.Token);
313+
return analysis;
314+
}
315+
finally
316+
{
317+
semaphore.Release();
318+
}
319+
}, cts.Token)).ToList();
320+
321+
// Wait for 2 tasks to finish, then cancel all
322+
await Task.WhenAny(tasks);
323+
await Task.WhenAny(tasks);
324+
await cts.CancelAsync();
325+
326+
var exceptions = new List<Exception>();
327+
foreach (var task in tasks)
328+
{
329+
try
330+
{
331+
await task;
332+
}
333+
catch (Exception e)
334+
{
335+
exceptions.Add(e);
336+
}
337+
}
338+
339+
Assert.IsNotEmpty(exceptions, "No exceptions were thrown on cancellation. Test was useless. " +
340+
".Try adjust cancellation timings to make cancellation at the moment, when ffprobe is still running.");
341+
342+
// Check that all exceptions are OperationCanceledException
343+
CollectionAssert.AllItemsAreInstancesOfType(exceptions, typeof(OperationCanceledException));
344+
}
345+
346+
[TestMethod]
347+
[Timeout(10000, CooperativeCancellation = true)]
348+
public async Task FFProbe_Should_Throw_FFMpegException_When_Exits_With_Non_Zero_Code()
349+
{
350+
var input = TestResources.SrtSubtitle; //non media file
351+
await Assert.ThrowsAsync<FFMpegException>(async () => await FFProbe.AnalyseAsync(input,
352+
cancellationToken: TestContext.CancellationToken, customArguments: "--some-invalid-argument"));
353+
}
288354
}

0 commit comments

Comments
 (0)