openipc-web 0.1.31

WebAssembly bindings for openipc-rs
Documentation
use js_sys::{Object, Reflect, Uint8Array};
use openipc_core::{
    Codec, CodecConfigState, DepacketizedFrame, RtpDepacketizerStatus, RtpReorderStatus,
};
use wasm_bindgen::prelude::*;

pub(crate) fn video_frame_object(frame: DepacketizedFrame) -> Result<Object, JsValue> {
    let object = Object::new();
    let codec_string = codec_string(&frame);
    let codec_config = JsValue::from(codec_config_object(frame.codec_config)?);
    Reflect::set(
        &object,
        &JsValue::from_str("data"),
        &Uint8Array::from(frame.data.as_slice()),
    )?;
    Reflect::set(
        &object,
        &JsValue::from_str("codec"),
        &JsValue::from_str(codec_name(frame.codec)),
    )?;
    Reflect::set(
        &object,
        &JsValue::from_str("codecString"),
        &JsValue::from_str(&codec_string),
    )?;
    Reflect::set(
        &object,
        &JsValue::from_str("isKeyFrame"),
        &JsValue::from_bool(frame.is_keyframe),
    )?;
    Reflect::set(
        &object,
        &JsValue::from_str("timestamp"),
        &JsValue::from_f64(f64::from(frame.timestamp)),
    )?;
    Reflect::set(
        &object,
        &JsValue::from_str("payloadType"),
        &JsValue::from_f64(f64::from(frame.payload_type)),
    )?;
    Reflect::set(
        &object,
        &JsValue::from_str("sequenceNumber"),
        &JsValue::from_f64(f64::from(frame.sequence_number)),
    )?;
    Reflect::set(
        &object,
        &JsValue::from_str("nalType"),
        &JsValue::from_f64(f64::from(frame.nal_type)),
    )?;
    Reflect::set(
        &object,
        &JsValue::from_str("decoderConfigComplete"),
        &JsValue::from_bool(frame.codec_config.is_complete_for(frame.codec)),
    )?;
    Reflect::set(&object, &JsValue::from_str("codecConfig"), &codec_config)?;
    Ok(object)
}

pub(crate) fn rtp_status_object(
    status: RtpDepacketizerStatus,
    reorder: RtpReorderStatus,
) -> Result<Object, JsValue> {
    let object = Object::new();
    let codec_config = JsValue::from(codec_config_object(status.codec_config)?);
    set_u64(&object, "packets", status.packets)?;
    set_u64(&object, "framesEmitted", status.frames_emitted)?;
    set_u64(&object, "configWaitDrops", status.config_wait_drops)?;
    set_u64(
        &object,
        "keyframesWithPrependedConfig",
        status.keyframes_with_prepended_config,
    )?;
    set_u64(
        &object,
        "parameterSetsPrepended",
        status.parameter_sets_prepended,
    )?;
    set_u64(
        &object,
        "fragmentSequenceGaps",
        status.fragment_sequence_gaps,
    )?;
    set_u64(&object, "fragmentOverflows", status.fragment_overflows)?;
    set_u64(&object, "unsupportedPayloads", status.unsupported_payloads)?;
    set_u64(&object, "malformedPackets", status.malformed_packets)?;
    set_option_u8(&object, "lastPayloadType", status.last_payload_type)?;
    set_option_u16(&object, "lastSequenceNumber", status.last_sequence_number)?;
    set_option_u32(&object, "lastTimestamp", status.last_timestamp)?;
    Reflect::set(
        &object,
        &JsValue::from_str("lastCodec"),
        &status
            .last_codec
            .map(codec_name)
            .map(JsValue::from_str)
            .unwrap_or(JsValue::NULL),
    )?;
    set_option_u8(&object, "lastNalType", status.last_nal_type)?;
    Reflect::set(&object, &JsValue::from_str("codecConfig"), &codec_config)?;
    Reflect::set(
        &object,
        &JsValue::from_str("h264ConfigComplete"),
        &JsValue::from_bool(status.codec_config.is_complete_for(Codec::H264)),
    )?;
    Reflect::set(
        &object,
        &JsValue::from_str("h265ConfigComplete"),
        &JsValue::from_bool(status.codec_config.is_complete_for(Codec::H265)),
    )?;
    Reflect::set(
        &object,
        &JsValue::from_str("reorderBufferedPackets"),
        &JsValue::from_f64(reorder.buffered_packets as f64),
    )?;
    set_u64(&object, "reorderedPackets", reorder.reordered_packets)?;
    set_u64(&object, "latePackets", reorder.late_packets)?;
    set_u64(&object, "forcedFlushes", reorder.forced_flushes)?;
    Ok(object)
}

