web_codecs/video/
decoder.rs

1use bytes::{Bytes, BytesMut};
2use tokio::sync::{mpsc, watch};
3use wasm_bindgen::prelude::*;
4
5use super::{Dimensions, VideoColorSpaceConfig, VideoFrame};
6use crate::{EncodedFrame, Error};
7
8#[derive(Debug, Default, Clone)]
9pub struct VideoDecoderConfig {
10	/// The codec mimetype string.
11	pub codec: String,
12
13	/// The resolution of the media.
14	/// Neither width nor height can be zero.
15	pub resolution: Option<Dimensions>,
16
17	/// The resolution that the media should be displayed at.
18	/// Neither width nor height can be zero.
19	pub display: Option<Dimensions>,
20
21	/// Color stuff.
22	pub color_space: Option<VideoColorSpaceConfig>,
23
24	/// Some codec formats use a description to configure the decoder.
25	/// ex. For h264:
26	///   - If present: AVC format, with the SPS/PPS in this description.
27	///   - If absent: Annex-B format, with the SPS/PPS before each keyframe.
28	pub description: Option<Bytes>,
29
30	/// Optionally require or disable hardware acceleration.
31	pub hardware_acceleration: Option<bool>,
32
33	/// Optionally optimize for latency.
34	pub latency_optimized: Option<bool>,
35}
36
37impl VideoDecoderConfig {
38	pub fn new<T: Into<String>>(codec: T) -> Self {
39		Self {
40			codec: codec.into(),
41			..Default::default()
42		}
43	}
44
45	/// Check if the configuration is supported by this browser.
46	/// Returns an error if the configuration is invalid, and false if just unsupported.
47	pub async fn is_supported(&self) -> Result<bool, Error> {
48		let res =
49			wasm_bindgen_futures::JsFuture::from(web_sys::VideoDecoder::is_config_supported(&self.into())).await?;
50
51		let supported = js_sys::Reflect::get(&res, &JsValue::from_str("supported"))
52			.unwrap()
53			.as_bool()
54			.unwrap();
55
56		Ok(supported)
57	}
58
59	pub fn is_valid(&self) -> Result<(), Error> {
60		if self.resolution.map_or(true, |d| d.width == 0 || d.height == 0) {
61			return Err(Error::InvalidDimensions);
62		}
63
64		if self.display.map_or(true, |d| d.width == 0 || d.height == 0) {
65			return Err(Error::InvalidDimensions);
66		}
67
68		Ok(())
69	}
70
71	pub fn build(self) -> Result<(VideoDecoder, VideoDecoded), Error> {
72		let (frames_tx, frames_rx) = mpsc::unbounded_channel();
73		let (closed_tx, closed_rx) = watch::channel(Ok(()));
74		let closed_tx2 = closed_tx.clone();
75
76		let on_error = Closure::wrap(Box::new(move |e: JsValue| {
77			closed_tx.send_replace(Err(Error::from(e))).ok();
78		}) as Box<dyn FnMut(_)>);
79
80		let on_frame = Closure::wrap(Box::new(move |e: JsValue| {
81			let frame: web_sys::VideoFrame = e.unchecked_into();
82			let frame = VideoFrame::from(frame);
83
84			if frames_tx.send(frame).is_err() {
85				closed_tx2.send_replace(Err(Error::Dropped)).ok();
86			}
87		}) as Box<dyn FnMut(_)>);
88
89		let init = web_sys::VideoDecoderInit::new(on_error.as_ref().unchecked_ref(), on_frame.as_ref().unchecked_ref());
90		let inner: web_sys::VideoDecoder = web_sys::VideoDecoder::new(&init).unwrap();
91		inner.configure(&(&self).into())?;
92
93		let decoder = VideoDecoder {
94			inner,
95			on_error,
96			on_frame,
97		};
98
99		let decoded = VideoDecoded {
100			frames: frames_rx,
101			closed: closed_rx,
102		};
103
104		Ok((decoder, decoded))
105	}
106}
107
108impl From<&VideoDecoderConfig> for web_sys::VideoDecoderConfig {
109	fn from(this: &VideoDecoderConfig) -> Self {
110		let config = web_sys::VideoDecoderConfig::new(&this.codec);
111
112		if let Some(Dimensions { width, height }) = this.resolution {
113			config.set_coded_width(width);
114			config.set_coded_height(height);
115		}
116
117		if let Some(Dimensions { width, height }) = this.display {
118			config.set_display_aspect_width(width);
119			config.set_display_aspect_height(height);
120		}
121
122		if let Some(description) = &this.description {
123			config.set_description(&js_sys::Uint8Array::from(description.as_ref()));
124		}
125
126		if let Some(color_space) = &this.color_space {
127			config.set_color_space(&color_space.into());
128		}
129
130		if let Some(preferred) = this.hardware_acceleration {
131			config.set_hardware_acceleration(match preferred {
132				true => web_sys::HardwareAcceleration::PreferHardware,
133				false => web_sys::HardwareAcceleration::PreferSoftware,
134			});
135		}
136
137		if let Some(value) = this.latency_optimized {
138			config.set_optimize_for_latency(value);
139		}
140
141		config
142	}
143}
144
145impl From<web_sys::VideoDecoderConfig> for VideoDecoderConfig {
146	fn from(this: web_sys::VideoDecoderConfig) -> Self {
147		let resolution = match (this.get_coded_width(), this.get_coded_height()) {
148			(Some(width), Some(height)) if width != 0 && height != 0 => Some(Dimensions { width, height }),
149			_ => None,
150		};
151
152		let display = match (this.get_display_aspect_width(), this.get_display_aspect_height()) {
153			(Some(width), Some(height)) if width != 0 && height != 0 => Some(Dimensions { width, height }),
154			_ => None,
155		};
156
157		let color_space = this.get_color_space().map(VideoColorSpaceConfig::from);
158
159		let description = this.get_description().map(|d| {
160			// TODO: An ArrayBuffer, a TypedArray, or a DataView containing a sequence of codec-specific bytes, commonly known as "extradata".
161			let buffer = js_sys::Uint8Array::new(&d);
162			let size = buffer.byte_length() as usize;
163
164			let mut payload = BytesMut::with_capacity(size);
165			payload.resize(size, 0);
166			buffer.copy_to(&mut payload);
167
168			payload.freeze()
169		});
170
171		let hardware_acceleration = match this.get_hardware_acceleration() {
172			Some(web_sys::HardwareAcceleration::PreferHardware) => Some(true),
173			Some(web_sys::HardwareAcceleration::PreferSoftware) => Some(false),
174			_ => None,
175		};
176
177		let latency_optimized = this.get_optimize_for_latency();
178
179		Self {
180			codec: this.get_codec(),
181			resolution,
182			display,
183			color_space,
184			description,
185			hardware_acceleration,
186			latency_optimized,
187		}
188	}
189}
190
191pub struct VideoDecoder {
192	inner: web_sys::VideoDecoder,
193
194	// These are held to avoid dropping them.
195	#[allow(dead_code)]
196	on_error: Closure<dyn FnMut(JsValue)>,
197	#[allow(dead_code)]
198	on_frame: Closure<dyn FnMut(JsValue)>,
199}
200
201impl VideoDecoder {
202	pub fn decode(&self, frame: EncodedFrame) -> Result<(), Error> {
203		let chunk_type = match frame.keyframe {
204			true => web_sys::EncodedVideoChunkType::Key,
205			false => web_sys::EncodedVideoChunkType::Delta,
206		};
207
208		let chunk = web_sys::EncodedVideoChunkInit::new(
209			&js_sys::Uint8Array::from(frame.payload.as_ref()),
210			frame.timestamp.as_micros() as _,
211			chunk_type,
212		);
213
214		let chunk = web_sys::EncodedVideoChunk::new(&chunk)?;
215		self.inner.decode(&chunk)?;
216
217		Ok(())
218	}
219
220	pub async fn flush(&self) -> Result<(), Error> {
221		wasm_bindgen_futures::JsFuture::from(self.inner.flush()).await?;
222		Ok(())
223	}
224
225	pub fn queue_size(&self) -> u32 {
226		self.inner.decode_queue_size()
227	}
228}
229
230impl Drop for VideoDecoder {
231	fn drop(&mut self) {
232		let _ = self.inner.close();
233	}
234}
235
236pub struct VideoDecoded {
237	frames: mpsc::UnboundedReceiver<VideoFrame>,
238	closed: watch::Receiver<Result<(), Error>>,
239}
240
241impl VideoDecoded {
242	pub async fn next(&mut self) -> Result<Option<VideoFrame>, Error> {
243		tokio::select! {
244			biased;
245			frame = self.frames.recv() => Ok(frame),
246			Ok(()) = self.closed.changed() => Err(self.closed.borrow().clone().err().unwrap()),
247		}
248	}
249}