mirror of
https://github.com/Xevion/byte-me.git
synced 2025-12-10 04:06:49 -06:00
feat: add bitrate visualization with file analysis
Add comprehensive bitrate data extraction and visualization capabilities: - Implement analyze_files command for file candidacy detection - Add extract_bitrate_data command using ffprobe - Create BitrateData, BitrateFrame, File, and FileCandidacy types with TS bindings - Update App to fetch and display bitrate data from dropped files - Refactor DropOverlay to use new file analysis system - Configure Graph component for packet size visualization - Simplify drag-drop flow to trigger on drop event only
This commit is contained in:
@@ -5,9 +5,10 @@ pub mod strings;
|
||||
|
||||
use ff::extract_streams;
|
||||
use media::{detect_media_type, is_media_file};
|
||||
use models::{StreamResult, StreamResultError};
|
||||
use models::{StreamResult, StreamResultError, File, FileCandidacy, BitrateData, BitrateFrame};
|
||||
use strings::transform_filename;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use tracing::{debug, error, info, instrument, warn};
|
||||
|
||||
// detection, helpers moved to modules above
|
||||
@@ -132,12 +133,174 @@ fn has_streams(paths: Vec<String>) -> Result<Vec<StreamResult>, StreamResultErro
|
||||
results
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[instrument(skip(paths), fields(file_count = paths.len()))]
|
||||
fn analyze_files(paths: Vec<String>) -> Vec<File> {
|
||||
info!(file_count = paths.len(), "Analyzing files for candidacy");
|
||||
|
||||
paths
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, path_str)| {
|
||||
let path = Path::new(&path_str);
|
||||
let filename = path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
|
||||
// Log full path only on first occurrence, then use truncated filename
|
||||
if index == 0 {
|
||||
debug!(full_path = %path_str, filename = %filename, "Processing first file");
|
||||
} else {
|
||||
let truncated_name = transform_filename(&filename, 15);
|
||||
debug!(filename = %truncated_name, "Processing file");
|
||||
}
|
||||
|
||||
// Get file size
|
||||
let size = std::fs::metadata(&path_str)
|
||||
.map(|metadata| metadata.len())
|
||||
.unwrap_or(0) as u32;
|
||||
|
||||
let truncated_name = transform_filename(&filename, 15);
|
||||
debug!(filename = %truncated_name, size = size, "File metadata retrieved");
|
||||
|
||||
// Check if file exists
|
||||
if !path.exists() {
|
||||
let truncated_name = transform_filename(&filename, 15);
|
||||
warn!(filename = %truncated_name, "File does not exist");
|
||||
return File {
|
||||
filename,
|
||||
size,
|
||||
candidacy: FileCandidacy::Error {
|
||||
reason: "File does not exist".to_string(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Check if it's a file (not directory)
|
||||
if !path.is_file() {
|
||||
let truncated_name = transform_filename(&filename, 15);
|
||||
warn!(filename = %truncated_name, "Path is not a file");
|
||||
return File {
|
||||
filename,
|
||||
size,
|
||||
candidacy: FileCandidacy::Error {
|
||||
reason: "Not a file (directory or other)".to_string(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Detect media type using magic numbers and fallback to extensions
|
||||
let media_type = detect_media_type(path);
|
||||
debug!(filename = %truncated_name, media_type = ?media_type, "Media type detected");
|
||||
|
||||
// Check if it's a media file
|
||||
if is_media_file(&media_type) {
|
||||
info!(filename = %truncated_name, media_type = ?media_type, "Valid media file detected");
|
||||
File {
|
||||
filename,
|
||||
size,
|
||||
candidacy: FileCandidacy::Success {
|
||||
file_type: media_type,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
debug!(filename = %truncated_name, media_type = ?media_type, "Non-media file detected");
|
||||
File {
|
||||
filename,
|
||||
size,
|
||||
candidacy: FileCandidacy::Error {
|
||||
reason: format!("Not a media file (detected as {media_type:?})"),
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[instrument(skip(path), fields(path = %path))]
|
||||
fn extract_bitrate_data(path: String) -> Result<BitrateData, String> {
|
||||
info!(path = %path, "Extracting bitrate data from video file");
|
||||
|
||||
let path_obj = Path::new(&path);
|
||||
let filename = path_obj
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
|
||||
// Check if file exists
|
||||
if !path_obj.exists() {
|
||||
error!(filename = %filename, "File does not exist");
|
||||
return Err("File does not exist".to_string());
|
||||
}
|
||||
|
||||
// Run ffprobe to get frame packet sizes
|
||||
// -v quiet: suppress ffprobe info
|
||||
// -select_streams v:0: only first video stream
|
||||
// -show_entries frame=pkt_size: only show packet size
|
||||
// -of csv=p=0: output as CSV without headers
|
||||
info!(filename = %filename, "Running ffprobe to extract frame data");
|
||||
|
||||
let output = Command::new("ffprobe")
|
||||
.args([
|
||||
"-v", "quiet",
|
||||
"-select_streams", "v:0",
|
||||
"-show_entries", "frame=pkt_size",
|
||||
"-of", "csv=p=0",
|
||||
&path
|
||||
])
|
||||
.output()
|
||||
.map_err(|e| {
|
||||
error!(error = %e, "Failed to execute ffprobe");
|
||||
format!("Failed to execute ffprobe: {e}")
|
||||
})?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
error!(stderr = %stderr, "ffprobe command failed");
|
||||
return Err(format!("ffprobe failed: {stderr}"));
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
debug!(line_count = stdout.lines().count(), "Parsing ffprobe output");
|
||||
|
||||
let frames: Vec<BitrateFrame> = stdout
|
||||
.lines()
|
||||
.enumerate()
|
||||
.filter_map(|(index, line)| {
|
||||
line.trim().parse::<u64>().ok().map(|packet_size| BitrateFrame {
|
||||
frame_num: index as u32,
|
||||
packet_size,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
if frames.is_empty() {
|
||||
warn!(filename = %filename, "No frame data extracted");
|
||||
return Err("No frame data could be extracted from file".to_string());
|
||||
}
|
||||
|
||||
info!(
|
||||
filename = %filename,
|
||||
frame_count = frames.len(),
|
||||
"Successfully extracted bitrate data"
|
||||
);
|
||||
|
||||
Ok(BitrateData {
|
||||
id: filename,
|
||||
frames,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
info!("Initializing Tauri application");
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.invoke_handler(tauri::generate_handler![has_streams])
|
||||
.invoke_handler(tauri::generate_handler![has_streams, analyze_files, extract_bitrate_data])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
@@ -51,6 +51,39 @@ pub struct StreamResultError {
|
||||
pub error_type: String,
|
||||
}
|
||||
|
||||
// New types for simplified drop overlay
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, TS)]
|
||||
pub struct File {
|
||||
pub filename: String,
|
||||
pub size: u32,
|
||||
pub candidacy: FileCandidacy,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, TS)]
|
||||
pub enum FileCandidacy {
|
||||
Success {
|
||||
#[serde(rename = "type")]
|
||||
file_type: MediaType,
|
||||
},
|
||||
Error {
|
||||
reason: String,
|
||||
},
|
||||
Loading,
|
||||
}
|
||||
|
||||
// Bitrate visualization types
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, TS)]
|
||||
pub struct BitrateFrame {
|
||||
pub frame_num: u32,
|
||||
pub packet_size: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, TS)]
|
||||
pub struct BitrateData {
|
||||
pub id: String,
|
||||
pub frames: Vec<BitrateFrame>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
@@ -62,5 +95,9 @@ mod tests {
|
||||
StreamResult::export_all_to("../src/bindings").expect("Failed to export bindings");
|
||||
StreamResultError::export_all_to("../src/bindings").expect("Failed to export bindings");
|
||||
MediaType::export_all_to("../src/bindings").expect("Failed to export bindings");
|
||||
File::export_all_to("../src/bindings").expect("Failed to export bindings");
|
||||
FileCandidacy::export_all_to("../src/bindings").expect("Failed to export bindings");
|
||||
BitrateFrame::export_all_to("../src/bindings").expect("Failed to export bindings");
|
||||
BitrateData::export_all_to("../src/bindings").expect("Failed to export bindings");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user