Skip to main content
added 148 characters in body
Source Link

Here is a function for doing this on Linux (and maybe Mac). I've looked around and I haven't seen any previous example of how to do this in MMA. Perhaps the comments will enlighten me as to prior work.

for my case this not only uses far less memory, it's also about 6x faster than Export["foo.mp4",frames,VideoEncoding -> "LIBX264",]. YMMV.

The function below accepts a (pure/stateless) function f: Any -> Graphics together withFor my case this method uses a listreasonable amount of values timesmemory, even for high-resolution, high-framecount animations. It spawns an ffmpeg subprocessIt's also significantly faster than using Export (ffmpeg should be on the path), and then proceeds to loop over the values6x in timesone test case). For each value, it calls f to generate a frame, which it then rasterizes and feeds to the ffmpeg process via a fifo/named pipe

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.

. This way, there's at most one frame in memory at a time. When all frames have been fed to ffmpeg,. and the fiforesult is closed, which signals ffmpeg to conclude its businessa video file.

The code currently suffers fromcomes with some caveats:

  1. It's been testedTested on linux only. It may work as-is on Mac. It almost certainly does not work as-is on windows (though it probably could be made to).
  2. The encoding parameters are a matter of course debatablepersonal preference. In particular, I preferchoose to force periodic keyframes because,even though it makes the filethat results in a substantially larger, it makes "scrubbing" smoother when using my media player file. I tend to doThe advantage is that a lot with technical animationsthe extra keyframes make seeking much smoother.
  3. The code isn't particularly elegant Which I find useful when I manually "scrub" through portions of a mathematical animation (and I often do).

Here is a function for doing this on Linux (and maybe Mac). I've looked around and I haven't seen any previous example of how to do this in MMA. Perhaps the comments will enlighten me as to prior work.

for my case this not only uses far less memory, it's also about 6x faster than Export["foo.mp4",frames,VideoEncoding -> "LIBX264",]. YMMV.

The function below accepts a (pure/stateless) function f: Any -> Graphics together with a list of values times. It spawns an ffmpeg subprocess (ffmpeg should be on the path), and then proceeds to loop over the values in times. For each value, it calls f to generate a frame, which it then rasterizes and feeds to the ffmpeg process via a fifo/named pipe. This way, there's at most one frame in memory at a time. When all frames have been fed to ffmpeg, the fifo is closed, which signals ffmpeg to conclude its business.

The code currently suffers from some caveats:

  1. It's been tested on linux only. It may work as-is on Mac. It does not work as-is on windows.
  2. The encoding parameters are of course debatable. I prefer to force periodic keyframes because, though it makes the file substantially larger, it makes "scrubbing" smoother when using my media player. I tend to do that a lot with technical animations.
  3. The code isn't particularly elegant.

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).
Update with benchmark
Source Link

Here is a proof-of-conceptfunction for doing this on Linux (and maybe Mac). I've looked around and I haven't seen any previous example of how to do this in MMA. Perhaps the comments will enlighten me as to prior work.

for my case this not only uses far less memory, it's also about 6x faster than Export["foo.mp4",frames,VideoEncoding -> "LIBX264",]. YMMV.

Here is a proof-of-concept for doing this on Linux (and maybe Mac). I've looked around and I haven't seen any previous example of how to do this in MMA. Perhaps the comments will enlighten me as to prior work.

Here is a function for doing this on Linux (and maybe Mac). I've looked around and I haven't seen any previous example of how to do this in MMA. Perhaps the comments will enlighten me as to prior work.

for my case this not only uses far less memory, it's also about 6x faster than Export["foo.mp4",frames,VideoEncoding -> "LIBX264",]. YMMV.

deleted 216 characters in body
Source Link

If you have the skills and interest, I encourage you to whip this code into shape and share it with the community. Please, don't forget the progress indicator -- It's the least important thing and what I miss most.

My thanks to [1] for guidance.

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

If you have the skills and interest, I encourage you to whip this code into shape and share it with the community. Please, don't forget the progress indicator -- It's the least important thing and what I miss most.

My thanks to [1] for guidance.

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

My thanks to [1] for guidance.

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]
  ]
Better progress reporting mechanism + mention the need for frame function to be pure + name change
Source Link
Loading
Better progress reporting mechanism + mention the need for frame function to be pure
Source Link
Loading
Updated code: 2x speed
Source Link
Loading
added 61 characters in body
Source Link
Loading
Update code
Source Link
Loading
Source Link
Loading