1use std::sync::Arc;
4
5use pixelflow_core::{
6 Clip, ClipFormat, ClipMedia, ClipResolution, ErrorCategory, ErrorCode, Frame, FrameCount,
7 FrameExecutor, FrameRate, FrameRequest, Graph, GraphBuilder, PixelFlowError, Rational,
8 RenderExecutorMap, Result,
9};
10
11pub struct SyntheticClip {
13 graph: Graph,
14 clip: Clip,
15 executors: RenderExecutorMap,
16}
17
18impl SyntheticClip {
19 #[must_use]
21 pub const fn graph(&self) -> &Graph {
22 &self.graph
23 }
24
25 #[must_use]
27 pub const fn clip(&self) -> Clip {
28 self.clip
29 }
30
31 #[must_use]
33 pub fn executors(&self) -> RenderExecutorMap {
34 self.executors.clone()
35 }
36}
37
38struct FrameSequenceExecutor {
39 frames: Arc<[Frame]>,
40}
41
42impl FrameExecutor for FrameSequenceExecutor {
43 fn prepare(&self, request: FrameRequest<'_>) -> Result<Frame> {
44 self.frames
45 .get(request.frame_number())
46 .cloned()
47 .ok_or_else(|| {
48 PixelFlowError::new(
49 ErrorCategory::Core,
50 ErrorCode::new("render.frame_out_of_range"),
51 format!("synthetic frame {} is out of range", request.frame_number()),
52 )
53 })
54 }
55}
56
57pub fn synthetic_clip_from_frames(
59 name: &str,
60 frames: Vec<Frame>,
61 frame_rate: Rational,
62) -> Result<SyntheticClip> {
63 let Some(first) = frames.first() else {
64 return Err(PixelFlowError::new(
65 ErrorCategory::Core,
66 ErrorCode::new("test.empty_synthetic_clip"),
67 "synthetic clip requires at least one frame",
68 ));
69 };
70
71 for frame in &frames {
72 if frame.format() != first.format()
73 || frame.width() != first.width()
74 || frame.height() != first.height()
75 {
76 return Err(PixelFlowError::new(
77 ErrorCategory::Core,
78 ErrorCode::new("test.inconsistent_synthetic_clip"),
79 "all synthetic clip frames must share format and dimensions",
80 ));
81 }
82 }
83
84 let media = ClipMedia::new(
85 ClipFormat::Fixed(first.format().clone()),
86 ClipResolution::Fixed {
87 width: first.width(),
88 height: first.height(),
89 },
90 FrameCount::Finite(frames.len()),
91 FrameRate::Cfr(frame_rate),
92 );
93 let mut builder = GraphBuilder::new();
94 let clip = builder.source(name, media);
95 builder.set_output(clip);
96 let graph = builder.build();
97
98 let mut executors = RenderExecutorMap::new();
99 executors.insert(
100 clip.node_id(),
101 Arc::new(FrameSequenceExecutor {
102 frames: Arc::from(frames),
103 }),
104 );
105
106 Ok(SyntheticClip {
107 graph,
108 clip,
109 executors,
110 })
111}
112
113#[cfg(test)]
114mod tests {
115 use pixelflow_core::{Rational, RenderEngine, RenderOptions, WorkerPoolConfig};
116
117 use crate::{
118 EXACT_GOLDEN_TOLERANCE, assert_plane_u8_near, synthetic_clip_from_frames,
119 synthetic_u8_frame,
120 };
121
122 #[test]
123 fn synthetic_clip_renders_in_memory_frames_without_media_files() {
124 let frames = vec![
125 synthetic_u8_frame("gray8", 2, 1, |_plane, x, _y| {
126 u8::try_from(x).expect("fixture sample fits u8")
127 })
128 .expect("frame 0"),
129 synthetic_u8_frame("gray8", 2, 1, |_plane, x, _y| {
130 u8::try_from(x + 10).expect("fixture sample fits u8")
131 })
132 .expect("frame 1"),
133 ];
134 let clip = synthetic_clip_from_frames(
135 "synthetic",
136 frames,
137 Rational {
138 numerator: 24,
139 denominator: 1,
140 },
141 )
142 .expect("clip should build");
143
144 let rendered = RenderEngine::new(WorkerPoolConfig::new(1))
145 .render_ordered(
146 clip.graph().clone(),
147 clip.executors(),
148 RenderOptions::new(0, None),
149 )
150 .expect("render starts")
151 .collect::<pixelflow_core::Result<Vec<_>>>()
152 .expect("render succeeds");
153
154 assert_eq!(rendered.len(), 2);
155 let first = rendered.first().expect("first frame exists");
156 let second = rendered.get(1).expect("second frame exists");
157 assert_plane_u8_near(first, 0, &[&[0, 1]], EXACT_GOLDEN_TOLERANCE);
158 assert_plane_u8_near(second, 0, &[&[10, 11]], EXACT_GOLDEN_TOLERANCE);
159 }
160
161 #[test]
162 fn synthetic_clip_rejects_empty_inputs() {
163 let Err(error) = synthetic_clip_from_frames(
164 "synthetic",
165 Vec::new(),
166 Rational {
167 numerator: 24,
168 denominator: 1,
169 },
170 ) else {
171 panic!("empty synthetic clip should fail");
172 };
173
174 assert_eq!(error.code().as_str(), "test.empty_synthetic_clip");
175 }
176
177 #[test]
178 fn synthetic_clip_rejects_mismatched_frame_shapes() {
179 let frames = vec![
180 synthetic_u8_frame("gray8", 2, 1, |_plane, _x, _y| 0).expect("frame 0"),
181 synthetic_u8_frame("gray8", 3, 1, |_plane, _x, _y| 1).expect("frame 1"),
182 ];
183
184 let Err(error) = synthetic_clip_from_frames(
185 "synthetic",
186 frames,
187 Rational {
188 numerator: 24,
189 denominator: 1,
190 },
191 ) else {
192 panic!("mismatched synthetic clip should fail");
193 };
194
195 assert_eq!(error.code().as_str(), "test.inconsistent_synthetic_clip");
196 }
197}