Home › Audio & video › Video coding
algorithm low

GOP structure

Problem A video encoder operator must decide how to arrange frames so that viewers can seek and switch streams quickly, latency stays low and file size stays small. These goals pull against each other.

How an encoder orders I, P and B frames into groups of pictures: the knob behind seekability, compression, latency and stream switching.

planning O(n); random access decodes about keyint/2 frames on average; pyramid of mini-GOP m adds log2(m) frames of reorder delay
videogopb-frameskeyframesidrreference-frames
Overview

Most frames are cheap deltas of other frames; a few are self-contained. A GOP (group of pictures) starts at a keyframe (IDR) that nothing before it may reference, so decoding can start there. Between anchors (I/P) sit B frames predicted from both past and future, so the encoder sends frames out of display order. Choosing the interval, the B depth and the reference shape trades bitrate against seek time, latency and error recovery.

Key facts

Use when

  • Setting keyint and B-frames for VOD, live, ABR ladders or scrubbing proxies.
  • Segmented HTTP streaming, real-time calls where a receiver asks for a new keyframe, and any storage or editing workflow that seeks into long-GOP video.

Avoid when

  • All-intra codecs (ProRes, MJPEG) or image sequences: there is no GOP to tune.

Deep dive

How it works

In: a frame sequence with scene-change and cost estimates, keyint, B depth and reference count · Out: a frame type (I, P, B) and decode order for every frame · State: the reference buffer and the lookahead window · Cost: planning O(n); the cost is in the encoder's extra reference searches

  1. Frame types: I (intra only), P (predicts from earlier decoded frames), B (predicts from earlier and later frames; may or may not be a reference itself).
  2. A closed GOP begins with an IDR: it flushes the reference buffer, and no later frame refers to anything before it. An open GOP begins with a non-IDR I; leading B frames that precede it in display order may reference the previous GOP (HEVC: CRA with RASL pictures; H.264: recovery-point SEI). Open GOPs save bits but cannot be cut or joined without dropping or re-encoding the leading frames.
  3. Anchors are placed every mini frames (the mini-GOP). The encoder codes the next anchor first, then the B frames between it and the previous anchor, so decode order differs from display order.
  4. A hierarchical B pyramid codes the middle frame first with both anchors as references, then recurses; frames that fewer others depend on get a higher QP (x264 ipratio 1.40 and pbratio 1.30 are the QP factors I to P and P to B).
display:  I0 b1 B2 b3 B4 b5 B6 b7 P8        mini = 8, levels: P8/I0=0, B4=1, B2 B6=2, b=3
decode:   I0 P8 B4 B2 b1 b3 B6 b5 b7
refs:     P8<-I0   B4<-I0,P8   B2<-I0,B4   b1<-I0,B2   (b = non-reference, droppable)
  1. Frame type decision runs in a lookahead: estimate intra vs inter cost per frame; a frame whose inter cost is not clearly below its intra cost is a scene cut and becomes a keyframe; B-adapt chooses how many B frames fit in each mini-GOP.
  2. A decoder needs num_reorder frames of buffering: the largest number of frames decoded before a frame that are displayed after it.

Code

Plans a closed-GOP hierarchical-B structure in decode order and validates it: every reference is decoded first and never crosses an IDR.

#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Kind { Idr, P, BRef, B } // BRef = B used as a reference, B = disposable

#[derive(Clone, Debug)]
pub struct Frame {
    pub poc: usize,       // display index (picture order count)
    pub kind: Kind,
    pub refs: Vec<usize>, // POCs this frame predicts from
    pub level: u8,        // 0 = anchor, 1.. = depth in the B pyramid
}

/// Closed GOPs of at most `keyint` frames, anchors every `mini` frames,
/// B frames between them as a binary pyramid. Returns frames in DECODE order.
pub fn plan(n: usize, keyint: usize, mini: usize, nref: usize) -> Vec<Frame> {
    assert!(keyint >= 1 && mini >= 1 && nref >= 1);
    let mut out = Vec::with_capacity(n);
    let mut start = 0;
    while start < n {
        let last = (start + keyint).min(n) - 1;
        out.push(Frame { poc: start, kind: Kind::Idr, refs: vec![], level: 0 });
        let mut anchors = vec![start];
        let mut a = start;
        while a < last {
            let hi = (a + mini).min(last);
            let refs = anchors.iter().rev().take(nref).copied().collect();
            out.push(Frame { poc: hi, kind: Kind::P, refs, level: 0 });
            fill(&mut out, a, hi, 1); // pyramid strictly between the two anchors
            anchors.push(hi);
            a = hi;
        }
        start = last + 1;
    }
    out
}

