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 128ร128 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:
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 |
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.
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.
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.
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 |
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.
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 |
HW = hardware-accelerated decode available. SW = software only. OS-dep = depends on OS codec installation.
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:
- Open
chrome://media-internals/while the video plays. - Find the active player entry and check the
video_decoderproperty. - A value of
VpxVideoDecoder(software) vsMojoVideoDecoderorD3D11VideoDecoder(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.
Related
- MIME type configuration for modern media servers โ serve correct
Content-Typeheaders so browsers decode VP9, H.265, and AV1 without guessing - Debugging incorrect Content-Type headers for WebM videos โ fix the specific case where
video/webmreturnsapplication/octet-stream - AVIF vs WebP compression benchmarks โ the same SSIM/VMAF methodology applied to next-gen still-image formats
- Cache-Control headers for image and video assets โ immutable caching and
Varyheader strategy for multi-codec video delivery - Core Media Fundamentals & Next-Gen Formats โ parent section covering format strategy, codec selection, and delivery pipeline fundamentals