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 dvb-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

Two 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` DVB fixture from the sibling `dvb-si`
//! crate at runtime (so the example compiles even when the fixture is absent).

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() {
    let path = concat!(
        env!("CARGO_MANIFEST_DIR"),
        "/../dvb-si/tests/fixtures/m6-single.ts"
    );
    let ts = match std::fs::read(path) {
        Ok(b) => b,
        Err(e) => {
            eprintln!("fixture not available ({e}); nothing to do");
            return;
        }
    };

    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) {
            if pid == PES_PID {
                if 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)");
    }
}

Structs§

Dts
Decoding Time Stamp (33-bit, 90 kHz units).
PesAssembler
Reassembles PES packets for a single PID from successive TS payloads.
PesHeader
The optional PES header present for non-special stream_ids (§2.4.3.6). The variable optional fields (PTS/DTS, ESCR, ES_rate, trick mode, …) are retained verbatim in optional_fields; pts/dts are decoded from their front for convenience.
PesPacket
A parsed PES packet.
Pts
Presentation Time Stamp (33-bit, 90 kHz units).
StreamId
8-bit stream_id of a PES packet.

Enums§

Error
A PES parse error.

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.