transmux 0.27.7

Any-to-any media container muxing hub: demux TS, fMP4/CMAF, MPEG-PS, WebM, FLV, or RTMP into one neutral IR and mux to CMAF/fMP4, progressive MP4, TS, DASH, low-latency DASH, HLS, low-latency HLS, Smooth Streaming, or RTMP. CENC/CBCS encrypt+decrypt, SSAI splice, RTP/RTCP, and an fMP4/CMAF conformance validator; parses codec config headers only, samples stay opaque. no_std + alloc, except the XML manifest paths (DASH/Smooth/PlayReady), which need the std feature.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
//! Per-track codec configuration — [`CodecConfig`] + [`DataCarriage`].
//!
//! Moved out of `pipeline.rs` (media plane step 2a, no-op): same types, same
//! fields, same impls.

use alloc::vec::Vec;

use crate::ac3::{Ac3SpecificBox, Ec3SpecificBox};
use crate::ac4::Ac4SpecificBox;
use crate::av1::Av1ConfigurationBox;
use crate::avc_config::AVCConfigurationBox;
use crate::dts::DtsSpecificBox;
use crate::flac::FlacSpecificBox;
use crate::hevc_config::HEVCConfigurationBox;
use crate::mp4esds::EsdsBox;
use crate::opus::OpusSpecificBox;
use crate::vp9::Vp9ConfigurationBox;
use crate::vvc_config::VvcConfigurationBox;

/// Whether a [`CodecConfig::Data`] elementary stream carries PES packets or
/// PSI/private sections on its PID.
///
/// ISO/IEC 13818-1 §2.4.4.8 / Table 2-34 splits `stream_type` into two
/// carriage families: most types (subtitles, teletext, SMPTE 2038 ANC,
/// metadata, and any unrecognised value) are PES-packetised (§2.4.3.6); a
/// fixed set (`0x05` private_sections, `0x0A`-`0x0D` DSM-CC, `0x14` DSM-CC
/// synchronized download, `0x86` SCTE-35/ANSI-scoped) are carried as raw
/// PSI-style sections instead (§2.4.4), with no PES header at all.
/// Reassembling a section stream with a PES parser (or vice versa) silently
/// yields nothing, so a demuxer/muxer must dispatch on this before touching
/// the payload — see `crate::ts_demux` / `crate::ts_mux` (issue #576).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum DataCarriage {
    /// The elementary stream is PES-packetised (ISO/IEC 13818-1 §2.4.3.6).
    Pes,
    /// The elementary stream carries PSI/private sections directly on its
    /// PID (ISO/IEC 13818-1 §2.4.4), with no PES header at all.
    Sections,
}

impl DataCarriage {
    /// A short label for this carriage kind.
    pub fn name(&self) -> &'static str {
        match self {
            DataCarriage::Pes => "pes",
            DataCarriage::Sections => "sections",
        }
    }
}

broadcast_common::impl_spec_display!(DataCarriage);

/// The wire format of a [`CodecConfig::Subtitle`] track (media plane step 2d).
///
/// `Ttml`/`WebVtt` are ISOBMFF sample entries this crate demuxes
/// ([`crate::media::Fmp4Demux`]); `DvbBitmap`/`Teletext` name the PES-carried
/// broadcast subtitle formats a TS demuxer can identify from their PMT
/// descriptors (ETSI EN 300 468) but does not yet classify as
/// `CodecConfig::Subtitle` itself (still `CodecConfig::Data` on that path
/// today) — the variant exists so a future TS-side classifier has a home
/// without another breaking enum change.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum SubtitleFormat {
    /// TTML/IMSC XML subtitles (ISO BMFF `stpp` sample entry, ISO/IEC
    /// 14496-30 §7.2).
    Ttml,
    /// WebVTT subtitles (ISO BMFF `wvtt` sample entry, ISO/IEC 14496-30 §9.2).
    WebVtt,
    /// DVB (bitmap) subtitling, carried as a PES stream (ETSI EN 300 743).
    DvbBitmap,
    /// Teletext, carried as a PES stream (ETSI EN 300 706 / EN 300 472).
    Teletext,
}

