//! Export settings and types for audio and video export //! //! This module contains platform-agnostic export settings that can be used //! across different frontends (native, web, etc.). The actual export implementation //! is in the platform-specific code (e.g., lightningbeam-editor). use serde::{Deserialize, Serialize}; /// Audio export formats #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum AudioFormat { /// WAV - Uncompressed audio (large files, best quality) Wav, /// FLAC - Lossless compressed audio (smaller than WAV, same quality) Flac, /// MP3 - Lossy compressed audio (widely compatible) Mp3, /// AAC - Lossy compressed audio (better quality than MP3 at same bitrate) Aac, } impl AudioFormat { /// Get the file extension for this format pub fn extension(&self) -> &'static str { match self { AudioFormat::Wav => "wav", AudioFormat::Flac => "flac", AudioFormat::Mp3 => "mp3", AudioFormat::Aac => "m4a", } } /// Get a human-readable name for this format pub fn name(&self) -> &'static str { match self { AudioFormat::Wav => "WAV (Uncompressed)", AudioFormat::Flac => "FLAC (Lossless)", AudioFormat::Mp3 => "MP3", AudioFormat::Aac => "AAC", } } /// Check if this format supports bit depth settings pub fn supports_bit_depth(&self) -> bool { matches!(self, AudioFormat::Wav | AudioFormat::Flac) } /// Check if this format uses bitrate settings (lossy formats) pub fn uses_bitrate(&self) -> bool { matches!(self, AudioFormat::Mp3 | AudioFormat::Aac) } } /// Audio export settings #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AudioExportSettings { /// Output format pub format: AudioFormat, /// Sample rate in Hz (e.g., 44100, 48000) pub sample_rate: u32, /// Number of channels (1 = mono, 2 = stereo) pub channels: u32, /// Bit depth for lossless formats (16 or 24) /// Only used for WAV and FLAC pub bit_depth: u16, /// Bitrate in kbps for lossy formats (e.g., 128, 192, 256, 320) /// Only used for MP3 and AAC pub bitrate_kbps: u32, /// Start time in seconds pub start_time: f64, /// End time in seconds pub end_time: f64, } impl Default for AudioExportSettings { fn default() -> Self { Self { format: AudioFormat::Wav, sample_rate: 48000, channels: 2, bit_depth: 24, bitrate_kbps: 320, start_time: 0.0, end_time: 60.0, } } } impl AudioExportSettings { /// Create high quality WAV export settings pub fn high_quality_wav() -> Self { Self { format: AudioFormat::Wav, sample_rate: 48000, channels: 2, bit_depth: 24, ..Default::default() } } /// Create high quality FLAC export settings pub fn high_quality_flac() -> Self { Self { format: AudioFormat::Flac, sample_rate: 48000, channels: 2, bit_depth: 24, ..Default::default() } } /// Create high quality AAC export settings pub fn high_quality_aac() -> Self { Self { format: AudioFormat::Aac, sample_rate: 48000, channels: 2, bitrate_kbps: 320, ..Default::default() } } /// Create high quality MP3 export settings pub fn high_quality_mp3() -> Self { Self { format: AudioFormat::Mp3, sample_rate: 44100, channels: 2, bitrate_kbps: 320, ..Default::default() } } /// Create standard quality AAC export settings pub fn standard_aac() -> Self { Self { format: AudioFormat::Aac, sample_rate: 44100, channels: 2, bitrate_kbps: 256, ..Default::default() } } /// Create standard quality MP3 export settings pub fn standard_mp3() -> Self { Self { format: AudioFormat::Mp3, sample_rate: 44100, channels: 2, bitrate_kbps: 192, ..Default::default() } } /// Create podcast-optimized AAC settings (mono, lower bitrate) pub fn podcast_aac() -> Self { Self { format: AudioFormat::Aac, sample_rate: 44100, channels: 1, bitrate_kbps: 128, ..Default::default() } } /// Create podcast-optimized MP3 settings (mono, lower bitrate) pub fn podcast_mp3() -> Self { Self { format: AudioFormat::Mp3, sample_rate: 44100, channels: 1, bitrate_kbps: 128, ..Default::default() } } /// Validate the settings pub fn validate(&self) -> Result<(), String> { // Validate sample rate if self.sample_rate == 0 { return Err("Sample rate must be greater than 0".to_string()); } // Validate channels if self.channels == 0 || self.channels > 2 { return Err("Channels must be 1 (mono) or 2 (stereo)".to_string()); } // Validate bit depth for lossless formats if self.format.supports_bit_depth() { if self.bit_depth != 16 && self.bit_depth != 24 { return Err("Bit depth must be 16 or 24".to_string()); } } // Validate bitrate for lossy formats if self.format.uses_bitrate() { if self.bitrate_kbps == 0 { return Err("Bitrate must be greater than 0".to_string()); } } // Validate time range if self.start_time < 0.0 { return Err("Start time cannot be negative".to_string()); } if self.end_time <= self.start_time { return Err("End time must be greater than start time".to_string()); } Ok(()) } /// Get the duration in seconds pub fn duration(&self) -> f64 { self.end_time - self.start_time } } /// Video codec types #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum VideoCodec { /// H.264 (AVC) - Most widely compatible H264, /// H.265 (HEVC) - Better compression than H.264 H265, /// VP8 - WebM codec VP8, /// VP9 - Improved WebM codec VP9, /// ProRes 422 - Professional editing codec ProRes422, } impl VideoCodec { /// Get the typical container format for this codec pub fn container_format(&self) -> &'static str { match self { VideoCodec::H264 | VideoCodec::H265 => "mp4", VideoCodec::VP8 | VideoCodec::VP9 => "webm", VideoCodec::ProRes422 => "mov", } } /// Get a human-readable name for this codec pub fn name(&self) -> &'static str { match self { VideoCodec::H264 => "H.264 (MP4)", VideoCodec::H265 => "H.265 (MP4)", VideoCodec::VP8 => "VP8 (WebM)", VideoCodec::VP9 => "VP9 (WebM)", VideoCodec::ProRes422 => "ProRes 422 (MOV)", } } } /// Video quality presets #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum VideoQuality { /// Low quality - ~2 Mbps Low, /// Medium quality - ~5 Mbps Medium, /// High quality - ~10 Mbps High, /// Very high quality - ~20 Mbps VeryHigh, /// Custom bitrate in kbps Custom(u32), } impl VideoQuality { /// Get the bitrate in kbps for this quality preset pub fn bitrate_kbps(&self) -> u32 { match self { VideoQuality::Low => 2000, VideoQuality::Medium => 5000, VideoQuality::High => 10000, VideoQuality::VeryHigh => 20000, VideoQuality::Custom(bitrate) => *bitrate, } } /// Get a human-readable name pub fn name(&self) -> String { match self { VideoQuality::Low => "Low (2 Mbps)".to_string(), VideoQuality::Medium => "Medium (5 Mbps)".to_string(), VideoQuality::High => "High (10 Mbps)".to_string(), VideoQuality::VeryHigh => "Very High (20 Mbps)".to_string(), VideoQuality::Custom(bitrate) => format!("Custom ({} kbps)", bitrate), } } } /// Video export settings #[derive(Debug, Clone, Serialize, Deserialize)] pub struct VideoExportSettings { /// Video codec pub codec: VideoCodec, /// Output width in pixels (None = use document width) pub width: Option, /// Output height in pixels (None = use document height) pub height: Option, /// Frame rate (fps) pub framerate: f64, /// Video quality pub quality: VideoQuality, /// Audio settings (None = no audio) pub audio: Option, /// Start time in seconds pub start_time: f64, /// End time in seconds pub end_time: f64, } impl Default for VideoExportSettings { fn default() -> Self { Self { codec: VideoCodec::H264, width: None, height: None, framerate: 60.0, quality: VideoQuality::High, audio: Some(AudioExportSettings::high_quality_aac()), start_time: 0.0, end_time: 60.0, } } } impl VideoExportSettings { /// Validate the settings pub fn validate(&self) -> Result<(), String> { // Validate dimensions if provided if let Some(width) = self.width { if width == 0 { return Err("Width must be greater than 0".to_string()); } } if let Some(height) = self.height { if height == 0 { return Err("Height must be greater than 0".to_string()); } } // Validate framerate if self.framerate <= 0.0 { return Err("Framerate must be greater than 0".to_string()); } // Validate time range if self.start_time < 0.0 { return Err("Start time cannot be negative".to_string()); } if self.end_time <= self.start_time { return Err("End time must be greater than start time".to_string()); } // Validate audio settings if present if let Some(audio) = &self.audio { audio.validate()?; } Ok(()) } /// Get the duration in seconds pub fn duration(&self) -> f64 { self.end_time - self.start_time } /// Calculate the total number of frames pub fn total_frames(&self) -> usize { (self.duration() * self.framerate).ceil() as usize } } // ── Image export ───────────────────────────────────────────────────────────── /// Image export formats (single-frame still image) #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum ImageFormat { Png, Jpeg, WebP, } impl ImageFormat { pub fn name(self) -> &'static str { match self { Self::Png => "PNG", Self::Jpeg => "JPEG", Self::WebP => "WebP" } } pub fn extension(self) -> &'static str { match self { Self::Png => "png", Self::Jpeg => "jpg", Self::WebP => "webp" } } /// Whether quality (1–100) applies to this format. pub fn has_quality(self) -> bool { matches!(self, Self::Jpeg | Self::WebP) } } /// Settings for exporting a single frame as a still image. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ImageExportSettings { pub format: ImageFormat, /// Document time (seconds) of the frame to render. pub time: f64, /// Override width; None = use document canvas width. pub width: Option, /// Override height; None = use document canvas height. pub height: Option, /// Encode quality 1–100 (JPEG / WebP only). pub quality: u8, /// Preserve the alpha channel in the output (respect document background alpha). /// When false, the image is composited onto an opaque background before encoding. /// Only meaningful for formats that support alpha (PNG, WebP). pub allow_transparency: bool, } impl Default for ImageExportSettings { fn default() -> Self { Self { format: ImageFormat::Png, time: 0.0, width: None, height: None, quality: 90, allow_transparency: false } } } impl ImageExportSettings { pub fn validate(&self) -> Result<(), String> { if let Some(w) = self.width { if w == 0 { return Err("Width must be > 0".into()); } } if let Some(h) = self.height { if h == 0 { return Err("Height must be > 0".into()); } } Ok(()) } } /// Progress updates during export #[derive(Debug, Clone)] pub enum ExportProgress { /// Export started Started { /// Total number of frames (0 for audio-only) total_frames: usize, }, /// A frame was rendered (video only) FrameRendered { /// Current frame number frame: usize, /// Total frames total: usize, }, /// Audio rendering completed AudioRendered, /// Finalizing the export (writing file, cleanup) Finalizing, /// Export completed successfully Complete { /// Path to the exported file output_path: std::path::PathBuf, }, /// Export failed Error { /// Error message message: String, }, } impl ExportProgress { /// Get a human-readable status message pub fn status_message(&self) -> String { match self { ExportProgress::Started { total_frames } => { if *total_frames > 0 { format!("Starting export ({} frames)...", total_frames) } else { "Starting audio export...".to_string() } } ExportProgress::FrameRendered { frame, total } => { format!("Rendering frame {} of {}...", frame, total) } ExportProgress::AudioRendered => "Audio rendered successfully".to_string(), ExportProgress::Finalizing => "Finalizing export...".to_string(), ExportProgress::Complete { output_path } => { format!("Export complete: {}", output_path.display()) } ExportProgress::Error { message } => { format!("Export failed: {}", message) } } } /// Get progress as a percentage (0.0 to 1.0) pub fn progress_percentage(&self) -> Option { match self { ExportProgress::Started { .. } => Some(0.0), ExportProgress::FrameRendered { frame, total } => { Some(*frame as f32 / *total as f32) } ExportProgress::AudioRendered => Some(0.9), ExportProgress::Finalizing => Some(0.95), ExportProgress::Complete { .. } => Some(1.0), ExportProgress::Error { .. } => None, } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_audio_format_extension() { assert_eq!(AudioFormat::Wav.extension(), "wav"); assert_eq!(AudioFormat::Flac.extension(), "flac"); assert_eq!(AudioFormat::Mp3.extension(), "mp3"); assert_eq!(AudioFormat::Aac.extension(), "m4a"); } #[test] fn test_audio_format_capabilities() { assert!(AudioFormat::Wav.supports_bit_depth()); assert!(AudioFormat::Flac.supports_bit_depth()); assert!(!AudioFormat::Mp3.supports_bit_depth()); assert!(!AudioFormat::Aac.supports_bit_depth()); assert!(AudioFormat::Mp3.uses_bitrate()); assert!(AudioFormat::Aac.uses_bitrate()); assert!(!AudioFormat::Wav.uses_bitrate()); assert!(!AudioFormat::Flac.uses_bitrate()); } #[test] fn test_audio_export_settings_validation() { let mut settings = AudioExportSettings::default(); assert!(settings.validate().is_ok()); // Test invalid sample rate settings.sample_rate = 0; assert!(settings.validate().is_err()); settings.sample_rate = 48000; // Test invalid channels settings.channels = 0; assert!(settings.validate().is_err()); settings.channels = 3; assert!(settings.validate().is_err()); settings.channels = 2; // Test invalid bit depth for WAV settings.format = AudioFormat::Wav; settings.bit_depth = 32; assert!(settings.validate().is_err()); settings.bit_depth = 24; assert!(settings.validate().is_ok()); // Test invalid time range settings.start_time = -1.0; assert!(settings.validate().is_err()); settings.start_time = 0.0; settings.end_time = 0.0; assert!(settings.validate().is_err()); settings.end_time = 60.0; assert!(settings.validate().is_ok()); } #[test] fn test_audio_presets() { let wav = AudioExportSettings::high_quality_wav(); assert_eq!(wav.format, AudioFormat::Wav); assert_eq!(wav.sample_rate, 48000); assert_eq!(wav.bit_depth, 24); assert_eq!(wav.channels, 2); let flac = AudioExportSettings::high_quality_flac(); assert_eq!(flac.format, AudioFormat::Flac); assert_eq!(flac.sample_rate, 48000); assert_eq!(flac.bit_depth, 24); let aac = AudioExportSettings::high_quality_aac(); assert_eq!(aac.format, AudioFormat::Aac); assert_eq!(aac.bitrate_kbps, 320); let mp3 = AudioExportSettings::podcast_mp3(); assert_eq!(mp3.format, AudioFormat::Mp3); assert_eq!(mp3.channels, 1); assert_eq!(mp3.bitrate_kbps, 128); } #[test] fn test_video_codec_container() { assert_eq!(VideoCodec::H264.container_format(), "mp4"); assert_eq!(VideoCodec::VP9.container_format(), "webm"); assert_eq!(VideoCodec::ProRes422.container_format(), "mov"); } #[test] fn test_video_quality_bitrate() { assert_eq!(VideoQuality::Low.bitrate_kbps(), 2000); assert_eq!(VideoQuality::High.bitrate_kbps(), 10000); assert_eq!(VideoQuality::Custom(15000).bitrate_kbps(), 15000); } #[test] fn test_video_export_total_frames() { let settings = VideoExportSettings { framerate: 30.0, start_time: 0.0, end_time: 10.0, ..Default::default() }; assert_eq!(settings.total_frames(), 300); } #[test] fn test_export_progress_percentage() { let progress = ExportProgress::FrameRendered { frame: 50, total: 100 }; assert_eq!(progress.progress_percentage(), Some(0.5)); let complete = ExportProgress::Complete { output_path: std::path::PathBuf::from("test.mp4"), }; assert_eq!(complete.progress_percentage(), Some(1.0)); } }