VP9 vs H.265 vs AV1: Video Codec Comparison for Production Pipelines

Codec selection is one of the highest-leverage decisions in a video delivery pipeline. Choosing wrong means paying unnecessary CDN egress costs, shipping unplayable streams to Safari users, or burning server CPU on encode jobs that take 10× longer than they should. This guide — part of Core Media Fundamentals & Next-Gen Formats — gives you an engineering-grade breakdown of VP9, H.265 (HEVC), and AV1: their lineage, compression mechanics, encode parameters, hardware decode status, and the browser fallback ladder that ties them together.

Codec Lineage & Architecture

Understanding where each codec came from explains why they make the trade-offs they do.

VP9 (2013, Google) is the second-generation successor to VP8. Its design goal was royalty-free delivery at YouTube scale: good compression for 1080p/4K streaming, fast software decode on mid-range Android hardware, and zero patent licensing friction. VP9 uses a superblock architecture (up to 64×64 luma blocks), recursive partitioning, and a two-pass rate control well-suited to constant-quality (-cq-level / --crf) encodes.

H.265 / HEVC (2013, JCT-VC — a joint effort between MPEG and ITU-T) extended H.264’s block-based hybrid prediction model to coding tree units (CTUs) up to 64×64 pixels, adding more flexible partitioning, improved intra prediction angular modes, and a sample-adaptive offset (SAO) filter. The result is roughly 40–50% bitrate savings over H.264 at equivalent quality. The cost: a fragmented patent landscape managed by multiple patent pools (MPEG LA, Technicolor, Via Licensing), which drove browser vendors to avoid mandatory H.265 support and pushed streaming providers toward royalty-free alternatives.

AV1 (2018, Alliance for Open Media — a coalition including Google, Apple, Mozilla, Netflix, Amazon, and Intel) was designed from the ground up to be royalty-free while surpassing H.265 compression efficiency. AV1 introduces several key innovations over VP9: larger coding units (up to 128×128), compound inter prediction, film-grain synthesis (encoding grain parameters rather than the actual grain signal), constrained directional enhancement filtering (CDEF), and a loop restoration filter. These additions account for both AV1’s superior quality-per-bit and its significantly longer encode time.

The diagram below traces the codec family tree and shows which delivery containers each codec targets:

Video codec lineage and container mapping A diagram showing the lineage of H.264 leading to H.265 (HEVC), VP8 leading to VP9 then AV1, with container mappings to MP4 and WebM. Proprietary / licensed Royalty-free / open H.264 2003 · MPEG/ITU H.265 / HEVC 2013 · JCT-VC · licensed MP4 container VP8 2008 · Google VP9 2013 · Google AV1 2018 · Alliance for Open Media WebM container WebM · MP4 · MKV WebM · MP4 · MKV

Block Partitioning, Prediction, and Transform Structures

All three codecs are block-based hybrid coders: carve the frame into blocks, predict each block from decoded neighbours (intra) or from previously decoded frames (inter), transform the residual, quantise it, entropy-code the result. Almost the entire bitrate gap between them comes from two things — how finely each one can carve the frame up, and how many ways it can form a prediction for the resulting block.

H.265 starts from a coding tree unit of at most 64×64 luma samples and splits it with a quadtree down to 8×8 coding units. Each CU carries its own prediction unit split — 2N×2N, 2N×N, N×2N, N×N, plus four asymmetric motion partitions (2N×nU, 2N×nD, nL×2N, nR×2N) that let a moving object’s edge land on a 25/75 boundary instead of forcing a full quadtree split. Residual coding then runs a second, independent quadtree — the residual quadtree — with transform units from 32×32 down to 4×4. Intra prediction offers 35 modes: DC, planar, and 33 angular directions.

VP9 uses a 64×64 superblock and a strictly recursive four-way split with the same four partition symbols at every level — NONE, HORZ, VERT, SPLIT — bottoming out at 8×8, with 4×4 sub-blocks available for the smallest predictions. It has 10 intra modes and four transform sizes from 4×4 to 32×32, each of which can select a DCT or an asymmetric discrete sine transform (ADST) independently per axis. ADST is the right basis when residual energy piles up against one edge of the block, which is exactly what happens next to a directional intra prediction.

