Automate video to GIF conversion in macOS

macos hack

Besides the before and after comparison we discussed in the last post, I also prefer to attach a screen recording for customer-facing change. Unfortunately, the Github’s pull requests do not support video upload; I have to convert the screen recording, usually a QuickTime movie, to GIF before attaching it to the pull request.

ffmpeg is the swiss knife for video processing, and I chose the jrottenberg/ffmpeg:4.1-alpine docker image1 to bypass the massive dependencies2.

docker pull jrottenberg/ffmpeg:4.1-alpine

It is also tricky to export high qualify GIF with the 256-color palette constraint in GIF format. We can run the two-phase transcoding as this article suggests:

docker run --rm -v $(pwd):/workdir -w /workdir  jrottenberg/ffmpeg:4.1-alpine \
  -i screen-recording.mov -vf "fps=12,palettegen" -y palette.png

docker run --rm -v $(pwd):/workdir -w /workdir  jrottenberg/ffmpeg:4.1-alpine \
  -i screen-recording.mov -i palette.png \
  -lavfi "fps=12 [x]; [x][1:v] paletteuse" \
  -y screen-recording.gif

In the above command, we bind the current directory to the /workdir in the container with -v, and change the work directory to /workdir with -w. We invoke ffmpeg with "fps=12,palettegen" to sample the video with 12fps, and generate the color palette as palette.png in the first step. In the second step, ffmpeg uses the palette.png as color palette to encode the gif, and save it as screen-recording.gif.

Integrate to Quick Action

The github pull request also limits the maximum size for image as 10MB, so it would be nice to support video resizing. Ideally, we should show the progress during video processing.

To interact with user, I rewrite the workflow with the JavaScript for Automation, aka JXA. JXA supports some ES6 traits, — I suspect it shares the same runtime as Safari.

function run(input, parameters) {
  var app = Application.currentApplication();
  app.includeStandardAdditions = true;

  var encodingOptions = [
    "Original size",
    "Downsample to 720p",
    "Downsample to 480p",
  ];
  var encoding = app.chooseFromList(encodingOptions, {
    withPrompt: "Select your GIF settings:",
    defaultItems: ["Original size"],
  });

  const filters = {
    "Original size": "fps=12",
    "Downsample to 720p": "fps=12,scale='min(720,iw)':-1:flags=lanczos",
    "Downsample to 480p": "fps=12,scale='min(480,iw)':-1:flags=lanczos",
  };
  const filter = filters[encoding];

  input.forEach((x, index) => {
    const path = x.toString();
    var parts = path.split(/\//);
    const basename = parts.pop();
    const cwd = parts.join("/");
    const name = basename.split(".").slice(0, -1).join(".");

    // Invoke the command line
    app.doShellScript(`
      /usr/local/bin/docker run --rm -v ${cwd}:/workdir -w /workdir \
        jrottenberg/ffmpeg:4.1-alpine \
        -i "${basename}" -vf "${filter},palettegen" -y palette.png

      /usr/local/bin/docker run --rm -v ${cwd}:/workdir -w /workdir \
        jrottenberg/ffmpeg:4.1-alpine \
        -i "${basename}" -i palette.png \
        -lavfi "${filter} [x]; [x][1:v] paletteuse" \
        -y "${name}.gif"
    `);
  });

  app.displayNotification(
    `Total ${input.length} video(s) have been converted.`,
    {
      withTitle: "Convert to GIF",
      subtitle: "Processing is complete.",
    },
  );
}

After several attempts, I gave up on the progress support. The sample code in the Mac Automation Scripting Guide only worked in the ScriptEditor, but rendered nothing in the Automator! Maybe the workflow should stay in the background, as this StackOverflow post suggests. At least, we can popup a notification at the end.

You can download and unzip this workflow, and then import via Import Actions…

You may encounter the Operation not permitted error due to the new sandbox security policy introduced in Catalina, you may need to grant access privilege for docker to continue.

References

  1. Mac Automation Scripting Guide
  2. JXA Cookbook
  3. ffmpeg Documentation

Footnotes

  1. It is unclear whether the hypervisor supports GPU passthrough to make vaapi work, — it is worth a try in the future.

  2. After the post is published, I also found encountered static-linked ffmpeg binaries, you may want to take a look.