Merge pull request from analogjupiter/pixmap-recorder

Pixmap Recorder revision Ⅰ
This commit is contained in:
Adam D. Ruppe 2024-08-18 12:24:49 -04:00 committed by GitHub
commit cb76629376
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 137 additions and 42 deletions

View File

@ -9,20 +9,41 @@
[FFmpeg](https://ffmpeg.org/about.html).
$(SIDEBAR
Piping frame data to an independent copy of FFmpeg enables this library
to be used with a wide range of verions of said third-party program
and (hopefully) helps to reduce the chances of breaking changes.
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 the constructor overloads
is passed to FFmpeg via the `-f` option.
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.
)
@ -32,6 +53,58 @@
[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
1. Install FFmpeg (the CLI version).
- 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>.
2. Determine where youve installed FFmpeg to.
Ideally, its somewhere within PATH so it can be run from the
command-line by just doing `ffmpeg`.
Otherwise, youll need the specific path to the executable to pass it
to the constructor of [PixmapRecorder].
---
import arsd.pixmaprecorder;
import arsd.pixmappaint;
@ -72,6 +145,7 @@ 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;
@ -83,26 +157,24 @@ private @safe {
return stderr;
}
auto stdoutFauxSafe() @trusted {
import std.stdio : stderr;
return stderr;
}
auto stderr() {
return stderrFauxSafe;
}
auto stdout() {
return stderrFauxSafe;
}
alias RecorderOutput = SumType!(string, File);
}
final class PixmapRecorder {
/++
Video file encoder
@safe:
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;
@ -118,6 +190,8 @@ final class PixmapRecorder {
bool _outputIsOurs = false;
}
@safe:
private this(
string ffmpegExecutablePath,
double frameRate,
@ -133,32 +207,34 @@ final class PixmapRecorder {
}
/++
Prepares a recorder for encoding video frames
into the specified file pipe.
Prepares a recorder for encoding a video file into the provided pipe.
$(WARNING
Certain formats cannot be produced in pipes by FFmpeg.
Look out for error message like such:
FFmpeg cannot produce certain formats in pipes.
Look out for error messages such as:
($BLOCKQUOTE
`[mp4 @ 0xdead1337beef] muxer does not support non seekable output`
$(BLOCKQUOTE
`[mp4 @ 0xdead1337beef] muxer does not support non-seekable output`
)
This is not a limitation of this library (but rather one of FFmpeg).
Let FFmpeg output the video to file path instead;
check out the other overloads of this constructor.
Nevertheless, its 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 is value passed to FFmpeg via the `-f` option.
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`).
/* Keep this table in sync with the ones of other overloads. */
$(COMMENT Keep this table in sync with the ones of other overloads.)
+/
public this(
double frameRate,
@ -182,8 +258,14 @@ final class PixmapRecorder {
}
/++
Prepares a recorder for encoding video frames
into a video file saved to the specified path.
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.
@ -192,15 +274,16 @@ final class PixmapRecorder {
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.
This is where error messages are written to, as well.
outputFormat = Video (container) format to output.
This is value passed to FFmpeg via the `-f` option.
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`).
/* Keep this table in sync with the ones of other overloads. */
$(COMMENT Keep this table in sync with the ones of other overloads.)
+/
public this(
double frameRate,
@ -215,8 +298,8 @@ final class PixmapRecorder {
in ((outputFormat is null) || outputFormat != "")
in (ffmpegExecutablePath != "") {
// Sanitize output path
// if it would get confused with a command-line arg.
// 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] == '-') {
@ -234,16 +317,16 @@ final class PixmapRecorder {
/++
$(I Advanced users only:)
Additional command-line arguments passed to FFmpeg.
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.
)
$(PITFAL
$(PITFALL
If code makes use of this and FFmpeg errors,
check the arguments provided here this first.
check the arguments provided here first.
)
+/
void advancedFFmpegAdditionalOutputArgs(string[] args) {
@ -258,6 +341,9 @@ final class PixmapRecorder {
return _input.writeEnd.isOpen;
}
/// ditto
alias isRecording = isOpen;
private string[] buildFFmpegCommand() pure {
// Build resolution as understood by FFmpeg.
const string resolutionString = format!"%sx%s"(
@ -310,7 +396,7 @@ final class PixmapRecorder {
$(SIDEBAR
Variable/dynamic resolution is neither supported by this library
nor most real-world applications.
nor by most real-world applications.
)
$(NOTE
@ -318,7 +404,7 @@ final class PixmapRecorder {
Theres usually no need to call this manually.
)
+/
void open(Size resolution)
void open(const Size resolution)
in (!this.isOpen) {
// Save resolution for sanity checks.
_resolution = resolution;
@ -368,13 +454,13 @@ final class PixmapRecorder {
alias startRecording = close;
/++
Provides the next video frame to encode.
Supplies the next frame to the video encoder.
$(TIP
This function automatically calls [open|open()] if necessary.
)
+/
void put(Pixmap frame) {
void put(const Pixmap frame) {
if (!this.isOpen) {
this.open(frame.size);
} else {
@ -384,6 +470,9 @@ final class PixmapRecorder {
_input.writeEnd.rawWrite(frame.data);
}
/// ditto
alias record = put;
/++
Ends the recording process.
@ -396,7 +485,7 @@ final class PixmapRecorder {
+/
int close() {
if (!this.isOpen) {
return;
return 0;
}
_input.writeEnd.flush();
@ -411,3 +500,9 @@ final class PixmapRecorder {
/// ditto
alias stopRecording = close;
}
// self-test
private {
static assert(isOutputRange!(PixmapRecorder, Pixmap));
static assert(isOutputRange!(PixmapRecorder, const(Pixmap)));
}