AV1 widens every one of those axes at once. The superblock grows to 128×128, the partition symbol set grows to ten by adding four T-shaped partitions (HORZ_A, HORZ_B, VERT_A, VERT_B) and two 4:1 splits (HORZ_4, VERT_4), and blocks reach down to 4×4. Intra prediction adds 56 directional modes at roughly 3-degree granularity, smooth and Paeth predictors, recursive filter intra, palette mode for screen content, and intra block copy for repeated on-screen elements. Inter prediction gains a seven-entry reference frame list, eighth-pel motion vectors, overlapped block motion compensation, warped motion, and compound modes that blend two predictors along a signalled wedge or by distance-weighted average. Transforms reach 64×64 and combine DCT, ADST, flipped ADST, and an identity transform into 16 kernel pairs.

Every one of those additions is a decision the encoder has to search, and the search space multiplies rather than adds. That search — not the arithmetic of the transforms themselves — is where AV1’s encode time goes, which is why -preset on SVT-AV1 is best understood as a knob controlling how aggressively the partition and mode space is pruned, not as a “quality” setting in the abstract.

VP9 and AV1 superblock partitioning compared Two squares. The left square shows a VP9 64 by 64 superblock split recursively into square quadrants plus 32 by 16 and 16 by 32 rectangles. The right square shows an AV1 128 by 128 superblock with a quadtree split, a T-shaped HORZ_A partition, a four-to-one VERT_4 split, and one undivided 64 by 64 block. VP9 — 64×64 superblock square quadtree + 2:1 rectangles AV1 — 128×128 superblock adds T-shapes and 4:1 splits 16×16 32×32 32×16 16×32 32×32 HORZ_A 64×64 VERT_4 recursive 4-way split, 64 down to 8, plus 2:1 rectangles 10 partition types, 128 down to 4, T-shapes and 4:1 Every extra partition type is another branch the encoder must search — that search is AV1's encode-time cost.

In-Loop Filtering, Tiling, and Threading

H.264 and VP9 apply a deblocking filter to the reconstructed frame and stop there. H.265 adds a sample adaptive offset stage after deblocking, which attacks ringing by adding a signalled offset per intensity band or per edge category. AV1 runs a three-stage chain: deblocking, then the constrained directional enhancement filter (CDEF), then loop restoration. CDEF estimates a dominant edge direction for each 8×8 block and filters along it, so it can remove ringing without smearing the very edge that produced the ringing. Loop restoration then applies either a separable Wiener filter or a self-guided restoration filter per restoration unit, with the filter taps carried in the bitstream rather than derived by the decoder.

Film-grain synthesis sits outside that chain entirely. Grain parameters are applied to the output picture after loop restoration, and the grained picture is never written back into the reference buffer. That is what makes the tool nearly free for the encoder and awkward for QA: your decoded output differs from the encoder’s own reference frames by design. It also means a hardware decoder needs a grain block in its display pipeline rather than in the decode loop, and some early AV1 decode blocks simply ignore the parameters and output a clean picture.

Tiles are rectangular regions that reset the entropy coder so each can be decoded independently. VP9 exposes tile columns only, with a minimum tile width of 256 luma samples, which caps a 1920-wide frame at four columns; if you are choosing -tile-columns against a specific frame width, the arithmetic is worked through in FFmpeg two-pass VP9 encoding for the web. AV1 allows both tile rows and tile columns and can signal explicit tile sizes so a decoder can seek straight to a tile’s data. H.265 offers tiles plus wavefront parallel processing, in which each CTU row begins once the row above is two CTUs ahead and inherits its entropy context — more parallelism than tiles without paying the compression cost of resetting contexts.

Threading follows from that structure. SVT-AV1 layers picture-level parallelism on top of tiles by splitting the encode into stages that run concurrently across pictures, which is why it keeps scaling well past the tile count while libaom-av1 flattens out. On the decode side, dav1d threads by tile and by frame, so a single-tile 1080p stream decodes on one core no matter how many the device has. A surprising number of “AV1 stutters on this machine” reports are really a single-tile encoder configuration, not a decoder limitation.

Compression Benchmark Data

The following table uses published encode benchmarks and Netflix/Bitmovin research data for 1080p source material encoded at VMAF target 93.