impl SubtitleFormat {
    /// A short label for this subtitle format.
    pub fn name(&self) -> &'static str {
        match self {
            SubtitleFormat::Ttml => "TTML (stpp)",
            SubtitleFormat::WebVtt => "WebVTT (wvtt)",
            SubtitleFormat::DvbBitmap => "DVB bitmap subtitle",
            SubtitleFormat::Teletext => "Teletext",
        }
    }
}

broadcast_common::impl_spec_display!(SubtitleFormat);

/// The ISOBMFF subtitle sample entry a [`CodecConfig::Subtitle`] track was
/// demuxed from (ISO/IEC 14496-30), carried whole so the track can be re-muxed
/// (issue #1186, ex-`TODO(#753)`).
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum SubtitleSampleEntry {
    /// `stpp` XMLSubtitleSampleEntry (TTML/IMSC), ISO/IEC 14496-30 §7.2.
    Ttml(crate::subtitle_entries::XmlSubtitleSampleEntry),
    /// `wvtt` WVTTSampleEntry (WebVTT), ISO/IEC 14496-30 §9.2.
    WebVtt(crate::subtitle_entries::WvttSampleEntry),
}

/// Per-track codec configuration for the initialization segment.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum CodecConfig {
    /// H.264/AVC video (`avc1` sample entry with an `avcC` config box).
    Avc {
        /// The `avcC` decoder configuration record.
        config: AVCConfigurationBox,
        /// Coded width in pixels.
        width: u16,
        /// Coded height in pixels.
        height: u16,
    },
    /// H.265/HEVC video (`hvc1`/`hev1` sample entry with an `hvcC` config box) —
    /// ISO/IEC 14496-15:2017 §8.4.
    Hevc {
        /// The `hvcC` decoder configuration record box.
        config: HEVCConfigurationBox,
        /// Coded width in pixels.
        width: u16,
        /// Coded height in pixels.
        height: u16,
    },
    /// H.266/VVC video (`vvc1`/`vvi1` sample entry with a `vvcC` config box) —
    /// ISO/IEC 14496-15:2022 §11.3.3.
    Vvc {
        /// The `vvcC` decoder configuration record box.
        config: VvcConfigurationBox,
        /// Coded width in pixels.
        width: u16,
        /// Coded height in pixels.
        height: u16,
    },
    /// AAC audio (`mp4a` sample entry with an `esds` box).
    Aac {
        /// The `esds` box carrying the AudioSpecificConfig.
        esds: EsdsBox,
        /// Channel count.
        channel_count: u16,
        /// Sampling rate in Hz (stored 16.16 in the sample entry).
        sample_rate: u32,
        /// Sample size in bits (typically 16).
        sample_size: u16,
    },
    /// AC-3 audio (`ac-3` sample entry with a `dac3` box).
    Ac3 {
        /// The `dac3` config box.
        config: Ac3SpecificBox,
        /// Channel count.
        channel_count: u16,
        /// Sampling rate in Hz.
        sample_rate: u32,
        /// Sample size in bits (typically 16).
        sample_size: u16,
    },
    /// E-AC-3 audio (`ec-3` sample entry with a `dec3` box).
    Eac3 {
        /// The `dec3` config box.
        config: Ec3SpecificBox,
        /// Channel count.
        channel_count: u16,
        /// Sampling rate in Hz.
        sample_rate: u32,
        /// Sample size in bits (typically 16).
        sample_size: u16,
    },
    /// AV1 video (`av01` sample entry with an `av1C` box).
    Av1 {
        /// The `av1C` configuration box.
        config: Av1ConfigurationBox,
        /// Coded width in pixels.
        width: u16,
        /// Coded height in pixels.
        height: u16,
    },
    /// VP9 video (`vp09` sample entry with a `vpcC` box).
    Vp9 {
        /// The `vpcC` configuration box.
        config: Vp9ConfigurationBox,
        /// Coded width in pixels.
        width: u16,
        /// Coded height in pixels.
        height: u16,
    },
    /// Opus audio (`Opus` sample entry with a `dOps` box).
    Opus {
        /// The `dOps` config box.
        config: OpusSpecificBox,
        /// Channel count.
        channel_count: u16,
        /// Sampling rate in Hz (stored 16.16; per spec always 48000).
        sample_rate: u32,
        /// Sample size in bits (typically 16).
        sample_size: u16,
    },
    /// FLAC audio (`fLaC` sample entry with a `dfLa` box).
    Flac {
        /// The `dfLa` config box.
        config: FlacSpecificBox,
        /// Channel count.
        channel_count: u16,
        /// Sampling rate in Hz.
        sample_rate: u32,
        /// Sample size in bits.
        sample_size: u16,
    },
    /// AC-4 audio (`ac-4` sample entry with a `dac4` box).
    Ac4 {
        /// The `dac4` config box (opaque `ac4_dsi_v1()`).
        config: Ac4SpecificBox,
        /// Channel count.
        channel_count: u16,
        /// Sampling rate in Hz.
        sample_rate: u32,
        /// Sample size in bits (16 per spec).
        sample_size: u16,
    },
    /// MPEG-H 3D Audio (`mha1` sample entry with an `mhaC` box) — ISO/IEC 23008-3 §20.
    ///
    /// Use `mha1` for raw MHAS frames (config in `mhaC`).  For in-band MHAS
    /// (`mhm1`) the caller should convert the `codec_type` on the resulting
    /// [`MhaSampleEntry`](crate::init_segment::MhaSampleEntry) if needed;
    /// `build_trak` always emits `mha1`.
    MpegH {
        /// The `MHADecoderConfigurationRecord` carried in the `mhaC` box.
        config: crate::mpegh::MHADecoderConfigurationRecord,
        /// Channel count.
        channel_count: u16,
        /// Sampling rate in Hz.
        sample_rate: u32,
        /// Sample size in bits (typically 16).
        sample_size: u16,
    },
    /// MPEG-2 video / H.262 (`mp4v` sample entry with an `esds` box) —
    /// ISO/IEC 13818-2 (ITU-T H.262) carried per ISO/IEC 14496-1 §7.2.6.6
    /// (ObjectTypeIndication 0x60–0x65, 0x61 = MPEG-2 Main Visual).
    ///
    /// The `esds` carries the ES/decoder descriptors (its DecoderSpecificInfo
    /// optionally the `sequence_header()` bytes); the coded picture geometry is
    /// decoded from the in-band `sequence_header()` (ISO/IEC 13818-2 §6.2.2.1).
    Mpeg2Video {
        /// The `esds` box (its body is re-embedded byte-identically).
        esds: EsdsBox,
        /// Coded width in pixels (from the `sequence_header()`).
        width: u16,
        /// Coded height in pixels (from the `sequence_header()`).
        height: u16,
    },
    /// MPEG-1/2 audio, Layers I/II/III (`mp4a` sample entry with an `esds` box)
    /// — ISO/IEC 11172-3 / ISO/IEC 13818-3, carried per ISO/IEC 14496-1
    /// §7.2.6.6 (ObjectTypeIndication 0x69 = MPEG-2 audio, 0x6B = MPEG-1 audio).
    MpegAudio {
        /// The `esds` box (OTI 0x69/0x6B; DecoderSpecificInfo usually empty).
        esds: EsdsBox,
        /// Audio layer (1/2/3), from the first frame header.
        layer: crate::mpeg_legacy::MpegAudioLayer,
        /// Channel count.
        channel_count: u16,
        /// Sampling rate in Hz.
        sample_rate: u32,
        /// Sample size in bits (typically 16).
        sample_size: u16,
    },
    /// DTS audio (`dtsc`/`dtsh`/`dtsl`/`dtse` sample entry with a `ddts` box) —
    /// ETSI TS 102 114 §E.2.
    ///
    /// `codec_fourcc` selects the sample-entry FourCC:
    /// `dtsc` (core only), `dtsh` (core + extension / multi-asset),
    /// `dtsl` (LBR only), or `dtse` (extension substream only).
    /// Use [`crate::dts::DTSC_FOURCC`] etc. for the named constants.
    Dts {
        /// The `ddts` DTSSpecificBox.
        config: DtsSpecificBox,
        /// Sample-entry FourCC: one of `dtsc`, `dtsh`, `dtsl`, `dtse`.
        codec_fourcc: [u8; 4],
        /// Channel count.
        channel_count: u16,
        /// Sampling rate in Hz (48000, 44100, or 32000 per §E.2.2.2).
        sample_rate: u32,
        /// Sample size in bits (always 16 per §E.2.2.2).
        sample_size: u16,
    },
    /// VP8 video (WebM-native; RFC 6386).
    ///
    /// VP8 has **no** out-of-band configuration box — the coded dimensions are
    /// carried in the key-frame header (RFC 6386 §9.1) and decoded from the first
    /// key frame at demux time (see `transmux/docs/codec/vp8-vorbis-webm.md`).
    /// Carried in the IR for `{WebM} → IR → {WebM}` / inspection; there is no
    /// ISOBMFF sample entry for VP8 in this crate (out of scope), so it does not
    /// participate in the fMP4 mux path.
    Vp8 {
        /// Coded width in pixels (key-frame header, masked `& 0x3FFF`).
        width: u16,
        /// Coded height in pixels (key-frame header, masked `& 0x3FFF`).
        height: u16,
    },
    /// Vorbis audio (WebM-native; Vorbis I specification, xiph.org).
    ///
    /// The three setup headers (Identification / Comment / Setup) are carried
    /// verbatim in `codec_private` (Xiph-laced, exactly as WebM `CodecPrivate`
    /// stores them — see `transmux/docs/codec/vp8-vorbis-webm.md`); `channels`
    /// and `sample_rate` are decoded from the Identification header (Vorbis I
    /// §4.2.2). Carried in the IR for `{WebM} → IR → {WebM}` / inspection; there
    /// is no ISOBMFF sample entry for Vorbis in this crate (out of scope), so it
    /// does not participate in the fMP4 mux path.
    Vorbis {
        /// The Xiph-laced 3-header `CodecPrivate`, verbatim.
        codec_private: Vec<u8>,
        /// Channel count (`audio_channels`, Vorbis I §4.2.2).
        channels: u16,
        /// Sampling rate in Hz (`audio_sample_rate`, Vorbis I §4.2.2).
        sample_rate: u32,
    },
    /// Subtitle track (media plane step 2d): a TTML/WebVTT ISOBMFF sample
    /// entry (`stpp`/`wvtt`, ISO/IEC 14496-30) demuxed by
    /// [`crate::media::Fmp4Demux`], carried losslessly rather than dropped.
    /// Samples are opaque payloads (TTML XML documents / WebVTT `vttc`/`vtte`
    /// boxes) — this crate never parses cue contents, only the sample
    /// entry's format tag.
    ///
    /// A track demuxed from ISOBMFF carries its full sample entry
    /// (`sample_entry`) and so re-muxes; a track with no sample entry
    /// (`sample_entry: None`) cannot be reconstructed, so every fMP4/CMAF mux
    /// entry point rejects it — naming it with
    /// [`Error::UnmuxableSubtitleTrack`](crate::error::Error::UnmuxableSubtitleTrack)
    /// (media plane step-2 fix wave 1, B1) — rather than silently dropping or
    /// mis-emitting it; a caller must pre-filter with
    /// [`Media::select_tracks_by`](crate::media::Media::select_tracks_by).
    Subtitle {
        /// Which subtitle wire format this track carries.
        format: SubtitleFormat,
        /// The ISOBMFF sample entry (`stpp` / `wvtt`) the track was demuxed
        /// from, carried whole (namespace / `vttC` header block / trailing
        /// boxes) so `build_trak` can re-emit it byte-for-byte (issue #1186,
        /// ex-`TODO(#753)`). `None` for a subtitle track with no ISOBMFF
        /// origin (a TS DVB/Teletext stream): such a track has no sample
        /// entry to reconstruct and is rejected by every fMP4/CMAF mux entry
        /// point with
        /// [`Error::UnmuxableSubtitleTrack`](crate::error::Error::UnmuxableSubtitleTrack).
        sample_entry: Option<SubtitleSampleEntry>,
    },
    /// Opaque data track (issue #557/#576): a PMT-listed elementary stream
    /// whose `stream_type` is not a codec this crate decodes (DVB subtitles
    /// EN 300 743, teletext EN 300 472, SMPTE 2038 ANC, ID3/KLV metadata,
    /// SCTE-35, DSM-CC, private sections, and any other/unrecognised
    /// `stream_type`) — carried losslessly rather than dropped. `stream_type`
    /// per ISO/IEC 13818-1 Table 2-34; `descriptors` is the raw PMT ES_info
    /// descriptor loop so consumers can classify it (e.g. with dvb-si's
    /// parsers); `carriage` records whether the samples are PES payloads or
    /// whole PSI/private sections — see [`DataCarriage`].
    ///
    /// Carried in the IR for `{TS} → IR → {TS}` / inspection; there is no
    /// ISOBMFF sample entry for an opaque stream in this crate (out of
    /// scope), so it does not participate in the fMP4 mux path (mirrors
    /// [`CodecConfig::Vp8`] / [`CodecConfig::Vorbis`]) — every fMP4/CMAF mux
    /// entry point (media plane step-2 fix wave 1) rejects such a track,
    /// naming it, rather than silently omitting it (issue #576/#557); a
    /// caller mixing carriable and opaque tracks must pre-filter with
    /// [`Media::select_tracks_by`](crate::media::Media::select_tracks_by) —
    /// see [`CodecConfig::is_muxable_in_bmff`].
    Data {
        /// PMT `stream_type` (ISO/IEC 13818-1 Table 2-34).
        stream_type: u8,
        /// The raw PMT ES_info descriptor-loop bytes for this stream.
        descriptors: Vec<u8>,
        /// Whether this elementary stream carries PES packets or PSI/private
        /// sections (ISO/IEC 13818-1 §2.4.4.8) — determines how a demuxer
        /// reassembles it and how a muxer re-emits it.
        carriage: DataCarriage,
    },
}

