transmux 0.29.1

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
//! Typed audio-track metadata recovered from a TS track's PMT `ES_info`
//! descriptor loop (issue #1162 R1).
//!
//! The loop is carried verbatim on [`TrackSpec::es_info_descriptors`]; this
//! module decodes the descriptors that identify *what an audio track is for*,
//! reusing `dvb-si`'s parsers (no second descriptor parser):
//!
//! - `ISO_639_language_descriptor` (ISO/IEC 13818-1 §2.6.18, tag `0x0A`):
//!   repeated `ISO_639_language_code` + `audio_type` (§2.6.19 Table 2-60 /
//!   ETSI EN 300 468 §6.2.22).
//! - DVB `supplementary_audio_descriptor` (ETSI EN 300 468 §6.4.11, extension
//!   descriptor tag `0x7F`, `descriptor_tag_extension` `0x06`): `mix_type`
//!   (Table 154), `editorial_classification` (Table 155), optional language.
//! - DVB `AAC_descriptor` (ETSI EN 300 468 Annex H §H.2.1, tag `0x7C`): its
//!   `AAC_type` is a Table 26 `component_type` (`stream_content` `0x6`). Only
//!   the HE-AAC v2 rows (`0x43`..`0x46`, `0x49`, `0x4A`) are acted on: v2 means
//!   Parametric Stereo, which always rides on SBR. The v1 rows (`0x01`, `0x03`,
//!   `0x05`, `0x40`..`0x42`, `0x47`, `0x48`, `0xA0`) carry Table 26 note 7,
//!   which says plain AAC-LC streams must use these same values too, so they do
//!   not show SBR is present and are deliberately not mapped.

use alloc::string::String;
use alloc::vec::Vec;

use dvb_si::descriptors::aac::AacComponentType;
use dvb_si::descriptors::any::{AnyDescriptor, parse_loop};
use dvb_si::descriptors::extension::ExtensionBody;
use dvb_si::descriptors::iso_639_language::AudioType as DvbAudioType;

use super::{Track, TrackSpec};
use crate::aac_asc::HeAacProfile;

/// Language code France 2 / TF1 use for their audio-description track.
const LANG_AUDIO_DESCRIPTION: &str = "qad";

/// `editorial_classification` values (ETSI EN 300 468 Table 155; `0x00` is
/// "main audio" per Annex J.4).
const EDITORIAL_MAIN: u8 = 0x00;
const EDITORIAL_AUDIO_DESCRIPTION: u8 = 0x01;
const EDITORIAL_CLEAN_AUDIO_HI: u8 = 0x02;
const EDITORIAL_SPOKEN_SUBTITLES: u8 = 0x03;
const EDITORIAL_PARAMETRIC: u8 = 0x04;
const EDITORIAL_GENERAL_AUDIENCE: u8 = 0x17;
const EDITORIAL_USER_DEFINED_MIN: u8 = 0x18;
const EDITORIAL_USER_DEFINED_MAX: u8 = 0x1F;

/// `audio_type` value of an `ISO_639_language_descriptor` entry — ISO/IEC
/// 13818-1 §2.6.19 Table 2-60.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum AudioType {
    /// `0x00` — undefined.
    Undefined,
    /// `0x01` — clean effects.
    CleanEffects,
    /// `0x02` — hearing impaired.
    HearingImpaired,
    /// `0x03` — visual impaired commentary.
    VisualImpairedCommentary,
    /// Any other value (user private `0x04..=0x7F`, DVB-extended `0x80..=0x84`,
    /// reserved), carried losslessly.
    Other(u8),
}

impl AudioType {
    fn from_dvb(t: DvbAudioType) -> Self {
        match t {
            DvbAudioType::Undefined => Self::Undefined,
            DvbAudioType::CleanEffects => Self::CleanEffects,
            DvbAudioType::HearingImpaired => Self::HearingImpaired,
            DvbAudioType::VisualImpairedCommentary => Self::VisualImpairedCommentary,
            other => Self::Other(other.to_u8()),
        }
    }

    /// Spec label.
    pub fn name(&self) -> &'static str {
        match self {
            Self::Undefined => "undefined",
            Self::CleanEffects => "clean effects",
            Self::HearingImpaired => "hearing impaired",
            Self::VisualImpairedCommentary => "visual impaired commentary",
            Self::Other(_) => "other",
        }
    }
}
broadcast_common::impl_spec_display!(AudioType, Other);

/// `mix_type` — ETSI EN 300 468 Table 154.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum MixType {
    /// `0` — a dependent stream, to be mixed by the receiver with a separate
    /// complete and independent stream.
    Dependent,
    /// `1` — a complete and independent stream.
    Independent,
}

impl MixType {
    /// Spec label.
    pub fn name(&self) -> &'static str {
        match self {
            Self::Dependent => "dependent",
            Self::Independent => "independent",
        }
    }
}
broadcast_common::impl_spec_display!(MixType);

/// `editorial_classification` — ETSI EN 300 468 Table 155 (value `0x00` is
/// "main audio" per Annex J.4's audio-purpose table).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum EditorialClassification {
    /// `0x00` — main audio (Annex J.4).
    MainAudio,
    /// `0x01` — audio description for the visually impaired.
    AudioDescription,
    /// `0x02` — clean audio for the hearing impaired.
    CleanAudioHearingImpaired,
    /// `0x03` — spoken subtitles for the visually impaired.
    SpokenSubtitles,
    /// `0x04` — dependent parametric data stream (not embedded).
    DependentParametricData,
    /// `0x17` — unspecific supplementary audio for the general audience.
    GeneralAudience,
    /// `0x18..=0x1F` — user defined.
    UserDefined(u8),
    /// `0x05..=0x16` — reserved for future use.
    Reserved(u8),
}

impl EditorialClassification {
    fn from_u8(v: u8) -> Self {
        match v {
            EDITORIAL_MAIN => Self::MainAudio,
            EDITORIAL_AUDIO_DESCRIPTION => Self::AudioDescription,
            EDITORIAL_CLEAN_AUDIO_HI => Self::CleanAudioHearingImpaired,
            EDITORIAL_SPOKEN_SUBTITLES => Self::SpokenSubtitles,
            EDITORIAL_PARAMETRIC => Self::DependentParametricData,
            EDITORIAL_GENERAL_AUDIENCE => Self::GeneralAudience,
            EDITORIAL_USER_DEFINED_MIN..=EDITORIAL_USER_DEFINED_MAX => Self::UserDefined(v),
            v => Self::Reserved(v),
        }
    }

    /// Spec label.
    pub fn name(&self) -> &'static str {
        match self {
            Self::MainAudio => "main audio",
            Self::AudioDescription => "audio description",
            Self::CleanAudioHearingImpaired => "clean audio for the hearing impaired",
            Self::SpokenSubtitles => "spoken subtitles",
            Self::DependentParametricData => "dependent parametric data",
            Self::GeneralAudience => "general audience supplementary audio",
            Self::UserDefined(_) => "user defined",
            Self::Reserved(_) => "reserved",
        }
    }
}
broadcast_common::impl_spec_display!(EditorialClassification, UserDefined, Reserved);

/// What an audio track is for, derived from its PMT descriptors.
///
/// Broadcasters signal the same purpose in different places, so the role is a
/// documented heuristic over both descriptors, **not** a spec-defined field.
/// Rules, in priority order:
///
/// 1. [`AudioRole::AudioDescription`] when any of: an `audio_type` of
///    `0x03` (visual impaired commentary); a supplementary
///    `editorial_classification` of `0x01` (audio description for the
///    visually impaired); or an ISO 639 language code `qad` (the
///    locally-assigned code France 2 / TF1 use for their AD track, alongside
///    `editorial_classification = 0x01`).
/// 2. [`AudioRole::HearingImpaired`] when `audio_type = 0x02` (hearing
///    impaired, e.g. ORF) or `editorial_classification = 0x02` (clean audio
///    for the hearing impaired).
/// 3. [`AudioRole::Main`] when the loop identifies the track (a language entry
///    and/or a supplementary descriptor) and nothing above matched, and the
///    supplementary descriptor (if any) is `editorial_classification = 0x00`
///    (main audio, ETSI EN 300 468 Annex J.4). Notably `audio_type = 0x01`
///    (clean effects) does **not** imply non-main: ServusTV tags its main mix
///    that way, and Annex J.4 lists "main audio" as `audio_type` `0x00` or
///    `0x01`.
/// 4. [`AudioRole::Unknown`] otherwise: no descriptors, or a supplementary
///    classification that is none of the above (spoken subtitles, dependent
///    parametric data, general-audience supplementary audio, user-defined,
///    reserved).
///
/// Audio description wins over hearing impaired if a (malformed) loop signals
/// both.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum AudioRole {
    /// The programme's main audio.
    Main,
    /// Audio description for the visually impaired.
    AudioDescription,
    /// Hearing-impaired / clean-audio service.
    HearingImpaired,
    /// Not determinable from the descriptors.
    Unknown,
}

impl AudioRole {
    /// Label.
    pub fn name(&self) -> &'static str {
        match self {
            Self::Main => "main",
            Self::AudioDescription => "audio description",
            Self::HearingImpaired => "hearing impaired",
            Self::Unknown => "unknown",
        }
    }
}
broadcast_common::impl_spec_display!(AudioRole);

/// One `ISO_639_language_descriptor` entry.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct AudioLanguage {
    /// ISO 639-2 three-letter code, lower-cased (e.g. `"fre"`, `"qad"`).
    pub language: String,
    /// The entry's `audio_type`.
    pub audio_type: AudioType,
}

/// A decoded `supplementary_audio_descriptor` (ETSI EN 300 468 §6.4.11).
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct SupplementaryAudio {
    /// `mix_type` (Table 154).
    pub mix_type: MixType,
    /// `editorial_classification` (Table 155).
    pub editorial_classification: EditorialClassification,
    /// The descriptor's own language code, lower-cased, when present.
    pub language: Option<String>,
}

/// Typed audio metadata of one track; see [`TrackSpec::audio_metadata`].
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct AudioMetadata {
    /// Every `ISO_639_language_descriptor` entry, in loop order.
    pub languages: Vec<AudioLanguage>,
    /// The first `supplementary_audio_descriptor`, if any.
    pub supplementary: Option<SupplementaryAudio>,
    /// Derived role (heuristic, see [`AudioRole`]).
    pub role: AudioRole,
    /// [`HeAacProfile::V2`] when the PMT `AAC_descriptor` declares an HE-AAC v2
    /// `component_type` (ETSI EN 300 468 Table 26), else `None`. Never `V1`:
    /// the v1 values are also what plain AAC-LC streams must signal (Table 26
    /// note 7), so they prove nothing.
    pub he_aac: Option<HeAacProfile>,
}

impl AudioMetadata {
    /// Decode a PMT `ES_info` descriptor loop. Malformed descriptors are
    /// skipped (never an error): this is best-effort metadata over a broadcast
    /// signal.
    pub fn from_es_info(es_info: &[u8]) -> Self {
        let mut languages = Vec::new();
        let mut supplementary = None;
        let mut he_aac = None;
        for d in parse_loop(es_info).flatten() {
            match d {
                AnyDescriptor::Aac(d) if he_aac.is_none() => {
                    he_aac = d
                        .extension
                        .and_then(|e| e.decoded_component_type())
                        .and_then(he_aac_of_component_type);
                }
                AnyDescriptor::Iso639Language(d) => {
                    for e in d.entries {
                        languages.push(AudioLanguage {
                            language: e.language_code.as_str().to_ascii_lowercase(),
                            audio_type: AudioType::from_dvb(e.audio_type),
                        });
                    }
                }
                AnyDescriptor::Extension(x) => {
                    if let ExtensionBody::SupplementaryAudio(s) = x.body
                        && supplementary.is_none()
                    {
                        supplementary = Some(SupplementaryAudio {
                            mix_type: if s.mix_type {
                                MixType::Independent
                            } else {
                                MixType::Dependent
                            },
                            editorial_classification: EditorialClassification::from_u8(
                                s.editorial_classification,
                            ),
                            language: s
                                .iso_639_language_code
                                .map(|l| l.as_str().to_ascii_lowercase()),
                        });
                    }
                }
                _ => {}
            }
        }
        let role = derive_role(&languages, supplementary.as_ref());
        Self {
            languages,
            supplementary,
            role,
            he_aac,
        }
    }
}

fn he_aac_of_component_type(ct: AacComponentType) -> Option<HeAacProfile> {
    ct.v2.then_some(HeAacProfile::V2)
}

fn derive_role(languages: &[AudioLanguage], sup: Option<&SupplementaryAudio>) -> AudioRole {
    let editorial = sup.map(|s| s.editorial_classification);
    let lang_is_ad = languages
        .iter()
        .any(|l| l.language == LANG_AUDIO_DESCRIPTION)
        || sup
            .and_then(|s| s.language.as_deref())
            .is_some_and(|l| l == LANG_AUDIO_DESCRIPTION);
    if languages
        .iter()
        .any(|l| l.audio_type == AudioType::VisualImpairedCommentary)
        || editorial == Some(EditorialClassification::AudioDescription)
        || lang_is_ad
    {
        return AudioRole::AudioDescription;
    }
    if languages
        .iter()
        .any(|l| l.audio_type == AudioType::HearingImpaired)
        || editorial == Some(EditorialClassification::CleanAudioHearingImpaired)
    {
        return AudioRole::HearingImpaired;
    }
    let identified = !languages.is_empty() || sup.is_some();
    match editorial {
        None | Some(EditorialClassification::MainAudio) if identified => AudioRole::Main,
        _ => AudioRole::Unknown,
    }
}

impl TrackSpec {
    /// This track with its AAC-LC config rewritten as explicit HE-AAC
    /// signalling ([`CodecConfig::with_explicit_he_aac`](super::CodecConfig::with_explicit_he_aac)).
    /// The media timescale becomes the SBR output rate (twice the core rate),
    /// as Apple's encoder writes it in `mdhd` and `mp4a`; samples already cut
    /// against this spec must follow with [`Track::with_explicit_he_aac`] or
    /// [`Sample::rescale_timescale`](super::Sample::rescale_timescale). The
    /// track id, PID and descriptor loop are unchanged.
    pub fn with_explicit_he_aac(mut self, profile: HeAacProfile) -> crate::error::Result<Self> {
        self.config = self.config.with_explicit_he_aac(profile)?;
        if let Some(output_hz) = self.config.audio_sample_rate() {
            self.timescale = output_hz;
        }
        Ok(self)
    }

    /// Typed audio metadata decoded from [`Self::es_info_descriptors`]
    /// (language(s), `audio_type`, supplementary-audio signalling, derived
    /// [`AudioRole`]). Empty with [`AudioRole::Unknown`] for a non-TS source
    /// or a loop carrying none of those descriptors.
    pub fn audio_metadata(&self) -> AudioMetadata {
        AudioMetadata::from_es_info(&self.es_info_descriptors)
    }
}

impl Track {
    /// [`TrackSpec::with_explicit_he_aac`] applied to the track, with every
    /// sample's `dts`/`pts`/`duration` and `start_decode_time` re-expressed
    /// in the new media timescale so no presentation time moves.
    pub fn with_explicit_he_aac(mut self, profile: HeAacProfile) -> crate::error::Result<Self> {
        let from = self.spec.timescale;
        self.spec = self.spec.with_explicit_he_aac(profile)?;
        let to = self.spec.timescale;
        if from != to {
            self.samples = self
                .samples
                .into_iter()
                .map(|s| s.rescale_timescale(from, to))
                .collect();
            self.start_decode_time = u64::try_from(super::sample::rescale_ticks(
                i64::try_from(self.start_decode_time).unwrap_or(i64::MAX),
                from,
                to,
            ))
            .unwrap_or(u64::MAX);
        }
        Ok(self)
    }
}