WebM for the Web: Allowed Codecs, VP9/AV1/Opus, and FFmpeg Muxing

Key takeaways

Matroska-based WebM: allowed codecs, browser support, VP9/AV1/Opus combinations, FFmpeg mux and streaming tips for web distribution beginners.

Introduction

WebM is a media container designed for web distribution. Structurally it is Matroska (MKV), but it is a profile of Matroska: it restricts which codecs and which elements a file may contain. Video must be VP8, VP9 or AV1; audio must be Vorbis or Opus; text tracks are WebVTT. That restriction is the whole point. A browser that sees video/webm knows the small, fixed set of decoders it might need, and the codecs involved were published as royalty-free, which is why WebM shows up wherever the open web stack is used: HTML5 <video>, Media Source Extensions (MSE), and the MediaRecorder API in Chrome and Firefox.

“Web video means WebM” is still not a safe rule. MP4 with H.264 (or HEVC) remains the format that plays almost everywhere, encodes quickly on hardware, drops straight into editing tools, and is what commercial DRM pipelines expect. This article covers what WebM actually guarantees, how its internals affect seeking and streaming, the FFmpeg commands that produce correct files, and the specific failures people hit when they ship it.


Container Overview

History and Development Background

Google announced WebM in 2010, shortly after acquiring On2 Technologies and releasing VP8 under a permissive license. The goal was an HTML5 video format that browser vendors could ship without paying H.264 licensing fees; Mozilla and Opera, which refused to ship H.264 at the time, backed it immediately. VP9 followed in 2013 and later AV1 (from the Alliance for Open Media) was added to the same container, so a WebM file today is most often VP9 or AV1 video with Opus audio. Vorbis and VP8 still appear in older content and in WebRTC-adjacent tooling.

Technical Features

  • Matroska-based: The binary layout is EBML/Matroska, but the WebM specification narrows the allowed elements and codecs. A generic Matroska demuxer can read WebM; the reverse is not true.
  • Fits the open web stack: The WebM Byte Stream Format is one of the formats MSE defines, so JavaScript players can append WebM segments to a SourceBuffer the same way they append fragmented MP4.
  • Deliberately limited: You cannot put H.264, AAC, AC-3 or PGS subtitles in a WebM file. FFmpeg’s WebM muxer refuses them outright rather than writing a file that browsers will reject later.

File Extensions

ExtensionCommon Use
.webmVP8/VP9/AV1 + Vorbis/Opus and other allowed combinations
.webm (audio-focused)Audio WebM containing Opus only also conventionally integrated

Don’t determine the codec from the extension alone; check with ffprobe. Files renamed from .mkv to .webm are common and are the source of many “WebM won’t play” reports, because the container header still claims Matroska and the tracks may hold codecs WebM doesn’t allow.


Internal Structure

EBML & Matroska Relationship

EBML is a binary, extensible format similar in spirit to XML: every element is an ID, a variable-length size, and a payload. WebM is a subset of Matroska, so the Segment → Cluster → SimpleBlock hierarchy is the same as in MKV. The first thing in the file is the EBML header, whose DocType field says webm instead of matroska; browsers use this to decide whether to even try.

Segment, Cluster, Track

  • Tracks: Declares each video/audio track, its codec ID (V_VP9, V_AV1, A_OPUS) and the codec private data the decoder needs before the first frame (for Opus, the OpusHead structure).
  • Cluster: A group of blocks sharing a base timestamp. Clusters are the natural cutting point for streaming, and in the WebM Byte Stream Format each MSE media segment is a Cluster. For clean seeking and segmenting, each cluster should start with a video keyframe.
  • Cues: The seek index, mapping timestamps to cluster byte positions. Where Cues sit in the file matters a lot, see below.
  • Metadata: Used much more sparingly than in MKV. Don’t expect chapters and rich tags to survive a web workflow.

Why Cues Placement Matters

When a muxer writes a file in one pass, it only knows the cluster positions after writing them, so FFmpeg by default puts the Cues element at the end of the file. For a local file this is harmless. Over HTTP, a browser that wants to seek has to issue a range request to the end of the file to find the index first, and if the server doesn’t support range requests at all, seeking simply doesn’t work until the whole file has downloaded. The fix is to ask the muxer to reserve space up front and write the Cues near the start, which FFmpeg exposes as the -cues_to_front 1 option of the WebM/Matroska muxer (it rewrites the file at the end, so it needs a seekable output, not a pipe).

