Skip to main content

bramley_engine/
runtime.rs

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}