Codec Encoder Relative bitrate vs H.264 Encode speed (1080p, real-time factor) Hardware decode
H.264 libx264 fast 1.0× (baseline) ~40× realtime Universal
VP9 libvpx-vp9 −35 to −40% ~3–5× realtime Broad (2017+)
H.265 libx265 medium −40 to −50% ~8–12× realtime (SW); ~40× (HW) Apple, Samsung, Intel
H.265 VideoToolbox (Apple Silicon) −40 to −50% ~40–80× realtime Native on Apple
AV1 libsvtav1 preset 6 −55 to −60% ~5–8× realtime Chrome 90+, Edge 92+, Safari 16.4+, Android 2022+
AV1 libaom-av1 speed 4 −55 to −60% ~0.3–1× realtime Same as above
AV1 libsvtav1 preset 10 −45 to −50% ~30–50× realtime Same as above
AV1 av1_qsv / av1_nvenc −35 to −45% ~60× realtime Same as above
H.265 libx265 slow −45 to −52% ~3–5× realtime Apple, Samsung, Intel

The chart below plots the middle column of that table as an index, with H.264 fixed at 100, so the compression gap and the encode-speed cost sit side by side:

Bitrate index at VMAF 93, 1080p SDR Horizontal bar chart. With H.264 set to an index of 100, VP9 needs about 63, H.265 about 55, and AV1 about 43 to reach the same VMAF 93 score. Software encode speed is printed beside each bar: about 40 times realtime for H.264, 3 to 5 for VP9, 8 to 12 for H.265 and 5 to 8 for AV1. Bitrate index at VMAF 93, 1080p SDR — lower is better software encode speed H.264 libx264 fast VP9 libvpx-vp9 H.265 libx265 medium AV1 libsvtav1 preset 6 100 63 · −37% 55 · −45% 43 · −57% ~40× realtime ~3–5× realtime ~8–12× realtime ~5–8× realtime 0 25 50 75 100 bitrate index (H.264 = 100)

Read the last three rows as the shape of the tradeoff rather than as fixed constants. Moving SVT-AV1 from preset 6 to preset 10 gives up roughly a third of AV1’s advantage over H.264 but encodes six to eight times faster; a fixed-function AV1 encoder such as av1_qsv on Intel Arc or av1_nvenc on Ada-generation NVIDIA silicon gives up more still, landing near H.265 quality at a speed no software encoder reaches. For a VOD library the software presets win because the encode is amortised over every view. For live, near-live, or user-generated content, the hardware encoders are the only ones that fit the time budget at all.

Tradeoff: AV1 with libsvtav1 at preset 6 achieves near-VP9 encode speeds while retaining most of the compression gains. libaom-av1 (the reference encoder) is roughly 10–30× slower than libsvtav1 for equivalent quality — use libaom-av1 only for archival masters or when libsvtav1 is unavailable. When these encodes run in constrained serverless environments, that speed gap becomes a direct billing difference — see AV1 vs VP9 encode time on AWS Lambda for the per-clip cost breakdown and the timeout limits that rule libaom out entirely.

Warning: H.265 software decode performance on low-end Android devices can cause frame drops even at 720p. Always profile your target device class before committing to H.265 as your primary delivery format.

Rate Control Modes and What a CRF Number Means

-crf is not one algorithm. It is a name that three unrelated encoders use for their quality-targeting mode, on three scales that were never calibrated against each other. The quantiser index a given CRF resolves to depends on the encoder’s internal lambda model, its adaptive quantisation, and the frame type, so transplanting a number between encoders is a guess, not a conversion.

Encoder Quality flag Scale Typical 1080p VOD Library mode
libx265 -crf 0–51 24–28 CRF, optionally bounded by VBV
libvpx-vp9 -crf with -b:v 0 0–63 30–34 end-usage=q (constant quality)
libvpx-vp9 -crf with -b:v <cap> 0–63 30–34 end-usage=cq (constrained quality)
libsvtav1 -crf 1–63 28–35 --rc 0 (CRF)
libaom-av1 -crf with -b:v 0 0–63 28–34 --end-usage=q

Four modes matter in practice. Constant quality spends whatever bits a scene needs and gives you a predictable perceptual result with an unpredictable file size — correct for a VOD library where storage is cheap relative to re-encoding. Capped CRF is constant quality plus a video buffering verifier bound (vbv-maxrate/vbv-bufsize in x265, maxrate/bufsize in SVT-AV1, -maxrate/-bufsize in libvpx), which leaves easy scenes alone but refuses to let a confetti shot spike past a peak your players can sustain. Two-pass VBR targets an average bitrate across the whole file, which is what you want when a rung in a ladder has a contractual byte budget. CBR fills a fixed pipe and is only appropriate for low-delay live.

