From 2f32267898d78ce5a8ef7c2b064fc8f943915d62 Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Sun, 18 Aug 2024 01:16:48 +0200 Subject: [PATCH 1/7] Fix bogus return statement in PixmapRecorder.close() --- pixmaprecorder.d | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pixmaprecorder.d b/pixmaprecorder.d index 79f8329..011e972 100644 --- a/pixmaprecorder.d +++ b/pixmaprecorder.d @@ -396,7 +396,7 @@ final class PixmapRecorder { +/ int close() { if (!this.isOpen) { - return; + return 0; } _input.writeEnd.flush(); From 2a12df337dd64a3c446319fd7fa26ab6164f2073 Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Sun, 18 Aug 2024 01:17:49 +0200 Subject: [PATCH 2/7] Mark PixmapRecorder as Output Range Because why not? :P --- pixmaprecorder.d | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pixmaprecorder.d b/pixmaprecorder.d index 011e972..99103d0 100644 --- a/pixmaprecorder.d +++ b/pixmaprecorder.d @@ -72,6 +72,7 @@ import arsd.pixmappaint; import std.format; import std.path : buildPath; import std.process; +import std.range : OutputRange; import std.sumtype; import std.stdio : File; @@ -100,7 +101,7 @@ private @safe { alias RecorderOutput = SumType!(string, File); } -final class PixmapRecorder { +final class PixmapRecorder : OutputRange!Pixmap { @safe: From 1d39d3b61ed5bb447f8a44f83a119ab897b1f01d Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Sun, 18 Aug 2024 01:19:52 +0200 Subject: [PATCH 3/7] Remove unused stdout-getter from Pixmap Recorder Wasn't even properly implemented anyway and would have returned `stderr` instead. --- pixmaprecorder.d | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/pixmaprecorder.d b/pixmaprecorder.d index 99103d0..6df6043 100644 --- a/pixmaprecorder.d +++ b/pixmaprecorder.d @@ -84,20 +84,10 @@ 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); } From f432e7d744e47f22674ddf999921c542521aa456 Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Sun, 18 Aug 2024 02:11:34 +0200 Subject: [PATCH 4/7] Improve documentation of Pixmap Recorder --- pixmaprecorder.d | 87 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 70 insertions(+), 17 deletions(-) diff --git a/pixmaprecorder.d b/pixmaprecorder.d index 6df6043..b612c5b 100644 --- a/pixmaprecorder.d +++ b/pixmaprecorder.d @@ -9,20 +9,39 @@ [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 verions 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. ) $(TIP - The value of the `outputFormat` parameter of the constructor overloads - is passed to FFmpeg via the `-f` option. + 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 to search 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. ) @@ -32,6 +51,21 @@ [PixmapRecorder.advancedFFmpegAdditionalOutputArgs|additional-output-args property]. ) + ## 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; @@ -91,6 +125,16 @@ private @safe { 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 required. + +/ final class PixmapRecorder : OutputRange!Pixmap { @safe: @@ -124,20 +168,21 @@ final class PixmapRecorder : OutputRange!Pixmap { } /++ - 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 message such as: - ($BLOCKQUOTE + $(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 file path instead; - check out the other overloads of this constructor. + check out the other constructor overloads. ) Params: @@ -149,7 +194,8 @@ final class PixmapRecorder : OutputRange!Pixmap { 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, @@ -173,8 +219,14 @@ final class PixmapRecorder : OutputRange!Pixmap { } /++ - 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. @@ -191,7 +243,8 @@ final class PixmapRecorder : OutputRange!Pixmap { 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, @@ -232,7 +285,7 @@ final class PixmapRecorder : OutputRange!Pixmap { validated and passed verbatim to FFmpeg. ) - $(PITFAL + $(PITFALL If code makes use of this and FFmpeg errors, check the arguments provided here this first. ) @@ -359,7 +412,7 @@ final class PixmapRecorder : OutputRange!Pixmap { 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. From 8efa5c77205bdc2c6858107e3d0ead3d354aee2c Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Sun, 18 Aug 2024 02:40:18 +0200 Subject: [PATCH 5/7] Overhaul Pixmap Recorder --- pixmaprecorder.d | 63 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 57 insertions(+), 6 deletions(-) diff --git a/pixmaprecorder.d b/pixmaprecorder.d index b612c5b..2aae5f6 100644 --- a/pixmaprecorder.d +++ b/pixmaprecorder.d @@ -27,6 +27,8 @@ 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`. @@ -51,9 +53,46 @@ [PixmapRecorder.advancedFFmpegAdditionalOutputArgs|additional-output-args property]. ) - ## Examples + $(TIP + Combining this module with [arsd.pixmappresenter|Pixmap Presenter] + is really straightforward. - ### Getting started + In the most simplistic case, setup 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` @@ -106,7 +145,7 @@ import arsd.pixmappaint; import std.format; import std.path : buildPath; import std.process; -import std.range : OutputRange; +import std.range : isOutputRange, OutputRange; import std.sumtype; import std.stdio : File; @@ -135,7 +174,7 @@ private @safe { FFmpeg will render an actual video file from the frame data. This uses the CLI version of FFmpeg, no linking required. +/ -final class PixmapRecorder : OutputRange!Pixmap { +final class PixmapRecorder : OutputRange!(const(Pixmap)) { @safe: @@ -302,6 +341,9 @@ final class PixmapRecorder : OutputRange!Pixmap { return _input.writeEnd.isOpen; } + /// ditto + alias isRecording = isOpen; + private string[] buildFFmpegCommand() pure { // Build resolution as understood by FFmpeg. const string resolutionString = format!"%sx%s"( @@ -362,7 +404,7 @@ final class PixmapRecorder : OutputRange!Pixmap { 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; @@ -418,7 +460,7 @@ final class PixmapRecorder : OutputRange!Pixmap { 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 { @@ -428,6 +470,9 @@ final class PixmapRecorder : OutputRange!Pixmap { _input.writeEnd.rawWrite(frame.data); } + /// ditto + alias record = put; + /++ Ends the recording process. @@ -455,3 +500,9 @@ final class PixmapRecorder : OutputRange!Pixmap { /// ditto alias stopRecording = close; } + +// self-test +private { + static assert(isOutputRange!(PixmapRecorder, Pixmap)); + static assert(isOutputRange!(PixmapRecorder, const(Pixmap))); +} From 997a7c8fd5dbe8c7696c28636e429ad7cb66589f Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Sun, 18 Aug 2024 02:59:28 +0200 Subject: [PATCH 6/7] Fix spelling and grammar --- pixmaprecorder.d | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/pixmaprecorder.d b/pixmaprecorder.d index 2aae5f6..19a5dab 100644 --- a/pixmaprecorder.d +++ b/pixmaprecorder.d @@ -10,7 +10,7 @@ $(SIDEBAR Piping frame data into an independent copy of FFmpeg - enables this library to be used with a wide range of verions of said + 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. @@ -35,7 +35,7 @@ It defaults to `ffmpeg`; this will trigger the usual lookup procedures of the system the application runs on. - On POSIX this usually means to search for FFmpeg in the directories + 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. @@ -57,7 +57,7 @@ Combining this module with [arsd.pixmappresenter|Pixmap Presenter] is really straightforward. - In the most simplistic case, setup a [PixmapRecorder] before running + In the most simplistic case, set up a [PixmapRecorder] before running the presenter. Then call [PixmapRecorder.put|pixmapRecorder.record(presenter.framebuffer)] @@ -172,7 +172,7 @@ private @safe { 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 required. + This uses the CLI version of FFmpeg, no linking is required. +/ final class PixmapRecorder : OutputRange!(const(Pixmap)) { @@ -211,16 +211,16 @@ final class PixmapRecorder : OutputRange!(const(Pixmap)) { $(WARNING FFmpeg cannot produce certain formats in pipes. - Look out for error message such as: + Look out for error messages such as: $(BLOCKQUOTE - `[mp4 @ 0xdead1337beef] muxer does not support non seekable output` + `[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 file path instead; + Let FFmpeg output the video to the file path instead; check out the other constructor overloads. ) @@ -228,7 +228,7 @@ final class PixmapRecorder : OutputRange!(const(Pixmap)) { 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 @@ -274,9 +274,9 @@ final class PixmapRecorder : OutputRange!(const(Pixmap)) { 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`. @@ -298,8 +298,8 @@ final class PixmapRecorder : OutputRange!(const(Pixmap)) { 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] == '-') { @@ -317,7 +317,7 @@ final class PixmapRecorder : OutputRange!(const(Pixmap)) { /++ $(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 @@ -326,7 +326,7 @@ final class PixmapRecorder : OutputRange!(const(Pixmap)) { $(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) { @@ -396,7 +396,7 @@ final class PixmapRecorder : OutputRange!(const(Pixmap)) { $(SIDEBAR Variable/dynamic resolution is neither supported by this library - nor most real-world applications. + nor by most real-world applications. ) $(NOTE From 8a68748bd60d415bab81c572610e4a46ee1e6439 Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Sun, 18 Aug 2024 03:39:20 +0200 Subject: [PATCH 7/7] Adjust placement of @safe for better readability --- pixmaprecorder.d | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pixmaprecorder.d b/pixmaprecorder.d index 19a5dab..8ab1746 100644 --- a/pixmaprecorder.d +++ b/pixmaprecorder.d @@ -176,8 +176,6 @@ private @safe { +/ final class PixmapRecorder : OutputRange!(const(Pixmap)) { -@safe: - private { string _ffmpegExecutablePath; double _frameRate; @@ -192,6 +190,8 @@ final class PixmapRecorder : OutputRange!(const(Pixmap)) { bool _outputIsOurs = false; } +@safe: + private this( string ffmpegExecutablePath, double frameRate,