Skip to main content

Crate st291

Crate st291 

Source
Expand description

SMPTE ST 291-1 — ancillary (ANC) data content.

ST 291-1 defines the ANC data packet: the generic carrier for VANC/HANC payloads (captions, AFD, timecode, audio metadata, …) multiplexed into a professional video signal. This crate is about that content, not any one carriage mechanism — ST 291-1 packets can be conveyed over more than one transport, and this crate grows to cover each as it is added.

§Transports

  • ts (default) — SMPTE ST 2038:2021 carriage of ANC data packets in an MPEG-2 Transport Stream. ST 2038 provides a transparent pipe so ST 291-1 ANC data packets can be conveyed frame-accurately alongside the video they belong to (ST 2038 §1); it is not for audio carriage and not for EDH packets (Introduction). This crate implements the two wire structures defined in ST 2038 §4:
    • AncDataDescriptor — the anc_data_descriptor (tag 0xC4) used in the PMT ES loop, plus the "VANC" registration_descriptor format_identifier 0x56414E43 and the ANC_STREAM_TYPE 0x06 (§4.1, Table 1).
    • AncDataPacket — the ANC data PES packet (stream_id == 0xBD, PTS, PES_header_data_length == 0x05) carrying a list of bit-packed AncPacket records + trailing 0xFF stuffing (§4.2, Table 2).
  • rtp — RFC 8331 / ST 2110-40 carriage of ANC data packets over RTP (issue #648). AncRtpPayload is the §2.1 payload (Extended Sequence Number/Length/ANC_Count/FieldSense/reserved + a list of RtpAncPackets), riding on an rtp_packet::RtpPacket’s payload (the RTP fixed header, RFC 3550, is the rtp-packet crate’s responsibility). See docs/anc_rtp_8331.md.

The per-ANC-packet content — DID/SDID/data_count/user_data_word/ checksum_word — is a contiguous MSB-first 10-bit bit stream, walked with broadcast_common::bits, and is byte-for-byte identical across both transports: it lives in the always-compiled AncContent type (gated behind neither ts nor rtp), wrapped by each transport’s own placement fields (AncPacket’s three for ST 2038, RtpAncPacket’s five for RFC 8331). Per §4.2.1/§2.1 the user_data_word loop counter uses only the low 8 bits of data_count; the full 10-bit values are stored verbatim and ST 291-1 parity/checksum is not validated (deferred to ST 291-1, which is not vendored — see docs/anc_packet_291.md).

Depends only on broadcast-common (plus rtp-packet, optionally, for the rtp feature) and is #![no_std] (+ alloc). The ST 2038 PES header is parsed inline (every field is fixed by ST 2038 Table 2, so the dedicated mpeg-pes parser adds a dependency without simplifying the bit-packed payload walk).

§Examples

Build an ANC PES packet from typed fields and round-trip it:

use st291::{AncDataPacket, AncPacket};

let pkt = AncDataPacket {
    pes_priority: false,
    copyright: false,
    original_or_copy: false,
    pts: 90_000,
    anc_packets: vec![AncPacket {
        c_not_y_channel_flag: false,
        line_number: 9,
        horizontal_offset: 0,
        did: 0x161,
        sdid: 0x101,
        data_count: 0x002,
        user_data_words: vec![0x2CF, 0x101],
        checksum: 0x233,
    }],
    stuffing_bytes: 0,
};
let bytes = {
    let mut b = vec![0u8; pkt.serialized_len()];
    pkt.serialize_into(&mut b).unwrap();
    b
};
assert_eq!(AncDataPacket::parse(&bytes).unwrap(), pkt);

Build an ANC-over-RTP payload from typed fields and round-trip it:

use broadcast_common::{Parse, Serialize};
use st291::{AncContent, AncRtpPayload, FieldSense, RtpAncPacket};

let payload = AncRtpPayload {
    extended_sequence_number: 0,
    field_sense: FieldSense::ProgressiveOrUnspecified,
    anc_packets: vec![RtpAncPacket {
        c: false,
        line_number: 9,
        horizontal_offset: 0,
        s: false,
        stream_num: 0,
        content: AncContent {
            did: 0x161,
            sdid: 0x101,
            data_count: 0x002,
            user_data_words: vec![0x2CF, 0x101],
            checksum: 0x233,
        },
    }],
};
let bytes = {
    let mut b = vec![0u8; payload.serialized_len()];
    payload.serialize_into(&mut b).unwrap();
    b
};
assert_eq!(AncRtpPayload::parse(&bytes).unwrap(), payload);

§Runnable examples

Run with cargo run -p st291 --example <name>.

§build_anc

/// Build an ANC data PES packet from typed fields, serialize it, and dump the
/// wire bytes — including the 10-bit bit-packed ANC records.
///
/// ```sh
/// cargo run -p st291 --example build_anc
/// ```
use st291::{AncDataPacket, AncPacket};

fn main() {
    // Two ANC packets on the same line's PES, plus a few stuffing bytes.
    let pkt = AncDataPacket {
        pes_priority: false,
        copyright: false,
        original_or_copy: false,
        pts: 90_000, // 1.0 s at 90 kHz
        anc_packets: vec![
            AncPacket {
                c_not_y_channel_flag: false,
                line_number: 9,
                horizontal_offset: 0,
                did: 0x161,
                sdid: 0x101,
                data_count: 0x002, // low 8 bits => 2 user_data_words
                user_data_words: vec![0x2CF, 0x101],
                checksum: 0x233,
            },
            AncPacket {
                c_not_y_channel_flag: true,
                line_number: 0x2A,
                horizontal_offset: 0x10,
                did: 0x241,
                sdid: 0x102,
                data_count: 0x003, // 3 user_data_words
                user_data_words: vec![0x111, 0x222, 0x333],
                checksum: 0x1AB,
            },
        ],
        stuffing_bytes: 4,
    };

    let mut bytes = vec![0u8; pkt.serialized_len()];
    pkt.serialize_into(&mut bytes).unwrap();

    println!("ANC data PES packet: {} bytes", bytes.len());
    println!("PTS: {} (90 kHz units)", pkt.pts);
    println!("ANC packets: {}", pkt.anc_packets.len());
    for (i, anc) in pkt.anc_packets.iter().enumerate() {
        println!(
            "  [{i}] line={} h_off={} DID={:#05X} SDID={:#05X} data_count={:#05X} \
             udw_loop={} checksum={:#05X}",
            anc.line_number,
            anc.horizontal_offset,
            anc.did,
            anc.sdid,
            anc.data_count,
            anc.udw_loop_count(),
            anc.checksum,
        );
    }
    println!("stuffing bytes: {}", pkt.stuffing_bytes);
    print!("wire bytes:");
    for b in &bytes {
        print!(" {b:02X}");
    }
    println!();

    // Round-trip sanity.
    assert_eq!(AncDataPacket::parse(&bytes).unwrap(), pkt);
    println!("round-trip: OK");
}

§parse_anc

/// Parse the committed ANC fixture at runtime, report the decoded ANC packets,
/// and verify a byte-exact round-trip.
///
/// ```sh
/// cargo run -p st291 --example parse_anc
/// ```
use std::fs;

use st291::AncDataPacket;

fn main() {
    // Resolve relative to the crate so it runs from any cwd.
    let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../fixtures/st291/anc.bin");
    let data = match fs::read(path) {
        Ok(d) => d,
        Err(e) => {
            eprintln!("fixture not available ({e}); nothing to do");
            return;
        }
    };

    let pkt = AncDataPacket::parse(&data).unwrap();

    println!("File: {path}");
    println!("Size: {} bytes", data.len());
    println!("PTS: {} (90 kHz units)", pkt.pts);
    println!("PES_priority={}", pkt.pes_priority);
    println!("ANC packets: {}", pkt.anc_packets.len());
    for (i, anc) in pkt.anc_packets.iter().enumerate() {
        print!(
            "  [{i}] c_not_y={} line={} h_off={} DID={:#05X} SDID={:#05X} \
             data_count={:#05X} udw=[",
            anc.c_not_y_channel_flag,
            anc.line_number,
            anc.horizontal_offset,
            anc.did,
            anc.sdid,
            anc.data_count,
        );
        for (j, w) in anc.user_data_words.iter().enumerate() {
            if j > 0 {
                print!(", ");
            }
            print!("{w:#05X}");
        }
        println!("] checksum={:#05X}", anc.checksum);
    }
    println!("stuffing bytes: {}", pkt.stuffing_bytes);

    // Byte-exact round-trip.
    let mut out = vec![0u8; pkt.serialized_len()];
    pkt.serialize_into(&mut out).unwrap();
    assert_eq!(
        out, data,
        "round-trip must be byte-identical to the fixture"
    );
    println!("byte-exact round-trip: OK");
}

§build_anc_rtp

/// Build an RFC 8331 ANC-over-RTP packet from typed fields, serialize it, and
/// dump the wire bytes — the RTP fixed header (`rtp_packet::RtpPacket`, RFC
/// 3550) wrapping the RFC 8331 §2.1 ANC payload (`st291::AncRtpPayload`).
///
/// ```sh
/// cargo run -p st291 --example build_anc_rtp --features rtp
/// ```
use broadcast_common::Serialize;
use rtp_packet::RtpPacket;
use st291::{ANC_RTP_DEFAULT_CLOCK_RATE, AncContent, AncRtpPayload, FieldSense, RtpAncPacket};

fn main() {
    // The RFC 8331 §2.1 ANC payload: two ANC packets, on lines 9 and 10 (as
    // in RFC 8331 Figure 1's own worked example).
    let anc_payload = AncRtpPayload {
        extended_sequence_number: 0,
        field_sense: FieldSense::ProgressiveOrUnspecified,
        anc_packets: vec![
            RtpAncPacket {
                c: false,
                line_number: 9,
                horizontal_offset: 0,
                s: false,
                stream_num: 0,
                content: AncContent {
                    did: 0x161,
                    sdid: 0x101,
                    data_count: 0x002, // low 8 bits => 2 user_data_words
                    user_data_words: vec![0x2CF, 0x101],
                    checksum: 0x233,
                },
            },
            RtpAncPacket {
                c: true,
                line_number: 10,
                horizontal_offset: 0x10,
                s: false,
                stream_num: 0,
                content: AncContent {
                    did: 0x241,
                    sdid: 0x102,
                    data_count: 0x003, // 3 user_data_words
                    user_data_words: vec![0x111, 0x222, 0x333],
                    checksum: 0x1AB,
                },
            },
        ],
    };
    let mut anc_bytes = vec![0u8; anc_payload.serialized_len()];
    anc_payload.serialize_into(&mut anc_bytes).unwrap();

    // Wrap it in an RFC 3550 RTP packet: marker=true (last ANC RTP packet for
    // this frame), dynamic payload type 112 (RFC 8331 §4's own worked SDP
    // example: `a=rtpmap:112 smpte291/90000`).
    let rtp = RtpPacket {
        marker: true,
        payload_type: 112,
        sequence_number: 1,
        timestamp: ANC_RTP_DEFAULT_CLOCK_RATE, // 1.0 s at the default 90 kHz clock rate
        ssrc: 0xCAFE_BABE,
        csrc: vec![],
        extension: None,
        padding: None,
        payload: &anc_bytes,
    };
    let mut bytes = vec![0u8; rtp.serialized_len()];
    rtp.serialize_into(&mut bytes).unwrap();

    println!("ANC-over-RTP packet: {} bytes", bytes.len());
    println!("ANC_Count: {}", anc_payload.anc_count());
    for (i, pkt) in anc_payload.anc_packets.iter().enumerate() {
        println!(
            "  [{i}] line={} h_off={} S={} StreamNum={} DID={:#05X} SDID={:#05X} \
             data_count={:#05X} udw_loop={} checksum={:#05X}",
            pkt.line_number,
            pkt.horizontal_offset,
            pkt.s,
            pkt.stream_num,
            pkt.content.did,
            pkt.content.sdid,
            pkt.content.data_count,
            pkt.content.udw_loop_count(),
            pkt.content.checksum,
        );
    }
    print!("wire bytes:");
    for b in &bytes {
        print!(" {b:02X}");
    }
    println!();

    // Round-trip sanity via the RtpPacket + AncRtpPayload composition helper.
    let (parsed_rtp, parsed_anc) = AncRtpPayload::parse_rtp_packet(&bytes).unwrap();
    assert_eq!(parsed_rtp.payload_type, 112);
    assert_eq!(parsed_anc, anc_payload);
    println!("round-trip: OK");
}

§parse_anc_rtp

//! Parse the committed real-ish fixture (`fixtures/st291/anc_rtp.bin`) and
//! print its decoded RFC 8331 ANC-over-RTP fields, then round-trip it back to
//! bytes and confirm the output is byte-identical.
//!
//! ```sh
//! cargo run -p st291 --example parse_anc_rtp --features rtp
//! ```
use broadcast_common::Serialize;
use st291::AncRtpPayload;

fn main() {
    let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../fixtures/st291/anc_rtp.bin");
    let bytes = std::fs::read(path).expect("fixture must exist");

    let (rtp, anc) = AncRtpPayload::parse_rtp_packet(&bytes).expect("parse ANC-over-RTP packet");

    println!("RTP marker:       {}", rtp.marker);
    println!("RTP payload_type: {}", rtp.payload_type);
    println!("RTP timestamp:    {}", rtp.timestamp);
    println!("RTP ssrc:         {:#010x}", rtp.ssrc);
    println!("Extended Sequence Number: {}", anc.extended_sequence_number);
    println!("F (field sense):  {}", anc.field_sense);
    println!("ANC_Count:        {}", anc.anc_count());
    for (i, pkt) in anc.anc_packets.iter().enumerate() {
        println!(
            "  [{i}] C={} line={} h_off={} S={} StreamNum={} DID={:#05X} SDID={:#05X} \
             checksum={:#05X}",
            pkt.c,
            pkt.line_number,
            pkt.horizontal_offset,
            pkt.s,
            pkt.stream_num,
            pkt.content.did,
            pkt.content.sdid,
            pkt.content.checksum,
        );
    }

    let mut out = vec![0u8; rtp.serialized_len()];
    rtp.serialize_into(&mut out).expect("serialize RTP packet");
    assert_eq!(out, bytes, "byte-identical RTP round trip");

    let mut anc_out = vec![0u8; anc.serialized_len()];
    anc.serialize_into(&mut anc_out)
        .expect("serialize ANC payload");
    assert_eq!(
        anc_out, rtp.payload,
        "byte-identical ANC payload round trip"
    );

    println!("round trip byte-identical: OK ({} bytes)", out.len());
}

Structs§

AncContent
One ST 291-1 ANC data packet’s content: DID/SDID/Data_Count/ User_Data_Words/Checksum_Word, every value the raw 10-bit wire word (including the ST 291-1 parity bits) stored verbatim — parity/checksum are not computed or validated here (docs/anc_packet_291.md scope note).
AncDataDescriptorts
The anc_data_descriptor (Table 1): a tag/length wrapper around an opaque inner descriptor loop. The descriptor_length body is retained verbatim in inner_descriptors — its content is currently undefined by ST 2038 and parsed lazily by the caller.
AncDataPacketts
A parsed ANC data PES packet (Table 2): the fixed PES header (stream_id 0xBD, PTS) + the list of AncPackets + a count of trailing 0xFF stuffing bytes.
AncPacketts
One ST 291-1 ANC data packet plus its ST 2038 placement (Table 2 inner loop). The 10-bit did/sdid/data_count/user_data_words/checksum are stored as raw u16 (ST 291-1 parity/checksum not validated here).
AncRtpPayloadrtp
The RFC 8331 RTP payload for SMPTE ST 291 ancillary data (§2.1): the payload header (Extended Sequence Number/Length/ANC_Count/F/ reserved) plus the list of RtpAncPackets. This is the bytes placed in an rtp_packet::RtpPacket’s payload field — the RTP fixed header itself is rtp_packet’s responsibility (see the module doc).
RtpAncPacketrtp
One ANC data packet as carried in an RFC 8331 RTP payload: the five RTP-specific placement fields (C/Line_Number/Horizontal_Offset/S/ StreamNum, §2.1) wrapping the transport-independent AncContent (DID/SDID/Data_Count/User_Data_Words/Checksum_Word).

Enums§

Error
An ST 2038 parse / serialize error.
FieldSensertp
F (2 bits) — field-sense signalling for the RTP timestamp in an interlaced SDI raster (RFC 8331 §2.1).

Constants§

ANC_DATA_DESCRIPTOR_TAGts
anc_data_descriptor tag — 0xC4 (user-defined in ATSC/DVB/SCTE), ST 2038 §4.1.2.
ANC_PES_HEADER_DATA_LENGTHts
PES_header_data_length0x05 (exactly a 5-byte PTS), Table 2.
ANC_RTP_DEFAULT_CLOCK_RATErtp
Default RTP timestamp clock rate — 90 kHz, “Otherwise, a 90 kHz rate SHOULD be used” (§3.1) when the ANC stream is not grouped with a specific video stream at another rate. RFC 8331’s own worked SDP example: a=rtpmap:112 smpte291/90000.
ANC_RTP_MEDIA_TYPErtp
video/smpte291 — the RFC 8331 §3.1 media type + subtype (Type name: video, Subtype name: smpte291).
ANC_RTP_PAYLOAD_HEADER_LENrtp
Length of the RFC 8331 §2.1 payload header: Extended Sequence Number(16) plus Length(16) plus ANC_Count(8) plus F(2) plus reserved(22), i.e. 64 bits, 8 bytes.
ANC_STREAM_IDts
stream_id0xBD (private_stream_1), Table 2.
ANC_STREAM_TYPEts
stream_type for the ANC data ES in the PMT — 0x06 (PES private data), ST 2038 §4.1.1.
PACKET_START_CODE_PREFIXts
packet_start_code_prefix0x000001 (Table 2).
STUFFING_BYTEts
stuffing_byte value — 0xFF ('1111 1111'), §4.2.1.
VANC_FORMAT_IDENTIFIERts
registration_descriptor format_identifier for ST 2038 — 0x56414E43 (the ASCII "VANC"), ST 2038 §4.1.3.
VANC_FORMAT_IDENTIFIER_BYTESts
The four ASCII bytes of VANC_FORMAT_IDENTIFIER (b"VANC").

Type Aliases§

Result
Result alias for ST 2038 parsing.