fn fill(out: &mut Vec<Frame>, lo: usize, hi: usize, level: u8) {
    if hi - lo < 2 { return; }
    let mid = (lo + hi) / 2;
    let kind = if hi - lo > 2 { Kind::BRef } else { Kind::B }; // referenced only if it has children
    out.push(Frame { poc: mid, kind, refs: vec![lo, hi], level });
    fill(out, lo, mid, level + 1);
    fill(out, mid, hi, level + 1);
}

/// Returns (reorder depth, longest reference distance in frames).
pub fn validate(dec: &[Frame], n: usize) -> (usize, usize) {
    let mut decoded = vec![false; n];
    let (mut cur_idr, mut max_dist) = (0, 0);
    for f in dec {
        if f.kind == Kind::Idr { cur_idr = f.poc; }
        for &r in &f.refs {
            assert!(decoded[r], "ref {r} of {} not decoded yet", f.poc);
            assert!(r >= cur_idr, "ref {r} crosses IDR at {cur_idr}"); // closed GOP
            max_dist = max_dist.max(r.abs_diff(f.poc));
        }
        assert!(!decoded[f.poc], "frame {} decoded twice", f.poc);
        decoded[f.poc] = true;
    }
    assert!(decoded.iter().all(|&d| d), "missing frames");
    let reorder = (0..dec.len())
        .map(|i| dec[..i].iter().filter(|g| g.poc > dec[i].poc).count())
        .max().unwrap_or(0);
    (reorder, max_dist)
}

Run as tests: plan(9, 100, 4, 1) decodes as POC 0 4 2 1 3 8 6 5 7 and validates to reorder 2; plan(20, 10, 8, 2) shows IbBbBbBbPPIbBbBbBbPP with reorder 3 (log2 of 8) and two IDRs; mini = 1 gives reorder 0.

Complexity and constants

  • Planning is O(n). The cost lives in the encoder: a B frame searches two reference lists, and each extra ref multiplies motion-estimation work.
  • Reorder delay is log2(mini) frames for a full pyramid (3 for mini 8), plus lookahead (x264 rc-lookahead default 40) held in memory as raw frames.
  • Decoded picture buffer: references plus reorder frames; at 4K 4:2:0 8-bit one frame is about 12 MB.
  • Random access: seeking to time t decodes from the previous keyframe, about keyint/2 frames on average; scrubbing cost is proportional to keyint.
  • Measured on one synthetic clip (20 s, 640x360, 30 fps noisy test pattern, x264 medium, CRF 23, scenecut off): keyframe 35 kB, P 10.7 kB, B 5.1 kB. Real footage differs; the order of the ratios is what carries over.

Efficiency

  • Cost: the bound is bits per frame versus seekability. On one synthetic clip (20 s, 640x360, 30 fps, x264 medium, CRF 23, scenecut off; real footage differs) a keyframe was 35 kB, a P frame 10.7 kB and a B frame 5.1 kB. Seeking to time t decodes about keyint/2 frames on average.
  • Levers:
    • fewer keyframes — bframes 3: keyint 250 gave 3.99 MB, keyint 30 gave 4.71 MB (+18%), measured on the clip above — use the longest interval seek, segment and recovery needs allow
    • B frames — bframes 0 gave 5.00 MB, bframes 3 gave 4.10 MB (-18%); bframes 8 with b-adapt 2 gave 3.90 MB for roughly half the encode speed (about 440 to 250 fps here) — depth beyond 3-4 rarely pays
    • closed GOPs encoded as chunks — independent, so separate cores or machines, no quality loss at the seams — large files
    • non-reference B frames at the bottom of the pyramid — can be dropped for cheap temporal scaling — scalable delivery
    • budget scenecut keyframes — on the noisy clip they added 2 keyframes and 2.7% size; on real cuts they pay; for ABR use one forced keyframe list for all renditions
  • Naive → standard → state of the art:
    • all-intra · largest files, seek anywhere
    • closed GOP, keyint 250, bframes 3 · 4.10 MB on the clip above
    • open GOP, hierarchical B pyramid and lookahead · fewer bytes, but cutting needs care at the leading frames; zero B frames with intra refresh (low-latency) when latency beats about 18% bitrate

Implementation notes

  • Streaming: segment = whole number of GOPs, each beginning with an IDR; use identical segment boundaries in every rung of the ladder so players can switch without re-sync. Common practice is 2 s or 4 s GOPs; fix them as frames (keyint = fps * seconds, min-keyint equal, scenecut off) or force keyframe timestamps from one list for all rungs.
  • Container: with B frames, PTS differs from DTS; MP4 needs ctts offsets, and the first PTS may be non-zero (containers).
  • Hardware encoders expose GOP size and B-frame count, often with fixed pyramid depth (hardware acceleration).
  • x264 b-pyramid normal keeps one B level as references (not Blu-ray compliant); strict allows a pure pyramid. With open-gop, x264 closes GOPs via recovery points and needs B frames; x265 enables open GOP by default.