impl CodecConfig {
    pub(crate) fn is_audio(&self) -> bool {
        matches!(
            self,
            CodecConfig::Aac { .. }
                | CodecConfig::Ac3 { .. }
                | CodecConfig::Eac3 { .. }
                | CodecConfig::Opus { .. }
                | CodecConfig::Flac { .. }
                | CodecConfig::Ac4 { .. }
                | CodecConfig::MpegH { .. }
                | CodecConfig::Dts { .. }
                | CodecConfig::MpegAudio { .. }
                | CodecConfig::Vorbis { .. }
        )
    }

    /// Sampling rate in Hz of an audio track, `None` for a non-audio variant
    /// (issue #1162 R2). For AAC / AC-3 / E-AC-3 / MPEG audio this is the rate
    /// the demuxer decoded from the stream's own header (ADTS
    /// `sampling_frequency_index`, AC-3 `fscod`, MPEG audio
    /// `sampling_frequency`).
    pub fn audio_sample_rate(&self) -> Option<u32> {
        match self {
            CodecConfig::Aac { sample_rate, .. }
            | CodecConfig::Ac3 { sample_rate, .. }
            | CodecConfig::Eac3 { sample_rate, .. }
            | CodecConfig::Opus { sample_rate, .. }
            | CodecConfig::Flac { sample_rate, .. }
            | CodecConfig::Ac4 { sample_rate, .. }
            | CodecConfig::MpegH { sample_rate, .. }
            | CodecConfig::Dts { sample_rate, .. }
            | CodecConfig::MpegAudio { sample_rate, .. }
            | CodecConfig::Vorbis { sample_rate, .. } => Some(*sample_rate),
            _ => None,
        }
    }

