1use std::collections::HashMap;
6use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
7
8use alpha2_extensions::{
9 Diagnostic, ExtensionError, FramePixels, Registry, ResourceResolver, SharedResolver,
10 VisualSource,
11};
12use alpha2_model::ProviderSource;
13use alpha2_time::MediaTime;
14use wasm_bindgen::prelude::*;
15
16fn js_error(error: impl std::fmt::Display) -> JsValue {
17 JsValue::from_str(&error.to_string())
18}
19
20#[derive(Default)]
23struct Files {
24 files: HashMap<String, Arc<[u8]>>,
25 fonts: Vec<Arc<[u8]>>,
26}
27
28#[derive(Clone, Default)]
31struct Resources(Arc<Mutex<Files>>);
32
33impl Resources {
34 fn lock(&self) -> MutexGuard<'_, Files> {
35 self.0.lock().unwrap_or_else(PoisonError::into_inner)
36 }
37}
38
39impl ResourceResolver for Resources {
40 fn bytes(&self, locator: &str) -> Result<Arc<[u8]>, ExtensionError> {
41 self.lock()
42 .files
43 .get(locator)
44 .cloned()
45 .ok_or_else(|| ExtensionError::MissingResource(locator.to_owned()))
46 }
47
48 fn fonts(&self) -> Vec<Arc<[u8]>> {
49 self.lock().fonts.clone()
50 }
51}
52
53#[wasm_bindgen]
57pub struct ExtensionRegistry {
58 registry: Registry,
59 resources: Resources,
60 sources: HashMap<u32, Box<dyn VisualSource>>,
61 next: u32,
62}
63
64#[wasm_bindgen]
66pub struct RenderedFrame {
67 width: u32,
68 height: u32,
69 pixels: FramePixels,
70 index: usize,
71 start: i64,
72 end: Option<i64>,
73 diagnostics: Vec<Diagnostic>,
74}
75
76#[wasm_bindgen]
77impl RenderedFrame {
78 #[wasm_bindgen(getter)]
80 #[must_use]
81 pub fn width(&self) -> u32 {
82 self.width
83 }
84
85 #[wasm_bindgen(getter)]
87 #[must_use]
88 pub fn height(&self) -> u32 {
89 self.height
90 }
91
92 #[wasm_bindgen(getter)]
95 #[must_use]
96 pub fn precision(&self) -> String {
97 match self.pixels {
98 FramePixels::LinearF16(_) => "linear_f16",
99 _ => "rgba8",
100 }
101 .to_owned()
102 }
103
104 #[wasm_bindgen(getter)]
108 #[must_use]
109 pub fn pixels(&self) -> js_sys::Uint8Array {
110 js_sys::Uint8Array::from(self.pixels.rgba8().unwrap_or_default())
111 }
112
113 #[wasm_bindgen(getter, js_name = halfPixels)]
117 #[must_use]
118 pub fn half_pixels(&self) -> js_sys::Uint16Array {
119 js_sys::Uint16Array::from(self.pixels.linear_f16().unwrap_or_default())
120 }
121
122 #[wasm_bindgen(getter)]
124 #[must_use]
125 pub fn diagnostics(&self) -> String {
126 serde_json::to_string(&self.diagnostics).unwrap_or_default()
127 }
128
129 #[wasm_bindgen(getter)]
131 #[must_use]
132 pub fn index(&self) -> usize {
133 self.index
134 }
135
136 #[wasm_bindgen(getter, js_name = startTicks)]
139 #[must_use]
140 pub fn start_ticks(&self) -> i64 {
141 self.start.max(0)
142 }
143
144 #[wasm_bindgen(getter, js_name = endTicks)]
147 #[must_use]
148 pub fn end_ticks(&self) -> Option<i64> {
149 self.end
150 }
151}
152
153impl Default for ExtensionRegistry {
154 fn default() -> Self {
155 Self::new()
156 }
157}
158
159#[wasm_bindgen]
160impl ExtensionRegistry {
161 #[wasm_bindgen(constructor)]
163 #[must_use]
164 pub fn new() -> Self {
165 Self {
166 registry: Registry::with_defaults(),
167 resources: Resources::default(),
168 sources: HashMap::new(),
169 next: 1,
170 }
171 }
172
173 #[must_use]
175 pub fn providers(&self) -> String {
176 serde_json::to_string(&self.registry.names()).unwrap_or_default()
177 }
178
179 #[must_use]
181 pub fn supports(&self, provider: &str) -> bool {
182 serde_json::from_str::<ProviderSource>(provider)
183 .is_ok_and(|source| self.registry.supports(&source))
184 }
185
186 pub fn probe(&self, bytes: &[u8]) -> Result<Option<String>, JsValue> {
192 match self.registry.probe(bytes) {
193 None => Ok(None),
194 Some(Err(error)) => Err(js_error(error)),
195 Some(Ok((provider, report))) => serde_json::to_string(&serde_json::json!({
196 "provider": provider,
197 "report": report,
198 }))
199 .map(Some)
200 .map_err(js_error),
201 }
202 }
203
204 #[wasm_bindgen(js_name = registerResource)]
208 pub fn register_resource(&mut self, locator: String, bytes: Vec<u8>) {
209 self.resources.lock().files.insert(locator, bytes.into());
210 }
211
212 #[wasm_bindgen(js_name = releaseResource)]
214 pub fn release_resource(&mut self, locator: &str) {
215 self.resources.lock().files.remove(locator);
216 }
217
218 #[wasm_bindgen(js_name = resourceBytes)]
220 #[must_use]
221 pub fn resource_bytes(&self) -> usize {
222 self.resources
223 .lock()
224 .files
225 .values()
226 .map(|file| file.len())
227 .sum()
228 }
229
230 #[wasm_bindgen(js_name = registerFont)]
233 pub fn register_font(&mut self, bytes: Vec<u8>) {
234 self.resources.lock().fonts.push(bytes.into());
235 }
236
237 #[wasm_bindgen(js_name = clearFonts)]
240 pub fn clear_fonts(&mut self) {
241 self.resources.lock().fonts.clear();
242 }
243
244 #[wasm_bindgen(js_name = openNeeds)]
250 pub fn open_needs(&self, provider: &str) -> Result<String, JsValue> {
251 let provider: ProviderSource = serde_json::from_str(provider).map_err(js_error)?;
252 serde_json::to_string(&self.registry.open_needs(&provider)).map_err(js_error)
253 }
254
255 pub fn needs(&self, handle: u32, time_ticks: i64) -> Result<String, JsValue> {
261 let source = self
262 .sources
263 .get(&handle)
264 .ok_or_else(|| js_error("the source is not open"))?;
265 serde_json::to_string(&source.needs(MediaTime::from_ticks(time_ticks))).map_err(js_error)
266 }
267
268 pub fn open(&mut self, provider: &str, bytes: Option<Vec<u8>>) -> Result<u32, JsValue> {
274 let provider: ProviderSource = serde_json::from_str(provider).map_err(js_error)?;
275 let source = self
276 .registry
277 .open(
278 &provider,
279 bytes.map(Arc::from),
280 Arc::new(self.resources.clone()) as SharedResolver,
281 )
282 .map_err(js_error)?;
283 let handle = self.next;
284 self.next = self.next.wrapping_add(1).max(1);
285 self.sources.insert(handle, source);
286 Ok(handle)
287 }
288
289 pub fn report(&self, handle: u32) -> Result<String, JsValue> {
294 let source = self
295 .sources
296 .get(&handle)
297 .ok_or_else(|| js_error("the source is not open"))?;
298 serde_json::to_string(source.report()).map_err(js_error)
299 }
300
301 pub fn render(
308 &mut self,
309 handle: u32,
310 time_ticks: i64,
311 fit_width: u32,
312 fit_height: u32,
313 ) -> Result<RenderedFrame, JsValue> {
314 let source = self
315 .sources
316 .get_mut(&handle)
317 .ok_or_else(|| js_error("the source is not open"))?;
318 let frame = source
319 .render(MediaTime::from_ticks(time_ticks), (fit_width, fit_height))
320 .map_err(js_error)?;
321 Ok(RenderedFrame {
322 width: frame.width,
323 height: frame.height,
324 index: frame.index,
325 start: frame.start.ticks(),
326 end: frame.end.map(MediaTime::ticks),
327 pixels: frame.pixels,
328 diagnostics: frame.diagnostics,
329 })
330 }
331
332 #[wasm_bindgen(js_name = residentBytes)]
339 pub fn resident_bytes(&self, handle: u32) -> Result<usize, JsValue> {
340 self.sources
341 .get(&handle)
342 .map(|source| source.resident_bytes())
343 .ok_or_else(|| js_error("the source is not open"))
344 }
345
346 pub fn reader(&mut self, handle: u32) -> Result<Option<u32>, JsValue> {
357 let source = self
358 .sources
359 .get(&handle)
360 .ok_or_else(|| js_error("the source is not open"))?;
361 let Some(reader) = source.reader() else {
362 return Ok(None);
363 };
364 let handle = self.next;
365 self.next = self.next.wrapping_add(1).max(1);
366 self.sources.insert(handle, reader);
367 Ok(Some(handle))
368 }
369
370 #[wasm_bindgen(js_name = sharedBytes)]
377 pub fn shared_bytes(&self, handle: u32) -> Result<usize, JsValue> {
378 self.sources
379 .get(&handle)
380 .map(|source| source.shared_bytes())
381 .ok_or_else(|| js_error("the source is not open"))
382 }
383
384 pub fn release(&mut self, handle: u32) {
386 self.sources.remove(&handle);
387 }
388}