Skip to main content

Crate dvb_si

Crate dvb_si 

Source
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 docs/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 descriptors26 typed in their own scoped namespaces (AnyIntDescriptor, AnyUntDescriptor, AnyRntDescriptor)
Private / regional descriptors9 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 carousel12 layers: section framing, DSI/DII/DDB, module reassembly, BIOP object carousel, carousel file tree
Registries171 CA_system_id and 174 private_data_specifier names (TSDuck data, build-time)
Standards19 standards / families — see the table below
Known gaps7 allocated codes not yet typed — listed in docs/COVERAGE.md

§Standards supported

StandardTitleEdition implementedScope coveredStatus
ETSI EN 300 468DVB Service Information (SI)V1.20.1 (2026-09); RFU defaults audited back to V1.3.1all 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 descriptorsFull (98/98 allocated codes)
Rec. ITU-T H.222.0 | ISO/IEC 13818-1MPEG-2 Systems — PSI and descriptors08/2023 text (clause numbers in COVERAGE.md); MPEG-H 3D audio descriptors 06/2021PAT, CAT, PMT, TSDT; Table 2-45 descriptors; Table 2-110 extension descriptorsPartial (82/85 allocated codes)
ISO/IEC 13818-6DSM-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 descriptorsPartial (3/5 allocated codes)
ETSI EN 301 192DVB data broadcastingV1.7.1MPE datagram, MPE-FEC, INT section; time_slice_fec_identifier + ECM_repetition_rate descriptors; DII carousel descriptorsFull (29/29 allocated codes)
ETSI TS 102 006DVB System Software Update (SSU)V1.4.1UNT section, compatibilityDescriptor, SSU data_broadcast_id selectorFull (16/16 allocated codes)
ETSI TS 102 809Application signalling (HbbTV AIT)V1.3.1AIT section, AIT descriptor namespace, application_signalling + service_identifier descriptors, protection message section + descriptorFull (16/16 allocated codes)
ETSI TS 102 727DVB-MHP 1.1.3V1.1.1 (descriptor subset)DVB-J application + location AIT descriptors, XAIT_location, XAIT_PIDPartial (11/12 allocated codes)
ETSI TS 102 323TV-Anytime carriage in DVBV1.4.1container, RCT, CIT, RNT sections; default_authority, related_content, TVA_id, content_identifier descriptorsFull (11/11 allocated codes)
ETSI TS 102 772MPE-IFECV1.1.1MPE-IFEC sectionFull (1/1 allocated codes)
ETSI EN 303 560DVB TTML subtitlingV1.1.1TTML_subtitling descriptor, downloadable font info section (DFIS)Full (2/2 allocated codes)
ETSI TS 102 825-9CPCM system adaptationV1.2.1cpcm_delivery_signalling extension descriptorFull (1/1 allocated codes)
ETSI TS 101 154Audio/video coding in DVBV2.10.1PMT StreamType values incl. AVS2/AVS3 (0xD2–0xD5); AVS private_data_specifier rulesPartial
ETSI TS 101 162DVB identifiersregistry data from TSDuck (see registries/README.md)CA_system_id and private_data_specifier value → name lookupsPartial
NorDig Unified RequirementsNordic IRD requirementsv3.2.2 (2025-11)logical channel descriptor v1 (0x83) + v2 (0x87), PDS 0x00000029Full (scoped)
EACEM / D-BookLogical channel numberingprivate (PDS 0x00000028); EACEM TR-030 is not vendoredlogical_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-2013China DTV closed captions2013caption_service_descriptor (0x86), StreamType::ChinaClosedCaptionFull (scoped)
GY/T 420-2025, GY/T 422-2025China UHD encapsulation / multi-audio2025AVS2_video (0x40 under ‘AVSV’), AVS3_video; GY/T 422 audio_preselection profileFull (scoped)
T/AI 109.6-2022, T/AI 109.7-2024AVS3 media format / audio transport2022 / 2024AVS3_video (0xD1), AVS3_audio (0xD2), AVS2_audio (0xD3)Full (scoped)
T/UWA 012.2-2023UWA UHD TS encapsulation2023older 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(&section)?;
assert_eq!(pat.entries.len(), 6); // the NIT pointer + five programmes
assert!(matches!(AnyTableSection::parse(&section)?, 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(&registry)
    .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
ModuleWhat it holds
tablesone *Section type per table_id (PSI, SI, AIT, UNT, INT, MPE, TV-Anytime, …) + tables::AnyTableSection dispatch + tables::TableRegistry
descriptorsone 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
carouselDSM-CC download protocol (DSI/DII/DDB), carousel::ModuleReassembler, BIOP object carousel and carousel::CarouselFs
compatibilitythe DSM-CC / SSU compatibilityDescriptor() structure
texttext::DvbText / text::LangCode — EN 300 468 Annex A decoding
collectmulti-section collection: collect::SectionSetCollector, collect::EitCollector, complete NIT/BAT/SDT/EIT views
demuxSiDemux: PID-filtered, CRC-checked, version-gated section pump (feature ts)
epgEpgStore: bounded EPG with now/next and schedules (feature chrono)
ownedOwned<T>: keep a zero-copy view past its buffer (feature yoke)
table_id / descriptor_tagthe typed TableId / DescriptorTag enums
traitsTableDef / DescriptorDef — the per-type consts the dispatch lists are generated from
errorthe 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; docs/COVERAGE.md lists every code.

§Which API do I use?

§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_use or reserved_zero_future_use region is accepted (no rejection) — unlike dvb-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 with reserved_flags = 1111, an MHP-era application_signalling_descriptor whose 16-bit application_type has bit 15 clear, a DVB-S descriptor with roll_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(..) / Default takes the value the governing edition prescribes: reserved / reserved_future_use bits are 1 (the DVB / H.222.0 convention that future equipment sees a “1” for unimplemented bits), reserved_zero_future_use bits are 0, with the explicit per-field exceptions documented on the field (AC-3 reserved_flags is 0000 in every edition; the J2K extended-branch reserved(5) is 00000).
  • Where a newer edition gives reserved bits a meaning (the carve-outs listed in docs/reserved-bits-edition-audit.md §4) the bits are typed with that meaning instead.

The per-edition evidence for every default lives in docs/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) and Serialize; every type has a round-trip test. Over-wide values are Error::FieldOverflow on serialize, never masked.
  • One dispatch list per namespace. tables::AnyTableSection, descriptors::AnyDescriptor, the extension bodies and descriptors::AnyAitDescriptor are generated from single declare_*! lists with drift tests, so dispatch (and the generated docs) cannot drift from the implemented set.
  • Labels. Every spec/field enum has name() and a Display from broadcast_common::impl_spec_display!.
  • #[non_exhaustive] public types with new(..) constructors, so additions are not breaking.

§Features

FeatureDefaultEnables
stdonLinks std. Off → #![no_std] + alloc; the non-Latin text charsets and wall-clock time need std.
chronoonMJD + 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.
tsondemux::SiDemux over 188-byte TS packets (framing from the mpeg-ts crate). Off → bring your own complete section bytes.
serdeonSerialize-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).
yokeoffyoke::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.
flate2offzlib decompression of compressed_module_descriptor modules in object carousels (implies std).
dvb-si = { version = "11", default-features = false }  # no_std + alloc

MSRV 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 = concat!(
        env!("CARGO_MANIFEST_DIR"),
        "/../fixtures/dvb-si/tnt-5w-12732v-isi6-10s.ts"
    );
    let data = std::fs::read(path)
        .unwrap_or_else(|e| panic!("committed fixture {path} could not be read: {e}"));

    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.
demuxts
SiDemux — PID-filtered, version-gated SI section pump.
descriptor_tag
DescriptorTag enum — typed descriptor tag byte values.
descriptors
DVB + MPEG-2 descriptors. Each descriptor tag gets its own submodule file.
epgchrono
EPG convenience layer.
error
Error type returned by every parser in this crate.
ownedyoke
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 in docs/tables/iso_13818_1/2-164-quality-access-unit.md, verified on the rendered pages.
table_id
TableId enum: typed table_id byte values.
tables
SI + PSI table-section parsers.
text
DVB-SI text decoding — ETSI EN 300 468 Annex A.
traits
SI-specific traits. Parse is provided by broadcast_common and imported directly at call sites.