For adaptive streaming the choice interacts with the ladder: a rung encoded in unbounded CRF mode can momentarily exceed the bandwidth its manifest advertises, and the player’s throughput estimator will down-switch when it sees the segment arrive late. That is why the rungs in building an HLS bitrate ladder with FFmpeg carry an explicit buffer bound rather than a bare quality target.

Warning: SVT-AV1’s -crf only behaves as constant quality while rc=0. Passing -b:v on the FFmpeg command line silently switches the encoder into VBR and your CRF value is reinterpreted as a starting point, not a target. Set the cap through -svtav1-params "maxrate=…:bufsize=…" instead.

Step-by-Step Implementation

Step 1 — Encode VP9 for Chromium/Firefox/Edge delivery

VP9 in a WebM container is the safe royalty-free choice for browsers that do not yet support AV1 hardware decode. The key flags for production use:

ffmpeg -i input.mp4 \
  -c:v libvpx-vp9 \
  -crf 32 \               # constrained quality; lower = higher quality (range 0–63)
  -b:v 0 \                # set to 0 to engage CRF-only mode (no bitrate cap)
  -row-mt 1 \             # row-based multithreading: ~2× faster on multicore CPUs
  -tile-columns 2 \       # split frame into 4 tile columns for parallel decode
  -tile-rows 1 \          # 2 tile rows; useful for 1080p+ content
  -cpu-used 2 \           # quality/speed tradeoff: 0=slowest/best, 8=fastest/worst
  -threads 8 \            # match to available cores
  -c:a libopus \          # Opus is the standard audio codec for WebM
  -b:a 128k \
  output_vp9.webm

Note on -b:v 0: Omitting this flag while setting -crf puts VP9 into constrained-quality mode with a bitrate ceiling. Setting -b:v 0 with -crf engages true constant-quality mode, which is correct for VOD.

Step 2 — Encode H.265 for Safari and iOS

Safari on macOS 11+ and iOS 11+ supports H.265 natively. The critical requirement is the hvc1 brand tag — without it, Safari will refuse to play the file even when the codec is otherwise valid:

ffmpeg -i input.mp4 \
  -c:v libx265 \
  -crf 26 \               # CRF range 0–51; 23–28 is typical VOD range (lower = better)
  -preset medium \        # slow/medium balance: quality vs encode time
  -tag:v hvc1 \           # MANDATORY for Safari/iOS: marks the stream as HEVC brand 'hvc1'
  -movflags +faststart \  # relocate moov atom to file start for progressive download
  -c:a aac \              # AAC is required for MP4/Safari compatibility
  -b:a 128k \
  output_h265.mp4

# Apple Silicon / Intel Quick Sync hardware encode (much faster, slightly lower quality ceiling):
ffmpeg -i input.mp4 \
  -c:v hevc_videotoolbox \ # macOS hardware encoder via VideoToolbox
  -q:v 50 \                # VideoToolbox uses -q:v (0–100 scale); 40–60 is typical
  -tag:v hvc1 \            # still required even with hardware encoder
  -movflags +faststart \
  -c:a aac -b:a 128k \
  output_h265_hw.mp4

Step 3 — Encode AV1 with SVT-AV1

SVT-AV1 (libsvtav1) is the production-grade AV1 encoder. It is architecturally different from libaom-av1: it uses a tile-parallel architecture that scales linearly with core count, making it viable for real-time or near-real-time transcoding pipelines:

ffmpeg -i input.mp4 \
  -c:v libsvtav1 \
  -crf 30 \               # CRF range 1–63; 28–35 is typical VOD range
  -preset 6 \             # 0=slowest/best, 13=fastest/worst; 4–7 is production sweet spot
  -svtav1-params \
    "film-grain=8:film-grain-denoise=0" \  # encode grain params, skip denoising pass
  -pix_fmt yuv420p \      # force 4:2:0 for maximum compatibility; AV1 supports 4:4:4 but decode HW may not
  -movflags +faststart \
  -c:a libopus \          # Opus works in MP4 (ISO 23003-3) and MKV; use AAC if targeting iTunes
  -b:a 128k \
  output_av1.mp4