Metadata Storage Policy

  • Basic tags like TITLE, LANGUAGE are supported, but exposure varies by platform.
  • For thumbnails and rich chapters, lower your expectations compared to MP4/MKV; put titles and descriptions in the page itself instead.

Structure Diagram (WebM Simplified)

flowchart LR
  subgraph webm [WebM File]
    S[Segment]
    T[Tracks: VP9/AV1 + Opus]
    C1[Cluster 1]
    C2[Cluster 2]
    S --> T
    S --> C1
    S --> C2
  end
  subgraph web [Web Stack]
    V[HTML video / MSE]
  end
  webm --> V

Practical Usage

Analyze Input

Always start by checking what you actually have. -show_streams lists codec names, pixel format and bit depth, and -show_format shows the container name, which tells you whether a .webm file is really WebM.

ffprobe -hide_banner -show_streams -show_format input.webm

VP9 + Opus (General Web Distribution)

ffmpeg -i input.mp4 -c:v libvpx-vp9 -crf 32 -b:v 0 -row-mt 1 \
  -c:a libopus -b:a 128k output.webm

The -crf and -b:v 0 pair is what puts libvpx-vp9 into constant-quality mode. If you pass -crf without -b:v 0, libvpx runs in constrained quality mode, where the CRF value is capped by a target bitrate, and older FFmpeg builds apply a low default bitrate that quietly limits quality. This is the most common reason people report “VP9 looks worse than H.264 at the same CRF”. CRF numbers are also not comparable across encoders: VP9 uses a 0–63 scale, so 31–33 is a reasonable 1080p starting point, not the x264 18–23 range.

-row-mt 1 enables row-based multithreading. Without it libvpx parallelizes only across tiles, and on a many-core machine VP9 encoding will use a fraction of the available CPU. VP9 is slow in general; Google’s own encoding guidance recommends two-pass encoding for VOD, which gives noticeably better bitrate allocation at the cost of doubling encode time.

AV1 + Opus (Modern Browsers & Bandwidth Reduction)

ffmpeg -i input.mp4 -c:v libsvtav1 -crf 28 -preset 6 \
  -c:a libopus -b:a 128k output.webm

SVT-AV1 is the practical CPU encoder for AV1: much faster than libaom at comparable quality. -preset runs from 0 (slowest, best) to 13 (fastest); 6–8 is the usual trade-off for VOD, while presets below 4 get very slow for little gain. If you have a hardware AV1 encoder (recent NVIDIA, Intel Arc, AMD RDNA3), use its FFmpeg encoder instead, accepting somewhat lower efficiency per bit.

The trade-off against VP9 is decode, not encode. AV1 saves bitrate, but on devices without a hardware AV1 decoder it falls back to software decoding, which costs battery on laptops and can stutter on low-end phones at 1080p and above.

Codec Copy (remux, when already WebM compatible)

ffmpeg -i input.webm -c copy remuxed.webm

Remuxing rewrites the container without touching the compressed frames. It is instant and lossless, and it is how you repair files with a broken index or missing duration (see MediaRecorder below). It only works when every stream is already a WebM-legal codec.

MP4 to WebM (Re-encode)

ffmpeg -i input.mp4 -c:v libvpx-vp9 -crf 30 -b:v 0 -c:a libopus -b:a 96k out.webm

You can’t remux H.264/AAC into WebM, so MP4 → WebM is always a full re-encode, with generation loss. Encode from the highest-quality master you have rather than from an already-compressed web MP4 when possible.

Multiple Audio & Subtitles

WebM is less flexible than MKV here. Browsers expose multiple audio tracks inconsistently through HTMLMediaElement.audioTracks (Chrome only behind a flag at the time of writing), so the common practice is one audio track per file and subtitles as external WebVTT files attached with <track>.

# WebM with multiple audio tracks (limited support)
# -c copy only works if video.mp4's video stream is already VP9/AV1
ffmpeg -i video.mp4 -i audio_en.opus -i audio_ko.opus \
  -map 0:v -map 1:a -map 2:a -c copy \
  -metadata:s:a:0 language=eng -metadata:s:a:1 language=kor multi.webm

Performance Comparison

Overhead vs Other Containers

