Lightningbeam/lightningbeam-ui/lightningbeam-core/src/export.rs

642 lines
19 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 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<u32>,
/// Output height in pixels (None = use document height)
pub height: Option<u32>,
/// Frame rate (fps)
pub framerate: f64,
/// Video quality
pub quality: VideoQuality,
/// Audio settings (None = no audio)
pub audio: Option<AudioExportSettings>,
/// 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 (1100) 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<u32>,
/// Override height; None = use document canvas height.
pub height: Option<u32>,
/// Encode quality 1100 (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<f32> {
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));
}
}