medias 0.5.0

TV media player on bare DRM/KMS: Vulkan UI plane over libmpv video, for local files, UPnP/DLNA servers and YouTube, controlled by HDMI-CEC remote, evdev or terminal
//! HDR signalling: the connector's HDR_OUTPUT_METADATA blob (CTA-861-G static
//! metadata type 1) and the sink's capabilities from the EDID.

/// CTA-861 EOTF codes, also the bit positions in the EDID HDR static metadata block.
pub const EOTF_PQ: u8 = 2;
pub const EOTF_HLG: u8 = 3;

/// Static HDR metadata of the playing video.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Hdr {
    pub eotf: u8,
    /// Mastering display luminance: max in cd/m2, min in 0.0001 cd/m2. 0: unknown.
    pub max_luma: u16,
    pub min_luma: u16,
    /// MaxCLL and MaxFALL in cd/m2. 0: unknown.
    pub max_cll: u16,
    pub max_fall: u16,
}

impl Hdr {
    /// `struct hdr_output_metadata` (linux/hdmi.h): 32 bytes, native endian.
    /// Mastering primaries are not exposed by mpv 0.40; BT.2020 and D65 are
    /// sent, in units of 0.00002, order G, B, R.
    pub fn blob(&self) -> [u8; 32] {
        let mut b = [0u8; 32];
        // b[0..4]: metadata_type 0 (static type 1); b[5]: infoframe metadata_type 0.
        b[4] = self.eotf;
        let words: [u16; 12] = [
            8500, 39850, 6550, 2300, 35400, 14600, 15635, 16450,
            self.max_luma, self.min_luma, self.max_cll, self.max_fall,
        ];
        for (i, w) in words.iter().enumerate() {
            b[6 + i * 2..8 + i * 2].copy_from_slice(&w.to_ne_bytes());
        }
        b
    }
}

/// HDR data blocks of an EDID's CTA-861 extensions.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct SinkHdr {
    /// Bit n: EOTF code n supported (0 SDR, 1 HDR gamma, 2 PQ, 3 HLG).
    pub eotfs: u8,
    /// Colorimetry data block byte: bit 5 BT2020cYCC, 6 BT2020YCC, 7 BT2020RGB.
    pub colorimetry: u8,
    /// Desired content max luminance code value; cd/m2 = 50 * 2^(cv/32).
    pub max_luminance_cv: Option<u8>,
}

impl SinkHdr {
    pub fn parse(edid: &[u8]) -> SinkHdr {
        let mut sink = SinkHdr::default();
        for block in edid.chunks_exact(128).skip(1).filter(|b| b[0] == 0x02) {
            let end = (block[2] as usize).clamp(4, 127);
            let mut i = 4;
            while i < end {
                let (tag, len) = (block[i] >> 5, (block[i] & 0x1f) as usize);
                let data = &block[i + 1..(i + 1 + len).min(128)];
                i += 1 + len;
                // Extended tag 5: colorimetry; 6: HDR static metadata.
                match (tag, data) {
                    (7, [5, bits, ..]) => sink.colorimetry = *bits,
                    (7, [6, eotfs, rest @ ..]) => {
                        sink.eotfs = *eotfs;
                        sink.max_luminance_cv = rest.get(1).copied();
                    }
                    _ => {}
                }
            }
        }
        sink
    }

    pub fn supports(&self, eotf: u8) -> bool {
        self.eotfs & (1 << eotf) != 0
    }
}

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

    #[test]
    fn blob_layout() {
        let b = Hdr { eotf: EOTF_PQ, max_luma: 1000, min_luma: 50, max_cll: 800, max_fall: 400 }.blob();
        assert_eq!(&b[..6], &[0, 0, 0, 0, 2, 0]);
        let word = |o: usize| u16::from_ne_bytes([b[o], b[o + 1]]);
        assert_eq!((word(6), word(18), word(20)), (8500, 15635, 16450));
        assert_eq!((word(22), word(24), word(26), word(28)), (1000, 50, 800, 400));
        assert_eq!(&b[30..], &[0, 0]);
    }

    #[test]
    fn sink_blocks() {
        let mut edid = vec![0u8; 256];
        edid[128] = 0x02;
        let blocks = [0xe3, 5, 0xc0, 0, 0xe4, 6, 0x0d, 1, 96];
        edid[132..132 + blocks.len()].copy_from_slice(&blocks);
        edid[130] = 4 + blocks.len() as u8;
        let sink = SinkHdr::parse(&edid);
        assert_eq!(sink, SinkHdr { eotfs: 0x0d, colorimetry: 0xc0, max_luminance_cv: Some(96) });
        assert!(sink.supports(EOTF_PQ) && sink.supports(EOTF_HLG) && !sink.supports(1));
        assert_eq!(SinkHdr::parse(&edid[..128]), SinkHdr::default());
    }
}