Expand description
DVB Service Information (ETSI EN 300 468 V1.20.1) and MPEG-2 PSI — a typed parser and builder for every table and descriptor a DVB transport stream carries.
dvb-si turns the signalling inside an MPEG transport stream — PAT/PMT,
NIT/SDT/BAT, the EIT programme guide, the TDT/TOT clock, the HbbTV AIT, SSU
and IP/MAC notification tables, DSM-CC data and object carousels — into typed
Rust values whose text fields decode to UTF-8 and whose descriptor loops walk
into typed descriptors, and turns those values back into byte-identical wire
sections. Every layout is cited to its spec clause and round-trip tested
against real broadcast captures.
§At a glance
The counts below are generated from the crate’s own dispatch lists by
tools/gen-dvbsi-coverage.py (CI fails when they go stale). The full
per-code reference — standard, verified clause, Rust type and status for every
table_id and descriptor tag, plus every allocated code that is not
implemented — is
private/specs-verified/dvb/dvb-si/COVERAGE.md.
| Section types (table_id) | 31 typed *Section parsers covering 72 table_id values — every allocated EN 300 468 Table 2 id |
| Descriptors (tag 0x00–0x7F) | 116 auto-dispatched: 52 MPEG-2 / DSM-CC (0x02–0x3F) + all 64 DVB tags (0x40–0x7F) |
| DVB extension descriptors (0x7F) | 30 typed descriptor_tag_extension bodies — every one EN 300 468 V1.20.1 Table 109 allocates |
| MPEG-2 extension descriptors (0x3F) | 24 typed extension_descriptor_tag bodies (7 MPEG-H 3D audio, plus HEVC/VVC/EVC/LCEVC/JPEG XS timing and structure, green, quality, OD update, timed metadata, media service kind) |
| AIT descriptors (TS 102 809 / MHP) | 21 typed in the separate AIT descriptor namespace |
| INT / UNT / RNT descriptors | 26 typed in their own scoped namespaces (AnyIntDescriptor, AnyUntDescriptor, AnyRntDescriptor) |
| Private / regional descriptors | 10 PDS- or registration-scoped opt-ins (NorDig, EACEM/D-Book, China GY/T + T/AI AVS2/AVS3, T/UWA) |
| Text (EN 300 468 Annex A) | 7 charset decoders covering every Table A.3 selector + Annex A.1 control codes |
| DSM-CC carousel | 12 layers: section framing, DSI/DII/DDB, module reassembly, BIOP object carousel, carousel file tree |
| Registries | 171 CA_system_id and 174 private_data_specifier names (TSDuck data, build-time) |
| Standards | 19 standards / families — see the table below |
| Known gaps | 7 allocated codes not yet typed — listed in private/specs-verified/dvb/dvb-si/COVERAGE.md (private repo) |
§Standards supported
| Standard | Title | Edition implemented | Scope covered | Status |
|---|---|---|---|---|
| ETSI EN 300 468 | DVB Service Information (SI) | V1.20.1 (2026-09); RFU defaults audited back to V1.3.1 | all SI tables (NIT, BAT, SDT, EIT, TDT, TOT, RST, ST, SAT, DIT, SIT), Table 12 descriptors 0x40–0x7F, Table 109 extension descriptors, Annex A text, Annexes D/G/H/L audio descriptors | Full (98/98 allocated codes) |
| Rec. ITU-T H.222.0 | ISO/IEC 13818-1 | MPEG-2 Systems — PSI and descriptors | 08/2023 text (clause numbers in COVERAGE.md); MPEG-H 3D audio descriptors 06/2021 | PAT, CAT, PMT, TSDT; Table 2-45 descriptors; Table 2-110 extension descriptors | Partial (82/85 allocated codes) |
| ISO/IEC 13818-6 | DSM-CC (data/object carousel) | as profiled by ETSI TR 101 202 V1.2.1 (13818-6 itself is not vendored) | DSM-CC sections, DSI/DII/DDB, module reassembly, BIOP object carousel, carousel_identifier + association_tag descriptors | Partial (3/5 allocated codes) |
| ETSI EN 301 192 | DVB data broadcasting | V1.7.1 | MPE datagram, MPE-FEC, INT section; time_slice_fec_identifier + ECM_repetition_rate descriptors; DII carousel descriptors | Full (29/29 allocated codes) |
| ETSI TS 102 006 | DVB System Software Update (SSU) | V1.4.1 | UNT section, compatibilityDescriptor, SSU data_broadcast_id selector | Full (16/16 allocated codes) |
| ETSI TS 102 809 | Application signalling (HbbTV AIT) | V1.3.1 | AIT section, AIT descriptor namespace, application_signalling + service_identifier descriptors, protection message section + descriptor | Full (16/16 allocated codes) |
| ETSI TS 102 727 | DVB-MHP 1.1.3 | V1.1.1 (descriptor subset) | DVB-J application + location AIT descriptors, XAIT_location, XAIT_PID | Partial (11/12 allocated codes) |
| ETSI TS 102 323 | TV-Anytime carriage in DVB | V1.4.1 | container, RCT, CIT, RNT sections; default_authority, related_content, TVA_id, content_identifier descriptors | Full (11/11 allocated codes) |
| ETSI TS 102 772 | MPE-IFEC | V1.1.1 | MPE-IFEC section | Full (1/1 allocated codes) |
| ETSI EN 303 560 | DVB TTML subtitling | V1.1.1 | TTML_subtitling descriptor, downloadable font info section (DFIS) | Full (2/2 allocated codes) |
| ETSI TS 102 825-9 | CPCM system adaptation | V1.2.1 | cpcm_delivery_signalling extension descriptor | Full (1/1 allocated codes) |
| ETSI TS 101 154 | Audio/video coding in DVB | V2.10.1 | PMT StreamType values incl. AVS2/AVS3 (0xD2–0xD5); AVS private_data_specifier rules | Partial |
| ETSI TS 101 162 | DVB identifiers | registry data from TSDuck (see registries/README.md) | CA_system_id and private_data_specifier value → name lookups | Partial |
| NorDig Unified Requirements | Nordic IRD requirements | v3.2.2 (2025-11) | logical channel descriptor v1 (0x83) + v2 (0x87), PDS 0x00000029 | Full (scoped) |
| EACEM / D-Book | Logical channel numbering | private (PDS 0x00000028); EACEM TR-030 is not vendored | logical_channel descriptor (0x83), 10-bit-LCN layout rests on real captures only; the vendored NorDig spec defines LCD v1 (0x83) and v2 (0x87) in §12.2.9 (both typed in nordig.rs) | Full (scoped) |
| GY/T 270-2013 | China DTV closed captions | 2013 | caption_service_descriptor (0x86), StreamType::ChinaClosedCaption | Full (scoped) |
| GY/T 420-2025, GY/T 422-2025 | China UHD encapsulation / multi-audio | 2025 | AVS2_video (0x40 under ‘AVSV’), AVS3_video; GY/T 422 audio_preselection profile | Full (scoped) |
| T/AI 109.6-2022, T/AI 109.7-2024 | AVS3 media format / audio transport | 2022 / 2024 | AVS3_video (0xD1), AVS3_audio (0xD2), AVS2_audio (0xD3) | Full (scoped) |
| T/UWA 012.2-2023 | UWA UHD TS encapsulation | 2023 | older AVS3 video descriptor (tag 62) | Full (scoped) |
Status: Full = every code the edition allocates is typed; Partial = some are (the tally counts allocated codes, so a single untyped one shows); Full (scoped) = typed, but only decoded inside the private_data_specifier / registration scope the standard requires (opt in via DescriptorRegistry); Not implemented = bytes are preserved but not typed. Every count above is generated by tools/gen-dvbsi-coverage.py from the crate’s dispatch lists and CI fails if it goes stale.
§Quick start
§Parse one section, then serialize it back
The bytes are the PAT of a real French TNT capture committed in the repository. Every parser has a symmetric serializer, so the round trip is byte-identical:
use broadcast_common::{Parse, Serialize};
use dvb_si::tables::AnyTableSection;
use dvb_si::tables::pat::PatSection;
let section: [u8; 36] = [
0x00, 0xB0, 0x21, 0x00, 0x06, 0xE5, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x10,
0x06, 0x01, 0xE0, 0x64, 0x06, 0x06, 0xE2, 0x58, 0x06, 0x08, 0xE1, 0xF4,
0x06, 0x09, 0xE2, 0xBC, 0x06, 0x0A, 0xE0, 0xC8, 0x98, 0xF3, 0xCC, 0x2D,
];
// Known type: parse it directly. Unknown type: dispatch on table_id.
let pat = PatSection::parse(§ion)?;
assert_eq!(pat.entries.len(), 6); // the NIT pointer + five programmes
assert!(matches!(AnyTableSection::parse(§ion)?, AnyTableSection::PatSection(_)));
let mut out = vec![0u8; pat.serialized_len()];
pat.serialize_into(&mut out)?;
assert_eq!(out, section);§Pump TS packets
Build a demux::SiDemux (feature ts), feed it 188-byte packets, match on
tables::AnyTableSection, walk descriptor loops with
descriptors::DescriptorLoop::iter, and decode text with
text::DvbText::decode:
use dvb_si::demux::SiDemux;
use dvb_si::descriptors::AnyDescriptor;
use dvb_si::tables::AnyTableSection;
let mut demux = SiDemux::builder().build();
// In real code, `packet` is each aligned 188-byte packet from your TS source
// (file, UDP, tuner). Here we hand-build one PAT packet to keep the doctest
// self-contained — see `dvb-tools dump` (in the `dvb-tools` crate) for the
// file-reading loop.
for event in demux.feed(&packet) {
match event.table_section() {
Ok(AnyTableSection::SdtSection(sdt)) => {
for service in &sdt.services {
for item in service.descriptors.iter().flatten() {
if let AnyDescriptor::Service(svc) = item {
// DvbText decodes EN 300 468 Annex A → UTF-8.
println!("service: {}", svc.service_name.decode());
}
}
}
}
Ok(AnyTableSection::PatSection(pat)) => {
println!("PAT v{} on {}", event.version().unwrap_or(0), event.pid());
assert_eq!(pat.entries.len(), 1);
}
Ok(other) => { let _ = other; }
Err(_) => {} // malformed section
}
}
§Private descriptors inside their scope
Private tags are never auto-dispatched; a descriptors::DescriptorRegistry
opts in, scoped by the private_data_specifier_descriptor in front of them:
use dvb_si::descriptors::nordig::NordigLogicalChannelV1;
use dvb_si::descriptors::{AnyDescriptor, DescriptorLoop, DescriptorRegistry};
// PDS 0x00000029 (NorDig), then NorDig LCD v1: service 1, visible, LCN 5.
let bytes = [0x5F, 0x04, 0x00, 0x00, 0x00, 0x29, 0x83, 0x04, 0x00, 0x01, 0xC0, 0x05];
let mut registry = DescriptorRegistry::new();
registry.with_nordig_lcn();
let items: Vec<_> = DescriptorLoop::new(&bytes)
.iter_with(®istry)
.collect::<Result<_, _>>()?;
match &items[1] {
AnyDescriptor::Other { value, .. } => {
let lcd = value.downcast_ref::<NordigLogicalChannelV1>().unwrap();
assert_eq!(lcd.entries[0].logical_channel_number, 5);
}
other => panic!("expected the NorDig LCD, got {other:?}"),
}§Architecture
TS packets ─▶ demux::SiDemux ─▶ SectionEvent
│ .table_section()
▼
tables::AnyTableSection (PatSection, SdtSection, …)
│ section.<loop field> : DescriptorLoop
▼
descriptors::parse_loop ─▶ AnyDescriptor
│ field : DvbText / LangCode
▼
text::DvbText::decode() ─▶ UTF-8 String
SectionEvent.bytes() ─▶ collect::SectionSetCollector ─▶ CompleteSectionSet
│ .table::<T>()
├ .nit() / .bat() / .sdt() / .eit()
▼
complete logical tables| Module | What it holds |
|---|---|
tables | one *Section type per table_id (PSI, SI, AIT, UNT, INT, MPE, TV-Anytime, …) + tables::AnyTableSection dispatch + tables::TableRegistry |
descriptors | one module per descriptor tag; descriptors::AnyDescriptor / descriptors::DescriptorLoop; descriptors::extension (tag 0x7F bodies), descriptors::mpeg2_extension (tag 0x3F, MPEG-H), descriptors::ait (AIT namespace); descriptors::DescriptorRegistry / descriptors::ExtensionRegistry for private ones |
carousel | DSM-CC download protocol (DSI/DII/DDB), carousel::ModuleReassembler, BIOP object carousel and carousel::CarouselFs |
compatibility | the DSM-CC / SSU compatibilityDescriptor() structure |
text | text::DvbText / text::LangCode — EN 300 468 Annex A decoding |
collect | multi-section collection: collect::SectionSetCollector, collect::EitCollector, complete NIT/BAT/SDT/EIT views |
demux | SiDemux: PID-filtered, CRC-checked, version-gated section pump (feature ts) |
epg | EpgStore: bounded EPG with now/next and schedules (feature chrono) |
owned | Owned<T>: keep a zero-copy view past its buffer (feature yoke) |
table_id / descriptor_tag | the typed TableId / DescriptorTag enums |
traits | TableDef / DescriptorDef — the per-type consts the dispatch lists are generated from |
error | the structured Error every parser and serializer returns |
§Standards
ETSI EN 300 468 V1.20.1 (DVB SI, incl. Annex A text and Annexes D/G/H/L
audio descriptors), Rec. ITU-T H.222.0 | ISO/IEC 13818-1 (PSI + descriptors,
MPEG-H 3D audio), ISO/IEC 13818-6 as profiled by ETSI TR 101 202 (DSM-CC),
ETSI EN 301 192 (data broadcasting: MPE, MPE-FEC, INT), ETSI TS 102 006
(SSU/UNT), ETSI TS 102 809 (AIT, protection messages), ETSI TS 102 727 (MHP
subset), ETSI TS 102 323 (TV-Anytime: container, RCT, CIT, RNT), ETSI TS
102 772 (MPE-IFEC), ETSI EN 303 560 (TTML subtitling, DFIS), ETSI TS
102 825-9 (CPCM), and the PDS-scoped regional extensions (NorDig, EACEM /
D-Book, China GY/T 270 / 420 / 422, T/AI 109.6 / 109.7, T/UWA 012.2). The
table above gives each one’s edition and status; private/specs-verified/dvb/dvb-si/COVERAGE.md lists
every code.
§Which API do I use?
- A TS source →
demux::SiDemux, thenSectionEvent::table_section(). - Complete section bytes →
tables::AnyTableSection::parse(or a specifictables::<t>::<T>Section::parse;parse_withadds atables::TableRegistry). - A table spanning sections →
collect; an EPG →epg::EpgStore. - A descriptor loop →
descriptors::DescriptorLoop::iter(ordescriptors::parse_loopon a bare slice); unknown tags becomedescriptors::AnyDescriptor::Unknown, never a panic. - Private / regional descriptors →
descriptors::DescriptorLoop::iter_withadescriptors::DescriptorRegistry. - Emitting SI → build the struct with
new(..), thenSerialize::serialize_into; packetise withmpeg-ts. - JSON → the
serdefeature (serialize-only; re-parse from wire bytes).
§RFU policy
One rule for every reserved region (owner decision, supersedes r03-W8/#1077 “drop on parse, emit the spec value”):
- Parse is tolerant. Any value of a
reserved,reserved_future_useorreserved_zero_future_useregion is accepted (no rejection) — unlikedvb-t2mi, which validates RFU bits. - Serialize preserves what was parsed. The reserved bits are captured
in typed, spec-named fields (
reserved,reserved_future_use,reserved_flags, … right-aligned to their bit width) and re-emitted verbatim, so a descriptor from a real sender that deviates from the edition this crate was transcribed from (e.g. an AC-3 descriptor withreserved_flags = 1111, an MHP-eraapplication_signalling_descriptorwhose 16-bitapplication_typehas bit 15 clear, a DVB-S descriptor withroll_off = 01) re-serializes byte-identically. There is no raw passthrough blob: every preserved bit is a named, typed field. - Constructed values carry the edition value. A struct built with
new(..)/Defaulttakes the value the governing edition prescribes:reserved/reserved_future_usebits are 1 (the DVB / H.222.0 convention that future equipment sees a “1” for unimplemented bits),reserved_zero_future_usebits are 0, with the explicit per-field exceptions documented on the field (AC-3reserved_flagsis0000in every edition; the J2K extended-branchreserved(5)is00000). - Where a newer edition gives reserved bits a meaning (the carve-outs
listed in
private/specs-verified/dvb/dvb-si/reserved-bits-edition-audit.md§4) the bits are typed with that meaning instead.
The per-edition evidence for every default lives in
private/specs-verified/dvb/dvb-si/reserved-bits-edition-audit.md; the real_capture_byte_identity
test re-serializes every descriptor found in the real-capture archive and
requires byte-identity.
§Design notes
- Zero-copy, symmetric. Every wire type implements
Parse(borrowing from the input) andSerialize; every type has a round-trip test. Over-wide values areError::FieldOverflowon serialize, never masked. - One dispatch list per namespace.
tables::AnyTableSection,descriptors::AnyDescriptor, the extension bodies anddescriptors::AnyAitDescriptorare generated from singledeclare_*!lists with drift tests, so dispatch (and the generated docs) cannot drift from the implemented set. - Labels. Every spec/field enum has
name()and aDisplayfrombroadcast_common::impl_spec_display!. #[non_exhaustive]public types withnew(..)constructors, so additions are not breaking.
§Features
| Feature | Default | Enables |
|---|---|---|
std | on | Links std. Off → #![no_std] + alloc; the non-Latin text charsets and wall-clock time need std. |
chrono | on | MJD + BCD time fields also decode to chrono::DateTime<Utc> (EIT start_time_chrono(), TDT/TOT utc_time()), local time offsets to chrono::Duration; the epg module. Off → broadcast_common::time::MjdBcdDateTime and raw bytes only. |
ts | on | demux::SiDemux over 188-byte TS packets (framing from the mpeg-ts crate). Off → bring your own complete section bytes. |
serde | off | Serialize-only — for display/export (JSON via serde_json); parsing FROM JSON is deliberately unsupported, re-parse from wire bytes. Serialize on every table/descriptor; text::DvbText serializes as its decoded UTF-8 string (camelCase JSON). |
yoke | off | yoke::Yokeable on every zero-copy view type + the owned::Owned wrapper — own a parsed view past the input buffer’s borrow (store/cache/send across threads) without re-parsing or a mirror type. |
flate2 | off | zlib decompression of compressed_module_descriptor modules in object carousels (implies std). |
dvb-si = { version = "11", default-features = false } # no_std + allocMSRV 1.95.0. MIGRATION-4.0.md covers the 3.x → 4.0 API break.
§Examples
Two runnable examples ship with this crate (cargo run -p dvb-si --example <name>).
§build_and_parse_pat
//! Basic: build a PAT section, serialize it to wire bytes, and parse it back.
//!
//! Run with: `cargo run -p dvb-si --example build_and_parse_pat`
//!
//! Shows the symmetric build/parse contract without needing a TS source —
//! every table type works the same way.
use broadcast_common::{Parse, Serialize};
use dvb_si::tables::pat::{PatEntry, PatSection};
use mpeg_ts::pid::Pid;
fn main() {
let pat = PatSection::new(
1,
0,
true,
0,
0,
vec![
PatEntry::new(1, Pid::new(0x0100)),
PatEntry::new(2, Pid::new(0x0200)),
],
);
// Serialize to the on-wire section bytes (CRC-32 appended automatically).
let mut bytes = vec![0u8; pat.serialized_len()];
pat.serialize_into(&mut bytes).expect("buffer is sized");
println!("serialized PAT : {} bytes", bytes.len());
println!("{bytes:02X?}");
// Parse them back and prove equality (the round-trip invariant).
let parsed = PatSection::parse(&bytes).expect("valid PAT");
assert_eq!(parsed, pat);
println!("\ntransport_stream_id = {}", parsed.transport_stream_id);
for e in &parsed.entries {
println!(" program {} → PMT PID {:#06X}", e.program_number, e.pid);
}
}§list_services
//! Advanced: demux a real MPEG-TS capture and list its services and tables.
//!
//! Run with: `cargo run -p dvb-si --example list_services` (needs the default
//! `ts` feature). Reads a committed French TNT capture (which carries an SDT)
//! at runtime.
use dvb_si::demux::SiDemux;
use dvb_si::descriptors::AnyDescriptor;
use dvb_si::tables::AnyTableSection;
use std::collections::BTreeMap;
const PKT: usize = 188;
fn main() {
// Fixtures live in the workspace-shared `fixtures/` tree, not under the
// 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 = test_fixtures::path("ts/tnt-5w-12732v-isi6-10s.ts");
let data = std::fs::read(&path).unwrap_or_else(|e| {
panic!(
"committed fixture {} could not be read: {e}",
path.display()
)
});
let mut demux = SiDemux::builder().build();
let mut tables: BTreeMap<&'static str, u32> = BTreeMap::new();
let mut services = Vec::new();
for pkt in data.chunks(PKT) {
if pkt.len() < PKT {
break;
}
for event in demux.feed(pkt) {
match event.table_section() {
Ok(AnyTableSection::SdtSection(sdt)) => {
*tables.entry("SDT").or_default() += 1;
for service in &sdt.services {
for item in service.descriptors.iter().flatten() {
if let AnyDescriptor::Service(svc) = item {
services.push(format!(
"{} — {} (\"{}\")",
service.service_id,
svc.service_type,
svc.service_name.decode()
));
}
}
}
}
Ok(AnyTableSection::PatSection(_)) => *tables.entry("PAT").or_default() += 1,
Ok(AnyTableSection::PmtSection(_)) => *tables.entry("PMT").or_default() += 1,
Ok(AnyTableSection::NitSection(_)) => *tables.entry("NIT").or_default() += 1,
Ok(AnyTableSection::EitSection(_)) => *tables.entry("EIT").or_default() += 1,
Ok(_) => *tables.entry("other").or_default() += 1,
Err(_) => {}
}
}
}
println!("tables seen (distinct sections):");
for (name, n) in &tables {
println!(" {name:<6} {n}");
}
services.sort();
services.dedup();
println!("\nservices:");
if services.is_empty() {
println!(" (no SDT in this capture)");
}
for s in &services {
println!(" {s}");
}
}Re-exports§
pub use descriptor_tag::DescriptorTag;pub use error::Error;pub use error::Result;pub use table_id::TableId;
Modules§
- carousel
- DSM-CC data-carousel download protocol — ISO/IEC 13818-6 §7.2/§7.3 as profiled by DVB (TR 101 202 §4.6/§4.7.5, TS 102 006 SSU, TS 102 809).
- collect
- Multi-section table collection.
- compatibility
- Compatibility Descriptor — ETSI TS 102 006 §9.4.2.2 Table 15 / ISO/IEC 13818-6.
- demux
ts SiDemux— PID-filtered, version-gated SI section pump.- descriptor_
tag DescriptorTagenum — typed descriptor tag byte values.- descriptors
- DVB + MPEG-2 descriptors. Each descriptor tag gets its own submodule file.
- epg
chrono - EPG convenience layer.
- error
- Error type returned by every parser in this crate.
- owned
yoke - Own a parsed view past the input buffer’s borrow (feature
yoke). - quality_
access_ unit Quality_Access_Unit()— Rec. ITU-T H.222.0 (08/2023) | ISO/IEC 13818-1:2023 §2.20.1, Table 2-164 (PDF pp.250-251, printed pp.228-229), syntax inprivate/specs-verified/dvb/dvb-si/tables/iso_13818_1/2-164-quality-access-unit.md, verified on the rendered pages.- table_
id TableIdenum: typedtable_idbyte values.- tables
- SI + PSI table-section parsers.
- text
- DVB-SI text decoding — ETSI EN 300 468 Annex A.
- traits
- SI-specific traits.
Parseis provided bybroadcast_commonand imported directly at call sites.