1use std::cell::RefCell;
2use std::collections::{BTreeMap, HashMap, HashSet};
3use std::rc::Rc;
4use std::sync::mpsc::{Receiver, Sender, channel};
5use std::time::{Duration, Instant};
6
7use anyhow::{Result, anyhow};
8use rquickjs::{
9 ArrayBuffer, CatchResultExt, Context, Ctx, Function, Object, Runtime, Value,
10 context::EvalOptions, function::IntoArgs,
11};
12use serde_json::{Value as J, json};
13
14use crate::EngineConfig;
15use crate::audio::{AudioSink, PulseSink};
16use crate::buffer::{MediaSourceNative, SOURCE_BUFFER_QUOTA_BYTES, SourceBufferNative};
17use crate::cdm::{CdmBackend, CdmEvent, Widevine};
18use crate::eme::{audio_type_supported, can_play_type, eme_check};
19use crate::media::{MediaEngine, SinkFactory};
20use crate::mp4;
21use crate::net::{self, Completion, HttpRequest};
22
23const PLATFORM: &str = include_str!("../js/platform.js");
24
25pub type CdmFactory = Box<dyn Fn(bool) -> Result<Box<dyn CdmBackend>>>;
26
27struct Native {
28 start: Instant,
29 next_id: u32,
30 mses: HashMap<u32, MediaSourceNative>,
31 sbs: HashMap<u32, SourceBufferNative>,
32 medias: HashMap<u32, MediaEngine>,
33 cdms: HashMap<u32, Box<dyn CdmBackend>>,
34 cdm_factory: CdmFactory,
35 sink_factory: SinkFactory,
36 cdm_error: Option<String>,
37 net_tx: Sender<Completion>,
38 aborted: HashSet<u32>,
39 op_counts: BTreeMap<&'static str, u64>,
40 eme_requests: Vec<J>,
41 script_log: Vec<J>,
42 http_count: u64,
43 http_bytes: u64,
44 redact: Vec<String>,
45 truncate_scripts: bool,
46 retired_output: OutputCounts,
47 in_flight: usize,
48}
49
50impl Native {
51 fn id(&mut self) -> u32 {
52 self.next_id += 1;
53 self.next_id
54 }
55 fn count(&mut self, op: &'static str) {
56 *self.op_counts.entry(op).or_default() += 1;
57 }
58 fn position_for_buffer(&self, sb_id: u32) -> i64 {
59 self.medias
60 .values()
61 .find(|m| {
62 m.ms_id
63 .and_then(|id| self.mses.get(&id))
64 .is_some_and(|ms| ms.sbs.contains(&sb_id))
65 })
66 .map(|m| m.current_time_us())
67 .unwrap_or(0)
68 }
69}
70
71type N = Rc<RefCell<Native>>;
72
73pub struct Engine {
74 ctx: Context,
75 rt: Runtime,
76 n: N,
77 net_rx: Receiver<Completion>,
78 last_gc: Instant,
79}
80
81const GC_INTERVAL: Duration = Duration::from_secs(2);
82const ACTIVE_WAIT_MS: f64 = 5.0;
83const IDLE_WAIT: Duration = Duration::from_secs(60);
84
85fn opts(name: &str) -> EvalOptions {
86 let mut o = EvalOptions::default();
87 o.strict = false;
88 o.global = true;
89 o.filename = Some(name.into());
90 o
91}
92
93fn bytes_of(v: &Value) -> Option<Vec<u8>> {
94 let ab = ArrayBuffer::from_value(v.clone())?;
95 unsafe { ab.as_bytes() }.map(|b| b.to_vec())
96}
97
98fn media_state(m: &MediaEngine) -> String {
99 json!({"currentTime": m.current_time(), "readyState": m.ready_state, "paused": m.paused, "seeking": m.seeking, "ended": m.ended}).to_string()
100}
101
102fn install_ops<'js>(ctx: &Ctx<'js>, n: &N) -> rquickjs::Result<()> {
103 let g = ctx.globals();
104 macro_rules! op {
105 ($name:literal, $f:expr) => {
106 g.set($name, Function::new(ctx.clone(), $f)?)?;
107 };
108 }
109 macro_rules! with_media {
110 ($name:literal, $count:literal, |$m:ident $(, $arg:ident : $ty:ty)*| $body:expr) => {
111 let s = n.clone();
112 op!($name, move |id: u32 $(, $arg: $ty)*| {
113 let mut st = s.borrow_mut();
114 st.count($count);
115 if let Some($m) = st.medias.get_mut(&id) {
116 $body;
117 }
118 });
119 };
120 }
121 macro_rules! with_cdm {
122 ($name:literal, $count:literal, |$c:ident, $p:ident $(, $arg:ident : $ty:ty)*| $body:expr) => {
123 let s = n.clone();
124 op!($name, move |keys_id: u32, $p: u32 $(, $arg: $ty)*| {
125 let mut st = s.borrow_mut();
126 st.count($count);
127 if let Some($c) = st.cdms.get_mut(&keys_id) {
128 $body;
129 }
130 });
131 };
132 }
133 let s = n.clone();
134 op!("__op_now", move || -> f64 {
135 s.borrow().start.elapsed().as_secs_f64() * 1000.0
136 });
137 let s = n.clone();
138 op!("__op_print", move |level: String, msg: String| {
139 eprintln!("JS[{level}] {}", crate::redact(&msg, &s.borrow().redact));
140 });
141 op!("__op_user_agent", crate::user_agent);
142 op!("__op_random", move |ctx: Ctx<'js>,
143 len: u32|
144 -> rquickjs::Result<
145 ArrayBuffer<'js>,
146 > {
147 let mut buf = vec![0u8; len as usize];
148 if let Err(e) = getrandom::getrandom(&mut buf) {
149 return Err(rquickjs::Exception::throw_internal(
150 &ctx,
151 &format!("crypto.getRandomValues: no system randomness: {e}"),
152 ));
153 }
154 ArrayBuffer::new(ctx, buf)
155 });
156 let s = n.clone();
157 op!("__op_http", move |id: u32,
158 method: String,
159 url: String,
160 headers: String,
161 body: Value<'js>,
162 timeout: f64,
163 cache: String| {
164 let mut st = s.borrow_mut();
165 st.count("http");
166 st.http_count += 1;
167 st.in_flight += 1;
168 let headers: Vec<(String, String)> = serde_json::from_str(&headers).unwrap_or_default();
169 let req = HttpRequest {
170 id,
171 method,
172 url,
173 headers,
174 body: bytes_of(&body),
175 timeout_ms: if timeout > 0.0 {
176 timeout as u64
177 } else {
178 60_000
179 },
180 use_cache: cache == "default",
181 };
182 net::spawn(req, st.net_tx.clone());
183 });
184 let s = n.clone();
185 op!("__op_http_abort", move |id: u32| {
186 let mut st = s.borrow_mut();
187 st.count("http_abort");
188 st.aborted.insert(id);
189 });
190 let s = n.clone();
191 op!("__op_load_script", move |id: u32, url: String| {
192 let mut st = s.borrow_mut();
193 st.count("load_script");
194 st.in_flight += 1;
195 st.script_log
196 .push(json!({"id": id, "url": url, "phase": "fetching"}));
197 net::spawn_script(id, url, st.net_tx.clone());
198 });
199 let s = n.clone();
200 op!("__op_ms_create", move || -> u32 {
201 let mut st = s.borrow_mut();
202 st.count("ms_create");
203 let id = st.id();
204 st.mses.insert(
205 id,
206 MediaSourceNative {
207 duration: f64::NAN,
208 ended: false,
209 sbs: Vec::new(),
210 },
211 );
212 id
213 });
214 let s = n.clone();
215 op!("__op_sb_add", move |ms_id: u32, mime: String| -> i32 {
216 let mut st = s.borrow_mut();
217 st.count("sb_add");
218 if !audio_type_supported(&mime) {
219 return -1;
220 }
221 let id = st.id();
222 st.sbs
223 .insert(id, SourceBufferNative::new(mime, SOURCE_BUFFER_QUOTA_BYTES));
224 if let Some(ms) = st.mses.get_mut(&ms_id) {
225 ms.sbs.push(id);
226 }
227 id as i32
228 });
229 let s = n.clone();
230 op!("__op_sb_remove_buffer", move |ms_id: u32, sb_id: u32| {
231 let mut st = s.borrow_mut();
232 st.count("sb_remove_buffer");
233 st.sbs.remove(&sb_id);
234 if let Some(ms) = st.mses.get_mut(&ms_id) {
235 ms.sbs.retain(|x| *x != sb_id);
236 }
237 });
238 let s = n.clone();
239 op!("__op_sb_prepare_append", move |sb_id: u32,
240 len: u32|
241 -> bool {
242 let mut st = s.borrow_mut();
243 st.count("sb_prepare_append");
244 let position = st.position_for_buffer(sb_id);
245 st.sbs
246 .get_mut(&sb_id)
247 .is_some_and(|sb| sb.make_room(len as usize, position))
248 });
249 let s = n.clone();
250 op!("__op_sb_append", move |sb_id: u32,
251 data: Value<'js>,
252 ts_offset: f64|
253 -> String {
254 let mut st = s.borrow_mut();
255 st.count("sb_append");
256 let Some(bytes) = bytes_of(&data) else {
257 return json!({"error": "not an ArrayBuffer"}).to_string();
258 };
259 let Some(sb) = st.sbs.get_mut(&sb_id) else {
260 return json!({"error": "no such SourceBuffer"}).to_string();
261 };
262 match sb.append(&bytes, if ts_offset.is_finite() { ts_offset } else { 0.0 }) {
263 Ok(r) => json!({"init": r.init_segment, "frames": r.media_frames, "hasInitData": r.init_segment && sb.init_data.is_some(), "initDuration": sb.init.as_ref().and_then(|i| i.duration_secs), "end": sb.end_us().map(|e| e as f64 / 1e6)}).to_string(),
264 Err(e) => json!({"error": format!("{e:#}")}).to_string(),
265 }
266 });
267 let s = n.clone();
268 op!(
269 "__op_sb_set_append_window",
270 move |sb_id: u32, start: f64, end: f64| {
271 if let Some(sb) = s.borrow_mut().sbs.get_mut(&sb_id) {
272 let us = |t: f64| (t * 1e6).round() as i64;
273 sb.append_window_us = (us(start), us(end));
274 }
275 }
276 );
277 let s = n.clone();
278 op!("__op_sb_init_data", move |ctx: Ctx<'js>,
279 sb_id: u32|
280 -> rquickjs::Result<
281 Value<'js>,
282 > {
283 match s
284 .borrow()
285 .sbs
286 .get(&sb_id)
287 .and_then(|sb| sb.init_data.clone())
288 {
289 Some(b) => Ok(ArrayBuffer::new(ctx, b)?.into_value()),
290 None => Ok(Value::new_null(ctx)),
291 }
292 });
293 let s = n.clone();
294 op!("__op_sb_remove", move |sb_id: u32, start: f64, end: f64| {
295 let mut st = s.borrow_mut();
296 st.count("sb_remove");
297 if let Some(sb) = st.sbs.get_mut(&sb_id) {
298 sb.remove(start, end);
299 }
300 });
301 let s = n.clone();
302 op!("__op_sb_abort", move |sb_id: u32| {
303 if let Some(sb) = s.borrow_mut().sbs.get_mut(&sb_id) {
304 sb.abort();
305 }
306 });
307 let s = n.clone();
308 op!("__op_sb_change_type", move |sb_id: u32,
309 mime: String|
310 -> bool {
311 if !audio_type_supported(&mime) {
312 return false;
313 }
314 if let Some(sb) = s.borrow_mut().sbs.get_mut(&sb_id) {
315 sb.mime = mime;
316 }
317 true
318 });
319 let s = n.clone();
320 op!("__op_sb_buffered", move |sb_id: u32| -> String {
321 let r: Vec<[f64; 2]> = s
322 .borrow()
323 .sbs
324 .get(&sb_id)
325 .map(|sb| sb.buffered().into_iter().map(|(a, b)| [a, b]).collect())
326 .unwrap_or_default();
327 serde_json::to_string(&r).unwrap()
328 });
329 let s = n.clone();
330 op!("__op_ms_set_duration", move |ms_id: u32, d: f64| {
331 if let Some(ms) = s.borrow_mut().mses.get_mut(&ms_id) {
332 ms.duration = d;
333 }
334 });
335 let s = n.clone();
336 op!("__op_ms_end", move |ms_id: u32, ended: bool| {
337 if let Some(ms) = s.borrow_mut().mses.get_mut(&ms_id) {
338 ms.ended = ended;
339 }
340 });
341 let s = n.clone();
342 op!("__op_media_create", move || -> u32 {
343 let mut st = s.borrow_mut();
344 st.count("media_create");
345 let id = st.id();
346 let m = MediaEngine::new(st.sink_factory.clone());
347 st.medias.insert(id, m);
348 id
349 });
350 with_media!("__op_media_attach", "media_attach", |m, ms_id: u32| m
351 .attach(ms_id));
352 with_media!("__op_media_detach", "media_detach", |m| m.detach());
353 with_media!("__op_media_play", "media_play", |m| m.play());
354 with_media!("__op_media_pause", "media_pause", |m| m.pause());
355 with_media!("__op_media_seek", "media_seek", |m, t: f64| m
356 .seek((t * 1e6).round() as i64));
357 with_media!(
358 "__op_media_set_loop",
359 "media_set_loop",
360 |m, looping: bool| m.looping = looping
361 );
362 with_media!("__op_media_set_gain", "media_set_gain", |m, gain: f64| m
363 .set_gain(gain as f32));
364 with_media!(
365 "__op_media_set_keys",
366 "media_set_keys",
367 |m, keys_id: u32| m.set_keys(Some(keys_id).filter(|k| *k != 0))
368 );
369 let s = n.clone();
370 op!("__op_media_state", move |id: u32| -> String {
371 s.borrow()
372 .medias
373 .get(&id)
374 .map(media_state)
375 .unwrap_or_else(|| "{}".into())
376 });
377 let s = n.clone();
378 op!("__op_destroy", move |kind: String, id: u32| {
379 let mut st = s.borrow_mut();
380 st.count("destroy");
381 match kind.as_str() {
382 "ms" => {
383 for sb in st.mses.remove(&id).map(|ms| ms.sbs).unwrap_or_default() {
384 st.sbs.remove(&sb);
385 }
386 }
387 "media" => {
388 if let Some(mut m) = st.medias.remove(&id) {
389 m.collect_underflows();
390 let counts = output_counts(&m.counters);
391 st.retired_output = add(st.retired_output, counts);
392 }
393 }
394 "cdm" => drop(st.cdms.remove(&id)),
395 _ => {}
396 }
397 });
398 op!("__op_can_play_type", |mime: String| can_play_type(&mime));
399 op!("__op_type_supported", |mime: String| audio_type_supported(
400 &mime
401 ));
402 let s = n.clone();
403 op!("__op_eme_check", move |key_system: String,
404 configs: String|
405 -> String {
406 let mut st = s.borrow_mut();
407 st.count("eme_check");
408 let r = eme_check(&key_system, &configs);
409 st.eme_requests.push(json!({"keySystem": key_system, "requested": serde_json::from_str::<J>(&configs).unwrap_or(J::Null), "granted": r.clone()}));
410 r.map(|x| x.to_string()).unwrap_or_default()
411 });
412 let s = n.clone();
413 op!(
414 "__op_cdm_create",
415 move |allow_persistent_state: bool| -> i32 {
416 let mut st = s.borrow_mut();
417 st.count("cdm_create");
418 match (st.cdm_factory)(allow_persistent_state) {
419 Ok(c) => {
420 let id = st.id();
421 st.cdms.insert(id, c);
422 id as i32
423 }
424 Err(e) => {
425 eprintln!("ENGINE cdm create failed: {e:#}");
426 st.cdm_error = Some(format!("{e:#}"));
427 -1
428 }
429 }
430 }
431 );
432 with_cdm!(
433 "__op_cdm_set_cert",
434 "cdm_set_cert",
435 |c, promise, data: Value<'js>| c
436 .set_server_certificate(promise, &bytes_of(&data).unwrap_or_default())
437 );
438 with_cdm!(
439 "__op_cdm_generate",
440 "cdm_generate",
441 |c, promise, init_type: u32, data: Value<'js>| c.create_session(
442 promise,
443 init_type,
444 &bytes_of(&data).unwrap_or_default()
445 )
446 );
447 with_cdm!(
448 "__op_cdm_update",
449 "cdm_update",
450 |c, promise, sid: String, data: Value<'js>| c.update_session(
451 promise,
452 &sid,
453 &bytes_of(&data).unwrap_or_default()
454 )
455 );
456 with_cdm!("__op_cdm_close", "cdm_close", |c, promise, sid: String| c
457 .close_session(promise, &sid));
458 with_cdm!(
459 "__op_cdm_remove",
460 "cdm_remove",
461 |c, promise, sid: String| c.remove_session(promise, &sid)
462 );
463 Ok(())
464}
465
466fn call<'js, A: IntoArgs<'js>>(
467 ctx: &Ctx<'js>,
468 n: &N,
469 obj: &str,
470 name: &str,
471 args: A,
472) -> Option<Value<'js>> {
473 let r = (|| -> rquickjs::Result<Value<'js>> {
474 let p: Object = ctx.globals().get(obj)?;
475 let f: Function = p.get(name)?;
476 f.call::<_, Value>(args)
477 })();
478 match r.catch(ctx) {
479 Ok(v) => Some(v),
480 Err(e) => {
481 let msg = crate::redact(&e.to_string(), &n.borrow().redact);
482 eprintln!("ENGINE call {obj}.{name} failed: {msg}");
483 None
484 }
485 }
486}
487
488fn buffer_or_null<'js>(ctx: &Ctx<'js>, body: Option<Vec<u8>>) -> Value<'js> {
489 body.and_then(|b| ArrayBuffer::new(ctx.clone(), b).ok())
490 .map(|b| b.into_value())
491 .unwrap_or_else(|| Value::new_null(ctx.clone()))
492}
493
494fn sha256_hex(bytes: &[u8]) -> String {
495 use sha2::{Digest, Sha256};
496 mp4::hex(&Sha256::digest(bytes))
497}
498
499fn handle_completion(ctx: &Ctx, n: &N, c: Completion) {
500 n.borrow_mut().in_flight -= 1;
501 match c {
502 Completion::Http(r) => {
503 {
504 let mut st = n.borrow_mut();
505 if st.aborted.remove(&r.id) {
506 return;
507 }
508 st.http_bytes += r.body.len() as u64;
509 }
510 let meta = json!({"status": r.status, "statusText": r.status_text, "url": r.url, "headers": r.headers, "error": r.error}).to_string();
511 call(
512 ctx,
513 n,
514 "__platform",
515 "dispatchHttp",
516 (r.id, meta, buffer_or_null(ctx, Some(r.body))),
517 );
518 }
519 Completion::Script { id, url, result } => {
520 let mut entry = json!({"id": id, "url": url});
521 let outcome = result.and_then(|mut bytes| {
522 entry["fetchedBytes"] = json!(bytes.len());
523 entry["fetchedSha256"] = json!(sha256_hex(&bytes));
524 if n.borrow().truncate_scripts {
525 bytes.truncate(bytes.len() / 2);
526 }
527 entry["evaluatedBytes"] = json!(bytes.len());
528 entry["evaluatedSha256"] = json!(sha256_hex(&bytes));
529 let t = Instant::now();
530 ctx.eval_with_options::<(), _>(bytes, opts(&url))
531 .catch(ctx)
532 .map(|_| t.elapsed())
533 .map_err(|e| e.to_string())
534 });
535 let (ok, err) = match outcome {
536 Ok(took) => {
537 entry["phase"] = json!("evaluated");
538 entry["evalMs"] = json!(took.as_secs_f64() * 1000.0);
539 (true, String::new())
540 }
541 Err(e) => {
542 entry["phase"] = json!("error");
543 entry["error"] = json!(e);
544 (false, e)
545 }
546 };
547 n.borrow_mut().script_log.push(entry);
548 call(ctx, n, "__platform", "dispatchScript", (id, ok, err));
549 }
550 }
551}
552
553fn dispatch_cdm(ctx: &Ctx, n: &N, keys_id: u32, e: CdmEvent) {
554 let (meta, body) = match e {
555 CdmEvent::Initialized(ok) => (json!({"type": "initialized", "ok": ok}), None),
556 CdmEvent::NewSession { promise, sid } => (
557 json!({"type": "newSession", "promise": promise, "sid": sid}),
558 None,
559 ),
560 CdmEvent::Resolve(p) => (json!({"type": "resolve", "promise": p}), None),
561 CdmEvent::Reject {
562 promise,
563 exception,
564 system_code,
565 message,
566 } => (
567 json!({"type": "reject", "promise": promise, "exception": exception, "code": system_code, "message": message}),
568 None,
569 ),
570 CdmEvent::Message { sid, kind, body } => (
571 json!({"type": "message", "sid": sid, "kind": kind}),
572 Some(body),
573 ),
574 CdmEvent::Keys { sid, keys } => (
575 json!({"type": "keys", "sid": sid, "keys": keys.iter().map(|(k, s)| json!({"kid": mp4::hex(k), "status": s})).collect::<Vec<_>>()}),
576 None,
577 ),
578 CdmEvent::Expiration { sid, expiry } => (
579 json!({"type": "expiration", "sid": sid, "expiry": expiry}),
580 None,
581 ),
582 CdmEvent::Closed { sid } => (json!({"type": "closed", "sid": sid}), None),
583 };
584 call(
585 ctx,
586 n,
587 "__platform",
588 "dispatchCdm",
589 (keys_id, meta.to_string(), buffer_or_null(ctx, body)),
590 );
591}
592
593impl Engine {
594 pub fn new(config: EngineConfig) -> Result<Engine> {
595 let EngineConfig {
596 cdm_path,
597 audio_sink_name,
598 output_device,
599 redact,
600 } = config;
601 let cdm: CdmFactory = Box::new(move |persistent| Widevine::create(&cdm_path, persistent));
602 let sink: SinkFactory = Rc::new(move || {
603 Box::new(PulseSink::start(
604 audio_sink_name.clone(),
605 output_device.clone(),
606 )) as Box<dyn AudioSink>
607 });
608 crate::net::clear_cache();
609 let engine = Engine::with_backends(cdm, sink)?;
610 {
611 let mut st = engine.n.borrow_mut();
612 st.redact = redact;
613 }
614 Ok(engine)
615 }
616
617 pub(crate) fn with_backends(
618 cdm_factory: CdmFactory,
619 sink_factory: SinkFactory,
620 ) -> Result<Engine> {
621 let (net_tx, net_rx) = channel();
622 let n: N = Rc::new(RefCell::new(Native {
623 start: Instant::now(),
624 next_id: 0,
625 mses: HashMap::new(),
626 sbs: HashMap::new(),
627 medias: HashMap::new(),
628 cdms: HashMap::new(),
629 cdm_factory,
630 sink_factory,
631 cdm_error: None,
632 net_tx,
633 aborted: HashSet::new(),
634 op_counts: BTreeMap::new(),
635 eme_requests: Vec::new(),
636 script_log: Vec::new(),
637 http_count: 0,
638 http_bytes: 0,
639 redact: Vec::new(),
640 truncate_scripts: false,
641 retired_output: [0; OUTPUT_COUNTS],
642 in_flight: 0,
643 }));
644 let rt = Runtime::new()?;
645 rt.set_max_stack_size(4 * 1024 * 1024);
646 let ctx = Context::full(&rt)?;
647 ctx.with(|ctx| install_ops(&ctx, &n))
648 .map_err(|e| anyhow!("install ops: {e}"))?;
649 let engine = Engine {
650 ctx,
651 rt,
652 n,
653 net_rx,
654 last_gc: Instant::now(),
655 };
656 engine.eval_script("platform.js", PLATFORM)?;
657 #[cfg(feature = "trace")]
658 engine.eval_script("trace.js", include_str!("../js/trace.js"))?;
659 Ok(engine)
660 }
661
662 fn drain(&self) {
663 for _ in 0..100_000 {
664 match self.rt.execute_pending_job() {
665 Ok(true) => continue,
666 Ok(false) => break,
667 Err(e) => eprintln!("ENGINE job error: {e:?}"),
668 }
669 }
670 }
671
672 #[cfg(any(test, feature = "sabotage"))]
673 pub fn sabotage_truncate_scripts(&self) {
674 self.n.borrow_mut().truncate_scripts = true;
675 }
676
677 #[cfg(any(test, feature = "diagnostics"))]
678 pub fn enable_diagnostics(&self) -> Result<()> {
679 self.eval_script("diagnostics.js", include_str!("../js/diagnostics.js"))
680 }
681
682 pub fn eval_script(&self, name: &str, source: &str) -> Result<()> {
683 let r = self.ctx.with(|ctx| {
684 ctx.eval_with_options::<(), _>(source, opts(name))
685 .catch(&ctx)
686 .map_err(|e| anyhow!("{name} failed: {e}"))
687 });
688 self.drain();
689 r
690 }
691
692 pub fn eval_json(&self, expression: &str) -> Result<J> {
693 let src = format!("JSON.stringify(({expression}))");
694 let r = self.ctx.with(|ctx| {
695 ctx.eval_with_options::<Option<String>, _>(src, opts("eval"))
696 .catch(&ctx)
697 .map_err(|e| anyhow!("eval failed: {e}"))
698 });
699 self.drain();
700 Ok(r?
701 .map(|s| serde_json::from_str(&s))
702 .transpose()?
703 .unwrap_or(J::Null))
704 }
705
706 pub(crate) fn global_number(&self, object: &str, key: &str) -> Option<f64> {
707 self.ctx
708 .with(|ctx| ctx.globals().get::<_, Object>(object)?.get::<_, f64>(key))
709 .ok()
710 }
711
712 pub fn run_gc(&mut self) {
713 self.rt.run_gc();
714 self.last_gc = Instant::now();
715 self.drain();
716 }
717
718 fn run_tasks(&self) -> (bool, Option<f64>) {
719 for ran in 0..500 {
720 let r: f64 = self.ctx.with(|ctx| {
721 call(&ctx, &self.n, "__platform", "runTimers", ())
722 .and_then(|v| v.as_number())
723 .unwrap_or(-1.0)
724 });
725 self.drain();
726 if r != -2.0 {
727 return (ran > 0, (r >= 0.0).then_some(r));
728 }
729 }
730 (true, None)
731 }
732
733 pub fn tick(&mut self) -> Duration {
734 if self.last_gc.elapsed() >= GC_INTERVAL {
735 self.run_gc();
736 }
737 self.drain();
738 let mut busy = false;
739 while let Ok(c) = self.net_rx.try_recv() {
740 busy = true;
741 self.ctx.with(|ctx| handle_completion(&ctx, &self.n, c));
742 self.drain();
743 self.run_tasks();
744 }
745 let cdm_events: Vec<(u32, CdmEvent)> = self
746 .n
747 .borrow_mut()
748 .cdms
749 .iter_mut()
750 .flat_map(|(id, c)| c.poll().into_iter().map(move |e| (*id, e)))
751 .collect();
752 busy |= !cdm_events.is_empty();
753 for (id, e) in cdm_events {
754 self.ctx.with(|ctx| dispatch_cdm(&ctx, &self.n, id, e));
755 self.drain();
756 }
757 let (ran, next_ms) = self.run_tasks();
758 busy |= ran;
759 let now = Instant::now();
760 let media_events: Vec<(u32, String)> = {
761 let mut guard = self.n.borrow_mut();
762 let st = &mut *guard;
763 let mut out = Vec::new();
764 for (id, m) in st.medias.iter_mut() {
765 m.pump(now, &st.sbs, &st.mses, &mut st.cdms);
766 if !m.events.is_empty() {
767 let events = std::mem::take(&mut m.events);
768 out.push((
769 *id,
770 format!(
771 "{{\"events\":{},\"state\":{}}}",
772 json!(events),
773 media_state(m)
774 ),
775 ));
776 }
777 }
778 out
779 };
780 busy |= !media_events.is_empty();
781 for (id, payload) in media_events {
782 self.ctx.with(|ctx| {
783 call(&ctx, &self.n, "__platform", "dispatchMedia", (id, payload));
784 });
785 self.drain();
786 self.run_tasks();
787 }
788 let st = self.n.borrow();
789 let active = busy
790 || st.in_flight > st.aborted.len()
791 || self.rt.is_job_pending()
792 || st.medias.values().any(|m| !m.idle());
793 if active {
794 let ms = next_ms.unwrap_or(ACTIVE_WAIT_MS).clamp(1.0, ACTIVE_WAIT_MS);
795 return Duration::from_micros((ms * 1000.0) as u64);
796 }
797 let js = next_ms.map(|ms| Duration::from_micros((ms.max(1.0) * 1000.0) as u64));
798 let cdm = st.cdms.values().filter_map(|c| c.next_timer()).min();
799 js.into_iter().chain(cdm).min().unwrap_or(IDLE_WAIT)
800 }
801
802 pub fn run_for(&mut self, d: Duration) {
803 let end = Instant::now() + d;
804 while Instant::now() < end {
805 let wait = self.tick();
806 std::thread::sleep(wait.min(end.saturating_duration_since(Instant::now())));
807 }
808 }
809
810 pub fn current_time(&self) -> Option<f64> {
811 let st = self.n.borrow();
812 st.medias
813 .iter()
814 .filter(|(_, m)| m.keys_id.is_some())
815 .min_by_key(|(id, _)| **id)
816 .map(|(_, m)| m.current_time())
817 }
818
819 pub fn output_failed(&self) -> bool {
820 self.n.borrow().medias.values().any(|m| m.output_failed())
821 }
822
823 pub fn output_counts(&self) -> OutputCounts {
824 let st = self.n.borrow();
825 let live = st.medias.values().map(|m| output_counts(&m.counters));
826 live.fold(st.retired_output, add)
827 }
828
829 pub fn status(&self) -> J {
830 let st = self.n.borrow();
831 let medias: serde_json::Map<String, J> = st
832 .medias
833 .iter()
834 .map(|(id, m)| {
835 let c = &m.counters;
836 let rate = if m.rate > 0 { m.rate as f64 } else { 48000.0 };
837 let v = json!({
838 "currentTime": m.current_time(), "maxCurrentTime": m.max_current_time_us as f64 / 1e6, "outputPosition": m.output_position(),
839 "readyState": m.ready_state, "paused": m.paused, "seeking": m.seeking, "ended": m.ended, "waitingForKey": m.waiting_for_key, "keysId": m.keys_id,
840 "rate": m.rate, "channels": m.channels, "clearSamples": c.clear_samples, "encryptedSamples": c.encrypted_samples, "decryptOk": c.decrypt_ok,
841 "decryptFail": c.decrypt_fail.iter().map(|(k, v)| (k.to_string(), json!(v))).collect::<serde_json::Map<_, _>>(),
842 "decodedClearSeconds": c.decoded_clear_frames as f64 / rate, "decodedDecryptedSeconds": c.decoded_decrypted_frames as f64 / rate,
843 "decodeErr": c.decode_err, "keyWaits": c.key_waits, "underruns": c.underruns, "seeks": c.seeks, "loops": c.loops,
844 "drainsAcked": c.drains_acked, "drainTimeouts": c.drain_timeouts, "sinkRestarts": c.sink_restarts,
845 });
846 (id.to_string(), v)
847 })
848 .collect();
849 let sbs: serde_json::Map<String, J> = st
850 .sbs
851 .iter()
852 .map(|(id, sb)| {
853 let init = sb.init.as_ref().map(|i| json!({"codec": i.codec, "timescale": i.timescale, "channels": i.channels, "sampleRate": i.sample_rate, "protected": i.protection.as_ref().map(|p| json!({"scheme": p.scheme, "kid": mp4::hex(&p.kid), "ivSize": p.iv_size})), "psshBytes": i.pssh.len()}));
854 (id.to_string(), json!({"mime": sb.mime, "appendedBytes": sb.appended_bytes, "bytes": sb.bytes, "evictedFrames": sb.evicted_frames, "frames": sb.frames.len(), "buffered": sb.buffered(), "init": init}))
855 })
856 .collect();
857 let platform = self
858 .ctx
859 .with(|ctx| {
860 call(&ctx, &self.n, "__platform", "statusJson", ())
861 .and_then(|v| v.as_string().and_then(|s| s.to_string().ok()))
862 })
863 .and_then(|s| serde_json::from_str(&s).ok())
864 .unwrap_or(J::Null);
865 json!({
866 "pid": std::process::id(),
867 "elapsedSec": st.start.elapsed().as_secs_f64(),
868 "opCounts": st.op_counts,
869 "live": {"medias": st.medias.len(), "mediaSources": st.mses.len(), "sourceBuffers": st.sbs.len(), "cdms": st.cdms.len()},
870 "medias": medias,
871 "sourceBuffers": sbs,
872 "cdm": st.cdm_error.as_ref().map(|e| json!({"error": e})).or_else(|| st.cdms.values().next().map(|c| c.describe())),
873 "emeRequests": st.eme_requests,
874 "scripts": st.script_log,
875 "http": {"count": st.http_count, "bytes": st.http_bytes},
876 "platform": platform,
877 })
878 }
879}
880
881pub const OUTPUT_COUNTS: usize = 8;
882pub type OutputCounts = [u64; OUTPUT_COUNTS];
883
884pub(crate) fn output_counts(c: &crate::media::Counters) -> OutputCounts {
885 let decrypt_fails = c.decrypt_fail.values().sum();
886 [
887 c.drains_acked,
888 c.drain_timeouts,
889 c.sent_us,
890 c.skipped_us,
891 c.decrypted_us,
892 c.underruns,
893 decrypt_fails,
894 c.output_underflows,
895 ]
896}
897
898fn add(a: OutputCounts, b: OutputCounts) -> OutputCounts {
899 std::array::from_fn(|i| a[i] + b[i])
900}