WebM container overhead is small, similar to the rest of the Matroska family and to MP4. The perceived size of a file is dominated by the video codec, CRF and resolution; switching containers will not meaningfully change file size.

Streaming Suitability

AspectWebM
Adaptive streamingfMP4 very dominant in industry—WebM is auxiliary in some DASH profiles
Single file <video>Advantageous for simple distribution in VP9/AV1 supporting browsers
LiveOften used with separate stacks like WebRTC

The reason fMP4 won adaptive streaming is ecosystem, not efficiency. HLS on Apple devices historically required MPEG-TS or fMP4, CMAF standardized fMP4 segments that serve both HLS and DASH from one set of files, and common DRM (Widevine, PlayReady, FairPlay via CENC/CBCS) is specified around ISO BMFF. VP9 and AV1 can be carried in fMP4 as well, so large services that want those codecs usually put them in MP4 segments rather than WebM.

Compatibility Range

  • Chrome, Firefox, Edge, Android Chrome: VP9/Opus WebM plays reliably; AV1 is supported in current versions.
  • Safari: WebM support arrived late and in stages, and AV1 decoding is tied to devices with hardware AV1 decoders. Because the matrix depends on OS version and hardware, a fallback MP4 remains the de facto standard pattern.
  • Editors: Professional NLEs center on MP4/MOV; many import WebM poorly or not at all.

Real-World Use Cases

Streaming Services (HLS, DASH)

  • Large OTT services center on fMP4, but some offer VP9 or AV1 tracks to clients that can decode them. When deciding, weigh the extra encoding cost (every rendition encoded again in another codec) against the bandwidth saved, and consider how many of your viewers’ devices decode the codec in hardware.

Web Browsers

  • The dual-source pattern lets the browser choose: it walks the <source> list in order and plays the first one whose type it believes it can decode. Put the more efficient format first. Including the codecs parameter matters, because without it the browser has to start downloading the file to find out whether it can play it.
<video controls>
  <source src="video.webm" type='video/webm; codecs="vp9, opus"'>
  <source src="video.mp4" type='video/mp4'>
  Your browser does not support the video tag.
</video>

For scripted decisions, video.canPlayType('video/webm; codecs="vp9, opus"') returns "probably", "maybe" or an empty string. navigator.mediaCapabilities.decodingInfo() goes further and reports whether decoding will be smooth and powerEfficient, which is the practical way to avoid serving AV1 to a device that would decode it in software.

Browser Recording (MediaRecorder)

Chrome and Firefox produce WebM from MediaRecorder, which makes WebM the default format for in-browser screen and camera recording. The recorder writes a live stream, so the resulting file has no Duration value and no Cues. The first time I worked with recorded blobs, the bug report was “the progress bar shows Infinity and seeking does nothing” — exactly this. Remuxing the file server-side with ffmpeg -i rec.webm -c copy fixed.webm writes a proper duration and index; in the browser, libraries such as ts-ebml or fix-webm-duration patch the header instead.

Mobile Apps

  • Android supports VP8/VP9 decoding across the platform, with hardware acceleration depending on the chipset. On iOS, native WebM support is newer and less predictable, so apps often transcode to MP4 or ship their own player.

Archiving

  • Keep WebM as the final web distribution format, but store the master in an edit-friendly format (e.g., MOV/ProRes, or MKV with lossless audio). WebM files are lossy delivery copies; re-encoding them later adds another generation of loss.

Optimization Tips

Streaming Optimization

  • For single-file web playback, a keyframe interval of 2–4 seconds balances seek precision against size: the player can only start decoding at a keyframe, so a 10-second GOP means seeking lands up to 10 seconds away from where the user clicked, or the browser has to decode forward from the previous keyframe.
  • For MSE, align the encoder’s keyframes with your segment boundaries, since each segment (Cluster) should begin with a keyframe.
# Set keyframe interval: 120 frames = 4 s at 30 fps, 2 s at 60 fps
ffmpeg -i input.mp4 -c:v libvpx-vp9 -g 120 -keyint_min 120 \
  -c:a libopus -b:a 128k output.webm

Setting -g and -keyint_min to the same value gives fixed-interval keyframes, which is what segmenters want. It costs a little efficiency because the encoder can no longer place keyframes only where scenes cut.

