Here is a function for doing this on Linux (and maybe Mac).
For my case this method uses a reasonable amount of memory, even for high-resolution, high-framecount animations. It's also significantly faster than using Export (6x in one test case).
How it works
- You make sure the
ffmpeg
executable is available on the path. - You supply
frameGraphicFun
, a frame generator function which accepts a singletime
argument and returns aGraphics[]
object. - You supply
times
, which is a list of time values . - The function
EncodeMyVideo
launchesffmpeg
as a subprocess. It then generates frames one at a time, by callingframeGraphicFun
using the values intimes
. It rasterizes the frameGraphics[]
and feeds it toffmpeg
using a fifo/named pipe (here's where this is *nix-dependent). - When all frames have been sent to
ffmpeg
, it closes the fifo and waits forffmpeg
to return.
... and the result is a video file.
The code comes with some caveats:
- Tested on linux only. It may work as-is on Mac. It almost certainly does not work on windows (though it probably could be made to).
- The encoding parameters are a matter of personal preference. In particular, I choose to force periodic keyframes even though that results in a substantially larger file. The advantage is that the extra keyframes make seeking much smoother. Which I find useful when I manually "scrub" through portions of a mathematical animation (and I often do).
My thanks to [1] for guidance.
[1] https://stackoverflow.com/questions/51903888/is-it-possible-to-send-ffmpeg-images-by-using-pipe
Implementation
EncodeMyVideo[frameGraphicFun_, times_, outputFile_, rasterWidth_,
fps_ : 24, keyframeInterval_ : 24, videoBitrate_ : "2500k",
batchSize_ : Max[$ProcessorCount, Min[16, $ProcessorCount*4]]] :=
Block[{
fifoName =
FileNameJoin[{$TemporaryDirectory, "fifo." <> CreateUUID[]}],
rasterHeight =
ImageAspectRatio[frameGraphicFun[times[[1]]]]*rasterWidth // N //
Ceiling,
templateCtx,
templateBody,
stdout, cmd, process, bst, i = 1, progressMessage, messageCell,
result
},
templateCtx =
<|
"width" -> ToString[rasterWidth],
"height" -> ToString[rasterHeight],
"fps" -> ToString[fps],
"fifoName" -> fifoName,
"keyframeInterval" -> keyframeInterval,
"videoBitrate" -> videoBitrate
|>;
templateBody =
"ffmpeg -hide_banner -nostats -loglevel 24 -y -f rawvideo -pix_fmt \
rgb24 -s `width`x`height` -r `fps` -i `fifoName` -c:v libx264 -b:v \
`videoBitrate` -x264-params keyint=`keyframeInterval` -an";
RunProcess[{"mkfifo", fifoName}];
WithCleanup[
cmd = (TemplateApply[templateBody, templateCtx] // StringSplit)~
Join~{outputFile};
process = StartProcess[cmd];
bst = OpenWrite[fifoName, BinaryFormat -> True];
progressMessage =
"Encoding Frame " <> ToString[i] <> "/" <> ToString[Length[times]];
messageCell = PrintTemporary[Dynamic[progressMessage]];
Scan[(
frames =
ParallelMap[
Rasterize[frameGraphicFun[#],
RasterSize -> {rasterWidth, rasterHeight}] &, #];
(*frames =Rasterize[frameGraphicFun[#],
RasterSize\[Rule]{rasterWidth,rasterHeight}]&/@#;*)
Scan[(
progressMessage =
"Encoding Frame " <> ToString[i++] <> "/" <>
ToString[Length[times]];
BinaryWrite[bst, ImageData[#, "Byte"]];
) &, frames];
) &, Partition[times, UpTo[batchSize]]];
NotebookDelete[messageCell];
ProcessStatus[process];
,
Close[bst], RunProcess[{"rm", fifoName}]];
progressMessage =
"Waiting for ffmpeg to return... " <> ProcessStatus[process];
messageCell = PrintTemporary[Dynamic[progressMessage]];
While[ProcessStatus[process] == "Running",
progressMessage =
"Waiting for ffmpeg to return... " <> ProcessStatus[process];
Pause[0.25]];
NotebookDelete[messageCell];
result = ReadString[ProcessConnection[process, "StandardError"]];
If [result == EndOfFile, "Encoding Completed Successfully!",
"Error:\n" <> result]
]
Usage
generateFrameGraphic[t_] :=
Plot[Sin[x t], {x, 0, 2 \[Pi]}, PlotRange -> {-2, 2}];
Block[{fps = 24, duration = 5, tStart = 0, tEnd = 1, videoWidth = 640,
times},
times = Range[tStart, tEnd, (tEnd - tStart)/(duration*fps)];
EncodeMyVideo[generateFrameGraphic, times, "sinAnimation.mp4",
videoWidth, fps]
]
```