Warning: libsvtav1 and librav1e write AV1 bitstreams but do not write WebM containers directly. Use -f webm with libvpx-vp9 for WebM output. For AV1 in WebM you must use libaom-av1 or a two-step encode + remux.

Step 4 — HTML5 <video> fallback ladder

The <source> order determines which codec the browser selects. Place the most efficient codec first; the browser picks the first type string it can decode. Explicit codec parameter strings prevent the browser from downloading the stream before knowing it is unsupported:

<video
  controls
  playsinline
  preload="metadata"
  width="100%"
  style="aspect-ratio: 16/9;"
>
  <!-- AV1 in MP4: codec string av01.0.05M.08 = AV1 profile 0, level 3.1, Main tier, 8-bit -->
  <source src="video.av1.mp4"  type='video/mp4; codecs="av01.0.05M.08"'>

  <!-- VP9 in WebM: royalty-free fallback for Chrome/Firefox/Edge without AV1 HW decode -->
  <source src="video.vp9.webm" type='video/webm; codecs="vp9"'>

  <!-- H.265 in MP4: Safari-primary; Edge and Chrome support on Win 10+ (OS-dependent) -->
  <source src="video.h265.mp4" type='video/mp4; codecs="hvc1"'>

  <!-- H.264 baseline fallback: last resort, universal support -->
  <source src="video.h264.mp4" type='video/mp4; codecs="avc1.42E01E"'>

  <!-- Always include a caption track; sync survives ABR switches -->
  <track kind="captions" src="captions.vtt" srclang="en" label="English" default>
</video>

For ABR / MSE-based players, use MediaSource.isTypeSupported() to select the manifest at runtime:

function selectOptimalCodec() {
  // AV1 hardware decode landed in Chrome 90, Edge 92, Safari 16.4, Firefox 113
  if (MediaSource.isTypeSupported('video/mp4; codecs="av01.0.05M.08"')) {
    return loadAV1Manifest();
  }
  // VP9 hardware decode available on most discrete GPUs since 2017
  if (MediaSource.isTypeSupported('video/webm; codecs="vp9"')) {
    return loadVP9Manifest();
  }
  // H.265 check: returns true only when the OS-level codec is installed
  if (MediaSource.isTypeSupported('video/mp4; codecs="hvc1"')) {
    return loadH265Manifest();
  }
  return loadH264Fallback();
}

Tradeoff: MediaSource.isTypeSupported() reports codec availability, not hardware acceleration. AV1 software decode on a 2018-era Android phone will pass the check but cause frame drops at 1080p. Supplement with navigator.hardwareConcurrency and navigator.deviceMemory heuristics to downgrade to VP9 on low-end devices.

Step 5 — A capped-CRF 1080p rung in all three codecs

The commands in steps 1–3 use unbounded constant quality, which is right for a single progressive-download file and wrong for a library that also feeds a streaming player. This is the same 1080p rung encoded with a peak-rate bound, a fixed four-second keyframe interval at 24 fps, and scene-cut insertion disabled so the keyframes land on the same timestamps in every codec — the precondition for switching between them or for segmenting them later:

# H.265 — VBV bound expressed in kbit; keyint == min-keyint pins the GOP
ffmpeg -i master.mov -c:v libx265 -crf 24 -preset medium \
  -x265-params "vbv-maxrate=6000:vbv-bufsize=12000:keyint=96:min-keyint=96:scenecut=0" \
  -tag:v hvc1 -movflags +faststart \
  -c:a aac -b:a 128k h265_1080p.mp4

# AV1 — cap goes through svtav1-params, NOT -b:v, or rc flips to VBR
ffmpeg -i master.mov -c:v libsvtav1 -crf 30 -preset 6 \
  -svtav1-params "rc=0:maxrate=5000:bufsize=10000:keyint=96:scd=0:tile-columns=1" \
  -pix_fmt yuv420p -movflags +faststart \
  -c:a libopus -b:a 128k av1_1080p.mp4

# VP9 — a non-zero -b:v is deliberate here: it selects constrained quality
ffmpeg -i master.mov -c:v libvpx-vp9 -crf 32 -b:v 5000k \
  -maxrate 6000k -bufsize 12000k \
  -g 96 -keyint_min 96 \
  -row-mt 1 -tile-columns 2 -cpu-used 2 \
  -c:a libopus -b:a 128k vp9_1080p.webm

