Skip to main content

Crate dvb_si

Crate dvb_si 

Source
Expand description

ETSI EN 300 468 v1.19.1 DVB Service Information parser and builder.

dvb-si turns a raw MPEG-TS into typed, decoded DVB sections and complete logical tables: feed packets in, get back PAT/PMT/SDT/EIT/… section structs whose text fields decode to UTF-8 and whose descriptor loops walk into typed descriptors. Every layout is cited to the ETSI spec and round-trip tested; the same types serialize back to bytes.

§30-second quickstart

Build a demux::SiDemux, feed it TS packets, match on tables::AnyTableSection, walk the descriptor loop with descriptors::DescriptorLoop::iter, and print decoded text via 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
    }
}

§Layer map

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

Each layer is independently usable: a caller who already has complete section bytes can skip demux and call tables::AnyTableSection::parse directly; a caller with a bare descriptor loop can call descriptors::parse_loop on it. Use collect when a table spans multiple sections.

§RFU policy

DVB reserved-bit fields carry a semantic distinction:

  • reserved_future_use bits are emitted as 1 (the DVB convention that future equipment sees a “1” for unimplemented bits).
  • reserved_zero_future_use bits are emitted as 0.

Parsers accept any value (no rejection on non-zero RFU) — unlike dvb-t2mi, which validates RFU bits. This crate prioritises forward compatibility with future broadcast streams.

§Features

FeatureDefaultEnables
chronoonMJD + BCD time fields decode to chrono::DateTime<Utc> (EIT start_time(), TDT/TOT). Off → raw bytes.
tsondemux::SiDemux, ts::SectionReassembler, TS packet parsing. 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.
dvb-si = { version = "4.0", default-features = false }  # tight, no_std-ish build

§Entry points

See the crate README and docs/ for the structured spec reference. 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 dvb_common::{Parse, Serialize};
use dvb_si::tables::pat::{PatEntry, PatSection};

fn main() {
    let pat = PatSection {
        transport_stream_id: 1,
        version_number: 0,
        current_next_indicator: true,
        section_number: 0,
        last_section_number: 0,
        entries: vec![
            PatEntry {
                program_number: 1,
                pid: 0x0100,
            },
            PatEntry {
                program_number: 2,
                pid: 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() {
    let path = concat!(
        env!("CARGO_MANIFEST_DIR"),
        "/tests/fixtures/tnt-5w-12732v-isi6-10s.ts"
    );
    let data = match std::fs::read(path) {
        Ok(b) => b,
        Err(e) => {
            eprintln!("fixture not available ({e}); nothing to do");
            return;
        }
    };

    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.
muxts
Section → TS packetizer (the byte-exact inverse of SectionReassembler::feed).
ownedyoke
Own a parsed view past the input buffer’s borrow (feature yoke).
pid
Reserved DVB/MPEG-2 PIDs.
resyncts
Stateful TS byte-stream resynchroniser — ISO/IEC 13818-1 §2.4.3.2.
section
Generic PSI/SI section framing — ETSI EN 300 468 §5.1.1.
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 dvb_common and imported directly at call sites.
tsts
MPEG-TS packet parser + section reassembler. Feature-gated under ts.