Skip to main content

Crate mpeg_pes

Crate mpeg_pes 

Source
Expand description

PES (Packetized Elementary Stream) depacketization + PTS/DTS — ISO/IEC 13818-1 (Rec. ITU-T H.222.0) §2.4.3.6 / §2.4.3.7.

mpeg-pes is the sublayer between an MPEG-TS packet layer (e.g. dvb-si’s TsPacket / SiDemux) and an elementary-stream consumer. Feed it the payload bytes of TS packets for one PID (split on payload_unit_start), and it yields PesPackets carrying the stream_id, presentation/decoding timestamps (Pts/Dts, 33-bit @ 90 kHz), and the elementary-stream bytes.

It depends only on broadcast-common and is #![no_std] (+ alloc), WASM-clean.

use mpeg_pes::{PesPacket, StreamId};
// A minimal PES packet: start code, stream_id 0xE0 (video), len, header, PTS, payload.
let bytes = [
    0x00, 0x00, 0x01, 0xE0, 0x00, 0x0A, 0x80, 0x80, 0x05,
    0x21, 0x00, 0x01, 0x00, 0x01, // PTS = 0
    0xAA, 0xBB, // ES payload
];
let pkt = PesPacket::parse(&bytes).unwrap();
assert_eq!(pkt.stream_id, StreamId(0xE0));
assert!(pkt.header.as_ref().unwrap().pts.is_some());
assert_eq!(pkt.payload, &[0xAA, 0xBB]);

§Examples

Three runnable examples ship with this crate (cargo run -p mpeg-pes --example <name>).

§parse_pes_packet

//! Basic: parse one PES packet from raw bytes and read its stream_id + PTS.
//!
//! Run with: `cargo run -p mpeg-pes --example parse_pes_packet`

use mpeg_pes::{PesPacket, StreamId};

fn main() {
    // A minimal PES packet: start_code(00 00 01), stream_id 0xE0 (video),
    // PES_packet_length, flags, PES_header_data_length, a 5-byte PTS, then payload.
    let bytes = [
        0x00, 0x00, 0x01, 0xE0, // packet_start_code_prefix + stream_id
        0x00, 0x0A, // PES_packet_length = 10
        0x80, 0x80, 0x05, // '10' marker, PTS_DTS_flags=10, PES_header_data_length=5
        0x21, 0x00, 0x01, 0x00, 0x01, // PTS = 0 (33-bit, 90 kHz)
        0xAA, 0xBB, // elementary-stream payload
    ];

    let pkt = PesPacket::parse(&bytes).expect("valid PES packet");

    println!("stream_id      : {:#04X}", pkt.stream_id.0);
    println!("is_video       : {}", pkt.stream_id == StreamId(0xE0));
    println!("packet_length  : {}", pkt.pes_packet_length);

    let header = pkt
        .header
        .as_ref()
        .expect("video PES carries an optional header");
    match header.pts {
        Some(pts) => println!(
            "PTS            : {} ticks ({:.6}s)",
            pts.ticks(),
            pts.seconds()
        ),
        None => println!("PTS            : (none)"),
    }
    println!("payload        : {:02X?}", pkt.payload);
}

§extract_pts

//! Advanced: depacketize a real MPEG-TS capture, reassemble PES on one PID,
//! and report the PTS timeline.
//!
//! Run with: `cargo run -p mpeg-pes --example extract_pts`
//!
//! Reads the committed `m6-single.ts` fixture from the workspace-shared
//! `fixtures/` tree at runtime.

use mpeg_pes::{PesAssembler, PesPacket};

const PKT: usize = 188;
const PES_PID: u16 = 0x0082; // an ES PID carrying PES with PTS in this capture

