3
$\begingroup$

The prevailing solutions seem to be:

  1. Use Export with ListAnimate (and similar solutions) which requires unrealistic amounts of memory even for short animations at 1080p. I suspect it rasterizes all the frames in memory before it begins encoding as well, which is even worse. Update: Its also far slower than using ffmpeg directly.
  2. Generate an Image file per frame and post-process into a video using ffmpeg. This does cap the memory use, but can use many GB of disk, and is also a lot less convenient than a simple function call.

Is there a better way in terms of convenience AND memory requirements?

$\endgroup$
1
  • $\begingroup$ You might be interested in the (experimental) VideoGenerator with a generator function. I think that in principle, that should follow your requirements. The biggest issue seems to be to get it to work reliably at the moment without the frontend hanging. $\endgroup$
    – Lukas Lang
    Commented Jul 29, 2021 at 7:25

1 Answer 1

3
$\begingroup$

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 single time argument and returns a Graphics[] object.
  • You supply times, which is a list of time values .
  • The function EncodeMyVideo launches ffmpeg as a subprocess. It then generates frames one at a time, by calling frameGraphicFun using the values in times. It rasterizes the frame Graphics[] and feeds it to ffmpeg 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 for ffmpeg to return.

... and the result is a video file.

The code comes with some caveats:

  1. 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).
  2. 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]
]
```
$\endgroup$
1
  • $\begingroup$ ffmpeg (or sometimes convert) is a solid option. For me saving individual frames and running ffmpeg directly from console works. Convenient frame name generation can be done with e.g. Table[StringTemplate["``_name.png"][IntegerString[i, 10, 3]], {i, 1, 10}], then run ffmpeg ... *name*png ... from console. $\endgroup$
    – I.M.
    Commented Jul 29, 2021 at 4:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.