1use std::ops::Range;
2
3use crate::error::{MediaError, RenderError};
4use crate::node::{NodeId, NodeProperty};
5
6use crate::gpu::{
7 BoundFrame, CompiledOutput, FrameBindContext, FrameBinding, GpuCompileNode, GpuFrameBindNode,
8 MediaTextureKey, RasterHandle, RasterMetadata,
9};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, lumen_macros::NodeEnum)]
12pub enum LoopMode {
13 Clamp,
14 Repeat,
15 PingPong,
16}
17
18impl LoopMode {
19 pub fn from_int(value: i64) -> Self {
20 match value {
21 1 => Self::Repeat,
22 2 => Self::PingPong,
23 _ => Self::Clamp,
24 }
25 }
26}
27
28#[cfg(any(feature = "json", feature = "metadata"))]
29#[derive(lumen_macros::NodeEnum)]
30pub enum MediaInSourceKind {
31 Image = 0,
32 Video = 1,
33}
34
35#[derive(Debug, Clone)]
36pub enum MediaInKind {
37 Image {
38 image_id: String,
39 },
40 Video {
41 stream_id: String,
42 range: Option<Range<u32>>,
43 speed: f32,
44 loop_mode: LoopMode,
45 },
46}
47
48#[derive(Debug, Clone, lumen_macros::Node)]
50#[node(kind = "media_in", name = "Media In", category = "source")]
51pub struct MediaIn {
52 pub id: NodeId,
53 #[property(kind = "enum", name = "Media type", enum_type = MediaInSourceKind)]
55 pub kind: NodeProperty,
56 #[property(kind = "string", role = "source_id")]
58 pub source: NodeProperty,
59 #[property(kind = "int", min = 0, step = 1)]
61 pub range_start: NodeProperty,
62 #[property(kind = "int", min = 0, step = 1)]
64 pub range_end: NodeProperty,
65 #[property(kind = "float", step = 0.1)]
67 pub speed: NodeProperty,
68 #[property(kind = "enum", enum_type = LoopMode)]
70 pub loop_mode: NodeProperty,
71}
72
73impl Default for MediaIn {
74 fn default() -> Self {
75 Self {
76 id: NodeId::new(0),
77 kind: NodeProperty::Int(0),
78 source: NodeProperty::String(String::new()),
79 range_start: NodeProperty::Int(0),
80 range_end: NodeProperty::Int(0),
81 speed: NodeProperty::Float(1.0),
82 loop_mode: NodeProperty::Int(0),
83 }
84 }
85}
86
87pub fn resolve_for_context(
88 media_in: &MediaIn,
89 ctx: &crate::expr::ExpressionContext<'_>,
90) -> crate::Result<MediaInKind> {
91 let kind = media_in.kind.resolve_int(media_in.id, "kind", ctx)?;
92 let source = media_in.source.resolve_string(media_in.id, "source", ctx)?;
93 let range_start = media_in
94 .range_start
95 .resolve_int(media_in.id, "range_start", ctx)?;
96 let range_end = media_in
97 .range_end
98 .resolve_int(media_in.id, "range_end", ctx)?;
99 let speed = media_in.speed.resolve_float(media_in.id, "speed", ctx)? as f32;
100 let loop_mode = LoopMode::from_int(media_in.loop_mode.resolve_int(
101 media_in.id,
102 "loop_mode",
103 ctx,
104 )?);
105
106 if kind == 1 {
107 Ok(MediaInKind::Video {
108 stream_id: source,
109 range: resolve_range(range_start, range_end),
110 speed,
111 loop_mode,
112 })
113 } else {
114 Ok(MediaInKind::Image { image_id: source })
115 }
116}
117
118pub fn resolve_range(start: i64, end: i64) -> Option<Range<u32>> {
119 if end <= start {
120 return None;
121 }
122 Some(start.max(0) as u32..end.max(0) as u32)
123}
124
125pub fn map_to_source_frame(
126 frame: u32,
127 composition_fps: f32,
128 source_fps: f32,
129 frame_count: u32,
130 range: Option<&Range<u32>>,
131 speed: f32,
132 loop_mode: LoopMode,
133) -> Option<u32> {
134 if frame_count == 0 {
135 return None;
136 }
137 let start = range.map(|range| range.start).unwrap_or(0);
138 let end = range
139 .map(|range| range.end)
140 .unwrap_or(frame_count)
141 .min(frame_count);
142 if end <= start {
143 return None;
144 }
145 let span = end - start;
146 let comp_fps = composition_fps.max(1.0);
147 let media_fps = source_fps.max(comp_fps);
148 let relative =
149 (((frame as f64 / comp_fps as f64) * media_fps as f64) * speed.abs() as f64).floor() as u32;
150 let forward = speed >= 0.0;
151 let mapped = match loop_mode {
152 LoopMode::Clamp => {
153 if forward {
154 start + relative.min(span.saturating_sub(1))
155 } else {
156 end - 1 - relative.min(span.saturating_sub(1))
157 }
158 }
159 LoopMode::Repeat => {
160 let offset = relative % span;
161 start + if forward { offset } else { span - 1 - offset }
162 }
163 LoopMode::PingPong => {
164 let period = span.saturating_mul(2).saturating_sub(2).max(1);
165 let offset = relative % period;
166 let offset = if offset < span {
167 offset
168 } else {
169 period - offset
170 };
171 start + if forward { offset } else { span - 1 - offset }
172 }
173 };
174 (mapped < frame_count).then_some(mapped)
175}
176
177impl GpuCompileNode for MediaIn {
178 fn compile_gpu(
179 &self,
180 ctx: &mut crate::gpu::CompileContext<'_>,
181 port: &crate::node::PortRef,
182 ) -> crate::Result<CompiledOutput> {
183 if port.port != "output" {
184 return Err(ctx.missing_output(self.id, &port.port));
185 }
186
187 let size = self.native_size(ctx).unwrap_or_else(|| {
188 lumen_gpu::Size::new(
189 ctx.composition().render_settings.width.max(1),
190 ctx.composition().render_settings.height.max(1),
191 )
192 });
193 let texture = ctx.builder_mut().texture_for(
194 lumen_gpu::NodeKey(self.id.0),
195 Some(format!("media-in:{}:frame", self.id.0)),
196 lumen_gpu::TextureDesc::sampled(size, lumen_gpu::wgpu::TextureFormat::Rgba8Unorm),
197 );
198 ctx.builder_mut().param(
199 lumen_gpu::ParamKey {
200 owner: lumen_gpu::NodeKey(self.id.0),
201 slot: 0,
202 },
203 lumen_gpu::ParamTarget::Texture(texture),
204 );
205 ctx.push_frame_binding(FrameBinding::MediaInput {
206 node_id: self.id,
207 source: self.source.clone(),
208 texture,
209 size,
210 });
211
212 Ok(CompiledOutput::Raster(RasterHandle {
213 texture,
214 domain: lumen_gpu::TextureDomain::full_frame(size),
215 metadata: RasterMetadata::default(),
216 }))
217 }
218}
219
220impl MediaIn {
221 fn native_size(&self, ctx: &crate::gpu::CompileContext<'_>) -> Option<lumen_gpu::Size> {
222 let media = ctx.media()?;
223 let kind = resolve_for_context(self, &ctx.expr_context(self.id, "source")).ok()?;
224 match kind {
225 MediaInKind::Image { image_id } => {
226 let metadata = media.get_image_resolver(&image_id)?.metadata();
227 Some(lumen_gpu::Size::new(
228 metadata.width.max(1),
229 metadata.height.max(1),
230 ))
231 }
232 MediaInKind::Video { stream_id, .. } => {
233 let metadata = media.get_video_resolver(&stream_id)?.metadata();
234 Some(lumen_gpu::Size::new(
235 metadata.width.max(1),
236 metadata.height.max(1),
237 ))
238 }
239 }
240 }
241}
242
243impl GpuFrameBindNode for MediaIn {
244 fn bind_gpu_frame(
245 &self,
246 ctx: &FrameBindContext<'_>,
247 binding: &FrameBinding,
248 bound: &mut BoundFrame,
249 ) -> crate::Result<()> {
250 let FrameBinding::MediaInput {
251 node_id,
252 texture,
253 size,
254 ..
255 } = binding
256 else {
257 return Ok(());
258 };
259 let media = ctx.media().ok_or_else(|| RenderError::NodeEvaluation {
260 frame: ctx.frame(),
261 node_id: *node_id,
262 node_kind: "MediaIn",
263 details: "media store is required for media input nodes".to_string(),
264 })?;
265 let kind = resolve_for_context(self, &ctx.expr_context(*node_id, "source"))?;
266 let (frame, key_source, key_frame) = match kind {
267 MediaInKind::Image { image_id } => media
268 .get_image_resolver(&image_id)
269 .ok_or_else(|| MediaError::SourceNotFound {
270 media_source: image_id.clone(),
271 })?
272 .frame()
273 .map(|frame| (frame, image_id, None))?,
274 MediaInKind::Video {
275 stream_id,
276 range,
277 speed,
278 loop_mode,
279 } => {
280 let resolver = media.get_video_resolver(&stream_id).ok_or_else(|| {
281 MediaError::SourceNotFound {
282 media_source: stream_id.clone(),
283 }
284 })?;
285 let metadata = resolver.metadata();
286 let source_frame = map_to_source_frame(
287 ctx.frame(),
288 ctx.expr_context(*node_id, "source").fps,
289 metadata.fps,
290 metadata.frame_count,
291 range.as_ref(),
292 speed,
293 loop_mode,
294 )
295 .ok_or_else(|| MediaError::FrameOutOfRange {
296 media_source: stream_id.clone(),
297 frame: ctx.frame(),
298 frame_count: metadata.frame_count,
299 })?;
300 resolver
301 .frame(source_frame)
302 .map(|frame| (frame, stream_id, Some(source_frame)))?
303 }
304 };
305 bound.use_media_texture(
306 *texture,
307 MediaTextureKey {
308 source: key_source,
309 frame: key_frame,
310 width: size.width,
311 height: size.height,
312 },
313 frame,
314 *size,
315 );
316 Ok(())
317 }
318}
319
320#[cfg(test)]
321mod tests {
322 use super::{LoopMode, map_to_source_frame};
323
324 #[test]
325 fn maps_negative_media_speed_in_reverse() {
326 assert_eq!(
327 map_to_source_frame(0, 30.0, 30.0, 10, Some(&(2..7)), -1.0, LoopMode::Clamp),
328 Some(6)
329 );
330 assert_eq!(
331 map_to_source_frame(3, 30.0, 30.0, 10, Some(&(2..7)), -1.0, LoopMode::Clamp),
332 Some(3)
333 );
334 assert_eq!(
335 map_to_source_frame(99, 30.0, 30.0, 10, Some(&(2..7)), -1.0, LoopMode::Clamp),
336 Some(2)
337 );
338 }
339
340 #[test]
341 fn maps_ping_pong_loop_mode() {
342 let frames = (0..8)
343 .map(|frame| map_to_source_frame(frame, 30.0, 30.0, 4, None, 1.0, LoopMode::PingPong))
344 .collect::<Vec<_>>();
345 assert_eq!(
346 frames,
347 vec![
348 Some(0),
349 Some(1),
350 Some(2),
351 Some(3),
352 Some(2),
353 Some(1),
354 Some(0),
355 Some(1)
356 ]
357 );
358 }
359
360 #[test]
361 fn maps_negative_ping_pong_loop_mode() {
362 let frames = (0..8)
363 .map(|frame| map_to_source_frame(frame, 30.0, 30.0, 4, None, -1.0, LoopMode::PingPong))
364 .collect::<Vec<_>>();
365 assert_eq!(
366 frames,
367 vec![
368 Some(3),
369 Some(2),
370 Some(1),
371 Some(0),
372 Some(1),
373 Some(2),
374 Some(3),
375 Some(2)
376 ]
377 );
378 }
379}