dvb_si/lib.rs
1//! DVB Service Information (ETSI EN 300 468 V1.20.1) and MPEG-2 PSI — a typed
2//! parser **and** builder for every table and descriptor a DVB transport stream
3//! carries.
4//!
5//! `dvb-si` turns the signalling inside an MPEG transport stream — PAT/PMT,
6//! NIT/SDT/BAT, the EIT programme guide, the TDT/TOT clock, the HbbTV AIT, SSU
7//! and IP/MAC notification tables, DSM-CC data and object carousels — into typed
8//! Rust values whose text fields decode to UTF-8 and whose descriptor loops walk
9//! into typed descriptors, and turns those values back into byte-identical wire
10//! sections. Every layout is cited to its spec clause and round-trip tested
11//! against real broadcast captures.
12//!
13//! # At a glance
14//!
15//! The counts below are generated from the crate's own dispatch lists by
16//! `tools/gen-dvbsi-coverage.py` (CI fails when they go stale). The full
17//! per-code reference — standard, verified clause, Rust type and status for every
18//! table_id and descriptor tag, plus every allocated code that is **not**
19//! implemented — is
20//! `private/specs-verified/dvb/dvb-si/COVERAGE.md`.
21//!
22//! <!-- BEGIN GENERATED: dvb-si-at-a-glance (tools/gen-dvbsi-coverage.py) -->
23//!
24//! | | |
25//! |---|---|
26//! | Section types (table_id) | **31** typed `*Section` parsers covering **72** table_id values — every allocated EN 300 468 Table 2 id |
27//! | Descriptors (tag 0x00–0x7F) | **116** auto-dispatched: **52** MPEG-2 / DSM-CC (0x02–0x3F) + all **64** DVB tags (0x40–0x7F) |
28//! | DVB extension descriptors (0x7F) | **30** typed `descriptor_tag_extension` bodies — every one EN 300 468 V1.20.1 Table 109 allocates |
29//! | 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) |
30//! | AIT descriptors (TS 102 809 / MHP) | **21** typed in the separate AIT descriptor namespace |
31//! | INT / UNT / RNT descriptors | **26** typed in their own scoped namespaces (`AnyIntDescriptor`, `AnyUntDescriptor`, `AnyRntDescriptor`) |
32//! | Private / regional descriptors | **10** PDS- or registration-scoped opt-ins (NorDig, EACEM/D-Book, China GY/T + T/AI AVS2/AVS3, T/UWA) |
33//! | Text (EN 300 468 Annex A) | **7** charset decoders covering every Table A.3 selector + Annex A.1 control codes |
34//! | DSM-CC carousel | **12** layers: section framing, DSI/DII/DDB, module reassembly, BIOP object carousel, carousel file tree |
35//! | Registries | **171** CA_system_id and **174** private_data_specifier names (TSDuck data, build-time) |
36//! | Standards | **19** standards / families — see the table below |
37//! | Known gaps | **7** allocated codes not yet typed — listed in `private/specs-verified/dvb/dvb-si/COVERAGE.md` (private repo) |
38//!
39//! ### Standards supported
40//!
41//! | Standard | Title | Edition implemented | Scope covered | Status |
42//! |---|---|---|---|---|
43//! | 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) |
44//! | 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) |
45//! | 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) |
46//! | 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) |
47//! | 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) |
48//! | 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) |
49//! | 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) |
50//! | 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) |
51//! | ETSI TS 102 772 | MPE-IFEC | V1.1.1 | MPE-IFEC section | Full (1/1 allocated codes) |
52//! | ETSI EN 303 560 | DVB TTML subtitling | V1.1.1 | TTML_subtitling descriptor, downloadable font info section (DFIS) | Full (2/2 allocated codes) |
53//! | ETSI TS 102 825-9 | CPCM system adaptation | V1.2.1 | cpcm_delivery_signalling extension descriptor | Full (1/1 allocated codes) |
54//! | 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 |
55//! | 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 |
56//! | NorDig Unified Requirements | Nordic IRD requirements | v3.2.2 (2025-11) | logical channel descriptor v1 (0x83) + v2 (0x87), PDS 0x00000029 | Full (scoped) |
57//! | 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) |
58//! | GY/T 270-2013 | China DTV closed captions | 2013 | caption_service_descriptor (0x86), `StreamType::ChinaClosedCaption` | Full (scoped) |
59//! | 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) |
60//! | 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) |
61//! | T/UWA 012.2-2023 | UWA UHD TS encapsulation | 2023 | older AVS3 video descriptor (tag 62) | Full (scoped) |
62//!
63//! 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.
64//!
65//! <!-- END GENERATED: dvb-si-at-a-glance -->
66//!
67//! # Quick start
68//!
69//! ## Parse one section, then serialize it back
70//!
71//! The bytes are the PAT of a real French TNT capture committed in the
72//! repository. Every parser has a symmetric serializer, so the round trip is
73//! byte-identical:
74//!
75//! ```
76//! use broadcast_common::{Parse, Serialize};
77//! use dvb_si::tables::AnyTableSection;
78//! use dvb_si::tables::pat::PatSection;
79//!
80//! let section: [u8; 36] = [
81//! 0x00, 0xB0, 0x21, 0x00, 0x06, 0xE5, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x10,
82//! 0x06, 0x01, 0xE0, 0x64, 0x06, 0x06, 0xE2, 0x58, 0x06, 0x08, 0xE1, 0xF4,
83//! 0x06, 0x09, 0xE2, 0xBC, 0x06, 0x0A, 0xE0, 0xC8, 0x98, 0xF3, 0xCC, 0x2D,
84//! ];
85//!
86//! // Known type: parse it directly. Unknown type: dispatch on table_id.
87//! let pat = PatSection::parse(§ion)?;
88//! assert_eq!(pat.entries.len(), 6); // the NIT pointer + five programmes
89//! assert!(matches!(AnyTableSection::parse(§ion)?, AnyTableSection::PatSection(_)));
90//!
91//! let mut out = vec![0u8; pat.serialized_len()];
92//! pat.serialize_into(&mut out)?;
93//! assert_eq!(out, section);
94//! # Ok::<(), dvb_si::Error>(())
95//! ```
96//!
97//! ## Pump TS packets
98//!
99//! Build a [`demux::SiDemux`] (feature `ts`), feed it 188-byte packets, match on
100//! [`tables::AnyTableSection`], walk descriptor loops with
101//! [`descriptors::DescriptorLoop::iter`], and decode text with
102//! [`text::DvbText::decode`]:
103//!
104//! ```
105//! use dvb_si::demux::SiDemux;
106//! use dvb_si::descriptors::AnyDescriptor;
107//! use dvb_si::tables::AnyTableSection;
108//!
109//! let mut demux = SiDemux::builder().build();
110//!
111//! // In real code, `packet` is each aligned 188-byte packet from your TS source
112//! // (file, UDP, tuner). Here we hand-build one PAT packet to keep the doctest
113//! // self-contained — see `dvb-tools dump` (in the `dvb-tools` crate) for the
114//! // file-reading loop.
115//! # let packet = build_pat_packet();
116//! for event in demux.feed(&packet) {
117//! match event.table_section() {
118//! Ok(AnyTableSection::SdtSection(sdt)) => {
119//! for service in &sdt.services {
120//! for item in service.descriptors.iter().flatten() {
121//! if let AnyDescriptor::Service(svc) = item {
122//! // DvbText decodes EN 300 468 Annex A → UTF-8.
123//! println!("service: {}", svc.service_name.decode());
124//! }
125//! }
126//! }
127//! }
128//! Ok(AnyTableSection::PatSection(pat)) => {
129//! println!("PAT v{} on {}", event.version().unwrap_or(0), event.pid());
130//! assert_eq!(pat.entries.len(), 1);
131//! }
132//! Ok(other) => { let _ = other; }
133//! Err(_) => {} // malformed section
134//! }
135//! }
136//!
137//! # // Minimal PAT-in-a-TS-packet builder used by the doctest above.
138//! # fn build_pat_packet() -> [u8; 188] {
139//! # use broadcast_common::Serialize;
140//! # use dvb_si::tables::pat::{PatSection, PatEntry};
141//! # use mpeg_ts::pid::Pid;
142//! # const PMT_PID: Pid = Pid::new(0x0100);
143//! # let pat = PatSection::new(1, 0, true, 0, 0, vec![PatEntry::new(1, PMT_PID)]);
144//! # let mut section = vec![0u8; pat.serialized_len()];
145//! # pat.serialize_into(&mut section).unwrap();
146//! # const TS_SYNC_BYTE: u8 = 0x47;
147//! # const PAYLOAD_UNIT_START_INDICATOR: u8 = 0x40;
148//! # const PID_LOW_BYTE: u8 = 0x00;
149//! # const PAYLOAD_ONLY: u8 = 0x10;
150//! # const POINTER_FIELD_START: u8 = 0x00;
151//! # const STUFFING_BYTE: u8 = 0xFF;
152//! # let mut pkt = [STUFFING_BYTE; 188];
153//! # pkt[0] = TS_SYNC_BYTE;
154//! # pkt[1] = PAYLOAD_UNIT_START_INDICATOR;
155//! # pkt[2] = PID_LOW_BYTE;
156//! # pkt[3] = PAYLOAD_ONLY;
157//! # pkt[4] = POINTER_FIELD_START;
158//! # pkt[5..5 + section.len()].copy_from_slice(§ion);
159//! # pkt
160//! # }
161//! ```
162//!
163//! ## Private descriptors inside their scope
164//!
165//! Private tags are never auto-dispatched; a [`descriptors::DescriptorRegistry`]
166//! opts in, scoped by the `private_data_specifier_descriptor` in front of them:
167//!
168//! ```
169//! use dvb_si::descriptors::nordig::NordigLogicalChannelV1;
170//! use dvb_si::descriptors::{AnyDescriptor, DescriptorLoop, DescriptorRegistry};
171//!
172//! // PDS 0x00000029 (NorDig), then NorDig LCD v1: service 1, visible, LCN 5.
173//! let bytes = [0x5F, 0x04, 0x00, 0x00, 0x00, 0x29, 0x83, 0x04, 0x00, 0x01, 0xC0, 0x05];
174//! let mut registry = DescriptorRegistry::new();
175//! registry.with_nordig_lcn();
176//! let items: Vec<_> = DescriptorLoop::new(&bytes)
177//! .iter_with(®istry)
178//! .collect::<Result<_, _>>()?;
179//! match &items[1] {
180//! AnyDescriptor::Other { value, .. } => {
181//! let lcd = value.downcast_ref::<NordigLogicalChannelV1>().unwrap();
182//! assert_eq!(lcd.entries[0].logical_channel_number, 5);
183//! }
184//! other => panic!("expected the NorDig LCD, got {other:?}"),
185//! }
186//! # Ok::<(), dvb_si::Error>(())
187//! ```
188//!
189//! # Architecture
190//!
191//! ```text
192//! TS packets ─▶ demux::SiDemux ─▶ SectionEvent
193//! │ .table_section()
194//! ▼
195//! tables::AnyTableSection (PatSection, SdtSection, …)
196//! │ section.<loop field> : DescriptorLoop
197//! ▼
198//! descriptors::parse_loop ─▶ AnyDescriptor
199//! │ field : DvbText / LangCode
200//! ▼
201//! text::DvbText::decode() ─▶ UTF-8 String
202//!
203//! SectionEvent.bytes() ─▶ collect::SectionSetCollector ─▶ CompleteSectionSet
204//! │ .table::<T>()
205//! ├ .nit() / .bat() / .sdt() / .eit()
206//! ▼
207//! complete logical tables
208//! ```
209//!
210//! | Module | What it holds |
211//! |---|---|
212//! | [`tables`] | one `*Section` type per table_id (PSI, SI, AIT, UNT, INT, MPE, TV-Anytime, …) + [`tables::AnyTableSection`] dispatch + [`tables::TableRegistry`] |
213//! | [`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 |
214//! | [`carousel`] | DSM-CC download protocol (DSI/DII/DDB), [`carousel::ModuleReassembler`], BIOP object carousel and [`carousel::CarouselFs`] |
215//! | [`compatibility`] | the DSM-CC / SSU `compatibilityDescriptor()` structure |
216//! | [`text`] | [`text::DvbText`] / [`text::LangCode`] — EN 300 468 Annex A decoding |
217//! | [`collect`] | multi-section collection: [`collect::SectionSetCollector`], [`collect::EitCollector`], complete NIT/BAT/SDT/EIT views |
218//! | `demux` | [`SiDemux`](crate::demux::SiDemux): PID-filtered, CRC-checked, version-gated section pump (feature `ts`) |
219//! | `epg` | `EpgStore`: bounded EPG with now/next and schedules (feature `chrono`) |
220//! | `owned` | `Owned<T>`: keep a zero-copy view past its buffer (feature `yoke`) |
221//! | [`table_id`] / [`descriptor_tag`] | the typed [`TableId`] / [`DescriptorTag`] enums |
222//! | [`traits`] | `TableDef` / `DescriptorDef` — the per-type consts the dispatch lists are generated from |
223//! | [`error`] | the structured [`Error`] every parser and serializer returns |
224//!
225//! # Standards
226//!
227//! ETSI EN 300 468 V1.20.1 (DVB SI, incl. Annex A text and Annexes D/G/H/L
228//! audio descriptors), Rec. ITU-T H.222.0 | ISO/IEC 13818-1 (PSI + descriptors,
229//! MPEG-H 3D audio), ISO/IEC 13818-6 as profiled by ETSI TR 101 202 (DSM-CC),
230//! ETSI EN 301 192 (data broadcasting: MPE, MPE-FEC, INT), ETSI TS 102 006
231//! (SSU/UNT), ETSI TS 102 809 (AIT, protection messages), ETSI TS 102 727 (MHP
232//! subset), ETSI TS 102 323 (TV-Anytime: container, RCT, CIT, RNT), ETSI TS
233//! 102 772 (MPE-IFEC), ETSI EN 303 560 (TTML subtitling, DFIS), ETSI TS
234//! 102 825-9 (CPCM), and the PDS-scoped regional extensions (NorDig, EACEM /
235//! D-Book, China GY/T 270 / 420 / 422, T/AI 109.6 / 109.7, T/UWA 012.2). The
236//! table above gives each one's edition and status; `private/specs-verified/dvb/dvb-si/COVERAGE.md` lists
237//! every code.
238//!
239//! # Which API do I use?
240//!
241//! - **A TS source** → [`demux::SiDemux`], then `SectionEvent::table_section()`.
242//! - **Complete section bytes** → [`tables::AnyTableSection::parse`] (or a
243//! specific `tables::<t>::<T>Section::parse`; `parse_with` adds a
244//! [`tables::TableRegistry`]).
245//! - **A table spanning sections** → [`collect`]; **an EPG** → `epg::EpgStore`.
246//! - **A descriptor loop** → [`descriptors::DescriptorLoop::iter`] (or
247//! [`descriptors::parse_loop`] on a bare slice); unknown tags become
248//! [`descriptors::AnyDescriptor::Unknown`], never a panic.
249//! - **Private / regional descriptors** →
250//! [`descriptors::DescriptorLoop::iter_with`] a [`descriptors::DescriptorRegistry`].
251//! - **Emitting SI** → build the struct with `new(..)`, then
252//! [`Serialize::serialize_into`](broadcast_common::Serialize::serialize_into);
253//! packetise with `mpeg-ts`.
254//! - **JSON** → the `serde` feature (serialize-only; re-parse from wire bytes).
255//!
256//! # RFU policy
257//!
258//! One rule for every reserved region (owner decision, supersedes r03-W8/#1077
259//! "drop on parse, emit the spec value"):
260//!
261//! - **Parse is tolerant.** Any value of a `reserved`, `reserved_future_use`
262//! or `reserved_zero_future_use` region is accepted (no rejection) — unlike
263//! `dvb-t2mi`, which validates RFU bits.
264//! - **Serialize preserves what was parsed.** The reserved bits are captured
265//! in typed, spec-named fields (`reserved`, `reserved_future_use`,
266//! `reserved_flags`, ... right-aligned to their bit width) and re-emitted
267//! verbatim, so a descriptor from a real sender that deviates from the
268//! edition this crate was transcribed from (e.g. an AC-3 descriptor with
269//! `reserved_flags = 1111`, an MHP-era `application_signalling_descriptor`
270//! whose 16-bit `application_type` has bit 15 clear, a DVB-S descriptor with
271//! `roll_off = 01`) re-serializes **byte-identically**. There is no raw
272//! passthrough blob: every preserved bit is a named, typed field.
273//! - **Constructed values carry the edition value.** A struct built with
274//! `new(..)` / `Default` takes the value the governing edition prescribes:
275//! `reserved` / `reserved_future_use` bits are **1** (the DVB / H.222.0
276//! convention that future equipment sees a "1" for unimplemented bits),
277//! `reserved_zero_future_use` bits are **0**, with the explicit per-field
278//! exceptions documented on the field (AC-3 `reserved_flags` is `0000` in
279//! every edition; the J2K extended-branch `reserved(5)` is `00000`).
280//! - Where a newer edition gives reserved bits a meaning (the carve-outs
281//! listed in `private/specs-verified/dvb/dvb-si/reserved-bits-edition-audit.md` §4) the bits are typed
282//! with that meaning instead.
283//!
284//! The per-edition evidence for every default lives in
285//! `private/specs-verified/dvb/dvb-si/reserved-bits-edition-audit.md`; the `real_capture_byte_identity`
286//! test re-serializes every descriptor found in the real-capture archive and
287//! requires byte-identity.
288//!
289//! # Design notes
290//!
291//! - **Zero-copy, symmetric.** Every wire type implements
292//! [`Parse`](broadcast_common::Parse) (borrowing from the input) and
293//! [`Serialize`](broadcast_common::Serialize); every type has a round-trip
294//! test. Over-wide values are `Error::FieldOverflow` on serialize, never
295//! masked.
296//! - **One dispatch list per namespace.** [`tables::AnyTableSection`],
297//! [`descriptors::AnyDescriptor`], the extension bodies and
298//! [`descriptors::AnyAitDescriptor`] are generated from single `declare_*!`
299//! lists with drift tests, so dispatch (and the generated docs) cannot drift
300//! from the implemented set.
301//! - **Labels.** Every spec/field enum has `name()` and a `Display` from
302//! `broadcast_common::impl_spec_display!`.
303//! - **`#[non_exhaustive]`** public types with `new(..)` constructors, so
304//! additions are not breaking.
305//!
306//! # Features
307//!
308//! | Feature | Default | Enables |
309//! |---|---|---|
310//! | `std` | on | Links `std`. Off → `#![no_std]` + `alloc`; the non-Latin text charsets and wall-clock time need `std`. |
311//! | `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. |
312//! | `ts` | on | [`demux::SiDemux`] over 188-byte TS packets (framing from the `mpeg-ts` crate). Off → bring your own complete section bytes. |
313//! | `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). |
314//! | `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. |
315//! | `flate2` | off | zlib decompression of `compressed_module_descriptor` modules in object carousels (implies `std`). |
316//!
317//! ```toml
318//! dvb-si = { version = "11", default-features = false } # no_std + alloc
319//! ```
320//!
321//! MSRV 1.95.0. `MIGRATION-4.0.md` covers the 3.x → 4.0 API break.
322#![warn(missing_docs)]
323#![cfg_attr(not(feature = "std"), no_std)]
324#![cfg_attr(docsrs, feature(doc_cfg))]
325// Runnable examples, embedded so they render on docs.rs and stay in sync with
326// the actual `examples/*.rs` files (shown, not compiled).
327#![doc = "\n# Examples\n"]
328#![doc = "Two runnable examples ship with this crate (`cargo run -p dvb-si --example <name>`).\n"]
329#![doc = "\n## `build_and_parse_pat`\n\n```rust,ignore"]
330#![doc = include_str!("../examples/build_and_parse_pat.rs")]
331#![doc = "```\n\n## `list_services`\n\n```rust,ignore"]
332#![doc = include_str!("../examples/list_services.rs")]
333#![doc = "```"]
334// The README is the crates.io landing page; `cfg(doctest)` is set only while
335// rustdoc collects doctests, so every Rust snippet in it is compiled and run
336// by `cargo test --doc` without the README text appearing on docs.rs.
337#![cfg_attr(doctest, doc = include_str!("../README.md"))]
338
339extern crate alloc;
340
341pub mod carousel;
342pub mod collect;
343pub mod compatibility;
344pub mod descriptor_tag;
345pub mod descriptors;
346#[cfg(feature = "chrono")]
347pub mod epg;
348pub mod error;
349pub(crate) mod h222_bits;
350pub mod quality_access_unit;
351mod registry_names;
352pub mod table_id;
353pub mod tables;
354pub mod text;
355pub mod traits;
356pub(crate) mod wire;
357
358#[cfg(feature = "yoke")]
359pub mod owned;
360
361#[cfg(feature = "ts")]
362pub mod demux;
363
364pub use descriptor_tag::DescriptorTag;
365pub use error::{Error, Result};
366pub use table_id::TableId;