-
-
Notifications
You must be signed in to change notification settings - Fork 515
/
Copy pathFFmpeg.cs
140 lines (127 loc) · 4.69 KB
/
FFmpeg.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
using System;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using CliWrap;
using CliWrap.Exceptions;
using YoutubeExplode.Converter.Utils.Extensions;
namespace YoutubeExplode.Converter;
// Ideally this should use named pipes and stream through stdout.
// However, named pipes aren't well supported on non-Windows OS and
// stdout streaming only works with some specific formats.
internal partial class FFmpeg(string filePath)
{
public async ValueTask ExecuteAsync(
string arguments,
IProgress<double>? progress,
CancellationToken cancellationToken = default
)
{
var stdErrBuffer = new StringBuilder();
var stdErrPipe = PipeTarget.Merge(
// Collect error output in case of failure
PipeTarget.ToStringBuilder(stdErrBuffer),
// Collect progress output if requested
progress?.Pipe(CreateProgressRouter) ?? PipeTarget.Null
);
try
{
await Cli.Wrap(filePath)
.WithArguments(arguments)
.WithStandardErrorPipe(stdErrPipe)
.ExecuteAsync(cancellationToken);
}
catch (CommandExecutionException ex)
{
throw new InvalidOperationException(
$"""
FFmpeg command-line tool failed with an error.
Standard error:
{stdErrBuffer}
""",
ex
);
}
}
}
internal partial class FFmpeg
{
public static string GetFilePath() =>
// Try to find FFmpeg in the probe directory
Directory
.EnumerateFiles(AppContext.BaseDirectory ?? Directory.GetCurrentDirectory())
.FirstOrDefault(f =>
string.Equals(
Path.GetFileNameWithoutExtension(f),
"ffmpeg",
StringComparison.OrdinalIgnoreCase
)
)
// Otherwise fallback to just "ffmpeg" and hope it's on the PATH
?? "ffmpeg";
private static PipeTarget CreateProgressRouter(IProgress<double> progress)
{
var totalDuration = default(TimeSpan?);
return PipeTarget.ToDelegate(line =>
{
// Extract total stream duration
if (totalDuration is null)
{
// Need to extract all components separately because TimeSpan cannot directly
// parse a time string that is greater than 24 hours.
var totalDurationMatch = Regex.Match(line, @"Duration:\s(\d+):(\d+):(\d+\.\d+)");
if (totalDurationMatch.Success)
{
var hours = int.Parse(
totalDurationMatch.Groups[1].Value,
CultureInfo.InvariantCulture
);
var minutes = int.Parse(
totalDurationMatch.Groups[2].Value,
CultureInfo.InvariantCulture
);
var seconds = double.Parse(
totalDurationMatch.Groups[3].Value,
CultureInfo.InvariantCulture
);
totalDuration =
TimeSpan.FromHours(hours)
+ TimeSpan.FromMinutes(minutes)
+ TimeSpan.FromSeconds(seconds);
}
}
if (totalDuration is null || totalDuration == TimeSpan.Zero)
return;
// Extract processed stream duration
var processedDurationMatch = Regex.Match(line, @"time=(\d+):(\d+):(\d+\.\d+)");
if (processedDurationMatch.Success)
{
var hours = int.Parse(
processedDurationMatch.Groups[1].Value,
CultureInfo.InvariantCulture
);
var minutes = int.Parse(
processedDurationMatch.Groups[2].Value,
CultureInfo.InvariantCulture
);
var seconds = double.Parse(
processedDurationMatch.Groups[3].Value,
CultureInfo.InvariantCulture
);
var processedDuration =
TimeSpan.FromHours(hours)
+ TimeSpan.FromMinutes(minutes)
+ TimeSpan.FromSeconds(seconds);
progress.Report(
(
processedDuration.TotalMilliseconds / totalDuration.Value.TotalMilliseconds
).Clamp(0, 1)
);
}
});
}
}