    /// Channel count of an audio track **including** the LFE channel (issue
    /// #1162 R2), `None` for a non-audio variant or when the codec config does
    /// not determine one.
    ///
    /// The `channel_count` field of [`CodecConfig::Ac3`]/[`CodecConfig::Eac3`]
    /// is full-bandwidth only (a 5.1 AC-3 track reports 5, matching the
    /// `AC3SampleEntry` `channelcount` convention); this accessor adds the LFE
    /// from the `dac3`/`dec3` `lfeon` flag (and, for E-AC-3, the extra
    /// locations of dependent substreams, ETSI TS 102 366 Table F.6.1). For the
    /// other codecs the stored `channel_count` already counts every channel
    /// (AAC: ISO/IEC 14496-3 Table 1.19, 5.1 = 6; DTS: `CHS` + LFE; MPEG audio:
    /// 1 or 2; Opus/FLAC/Vorbis have no separate LFE concept); an AAC stream
    /// whose mapping is carried in-band by a `program_config_element`, and an
    /// MPEG-H/AC-4 track whose count is unspecified, return `None`.
    pub fn channels_with_lfe(&self) -> Option<u16> {
        let n = match self {
            CodecConfig::Ac3 { config, .. } => u16::from(config.channels_with_lfe()),
            CodecConfig::Eac3 { config, .. } => u16::from(config.channels_with_lfe()),
            CodecConfig::Aac { channel_count, .. }
            | CodecConfig::Opus { channel_count, .. }
            | CodecConfig::Flac { channel_count, .. }
            | CodecConfig::Ac4 { channel_count, .. }
            | CodecConfig::MpegH { channel_count, .. }
            | CodecConfig::Dts { channel_count, .. }
            | CodecConfig::MpegAudio { channel_count, .. } => *channel_count,
            CodecConfig::Vorbis { channels, .. } => *channels,
            _ => return None,
        };
        (n != 0).then_some(n)
    }

