Mazzarolo MatteoMazzarolo Matteo

How I capture, encode, and embed screen recordings for the web

By Mazzarolo Matteo


I've been asked a few times how I capture, encode, and embed screen recordings on my blog, such as this one:

So, here's my process.

Recording the screen

I'm a macOS user. To record my screen, I use QuickTime.
Nothing fancy here, I record the smallest portion of the screen that can provide value/context, trim it, and save it as input.mov.

Resizing, converting, and encoding

To resize, convert, and encode the screen recording, I use ffmpeg.

I convert the screen recording both to WebM and MP4 to offer the best quality/size for modern browsers and aim for a width of 1280px (sometimes 1920px) at 60fps to provide a crisp picture even on high-density displays.

For WebM, I use the VP9 video encoding, which is supported by all modern browsers except for Safari (who'd have thought it?).
To run the conversion, I use a slightly edited version of Google's "Best Quality (Slowest) Recommended Settings" for VP9:

# WebM VP9 encoding (in two pass)
 
# This pass just creates a log file (ignore the warning at the end of it)
ffmpeg -i input.mov \
  -vf "scale=1280:trunc(ow/a/2)*2" \
  -c:v libvpx-vp9 -pass 1 -b:v 1000K -threads 1 -speed 4 \
  -tile-columns 0 -frame-parallel 0 -auto-alt-ref 1 -lag-in-frames 25 \
  -g 9999 -aq-mode 0 -an -f webm /dev/null
 
# This pass uses the log file to generate output.webm
ffmpeg -i input.mov \
  -vf "scale=1280:trunc(ow/a/2)*2" \
  -c:v libvpx-vp9 -pass 2 -b:v 1000K -threads 1 -speed 0 \
  -tile-columns 0 -frame-parallel 0 -auto-alt-ref 1 -lag-in-frames 25 \
  -g 9999 -aq-mode 0 -c:a libopus -b:a 64k -f webm output.webm
# WebM VP9 encoding (in two pass)
 
# This pass just creates a log file (ignore the warning at the end of it)
ffmpeg -i input.mov \
  -vf "scale=1280:trunc(ow/a/2)*2" \
  -c:v libvpx-vp9 -pass 1 -b:v 1000K -threads 1 -speed 4 \
  -tile-columns 0 -frame-parallel 0 -auto-alt-ref 1 -lag-in-frames 25 \
  -g 9999 -aq-mode 0 -an -f webm /dev/null
 
# This pass uses the log file to generate output.webm
ffmpeg -i input.mov \
  -vf "scale=1280:trunc(ow/a/2)*2" \
  -c:v libvpx-vp9 -pass 2 -b:v 1000K -threads 1 -speed 0 \
  -tile-columns 0 -frame-parallel 0 -auto-alt-ref 1 -lag-in-frames 25 \
  -g 9999 -aq-mode 0 -c:a libopus -b:a 64k -f webm output.webm

The main difference from Google's suggestion is in the -vf "scale=1280:trunc(ow/a/2)*2" parameter, which I use to scale the video to a width of 1280 maintaining the original aspect ratio.
With these settings, the average size of my screen recording outputs for a ~10s video is ~1 MB (may vary depending on the video content).

For MP4, I use the H.264 video encoding (which is the default encoding picked by ffmpeg if you convert a screen recording captured with QuickTime):

ffmpeg -i input.mov -vf "scale=1280:trunc(ow/a/2)*2" output.mp4
ffmpeg -i input.mov -vf "scale=1280:trunc(ow/a/2)*2" output.mp4

With these settings, the average size of the output is around twice as big as the WebM version — I haven't spent much time playing with presets and settings, though, so there's a margin for improvements. I'm sorry, Safari users.

Embedding the screen recording

To embed the screen recording in the page, I use this HTML structure:

<div class="screen-recording">
  <video autoplay controls loop muted playsinline style="width: 640px; aspect-ratio: 1.3793103448">
    <source src="output.webm" type="video/webm; codecs=vp9,vorbis" />
    <source src="output.mp4" type="video/mp4" />
  </video>
</div>
<div class="screen-recording">
  <video autoplay controls loop muted playsinline style="width: 640px; aspect-ratio: 1.3793103448">
    <source src="output.webm" type="video/webm; codecs=vp9,vorbis" />
    <source src="output.mp4" type="video/mp4" />
  </video>
</div>

Which takes care of serving the correct video based on the browser support (MP4 for Safari, WebM for the rest of the gang).

I manually tweak the video's style attribute for each screen recording by:

  1. Setting the width of the video. Generally I set it to half the size of the screen recording for a crisp image even on high-density displays.
  2. Setting the aspect-ratio of the video. It's important to set it correctly to avoid shifting the rest of the content of the page when the video is loaded. By setting it to the right video aspect-ratio, the browser will reserve the right amount of vertical space to the video player even before the video is loaded.

I also use this style:

.screen-recording {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
}
.screen-recording > video {
  max-width: 100%;
  height: auto;
  border-radius: 4px;
}
.screen-recording {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
}
.screen-recording > video {
  max-width: 100%;
  height: auto;
  border-radius: 4px;
}

Which ensures the video is centered on the page and adds a small border radius around it.

Note #1: I don't add poster images, since they make an additional network request, and I'm almost OK with showing the first frame of the video.

Note #2: I don't change the preload setting, I let the browser take care of it.

Note #3: I don't create multiple videos for different pixel densities, because... I'm lazy.

Snippets

Finally, here's a handy script with the various snippets I've shown above:

#! /bin/bash
 
# Encode a screen recording for the web as WebM/MP4 and print the aspect-ratio
# Example usage:
# my-script.sh -i my_input -o my_output -w "1024"
 
# Read the input args
input="input"
output="output"
width="1280"
 
while getopts ":i:o:w:" opt; do
  case $opt in
    i) input="$OPTARG"
    ;;
    o) output="$OPTARG"
    ;;
    w) width="$OPTARG"
    ;;
    \?) echo "Invalid option -$OPTARG" >&2
    ;;
  esac
