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