/// (pid, payload_unit_start, payload) for a TS packet that carries payload.
fn ts_payload(p: &[u8]) -> Option<(u16, bool, &[u8])> {
    if p.len() != PKT || p[0] != 0x47 {
        return None;
    }
    let pid = (u16::from(p[1] & 0x1F) << 8) | u16::from(p[2]);
    let pusi = p[1] & 0x40 != 0;
    let start = match (p[3] >> 4) & 0x03 {
        1 => 4,                     // payload only
        3 => 5 + usize::from(p[4]), // adaptation field, then payload
        _ => return None,           // 0 reserved / 2 adaptation only
    };
    (start < PKT).then(|| (pid, pusi, &p[start..]))
}

fn main() {
    // Fixtures live in the workspace-shared `fixtures/` tree, not under a
    // sibling crate. A committed fixture that cannot be read is a bug, not a
    // reason to skip — so a missing/unreadable fixture is a hard failure.
    let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../fixtures/ts/m6-single.ts");
    let ts = std::fs::read(path)
        .unwrap_or_else(|e| panic!("committed fixture {path} could not be read: {e}"));

    let mut asm = PesAssembler::new();
    let mut pes = Vec::new();
    for pkt in ts.chunks(PKT) {
        if let Some((pid, pusi, payload)) = ts_payload(pkt)
            && pid == PES_PID
            && let Some(v) = asm.feed(pusi, payload)
        {
            pes.push(v);
        }
    }
    if let Some(v) = asm.flush() {
        pes.push(v);
    }

    let mut pts_ticks = Vec::new();
    for raw in &pes {
        let pkt = PesPacket::parse(raw).expect("captured PES must parse");
        if let Some(pts) = pkt.header.as_ref().and_then(|h| h.pts) {
            assert!(pts.ticks() < (1 << 33), "PTS must be 33-bit");
            pts_ticks.push(pts.ticks());
        }
    }

    println!(
        "PID {PES_PID:#06X}: {} PES packets, {} with PTS",
        pes.len(),
        pts_ticks.len()
    );
    if let (Some(&min), Some(&max)) = (pts_ticks.iter().min(), pts_ticks.iter().max()) {
        let span = (max - min) as f64 / 90_000.0;
        println!("PTS range      : {min}..={max} ticks (~{span:.3}s span @ 90 kHz)");
    }
}

§build_pes_header

//! Build a PES packet from typed fields, serialize, re-parse, and verify
//! PTS/DTS/ESCR/trick-mode round-trip — demonstrating the typed write/edit side
//! of `mpeg-pes` (mpeg-pes 0.1.3+).
//!
//! Because `PesHeader` is `#[non_exhaustive]` (for future optional-field
//! additions), this example assembles the wire bytes using public typed encoders
//! (`Pts::to_field_bytes`, `Dts::to_field_bytes`, `Escr::to_field_bytes`,
//! `TrickMode::to_byte`), then round-trips through `PesPacket::serialize_into`.
//!
//! Run with:
//!   cargo run -p mpeg-pes --example build_pes_header

use mpeg_pes::{Escr, PACKET_START_CODE_PREFIX, PesPacket, TrickMode};

/// Encode a 33-bit value into a 5-byte PTS field with the given 4-bit prefix.
/// Duplicated here because `mpeg_pes::timestamp::write` is `pub(crate)`.
fn encode_ts(ts: u64, prefix: u8) -> [u8; 5] {
    let ts = ts & 0x1_FFFF_FFFF;
    [
        (prefix << 4) | ((((ts >> 30) & 0x07) as u8) << 1) | 0x01,
        ((ts >> 22) & 0xFF) as u8,
        ((((ts >> 15) & 0x7F) as u8) << 1) | 0x01,
        ((ts >> 7) & 0xFF) as u8,
        (((ts & 0x7F) as u8) << 1) | 0x01,
    ]
}

