waterkit-codec 0.1.1

Hardware-aware video codec with deterministic timing and wgpu texture output
use crate::CodecError;

#[swift_bridge::bridge]
mod ffi {
    #[swift_bridge(swift_repr = "struct")]
    struct SwiftDecodedImage {
        width: u32,
        height: u32,
        pixels: Vec<u8>,
        is_valid: bool,
        hdr: bool,
        pixel_format: u8,
    }

    extern "Swift" {
        fn decode_isobmff_image(data: Vec<u8>) -> SwiftDecodedImage;
        fn av1_hardware_decode_supported() -> bool;
    }
}

const PIXEL_FORMAT_RGBA8_UNORM_SRGB: u8 = 1;
const PIXEL_FORMAT_RGBA16_FLOAT: u8 = 2;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AppleDecodedPixelFormat {
    Rgba8UnormSrgb,
    Rgba16Float,
}

#[derive(Debug)]
pub struct AppleDecodedImage {
    pub width: u32,
    pub height: u32,
    pub pixels: Vec<u8>,
    pub pixel_format: AppleDecodedPixelFormat,
    pub hdr: bool,
}

pub fn is_av1_hardware_decode_supported() -> bool {
    ffi::av1_hardware_decode_supported()
}

pub fn decode_isobmff_image(data: &[u8]) -> Result<AppleDecodedImage, CodecError> {
    let decoded = ffi::decode_isobmff_image(data.to_vec());
    if !decoded.is_valid {
        return Err(CodecError::DecodingFailed(
            "Swift ISOBMFF decode returned invalid image".into(),
        ));
    }
    if decoded.width == 0 || decoded.height == 0 {
        return Err(CodecError::DecodingFailed(
            "Swift ISOBMFF decode returned zero-sized image".into(),
        ));
    }

    let (pixel_format, bytes_per_pixel) = match decoded.pixel_format {
        PIXEL_FORMAT_RGBA8_UNORM_SRGB => (AppleDecodedPixelFormat::Rgba8UnormSrgb, 4usize),
        PIXEL_FORMAT_RGBA16_FLOAT => (AppleDecodedPixelFormat::Rgba16Float, 8usize),
        other => {
            return Err(CodecError::DecodingFailed(format!(
                "Swift ISOBMFF decode returned unknown pixel format tag {other}"
            )));
        }
    };

    let expected_len = usize::try_from(decoded.width)
        .ok()
        .zip(usize::try_from(decoded.height).ok())
        .and_then(|(width, height)| width.checked_mul(height))
        .and_then(|pixels| pixels.checked_mul(bytes_per_pixel))
        .ok_or_else(|| CodecError::DecodingFailed("ISOBMFF decoded buffer size overflow".into()))?;
    if decoded.pixels.len() != expected_len {
        return Err(CodecError::DecodingFailed(format!(
            "Swift ISOBMFF decode returned {} bytes, expected {}",
            decoded.pixels.len(),
            expected_len
        )));
    }

    Ok(AppleDecodedImage {
        width: decoded.width,
        height: decoded.height,
        pixels: decoded.pixels,
        pixel_format,
        hdr: decoded.hdr,
    })
}