1use crate::engine::compose::{placement, Compositor};
28use crate::engine::gpu::LayerSet;
29use crate::engine::mixer::Mixer;
30use crate::engine::shapes::ShapeRasterizer;
31use crate::engine::text::TextRasterizer;
32use crate::media::{Backend, DecoderPool, Frame, SAMPLE_RATE};
33use crate::model::{Clip, ClipKind, Project, TrackKind};
34use std::collections::{HashMap, VecDeque};
35use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
36use std::sync::mpsc::{self, Receiver, RecvTimeoutError, Sender, SyncSender, TryRecvError};
37use std::sync::{Arc, Mutex, MutexGuard};
38use std::time::{Duration, Instant};
39
40const BLOCK: usize = 1024;
42const LEAD_SECS: f64 = 0.12;
43const CACHE_BYTES: usize = 512 << 20;
46const READ_AHEAD_SECS: f64 = 1.5;
48const TRAIL_SECS: f64 = 0.5;
51const STALL_BEHIND: f64 = 0.15;
54const FPOOL_KEEP: usize = 4;
56
57struct Cache<T> {
60 map: HashMap<i64, (u64, usize, Arc<T>)>,
62 bytes: usize,
63 tick: u64,
64 protect: (i64, i64),
66}
67
68impl<T> Cache<T> {
69 fn new() -> Self {
70 Cache { map: HashMap::new(), bytes: 0, tick: 0, protect: (i64::MAX, i64::MIN) }
71 }
72 fn set_protect(&mut self, lo: i64, hi: i64) {
74 self.protect = (lo, hi);
75 }
76 fn get(&mut self, idx: i64) -> Option<Arc<T>> {
77 self.tick += 1;
78 let (t, _, v) = self.map.get_mut(&idx)?;
79 *t = self.tick;
80 Some(v.clone())
81 }
82 fn contains(&self, idx: i64) -> bool {
83 self.map.contains_key(&idx)
84 }
85 fn insert(&mut self, idx: i64, bytes: usize, v: Arc<T>) -> Vec<Arc<T>> {
87 self.tick += 1;
88 let mut out = Vec::new();
89 if let Some((_, b, old)) = self.map.insert(idx, (self.tick, bytes, v)) {
90 self.bytes -= b;
91 out.push(old);
92 }
93 self.bytes += bytes;
94 while self.bytes > CACHE_BYTES {
95 let (lo, hi) = self.protect;
97 let pick = self
98 .map
99 .iter()
100 .filter(|(i, _)| !(lo..=hi).contains(i))
101 .min_by_key(|(_, (t, _, _))| *t)
102 .or_else(|| self.map.iter().min_by_key(|(_, (t, _, _))| *t));
103 let Some((&i, _)) = pick else { break };
104 let (_, b, old) = self.map.remove(&i).expect("key from iter");
105 self.bytes -= b;
106 out.push(old);
107 }
108 out
109 }
110 fn evict_range(&mut self, lo: i64, hi: i64) -> Vec<Arc<T>> {
112 let keys: Vec<i64> = self.map.keys().copied().filter(|i| (lo..=hi).contains(i)).collect();
113 let mut out = Vec::new();
114 for i in keys {
115 let (_, b, v) = self.map.remove(&i).expect("key from keys");
116 self.bytes -= b;
117 out.push(v);
118 }
119 out
120 }
121 fn drain(&mut self) -> Vec<Arc<T>> {
123 self.bytes = 0;
124 self.map.drain().map(|(_, (_, _, v))| v).collect()
125 }
126}
127
128struct Clock {
130 playing: bool,
131 base_t: f64,
132 base_at: Instant,
133 duration: f64,
134 canvas: (u32, u32),
136}
137
138impl Clock {
139 fn now(&mut self) -> f64 {
140 if !self.playing {
141 return self.base_t;
142 }
143 let t = self.base_t + self.base_at.elapsed().as_secs_f64();
144 if t < self.duration {
145 return t;
146 }
147 self.playing = false;
148 self.base_t = self.duration;
149 self.duration
150 }
151}
152
153struct Shared {
154 clock: Mutex<Clock>,
155 frame: Mutex<Option<Arc<Frame>>>,
157 layers: Mutex<Option<Arc<LayerSet>>>,
159 project: Mutex<Arc<Project>>,
160 cache_hits: AtomicU64,
162 buffering: AtomicBool,
164}
165
166fn lock<T>(m: &Mutex<T>) -> MutexGuard<'_, T> {
168 m.lock().unwrap_or_else(|e| e.into_inner())
169}
170
171#[derive(Clone)]
172enum Cmd {
173 SetProject,
174 Seek,
175 Play,
176 Pause,
177 Canvas,
178 Backend(Backend),
179 Gpu(bool),
181 Proxies(HashMap<String, String>),
183 ClearDecoders(SyncSender<()>),
184 RenderOnce(f64, u32, SyncSender<Arc<Frame>>),
186 LayersOnce(f64, u32, SyncSender<Arc<LayerSet>>),
188 Quit,
189}
190
191pub struct Player {
192 shared: Arc<Shared>,
193 render: Sender<Cmd>,
194 audio: Sender<Cmd>,
195}
196
197impl Player {
198 pub fn new(ctx: eframe::egui::Context, backend: Backend, text: Arc<Mutex<TextRasterizer>>) -> Self {
199 let shared = Arc::new(Shared {
200 clock: Mutex::new(Clock {
201 playing: false,
202 base_t: 0.0,
203 base_at: Instant::now(),
204 duration: 0.0,
205 canvas: (0, 0),
206 }),
207 frame: Mutex::new(None),
208 layers: Mutex::new(None),
209 project: Mutex::new(Arc::new(Project::new())),
210 cache_hits: AtomicU64::new(0),
211 buffering: AtomicBool::new(false),
212 });
213 let (render, rx) = mpsc::channel();
214 let s = shared.clone();
215 let _ =
216 std::thread::Builder::new().name("render".into()).spawn(move || render_thread(s, rx, ctx, backend, text));
217 let (audio, rx) = mpsc::channel();
218 let s = shared.clone();
219 let _ = std::thread::Builder::new().name("audio".into()).spawn(move || audio_thread(s, rx, backend));
220 Self { shared, render, audio }
221 }
222 fn both(&self, c: Cmd) {
223 let _ = self.audio.send(c.clone());
224 let _ = self.render.send(c);
225 }
226 pub fn set_project(&mut self, project: &Project) {
228 *lock(&self.shared.project) = Arc::new(project.clone());
229 lock(&self.shared.clock).duration = project.duration();
230 self.both(Cmd::SetProject);
231 }
232 pub fn set_backend(&mut self, b: Backend) {
233 self.both(Cmd::Backend(b));
234 }
235 pub fn set_gpu(&mut self, on: bool) {
238 let _ = self.render.send(Cmd::Gpu(on));
239 }
240 pub fn set_canvas(&mut self, w: u32, h: u32, max_width: u32) {
242 let (w, h) = if max_width > 0 && w > max_width {
243 (max_width, ((h as u64 * max_width as u64) / w as u64).max(1) as u32)
244 } else {
245 (w, h)
246 };
247 let mut c = lock(&self.shared.clock);
248 if c.canvas != (w, h) {
249 c.canvas = (w, h);
250 drop(c);
251 let _ = self.render.send(Cmd::Canvas);
252 }
253 }
254 pub fn play(&mut self) {
255 {
256 let mut c = lock(&self.shared.clock);
257 let t = c.now();
258 c.base_t = if t >= c.duration - 1e-6 { 0.0 } else { t };
259 c.base_at = Instant::now();
260 c.playing = true;
261 }
262 self.both(Cmd::Play);
263 }
264 pub fn pause(&mut self) {
265 {
266 let mut c = lock(&self.shared.clock);
267 c.base_t = c.now();
268 c.playing = false;
269 }
270 self.both(Cmd::Pause);
271 }
272 pub fn toggle(&mut self) {
273 if self.is_playing() {
274 self.pause()
275 } else {
276 self.play()
277 }
278 }
279 pub fn is_playing(&self) -> bool {
280 let mut c = lock(&self.shared.clock);
281 c.now();
282 c.playing
283 }
284 pub fn time(&self) -> f64 {
286 lock(&self.shared.clock).now()
287 }
288 pub fn seek(&mut self, t: f64) {
290 {
291 let mut c = lock(&self.shared.clock);
292 c.base_t = t.clamp(0.0, c.duration.max(0.0));
293 c.base_at = Instant::now();
294 }
295 self.both(Cmd::Seek);
296 }
297 pub fn take_frame(&mut self) -> Option<Arc<Frame>> {
299 lock(&self.shared.frame).take()
300 }
301 pub fn take_layers(&mut self) -> Option<Arc<LayerSet>> {
304 lock(&self.shared.layers).take()
305 }
306 pub fn is_buffering(&self) -> bool {
309 self.shared.buffering.load(Ordering::Relaxed)
310 }
311 pub fn cache_hits(&self) -> u64 {
313 self.shared.cache_hits.load(Ordering::Relaxed)
314 }
315 pub fn render_once(&self, t: f64, max_w: u32) -> Option<Arc<Frame>> {
319 let (tx, rx) = mpsc::sync_channel(1);
320 self.render.send(Cmd::RenderOnce(t, max_w, tx)).ok()?;
321 rx.recv_timeout(Duration::from_secs(3)).ok()
322 }
323 pub fn layers_once(&self, t: f64, max_w: u32) -> Option<Arc<LayerSet>> {
326 let (tx, rx) = mpsc::sync_channel(1);
327 self.render.send(Cmd::LayersOnce(t, max_w, tx)).ok()?;
328 rx.recv_timeout(Duration::from_secs(3)).ok()
329 }
330 pub fn set_proxies(&mut self, map: HashMap<String, String>) {
333 let _ = self.render.send(Cmd::Proxies(map));
334 }
335 pub fn release_files(&mut self) {
337 let (tx, rx) = mpsc::sync_channel(2);
338 self.both(Cmd::ClearDecoders(tx));
339 let deadline = Instant::now() + Duration::from_secs(2);
340 for _ in 0..2 {
341 if rx.recv_timeout(deadline.saturating_duration_since(Instant::now())).is_err() {
343 break;
344 }
345 }
346 }
347}
348
349impl Drop for Player {
350 fn drop(&mut self) {
351 self.both(Cmd::Quit);
352 }
353}
354
355fn guarded(pool: &mut DecoderPool, f: impl FnOnce(&mut DecoderPool)) -> bool {
359 if std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(pool))).is_ok() {
360 return true;
361 }
362 pool.clear();
363 false
364}
365
366fn render_thread(
367 shared: Arc<Shared>,
368 rx: Receiver<Cmd>,
369 ctx: eframe::egui::Context,
370 backend: Backend,
371 text: Arc<Mutex<TextRasterizer>>,
372) {
373 let mut pool = DecoderPool::new(backend);
374 let mut comp = Compositor::new();
375 let mut shapes = ShapeRasterizer::new();
376 let mut project = lock(&shared.project).clone();
377 let mut dirty = false;
378 let mut pending: Option<Cmd> = None;
379 let mut gpu = false;
381 let mut spare_layers: Vec<Frame> = Vec::new();
382 let mut fcache: Cache<Frame> = Cache::new();
385 let mut lcache: Cache<LayerSet> = Cache::new();
386 let mut fpool: Vec<Frame> = Vec::new();
387 let mut last_pub: i64 = -1;
388 let mut stall = false;
391 macro_rules! clear_caches {
393 () => {
394 for old in fcache.drain() {
395 reclaim(old, &mut fpool);
396 }
397 for old in lcache.drain() {
398 recycle(Some(old), &mut spare_layers);
399 }
400 last_pub = -1;
401 };
402 }
403 loop {
404 let playing = {
405 let mut c = lock(&shared.clock);
406 c.now();
407 c.playing
408 };
409 if pending.is_none() && !dirty && !playing && !stall {
410 let Ok(c) = rx.recv() else { return }; pending = Some(c);
412 }
413 loop {
415 let c = match pending.take() {
416 Some(c) => c,
417 None => match rx.try_recv() {
418 Ok(c) => c,
419 Err(TryRecvError::Empty) => break,
420 Err(TryRecvError::Disconnected) => return,
421 },
422 };
423 match c {
424 Cmd::SetProject => {
425 let new = lock(&shared.project).clone();
426 dirty = true;
427 match video_dirty_spans(&project, &new) {
429 Some(spans) => {
430 let fps = new.fps.max(1.0);
431 for (a, b) in spans {
432 let (lo, hi) = ((a * fps).floor() as i64 - 1, (b * fps).ceil() as i64 + 1);
433 for old in fcache.evict_range(lo, hi) {
434 reclaim(old, &mut fpool);
435 }
436 for old in lcache.evict_range(lo, hi) {
437 recycle(Some(old), &mut spare_layers);
438 }
439 }
440 last_pub = -1; }
442 None => {
443 clear_caches!();
444 }
445 }
446 project = new;
447 }
448 Cmd::Seek | Cmd::Play | Cmd::Pause => dirty = true,
449 Cmd::Canvas => {
450 dirty = true;
451 clear_caches!(); }
453 Cmd::Backend(b) => {
454 pool.set_backend(b);
455 dirty = true;
456 clear_caches!();
457 }
458 Cmd::Gpu(on) => {
459 if gpu != on {
460 gpu = on;
461 *lock(&shared.layers) = None;
462 dirty = true;
463 clear_caches!();
464 }
465 }
466 Cmd::Proxies(map) => {
467 pool.set_proxies(map);
468 dirty = true;
469 clear_caches!(); }
471 Cmd::ClearDecoders(ack) => {
472 pool.clear();
473 clear_caches!(); let _ = ack.send(());
475 }
476 Cmd::LayersOnce(t, max_w, reply) => {
477 let (pw, ph) = (project.width.max(1), project.height.max(1));
478 let w = pw.min(max_w.max(16));
479 let h = ((ph as u64 * w as u64) / pw as u64).max(1) as u32;
480 let mut set = LayerSet::default();
481 let text = &mut lock(&text);
482 guarded(&mut pool, |pool| {
483 set = decode_layers(&project, t, w, h, pool, &mut spare_layers, text, &mut shapes, &mut comp)
484 });
485 let _ = reply.send(Arc::new(set));
486 }
487 Cmd::RenderOnce(t, max_w, reply) => {
488 let (pw, ph) = (project.width.max(1), project.height.max(1));
489 let w = pw.min(max_w.max(16));
490 let h = ((ph as u64 * w as u64) / pw as u64).max(1) as u32;
491 let mut f = Frame::default();
492 guarded(&mut pool, |pool| comp.render(&project, t, w, h, pool, &mut lock(&text), &mut f));
493 let _ = reply.send(Arc::new(f));
494 }
495 Cmd::Quit => return,
496 }
497 }
498 let (playing, t, (w, h), duration) = {
499 let mut c = lock(&shared.clock);
500 let t = c.now();
501 (c.playing, t, c.canvas, c.duration)
502 };
503 if !playing && !dirty && !stall {
504 continue;
505 }
506 let force = std::mem::take(&mut dirty);
507 let fps = project.fps.max(1.0);
508 if w == 0 || h == 0 {
509 stall = false;
510 shared.buffering.store(false, Ordering::Relaxed);
511 if playing {
512 std::thread::sleep(Duration::from_secs_f64(1.0 / fps)); }
514 continue;
515 }
516 let idx = (t * fps + 1e-6).floor() as i64;
518 let frame_bytes = (w as usize * h as usize * 4).max(1);
521 let read_ahead =
522 ((fps * READ_AHEAD_SECS).ceil() as i64).clamp(8, (CACHE_BYTES / 2 / frame_bytes).max(8) as i64);
523 let last_idx = (((duration * fps).ceil() as i64) - 1).max(idx);
524 let trail = (fps * TRAIL_SECS).ceil() as i64;
526 fcache.set_protect(idx - trail, idx + read_ahead);
527 lcache.set_protect(idx - trail, idx + read_ahead);
528 macro_rules! cached {
529 ($i:expr) => {
530 if gpu {
531 lcache.contains($i)
532 } else {
533 fcache.contains($i)
534 }
535 };
536 }
537 if playing && !stall && last_pub >= 0 && idx > last_pub {
541 let behind = t - (last_pub + 1) as f64 / fps;
542 if behind > STALL_BEHIND && !cached!(idx) {
543 stall = true;
544 shared.buffering.store(true, Ordering::Relaxed);
545 ctx.request_repaint();
546 }
547 }
548 if stall {
549 let horizon = (idx + read_ahead).min(last_idx);
551 if let Some(i) = (idx..=horizon).find(|i| !cached!(*i)) {
552 let ok = if gpu {
553 gpu_cached(
554 &mut lcache,
555 &mut spare_layers,
556 &project,
557 i,
558 fps,
559 w,
560 h,
561 &mut pool,
562 &text,
563 &mut shapes,
564 &mut comp,
565 &shared.cache_hits,
566 )
567 .1
568 } else {
569 cpu_cached(
570 &mut fcache,
571 &mut fpool,
572 &project,
573 i,
574 fps,
575 w,
576 h,
577 &mut pool,
578 &text,
579 &mut comp,
580 &shared.cache_hits,
581 )
582 .1
583 };
584 if !ok {
585 stall = false; shared.buffering.store(false, Ordering::Relaxed);
587 ctx.request_repaint();
588 }
589 }
590 let goal = (idx + (read_ahead / 3).max(2)).min(last_idx);
592 if stall && (idx..=goal).all(|i| cached!(i)) {
593 stall = false;
594 shared.buffering.store(false, Ordering::Relaxed);
595 dirty = true; ctx.request_repaint();
597 }
598 continue; }
600 if force || idx != last_pub {
601 if gpu {
602 let (set, _) = gpu_cached(
604 &mut lcache,
605 &mut spare_layers,
606 &project,
607 idx,
608 fps,
609 w,
610 h,
611 &mut pool,
612 &text,
613 &mut shapes,
614 &mut comp,
615 &shared.cache_hits,
616 );
617 *lock(&shared.layers) = Some(set);
618 } else {
619 let (frame, _) = cpu_cached(
620 &mut fcache,
621 &mut fpool,
622 &project,
623 idx,
624 fps,
625 w,
626 h,
627 &mut pool,
628 &text,
629 &mut comp,
630 &shared.cache_hits,
631 );
632 *lock(&shared.frame) = Some(frame); }
634 last_pub = idx;
635 ctx.request_repaint();
636 }
637 if playing {
638 loop {
645 let remain = (idx + 1) as f64 / fps - lock(&shared.clock).now();
646 if remain <= 0.0 {
647 break;
648 }
649 let horizon = (((duration * fps).ceil() as i64) - 1).min(idx + read_ahead);
650 let missing =
651 ((idx + 1)..=horizon).find(|i| if gpu { !lcache.contains(*i) } else { !fcache.contains(*i) });
652 let Some(i) = missing else {
653 std::thread::sleep(Duration::from_secs_f64(remain));
654 break;
655 };
656 let ok = if gpu {
657 gpu_cached(
658 &mut lcache,
659 &mut spare_layers,
660 &project,
661 i,
662 fps,
663 w,
664 h,
665 &mut pool,
666 &text,
667 &mut shapes,
668 &mut comp,
669 &shared.cache_hits,
670 )
671 .1
672 } else {
673 cpu_cached(
674 &mut fcache,
675 &mut fpool,
676 &project,
677 i,
678 fps,
679 w,
680 h,
681 &mut pool,
682 &text,
683 &mut comp,
684 &shared.cache_hits,
685 )
686 .1
687 };
688 if !ok {
689 break; }
691 }
692 }
693 }
694}
695
696fn video_dirty_spans(old: &Project, new: &Project) -> Option<Vec<(f64, f64)>> {
701 fn json<T: serde::Serialize>(v: &T) -> String {
703 serde_json::to_string(v).unwrap_or_default()
704 }
705 if old.width != new.width
707 || old.height != new.height
708 || old.fps != new.fps
709 || old.editing != new.editing
710 || json(&old.scaler) != json(&new.scaler)
711 || old.show_subtitles != new.show_subtitles
712 || old.tracks.len() != new.tracks.len()
713 || json(&old.assets) != json(&new.assets)
714 || json(&old.sequences) != json(&new.sequences)
715 {
716 return None;
717 }
718 let mut spans: Vec<(f64, f64)> = Vec::new();
719 let clip_span = |c: &Clip| (c.start, c.start + c.duration.max(0.0));
720 if json(&old.subtitle_style) != json(&new.subtitle_style)
722 || old.subtitle_margin != new.subtitle_margin
723 || json(&old.subtitles) != json(&new.subtitles)
724 {
725 for c in old.subtitles.iter().chain(&new.subtitles) {
726 spans.push((c.start, c.end));
727 }
728 }
729 for (ot, nt) in old.tracks.iter().zip(&new.tracks) {
730 if ot.kind != nt.kind {
731 return None;
732 }
733 if ot.kind != TrackKind::Video {
734 continue; }
736 if ot.muted != nt.muted || ot.solo != nt.solo {
738 return None;
739 }
740 if json(&ot.transitions) != json(&nt.transitions) {
743 spans.push((0.0, ot.end().max(nt.end())));
744 continue;
745 }
746 let olds: HashMap<crate::model::Id, &Clip> = ot.clips.iter().map(|c| (c.id, c)).collect();
747 let news: HashMap<crate::model::Id, &Clip> = nt.clips.iter().map(|c| (c.id, c)).collect();
748 for c in &ot.clips {
749 match news.get(&c.id) {
750 None => spans.push(clip_span(c)), Some(n) => {
752 if json(*n) != json(c) {
753 spans.push(clip_span(c));
754 spans.push(clip_span(n));
755 }
756 }
757 }
758 }
759 for c in nt.clips.iter().filter(|c| !olds.contains_key(&c.id)) {
760 spans.push(clip_span(c)); }
762 }
763 Some(spans)
764}
765
766fn reclaim(old: Arc<Frame>, fpool: &mut Vec<Frame>) {
768 if fpool.len() < FPOOL_KEEP {
769 if let Ok(f) = Arc::try_unwrap(old) {
770 fpool.push(f);
771 }
772 }
773}
774
775#[allow(clippy::too_many_arguments)]
778fn cpu_cached(
779 cache: &mut Cache<Frame>,
780 fpool: &mut Vec<Frame>,
781 project: &Project,
782 idx: i64,
783 fps: f64,
784 w: u32,
785 h: u32,
786 pool: &mut DecoderPool,
787 text: &Mutex<TextRasterizer>,
788 comp: &mut Compositor,
789 hits: &AtomicU64,
790) -> (Arc<Frame>, bool) {
791 if let Some(f) = cache.get(idx) {
792 hits.fetch_add(1, Ordering::Relaxed);
793 return (f, true);
794 }
795 let t = idx as f64 / fps;
796 let mut frame = fpool.pop().unwrap_or_default();
797 let ok = guarded(pool, |pool| comp.render(project, t, w, h, pool, &mut lock(text), &mut frame));
798 let frame = Arc::new(frame);
799 if ok {
800 for old in cache.insert(idx, frame.rgba.len(), frame.clone()) {
801 reclaim(old, fpool);
802 }
803 }
804 (frame, ok)
805}
806
807#[allow(clippy::too_many_arguments)]
809fn gpu_cached(
810 cache: &mut Cache<LayerSet>,
811 spare: &mut Vec<Frame>,
812 project: &Project,
813 idx: i64,
814 fps: f64,
815 w: u32,
816 h: u32,
817 pool: &mut DecoderPool,
818 text: &Mutex<TextRasterizer>,
819 shapes: &mut ShapeRasterizer,
820 comp: &mut Compositor,
821 hits: &AtomicU64,
822) -> (Arc<LayerSet>, bool) {
823 if let Some(s) = cache.get(idx) {
824 hits.fetch_add(1, Ordering::Relaxed);
825 return (s, true);
826 }
827 let t = idx as f64 / fps;
828 let mut set = LayerSet::default();
829 let ok = {
830 let text = &mut lock(text);
831 guarded(pool, |pool| set = decode_layers(project, t, w, h, pool, spare, text, shapes, comp))
832 };
833 let set = Arc::new(set);
834 if ok {
835 let bytes: usize = set.layers.iter().map(|(_, f)| f.rgba.len()).sum::<usize>()
836 + set.motion.iter().map(|(_, _, f)| f.rgba.len()).sum::<usize>();
837 for old in cache.insert(idx, bytes, set.clone()) {
838 recycle(Some(old), spare);
839 }
840 }
841 (set, ok)
842}
843
844#[allow(clippy::too_many_arguments)]
852pub(crate) fn decode_layers(
853 project: &Project,
854 t: f64,
855 w: u32,
856 h: u32,
857 pool: &mut DecoderPool,
858 spare: &mut Vec<Frame>,
859 text: &mut TextRasterizer,
860 shapes: &mut ShapeRasterizer,
861 comp: &mut Compositor,
862) -> LayerSet {
863 let mut set = LayerSet::default();
864 for (ti, track) in project.tracks.iter().enumerate() {
865 if track.kind != TrackKind::Video || !project.active(ti) {
866 continue;
867 }
868 if let Some((_, left, right)) = track.transition_at(t) {
871 for clip in [left, right].into_iter().flatten() {
872 layer_for(project, clip, t, w, h, pool, spare, text, shapes, comp, &mut set);
873 }
874 continue;
875 }
876 for clip in &track.clips {
877 if clip.enabled && clip.contains(t) {
878 layer_for(project, clip, t, w, h, pool, spare, text, shapes, comp, &mut set);
879 }
880 }
881 }
882 if project.show_subtitles {
883 if let Some(cue) = project.cue_at(t).filter(|c| !c.text.trim().is_empty()) {
884 let mut style = project.subtitle_style.clone();
885 style.text.clone_from(&cue.text);
886 let img = text.render(&style, w as f32 / project.width.max(1) as f32);
887 if img.width > 1 || img.height > 1 {
888 set.layers.push((LayerSet::SUBTITLES, img));
889 }
890 }
891 }
892 set
893}
894
895#[allow(clippy::too_many_arguments)]
898fn layer_for(
899 project: &Project,
900 clip: &Clip,
901 t: f64,
902 w: u32,
903 h: u32,
904 pool: &mut DecoderPool,
905 spare: &mut Vec<Frame>,
906 text: &mut TextRasterizer,
907 shapes: &mut ShapeRasterizer,
908 comp: &mut Compositor,
909 set: &mut LayerSet,
910) {
911 for aid in clip.graph.iter().flat_map(|g| g.nodes.iter()).filter_map(|n| match n.kind {
916 crate::model::NodeKind::Asset(a) => Some(a),
917 _ => None,
918 }) {
919 let Some(asset) = project.asset(aid).filter(|a| a.width > 0 && a.height > 0) else { continue };
920 if set.get(aid).is_some() {
921 continue;
922 }
923 let (dw, dh) = (w.min(asset.width), h.min(asset.height));
924 if let Some(f) = decode_one(pool, &asset.path, clip.local(t), asset.duration, dw, dh, spare) {
925 set.layers.push((aid, f));
926 }
927 }
928 if !clip.draws() {
929 return;
930 }
931 let s = w as f32 / project.width.max(1) as f32;
932 match clip.kind {
933 ClipKind::Video | ClipKind::Image => {
934 if clip.is_empty_container() {
935 let label_text = if !clip.container_label.is_empty() {
936 format!("Container Slot\n[{}]", clip.container_label)
937 } else {
938 "Container Slot\n(Empty)".to_string()
939 };
940 let style = crate::model::TextStyle {
941 text: label_text,
942 size: 32.0,
943 color: [220, 220, 220, 255],
944 box_color: [40, 40, 40, 220],
945 box_padding: 16.0,
946 align: 1,
947 ..Default::default()
948 };
949 let img = text.render(&style, s);
950 if img.width > 1 || img.height > 1 {
951 set.layers.push((clip.id, img));
952 }
953 return;
954 }
955 let Some(asset) = project.asset(clip.asset) else { return };
956 let native = (asset.width, asset.height);
957 if native.0 == 0 || native.1 == 0 {
958 return;
959 }
960 let p = placement(project, clip, t, native, w, h, true);
961 if !(p.w.is_finite() && p.h.is_finite()) {
962 return;
963 }
964 let dw = (p.w.round().max(1.0) as u32).clamp(1, native.0);
965 let dh = (p.h.round().max(1.0) as u32).clamp(1, native.1);
966 if let Some(f) = decode_one(pool, &asset.path, clip.src_time(t), asset.duration, dw, dh, spare) {
967 set.layers.push((clip.id, f));
968 }
969 if clip.effects.iter().any(|e| e.enabled && e.kind.needs_motion()) {
972 let fd = project.frame_dur();
973 for k in [-1.0, -0.5] {
974 let st = clip.src_time(t + k * fd);
975 if let Some(f) = decode_one(pool, &asset.path, st, asset.duration, dw, dh, spare) {
976 set.motion.push((clip.id, k * fd, f));
977 }
978 }
979 }
980 }
981 ClipKind::Text => {
982 let Some(style) = &clip.text else { return };
983 let img = text.render(style, s);
984 if img.width > 1 || img.height > 1 {
985 set.layers.push((clip.id, img));
986 }
987 }
988 ClipKind::Shape => {
989 let Some(style) = &clip.shape else { return };
990 let img = shapes.render(style, s, clip.local(t));
991 if !img.is_empty() {
992 set.layers.push((clip.id, img));
993 }
994 }
995 ClipKind::Sequence => {
996 let editing = project.editing == Some(clip.sequence);
997 let (qw, qh) = if editing {
998 (project.width, project.height) } else {
1000 match project.sequence(clip.sequence) {
1001 Some(sq) => (sq.width, sq.height),
1002 None => return,
1003 }
1004 };
1005 if qw == 0 || qh == 0 {
1006 return;
1007 }
1008 let p = placement(project, clip, t, (qw, qh), w, h, true);
1009 if !(p.w.is_finite() && p.h.is_finite() && p.w > 0.0 && p.h > 0.0) {
1010 return;
1011 }
1012 let dw = (p.w.round() as u32).clamp(1, qw);
1013 let dh = (p.h.round() as u32).clamp(1, qh);
1014 let dur = project.sequence_duration(clip.sequence);
1015 let st = clip.src_time(t).clamp(0.0, (dur - 1e-6).max(0.0));
1016 let mut sub = project.clone();
1020 if !editing {
1021 let Some(sq) = project.sequence(clip.sequence) else { return };
1022 sub.tracks.clone_from(&sq.tracks);
1023 (sub.width, sub.height) = (qw, qh);
1024 sub.editing = Some(clip.sequence);
1025 }
1026 sub.show_subtitles = false; let mut frame = spare.pop().unwrap_or_default();
1028 comp.render(&sub, st, dw, dh, pool, text, &mut frame);
1029 set.layers.push((clip.id, Arc::new(frame)));
1030 }
1031 ClipKind::Audio | ClipKind::Adjustment => {}
1032 }
1033}
1034
1035fn decode_one(
1037 pool: &mut DecoderPool,
1038 path: &str,
1039 src_t: f64,
1040 src_duration: f64,
1041 dw: u32,
1042 dh: u32,
1043 spare: &mut Vec<Frame>,
1044) -> Option<Arc<Frame>> {
1045 let st = if src_duration > 0.0 { src_t.clamp(0.0, (src_duration - 1e-4).max(0.0)) } else { src_t.max(0.0) };
1046 let mut frame = spare.pop().unwrap_or_default();
1047 let dec = pool.video(path)?;
1048 if !dec.frame_at(st, dw, dh, &mut frame) {
1049 spare.push(frame);
1050 return None;
1051 }
1052 Some(Arc::new(frame))
1053}
1054
1055fn recycle(old: Option<Arc<LayerSet>>, spare: &mut Vec<Frame>) {
1057 const KEEP: usize = 8;
1058 let Some(Ok(set)) = old.map(Arc::try_unwrap) else { return };
1059 let frames = set.layers.into_iter().map(|(_, f)| f).chain(set.motion.into_iter().map(|(_, _, f)| f));
1060 for f in frames {
1061 if spare.len() >= KEEP {
1062 return;
1063 }
1064 if let Ok(f) = Arc::try_unwrap(f) {
1065 spare.push(f);
1066 }
1067 }
1068}
1069
1070fn audio_thread(shared: Arc<Shared>, rx: Receiver<Cmd>, backend: Backend) {
1071 use cpal::traits::StreamTrait;
1072 let mut pool = DecoderPool::new(backend);
1073 let mut mixer = Mixer::new();
1074 let ring: Arc<Mutex<VecDeque<f32>>> = Arc::new(Mutex::new(VecDeque::new()));
1075 let dead = Arc::new(AtomicBool::new(false)); let mut stream: Option<cpal::Stream> = None; let mut running = false; let mut project = lock(&shared.project).clone();
1079 let mut block = vec![0f32; BLOCK * 2];
1080 let mut mixed_until = 0.0;
1081 let mut filling = false;
1085 let mut pending: Option<Cmd> = None;
1086 let is_playing = || {
1087 let mut c = lock(&shared.clock);
1088 c.now();
1089 c.playing
1090 };
1091 loop {
1092 if pending.is_none() && !running && !(stream.is_some() && is_playing()) {
1093 let Ok(c) = rx.recv() else { return }; pending = Some(c);
1095 }
1096 loop {
1097 let c = match pending.take() {
1098 Some(c) => c,
1099 None => match rx.try_recv() {
1100 Ok(c) => c,
1101 Err(TryRecvError::Empty) => break,
1102 Err(TryRecvError::Disconnected) => return,
1103 },
1104 };
1105 match c {
1106 Cmd::SetProject => project = lock(&shared.project).clone(),
1107 Cmd::Play | Cmd::Seek => {
1108 lock(&ring).clear();
1109 mixed_until = lock(&shared.clock).now();
1110 filling = true;
1111 }
1112 Cmd::Pause => lock(&ring).clear(),
1113 Cmd::Canvas => {}
1114 Cmd::Backend(b) => pool.set_backend(b),
1115 Cmd::Gpu(_) | Cmd::Proxies(_) => {} Cmd::ClearDecoders(ack) => {
1117 pool.clear();
1118 let _ = ack.send(());
1119 }
1120 Cmd::RenderOnce(..) | Cmd::LayersOnce(..) => {} Cmd::Quit => return,
1122 }
1123 }
1124 let playing = is_playing();
1125 if playing && (stream.is_none() || dead.swap(false, Ordering::Relaxed)) {
1126 stream = open_output(ring.clone(), dead.clone());
1129 running = false;
1130 }
1131 let Some(stream) = &stream else { continue };
1132 if playing && !running {
1133 running = stream.play().is_ok();
1134 } else if !playing && running && lock(&ring).is_empty() {
1135 let _ = stream.pause();
1137 running = false;
1138 }
1139 let queued = lock(&ring).len() as f64 / (2 * SAMPLE_RATE) as f64;
1140 if playing && queued < LEAD_SECS {
1141 let now = lock(&shared.clock).now();
1149 if !filling && now - (mixed_until - queued) > 0.05 {
1150 mixed_until = now + queued;
1151 }
1152 if guarded(&mut pool, |pool| mixer.mix(&project, mixed_until, pool, &mut block)) {
1153 lock(&ring).extend(block.iter().copied()); }
1155 mixed_until += BLOCK as f64 / SAMPLE_RATE as f64;
1156 } else {
1157 filling = false; match rx.recv_timeout(Duration::from_millis(4)) {
1159 Ok(c) => pending = Some(c),
1160 Err(RecvTimeoutError::Disconnected) => return,
1161 Err(RecvTimeoutError::Timeout) => {}
1162 }
1163 }
1164 }
1165}
1166
1167fn open_output(ring: Arc<Mutex<VecDeque<f32>>>, dead: Arc<AtomicBool>) -> Option<cpal::Stream> {
1169 use cpal::traits::{DeviceTrait, HostTrait};
1170 let device = cpal::default_host().default_output_device()?;
1171 let config = cpal::StreamConfig { channels: 2, sample_rate: SAMPLE_RATE, buffer_size: cpal::BufferSize::Default };
1174 let stream = device
1175 .build_output_stream(
1176 config,
1177 move |out: &mut [f32], _: &cpal::OutputCallbackInfo| {
1178 let mut r = lock(&ring);
1179 for s in out.iter_mut() {
1180 *s = r.pop_front().unwrap_or(0.0); }
1182 },
1183 move |e| {
1184 eprintln!("audio: {e}");
1185 if e.kind() != cpal::ErrorKind::Xrun {
1188 dead.store(true, Ordering::Relaxed);
1189 }
1190 },
1191 None,
1192 )
1193 .map_err(|e| eprintln!("audio: no output stream ({e}); playing silently"))
1194 .ok()?;
1195 Some(stream)
1196}
1197
1198#[cfg(test)]
1199mod tests {
1200 use super::*;
1201 use crate::media;
1202 use std::thread::sleep;
1203
1204 fn wait_frame(p: &mut Player, t: f64, size: (u32, u32)) -> Arc<Frame> {
1206 for _ in 0..300 {
1207 if let Some(f) = p.take_frame() {
1208 if (f.pts - t).abs() < 1e-6 && (f.width, f.height) == size {
1209 return f;
1210 }
1211 }
1212 sleep(Duration::from_millis(10));
1213 }
1214 panic!("no {size:?} frame for t={t} within 3 s");
1215 }
1216 fn centre(f: &Frame) -> [u8; 4] {
1217 let i = ((f.height / 2 * f.width + f.width / 2) * 4) as usize;
1218 [f.rgba[i], f.rgba[i + 1], f.rgba[i + 2], f.rgba[i + 3]]
1219 }
1220
1221 #[test]
1222 fn player_seek_play_pause_release() {
1223 use cpal::traits::HostTrait;
1224 if cpal::default_host().default_output_device().is_none() {
1225 println!("playback test: no audio device, running silently");
1226 }
1227 let path = media::ffpipe::tests::test_mp4(); let asset = media::probe(&path, Backend::Auto).unwrap();
1229 let project = Project::from_media(asset);
1230 let mut p =
1231 Player::new(eframe::egui::Context::default(), Backend::Auto, Arc::new(Mutex::new(TextRasterizer::new())));
1232 p.set_project(&project);
1233 p.set_canvas(320, 240, 1280);
1234 let dur = project.duration();
1235 assert!((dur - 4.0).abs() < 0.2, "duration {dur}");
1236
1237 p.seek(0.5);
1238 let c = centre(&wait_frame(&mut p, 0.5, (320, 240)));
1239 assert!(c[0] > 200 && c[1] < 70 && c[2] < 70, "expected red at 0.5 s, got {c:?}");
1240
1241 p.seek(2.5);
1242 let c = centre(&wait_frame(&mut p, 2.5, (320, 240)));
1243 assert!(c[0] < 70 && c[1] > 200 && c[2] < 70, "expected green at 2.5 s, got {c:?}");
1244
1245 p.play();
1246 assert!(p.is_playing());
1247 let (start, mut pts) = (Instant::now(), Vec::new());
1250 while start.elapsed() < Duration::from_millis(600) {
1251 if let Some(f) = p.take_frame() {
1252 let now = p.time();
1253 assert!(f.pts >= 2.5 && f.pts <= now, "pts {} vs time {now}", f.pts);
1255 pts.push(f.pts);
1256 }
1257 sleep(Duration::from_millis(1));
1258 }
1259 let t = p.time();
1260 assert!((3.05..3.45).contains(&t), "time after 600 ms of play: {t}");
1261 let mut gaps: Vec<f64> = pts.windows(2).map(|w| w[1] - w[0]).collect();
1262 gaps.sort_by(f64::total_cmp);
1263 assert!(gaps.len() >= 8, "only {} frames in 600 ms", pts.len());
1264 let q1 = gaps[gaps.len() / 4]; assert!(q1 < 1.25 / project.fps, "frame gap {q1:.4} s at {} fps", project.fps);
1266
1267 p.pause();
1268 let t1 = p.time();
1269 sleep(Duration::from_millis(100));
1270 assert_eq!(p.time(), t1);
1271 assert!(!p.is_playing());
1272
1273 p.seek(dur);
1275 p.play();
1276 let t = p.time();
1277 assert!(t < 0.2, "rewound: {t}");
1278 p.pause();
1279
1280 let start = Instant::now();
1281 p.release_files();
1282 assert!(start.elapsed() < Duration::from_secs(2), "release_files took {:?}", start.elapsed());
1283 p.seek(1.0); p.set_canvas(160, 120, 100); wait_frame(&mut p, 1.0, (100, 75));
1286
1287 let f = p.render_once(2.5, 160).expect("render_once");
1289 assert_eq!((f.width, f.height), (160, 120));
1290 let c = centre(&f);
1291 assert!(c[0] < 70 && c[1] > 200, "expected green from render_once at 2.5 s, got {c:?}");
1292 }
1293
1294 #[test]
1296 fn replay_serves_cached_frames() {
1297 let path = media::ffpipe::tests::test_mp4(); let asset = media::probe(&path, Backend::Auto).unwrap();
1299 let project = Project::from_media(asset);
1300 let mut p =
1301 Player::new(eframe::egui::Context::default(), Backend::Auto, Arc::new(Mutex::new(TextRasterizer::new())));
1302 p.set_project(&project);
1303 p.set_canvas(320, 240, 1280);
1304 p.seek(0.5);
1305 wait_frame(&mut p, 0.5, (320, 240));
1306 let h0 = p.cache_hits();
1307 p.seek(0.5); wait_frame(&mut p, 0.5, (320, 240));
1309 assert!(p.cache_hits() > h0, "re-seek to the same frame must hit the cache");
1310
1311 p.play();
1313 sleep(Duration::from_millis(400));
1314 p.pause();
1315 let h1 = p.cache_hits();
1316 p.seek(0.6); let f = wait_frame(&mut p, 0.6, (320, 240));
1318 assert!(p.cache_hits() > h1, "replay after playback must be served from cache");
1319 let c = centre(&f);
1320 assert!(c[0] > 200 && c[1] < 70, "cached frame content must be right: {c:?}");
1321
1322 let h2 = p.cache_hits();
1324 p.set_project(&project);
1325 p.seek(0.6);
1326 wait_frame(&mut p, 0.6, (320, 240));
1327 assert!(p.cache_hits() > h2, "an identical project must keep the cache");
1328
1329 let mut edited = project.clone();
1331 let (_, clip) = edited.all_clips().next().expect("one clip");
1332 let id = clip.id;
1333 edited.clip_mut(id).unwrap().speed = 2.0;
1334 let h3 = p.cache_hits();
1335 p.set_project(&edited);
1336 p.seek(0.6);
1337 wait_frame(&mut p, 0.6, (320, 240));
1338 assert_eq!(p.cache_hits(), h3, "an edit inside the clip's span must evict its frames");
1339 }
1340
1341 #[test]
1343 fn dirty_spans_bound_the_edit() {
1344 let path = media::ffpipe::tests::test_mp4();
1345 let asset = media::probe(&path, Backend::Auto).unwrap();
1346 let project = Project::from_media(asset);
1347 assert_eq!(video_dirty_spans(&project, &project.clone()), Some(vec![]));
1349 let mut m = project.clone();
1351 m.in_point = Some(1.0);
1352 assert_eq!(video_dirty_spans(&project, &m), Some(vec![]));
1353 let mut e = project.clone();
1355 let id = e.all_clips().next().unwrap().1.id;
1356 e.clip_mut(id).unwrap().speed = 2.0;
1357 let spans = video_dirty_spans(&project, &e).expect("bounded");
1358 assert!(!spans.is_empty());
1359 let mut g = project.clone();
1361 g.width += 2;
1362 assert_eq!(video_dirty_spans(&project, &g), None);
1363 }
1364
1365 #[test]
1368 fn gpu_mode_publishes_layers() {
1369 let path = media::ffpipe::tests::test_mp4();
1370 let asset = media::probe(&path, Backend::Auto).unwrap();
1371 let project = Project::from_media(asset);
1372 let mut p =
1373 Player::new(eframe::egui::Context::default(), Backend::Auto, Arc::new(Mutex::new(TextRasterizer::new())));
1374 p.set_project(&project);
1375 p.set_canvas(320, 240, 1280);
1376 p.set_gpu(true);
1377 p.seek(0.5);
1378 let mut set = None;
1379 for _ in 0..300 {
1380 if let Some(s) = p.take_layers() {
1381 if s.layers.first().map(|(_, f)| (f.pts - 0.5).abs() < 0.2).unwrap_or(false) {
1382 set = Some(s);
1383 break;
1384 }
1385 }
1386 sleep(Duration::from_millis(10));
1387 }
1388 let set = set.expect("no layers within 3 s");
1389 assert_eq!(set.layers.len(), 1, "one video clip → one layer");
1390 let (id, frame) = &set.layers[0];
1391 assert_eq!(*id, project.all_clips().find(|(_, c)| c.is_visual()).unwrap().1.id);
1392 assert!(set.get(*id).is_some());
1393 let c = centre(frame);
1394 assert!(c[0] > 200 && c[1] < 70, "expected red at 0.5 s, got {c:?}");
1395 assert!(set.motion.is_empty(), "no motion-blur effect → no extra samples");
1396
1397 p.set_gpu(false);
1399 p.seek(2.5);
1400 let f = wait_frame(&mut p, 2.5, (320, 240));
1401 let c = centre(&f);
1402 assert!(c[1] > 200 && c[0] < 70, "expected green from the CPU path, got {c:?}");
1403 assert!(p.take_layers().is_none(), "layers must stop when the GPU path is off");
1404 }
1405
1406 #[test]
1409 fn layers_cover_text_shape_sequence_and_subtitles() {
1410 use crate::model::{ShapeKind, ShapeStyle};
1411 let mut project = Project::new();
1412 (project.width, project.height) = (320, 240);
1413 let txt = project.add_text_clip(0.0, 4.0);
1414 let shp = project.add_shape_clip(ShapeKind::Star, 0.0, 4.0);
1415 let sid = project.new_sequence("seq", 320, 240, 30.0);
1416 let mut inner = Clip::new(project.new_id(), ClipKind::Shape, "inner", 0.0, 4.0);
1417 inner.shape = Some(ShapeStyle::new(ShapeKind::Rect)); project.sequence_mut(sid).unwrap().tracks[0].clips.push(inner);
1419 let seq = project.insert_sequence_clip(sid, 0.0, None).expect("sequence clip");
1420 project.add_cue(0.0, 2.0, "HELLO");
1421 assert!(project.show_subtitles);
1422
1423 let (mut pool, mut spare) = (DecoderPool::new(Backend::Ffmpeg), Vec::new());
1424 let (mut text, mut shapes, mut comp) = (TextRasterizer::new(), ShapeRasterizer::new(), Compositor::new());
1425 let set = decode_layers(&project, 1.0, 320, 240, &mut pool, &mut spare, &mut text, &mut shapes, &mut comp);
1426 assert!(set.get(shp).is_some(), "shape clip has no layer");
1427 assert!(set.get(seq).is_some(), "nested sequence has no layer");
1428 if text.families().is_empty() {
1429 eprintln!("no system fonts - skipping the text assertions");
1430 return;
1431 }
1432 assert!(set.get(txt).is_some(), "text clip has no layer");
1433 assert!(set.get(LayerSet::SUBTITLES).is_some(), "no subtitle layer");
1434 let set = decode_layers(&project, 3.0, 320, 240, &mut pool, &mut spare, &mut text, &mut shapes, &mut comp);
1436 assert!(set.get(LayerSet::SUBTITLES).is_none(), "subtitle layer outside the cue");
1437 }
1438
1439 #[test]
1442 fn transition_window_yields_both_clips() {
1443 use crate::model::{Ease, ShapeKind, Transition, TransitionKind};
1444 let mut project = Project::new();
1445 (project.width, project.height) = (320, 240);
1446 let a = project.add_shape_clip(ShapeKind::Rect, 0.0, 2.0);
1447 let b = project.add_shape_clip(ShapeKind::Ellipse, 2.0, 2.0);
1448 let vt = project.video_tracks()[0];
1449 assert!(project.tracks[vt].clips.len() == 2, "the two clips must abut on one track");
1450 let id = project.new_id();
1451 project.tracks[vt].transitions.push(Transition {
1452 id,
1453 right: b,
1454 kind: TransitionKind::CrossFade,
1455 duration: 1.0,
1456 color: [0, 0, 0, 255],
1457 direction: 0,
1458 ease: Ease::Linear,
1459 edge: Default::default(),
1460 });
1461 let (mut pool, mut spare) = (DecoderPool::new(Backend::Ffmpeg), Vec::new());
1462 let (mut text, mut shapes, mut comp) = (TextRasterizer::new(), ShapeRasterizer::new(), Compositor::new());
1463 for t in [1.7, 2.3] {
1465 let set = decode_layers(&project, t, 320, 240, &mut pool, &mut spare, &mut text, &mut shapes, &mut comp);
1466 assert!(set.get(a).is_some(), "outgoing clip missing at t={t}");
1467 assert!(set.get(b).is_some(), "incoming clip missing at t={t}");
1468 }
1469 let set = decode_layers(&project, 0.5, 320, 240, &mut pool, &mut spare, &mut text, &mut shapes, &mut comp);
1471 assert!(set.get(a).is_some() && set.get(b).is_none());
1472 }
1473
1474 #[test]
1476 fn guarded_contains_a_panicking_decode() {
1477 let mut pool = DecoderPool::new(Backend::Ffmpeg);
1478 let hook = std::panic::take_hook();
1479 std::panic::set_hook(Box::new(|_| {})); assert!(!guarded(&mut pool, |_| panic!("decoder exploded")));
1481 std::panic::set_hook(hook);
1482 let mut ran = false;
1483 assert!(guarded(&mut pool, |_| ran = true));
1484 assert!(ran);
1485 }
1486}