done
 
# WebM VP9 encoding (in two pass)
# This pass just creates a log file (ignore the warning at the end of it)
ffmpeg -i "${input}.mov" \
  -y \
  -vf "scale=${width}:trunc(ow/a/2)*2" \
  -c:v libvpx-vp9 -pass 1 -b:v 1000K -threads 1 -speed 4 \
  -tile-columns 0 -frame-parallel 0 -auto-alt-ref 1 -lag-in-frames 25 \
  -g 9999 -aq-mode 0 -an -f webm /dev/null
# This pass uses the log file to generate output.webm
ffmpeg -i "${input}.mov" \
  -y \
  -vf "scale=${width}:trunc(ow/a/2)*2" \
  -c:v libvpx-vp9 -pass 2 -b:v 1000K -threads 1 -speed 0 \
  -tile-columns 0 -frame-parallel 0 -auto-alt-ref 1 -lag-in-frames 25 \
  -g 9999 -aq-mode 0 -c:a libopus -b:a 64k -f webm "${output}.webm"
# Delete the generated log file
rm ffmpeg*.log
 
# MP4 H.264 encoding
ffmpeg -i "${input}.mov" -vf "scale=${width}:trunc(ow/a/2)*2" -y "${output}.mp4"
 
# Print the aspect ratio
echo "scale=10;$(ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=s=x:p=0 ${output}.mp4 | sed -r 's/x/ \/ /g')" | bc | (read aspect_ratio; echo -e "\nAspect ratio: ${aspect_ratio}")
#! /bin/bash
 
# Encode a screen recording for the web as WebM/MP4 and print the aspect-ratio
# Example usage:
# my-script.sh -i my_input -o my_output -w "1024"
 
# Read the input args
input="input"
output="output"
width="1280"
 
while getopts ":i:o:w:" opt; do
  case $opt in
    i) input="$OPTARG"
    ;;
    o) output="$OPTARG"
    ;;
    w) width="$OPTARG"
    ;;
    \?) echo "Invalid option -$OPTARG" >&2
    ;;
  esac
done
 
# WebM VP9 encoding (in two pass)
# This pass just creates a log file (ignore the warning at the end of it)
ffmpeg -i "${input}.mov" \
  -y \
  -vf "scale=${width}:trunc(ow/a/2)*2" \
  -c:v libvpx-vp9 -pass 1 -b:v 1000K -threads 1 -speed 4 \
  -tile-columns 0 -frame-parallel 0 -auto-alt-ref 1 -lag-in-frames 25 \
  -g 9999 -aq-mode 0 -an -f webm /dev/null
# This pass uses the log file to generate output.webm
ffmpeg -i "${input}.mov" \
  -y \
  -vf "scale=${width}:trunc(ow/a/2)*2" \
  -c:v libvpx-vp9 -pass 2 -b:v 1000K -threads 1 -speed 0 \
  -tile-columns 0 -frame-parallel 0 -auto-alt-ref 1 -lag-in-frames 25 \
  -g 9999 -aq-mode 0 -c:a libopus -b:a 64k -f webm "${output}.webm"
# Delete the generated log file
rm ffmpeg*.log
 
# MP4 H.264 encoding
ffmpeg -i "${input}.mov" -vf "scale=${width}:trunc(ow/a/2)*2" -y "${output}.mp4"
 
# Print the aspect ratio
echo "scale=10;$(ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=s=x:p=0 ${output}.mp4 | sed -r 's/x/ \/ /g')" | bc | (read aspect_ratio; echo -e "\nAspect ratio: ${aspect_ratio}")

Other resources