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— theanc_data_descriptor(tag0xC4) used in the PMT ES loop, plus the"VANC"registration_descriptorformat_identifier0x56414E43and theANC_STREAM_TYPE0x06(§4.1, Table 1).AncDataPacket— the ANC data PES packet (stream_id == 0xBD, PTS,PES_header_data_length == 0x05) carrying a list of bit-packedAncPacketrecords + trailing0xFFstuffing (§4.2, Table 2).
rtp— RFC 8331 / ST 2110-40 carriage of ANC data packets over RTP (issue #648).AncRtpPayloadis the §2.1 payload (Extended Sequence Number/Length/ANC_Count/FieldSense/reserved+ a list ofRtpAncPackets), riding on anrtp_packet::RtpPacket’s payload (the RTP fixed header, RFC 3550, is thertp-packetcrate’s responsibility). Seedocs/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.mdscope note). - AncData
Descriptor ts - The
anc_data_descriptor(Table 1): a tag/length wrapper around an opaque inner descriptor loop. Thedescriptor_lengthbody is retained verbatim ininner_descriptors— its content is currently undefined by ST 2038 and parsed lazily by the caller. - AncData
Packet ts - A parsed ANC data PES packet (Table 2): the fixed PES header (stream_id
0xBD, PTS) + the list ofAncPackets + a count of trailing0xFFstuffing bytes. - AncPacket
ts - 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/checksumare stored as rawu16(ST 291-1 parity/checksum not validated here). - AncRtp
Payload rtp - 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 ofRtpAncPackets. This is the bytes placed in anrtp_packet::RtpPacket’spayloadfield — the RTP fixed header itself isrtp_packet’s responsibility (see the module doc). - RtpAnc
Packet rtp - 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-independentAncContent(DID/SDID/Data_Count/User_Data_Words/Checksum_Word).
Enums§
- Error
- An ST 2038 parse / serialize error.
- Field
Sense rtp F(2 bits) — field-sense signalling for the RTP timestamp in an interlaced SDI raster (RFC 8331 §2.1).
Constants§
- ANC_
DATA_ DESCRIPTOR_ TAG ts anc_data_descriptortag —0xC4(user-defined in ATSC/DVB/SCTE), ST 2038 §4.1.2.- ANC_
PES_ HEADER_ DATA_ LENGTH ts PES_header_data_length—0x05(exactly a 5-byte PTS), Table 2.- ANC_
RTP_ DEFAULT_ CLOCK_ RATE rtp - 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_ TYPE rtp video/smpte291— the RFC 8331 §3.1 media type + subtype (Type name: video,Subtype name: smpte291).- ANC_
RTP_ PAYLOAD_ HEADER_ LEN rtp - Length of the RFC 8331 §2.1 payload header:
Extended Sequence Number(16) plusLength(16) plusANC_Count(8) plusF(2) plusreserved(22), i.e. 64 bits, 8 bytes. - ANC_
STREAM_ ID ts stream_id—0xBD(private_stream_1), Table 2.- ANC_
STREAM_ TYPE ts stream_typefor the ANC data ES in the PMT —0x06(PES private data), ST 2038 §4.1.1.- PACKET_
START_ CODE_ PREFIX ts packet_start_code_prefix—0x000001(Table 2).- STUFFING_
BYTE ts stuffing_bytevalue —0xFF('1111 1111'), §4.2.1.- VANC_
FORMAT_ IDENTIFIER ts registration_descriptorformat_identifierfor ST 2038 —0x56414E43(the ASCII"VANC"), ST 2038 §4.1.3.- VANC_
FORMAT_ IDENTIFIER_ BYTES ts - The four ASCII bytes of
VANC_FORMAT_IDENTIFIER(b"VANC").
Type Aliases§
- Result
- Result alias for ST 2038 parsing.