Note the deliberate reversal in the third command. Step 1 warned that -crf without -b:v 0 puts libvpx into constrained quality rather than pure CRF; here constrained quality is exactly the mode you want, so the bitrate argument is a ceiling the encoder is allowed to spend up to, not a target it must hit. The AV1 command drops to a single tile column because a 1920-wide frame gives you at most two useful ones and each extra tile costs a little compression efficiency at the boundary — worth paying only when you actually need the decode parallelism.

Tradeoff: pinning the GOP with scenecut=0 / scd=0 costs roughly 1–3% bitrate at the same VMAF, because the encoder can no longer place a keyframe on a hard cut where one would have paid for itself. You buy segment alignment with that. On a single progressive file with no segmenting, leave scene-cut detection on.

Parameter Reference

Flag / attribute Context Effect
-crf libvpx-vp9, libx265, libsvtav1 Constant-rate-factor quality target; lower = better quality, larger file
-b:v 0 libvpx-vp9 Disables bitrate ceiling, enabling true CRF mode (mandatory with -crf in VP9)
-row-mt 1 libvpx-vp9 Enables row-based multithreading; speeds up VP9 encode by 1.5–2× on 4+ cores
-tile-columns N libvpx-vp9, libsvtav1 Splits the frame into 2^N horizontal tile columns for parallel encode/decode
-tag:v hvc1 libx265, hevc_videotoolbox Sets the MP4 brand to hvc1; required for Safari/iOS H.265 playback
-movflags +faststart MP4 outputs Moves the moov atom to the file start so playback begins before the file fully downloads
-preset N libsvtav1 0 = slowest/best; 13 = fastest/worst; 5–7 is practical for VOD pipelines
-pix_fmt yuv420p AV1 Forces 4:2:0 chroma subsampling for maximum hardware decoder compatibility
codecs="av01.0.05M.08" <source> type string AV1 profile 0 (main), level 3.1, Main tier, 8-bit; matches most 1080p SDR content
codecs="hvc1" <source> type string H.265 brand string; browser checks OS codec availability before downloading
-svtav1-params rc=N libsvtav1 0 = CRF, 1 = VBR, 2 = CBR (low-delay only); CRF is only honoured while rc=0
maxrate / bufsize libsvtav1, libvpx-vp9 Peak rate and buffer size in kbit; converts constant quality into capped CRF
vbv-maxrate / vbv-bufsize libx265 (-x265-params) Same bound for H.265; values are in kbit, not bits per second
-g N / keyint all Keyframe interval in frames; set equal to keyint_min / min-keyint to pin the GOP
scenecut=0 / scd=0 libx265, libsvtav1 Disables scene-cut keyframe insertion so GOP boundaries stay aligned across rungs
-lag-in-frames N libvpx-vp9, libaom-av1 Lookahead depth for alt-ref frame placement; 25 is the practical VOD default
-aq-mode N libvpx-vp9, libx265 Adaptive quantisation strategy; moves bits toward flat areas that show banding
-pix_fmt yuv420p10le libx265 (Main10), libsvtav1 10-bit encode of an 8-bit source; reduces banding at a small bitrate cost
-tune ssim / -tune 2 libx265 / libsvtav1 Switches the RD metric away from the default psychovisual tuning

Configuring correct MIME type headers on your media server is equally important: a browser that receives Content-Type: video/mp4 without a codec parameter may attempt to decode an AV1 stream with an H.264 decoder and fail silently.

Tradeoffs & Edge Cases

1. SVT-AV1 does not write WebM. libsvtav1 outputs to MP4 or Matroska. If your CDN or player stack requires AV1 in WebM (e.g. some older Chromium builds), you must use libaom-av1, which is significantly slower. In most cases, AV1 in MP4 is the correct choice.

2. H.265 patent ambiguity affects CDN caching. Some CDN edge nodes have had to disable server-side H.265 transcoding due to licensing. Confirm your CDN’s HEVC support before building H.265 into your primary encode pipeline. AWS Elemental MediaConvert and Cloudflare Stream both support H.265 output but require explicit product tiers.

3. VP9 CRF mode requires -b:v 0. Setting -crf alone on libvpx-vp9 activates constrained quality (VBR with a quality floor), not pure CRF. Omitting -b:v 0 results in the encoder guessing a bitrate target from the resolution, often producing bloated files on high-resolution inputs.

