Skip to main content

lumen_engine/node/source/
media_in.rs

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/// Binds an external image or video frame as a GPU texture.
49#[derive(Debug, Clone, lumen_macros::Node)]
50#[node(kind = "media_in", name = "Media In", category = "source")]
51pub struct MediaIn {
52    pub id: NodeId,
53    /// Type of external media source.
54    #[property(kind = "enum", name = "Media type", enum_type = MediaInSourceKind)]
55    pub kind: NodeProperty,
56    /// External media source identifier.
57    #[property(kind = "string", role = "source_id")]
58    pub source: NodeProperty,
59    /// First source frame to include.
60    #[property(kind = "int", min = 0, step = 1)]
61    pub range_start: NodeProperty,
62    /// Last source frame to include.
63    #[property(kind = "int", min = 0, step = 1)]
64    pub range_end: NodeProperty,
65    /// Playback speed multiplier.
66    #[property(kind = "float", step = 0.1)]
67    pub speed: NodeProperty,
68    /// Behavior when playback leaves the source range.
69    #[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}