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