-
Notifications
You must be signed in to change notification settings - Fork 128
/
pixmaprecorder.d
514 lines (430 loc) · 12.6 KB
/
pixmaprecorder.d
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
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
/+
== pixmaprecorder ==
Copyright Elias Batek (0xEAB) 2024.
Distributed under the Boost Software License, Version 1.0.
+/
/++
$(B Pixmap Recorder) is an auxiliary library for rendering video files from
[arsd.pixmappaint.Pixmap|Pixmap] frames by piping them to
[FFmpeg](https://ffmpeg.org/about.html).
$(SIDEBAR
Piping frame data into an independent copy of FFmpeg
enables this library to be used with a wide range of versions of said
third-party program
and (hopefully) helps to reduce the potential for breaking changes.
It also allows end-users to upgrade their possibilities by swapping the
accompanying copy FFmpeg.
This could be useful in cases where software distributors can only
provide limited functionality in their bundled binaries because of
legal requirements like patent licenses.
Keep in mind, support for more formats can be added to FFmpeg by
linking it against external libraries; such can also come with
additional distribution requirements that must be considered.
These things might be perceived as extra burdens and can make their
inclusion a matter of viability for distributors.
)
### Tips and tricks
$(TIP
The FFmpeg binary to be used can be specified by the optional
constructor parameter `ffmpegExecutablePath`.
It defaults to `ffmpeg`; this will trigger the usual lookup procedures
of the system the application runs on.
On POSIX this usually means searching for FFmpeg in the directories
specified by the environment variable PATH.
On Windows it will also look for an executable file with that name in
the current working directory.
)
$(TIP
The value of the `outputFormat` parameter of various constructor
overloads is passed to FFmpeg via the `-f` (“format”) option.
Run `ffmpeg -formats` to get a list of available formats.
)
$(TIP
To pass additional options to FFmpeg, use the
[PixmapRecorder.advancedFFmpegAdditionalOutputArgs|additional-output-args property].
)
$(TIP
Combining this module with [arsd.pixmappresenter|Pixmap Presenter]
is really straightforward.
In the most simplistic case, set up a [PixmapRecorder] before running
the presenter.
Then call
[PixmapRecorder.put|pixmapRecorder.record(presenter.framebuffer)]
at the end of the drawing callback in the eventloop.
---
auto recorder = new PixmapRecorder(60, /* … */);
scope(exit) {
const recorderStatus = recorder.stopRecording();
}
return presenter.eventLoop(delegate() {
// […]
recorder.record(presenter.framebuffer);
return LoopCtrl.redrawIn(16);
});
---
)
$(TIP
To use this module with [arsd.color] (which includes the image file
loading functionality provided by other arsd modules),
convert the
[arsd.color.TrueColorImage|TrueColorImage] or
[arsd.color.MemoryImage|MemoryImage] to a
[arsd.pixmappaint.Pixmap|Pixmap] first by calling
[arsd.pixmappaint.Pixmap.fromTrueColorImage|Pixmap.fromTrueColorImage()]
or
[arsd.pixmappaint.Pixmap.fromMemoryImage|Pixmap.fromMemoryImage()]
respectively.
)
### Examples
#### Getting started
$(NUMBERED_LIST
* Install FFmpeg (the CLI version).
$(LIST
* Debian derivatives (with FFmpeg in their repos): `apt install ffmpeg`
* Homebew: `brew install ffmpeg`
* Chocolatey: `choco install ffmpeg`
* Links to pre-built binaries can be found on <https://ffmpeg.org/download.html>.
)
* Determine where you’ve installed FFmpeg to.
Ideally, it’s somewhere within “PATH” so it can be run from the
command-line by just doing `ffmpeg`.
Otherwise, you’ll need the specific path to the executable to pass it
to the constructor of [PixmapRecorder].
)
---
import arsd.pixmaprecorder;
import arsd.pixmappaint;
/++
This demo renders a 1280×720 video at 30 FPS
fading from white (#FFF) to blue (#00F).
+/
int main() {
// Instantiate a recorder.
auto recorder = new PixmapRecorder(
30, // Video framerate [=FPS]
"out.mkv", // Output path to write the video file to.
);
// We will use this framebuffer later on to provide image data
// to the encoder.
auto frame = Pixmap(1280, 720);
for (int light = 0xFF; light >= 0; --light) {
auto color = Color(light, light, 0xFF);
frame.clear(color);
// Record the current frame.
// The video resolution to use is derived from the first frame.
recorder.put(frame);
}
// End and finalize the recording process.
return recorder.stopRecording();
}
---
+/
module arsd.pixmaprecorder;
import arsd.pixmappaint;
import std.format;
import std.path : buildPath;
import std.process;
import std.range : isOutputRange, OutputRange;
import std.sumtype;
import std.stdio : File;
private @safe {
auto stderrFauxSafe() @trusted {
import std.stdio : stderr;
return stderr;
}
auto stderr() {
return stderrFauxSafe;
}
alias RecorderOutput = SumType!(string, File);
}
/++
Video file encoder
Feed in video data frame by frame to encode video files
in one of the various formats supported by FFmpeg.
This is a convenience wrapper for piping pixmaps into FFmpeg.
FFmpeg will render an actual video file from the frame data.
This uses the CLI version of FFmpeg, no linking is required.
+/
final class PixmapRecorder : OutputRange!(const(Pixmap)) {
private {
string _ffmpegExecutablePath;
double _frameRate;
string _outputFormat;
RecorderOutput _output;
File _log;
string[] _outputAdditionalArgs;
Pid _pid;
Pipe _input;
Size _resolution;
bool _outputIsOurs = false;
}
@safe:
private this(
string ffmpegExecutablePath,
double frameRate,
string outputFormat,
RecorderOutput output,
File log,
) {
_ffmpegExecutablePath = ffmpegExecutablePath;
_frameRate = frameRate;
_outputFormat = outputFormat;
_output = output;
_log = log;
}
/++
Prepares a recorder for encoding a video file into the provided pipe.
$(WARNING
FFmpeg cannot produce certain formats in pipes.
Look out for error messages such as:
$(BLOCKQUOTE
`[mp4 @ 0xdead1337beef] muxer does not support non-seekable output`
)
This is not a limitation of this library (but rather one of FFmpeg).
Nevertheless, it’s still possible to use the affected formats.
Let FFmpeg output the video to the file path instead;
check out the other constructor overloads.
)
Params:
frameRate = Framerate of the video output; in frames per second.
output = File handle to write the video output to.
outputFormat = Video (container) format to output.
This value is passed to FFmpeg via the `-f` option.
log = Target file for the stderr log output of FFmpeg.
This is where error messages are written to.
ffmpegExecutablePath = Path to the FFmpeg executable
(e.g. `ffmpeg`, `ffmpeg.exe` or `/usr/bin/ffmpeg`).
$(COMMENT Keep this table in sync with the ones of other overloads.)
+/
public this(
double frameRate,
File output,
string outputFormat,
File log = stderr,
string ffmpegExecutablePath = "ffmpeg",
)
in (frameRate > 0)
in (output.isOpen)
in (outputFormat != "")
in (log.isOpen)
in (ffmpegExecutablePath != "") {
this(
ffmpegExecutablePath,
frameRate,
outputFormat,
RecorderOutput(output),
log,
);
}
/++
Prepares a recorder for encoding a video file
saved to the specified path.
$(TIP
This allows FFmpeg to seek through the output file
and enables the creation of file formats otherwise not supported
when using piped output.
)
Params:
frameRate = Framerate of the video output; in frames per second.
outputPath = File path to write the video output to.
Existing files will be overwritten.
FFmpeg will use this to autodetect the format
when no `outputFormat` is provided.
log = Target file for the stderr log output of FFmpeg.
This is where error messages are written to, as well.
outputFormat = Video (container) format to output.
This value is passed to FFmpeg via the `-f` option.
If `null`, the format is not provided and FFmpeg
will try to autodetect the format from the filename
of the `outputPath`.
ffmpegExecutablePath = Path to the FFmpeg executable
(e.g. `ffmpeg`, `ffmpeg.exe` or `/usr/bin/ffmpeg`).
$(COMMENT Keep this table in sync with the ones of other overloads.)
+/
public this(
double frameRate,
string outputPath,
File log = stderr,
string outputFormat = null,
string ffmpegExecutablePath = "ffmpeg",
)
in (frameRate > 0)
in ((outputPath != "") && (outputPath != "-"))
in (log.isOpen)
in ((outputFormat is null) || outputFormat != "")
in (ffmpegExecutablePath != "") {
// Sanitize the output path
// if it were to get confused with a command-line arg.
// Otherwise a relative path like `-my.mkv` would make FFmpeg complain
// about an “Unrecognized option 'out.mkv'”.
if (outputPath[0] == '-') {
outputPath = buildPath(".", outputPath);
}
this(
ffmpegExecutablePath,
frameRate,
null,
RecorderOutput(outputPath),
log,
);
}
/++
$(I Advanced users only:)
Additional command-line arguments to be passed to FFmpeg.
$(WARNING
The values provided through this property function are not
validated and passed verbatim to FFmpeg.
)
$(PITFALL
If code makes use of this and FFmpeg errors,
check the arguments provided here first.
)
+/
void advancedFFmpegAdditionalOutputArgs(string[] args) {
_outputAdditionalArgs = args;
}
/++
Determines whether the recorder is active
(which implies that an output file is open).
+/
bool isOpen() {
return _input.writeEnd.isOpen;
}
/// ditto
alias isRecording = isOpen;
private string[] buildFFmpegCommand() pure {
// Build resolution as understood by FFmpeg.
const string resolutionString = format!"%sx%s"(
_resolution.width,
_resolution.height,
);
// Convert framerate to string.
const string frameRateString = format!"%s"(_frameRate);
// Build command-line argument list.
auto cmd = [
_ffmpegExecutablePath,
"-y",
"-r",
frameRateString,
"-f",
"rawvideo",
"-pix_fmt",
"rgba",
"-s",
resolutionString,
"-i",
"-",
];
if (_outputFormat !is null) {
cmd ~= "-f";
cmd ~= _outputFormat;
}
if (_outputAdditionalArgs.length > 0) {
cmd = cmd ~ _outputAdditionalArgs;
}
cmd ~= _output.match!(
(string filePath) => filePath,
(ref File file) => "-",
);
return cmd;
}
/++
Starts the video encoding process.
Launches FFmpeg.
This function sets the video resolution for the encoding process.
All frames to record must match it.
$(SIDEBAR
Variable/dynamic resolution is neither supported by this library
nor by most real-world applications.
)
$(NOTE
This function is called by [put|put()] automatically.
There’s usually no need to call this manually.
)
+/
void open(const Size resolution)
in (!this.isOpen) {
// Save resolution for sanity checks.
_resolution = resolution;
const string[] cmd = buildFFmpegCommand();
// Prepare arsd → FFmpeg I/O pipe.
_input = pipe();
// Launch FFmpeg.
const processConfig = (
Config.suppressConsole
| Config.newEnv
);
// dfmt off
_pid = _output.match!(
delegate(string filePath) {
auto stdout = pipe();
stdout.readEnd.close();
return spawnProcess(
cmd,
_input.readEnd,
stdout.writeEnd,
_log,
null,
processConfig,
);
},
delegate(File file) {
auto stdout = pipe();
stdout.readEnd.close();
return spawnProcess(
cmd,
_input.readEnd,
file,
_log,
null,
processConfig,
);
}
);
// dfmt on
}
/// ditto
alias startRecording = close;
/++
Supplies the next frame to the video encoder.
$(TIP
This function automatically calls [open|open()] if necessary.
)
+/
void put(const Pixmap frame) @trusted {
if (!this.isOpen) {
this.open(frame.size);
} else {
assert(frame.size == _resolution, "Variable resolutions are not supported.");
}
_input.writeEnd.rawWrite(frame.data);
}
/// ditto
alias record = put;
/++
Ends the recording process.
$(NOTE
Waits for the FFmpeg process to exit in a blocking way.
)
Returns:
The status code provided by the FFmpeg program.
+/
int close() {
if (!this.isOpen) {
return 0;
}
_input.writeEnd.flush();
_input.writeEnd.close();
scope (exit) {
_input.close();
}
return wait(_pid);
}
/// ditto
alias stopRecording = close;
}
// self-test
private {
static assert(isOutputRange!(PixmapRecorder, Pixmap));
static assert(isOutputRange!(PixmapRecorder, const(Pixmap)));
}