lumen-server 0.2.2

Embeddable render service primitives and local HTTP server for Lumen.
Documentation
mod encoder;
#[cfg(all(target_os = "linux", feature = "cuda", feature = "vulkan"))]
mod frame_timing;
mod gpu;
mod media;

use std::path::PathBuf;

use lumen_engine::composition::Composition;

pub use gpu::render_project_mp4;

#[derive(Debug, Clone)]
pub struct RenderError {
    pub code: &'static str,
    pub message: String,
    pub retryable: bool,
}

impl std::fmt::Display for RenderError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}: {}", self.code, self.message)
    }
}

impl std::error::Error for RenderError {}

#[derive(Debug, Clone)]
pub struct RenderProgress {
    pub stage: &'static str,
    pub frame: u32,
    pub total_frames: u32,
    pub ratio: f32,
}

#[derive(Debug, Clone, Default)]
pub struct RenderOptions {
    pub media_root: Option<PathBuf>,
    pub verbose_debug: bool,
    pub video_encoder: Option<String>,
}

#[derive(Debug, Clone)]
pub struct ProjectInfo {
    pub width: u32,
    pub height: u32,
    pub duration_frames: u32,
}

pub struct ProjectBundle {
    pub project: ProjectInfo,
    pub composition: Composition,
}

pub fn convert_project_payload(payload: &serde_json::Value) -> Result<ProjectBundle, RenderError> {
    let project = serde_json::to_string(payload).map_err(|err| RenderError {
        code: "invalid_project_payload",
        message: err.to_string(),
        retryable: false,
    })?;

    let composition = lumen_engine::json::parse(&project).map_err(|err| RenderError {
        code: "invalid_project_payload",
        message: err.to_string(),
        retryable: false,
    })?;
    Ok(ProjectBundle {
        project: ProjectInfo {
            width: composition.render_settings.width,
            height: composition.render_settings.height,
            duration_frames: composition.timeline.duration_frames,
        },
        composition,
    })
}

#[cfg(test)]
mod tests {
    use serde_json::json;

    use super::convert_project_payload;

    #[test]
    fn convert_current_composition_preserves_duration_frames() {
        let payload = json!({
            "timeline": {
                "fps": 24,
                "duration_frames": 120
            },
            "render_settings": {
                "width": 64,
                "height": 64,
                "background_color": [0, 0, 0, 255]
            },
            "nodes": [
                {
                    "id": 1,
                    "type": "background",
                    "properties": {
                        "paint": [255, 255, 255, 255],
                        "width": 64,
                        "height": 64
                    }
                },
                {
                    "id": 2,
                    "type": "media_output",
                    "properties": {}
                }
            ],
            "connections": [
                {
                    "from_node": 1,
                    "from_port": "output",
                    "to_node": 2,
                    "to_port": "source"
                }
            ]
        });

        let bundle = convert_project_payload(&payload).expect("project bundle");
        assert_eq!(bundle.project.duration_frames, 120);
    }
}