use std::collections::HashMap;
use std::sync::{Arc, Mutex as SyncMutex};
use tauri::command;
use tokio::sync::RwLock;
use crate::platform::PlatformCamera;
use crate::recording::{Recorder, RecordingConfig, RecordingQuality, RecordingStats};
use crate::types::CameraFormat;
lazy_static::lazy_static! {
static ref RECORDER_REGISTRY: Arc<RwLock<HashMap<String, Arc<SyncMutex<RecordingSession>>>>> =
Arc::new(RwLock::new(HashMap::new()));
}
struct RecordingSession {
recorder: Option<Recorder>,
camera: Arc<SyncMutex<PlatformCamera>>,
is_running: bool,
}
#[allow(clippy::too_many_arguments)]
#[command]
pub async fn start_recording(
device_id: Option<String>,
output_path: String,
width: u32,
height: u32,
fps: f64,
quality: Option<String>,
title: Option<String>,
#[cfg(feature = "audio")] audio_device_id: Option<String>,
) -> Result<String, String> {
let camera_id = device_id.unwrap_or_else(|| "0".to_string());
#[cfg(feature = "audio")]
{
if let Some(ref audio_id) = audio_device_id {
log::info!(
"Starting recording from camera {} with audio {} to {}",
camera_id,
audio_id,
output_path
);
} else {
log::info!(
"Starting recording from camera {} (no audio) to {}",
camera_id,
output_path
);
}
}
#[cfg(not(feature = "audio"))]
log::info!(
"Starting recording from camera {} to {}",
camera_id,
output_path
);
let recording_quality = match quality.as_deref() {
Some("low") | Some("720p") => Some(RecordingQuality::Low),
Some("medium") | Some("1080p") => Some(RecordingQuality::Medium),
Some("high") | Some("4k") => Some(RecordingQuality::High),
_ => None,
};
let mut config = if let Some(q) = recording_quality {
RecordingConfig::from_quality_with_fps(q, fps)
} else {
RecordingConfig::new(width, height, fps)
};
if let Some(t) = title {
config = config.with_title(t);
}
#[cfg(feature = "audio")]
if let Some(audio_id) = audio_device_id {
config = config.with_audio(crate::recording::AudioConfig {
device_id: if audio_id == "default" {
None
} else {
Some(audio_id)
},
sample_rate: 48000,
channels: 2,
bitrate: 128_000,
});
}
let camera = super::capture::get_or_create_camera(
camera_id.clone(),
CameraFormat::new(config.width, config.height, fps as f32),
)
.await
.map_err(|e| format!("Failed to initialize camera: {}", e))?;
{
let mut cam = camera
.lock()
.map_err(|_| "Camera mutex poisoned".to_string())?;
cam.start_stream()
.map_err(|e| format!("Failed to start camera stream: {}", e))?;
}
let recorder = Recorder::new(&output_path, config)
.map_err(|e| format!("Failed to create recorder: {}", e))?;
let session_id = format!("rec_{}", chrono::Utc::now().timestamp_millis());
let session = RecordingSession {
recorder: Some(recorder),
camera,
is_running: true,
};
{
let mut registry = RECORDER_REGISTRY.write().await;
registry.insert(session_id.clone(), Arc::new(SyncMutex::new(session)));
}
log::info!("Recording started: session {}", session_id);
Ok(session_id)
}
#[command]
pub async fn record_frame(session_id: String) -> Result<u64, String> {
let session_arc = {
let registry = RECORDER_REGISTRY.read().await;
registry
.get(&session_id)
.cloned()
.ok_or_else(|| format!("Recording session not found: {}", session_id))?
};
let mut session = session_arc
.lock()
.map_err(|_| "Mutex poisoned".to_string())?;
if !session.is_running {
return Err("Recording is not running".to_string());
}
let frame = {
let mut camera = session
.camera
.lock()
.map_err(|_| "Mutex poisoned".to_string())?;
camera
.capture_frame()
.map_err(|e| format!("Failed to capture frame: {}", e))?
};
session
.recorder
.as_mut()
.ok_or_else(|| "Recorder not available".to_string())?
.write_frame(&frame)
.map_err(|e| format!("Failed to write frame: {}", e))?;
Ok(session.recorder.as_ref().unwrap().frame_count())
}
#[command]
pub async fn stop_recording(session_id: String) -> Result<RecordingStats, String> {
let session_arc = {
let mut registry = RECORDER_REGISTRY.write().await;
registry
.remove(&session_id)
.ok_or_else(|| format!("Recording session not found: {}", session_id))?
};
let mut session = session_arc
.lock()
.map_err(|_| "Mutex poisoned".to_string())?;
{
let mut camera = session
.camera
.lock()
.map_err(|_| "Camera mutex poisoned".to_string())?;
let _ = camera.stop_stream();
}
let stats = session
.recorder
.take()
.ok_or_else(|| "Recorder already taken".to_string())?
.finish()
.map_err(|e| format!("Failed to finalize recording: {}", e))?;
log::info!(
"Recording stopped: {} frames, {:.2}s, {} bytes",
stats.video_frames,
stats.duration_secs,
stats.bytes_written
);
Ok(stats)
}
#[command]
pub async fn get_recording_status(session_id: String) -> Result<RecordingStatus, String> {
let session_arc = {
let registry = RECORDER_REGISTRY.read().await;
registry
.get(&session_id)
.cloned()
.ok_or_else(|| format!("Recording session not found: {}", session_id))?
};
let session = session_arc
.lock()
.map_err(|_| "Mutex poisoned".to_string())?;
let recorder = session
.recorder
.as_ref()
.ok_or_else(|| "Recorder not available".to_string())?;
#[cfg(feature = "audio")]
let audio_status = if recorder.audio_enabled() {
Some(AudioStatus {
enabled: true,
failed: recorder.audio_failed(),
})
} else {
None
};
Ok(RecordingStatus {
session_id,
is_running: session.is_running,
frame_count: recorder.frame_count(),
dropped_frames: recorder.dropped_frames(),
duration_secs: recorder.duration(),
#[cfg(feature = "audio")]
audio_status,
})
}
#[command]
pub async fn list_recording_sessions() -> Result<Vec<String>, String> {
let registry = RECORDER_REGISTRY.read().await;
Ok(registry.keys().cloned().collect())
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RecordingStatus {
pub session_id: String,
pub is_running: bool,
pub frame_count: u64,
pub dropped_frames: u64,
pub duration_secs: f64,
#[cfg(feature = "audio")]
pub audio_status: Option<AudioStatus>,
}
#[cfg(feature = "audio")]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AudioStatus {
pub enabled: bool,
pub failed: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_recording_status_serialization() {
let status = RecordingStatus {
session_id: "test_123".to_string(),
is_running: true,
frame_count: 100,
dropped_frames: 2,
duration_secs: 3.33,
#[cfg(feature = "audio")]
audio_status: Some(AudioStatus {
enabled: true,
failed: false,
}),
};
let json = serde_json::to_string(&status).unwrap();
assert!(json.contains("test_123"));
assert!(json.contains("100"));
#[cfg(feature = "audio")]
{
assert!(json.contains("audioStatus"));
}
}
}