diff --git a/pixmaprecorder.d b/pixmaprecorder.d index 79f8329..8ab1746 100644 --- a/pixmaprecorder.d +++ b/pixmaprecorder.d @@ -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 . + 2. 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; @@ -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, 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 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 { There’s 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))); +}