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.ready()
354 }
355 Command::Seek { frame } => {
356 self.frame = frame;
357 if let Some((_, _)) = self.playing {
358 self.playing = Some((now, frame));
359 }
360 self.show()
361 }
362 Command::Step { delta } => {
363 self.playing = None;
364 self.frame = self.frame.saturating_add_signed(delta);
365 let mut out = vec![self.playback()];
366 out.extend(self.show());
367 out
368 }
369 Command::Play => {
370 if self.frame + 1 >= self.engine.total_frames() {
371 self.frame = 0;
372 }
373 self.playing = Some((now, self.frame));
374 let mut out = vec![self.playback()];
375 out.extend(self.show());
376 out
377 }
378 Command::Pause => {
379 self.playing = None;
380 vec![self.playback()]
381 }
382 Command::SetViewport { w, h, dpr } => {
383 let dpr = if dpr.is_finite() && dpr > 0.0 {
384 dpr
385 } else {
386 1.0
387 };
388 self.viewport = (
389 ((f64::from(w) * dpr) as u32).max(1),
390 ((f64::from(h) * dpr) as u32).max(1),
391 );
392 self.show()
393 }
394 Command::SetMarker { name, time, scene } => {
395 self.edit(MetaEdit::Marker { name, time, scene })
396 }
397 Command::SetEase { name, value } => self.edit(MetaEdit::Ease { name, value }),
398 Command::SetTunable { name, value } => self.edit(MetaEdit::Tunable { name, value }),
399 Command::SetTransition { scene, seconds } => {
400 self.edit(MetaEdit::Transition { scene, seconds })
401 }
402 Command::Pick { x, y } => {
403 self.selected = self.engine.pick(self.frame, x, y);
404 let mut out = vec![Output::Event(Event::Selected {
405 node_id: self.selected,
406 })];
407 out.extend(self.show());
408 out
409 }
410 Command::Select { node_id } => {
411 self.selected = node_id;
412 let mut out = vec![Output::Event(Event::Selected { node_id })];
413 out.extend(self.show());
414 out
415 }
416 Command::Shutdown => Vec::new(),
418 }
419 }
420
421 fn playback(&self) -> Output {
422 Output::Event(Event::Playback {
423 playing: self.playing.is_some(),
424 frame: self.frame,
425 })
426 }
427
428 fn edit(&mut self, edit: MetaEdit) -> Vec<Output> {
429 match self.engine.edit(edit) {
430 Ok(changed) => {
431 self.version += 1;
432 self.cache.clear();
433 let mut out: Vec<Output> = changed
434 .into_iter()
435 .map(|(scene, meta)| Output::Event(Event::MetaChanged { scene, meta }))
436 .collect();
437 out.extend(self.timeline(false));
438 out.extend(self.show());
439 out
440 }
441 Err(e) => vec![Self::error(ErrorKind::Meta, e)],
442 }
443 }
444
445 pub fn tick(&mut self, now: f64) -> Vec<Output> {
448 let Some((start, from)) = self.playing else {
449 return Vec::new();
450 };
451 let total = self.engine.total_frames();
452 let elapsed = ((now - start) * f64::from(self.engine.fps()))
453 .floor()
454 .max(0.0) as u32;
455 let mut target = from.saturating_add(elapsed);
456 if target >= total {
457 if self.looping && total > 0 {
458 target %= total;
459 self.playing = Some((now, target));
460 } else {
461 self.playing = None;
462 self.frame = total.saturating_sub(1);
463 let mut out = vec![self.playback()];
464 out.extend(self.show());
465 return out;
466 }
467 }
468 if target == self.frame {
469 return Vec::new();
470 }
471 self.frame = target;
472 self.show()
473 }
474
475 pub fn next_tick_in(&self, now: f64) -> Option<f64> {
477 let (start, _) = self.playing?;
478 let dt = 1.0 / f64::from(self.engine.fps());
479 let elapsed = now - start;
480 Some(dt - elapsed.rem_euclid(dt))
481 }
482}
483
484pub fn rgba_encoder() -> Encoder {
486 Box::new(|img: &Image| (FrameFormat::Rgba, img.data.clone()))
487}
488
489#[cfg(test)]
490mod tests {
491 use super::*;
492
493 struct Fake {
494 renders: u32,
495 markers: Vec<MarkerInfo>,
496 }
497
498 impl FrameSource for Fake {
499 fn fps(&self) -> u32 {
500 10
501 }
502 fn size(&self) -> (u32, u32) {
503 (200, 100)
504 }
505 fn total_frames(&self) -> u32 {
506 20
507 }
508 fn render(&mut self, frame: u32, size: (u32, u32)) -> Image {
509 self.renders += 1;
510 Image {
511 width: size.0,
512 height: size.1,
513 data: vec![frame as u8; (size.0 * size.1 * 4) as usize],
514 }
515 }
516 }
517
518 impl PreviewEngine for Fake {
519 fn engine_version(&self) -> String {
520 "test".into()
521 }
522 fn scenes(&self) -> Vec<SceneInfo> {
523 vec![SceneInfo {
524 name: "a".into(),
525 start: 0,
526 frames: 20,
527 transition: 0,
528 transition_at: 0,
529 }]
530 }
531 fn markers(&self) -> Vec<MarkerInfo> {
532 self.markers.clone()
533 }
534 fn metas(&self) -> Vec<(String, Meta)> {
535 vec![("a".into(), Meta::default())]
536 }
537 fn edit(&mut self, edit: MetaEdit) -> Result<Vec<(String, Meta)>, String> {
538 match edit {
539 MetaEdit::Marker { name, time, .. } => {
540 self.markers = vec![MarkerInfo {
541 scene: "a".into(),
542 name,
543 time,
544 }];
545 Ok(vec![("a".into(), Meta::default())])
546 }
547 _ => Err("unsupported".into()),
548 }
549 }
550 fn pick(&mut self, _: u32, x: f64, _: f64) -> Option<u32> {
551 (x < 100.0).then_some(1)
552 }
553 fn tree(&mut self, _: u32) -> Vec<NodeInfo> {
554 Vec::new()
555 }
556 fn inspect(&mut self, _: u32, node: u32) -> Option<(Vec<SignalValue>, [f64; 8])> {
557 Some((
558 vec![SignalValue {
559 name: "id".into(),
560 value: node.to_string(),
561 }],
562 [0.0; 8],
563 ))
564 }
565 fn waveform(&mut self, _: usize) -> Option<Vec<f32>> {
566 None
567 }
568 }
569
570 fn session() -> Session<Fake> {
571 Session::new(
572 Fake {
573 renders: 0,
574 markers: Vec::new(),
575 },
576 rgba_encoder(),
577 16,
578 )
579 }
580
581 fn events(out: &[Output]) -> Vec<String> {
582 out.iter()
583 .map(|o| match o {
584 Output::Event(e) => serde_json::to_value(e).unwrap()["type"]
585 .as_str()
586 .unwrap()
587 .to_owned(),
588 Output::Frame(b) => format!("bin{}", FrameHeader::decode(b).unwrap().frame),
589 })
590 .collect()
591 }
592
593 #[test]
594 fn handshake_and_version_mismatch() {
595 let mut s = session();
596 let out = s.handle(
597 Command::Hello {
598 protocol_version: PROTOCOL_VERSION,
599 ui_version: "x".into(),
600 },
601 0.0,
602 );
603 assert_eq!(
604 &events(&out)[..5],
605 ["Ready", "MetaChanged", "Timeline", "Frame", "bin0"]
606 );
607 let bad = s.handle(
608 Command::Hello {
609 protocol_version: 999,
610 ui_version: "x".into(),
611 },
612 0.0,
613 );
614 assert!(matches!(
615 &bad[0],
616 Output::Event(Event::Error {
617 kind: ErrorKind::Incompatible,
618 ..
619 })
620 ));
621 }
622
623 #[test]
624 fn viewport_fits_aspect_and_cache_hits() {
625 let mut s = session();
626 s.handle(
627 Command::SetViewport {
628 w: 50,
629 h: 50,
630 dpr: 2.0,
631 },
632 0.0,
633 );
634 assert_eq!(s.render_size(), (100, 50));
635 let renders = s.engine().renders;
636 s.handle(Command::Seek { frame: 0 }, 0.0);
637 assert_eq!(s.engine().renders, renders, "served from cache");
638 s.handle(Command::Seek { frame: 5 }, 0.0);
639 assert_eq!(s.engine().renders, renders + 1);
640 }
641
642 #[test]
643 fn playback_follows_the_clock_and_loops() {
644 let mut s = session();
645 s.handle(Command::Seek { frame: 0 }, 0.0);
646 s.handle(Command::Play, 10.0);
647 assert!(s.tick(10.05).is_empty(), "half a frame later");
648 assert_eq!(events(&s.tick(10.35))[..2], ["Frame", "bin3"]);
649 s.tick(12.15); assert_eq!(s.frame(), 1);
651 s.handle(Command::Pause, 13.0);
652 assert!(!s.is_playing());
653 }
654
655 #[test]
656 fn edits_invalidate_cache_and_report_meta() {
657 let mut s = session();
658 s.handle(Command::Seek { frame: 2 }, 0.0);
659 let out = s.handle(
660 Command::SetMarker {
661 name: "beat".into(),
662 time: 1.0,
663 scene: None,
664 },
665 0.0,
666 );
667 assert_eq!(
668 &events(&out)[..4],
669 ["MetaChanged", "Timeline", "Frame", "bin2"]
670 );
671 let err = s.handle(
672 Command::SetTunable {
673 name: "x".into(),
674 value: 1.0,
675 },
676 0.0,
677 );
678 assert!(matches!(
679 &err[0],
680 Output::Event(Event::Error {
681 kind: ErrorKind::Meta,
682 ..
683 })
684 ));
685 }
686
687 #[test]
688 fn pick_selects_and_streams_signals() {
689 let mut s = session();
690 let out = s.handle(Command::Pick { x: 10.0, y: 10.0 }, 0.0);
691 let names = events(&out);
692 assert_eq!(names[0], "Selected");
693 assert!(names.contains(&"Signals".to_string()));
694 }
695}