use crate::av1::{Av1Obu, Av1Parser};
use crate::codec::{CodecImplementation, CodecSupport, SampleDependency};
use crate::media::required_plane_layouts;
use crate::web_decoder::{js_to_promise, normalize_js_error, schedule_event_loop_tick};
use crate::{
Codec, CodecProfile, Error, ErrorKind, HardwarePreference, Limits, PixelFormat, Result,
VideoFrame,
};
use std::cell::RefCell;
use std::collections::VecDeque;
use std::rc::Rc;
use wasm_bindgen::JsCast;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::JsFuture;
use web_sys::{
AudioData as JsAudioData, AudioDataInit, AudioEncoder as JsAudioEncoder,
AudioEncoderConfig as JsAudioEncoderConfig, AudioEncoderInit, AudioSampleFormat,
EncodedAudioChunk, EncodedAudioChunkMetadata, EncodedAudioChunkType, EncodedVideoChunk,
EncodedVideoChunkMetadata, PlaneLayout, VideoEncoder as JsVideoEncoder,
VideoEncoderConfig as JsVideoEncoderConfig, VideoEncoderEncodeOptions, VideoEncoderInit,
VideoEncoderSupport, VideoFrame as JsVideoFrame, VideoFrameBufferInit, VideoPixelFormat,
};
pub struct WebEncodedVideoChunk {
pub data: Vec<u8>,
pub is_sync: bool,
pub decoder_config: Option<Vec<u8>>,
}
pub fn video_encode_capability(
codec: Codec,
profile: CodecProfile,
hardware: HardwarePreference,
) -> CodecSupport {
let supported = matches!(
(codec, profile),
(Codec::Av1, CodecProfile::Av1Main)
| (Codec::Hevc, CodecProfile::HevcMain)
| (Codec::Vp8, CodecProfile::Vp8)
| (Codec::Vp9, CodecProfile::Vp9Profile0)
);
if !supported {
return CodecSupport::UnsupportedProfile;
}
if hardware == HardwarePreference::Require {
return CodecSupport::HardwareUnavailable;
}
let global = js_sys::global();
let has_constructor =
js_sys::Reflect::has(&global, &JsValue::from_str("VideoEncoder")).unwrap_or(false);
if !has_constructor {
return CodecSupport::UnsupportedCodec;
}
CodecSupport::Supported {
implementation: CodecImplementation::Software,
}
}
pub struct WebVideoEncodeSession {
encoder: JsVideoEncoder,
codec: Codec,
vp9_level: u8,
width: u32,
height: u32,
pending_chunks: Rc<RefCell<VecDeque<(EncodedVideoChunk, EncodedVideoChunkMetadata)>>>,
encode_error: Rc<RefCell<Option<String>>>,
waker: Rc<RefCell<Option<js_sys::Function>>>,
emitted_config: bool,
finished: bool,
_output_closure: Closure<dyn FnMut(EncodedVideoChunk, EncodedVideoChunkMetadata)>,
_error_closure: Closure<dyn FnMut(JsValue)>,
}
impl WebVideoEncodeSession {
pub async fn open(
codec: Codec,
width: u32,
height: u32,
timescale: u32,
frame_duration: u32,
bitrate_bits_per_second: Option<u32>,
) -> Result<Self> {
let vp9_level = zvidlib_vp9_syntax::pick_level(
crate::VideoDimensions { width, height },
timescale,
frame_duration,
)
.unwrap_or(62);
let vp9_codec_string = format!("vp09.00.{vp9_level:02}.08");
let initial_codec_string = match codec {
Codec::Av1 => "av01.0.00M.08",
Codec::Hevc => "hev1.1.6.L93.B0",
Codec::Vp8 => "vp8",
Codec::Vp9 => vp9_codec_string.as_str(),
_ => {
return Err(Error::new(
ErrorKind::Unsupported,
"the WebCodecs video encoder bridge only supports AV1, HEVC, VP8 and VP9",
));
}
};
let config = JsVideoEncoderConfig::new(initial_codec_string, height, width);
if let Some(bitrate) = bitrate_bits_per_second {
config.set_bitrate(bitrate);
}
let support: VideoEncoderSupport =
JsFuture::from(js_to_promise(JsVideoEncoder::is_config_supported(&config)))
.await
.map_err(|error| normalize_js_error(error, "querying WebCodecs encoder support"))?
.unchecked_into();
if !support.get_supported().unwrap_or(false) {
return Err(Error::new(
ErrorKind::Unsupported,
format!("this browser cannot encode {initial_codec_string} via WebCodecs"),
));
}
let pending_chunks: Rc<RefCell<VecDeque<(EncodedVideoChunk, EncodedVideoChunkMetadata)>>> =
Rc::new(RefCell::new(VecDeque::new()));
let encode_error: Rc<RefCell<Option<String>>> = Rc::new(RefCell::new(None));
let waker: Rc<RefCell<Option<js_sys::Function>>> = Rc::new(RefCell::new(None));
let output_chunks = Rc::clone(&pending_chunks);
let output_waker = Rc::clone(&waker);
let output_closure = Closure::new(
move |chunk: EncodedVideoChunk, metadata: EncodedVideoChunkMetadata| {
output_chunks.borrow_mut().push_back((chunk, metadata));
if let Some(resolve) = output_waker.borrow_mut().take() {
let _ = resolve.call0(&JsValue::NULL);
}
},
);
let error_state = Rc::clone(&encode_error);
let error_waker = Rc::clone(&waker);
let error_closure = Closure::new(move |error: JsValue| {
let message = js_sys::Reflect::get(&error, &JsValue::from_str("message"))
.ok()
.and_then(|value| value.as_string())
.unwrap_or_else(|| "WebCodecs encoder reported an error".to_owned());
*error_state.borrow_mut() = Some(message);
if let Some(resolve) = error_waker.borrow_mut().take() {
let _ = resolve.call0(&JsValue::NULL);
}
});
let init = VideoEncoderInit::new(
error_closure.as_ref().unchecked_ref(),
output_closure.as_ref().unchecked_ref(),
);
let encoder = JsVideoEncoder::new(&init)
.map_err(|error| normalize_js_error(error, "constructing a WebCodecs VideoEncoder"))?;
encoder
.configure(&config)
.map_err(|error| normalize_js_error(error, "configuring the WebCodecs VideoEncoder"))?;
Ok(Self {
encoder,
codec,
vp9_level,
width,
height,
pending_chunks,
encode_error,
waker,
emitted_config: false,
finished: false,
_output_closure: output_closure,
_error_closure: error_closure,
})
}
pub async fn encode(
&mut self,
frame: &VideoFrame,
timestamp_micros: u64,
duration_micros: u32,
key_frame: bool,
) -> Result<WebEncodedVideoChunk> {
if self.finished {
return Err(Error::new(
ErrorKind::InvalidState,
"the WebCodecs video encoder session has already finished",
));
}
if frame.dimensions.width != self.width || frame.dimensions.height != self.height {
return Err(Error::new(
ErrorKind::InvalidInput,
"video frame dimensions do not match the configured width/height",
));
}
let format = web_pixel_format(frame.pixel_format)?;
let (bytes, layout) = pack_planes_tightly(frame)?;
let data = js_sys::Uint8Array::from(bytes.as_slice());
let init = VideoFrameBufferInit::new_with_f64(
self.height,
self.width,
format,
timestamp_micros as f64,
);
init.set_duration_f64(f64::from(duration_micros));
if let Some(layout) = layout.as_deref() {
init.set_layout(layout);
}
let js_frame =
JsVideoFrame::new_with_buffer_source_and_video_frame_buffer_init(&data, &init)
.map_err(|error| normalize_js_error(error, "constructing a VideoFrame"))?;
let options = VideoEncoderEncodeOptions::new();
options.set_key_frame(key_frame);
let result = self.encoder.encode_with_options(&js_frame, &options);
js_frame.close();
result.map_err(|error| normalize_js_error(error, "encoding a video frame"))?;
self.wait_for_chunk().await
}
pub async fn finish(&mut self) -> Result<Vec<WebEncodedVideoChunk>> {
if self.finished {
return Ok(Vec::new());
}
self.finished = true;
JsFuture::from(js_to_promise(self.encoder.flush()))
.await
.map_err(|error| normalize_js_error(error, "flushing the WebCodecs VideoEncoder"))?;
let mut chunks = Vec::new();
while let Some(chunk) = self.take_ready_chunk()? {
chunks.push(chunk);
}
let _ = self.encoder.close();
Ok(chunks)
}
async fn wait_for_chunk(&mut self) -> Result<WebEncodedVideoChunk> {
loop {
if let Some(message) = self.encode_error.borrow_mut().take() {
return Err(Error::new(ErrorKind::Codec, message));
}
if let Some(chunk) = self.take_ready_chunk()? {
return Ok(chunk);
}
self.wait_for_output().await;
}
}
fn take_ready_chunk(&mut self) -> Result<Option<WebEncodedVideoChunk>> {
let Some((chunk, metadata)) = self.pending_chunks.borrow_mut().pop_front() else {
return Ok(None);
};
let destination = js_sys::Uint8Array::new_with_length(chunk.byte_length());
chunk
.copy_to_with_u8_array(&destination)
.map_err(|error| normalize_js_error(error, "copying an encoded video chunk"))?;
let data = destination.to_vec();
let is_sync = chunk.type_() == web_sys::EncodedVideoChunkType::Key;
let decoder_config = if !self.emitted_config && is_sync {
let config = match self.codec {
Codec::Av1 => av1c_from_bitstream(&data),
Codec::Hevc => hvcc_from_metadata(&metadata),
Codec::Vp8 => Some(Vec::new()),
Codec::Vp9 => zvidlib_vp9_syntax::vpcc_from_key_frame(&data, self.vp9_level),
_ => None,
};
config.inspect(|_| {
self.emitted_config = true;
})
} else {
None
};
Ok(Some(WebEncodedVideoChunk {
data,
is_sync,
decoder_config,
}))
}
async fn wait_for_output(&self) {
let waker = Rc::clone(&self.waker);
let promise = js_sys::Promise::new(&mut |resolve, _reject| {
schedule_event_loop_tick(&resolve);
*waker.borrow_mut() = Some(resolve);
});
let _ = JsFuture::from(promise).await;
}
}
impl Drop for WebVideoEncodeSession {
fn drop(&mut self) {
if !self.finished {
let _ = self.encoder.close();
}
}
}
fn web_pixel_format(pixel_format: PixelFormat) -> Result<VideoPixelFormat> {
match pixel_format {
PixelFormat::Rgba8 => Ok(VideoPixelFormat::Rgba),
PixelFormat::Bgra8 => Ok(VideoPixelFormat::Bgra),
PixelFormat::Yuv420p8 => Ok(VideoPixelFormat::I420),
_ => Err(Error::new(
ErrorKind::Unsupported,
"this pixel format is not supported by the WebCodecs export bridge",
)),
}
}
fn pack_planes_tightly(frame: &VideoFrame) -> Result<(Vec<u8>, Option<Vec<PlaneLayout>>)> {
let layouts = required_plane_layouts(frame.dimensions, frame.pixel_format)?;
let mut bytes = Vec::new();
let mut plane_layout = Vec::with_capacity(layouts.len());
for (plane, (row_bytes, rows)) in frame.planes.iter().zip(&layouts) {
let offset = u32::try_from(bytes.len())
.map_err(|_| Error::new(ErrorKind::ResourceLimit, "video plane offset overflow"))?;
for row in 0..*rows {
let start = row
.checked_mul(plane.stride)
.ok_or_else(|| Error::new(ErrorKind::ResourceLimit, "video plane row overflow"))?;
let end = start
.checked_add(*row_bytes)
.ok_or_else(|| Error::new(ErrorKind::ResourceLimit, "video plane row overflow"))?;
let row_data = plane.data.get(start..end).ok_or_else(|| {
Error::new(
ErrorKind::InvalidInput,
"a video plane is shorter than its stride and height",
)
})?;
bytes.extend_from_slice(row_data);
}
let stride = u32::try_from(*row_bytes)
.map_err(|_| Error::new(ErrorKind::ResourceLimit, "video row size overflow"))?;
plane_layout.push(PlaneLayout::new(offset, stride));
}
let layout = (layouts.len() > 1).then_some(plane_layout);
Ok((bytes, layout))
}
pub fn sample_dependency(is_sync: bool) -> SampleDependency {
if is_sync {
SampleDependency::INDEPENDENT
} else {
SampleDependency::DEPENDENT
}
}
pub struct WebEncodedAudioChunk {
pub data: Vec<u8>,
pub is_sync: bool,
pub timestamp_micros: f64,
pub duration_micros: Option<f64>,
pub decoder_config: Option<Vec<u8>>,
}
pub fn audio_encode_capability(
codec: Codec,
profile: CodecProfile,
hardware: HardwarePreference,
) -> CodecSupport {
match (codec, profile) {
(Codec::Aac, CodecProfile::AacLowComplexity)
| (Codec::Opus, CodecProfile::Opus)
| (Codec::Vorbis, CodecProfile::Vorbis) => {}
_ => return CodecSupport::UnsupportedProfile,
}
if hardware == HardwarePreference::Require {
return CodecSupport::HardwareUnavailable;
}
if codec != Codec::Aac {
return CodecSupport::Supported {
implementation: CodecImplementation::Software,
};
}
let global = js_sys::global();
let has_constructor =
js_sys::Reflect::has(&global, &JsValue::from_str("AudioEncoder")).unwrap_or(false);
if !has_constructor {
return CodecSupport::UnsupportedCodec;
}
CodecSupport::Supported {
implementation: CodecImplementation::Software,
}
}
pub struct WebAudioEncodeSession {
codec: Codec,
encoder: JsAudioEncoder,
sample_rate: u32,
channels: u16,
pending_chunks: Rc<RefCell<VecDeque<(EncodedAudioChunk, EncodedAudioChunkMetadata)>>>,
encode_error: Rc<RefCell<Option<String>>>,
emitted_config: bool,
finished: bool,
_output_closure: Closure<dyn FnMut(EncodedAudioChunk, EncodedAudioChunkMetadata)>,
_error_closure: Closure<dyn FnMut(JsValue)>,
}
impl WebAudioEncodeSession {
pub fn open(
codec: Codec,
sample_rate: u32,
channels: u16,
bitrate_bits_per_second: Option<u32>,
) -> Result<Self> {
let pending_chunks: Rc<RefCell<VecDeque<(EncodedAudioChunk, EncodedAudioChunkMetadata)>>> =
Rc::new(RefCell::new(VecDeque::new()));
let encode_error: Rc<RefCell<Option<String>>> = Rc::new(RefCell::new(None));
let output_chunks = Rc::clone(&pending_chunks);
let output_closure = Closure::new(
move |chunk: EncodedAudioChunk, metadata: EncodedAudioChunkMetadata| {
output_chunks.borrow_mut().push_back((chunk, metadata));
},
);
let error_state = Rc::clone(&encode_error);
let error_closure = Closure::new(move |error: JsValue| {
let message = js_sys::Reflect::get(&error, &JsValue::from_str("message"))
.ok()
.and_then(|value| value.as_string())
.unwrap_or_else(|| "WebCodecs encoder reported an error".to_owned());
*error_state.borrow_mut() = Some(message);
});
let init = AudioEncoderInit::new(
error_closure.as_ref().unchecked_ref(),
output_closure.as_ref().unchecked_ref(),
);
let encoder = JsAudioEncoder::new(&init)
.map_err(|error| normalize_js_error(error, "constructing a WebCodecs AudioEncoder"))?;
let codec_string = match codec {
Codec::Aac => "mp4a.40.2",
Codec::Opus => "opus",
_ => {
return Err(Error::new(
ErrorKind::Unsupported,
"the WebCodecs audio encoder bridge supports AAC-LC and Opus",
));
}
};
let config = JsAudioEncoderConfig::new(codec_string, u32::from(channels), sample_rate);
if let Some(bitrate) = bitrate_bits_per_second {
config.set_bitrate(bitrate);
}
encoder
.configure(&config)
.map_err(|error| normalize_js_error(error, "configuring the WebCodecs AudioEncoder"))?;
Ok(Self {
codec,
encoder,
sample_rate,
channels,
pending_chunks,
encode_error,
emitted_config: false,
finished: false,
_output_closure: output_closure,
_error_closure: error_closure,
})
}
pub async fn encode(
&mut self,
samples: &[f32],
timestamp_micros: f64,
) -> Result<Vec<WebEncodedAudioChunk>> {
if self.finished {
return Err(Error::new(
ErrorKind::InvalidState,
"the WebCodecs audio encoder session has already finished",
));
}
let channels = usize::from(self.channels);
if channels == 0 || samples.len() % channels != 0 {
return Err(Error::new(
ErrorKind::InvalidInput,
"interleaved audio sample count is not a multiple of the channel count",
));
}
let number_of_frames = u32::try_from(samples.len() / channels).map_err(|_| {
Error::new(
ErrorKind::ResourceLimit,
"audio buffer frame count overflow",
)
})?;
let bytes: &[u8] = bytemuck_cast_f32_slice(samples);
let data_array = js_sys::Uint8Array::from(bytes);
let init = AudioDataInit::new_with_u8_array(
&data_array,
AudioSampleFormat::F32,
u32::from(self.channels),
number_of_frames,
self.sample_rate as f32,
0,
);
init.set_timestamp_f64(timestamp_micros);
let audio_data = JsAudioData::new(&init)
.map_err(|error| normalize_js_error(error, "constructing an AudioData"))?;
let result = self.encoder.encode(&audio_data);
audio_data.close();
result.map_err(|error| normalize_js_error(error, "encoding an audio buffer"))?;
if let Some(message) = self.encode_error.borrow_mut().take() {
return Err(Error::new(ErrorKind::Codec, message));
}
self.take_ready_chunks()
}
pub async fn finish(&mut self) -> Result<Vec<WebEncodedAudioChunk>> {
if self.finished {
return Ok(Vec::new());
}
self.finished = true;
let flush_result = JsFuture::from(js_to_promise(self.encoder.flush())).await;
if let Some(message) = self.encode_error.borrow_mut().take() {
let _ = self.encoder.close();
return Err(Error::new(ErrorKind::Codec, message));
}
flush_result
.map_err(|error| normalize_js_error(error, "flushing the WebCodecs AudioEncoder"))?;
let chunks = self.take_ready_chunks()?;
let _ = self.encoder.close();
Ok(chunks)
}
fn take_ready_chunks(&mut self) -> Result<Vec<WebEncodedAudioChunk>> {
let mut chunks = Vec::new();
while let Some((chunk, metadata)) = self.pending_chunks.borrow_mut().pop_front() {
let destination = js_sys::Uint8Array::new_with_length(chunk.byte_length());
chunk
.copy_to_with_buffer_source(&destination)
.map_err(|error| normalize_js_error(error, "copying an encoded audio chunk"))?;
let data = destination.to_vec();
let is_sync = chunk.type_() == EncodedAudioChunkType::Key;
let decoder_config = if !self.emitted_config {
let description = metadata
.get_decoder_config()
.and_then(|config| config.get_description())
.map(|description| js_sys::Uint8Array::new(&description).to_vec());
match self.codec {
Codec::Opus => Some(opus_dops(description.as_deref(), self.channels)?),
_ => description.map(|bytes| esds_box(&bytes)),
}
.inspect(|_| {
self.emitted_config = true;
})
} else {
None
};
chunks.push(WebEncodedAudioChunk {
data,
is_sync,
timestamp_micros: chunk.timestamp(),
duration_micros: chunk.duration(),
decoder_config,
});
}
Ok(chunks)
}
}
impl Drop for WebAudioEncodeSession {
fn drop(&mut self) {
if !self.finished {
let _ = self.encoder.close();
}
}
}
fn opus_dops(description: Option<&[u8]>, channels: u16) -> Result<Vec<u8>> {
const LIBOPUS_AUDIO_PRE_SKIP: u16 = 312;
let head = match description {
Some(description) => crate::OpusHead::from_identification_header(description)?,
None => crate::OpusHead::new(
u8::try_from(channels)
.map_err(|_| Error::new(ErrorKind::Unsupported, "too many Opus channels"))?,
LIBOPUS_AUDIO_PRE_SKIP,
crate::OPUS_SAMPLE_RATE,
)?,
};
Ok(head.to_dops())
}
fn bytemuck_cast_f32_slice(samples: &[f32]) -> &[u8] {
unsafe {
std::slice::from_raw_parts(
samples.as_ptr().cast::<u8>(),
std::mem::size_of_val(samples),
)
}
}
fn write_descriptor_length(out: &mut Vec<u8>, length: u32) {
let mut chunks = [0_u8; 4];
let mut remaining = length;
let mut count = 0;
loop {
chunks[count] = (remaining & 0x7f) as u8;
remaining >>= 7;
count += 1;
if remaining == 0 || count == chunks.len() {
break;
}
}
for (position, index) in (0..count).rev().enumerate() {
let mut byte = chunks[index];
if position != count - 1 {
byte |= 0x80;
}
out.push(byte);
}
}
fn write_descriptor(out: &mut Vec<u8>, tag: u8, content: &[u8]) {
out.push(tag);
write_descriptor_length(out, u32::try_from(content.len()).unwrap_or(u32::MAX));
out.extend_from_slice(content);
}
fn esds_box(audio_specific_config: &[u8]) -> Vec<u8> {
let mut decoder_specific_info = Vec::new();
write_descriptor(&mut decoder_specific_info, 0x05, audio_specific_config);
let mut decoder_config_descriptor = vec![
0x40, 0x15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ];
decoder_config_descriptor.extend_from_slice(&decoder_specific_info);
let mut es_descriptor = vec![0, 0]; es_descriptor.push(0); write_descriptor(&mut es_descriptor, 0x04, &decoder_config_descriptor);
write_descriptor(&mut es_descriptor, 0x06, &[0x02]);
let mut payload = vec![0, 0, 0, 0]; write_descriptor(&mut payload, 0x03, &es_descriptor);
wrap_box(b"esds", &payload)
}
fn wrap_box(fourcc: &[u8; 4], payload: &[u8]) -> Vec<u8> {
let mut boxed = Vec::with_capacity(8 + payload.len());
let size = (8 + payload.len()) as u32;
boxed.extend_from_slice(&size.to_be_bytes());
boxed.extend_from_slice(fourcc);
boxed.extend_from_slice(payload);
boxed
}
fn av1c_from_bitstream(data: &[u8]) -> Option<Vec<u8>> {
let sequence_header_obu = extract_sequence_header_obu(data)?;
let mut parser = Av1Parser::new(Limits::default()).ok()?;
let parsed = parser.parse_low_overhead(sequence_header_obu).ok()?;
let sequence = parsed.iter().find_map(|obu| match obu {
Av1Obu::SequenceHeader { sequence, .. } => Some(sequence),
_ => None,
})?;
let operating_point = sequence.operating_points.first()?;
let color = &sequence.color_config;
let mut payload = Vec::with_capacity(4 + sequence_header_obu.len());
payload.push(0x81);
payload.push((sequence.seq_profile << 5) | (operating_point.level & 0x1f));
payload.push(
(u8::from(operating_point.tier) << 7)
| (u8::from(color.bit_depth > 8) << 6)
| (u8::from(color.bit_depth == 12) << 5)
| (u8::from(color.monochrome) << 4)
| (u8::from(color.subsampling_x) << 3)
| (u8::from(color.subsampling_y) << 2)
| (color.chroma_sample_position & 3),
);
payload.push(0); payload.extend_from_slice(sequence_header_obu);
Some(wrap_box(b"av1C", &payload))
}
fn hvcc_from_metadata(metadata: &EncodedVideoChunkMetadata) -> Option<Vec<u8>> {
let decoder_config = metadata.get_decoder_config()?;
let description = decoder_config.get_description()?;
let bytes = js_sys::Uint8Array::new(&description).to_vec();
Some(wrap_box(b"hvcC", &bytes))
}
fn extract_sequence_header_obu(bytes: &[u8]) -> Option<&[u8]> {
const SEQUENCE_HEADER_OBU_TYPE: u8 = 1;
let mut offset = 0;
while offset < bytes.len() {
let header_byte = *bytes.get(offset)?;
let obu_type = (header_byte >> 3) & 0b1111;
let has_extension = header_byte & 0b0000_0100 != 0;
let has_size_field = header_byte & 0b0000_0010 != 0;
if !has_size_field {
return None;
}
let size_offset = offset + 1 + usize::from(has_extension);
let (payload_len, leb_len) = read_leb128(bytes.get(size_offset..)?)?;
let payload_start = size_offset + leb_len;
let obu_len = (payload_start - offset).checked_add(usize::try_from(payload_len).ok()?)?;
let obu = bytes.get(offset..offset.checked_add(obu_len)?)?;
if obu_type == SEQUENCE_HEADER_OBU_TYPE {
return Some(obu);
}
offset = offset.checked_add(obu_len)?;
}
None
}
fn read_leb128(bytes: &[u8]) -> Option<(u64, usize)> {
let mut value: u64 = 0;
for (index, &byte) in bytes.iter().enumerate().take(8) {
value |= u64::from(byte & 0x7f) << (index * 7);
if byte & 0x80 == 0 {
return Some((value, index + 1));
}
}
None
}