Automate video to GIF conversion in macOS
macos hackBesides 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
Footnotes
-
It is unclear whether the hypervisor supports GPU passthrough to make
vaapi
work, — it is worth a try in the future. ↩ -
After the post is published, I also found encountered static-linked ffmpeg binaries, you may want to take a look. ↩