1#![warn(missing_docs)]
17
18use std::collections::VecDeque;
19
20use oxframe_core::{FrameSource, Image};
21use oxframe_protocol::{
22 Command, ErrorKind, Event, FrameFormat, FrameHeader, MarkerInfo, Meta, NodeInfo,
23 PROTOCOL_VERSION, SceneInfo, SignalValue,
24};
25
26#[cfg(all(feature = "server", not(target_arch = "wasm32")))]
27pub mod server;
28
29#[derive(Debug, Clone, PartialEq)]
31pub enum MetaEdit {
32 Marker {
34 name: String,
36 time: f64,
38 scene: Option<String>,
40 },
41 Ease {
43 name: String,
45 value: [f64; 4],
47 },
48 Tunable {
50 name: String,
52 value: f64,
54 },
55 Transition {
57 scene: String,
59 seconds: f64,
61 },
62}
63
64pub trait PreviewEngine: FrameSource {
67 fn engine_version(&self) -> String;
69 fn scenes(&self) -> Vec<SceneInfo>;
71 fn markers(&self) -> Vec<MarkerInfo>;
73 fn metas(&self) -> Vec<(String, Meta)>;
76 fn edit(&mut self, edit: MetaEdit) -> Result<Vec<(String, Meta)>, String>;
83 fn pick(&mut self, frame: u32, x: f64, y: f64) -> Option<u32>;
85 fn tree(&mut self, frame: u32) -> Vec<NodeInfo>;
87 fn inspect(&mut self, frame: u32, node: u32) -> Option<(Vec<SignalValue>, [f64; 8])>;
89 fn waveform(&mut self, buckets: usize) -> Option<Vec<f32>>;
91}
92
93#[derive(Debug, Clone)]
95pub enum Output {
96 Event(Event),
98 Frame(Vec<u8>),
100}
101
102pub type Encoder = Box<dyn FnMut(&Image) -> (FrameFormat, Vec<u8>)>;
104
105type CacheKey = (u32, u32, u32, u64);
107
108struct FrameCache {
110 limit: usize,
111 used: usize,
112 entries: VecDeque<(CacheKey, Vec<u8>)>,
113}
114
115impl FrameCache {
116 fn get(&mut self, key: CacheKey) -> Option<Vec<u8>> {
117 let pos = self.entries.iter().position(|(k, _)| *k == key)?;
118 let entry = self.entries.remove(pos)?;
119 let bytes = entry.1.clone();
120 self.entries.push_back(entry);
121 Some(bytes)
122 }
123
124 fn put(&mut self, key: CacheKey, bytes: Vec<u8>) {
125 self.used += bytes.len();
126 self.entries.push_back((key, bytes));
127 while self.used > self.limit {
128 let Some((_, old)) = self.entries.pop_front() else {
129 break;
130 };
131 self.used -= old.len();
132 }
133 }
134
135 fn clear(&mut self) {
136 self.entries.clear();
137 self.used = 0;
138 }
139}
140
141pub struct Session<E> {
143 engine: E,
144 encode: Encoder,
145 frame: u32,
146 playing: Option<(f64, u32)>,
147 looping: bool,
148 viewport: (u32, u32),
149 selected: Option<u32>,
150 version: u64,
152 cache: FrameCache,
153 last_tree: Vec<NodeInfo>,
154}
155
156impl<E: PreviewEngine> Session<E> {
157 pub fn new(engine: E, encode: Encoder, cache_mb: usize) -> Self {
159 let size = engine.size();
160 Self {
161 engine,
162 encode,
163 frame: 0,
164 playing: None,
165 looping: true,
166 viewport: (size.0.min(960), size.1.min(540)),
167 selected: None,
168 version: 0,
169 cache: FrameCache {
170 limit: cache_mb * 1024 * 1024,
171 used: 0,
172 entries: VecDeque::new(),
173 },
174 last_tree: Vec::new(),
175 }
176 }
177
178 pub fn engine(&mut self) -> &mut E {
180 &mut self.engine
181 }
182
183 pub fn frame(&self) -> u32 {
185 self.frame
186 }
187
188 pub fn is_playing(&self) -> bool {
190 self.playing.is_some()
191 }
192
193 pub fn render_size(&self) -> (u32, u32) {
196 let (w, h) = self.engine.size();
197 let (vw, vh) = self.viewport;
198 let k = (f64::from(vw) / f64::from(w))
199 .min(f64::from(vh) / f64::from(h))
200 .min(1.0);
201 (
202 ((f64::from(w) * k).round() as u32).max(1),
203 ((f64::from(h) * k).round() as u32).max(1),
204 )
205 }
206
207 fn ready(&mut self) -> Vec<Output> {
208 let (w, h) = self.engine.size();
209 let mut out = vec![Output::Event(Event::Ready {
210 protocol_version: PROTOCOL_VERSION,
211 engine_version: self.engine.engine_version(),
212 fps: self.engine.fps(),
213 size: [w, h],
214 scenes: self.engine.scenes(),
215 })];
216 out.extend(
217 self.engine
218 .metas()
219 .into_iter()
220 .map(|(scene, meta)| Output::Event(Event::MetaChanged { scene, meta })),
221 );
222 out.push(self.timeline(true));
223 out.extend(self.show());
224 out
225 }
226
227 fn timeline(&mut self, with_audio: bool) -> Output {
228 let frames = self.engine.total_frames();
229 Output::Event(Event::Timeline {
230 frames,
231 markers: self.engine.markers(),
232 scenes: self.engine.scenes(),
233 audio_waveform: if with_audio {
234 self.engine.waveform((frames as usize).clamp(1, 4096))
235 } else {
236 None
237 },
238 })
239 }
240
241 fn show(&mut self) -> Vec<Output> {
243 let total = self.engine.total_frames();
244 self.frame = self.frame.min(total.saturating_sub(1));
245 let size = self.render_size();
246 let key = (self.frame, size.0, size.1, self.version);
247 let body = match self.cache.get(key) {
248 Some(b) => b,
249 None => {
250 let img = self.engine.render(self.frame, size);
251 let (format, bytes) = (self.encode)(&img);
252 let header = FrameHeader {
253 frame: self.frame,
254 width: img.width as u16,
255 height: img.height as u16,
256 format,
257 len: bytes.len() as u32,
258 };
259 let mut msg = Vec::with_capacity(FrameHeader::SIZE + bytes.len());
260 msg.extend_from_slice(&header.encode());
261 msg.extend_from_slice(&bytes);
262 if format != FrameFormat::Surface {
264 self.cache.put(key, msg.clone());
265 }
266 msg
267 }
268 };
269 let mut out = vec![
270 Output::Event(Event::Frame { index: self.frame }),
271 Output::Frame(body),
272 ];
273 let tree = self.engine.tree(self.frame);
274 if tree != self.last_tree {
275 self.last_tree = tree.clone();
276 out.push(Output::Event(Event::Tree { nodes: tree }));
277 }
278 if let Some(id) = self.selected {
279 match self.engine.inspect(self.frame, id) {
280 Some((values, bounds)) => out.push(Output::Event(Event::Signals {
281 node_id: id,
282 values,
283 bounds: Some(bounds),
284 })),
285 None => {
286 self.selected = None;
287 out.push(Output::Event(Event::Selected { node_id: None }));
288 }
289 }
290 }
291 out
292 }
293
294 fn error(kind: ErrorKind, message: impl Into<String>) -> Output {
295 Output::Event(Event::Error {
296 kind,
297 message: message.into(),
298 spans: Vec::new(),
299 })
300 }
301
302 pub fn handle(&mut self, cmd: Command, now: f64) -> Vec<Output> {
305 match cmd {
306 Command::Hello {
307 protocol_version, ..
308 } => {
309 if protocol_version != PROTOCOL_VERSION {
310 return vec![Self::error(
311 ErrorKind::Incompatible,
312 format!(
313 "editor speaks protocol v{protocol_version}, engine speaks v{PROTOCOL_VERSION}; \
314 update oxframe-cli or the SDK so they match"
315 ),
316 )];
317 }
318 self.ready()
319 }
320 Command::Seek { frame } => {
321 self.frame = frame;
322 if let Some((_, _)) = self.playing {
323 self.playing = Some((now, frame));
324 }
325 self.show()
326 }
327 Command::Step { delta } => {
328 self.playing = None;
329 self.frame = self.frame.saturating_add_signed(delta);
330 let mut out = vec![self.playback()];
331 out.extend(self.show());
332 out
333 }
334 Command::Play => {
335 if self.frame + 1 >= self.engine.total_frames() {
336 self.frame = 0;
337 }
338 self.playing = Some((now, self.frame));
339 let mut out = vec![self.playback()];
340 out.extend(self.show());
341 out
342 }
343 Command::Pause => {
344 self.playing = None;
345 vec![self.playback()]
346 }
347 Command::SetViewport { w, h, dpr } => {
348 let dpr = if dpr.is_finite() && dpr > 0.0 {
349 dpr
350 } else {
351 1.0
352 };
353 self.viewport = (
354 ((f64::from(w) * dpr) as u32).max(1),
355 ((f64::from(h) * dpr) as u32).max(1),
356 );
357 self.show()
358 }
359 Command::SetMarker { name, time, scene } => {
360 self.edit(MetaEdit::Marker { name, time, scene })
361 }
362 Command::SetEase { name, value } => self.edit(MetaEdit::Ease { name, value }),
363 Command::SetTunable { name, value } => self.edit(MetaEdit::Tunable { name, value }),
364 Command::SetTransition { scene, seconds } => {
365 self.edit(MetaEdit::Transition { scene, seconds })
366 }
367 Command::Pick { x, y } => {
368 self.selected = self.engine.pick(self.frame, x, y);
369 let mut out = vec![Output::Event(Event::Selected {
370 node_id: self.selected,
371 })];
372 out.extend(self.show());
373 out
374 }
375 Command::Select { node_id } => {
376 self.selected = node_id;
377 let mut out = vec![Output::Event(Event::Selected { node_id })];
378 out.extend(self.show());
379 out
380 }
381 Command::Shutdown => Vec::new(),
383 }
384 }
385
386 fn playback(&self) -> Output {
387 Output::Event(Event::Playback {
388 playing: self.playing.is_some(),
389 frame: self.frame,
390 })
391 }
392
393 fn edit(&mut self, edit: MetaEdit) -> Vec<Output> {
394 match self.engine.edit(edit) {
395 Ok(changed) => {
396 self.version += 1;
397 self.cache.clear();
398 let mut out: Vec<Output> = changed
399 .into_iter()
400 .map(|(scene, meta)| Output::Event(Event::MetaChanged { scene, meta }))
401 .collect();
402 out.push(self.timeline(false));
403 out.extend(self.show());
404 out
405 }
406 Err(e) => vec![Self::error(ErrorKind::Meta, e)],
407 }
408 }
409
410 pub fn tick(&mut self, now: f64) -> Vec<Output> {
413 let Some((start, from)) = self.playing else {
414 return Vec::new();
415 };
416 let total = self.engine.total_frames();
417 let elapsed = ((now - start) * f64::from(self.engine.fps()))
418 .floor()
419 .max(0.0) as u32;
420 let mut target = from.saturating_add(elapsed);
421 if target >= total {
422 if self.looping && total > 0 {
423 target %= total;
424 self.playing = Some((now, target));
425 } else {
426 self.playing = None;
427 self.frame = total.saturating_sub(1);
428 let mut out = vec![self.playback()];
429 out.extend(self.show());
430 return out;
431 }
432 }
433 if target == self.frame {
434 return Vec::new();
435 }
436 self.frame = target;
437 self.show()
438 }
439
440 pub fn next_tick_in(&self, now: f64) -> Option<f64> {
442 let (start, _) = self.playing?;
443 let dt = 1.0 / f64::from(self.engine.fps());
444 let elapsed = now - start;
445 Some(dt - elapsed.rem_euclid(dt))
446 }
447}
448
449pub fn rgba_encoder() -> Encoder {
451 Box::new(|img: &Image| (FrameFormat::Rgba, img.data.clone()))
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457
458 struct Fake {
459 renders: u32,
460 markers: Vec<MarkerInfo>,
461 }
462
463 impl FrameSource for Fake {
464 fn fps(&self) -> u32 {
465 10
466 }
467 fn size(&self) -> (u32, u32) {
468 (200, 100)
469 }
470 fn total_frames(&self) -> u32 {
471 20
472 }
473 fn render(&mut self, frame: u32, size: (u32, u32)) -> Image {
474 self.renders += 1;
475 Image {
476 width: size.0,
477 height: size.1,
478 data: vec![frame as u8; (size.0 * size.1 * 4) as usize],
479 }
480 }
481 }
482
483 impl PreviewEngine for Fake {
484 fn engine_version(&self) -> String {
485 "test".into()
486 }
487 fn scenes(&self) -> Vec<SceneInfo> {
488 vec![SceneInfo {
489 name: "a".into(),
490 start: 0,
491 frames: 20,
492 transition: 0,
493 transition_at: 0,
494 }]
495 }
496 fn markers(&self) -> Vec<MarkerInfo> {
497 self.markers.clone()
498 }
499 fn metas(&self) -> Vec<(String, Meta)> {
500 vec![("a".into(), Meta::default())]
501 }
502 fn edit(&mut self, edit: MetaEdit) -> Result<Vec<(String, Meta)>, String> {
503 match edit {
504 MetaEdit::Marker { name, time, .. } => {
505 self.markers = vec![MarkerInfo {
506 scene: "a".into(),
507 name,
508 time,
509 }];
510 Ok(vec![("a".into(), Meta::default())])
511 }
512 _ => Err("unsupported".into()),
513 }
514 }
515 fn pick(&mut self, _: u32, x: f64, _: f64) -> Option<u32> {
516 (x < 100.0).then_some(1)
517 }
518 fn tree(&mut self, _: u32) -> Vec<NodeInfo> {
519 Vec::new()
520 }
521 fn inspect(&mut self, _: u32, node: u32) -> Option<(Vec<SignalValue>, [f64; 8])> {
522 Some((
523 vec![SignalValue {
524 name: "id".into(),
525 value: node.to_string(),
526 }],
527 [0.0; 8],
528 ))
529 }
530 fn waveform(&mut self, _: usize) -> Option<Vec<f32>> {
531 None
532 }
533 }
534
535 fn session() -> Session<Fake> {
536 Session::new(
537 Fake {
538 renders: 0,
539 markers: Vec::new(),
540 },
541 rgba_encoder(),
542 16,
543 )
544 }
545
546 fn events(out: &[Output]) -> Vec<String> {
547 out.iter()
548 .map(|o| match o {
549 Output::Event(e) => serde_json::to_value(e).unwrap()["type"]
550 .as_str()
551 .unwrap()
552 .to_owned(),
553 Output::Frame(b) => format!("bin{}", FrameHeader::decode(b).unwrap().frame),
554 })
555 .collect()
556 }
557
558 #[test]
559 fn handshake_and_version_mismatch() {
560 let mut s = session();
561 let out = s.handle(
562 Command::Hello {
563 protocol_version: PROTOCOL_VERSION,
564 ui_version: "x".into(),
565 },
566 0.0,
567 );
568 assert_eq!(
569 &events(&out)[..5],
570 ["Ready", "MetaChanged", "Timeline", "Frame", "bin0"]
571 );
572 let bad = s.handle(
573 Command::Hello {
574 protocol_version: 999,
575 ui_version: "x".into(),
576 },
577 0.0,
578 );
579 assert!(matches!(
580 &bad[0],
581 Output::Event(Event::Error {
582 kind: ErrorKind::Incompatible,
583 ..
584 })
585 ));
586 }
587
588 #[test]
589 fn viewport_fits_aspect_and_cache_hits() {
590 let mut s = session();
591 s.handle(
592 Command::SetViewport {
593 w: 50,
594 h: 50,
595 dpr: 2.0,
596 },
597 0.0,
598 );
599 assert_eq!(s.render_size(), (100, 50));
600 let renders = s.engine().renders;
601 s.handle(Command::Seek { frame: 0 }, 0.0);
602 assert_eq!(s.engine().renders, renders, "served from cache");
603 s.handle(Command::Seek { frame: 5 }, 0.0);
604 assert_eq!(s.engine().renders, renders + 1);
605 }
606
607 #[test]
608 fn playback_follows_the_clock_and_loops() {
609 let mut s = session();
610 s.handle(Command::Seek { frame: 0 }, 0.0);
611 s.handle(Command::Play, 10.0);
612 assert!(s.tick(10.05).is_empty(), "half a frame later");
613 assert_eq!(events(&s.tick(10.35))[..2], ["Frame", "bin3"]);
614 s.tick(12.15); assert_eq!(s.frame(), 1);
616 s.handle(Command::Pause, 13.0);
617 assert!(!s.is_playing());
618 }
619
620 #[test]
621 fn edits_invalidate_cache_and_report_meta() {
622 let mut s = session();
623 s.handle(Command::Seek { frame: 2 }, 0.0);
624 let out = s.handle(
625 Command::SetMarker {
626 name: "beat".into(),
627 time: 1.0,
628 scene: None,
629 },
630 0.0,
631 );
632 assert_eq!(
633 &events(&out)[..4],
634 ["MetaChanged", "Timeline", "Frame", "bin2"]
635 );
636 let err = s.handle(
637 Command::SetTunable {
638 name: "x".into(),
639 value: 1.0,
640 },
641 0.0,
642 );
643 assert!(matches!(
644 &err[0],
645 Output::Event(Event::Error {
646 kind: ErrorKind::Meta,
647 ..
648 })
649 ));
650 }
651
652 #[test]
653 fn pick_selects_and_streams_signals() {
654 let mut s = session();
655 let out = s.handle(Command::Pick { x: 10.0, y: 10.0 }, 0.0);
656 let names = events(&out);
657 assert_eq!(names[0], "Selected");
658 assert!(names.contains(&"Signals".to_string()));
659 }
660}