    /// Nominal bit rate of an audio track in bits per second (issue #1162 R2),
    /// `None` when the stream does not state one or for a non-audio variant.
    ///
    /// - AC-3: `bit_rate_code` (ETSI TS 102 366 Table F.4.1, from `frmsizecod`).
    /// - E-AC-3: `dec3` `data_rate` (§F.6.2.2, whole bitstream).
    /// - MPEG audio: the first frame header's `bitrate_index`, carried as the
    ///   `esds` `avgBitrate` by the TS demuxer (constant-bitrate assumption).
    /// - AAC: ADTS and `AudioSpecificConfig` carry no bit rate; the `esds`
    ///   `avgBitrate` is reported when the source signalled a non-zero one
    ///   (0 = VBR/unknown), else `None`.
    /// - DTS: the core `ddts` `avgBitrate` (`None` for open/unspecified rate).
    pub fn nominal_bitrate(&self) -> Option<u32> {
        let bps = match self {
            CodecConfig::Ac3 { config, .. } => config.nominal_bitrate_kbps()?.checked_mul(1000)?,
            CodecConfig::Eac3 { config, .. } => config.nominal_bitrate_kbps().checked_mul(1000)?,
            CodecConfig::Aac { esds, .. } | CodecConfig::MpegAudio { esds, .. } => esds
                .es_descriptor
                .decoder_config
                .as_ref()
                .map_or(0, |dc| dc.avg_bitrate),
            CodecConfig::Dts { config, .. } => config.avg_bitrate,
            _ => return None,
        };
        (bps != 0).then_some(bps)
    }

