Skip to main content
9 of 9
added 148 characters in body

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]
]
```