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