4. AV1 film-grain synthesis can confuse QA tools. When using film-grain=N in SVT-AV1, the encoder strips grain from the source and stores parameters in the bitstream. SSIM and PSNR scores will appear lower than the perceptual quality because the reference frame no longer contains grain. Use VMAF with the neg model (trained on film-grain content) for accurate quality assessment.

5. H.265 on Chrome is OS-dependent, not browser-dependent. Chrome on Windows 10+ will play H.265 if the Media Feature Pack is installed; Chrome on macOS 13+ uses VideoToolbox. Chrome on Linux has no H.265 support regardless of FFmpeg build flags. MediaSource.isTypeSupported('video/mp4; codecs="hvc1"') reflects the OS state, not a Chrome-level decision.

6. The level field in a codec string is a promise the decoder can enforce. av01.0.05M.08 declares profile 0, level 3.1, Main tier, 8-bit. Level 3.1 tops out around 1920×1080 at 60 fps; ship a 4K stream still carrying that string and a permissive software decoder plays it while a strict hardware decoder refuses the whole track. The same applies to avc1.640028 and to H.265, where the choice between the hvc1 and hev1 brands is not cosmetic: hev1 permits parameter sets to change mid-stream and in-band, and Safari will not play it. Derive the string from the encoded file rather than copying it from an example.

7. 10-bit is a banding fix, not only an HDR feature. Encoding an 8-bit SDR source at yuv420p10le gives the encoder more headroom in the internal transform and typically removes visible banding from gradients and slow fades for 1–3% more bits. AV1 Main profile carries 10-bit, so hardware AV1 decoders take it; H.265 needs Main10, which most silicon supports; VP9 needs Profile 2, which is much thinner on hardware support and will silently drop many devices to software decode. Use 10-bit freely for AV1 and H.265, cautiously for VP9.

8. Opus in MP4 is standardised but not universally decoded. ISO/IEC 14496-30 and the Opus-in-ISOBMFF mapping make Opus in an MP4 track legal, and Chromium and Firefox decode it. Safari does not, and several smart-TV browsers do not. If a single MP4 has to serve both an AV1-capable browser and Safari, mux AAC into the MP4 rung and keep Opus for the WebM rung — the audio saving is a few kilobits per second and is never worth a silent track.

Browser & Hardware Decode Compatibility Matrix

Codec Chrome 85+ Firefox 93+ Safari 14 Safari 16 Edge 18+ iOS 14 iOS 16 Android (Pixel 6+)
VP9 HW SW/HW SW HW HW SW HW HW
H.265 OS-dep No HW HW OS-dep (Win 10+) HW HW No
AV1 HW (90+) HW (113+) No HW (16.4+) HW (92+) No No HW (Tensor GPU)
H.264 HW HW HW HW HW HW HW HW
VP9 Profile 2 (10-bit) SW/HW SW No SW SW No SW SW
H.265 Main10 OS-dep No HW HW OS-dep (Win 10+) HW HW No
AV1 Main 10-bit HW (90+) HW (113+) No HW (16.4+) HW (92+) No No HW (Tensor GPU)

HW = hardware-accelerated decode available. SW = software only. OS-dep = depends on OS codec installation.

The AV1 row hides a large device-by-device tail: many shipping phones and Macs decode AV1 in software rather than in a fixed-function block. For the full breakdown of which silicon has a hardware AV1 decoder — and how to detect it at runtime with MediaCapabilities — see hardware AV1 decode support by device class.

The AVIF vs WebP compression benchmark methodology applies analogous principles to still-image codec evaluation — the SSIM/VMAF tradeoff analysis maps directly to the video codec selection problem.

Cache-Control headers for video assets interact with codec negotiation: if you serve multiple codec variants from the same URL using content negotiation, the CDN must see a Vary: Accept header to cache them separately, or you will serve incorrect variants to cached clients.

Debugging & Validation

Confirm the codec string your server sends:

# Check Content-Type and inspect the actual video codec in the file
curl -sI https://example.com/video.av1.mp4 | grep -i content-type

# Probe the container and codec without downloading the full file
ffprobe -v quiet -print_format json -show_streams \
  -select_streams v:0 \
  https://example.com/video.av1.mp4 2>/dev/null \
  | python3 -m json.tool | grep -E '"codec_name"|"codec_tag_string"|"pix_fmt"'