/// Build a complete PES packet (raw bytes) from typed optional-header fields.
fn build_pes(
    stream_id: u8,
    pts: Option<(u64, bool /* has_dts */)>,
    dts: Option<u64>,
    escr: Option<Escr>,
    trick_mode: Option<TrickMode>,
    payload: &[u8],
) -> Vec<u8> {
    let pts_dts_flags = match (pts, dts) {
        (Some(_), Some(_)) => 0b11u8,
        (Some(_), None) => 0b10,
        _ => 0b00,
    };

    // Compute optional-field lengths
    let mut opt_len = 0usize;
    if pts.is_some() {
        opt_len += 5;
    }
    if dts.is_some() {
        opt_len += 5;
    }
    if escr.is_some() {
        opt_len += 6;
    }
    if trick_mode.is_some() {
        opt_len += 1;
    }

    let has_opt = opt_len > 0;
    let hdr_extra = if has_opt { 3 } else { 0 }; // f1 + f2 + hdl

    let f1: u8 = 0x80; // marker "10", all flags clear
    let f2: u8 = (pts_dts_flags << 6)
        | (u8::from(escr.is_some()) << 5)
        | (u8::from(trick_mode.is_some()) << 3);

    let pes_len = (6 + hdr_extra + opt_len + payload.len()) as u16;
    let mut out = vec![0u8; 6 + hdr_extra + opt_len + payload.len()];
    let mut cursor = 0usize;

    // Fixed header
    out[cursor..cursor + 3].copy_from_slice(&PACKET_START_CODE_PREFIX);
    cursor += 3;
    out[cursor] = stream_id;
    cursor += 1;
    out[cursor..cursor + 2].copy_from_slice(&pes_len.to_be_bytes());
    cursor += 2;

    if !has_opt {
        out[cursor..].copy_from_slice(payload);
        return out;
    }

    out[cursor] = f1;
    cursor += 1;
    out[cursor] = f2;
    cursor += 1;
    out[cursor] = opt_len as u8;
    cursor += 1;

    // PTS (and DTS if present)
    if let Some((pts_val, has_dts)) = pts {
        let prefix = if has_dts { 0b0011 } else { 0b0010 };
        out[cursor..cursor + 5].copy_from_slice(&encode_ts(pts_val, prefix));
        cursor += 5;
    }
    if let Some(dts_val) = dts {
        out[cursor..cursor + 5].copy_from_slice(&encode_ts(dts_val, 0b0001));
        cursor += 5;
    }
    // ESCR
    if let Some(escr) = escr {
        out[cursor..cursor + 6].copy_from_slice(&escr.to_field_bytes());
        cursor += 6;
    }
    // DSM trick mode
    if let Some(tm) = trick_mode {
        out[cursor] = tm.to_byte();
        cursor += 1;
    }

    out[cursor..].copy_from_slice(payload);
    out
}

