#![forbid(unsafe_code)]
use std::cell::RefCell;
use std::rc::Rc;
use js_sys::Uint8Array;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::JsFuture;
use web_sys::{
EncodedVideoChunk, EncodedVideoChunkInit, EncodedVideoChunkType, PlaneLayout, VideoDecoder,
VideoDecoderConfig as WebVideoDecoderConfig, VideoDecoderInit, VideoFrame,
};
use crate::web::frames::DecodedVideoFrames;
use crate::web::timestamp::timestamp_us_to_i32;
fn take_js_err(cell: &RefCell<Option<JsValue>>) -> Result<(), JsValue> {
cell.borrow_mut().take().map_or(Ok(()), Err)
}
fn video_decoder_config(
codec: &str,
width: u32,
height: u32,
description: Option<&[u8]>,
) -> WebVideoDecoderConfig {
let cfg = WebVideoDecoderConfig::new(codec);
cfg.set_coded_width(width);
cfg.set_coded_height(height);
if let Some(desc) = description {
cfg.set_description_u8_array(&Uint8Array::from(desc));
}
cfg
}
fn timestamp_i32(timestamp_us: f64) -> Result<i32, JsValue> {
timestamp_us_to_i32(timestamp_us)
.ok_or_else(|| JsValue::from_str("timestamp does not fit i32 microseconds"))
}
#[cfg(feature = "video")]
#[wasm_bindgen]
pub async fn is_webcodecs_video_decode_supported(codec: String, width: u32, height: u32) -> bool {
let cfg = video_decoder_config(&codec, width, height, None);
JsFuture::from(VideoDecoder::is_config_supported(&cfg))
.await
.ok()
.and_then(|support| support.get_supported())
.unwrap_or(false)
}
#[cfg(feature = "video")]
#[wasm_bindgen]
#[allow(
clippy::too_many_arguments,
reason = "flattened chunk list crossing a wasm module boundary; see doc comment"
)]
pub async fn decode_video_chunks(
codec: String,
width: u32,
height: u32,
description: Option<Vec<u8>>,
chunk_data: Vec<u8>,
chunk_offsets: Vec<u32>,
chunk_lengths: Vec<u32>,
chunk_timestamps_us: Vec<f64>,
chunk_is_key: Vec<u8>,
) -> Result<DecodedVideoFrames, JsValue> {
let n = chunk_offsets.len();
if chunk_lengths.len() != n || chunk_timestamps_us.len() != n || chunk_is_key.len() != n {
return Err(JsValue::from_str("chunk array length mismatch"));
}
let frames: Rc<RefCell<Vec<VideoFrame>>> = Rc::new(RefCell::new(Vec::new()));
let frames_cb = frames.clone();
let output = Closure::wrap(Box::new(move |frame: VideoFrame| {
frames_cb.borrow_mut().push(frame);
}) as Box<dyn FnMut(VideoFrame)>);
let dec_err: Rc<RefCell<Option<JsValue>>> = Rc::new(RefCell::new(None));
let dec_err_cb = dec_err.clone();
let error = Closure::wrap(Box::new(move |e: JsValue| {
*dec_err_cb.borrow_mut() = Some(e);
}) as Box<dyn FnMut(JsValue)>);
let init = VideoDecoderInit::new(
error.as_ref().unchecked_ref(),
output.as_ref().unchecked_ref(),
);
let dec = VideoDecoder::new(&init).map_err(|_| JsValue::from_str("VideoDecoder::new"))?;
error.forget();
output.forget();
let cfg = video_decoder_config(&codec, width, height, description.as_deref());
dec.configure(&cfg)
.map_err(|_| JsValue::from_str("VideoDecoder::configure"))?;
#[allow(
clippy::needless_range_loop,
reason = "indexes four parallel input vecs plus a chunk_data slice by position"
)]
for i in 0..n {
let start = chunk_offsets[i] as usize;
let end = start + chunk_lengths[i] as usize;
let payload = chunk_data
.get(start..end)
.ok_or_else(|| JsValue::from_str("chunk offset/length out of bounds"))?;
let kind = if chunk_is_key[i] == 0 {
EncodedVideoChunkType::Delta
} else {
EncodedVideoChunkType::Key
};
let timestamp = timestamp_i32(chunk_timestamps_us[i])?;
let chunk_init =
EncodedVideoChunkInit::new_with_u8_array(&Uint8Array::from(payload), timestamp, kind);
let chunk = EncodedVideoChunk::new(&chunk_init)
.map_err(|_| JsValue::from_str("EncodedVideoChunk::new"))?;
dec.decode(&chunk)
.map_err(|_| JsValue::from_str("VideoDecoder::decode"))?;
}
JsFuture::from(dec.flush()).await?;
let _ = dec.close();
take_js_err(&dec_err)?;
let decoded_frames: Vec<VideoFrame> = frames.borrow_mut().drain(..).collect();
let mut timestamps_us = Vec::new();
let mut luma_planes = Vec::new();
for frame in decoded_frames {
timestamps_us.push(frame.timestamp());
luma_planes.push(read_luma_plane(&frame, width, height).await?);
frame.close();
}
Ok(DecodedVideoFrames::new(timestamps_us, luma_planes))
}
#[cfg(feature = "video")]
async fn read_luma_plane(frame: &VideoFrame, width: u32, height: u32) -> Result<Vec<u8>, JsValue> {
let size = frame
.allocation_size()
.map_err(|_| JsValue::from_str("VideoFrame::allocationSize"))?;
let readback = Uint8Array::new_with_length(size);
let layouts = JsFuture::from(frame.copy_to_with_u8_array(&readback))
.await
.map_err(|_| JsValue::from_str("VideoFrame::copyTo"))?;
let luma_layout: PlaneLayout = layouts.get(0);
let stride = luma_layout.get_stride() as usize;
let offset = luma_layout.get_offset() as usize;
let raw = readback.to_vec();
let (width, height) = (width as usize, height as usize);
let mut luma = vec![0u8; width * height];
for row in 0..height {
let row_start = offset + row * stride;
let out_start = row * width;
luma[out_start..out_start + width].copy_from_slice(
raw.get(row_start..row_start + width)
.ok_or_else(|| JsValue::from_str("copyTo buffer too small for stride"))?,
);
}
Ok(luma)
}