Pitfalls

  • Open GOP in HLS/DASH: a segment starting on a CRA or non-IDR I can show corrupted leading frames after a switch or seek. Use IDR for segment starts.
  • Scenecut keyframes shift GOP boundaries differently per rendition, breaking alignment. Disable it or use a forced-keyframe list.
  • x264/x265 min-keyint: a scene cut closer than that to the last keyframe becomes a plain I or P frame, not an IDR, so segment starts can be missed.
  • Very long GOPs ("infinite") make seeking and error recovery slow: a lost reference corrupts frames until the next keyframe.
  • Treating B depth as free: it adds latency (reorder + lookahead) and DPB memory, which matters for live and WebRTC.
  • Cutting with stream copy (-c copy) can only start at a keyframe, not at the requested frame.

Variants and alternatives

Intra refresh (gradual decoder refresh)a moving column or row of intra blocks recovers streams without IDR bit spikes; used in low-latency and RTC (x264 --intra-refresh).
Scalable / simulcast layers (simulcast and SVC)temporal layers are a reference structure where dropping higher layers keeps the stream decodable.
All-intramezzanine and editing codecs; every frame random-access, bitrate several times higher.
AV1no B frame type: a frame is shown or hidden; hidden altref frames are decoded early and shown later via show_existing_frame.
Low-delay P / BHEVC low-delay configurations use only past references: no reorder delay.

In production

  • Streaming: x264 and x265 behind FFmpeg encode much of VOD and live; HTTP Live Streaming says any media segment containing H.264 video SHOULD contain an IDR (RFC 8216), so ABR packagers want segment-aligned keyframes.
  • Real-time calls: senders use P-only streams and send a keyframe when the receiver reports picture loss (RFC 4585, PLI/FIR).
  • Codecs: AV1 key frames and show_existing_frame are defined in the AV1 specification (Sources).

Implementations

namelangrepoinstallverdictlicencehealthas_ofnotes
x264Cvideolan/x264brew install x264————--keyint --bframes --b-pyramid --open-gop --scenecut
x265C++multicoreware/x265_gitbrew install x265————open GOP default, --gop-lookahead
SVT-AV1CAOMediaCodec/SVT-AV1brew install svt-av1————hierarchical levels, --keyint, forward key frames
libaomCaombrew install aom————AV1 reference encoder, altref/lag-in-frames
FFmpegCFFmpeg/FFmpegbrew install ffmpeg————-g, -bf, -force_key_frames; ffprobe -show_frames shows types

Sources

  • Thomas Wiegand, Gary J. Sullivan, Gisle Bjontegaard, Ajay Luthra, Overview of the H.264/AVC Video Coding Standard, IEEE TCSVT 2003. paper
  • Gary J. Sullivan, Jens-Rainer Ohm, Woo-Jin Han, Thomas Wiegand, Overview of the High Efficiency Video Coding (HEVC) Standard, IEEE TCSVT 2012. paper
  • ITU-T H.264 and H.265 (IDR, CRA, recovery point, POC, DPB). H.264 · H.265
  • x264 encoder/slicetype.c (scenecut and B-adapt decisions) and x264 --fullhelp (defaults). code
  • x265 CLI documentation, --open-gop, --keyint, --min-keyint. docs
  • AOMedia, AV1 Bitstream & Decoding Process Specification (show_existing_frame, key frames). spec

Builds on

video codecs · plannedH.264/HEVC/AV1 basics: frames, GOPs, hardware decode/encode APIs and latency settings

Used by

Adaptive bitrate streamingA player on a link whose speed changes unpredictably must keep media playing without stalls while showing the best quality it can. For each next piece it must decide which of several pre-encoded versions to fetch.
chunked parallel encoding · plannedSplit video at keyframes, encode chunks in parallel, stitch with rate consistency
low latency video encoding · plannedVideo encoding tuned for latency: no B-frames, intra refresh, slice output, low-delay rate control
simulcast svc · plannedSimulcast and scalable video coding layers for per-receiver quality
video rate control · plannedCBR/VBR/CRF rate control with VBV buffer model and lookahead
Video transcoding ladderA service that accepts uploaded or recorded video must play it on phones, laptops and TVs over slow and fast links. It needs to turn one source file into several versions that players can switch between.

Systems (5):

Alternatives and related

motion estimation · plannedBlock matching and sub-pixel motion search for inter prediction
rate distortion optimization · plannedChoose modes minimizing D + lambda*R for coding decisions
scene cut detection · plannedDetect shot changes from frame-difference statistics
media containers · plannedMP4/ISO BMFF, fragmented MP4/CMAF, Matroska and MPEG-TS mux/demux
hardware video acceleration · plannedGPU/ASIC encode and decode APIs (NVENC, VideoToolbox, VA-API) with zero-copy surfaces