fn main() {
    // ── 1) Build with PTS + DTS ─────────────────────────────────────────────
    let pts_val = 90_000u64; // 1 second @ 90 kHz
    let dts_val = 45_000u64; // 0.5 seconds
    let bytes = build_pes(
        0xE0,
        Some((pts_val, true)),
        Some(dts_val),
        None,
        None,
        &[0xAA, 0xBB],
    );

    let pkt = PesPacket::parse(&bytes).expect("PES with PTS+DTS must parse");
    let hdr = pkt.header.as_ref().expect("video PES has optional header");
    println!("--- PTS + DTS ---");
    println!("stream_id       : 0x{:02X} (video)", pkt.stream_id.0);
    let pts = hdr.pts.unwrap();
    let dts = hdr.dts.unwrap();
    println!(
        "PTS             : {} ticks ({:.6}s)",
        pts.ticks(),
        pts.seconds()
    );
    println!(
        "DTS             : {} ticks ({:.6}s)",
        dts.ticks(),
        dts.seconds()
    );
    assert_eq!(pts.ticks(), 90_000);
    assert_eq!(dts.ticks(), 45_000);

    // ── 2) Round-trip via serialize_into ─────────────────────────────────────
    let mut buf = vec![0u8; pkt.serialized_len()];
    pkt.serialize_into(&mut buf).expect("serialize");
    let pkt2 = PesPacket::parse(&buf).expect("re-parse");
    let hdr2 = pkt2.header.unwrap();
    assert_eq!(hdr2.pts.unwrap().ticks(), 90_000);
    assert_eq!(hdr2.dts.unwrap().ticks(), 45_000);
    println!("  round-trip    : PTS+DTS preserved after serialize ✓");

    // ── 3) Build with ESCR ──────────────────────────────────────────────────
    let escr = Escr::from_27mhz(27_000_000); // 1 second @ 27 MHz
    let bytes2 = build_pes(
        0xE0,
        Some((pts_val, false)),
        None,
        Some(escr),
        None,
        &[0xCC],
    );
    let pkt3 = PesPacket::parse(&bytes2).expect("PES with ESCR must parse");
    let hdr3 = pkt3.header.unwrap();
    println!("\n--- ESCR ---");
    println!(
        "ESCR            : base={} ext={}  ({} ticks @ 27 MHz)",
        hdr3.escr.unwrap().base,
        hdr3.escr.unwrap().extension,
        hdr3.escr.unwrap().as_27mhz()
    );
    assert_eq!(hdr3.escr.unwrap().as_27mhz(), 27_000_000);

    // ── 4) Build with trick mode + PTS+DTS ──────────────────────────────────
    let trick = TrickMode::FastForward {
        field_id: 1,
        intra_slice_refresh: true,
        frequency_truncation: 1,
    };
    let bytes3 = build_pes(
        0xE0,
        Some((pts_val, true)),
        Some(dts_val),
        None,
        Some(trick),
        &[0xDD],
    );
    let pkt4 = PesPacket::parse(&bytes3).expect("PES with trick mode must parse");
    let hdr4 = pkt4.header.as_ref().unwrap();
    let tm = hdr4.dsm_trick_mode.unwrap();
    println!("\n--- DSM Trick Mode ---");
    println!("trick_mode      : {tm:?}");
    assert!(matches!(tm, TrickMode::FastForward { field_id: 1, .. }));

    // ── 5) Confirm serialize → re-parse preserves all fields ────────────────
    let mut buf3 = vec![0u8; pkt4.serialized_len()];
    pkt4.serialize_into(&mut buf3).expect("serialize");
    let pkt5 = PesPacket::parse(&buf3).expect("re-parse");
    let hdr5 = pkt5.header.as_ref().unwrap();
    assert_eq!(hdr5.pts.unwrap().ticks(), 90_000);
    assert_eq!(hdr5.dts.unwrap().ticks(), 45_000);
    assert!(hdr5.dsm_trick_mode.is_some());
    println!("  serialize → re-parse: all fields preserved ✓");

    println!("\nAll build/write operations completed successfully.");
}

Structs§

Dts
Decoding Time Stamp (33-bit, 90 kHz units).
Escr
Elementary Stream Clock Reference: 33-bit base (90 kHz) + 9-bit extension (27 MHz) — ISO/IEC 13818-1 §2.4.3.7, Table 2-21.
PStdBuffer
P-STD_buffer sub-field of PesExtension.
PesAssembler
Reassembles PES packets for a single PID from successive TS payloads.
PesExtension
Typed PES header extension sub-structure (ISO/IEC 13818-1 §2.4.3.7, Table 2-21, PES_extension_flag = 1).
PesHeader
The optional PES header present for non-special stream_ids (ISO/IEC 13818-1 §2.4.3.6, §2.4.3.7). All optional sub-fields are fully typed — the raw optional_fields blob has been replaced with the individual decoded fields.
PesPacket
A parsed PES packet.
ProgramPacketSequenceCounter
program_packet_sequence_counter sub-field of PesExtension.
Pts
Presentation Time Stamp (33-bit, 90 kHz units).
StreamId
8-bit stream_id of a PES packet.

Enums§

Error
A PES parse error.
TrickMode
Trick-mode control values for the DSM_trick_mode_flag field (ISO/IEC 13818-1 §2.4.3.7, Table 2-24).

Constants§

PACKET_START_CODE_PREFIX
The 3-byte packet_start_code_prefix that opens every PES packet (0x000001).

Type Aliases§

Result
Result alias for PES parsing.