233 lines
6.3 KiB
Rust
233 lines
6.3 KiB
Rust
//! Recording output configuration and results.
|
|
|
|
use std::path::PathBuf;
|
|
|
|
use chrono::{DateTime, Duration, Utc};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// Output configuration for recordings.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct OutputConfig {
|
|
/// Output container format.
|
|
pub container: String,
|
|
/// Output file path pattern.
|
|
pub path_pattern: String,
|
|
/// Maximum file size before splitting (MB). 0 = no limit.
|
|
pub max_size_mb: u32,
|
|
/// Maximum duration before splitting (seconds). 0 = no limit.
|
|
pub max_duration_secs: u32,
|
|
/// Whether to use fragmented MP4 for better crash recovery.
|
|
pub fragmented: bool,
|
|
}
|
|
|
|
impl Default for OutputConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
container: "mp4".to_string(),
|
|
path_pattern: "{date}_{time}".to_string(),
|
|
max_size_mb: 0,
|
|
max_duration_secs: 0,
|
|
fragmented: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Recording output handle.
|
|
#[derive(Debug, Clone)]
|
|
pub struct RecordingOutput {
|
|
/// Output file path.
|
|
pub path: PathBuf,
|
|
/// Game ID if available.
|
|
pub game_id: Option<u64>,
|
|
/// Champion name if available.
|
|
pub champion: Option<String>,
|
|
}
|
|
|
|
/// Result of a completed recording.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct RecordingResult {
|
|
/// Output file path.
|
|
pub path: PathBuf,
|
|
/// Game ID if available.
|
|
pub game_id: Option<u64>,
|
|
/// Champion name if available.
|
|
pub champion: Option<String>,
|
|
/// Recording start time.
|
|
pub start_time: DateTime<Utc>,
|
|
/// Recording end time.
|
|
pub end_time: DateTime<Utc>,
|
|
/// Recording duration.
|
|
// #[serde(with = "chrono::serde::seconds")]
|
|
pub duration: Duration,
|
|
/// Recording statistics (if available).
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub stats: Option<RecordingStats>,
|
|
}
|
|
|
|
impl RecordingResult {
|
|
/// Get the file size in bytes.
|
|
pub fn file_size(&self) -> Option<u64> {
|
|
std::fs::metadata(&self.path).ok().map(|m| m.len())
|
|
}
|
|
|
|
/// Get the file size in a human-readable format.
|
|
pub fn file_size_human(&self) -> String {
|
|
let bytes = self.file_size().unwrap_or(0);
|
|
let mb = bytes as f64 / (1024.0 * 1024.0);
|
|
format!("{:.2} MB", mb)
|
|
}
|
|
|
|
/// Get the duration in a human-readable format.
|
|
pub fn duration_human(&self) -> String {
|
|
let total_secs = self.duration.num_seconds();
|
|
let hours = total_secs / 3600;
|
|
let mins = (total_secs % 3600) / 60;
|
|
let secs = total_secs % 60;
|
|
|
|
if hours > 0 {
|
|
format!("{}h {}m {}s", hours, mins, secs)
|
|
} else if mins > 0 {
|
|
format!("{}m {}s", mins, secs)
|
|
} else {
|
|
format!("{}s", secs)
|
|
}
|
|
}
|
|
|
|
/// Check if the recording file exists.
|
|
pub fn exists(&self) -> bool {
|
|
self.path.exists()
|
|
}
|
|
|
|
/// Delete the recording file.
|
|
pub fn delete(&self) -> std::io::Result<()> {
|
|
std::fs::remove_file(&self.path)
|
|
}
|
|
}
|
|
|
|
/// Output format type.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum OutputFormat {
|
|
/// MP4 container.
|
|
Mp4,
|
|
/// MKV container.
|
|
Mkv,
|
|
/// MOV container.
|
|
Mov,
|
|
/// FLV container.
|
|
Flv,
|
|
}
|
|
|
|
impl OutputFormat {
|
|
/// Get the file extension.
|
|
pub fn extension(&self) -> &'static str {
|
|
match self {
|
|
OutputFormat::Mp4 => "mp4",
|
|
OutputFormat::Mkv => "mkv",
|
|
OutputFormat::Mov => "mov",
|
|
OutputFormat::Flv => "flv",
|
|
}
|
|
}
|
|
|
|
/// Get the OBS output ID.
|
|
pub fn obs_output_id(&self) -> &'static str {
|
|
match self {
|
|
OutputFormat::Mp4 => "ffmpeg_muxer",
|
|
OutputFormat::Mkv => "ffmpeg_muxer",
|
|
OutputFormat::Mov => "ffmpeg_muxer",
|
|
OutputFormat::Flv => "ffmpeg_muxer",
|
|
}
|
|
}
|
|
|
|
/// Get the FFmpeg format name.
|
|
pub fn ffmpeg_format(&self) -> &'static str {
|
|
match self {
|
|
OutputFormat::Mp4 => "mp4",
|
|
OutputFormat::Mkv => "matroska",
|
|
OutputFormat::Mov => "mov",
|
|
OutputFormat::Flv => "flv",
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<&str> for OutputFormat {
|
|
fn from(s: &str) -> Self {
|
|
match s.to_lowercase().as_str() {
|
|
"mp4" => OutputFormat::Mp4,
|
|
"mkv" => OutputFormat::Mkv,
|
|
"mov" => OutputFormat::Mov,
|
|
"flv" => OutputFormat::Flv,
|
|
_ => OutputFormat::Mp4,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Recording statistics.
|
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
pub struct RecordingStats {
|
|
/// Total frames recorded.
|
|
pub frames_recorded: u64,
|
|
/// Frames dropped due to encoding lag.
|
|
pub frames_dropped: u64,
|
|
/// Average frame time in milliseconds.
|
|
pub avg_frame_time_ms: f64,
|
|
/// Current bitrate in kbps.
|
|
pub current_bitrate: u32,
|
|
/// Recording duration in seconds.
|
|
pub duration_secs: f64,
|
|
}
|
|
|
|
impl RecordingStats {
|
|
/// Calculate the drop rate percentage.
|
|
pub fn drop_rate(&self) -> f64 {
|
|
if self.frames_recorded == 0 {
|
|
return 0.0;
|
|
}
|
|
(self.frames_dropped as f64 / self.frames_recorded as f64) * 100.0
|
|
}
|
|
|
|
/// Check if the recording is healthy (low drop rate).
|
|
pub fn is_healthy(&self) -> bool {
|
|
self.drop_rate() < 5.0 // Less than 5% dropped frames
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_output_format_from_str() {
|
|
assert_eq!(OutputFormat::from("mp4"), OutputFormat::Mp4);
|
|
assert_eq!(OutputFormat::from("MKV"), OutputFormat::Mkv);
|
|
assert_eq!(OutputFormat::from("unknown"), OutputFormat::Mp4);
|
|
}
|
|
|
|
#[test]
|
|
fn test_recording_result_duration_human() {
|
|
let result = RecordingResult {
|
|
path: PathBuf::from("/tmp/test.mp4"),
|
|
game_id: Some(12345),
|
|
champion: Some("Ahri".to_string()),
|
|
start_time: Utc::now(),
|
|
end_time: Utc::now() + Duration::seconds(125),
|
|
duration: Duration::seconds(125),
|
|
stats: None,
|
|
};
|
|
|
|
assert_eq!(result.duration_human(), "2m 5s");
|
|
}
|
|
|
|
#[test]
|
|
fn test_recording_stats_drop_rate() {
|
|
let stats = RecordingStats {
|
|
frames_recorded: 1000,
|
|
frames_dropped: 40,
|
|
..Default::default()
|
|
};
|
|
|
|
assert_eq!(stats.drop_rate(), 4.0);
|
|
assert!(stats.is_healthy());
|
|
}
|
|
}
|