mirror of https://github.com/adamdruppe/arsd.git
Merge pull request #450 from analogjupiter/pixmap-recorder
Pixmap Recorder revision Ⅰ
This commit is contained in:
commit
cb76629376
179
pixmaprecorder.d
179
pixmaprecorder.d
|
@ -9,20 +9,41 @@
|
||||||
[FFmpeg](https://ffmpeg.org/about.html).
|
[FFmpeg](https://ffmpeg.org/about.html).
|
||||||
|
|
||||||
$(SIDEBAR
|
$(SIDEBAR
|
||||||
Piping frame data to an independent copy of FFmpeg enables this library
|
Piping frame data into an independent copy of FFmpeg
|
||||||
to be used with a wide range of verions of said third-party program
|
enables this library to be used with a wide range of versions of said
|
||||||
and (hopefully) helps to reduce the chances of breaking changes.
|
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
|
It also allows end-users to upgrade their possibilities by swapping the
|
||||||
accompanying copy FFmpeg.
|
accompanying copy FFmpeg.
|
||||||
|
|
||||||
This could be useful in cases where software distributors can only
|
This could be useful in cases where software distributors can only
|
||||||
provide limited functionality in their bundled binaries because of
|
provide limited functionality in their bundled binaries because of
|
||||||
legal requirements like patent licenses.
|
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
|
$(TIP
|
||||||
The value of the `outputFormat` parameter of the constructor overloads
|
The value of the `outputFormat` parameter of various constructor
|
||||||
is passed to FFmpeg via the `-f` option.
|
overloads is passed to FFmpeg via the `-f` (“format”) option.
|
||||||
|
|
||||||
Run `ffmpeg -formats` to get a list of available formats.
|
Run `ffmpeg -formats` to get a list of available formats.
|
||||||
)
|
)
|
||||||
|
@ -32,6 +53,58 @@
|
||||||
[PixmapRecorder.advancedFFmpegAdditionalOutputArgs|additional-output-args property].
|
[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 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.pixmaprecorder;
|
||||||
import arsd.pixmappaint;
|
import arsd.pixmappaint;
|
||||||
|
@ -72,6 +145,7 @@ import arsd.pixmappaint;
|
||||||
import std.format;
|
import std.format;
|
||||||
import std.path : buildPath;
|
import std.path : buildPath;
|
||||||
import std.process;
|
import std.process;
|
||||||
|
import std.range : isOutputRange, OutputRange;
|
||||||
import std.sumtype;
|
import std.sumtype;
|
||||||
import std.stdio : File;
|
import std.stdio : File;
|
||||||
|
|
||||||
|
@ -83,26 +157,24 @@ private @safe {
|
||||||
return stderr;
|
return stderr;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto stdoutFauxSafe() @trusted {
|
|
||||||
import std.stdio : stderr;
|
|
||||||
|
|
||||||
return stderr;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto stderr() {
|
auto stderr() {
|
||||||
return stderrFauxSafe;
|
return stderrFauxSafe;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto stdout() {
|
|
||||||
return stderrFauxSafe;
|
|
||||||
}
|
|
||||||
|
|
||||||
alias RecorderOutput = SumType!(string, File);
|
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 {
|
private {
|
||||||
string _ffmpegExecutablePath;
|
string _ffmpegExecutablePath;
|
||||||
|
@ -118,6 +190,8 @@ final class PixmapRecorder {
|
||||||
bool _outputIsOurs = false;
|
bool _outputIsOurs = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@safe:
|
||||||
|
|
||||||
private this(
|
private this(
|
||||||
string ffmpegExecutablePath,
|
string ffmpegExecutablePath,
|
||||||
double frameRate,
|
double frameRate,
|
||||||
|
@ -133,32 +207,34 @@ final class PixmapRecorder {
|
||||||
}
|
}
|
||||||
|
|
||||||
/++
|
/++
|
||||||
Prepares a recorder for encoding video frames
|
Prepares a recorder for encoding a video file into the provided pipe.
|
||||||
into the specified file pipe.
|
|
||||||
|
|
||||||
$(WARNING
|
$(WARNING
|
||||||
Certain formats cannot be produced in pipes by FFmpeg.
|
FFmpeg cannot produce certain formats in pipes.
|
||||||
Look out for error message like such:
|
Look out for error messages such as:
|
||||||
|
|
||||||
($BLOCKQUOTE
|
$(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).
|
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:
|
Params:
|
||||||
frameRate = Framerate of the video output; in frames per second.
|
frameRate = Framerate of the video output; in frames per second.
|
||||||
output = File handle to write the video output to.
|
output = File handle to write the video output to.
|
||||||
outputFormat = Video (container) format to output.
|
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.
|
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.
|
||||||
ffmpegExecutablePath = Path to the FFmpeg executable
|
ffmpegExecutablePath = Path to the FFmpeg executable
|
||||||
(e.g. `ffmpeg`, `ffmpeg.exe` or `/usr/bin/ffmpeg`).
|
(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(
|
public this(
|
||||||
double frameRate,
|
double frameRate,
|
||||||
|
@ -182,8 +258,14 @@ final class PixmapRecorder {
|
||||||
}
|
}
|
||||||
|
|
||||||
/++
|
/++
|
||||||
Prepares a recorder for encoding video frames
|
Prepares a recorder for encoding a video file
|
||||||
into a video file saved to the specified path.
|
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:
|
Params:
|
||||||
frameRate = Framerate of the video output; in frames per second.
|
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
|
FFmpeg will use this to autodetect the format
|
||||||
when no `outputFormat` is provided.
|
when no `outputFormat` is provided.
|
||||||
log = Target file for the stderr log output of FFmpeg.
|
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.
|
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
|
If `null`, the format is not provided and FFmpeg
|
||||||
will try to autodetect the format from the filename
|
will try to autodetect the format from the filename
|
||||||
of the `outputPath`.
|
of the `outputPath`.
|
||||||
ffmpegExecutablePath = Path to the FFmpeg executable
|
ffmpegExecutablePath = Path to the FFmpeg executable
|
||||||
(e.g. `ffmpeg`, `ffmpeg.exe` or `/usr/bin/ffmpeg`).
|
(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(
|
public this(
|
||||||
double frameRate,
|
double frameRate,
|
||||||
|
@ -215,8 +298,8 @@ final class PixmapRecorder {
|
||||||
in ((outputFormat is null) || outputFormat != "")
|
in ((outputFormat is null) || outputFormat != "")
|
||||||
in (ffmpegExecutablePath != "") {
|
in (ffmpegExecutablePath != "") {
|
||||||
|
|
||||||
// Sanitize output path
|
// Sanitize the output path
|
||||||
// if it would get confused with a command-line arg.
|
// if it were to get confused with a command-line arg.
|
||||||
// Otherwise a relative path like `-my.mkv` would make FFmpeg complain
|
// Otherwise a relative path like `-my.mkv` would make FFmpeg complain
|
||||||
// about an “Unrecognized option 'out.mkv'”.
|
// about an “Unrecognized option 'out.mkv'”.
|
||||||
if (outputPath[0] == '-') {
|
if (outputPath[0] == '-') {
|
||||||
|
@ -234,16 +317,16 @@ final class PixmapRecorder {
|
||||||
|
|
||||||
/++
|
/++
|
||||||
$(I Advanced users only:)
|
$(I Advanced users only:)
|
||||||
Additional command-line arguments passed to FFmpeg.
|
Additional command-line arguments to be passed to FFmpeg.
|
||||||
|
|
||||||
$(WARNING
|
$(WARNING
|
||||||
The values provided through this property function are not
|
The values provided through this property function are not
|
||||||
validated and passed verbatim to FFmpeg.
|
validated and passed verbatim to FFmpeg.
|
||||||
)
|
)
|
||||||
|
|
||||||
$(PITFAL
|
$(PITFALL
|
||||||
If code makes use of this and FFmpeg errors,
|
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) {
|
void advancedFFmpegAdditionalOutputArgs(string[] args) {
|
||||||
|
@ -258,6 +341,9 @@ final class PixmapRecorder {
|
||||||
return _input.writeEnd.isOpen;
|
return _input.writeEnd.isOpen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ditto
|
||||||
|
alias isRecording = isOpen;
|
||||||
|
|
||||||
private string[] buildFFmpegCommand() pure {
|
private string[] buildFFmpegCommand() pure {
|
||||||
// Build resolution as understood by FFmpeg.
|
// Build resolution as understood by FFmpeg.
|
||||||
const string resolutionString = format!"%sx%s"(
|
const string resolutionString = format!"%sx%s"(
|
||||||
|
@ -310,7 +396,7 @@ final class PixmapRecorder {
|
||||||
|
|
||||||
$(SIDEBAR
|
$(SIDEBAR
|
||||||
Variable/dynamic resolution is neither supported by this library
|
Variable/dynamic resolution is neither supported by this library
|
||||||
nor most real-world applications.
|
nor by most real-world applications.
|
||||||
)
|
)
|
||||||
|
|
||||||
$(NOTE
|
$(NOTE
|
||||||
|
@ -318,7 +404,7 @@ final class PixmapRecorder {
|
||||||
There’s usually no need to call this manually.
|
There’s usually no need to call this manually.
|
||||||
)
|
)
|
||||||
+/
|
+/
|
||||||
void open(Size resolution)
|
void open(const Size resolution)
|
||||||
in (!this.isOpen) {
|
in (!this.isOpen) {
|
||||||
// Save resolution for sanity checks.
|
// Save resolution for sanity checks.
|
||||||
_resolution = resolution;
|
_resolution = resolution;
|
||||||
|
@ -368,13 +454,13 @@ final class PixmapRecorder {
|
||||||
alias startRecording = close;
|
alias startRecording = close;
|
||||||
|
|
||||||
/++
|
/++
|
||||||
Provides the next video frame to encode.
|
Supplies the next frame to the video encoder.
|
||||||
|
|
||||||
$(TIP
|
$(TIP
|
||||||
This function automatically calls [open|open()] if necessary.
|
This function automatically calls [open|open()] if necessary.
|
||||||
)
|
)
|
||||||
+/
|
+/
|
||||||
void put(Pixmap frame) {
|
void put(const Pixmap frame) {
|
||||||
if (!this.isOpen) {
|
if (!this.isOpen) {
|
||||||
this.open(frame.size);
|
this.open(frame.size);
|
||||||
} else {
|
} else {
|
||||||
|
@ -384,6 +470,9 @@ final class PixmapRecorder {
|
||||||
_input.writeEnd.rawWrite(frame.data);
|
_input.writeEnd.rawWrite(frame.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ditto
|
||||||
|
alias record = put;
|
||||||
|
|
||||||
/++
|
/++
|
||||||
Ends the recording process.
|
Ends the recording process.
|
||||||
|
|
||||||
|
@ -396,7 +485,7 @@ final class PixmapRecorder {
|
||||||
+/
|
+/
|
||||||
int close() {
|
int close() {
|
||||||
if (!this.isOpen) {
|
if (!this.isOpen) {
|
||||||
return;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
_input.writeEnd.flush();
|
_input.writeEnd.flush();
|
||||||
|
@ -411,3 +500,9 @@ final class PixmapRecorder {
|
||||||
/// ditto
|
/// ditto
|
||||||
alias stopRecording = close;
|
alias stopRecording = close;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// self-test
|
||||||
|
private {
|
||||||
|
static assert(isOutputRange!(PixmapRecorder, Pixmap));
|
||||||
|
static assert(isOutputRange!(PixmapRecorder, const(Pixmap)));
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue