arib-cli 0.2.0

Reads the signalling of ARIB broadcasts and descrambles them, as an example of the arib crate
//! What the streams of a service are, in words, out of the PMT or the MPT and the component
//! descriptors of either transport.

use std::fmt::{Display, Formatter};

use arib::component::{
    AspectRatio, AudioAccessibility, AudioComponentType, AudioMode, SamplingRate,
};
use arib::mmt::descriptor::{
    TransferCharacteristics, VideoComponentDescriptor, VideoFrameRate, VideoResolution,
};
use arib::ts::descriptor::{ComponentType, VideoFormat};

/// A stream of a service, as the PMT or the MPT lists it.
#[derive(Clone, Debug)]
pub struct Stream {
    /// The PID of MPEG-2 TS, or the packet ID of MMT.
    pub id: u16,
    /// The label the component descriptors refer to the stream by.
    pub component_tag: Option<u16>,
    pub kind: Kind,
    pub codec: String,
    /// What the descriptors of the stream itself tell; MMT carries the component descriptors in
    /// the MPT as well as the MH-EIT.
    pub details: Vec<String>,
}

#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum Kind {
    Video,
    Audio,
    Subtitle,
    Data,
}

impl Display for Kind {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        f.write_str(match self {
            Self::Video => "Video",
            Self::Audio => "Audio",
            Self::Subtitle => "Subtitle",
            Self::Data => "Data",
        })
    }
}

/// What a component descriptor of an event tells of the stream it labels.
#[derive(Clone, Debug)]
pub struct Component {
    pub component_tag: u16,
    pub details: Vec<String>,
}

/// The kind and the codec of a stream of MPEG-2 TS, by its stream type in the PMT.
pub fn of_stream_type(stream_type: u8, component_tag: Option<u16>) -> (Kind, String) {
    let (kind, codec) = match stream_type {
        0x01 => (Kind::Video, "MPEG-1"),
        0x02 => (Kind::Video, "MPEG-2"),
        0x1B => (Kind::Video, "H.264"),
        0x24 => (Kind::Video, "HEVC"),
        0x03 => (Kind::Audio, "MPEG-1 Audio"),
        0x04 => (Kind::Audio, "MPEG-2 Audio"),
        0x0F => (Kind::Audio, "AAC (ADTS)"),
        0x11 => (Kind::Audio, "AAC (LATM)"),
        // Told apart by the component tags broadcasters give them.
        0x06 => match component_tag {
            Some(0x30..=0x37) => (Kind::Subtitle, "ARIB caption"),
            Some(0x38..=0x3F) => (Kind::Subtitle, "ARIB superimpose"),
            _ => (Kind::Data, "private PES"),
        },
        0x0D => (Kind::Data, "DSM-CC"),
        0x05 => (Kind::Data, "private sections"),
        stream_type => return (Kind::Data, format!("stream type {stream_type:#04X}")),
    };
    (kind, codec.to_owned())
}

/// The kind and the codec of an asset of MMT, by its type in the MPT.
pub fn of_asset_type(asset_type: [u8; 4]) -> (Kind, String) {
    let (kind, codec) = match &asset_type {
        b"hev1" | b"hvc1" => (Kind::Video, "HEVC"),
        b"mp4a" => (Kind::Audio, "AAC (LATM)"),
        b"stpp" => (Kind::Subtitle, "TTML"),
        b"aapp" => (Kind::Data, "application"),
        _ => {
            return (
                Kind::Data,
                String::from_utf8_lossy(&asset_type).into_owned(),
            );
        }
    };
    (kind, codec.to_owned())
}

/// The video of MPEG-2 TS, by the component type of its component descriptor.
pub fn video_of_component_type(component_type: ComponentType) -> Vec<String> {
    let ComponentType::Video {
        format,
        aspect_ratio,
    } = component_type
    else {
        return vec![];
    };

    [
        known(format, matches!(format, VideoFormat::Unknown(_))),
        aspect_ratio_of(aspect_ratio),
    ]
    .into_iter()
    .flatten()
    .collect()
}

/// The video of MMT, by its video component descriptor.
pub fn video_of_mmt(descriptor: &VideoComponentDescriptor) -> Vec<String> {
    let resolution = descriptor.video_resolution;
    let lines = !matches!(
        resolution,
        VideoResolution::Unspecified | VideoResolution::Unknown(_)
    );
    let scan = if descriptor.video_scan_flag { "p" } else { "i" };
    let frame_rate = descriptor.video_frame_rate;
    let transfer = descriptor.video_transfer_characteristics;

    [
        lines.then(|| format!("{resolution}{scan}")),
        aspect_ratio_of(descriptor.video_aspect_ratio),
        known(
            frame_rate,
            matches!(
                frame_rate,
                VideoFrameRate::Unspecified | VideoFrameRate::Unknown(_)
            ),
        ),
        known(
            transfer,
            matches!(
                transfer,
                TransferCharacteristics::Unspecified | TransferCharacteristics::Unknown(_)
            ),
        ),
    ]
    .into_iter()
    .flatten()
    .collect()
}

fn aspect_ratio_of(aspect_ratio: AspectRatio) -> Option<String> {
    known(
        aspect_ratio,
        matches!(
            aspect_ratio,
            AspectRatio::Unspecified | AspectRatio::Unknown(_)
        ),
    )
}

/// The value as it is displayed, unless it tells nothing.
fn known(value: impl Display, unknown: bool) -> Option<String> {
    (!unknown).then(|| value.to_string())
}

/// The audio of either transport, by its audio component descriptor.
pub struct Audio<'a> {
    pub component_type: AudioComponentType,
    pub sampling_rate: SamplingRate,
    pub main_component_flag: bool,
    pub iso_639_language_code: [u8; 3],
    pub iso_639_language_code_2: Option<[u8; 3]>,
    /// The description, decoded as its transport has it.
    pub text: &'a str,
}

impl Audio<'_> {
    pub fn details(&self) -> Vec<String> {
        // The common modes by the names they go by, and the rest as the standards denote them.
        let mode = match self.component_type.mode {
            AudioMode::Mono => "mono".to_owned(),
            AudioMode::DualMono => "dual mono".to_owned(),
            AudioMode::Stereo => "stereo".to_owned(),
            AudioMode::Mode3_2_1 => "5.1ch".to_owned(),
            AudioMode::Mode5_2_1 => "7.1ch".to_owned(),
            AudioMode::Mode3_3_3_5_2_3_3_0_0_2 => "22.2ch".to_owned(),
            mode => mode.to_string(),
        };
        let sampling_rate = self.sampling_rate;
        let languages = [
            Some(self.iso_639_language_code),
            self.iso_639_language_code_2,
        ]
        .into_iter()
        .flatten()
        .map(|code| String::from_utf8_lossy(&code).into_owned())
        .collect::<Vec<_>>()
        .join("+");
        let accessibility = self.component_type.accessibility;

        [
            Some(mode),
            known(sampling_rate, sampling_rate.hz().is_none()),
            (!languages.is_empty()).then_some(languages),
            self.main_component_flag.then(|| "main".to_owned()),
            known(
                accessibility,
                matches!(
                    accessibility,
                    AudioAccessibility::None | AudioAccessibility::Unknown(_)
                ),
            ),
            self.component_type
                .dialog_control
                .then(|| "dialogue control".to_owned()),
            (!self.text.is_empty()).then(|| format!("\"{}\"", self.text)),
        ]
        .into_iter()
        .flatten()
        .collect()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn describes_video_by_its_component_type() {
        let video = |format, aspect_ratio| {
            video_of_component_type(ComponentType::Video {
                format,
                aspect_ratio,
            })
        };

        assert_eq!(
            video(VideoFormat::I1080, AspectRatio::SixteenByNine),
            ["1080i", "16:9"]
        );
        assert_eq!(
            video(VideoFormat::P2160, AspectRatio::FourByThree),
            ["2160p", "4:3"]
        );
        assert!(video_of_component_type(ComponentType::Other(0x03)).is_empty());
    }

    #[test]
    fn describes_audio_by_its_component() {
        let audio = Audio {
            component_type: AudioComponentType::from(0x03),
            sampling_rate: SamplingRate::Khz48,
            main_component_flag: true,
            iso_639_language_code: *b"jpn",
            iso_639_language_code_2: None,
            text: "日本語",
        };
        assert_eq!(
            audio.details(),
            ["stereo", "48 kHz", "jpn", "main", "\"日本語\""]
        );

        let dual = Audio {
            component_type: AudioComponentType::from(0x02 | 0b0100_0000),
            sampling_rate: SamplingRate::Khz44_1,
            main_component_flag: false,
            iso_639_language_code: *b"jpn",
            iso_639_language_code_2: Some(*b"eng"),
            text: "",
        };
        assert_eq!(
            dual.details(),
            [
                "dual mono",
                "44.1 kHz",
                "jpn+eng",
                "for the hearing impaired"
            ]
        );
    }

    #[test]
    fn names_the_other_audio_modes_as_the_standards_do() {
        let mode = |component_type| {
            Audio {
                component_type: AudioComponentType::from(component_type),
                sampling_rate: SamplingRate::Unknown(0),
                main_component_flag: false,
                iso_639_language_code: *b"jpn",
                iso_639_language_code_2: None,
                text: "",
            }
            .details()
            .remove(0)
        };

        assert_eq!(mode(0x08), "3/2");
        assert_eq!(mode(0x0A), "3/3.1");
        assert_eq!(mode(0x0B), "2/0/0-2/0/2-0.1");
    }
}