Verify hardware decode is active in Chrome:

  1. Open chrome://media-internals/ while the video plays.
  2. Find the active player entry and check the video_decoder property.
  3. A value of VpxVideoDecoder (software) vs MojoVideoDecoder or D3D11VideoDecoder (hardware) identifies the decode path.

Check VMAF score for your encode output:

ffmpeg -i output_av1.mp4 -i input.mp4 \
  -lavfi "[0:v][1:v]libvmaf=model=version=vmaf_v0.6.1:log_fmt=json:log_path=vmaf.json" \
  -f null -

# Read the aggregate VMAF score
python3 -c "import json; d=json.load(open('vmaf.json')); print(d['pooled_metrics']['vmaf']['mean'])"

Warning: A VMAF score below 85 on 1080p content at your target bitrate indicates the CRF is too high (quality too low) or the encoder preset is too fast. For VOD, target VMAF 93–96; for live or near-live, VMAF 88–92 is an acceptable trade for encode speed.

Validate the <video> fallback ladder in DevTools:

Open the Network panel, filter by media, and reload the page. The browser must request exactly one video resource. If you see two requests (e.g. both the AV1 and VP9 source files), the first <source> type string is either missing or invalid — the browser downloaded the first file to detect the codec rather than reading the MIME type string.

Derive the codec string from the file instead of guessing it:

# The four av01 fields come from profile, level+tier, and bit depth in the sequence header
ffprobe -v error -select_streams v:0 \
  -show_entries stream=profile,level,pix_fmt,width,height,r_frame_rate \
  -of default=nw=1 av1_1080p.mp4

# MP4Box prints the sample entry's own codec string, which is what the browser parses
MP4Box -info av1_1080p.mp4 2>&1 | grep -iE 'RFC6381|Codec|Visual'

ffprobe’s level is printed as an integer — 5 means level 2.1, 8 means 3.0, 9 means 3.1, 12 means 4.0, 13 means 4.1 — and the MP4Box RFC 6381 line is the authoritative string to paste into the type attribute. If the two disagree, the muxer wrote a sample entry that does not describe the bitstream, and you should remux rather than hand-edit the HTML.


Frequently Asked Questions

Is an H.264 rung still worth shipping?

Yes, as the last entry in the source list. H.264 is the only codec with a hardware decoder in effectively every shipping browser, TV, set-top box, and in-app WebView, and it is what a stripped-down embedded WebView falls back to when it cannot parse anything above it. The rung costs one extra encode and some origin storage, and a client that can decode something better never fetches it — the browser stops at the first type it accepts. The cost is bounded and the failure it prevents is a black player.

Do CRF values mean the same thing in libx265, libsvtav1, and libvpx-vp9?

No. libx265 uses a 0–51 scale, libsvtav1 and libvpx-vp9 use 1–63, and none of the three are calibrated against each other. On 1080p SDR material, libx265 CRF 26, libsvtav1 CRF 30, and libvpx-vp9 CRF 32 with -b:v 0 tend to land in a broadly similar VMAF band, which is why those numbers appear in the commands above — but they are a starting point for a sweep, not a conversion table. Encode three or four CRF values across a representative sample of your own content, score each with VMAF, and pick the point where the curve flattens.

Can I drop the VP9 rung now that AV1 is widely supported?

Only if your telemetry says so. AV1 decode support and AV1 hardware decode support are different populations: a large number of shipping devices report the codec as playable and then decode it in software, which drains battery and drops frames at 1080p. VP9 remains the cheapest safety net for that middle tier because its hardware decoder shipped years earlier and is present on silicon that predates any AV1 block.

Does AV1 pay off for short autoplay loops?

Usually yes for hero loops served at high volume, because the encode is paid once and the byte saving is collected on every impression — a five-second loop at a few hundred kilobytes, multiplied by millions of views, repays a slow encode quickly. It rarely pays for user-generated clips with a rarely-requested tail of near-zero views, where a fast VP9 or H.264 encode and a shorter time to publish are worth more than 20% fewer bytes on a file almost nobody fetches.

Why does my AV1 file play in Chrome but not in Safari?

Two usual causes. Either the <source> element carries a bare type="video/mp4" with no codecs parameter, so Safari cannot evaluate the track before fetching it and skips to the next source, or the audio track is Opus inside MP4, which Safari does not decode — the video track is fine and the file still fails. Add the full av01 codec string to the type attribute and mux AAC for any MP4 rung that Safari is expected to reach.