Minimize File Size

  • Reduce resolution and frame rate first, then adjust the AV1/VP9 CRF and preset. A 720p encode at a sane CRF almost always looks better than a 1080p encode starved of bits.
  • For speech-only content, Opus is efficient at low bitrates; 64–96 kbps is usually transparent for voice, and mono halves the requirement again.
# Voice-optimized WebM
ffmpeg -i input.mp4 -c:v libvpx-vp9 -crf 35 -b:v 0 \
  -c:a libopus -b:a 64k -ac 1 voice.webm

Improve Compatibility

  • Always preparing a fallback MP4 (H.264+AAC) removes most Safari and old-device risk for the cost of one more encode and some storage.

Common Issues and Solutions

Won’t Play in Browser

  • Codec unsupported: A file with AV1 only fails on older browsers and devices. Add a VP9 or MP4 fallback source.
  • The file isn’t really WebM: FFmpeg’s WebM muxer won’t write AAC or H.264; it stops with Only VP8 or VP9 or AV1 video and Vorbis or Opus audio and WebVTT subtitles are supported for WebM. So a .webm with AAC inside is almost always a Matroska file that was renamed. ffprobe will show matroska,webm as the format and the offending codec. Re-encode the non-WebM streams:
# Check codecs in WebM
ffprobe input.webm 2>&1 | grep -i codec
# Convert AAC to Opus (video copy works only if the video is already VP8/VP9/AV1)
ffmpeg -i input.webm -c:v copy -c:a libopus -b:a 128k fixed.webm
  • Plays but can’t seek: Check that the server honors HTTP Range requests (the response should be 206 Partial Content) and that the file has Cues. Remux with -cues_to_front 1 for progressive playback.
  • Wrong MIME type: Some servers send .webm as application/octet-stream. Most browsers sniff the content anyway, but configure video/webm explicitly to avoid inconsistent behavior.

Metadata Loss

  • Web players rarely display container tags. If you need a title in the UI, put it in the page markup and in JSON-LD (VideoObject) for search engines.

Codec Compatibility Issues

  • 10-bit VP9 (profile 2) and 10-bit AV1 decode fine on desktops but can fail or fall back to slow software decoding on some phones and TVs. When a file plays on your machine but not on a device, re-test with 8-bit yuv420p, which is VP9 profile 0 and the most widely supported variant. Sources from cameras or screen recorders in yuv444p produce VP9 profile 1, which many hardware decoders don’t support at all.
# Ensure 8-bit yuv420p
ffmpeg -i input.mp4 -c:v libvpx-vp9 -pix_fmt yuv420p \
  -c:a libopus -b:a 128k output.webm

Conclusion

Key Summary

  • WebM is a web-specific profile of Matroska; its restricted codec list (VP8/VP9/AV1, Vorbis/Opus, WebVTT) is what lets browsers support it predictably.
  • Broad compatibility still favors MP4, so dual sources with an MP4 fallback remain the realistic pattern.
  • Quality and size are decided by the codec and encoder settings, not the container; seekability over HTTP is decided by Cues placement and server range support.
  • WebM fits services that prioritize open codecs, VP9/AV1 rollouts aimed at bandwidth reduction, browser-side recording, and internal tools where royalty burden matters. For streaming segments, DRM, and maximum compatibility, see the MP4 Guide and MKV Guide together.

Quick Command Reference

# Check WebM file
ffprobe input.webm
# Create VP9 + Opus WebM
ffmpeg -i input.mp4 -c:v libvpx-vp9 -crf 30 -b:v 0 -c:a libopus -b:a 128k output.webm
# Create AV1 + Opus WebM
ffmpeg -i input.mp4 -c:v libsvtav1 -crf 28 -preset 6 -c:a libopus -b:a 128k output.webm
# Remux existing WebM
ffmpeg -i input.webm -c copy output.webm
# HTML5 with fallback
# <video controls>
#   <source src="video.webm" type='video/webm; codecs="vp9, opus"'>
#   <source src="video.mp4" type='video/mp4'>
# </video>


Frequently Asked Questions (FAQ)

Q. Should I encode my web videos to VP9 or AV1 inside WebM?

A. AV1 generally reaches the same visual quality at a lower bitrate, but encoding is much slower and older devices may lack hardware decoding, which costs battery and can stutter. VP9 encodes faster and is supported by a wider range of existing browsers and devices. A practical setup is to serve AV1 first and VP9 (or H.264 MP4) as a fallback via multiple <source> elements, so the browser picks the first format it can play.