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