-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathProgram.cs
More file actions
207 lines (174 loc) · 7.71 KB
/
Copy pathProgram.cs
File metadata and controls
207 lines (174 loc) · 7.71 KB
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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
using System.Globalization;
using AppKit;
using AVFoundation;
using AVKit;
using CoreGraphics;
using Ffmpegkit.Mac;
using Foundation;
// The app's own root namespace starts with 'FFmpegKit', which would otherwise shadow the bound
// type - the same collision the iOS and Android samples document.
using FFmpeg = Ffmpegkit.Mac.FFmpegKit;
namespace FFmpegKit.Mac.Example;
/// <summary>
/// The macOS counterpart of the MAUI samples in the iOS and Android repositories: probe a bundled
/// clip, run one of three conversions with a live progress bar driven by the statistics callback,
/// support cancelling mid-run, and preview source and result side by side.
/// </summary>
public static class Program
{
private static void Main(string[] args)
{
NSApplication.Init();
NSApplication.SharedApplication.Delegate = new AppDelegate();
NSApplication.SharedApplication.Run();
}
}
public sealed class AppDelegate : NSApplicationDelegate
{
private NSWindow _window = null!;
private AVPlayerView _source = null!;
private AVPlayerView _result = null!;
private NSProgressIndicator _progress = null!;
private NSTextField _status = null!;
private NSButton _cancel = null!;
private NSButton[] _actions = null!;
private string _inputPath = null!;
private double _durationSeconds;
public override void DidFinishLaunching(NSNotification notification)
{
_inputPath = NSBundle.MainBundle.PathForResource("sample", "mp4")
?? throw new InvalidOperationException("sample.mp4 is missing from the bundle.");
BuildWindow();
_source.Player = AVPlayer.FromUrl(NSUrl.FromFilename(_inputPath));
// Statistics arrive on an FFmpegKit thread for whichever session is running; the probe
// below supplies the total duration that turns them into a percentage.
FFmpegKitConfig.EnableStatisticsCallback(statistics =>
{
if (_durationSeconds <= 0)
return;
var fraction = Math.Clamp(statistics.Time / 1000.0 / _durationSeconds, 0, 1);
InvokeOnMainThread(() =>
{
_progress.DoubleValue = fraction * 100;
_status.StringValue = $"{fraction:P0} frame {statistics.VideoFrameNumber} {statistics.Speed:0.##}x";
});
});
_ = ProbeAsync();
}
public override bool ApplicationShouldTerminateAfterLastWindowClosed(NSApplication sender) => true;
private async Task ProbeAsync()
{
var session = await FFprobeKit.GetMediaInformationAsync(_inputPath);
var information = session.MediaInformation;
// FFprobe reports the duration as an invariant-format string; parsing it with the
// ambient culture misreads it on a de-DE or fr-FR system.
if (information?.Duration is { } duration
&& double.TryParse(duration, NumberStyles.Float, CultureInfo.InvariantCulture, out var seconds))
{
_durationSeconds = seconds;
}
InvokeOnMainThread(() => _status.StringValue = information is null
? "probe failed"
: $"{information.Format} {_durationSeconds:0.#}s - pick a conversion");
}
private async Task RunAsync(string name, string arguments, string extension)
{
var output = Path.Combine(Path.GetTempPath(), $"ffmpegkit-example-{name}.{extension}");
File.Delete(output);
SetBusy(true);
_status.StringValue = $"running {name}…";
_progress.DoubleValue = 0;
try
{
// ExecuteAsync wraps FFmpegKit's own asynchronous path - nothing blocks the UI
// thread, and the Cancel button stops the running session via the static Cancel().
var session = await FFmpeg.ExecuteAsync($"-y -i \"{_inputPath}\" {arguments} \"{output}\"");
if (session.Succeeded())
{
_status.StringValue = $"{name}: done ({new FileInfo(output).Length / 1024} KB)";
_progress.DoubleValue = 100;
_result.Player = AVPlayer.FromUrl(NSUrl.FromFilename(output));
}
else
{
// A failed command explains itself in Output; its last line is FFmpeg's own
// error message.
var cancelled = session.ReturnCode?.IsValueCancel == true;
_status.StringValue = cancelled ? $"{name}: cancelled" : $"{name}: failed - {LastLine(session.Output)}";
}
}
finally
{
SetBusy(false);
}
}
private void Cancel()
{
// Static cancel stops every running session - there is at most one here.
FFmpeg.Cancel();
_status.StringValue = "cancelling…";
}
private static string LastLine(string? output) =>
output?.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) is { Length: > 0 } lines
? lines[^1]
: "(no output captured)";
private void SetBusy(bool busy)
{
foreach (var action in _actions)
action.Enabled = !busy;
_cancel.Enabled = busy;
}
private void BuildWindow()
{
_source = new AVPlayerView { ShowsFullScreenToggleButton = false };
_result = new AVPlayerView { ShowsFullScreenToggleButton = false };
var players = NSStackView.FromViews([_source, _result]);
players.Orientation = NSUserInterfaceLayoutOrientation.Horizontal;
players.Distribution = NSStackViewDistribution.FillEqually;
players.Spacing = 8;
_actions =
[
// mpeg4, not libx264: this sample references the Full (LGPL) variant, and x264 only
// exists in the -Gpl packages - requesting it here fails with "Unknown encoder".
Button("Resize 320x240", () => RunAsync("resize", "-vf scale=320:240 -c:v mpeg4 -an", "mp4")),
Button("Grayscale", () => RunAsync("grayscale", "-vf format=gray -c:v mpeg4 -an", "mp4")),
Button("Extract audio", () => RunAsync("audio", "-vn -acodec aac", "m4a")),
];
_cancel = NSButton.CreateButton("Cancel", Cancel);
_cancel.Enabled = false;
var buttons = NSStackView.FromViews([.. _actions, _cancel]);
buttons.Orientation = NSUserInterfaceLayoutOrientation.Horizontal;
buttons.Spacing = 8;
_progress = new NSProgressIndicator
{
Style = NSProgressIndicatorStyle.Bar,
Indeterminate = false,
MinValue = 0,
MaxValue = 100,
};
_status = NSTextField.CreateLabel("probing…");
var column = NSStackView.FromViews([players, buttons, _progress, _status]);
column.Orientation = NSUserInterfaceLayoutOrientation.Vertical;
column.Spacing = 10;
column.EdgeInsets = new NSEdgeInsets(12, 12, 12, 12);
_window = new NSWindow(
new CGRect(0, 0, 760, 420),
NSWindowStyle.Titled | NSWindowStyle.Closable | NSWindowStyle.Miniaturizable | NSWindowStyle.Resizable,
NSBackingStore.Buffered,
deferCreation: false)
{
Title = "FFmpegKit.Mac Example",
ContentView = column,
};
_window.Center();
_window.MakeKeyAndOrderFront(null);
// Activate() replaced ActivateIgnoringOtherApps(bool) in macOS 14; the sample's floor is
// 12.0, so call whichever the running OS supports rather than eat a CA1422 deprecation.
if (OperatingSystem.IsMacOSVersionAtLeast(14))
NSApplication.SharedApplication.Activate();
else
NSApplication.SharedApplication.ActivateIgnoringOtherApps(true);
}
private NSButton Button(string title, Func<Task> action) =>
NSButton.CreateButton(title, () => _ = action());
}