record-daemon: initial commit
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
//! 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,
|
||||
}
|
||||
|
||||
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),
|
||||
};
|
||||
|
||||
assert_eq!(result.duration_human(), "2m 5s");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_recording_stats_drop_rate() {
|
||||
let stats = RecordingStats {
|
||||
frames_recorded: 1000,
|
||||
frames_dropped: 50,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(stats.drop_rate(), 5.0);
|
||||
assert!(stats.is_healthy());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user