    /// True for the opaque [`CodecConfig::Data`] variant (issue #557/#576): a
    /// PMT-carried elementary stream with no ISOBMFF sample entry in this
    /// crate. Every fMP4/CMAF mux entry point (init segment + every packager
    /// built on it) rejects such a track, naming it, rather than silently
    /// omitting it — mirrors how the TS mux path, unlike this one, *can*
    /// carry them verbatim. See [`CodecConfig::is_muxable_in_bmff`], which
    /// also covers an entry-less [`CodecConfig::Subtitle`] (B1).
    pub(crate) fn is_opaque_data(&self) -> bool {
        matches!(self, CodecConfig::Data { .. })
    }

    /// True for a [`CodecConfig::Subtitle`] with no `sample_entry`: this
    /// crate has no ISOBMFF sample entry to re-emit for it (issue #1186).
    pub(crate) fn is_subtitle_without_entry(&self) -> bool {
        matches!(
            self,
            CodecConfig::Subtitle {
                sample_entry: None,
                ..
            }
        )
    }

    /// True when `self` can be placed into an ISOBMFF `trak` by this crate —
    /// i.e. neither the opaque [`CodecConfig::Data`] carriage nor
    /// [`CodecConfig::Subtitle`] (media plane step-2 fix wave 1, B1-B4: the
    /// single predicate every fMP4/CMAF mux entry point
    /// ([`build_init_segment`](crate::pipeline::build_init_segment) and
    /// every packager built on it — `CmafMux`, `ProgressiveMux`, `Segmenter`,
    /// `LlSegmenter`, `LlHlsSegmenter`) checks before building a `trak`:
    /// a track failing this predicate is rejected, naming the track, rather
    /// than silently dropped — a caller mixing carriable and non-carriable
    /// tracks must pre-filter first (e.g. with
    /// [`Media::select_tracks_by`](crate::media::Media::select_tracks_by)).
    ///
    /// Does NOT cover the WebM-native [`CodecConfig::Vp8`]/[`CodecConfig::Vorbis`]
    /// (genuinely unimplemented fMP4 carriage, not a filterable policy choice
    /// — those still fail with the generic
    /// [`Error::UnsupportedCodec`](crate::error::Error::UnsupportedCodec)
    /// regardless of filtering).
    ///
    /// `pub` (unlike the crate's other `CodecConfig` predicates): this is the
    /// caller-facing filter predicate the `Unmuxable*Track` errors point to,
    /// e.g. `media.select_tracks_by(|t| t.spec.config.is_muxable_in_bmff())`.
    pub fn is_muxable_in_bmff(&self) -> bool {
        !(self.is_opaque_data() || self.is_subtitle_without_entry())
    }

    /// True if `self` is a video codec (issue #628) — codec-family-complete
    /// regardless of which output container actually carries it (e.g. the TS
    /// mux path does not carry every variant matched here; see
    /// `ts_mux::EsKind::from_config`). Mirrors [`CodecConfig::is_audio`]. Used
    /// to pick the anchor/segmentation track (`ts_hls::choose_anchor`,
    /// `Segmenter::new`).
    pub(crate) fn is_video(&self) -> bool {
        matches!(
            self,
            CodecConfig::Avc { .. }
                | CodecConfig::Hevc { .. }
                | CodecConfig::Vvc { .. }
                | CodecConfig::Av1 { .. }
                | CodecConfig::Vp9 { .. }
                | CodecConfig::Vp8 { .. }
                | CodecConfig::Mpeg2Video { .. }
        )
    }
}