fn codec_config_object(config: CodecConfigState) -> Result<Object, JsValue> {
    let object = Object::new();
    Reflect::set(
        &object,
        &JsValue::from_str("h264Sps"),
        &JsValue::from_bool(config.h264_sps),
    )?;
    Reflect::set(
        &object,
        &JsValue::from_str("h264Pps"),
        &JsValue::from_bool(config.h264_pps),
    )?;
    Reflect::set(
        &object,
        &JsValue::from_str("h265Vps"),
        &JsValue::from_bool(config.h265_vps),
    )?;
    Reflect::set(
        &object,
        &JsValue::from_str("h265Sps"),
        &JsValue::from_bool(config.h265_sps),
    )?;
    Reflect::set(
        &object,
        &JsValue::from_str("h265Pps"),
        &JsValue::from_bool(config.h265_pps),
    )?;
    Ok(object)
}

fn codec_name(codec: Codec) -> &'static str {
    match codec {
        Codec::H264 => "h264",
        Codec::H265 => "h265",
    }
}

fn codec_string(frame: &DepacketizedFrame) -> String {
    match frame.codec {
        Codec::H264 => h264_codec_string(&frame.data).unwrap_or_else(|| "avc1.42E01E".to_owned()),
        Codec::H265 => "hev1.1.6.L93.B0".to_owned(),
    }
}

pub(crate) fn h264_codec_string(frame: &[u8]) -> Option<String> {
    for unit in annex_b_units(frame) {
        let nalu = &frame[unit.start..unit.end];
        if nalu.len() >= 4 && nalu[0] & 0x1f == 7 {
            return Some(format!(
                "avc1.{}{}{}",
                hex_byte(nalu[1]),
                hex_byte(nalu[2]),
                hex_byte(nalu[3])
            ));
        }
    }
    None
}

#[derive(Debug, Clone, Copy)]
struct AnnexBUnit {
    start: usize,
    end: usize,
}

fn annex_b_units(frame: &[u8]) -> Vec<AnnexBUnit> {
    let mut starts = Vec::new();
    let mut index = 0;
    while index + 3 < frame.len() {
        let len = start_code_len(frame, index);
        if len > 0 {
            starts.push(index);
            index += len;
        } else {
            index += 1;
        }
    }
    if starts.is_empty() && !frame.is_empty() {
        return vec![AnnexBUnit {
            start: 0,
            end: frame.len(),
        }];
    }
    starts
        .iter()
        .enumerate()
        .map(|(index, start)| AnnexBUnit {
            start: start + start_code_len(frame, *start),
            end: starts.get(index + 1).copied().unwrap_or(frame.len()),
        })
        .collect()
}

fn start_code_len(frame: &[u8], offset: usize) -> usize {
    if frame.get(offset) != Some(&0) || frame.get(offset + 1) != Some(&0) {
        return 0;
    }
    if frame.get(offset + 2) == Some(&1) {
        return 3;
    }
    if frame.get(offset + 2) == Some(&0) && frame.get(offset + 3) == Some(&1) {
        return 4;
    }
    0
}

fn hex_byte(value: u8) -> String {
    format!("{value:02X}")
}

fn set_u64(object: &Object, key: &str, value: u64) -> Result<(), JsValue> {
    Reflect::set(
        object,
        &JsValue::from_str(key),
        &JsValue::from_f64(value as f64),
    )?;
    Ok(())
}

fn set_option_u8(object: &Object, key: &str, value: Option<u8>) -> Result<(), JsValue> {
    Reflect::set(
        object,
        &JsValue::from_str(key),
        &value
            .map(|value| JsValue::from_f64(f64::from(value)))
            .unwrap_or(JsValue::NULL),
    )?;
    Ok(())
}

fn set_option_u16(object: &Object, key: &str, value: Option<u16>) -> Result<(), JsValue> {
    Reflect::set(
        object,
        &JsValue::from_str(key),
        &value
            .map(|value| JsValue::from_f64(f64::from(value)))
            .unwrap_or(JsValue::NULL),
    )?;
    Ok(())
}

fn set_option_u32(object: &Object, key: &str, value: Option<u32>) -> Result<(), JsValue> {
    Reflect::set(
        object,
        &JsValue::from_str(key),
        &value
            .map(|value| JsValue::from_f64(f64::from(value)))
            .unwrap_or(JsValue::NULL),
    )?;
    Ok(())
}

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

    #[test]
    fn h264_codec_string_comes_from_annex_b_sps() {
        let frame = [0, 0, 0, 1, 0x67, 0x64, 0x00, 0x1f, 0xac, 0xd9];
        assert_eq!(h264_codec_string(&frame).as_deref(), Some("avc1.64001F"));
    }
}