DEV Community

Alex Neamtu
Alex Neamtu

Posted on • Originally published at sendrec.eu

How We Made Screen Recording Work on Every Browser

Our screen recorder was Chrome-only for months. Safari users couldn't record at all. Firefox users got a cryptic error. And anyone who recorded on Chrome still produced WebM files that needed server-side transcoding before Safari users could watch them.

We shipped a fix in v1.60.0 that makes recording work on Safari, Firefox, and Chrome — and eliminates most transcoding entirely. Here's what we changed and why.

The problem: hardcoded WebM

The original recorder had this:

const recorder = new MediaRecorder(screenStream, {
  mimeType: "video/webm;codecs=vp9,opus",
});
Enter fullscreen mode Exit fullscreen mode

This worked on Chrome, which supports VP9 WebM recording. But it fails on two other major browsers:

Safari doesn't support WebM MediaRecorder at all. It supports video/mp4 recording (since Safari 14.1), but not WebM. Calling new MediaRecorder(stream, { mimeType: "video/webm;..." }) throws a NotSupportedError.

Firefox supports WebM recording but with VP8, not VP9. Passing video/webm;codecs=vp9,opus throws because Firefox's MediaRecorder doesn't support VP9 encoding.

In both cases, the error bubbled up as "Screen recording was blocked or failed" — a generic message that told users nothing useful.

The fix: detect what the browser supports

Instead of hardcoding a format, we detect what the browser can do:

export function getSupportedMimeType(): string {
  if (typeof MediaRecorder === "undefined") return "video/mp4";
  if (MediaRecorder.isTypeSupported("video/mp4")) return "video/mp4";
  if (MediaRecorder.isTypeSupported("video/webm;codecs=vp9,opus"))
    return "video/webm;codecs=vp9,opus";
  if (MediaRecorder.isTypeSupported("video/webm;codecs=vp8,opus"))
    return "video/webm;codecs=vp8,opus";
  if (MediaRecorder.isTypeSupported("video/webm")) return "video/webm";
  return "video/mp4";
}
Enter fullscreen mode Exit fullscreen mode

MP4 is preferred because it plays everywhere without transcoding. If the browser doesn't support MP4 recording, we fall through VP9, VP8, and plain WebM before giving up.

This means:

  • Chrome 130+ records MP4 natively (no transcoding needed)
  • Safari records MP4 natively (it was always capable, we just never asked)
  • Firefox records WebM with VP8 (server transcodes to MP4 later)
  • Older Chrome records WebM with VP9 (server transcodes)

We extracted this into a shared mediaFormat.ts utility because we had the same logic duplicated across two components — the screen recorder and the camera recorder. The camera recorder already did format detection correctly. The screen recorder was the one that was broken.

Chrome's H.264 encoder has limits

After switching Chrome to MP4, we discovered a new problem. Chrome's H.264 hardware encoder fails on high-resolution display captures — common with Retina screens at 2x or 3x scaling. The encoder silently produces empty MP4 blobs or fires an error event.

We handle this with a fallback chain in the recorder itself:

recorder.onerror = () => {
  if (!mimeTypeRef.current.startsWith("video/mp4")) return;
  encoderFailed = true;

  const webmMimeType = MediaRecorder.isTypeSupported("video/webm;codecs=vp9,opus")
    ? "video/webm;codecs=vp9,opus"
    : "video/webm";

  const fallback = new MediaRecorder(screenStream, { mimeType: webmMimeType });
  mediaRecorderRef.current = fallback;
  fallback.start();
};
Enter fullscreen mode Exit fullscreen mode

If the MP4 encoder fails, we swap to a WebM recorder on the same stream. The user doesn't see anything — recording continues seamlessly. The WebM file will need server-side transcoding, but that's better than a failed recording.

iOS playback: native controls win

Our custom video player — with seek bar, speed control, keyboard shortcuts, picture-in-picture — works well on desktop browsers. On iOS Safari, it had problems. Touch events conflicted with the native video element behavior. Play/pause was unreliable. The seek bar didn't track correctly.

Rather than fighting iOS Safari's quirks, we detect iOS and fall back to native controls:

var isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) ||
            (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
if (isIOS) {
    player.setAttribute('controls', '');
    controls.style.display = 'none';
    overlay.style.display = 'none';
}
Enter fullscreen mode Exit fullscreen mode

The second condition catches iPads running iPadOS 13+, which report themselves as "MacIntel" with touch points. Without that check, iPads would get the custom controls and the same broken experience.

iOS users get native controls that work reliably — play, pause, seek, volume, AirPlay, picture-in-picture — all built into Safari. Desktop users keep the custom controls with keyboard shortcuts and chapter markers.

iOS-safe ffmpeg encoding

When the server produces MP4 files — whether from transcoding WebM, compositing webcam overlays, or trimming clips — the output needs to play on iOS Safari. iOS is picky about H.264 encoding parameters. Videos that play fine on desktop Safari may fail on iPhone or iPad.

We standardized all server-side ffmpeg operations to use iOS-safe encoding flags:

"-profile:v", "high",
"-level:v", "5.1",
"-preset", "fast",
"-crf", "23",
"-r", "60",
"-c:a", "aac",
"-movflags", "+faststart",
"-vf", "scale='min(1920,iw)':'min(1080,ih)':force_original_aspect_ratio=decrease:force_divisible_by=2",
Enter fullscreen mode Exit fullscreen mode

The key constraints: H.264 High profile at level 5.1, resolution capped at 1920x1080, framerate capped at 60fps, AAC audio, and faststart for progressive download. The scale filter also forces dimensions to be divisible by 2, which H.264 requires.

This applies to composite output, trim output, and the normalize pipeline that re-encodes incompatible MP4 uploads.

WebM on Safari: a gentler warning

Some videos are still WebM — older recordings, or new ones from Firefox before the transcoder runs. Safari can't play WebM. Previously, we showed a harsh warning: "This video was recorded in WebM format, which is not supported by Safari. Please open this link in Chrome or Firefox to watch."

On iOS, that message is useless — there's no Chrome or Firefox with WebM support on iOS since they all use WebKit. We now show a contextual message:

  • Desktop Safari: "Please open this link in Chrome or Firefox to watch."
  • iOS Safari: "This video is still being processed. Please check back in a moment."

The iOS message is accurate — the transcoder will convert the WebM to MP4 within a few minutes. Rather than asking users to switch browsers (which won't help), we tell them it's coming.

Upload progress

While fixing recording, we also improved the upload experience. Previously, after recording finished, the page showed a static "Uploading..." message. If the upload took a while — large recordings over slow connections — users had no idea what was happening.

Now the upload page shows the current step: "Creating video...", "Uploading recording...", "Uploading camera...", "Finalizing...". Each step updates as the upload progresses. And a beforeunload handler prevents accidental navigation away from the page during upload, with a gentle "Please don't close this page" reminder.

What we eliminated

The biggest win isn't a feature — it's what we removed. Chrome 130+ and Safari now record MP4 directly. That means:

  • No server-side transcoding for the majority of recordings
  • Faster time-to-playback (video is ready immediately after upload)
  • Less server CPU spent on ffmpeg
  • Smaller storage footprint (no WebM originals sitting around waiting for transcoding)

Firefox recordings still need transcoding, but Firefox is a small percentage of our traffic. The transcoder runs on a background worker and typically finishes within a few minutes.

Try it

SendRec is open source (AGPL-3.0) and self-hostable. Try recording at app.sendrec.eu — it should work on whatever browser you're reading this in. The format detection code is in mediaFormat.ts and the iOS player fallback is in player_shared.go.

Top comments (0)