From f28791c2c908f3ebc3f565b2e7d1780976723065 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Fri, 7 Nov 2025 03:23:59 -0500 Subject: [PATCH] remove slower methods --- src-tauri/Cargo.lock | 57 ----- src-tauri/Cargo.toml | 4 - src-tauri/src/frame_streamer.rs | 4 - src-tauri/src/lib.rs | 12 - src-tauri/src/video.rs | 399 +------------------------------- src-tauri/src/video_server.rs | 203 ---------------- src/frame-receiver.js | 4 - 7 files changed, 3 insertions(+), 680 deletions(-) delete mode 100644 src-tauri/src/video_server.rs diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index f6ea37c..204e35a 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -158,12 +158,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" -[[package]] -name = "ascii" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" - [[package]] name = "ashpd" version = "0.10.2" @@ -552,12 +546,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "chunked_transfer" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901" - [[package]] name = "clang-sys" version = "1.8.1" @@ -599,12 +587,6 @@ dependencies = [ "objc", ] -[[package]] -name = "color_quant" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" - [[package]] name = "colorchoice" version = "1.0.4" @@ -1969,12 +1951,6 @@ version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - [[package]] name = "hyper" version = "1.5.2" @@ -2191,19 +2167,6 @@ dependencies = [ "icu_properties", ] -[[package]] -name = "image" -version = "0.24.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" -dependencies = [ - "bytemuck", - "byteorder", - "color_quant", - "jpeg-decoder", - "num-traits", -] - [[package]] name = "indexmap" version = "1.9.3" @@ -2374,12 +2337,6 @@ dependencies = [ "libc", ] -[[package]] -name = "jpeg-decoder" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07" - [[package]] name = "js-sys" version = "0.3.77" @@ -2510,7 +2467,6 @@ dependencies = [ "daw-backend", "env_logger", "ffmpeg-next", - "image", "log", "lru", "rtrb", @@ -2521,7 +2477,6 @@ dependencies = [ "tauri-plugin-dialog", "tauri-plugin-fs", "tauri-plugin-shell", - "tiny_http", "tungstenite", ] @@ -5047,18 +5002,6 @@ dependencies = [ "time-core", ] -[[package]] -name = "tiny_http" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389915df6413a2e74fb181895f933386023c71110878cd0825588928e64cdc82" -dependencies = [ - "ascii", - "chunked_transfer", - "httpdate", - "log", -] - [[package]] name = "tinystr" version = "0.7.6" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index e639876..b1e818a 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -35,10 +35,6 @@ rtrb = "0.3" # Video decoding ffmpeg-next = "7.0" lru = "0.12" -image = { version = "0.24", default-features = false, features = ["jpeg"] } - -# HTTP server for video streaming -tiny_http = "0.12" # WebSocket for frame streaming (disable default features to remove tracing, but keep handshake) tungstenite = { version = "0.20", default-features = false, features = ["handshake"] } diff --git a/src-tauri/src/frame_streamer.rs b/src-tauri/src/frame_streamer.rs index ef947d0..4105f95 100644 --- a/src-tauri/src/frame_streamer.rs +++ b/src-tauri/src/frame_streamer.rs @@ -62,10 +62,6 @@ impl FrameStreamer { pub fn send_frame(&self, pool_index: usize, timestamp: f64, width: u32, height: u32, rgba_data: &[u8]) { let mut clients = self.clients.lock().unwrap(); - // Debug: Log input dimensions and first few RGBA bytes - eprintln!("[Frame Streamer SEND] pool={}, {}x{} pixels, RGBA input len={}, first 20 RGBA bytes: {:?}", - pool_index, width, height, rgba_data.len(), &rgba_data[..20.min(rgba_data.len())]); - // Build frame message (rgba_data is already in RGBA format from decoder) let mut frame_msg = Vec::with_capacity(16 + rgba_data.len()); frame_msg.extend_from_slice(&(pool_index as u32).to_le_bytes()); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 8890a02..52f0cd9 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -5,7 +5,6 @@ use tauri::{AppHandle, Manager, Url, WebviewUrl, WebviewWindowBuilder}; mod audio; mod video; -mod video_server; mod frame_streamer; @@ -139,11 +138,6 @@ pub fn run() { .filter_level(log::LevelFilter::Error) .init(); - // Initialize video HTTP server - let video_server = video_server::VideoServer::new() - .expect("Failed to start video server"); - eprintln!("[App] Video server started on port {}", video_server.port()); - // Initialize WebSocket frame streamer let frame_streamer = frame_streamer::FrameStreamer::new() .expect("Failed to start frame streamer"); @@ -153,7 +147,6 @@ pub fn run() { .manage(Mutex::new(AppState::default())) .manage(Arc::new(Mutex::new(audio::AudioState::default()))) .manage(Arc::new(Mutex::new(video::VideoState::default()))) - .manage(Arc::new(Mutex::new(video_server))) .manage(Arc::new(Mutex::new(frame_streamer))) .setup(|app| { #[cfg(any(windows, target_os = "linux"))] // Windows/Linux needs different handling from macOS @@ -281,14 +274,9 @@ pub fn run() { audio::audio_serialize_track_graph, audio::audio_load_track_graph, video::video_load_file, - video::video_get_frame, - video::video_get_frames_batch, video::video_stream_frame, video::video_set_cache_size, video::video_get_pool_info, - video::video_ipc_benchmark, - video::video_get_transcode_status, - video::video_allow_asset, ]) // .manage(window_counter) .build(tauri::generate_context!()) diff --git a/src-tauri/src/video.rs b/src-tauri/src/video.rs index 9ecd841..e183de8 100644 --- a/src-tauri/src/video.rs +++ b/src-tauri/src/video.rs @@ -19,10 +19,6 @@ pub struct VideoFileMetadata { pub audio_sample_rate: Option, pub audio_channels: Option, pub audio_waveform: Option>, - pub codec_name: String, - pub is_browser_compatible: bool, - pub http_url: Option, // HTTP URL to stream video (if compatible or transcode complete) - pub transcoding: bool, // True if currently transcoding } struct VideoDecoder { @@ -244,24 +240,10 @@ impl VideoDecoder { } } -use std::collections::HashMap; -use std::path::PathBuf; - -#[derive(Clone)] -pub struct TranscodeJob { - pub pool_index: usize, - pub input_path: String, - pub output_path: String, - pub http_url: Option, // HTTP URL when transcode completes - pub progress: f32, // 0.0 to 1.0 - pub completed: bool, -} - pub struct VideoState { pool: Vec>>, next_pool_index: usize, cache_size: usize, - transcode_jobs: Arc>>, // pool_index -> job } impl Default for VideoState { @@ -270,7 +252,6 @@ impl Default for VideoState { pool: Vec::new(), next_pool_index: 0, cache_size: 20, // Default cache size - transcode_jobs: Arc::new(Mutex::new(HashMap::new())), } } } @@ -279,7 +260,6 @@ impl Default for VideoState { pub async fn video_load_file( video_state: tauri::State<'_, Arc>>, audio_state: tauri::State<'_, Arc>>, - video_server: tauri::State<'_, Arc>>, path: String, ) -> Result { eprintln!("[Video] Loading file: {}", path); @@ -408,45 +388,13 @@ pub async fn video_load_file( (None, None, None, None, None) }; - // Detect video codec - let video_stream = input.streams() - .best(ffmpeg::media::Type::Video) - .ok_or("No video stream found")?; - - let codec_id = video_stream.parameters().id(); - let codec_name = ffmpeg::codec::Id::name(&codec_id).to_string(); - - // Check if codec is browser-compatible (can play directly) - // Browsers support: H.264/AVC, VP8, VP9, AV1 (limited) - let is_browser_compatible = matches!( - codec_id, - ffmpeg::codec::Id::H264 | - ffmpeg::codec::Id::VP8 | - ffmpeg::codec::Id::VP9 | - ffmpeg::codec::Id::AV1 - ); - - eprintln!("[Video Codec] {} - Browser compatible: {}", codec_name, is_browser_compatible); - - // Create video decoder with max dimensions for playback (800x600) - // This scales down high-res videos to reduce data transfer + // Create video decoder with max dimensions for playback (1920x1080) + // This scales videos to reduce data transfer over WebSocket let mut video_state_guard = video_state.lock().unwrap(); let pool_index = video_state_guard.next_pool_index; video_state_guard.next_pool_index += 1; - let decoder = VideoDecoder::new(path.clone(), video_state_guard.cache_size, Some(800), Some(600))?; - - // Add file to HTTP server if browser-compatible - let http_url = if is_browser_compatible { - let server = video_server.lock().unwrap(); - let url_path = format!("/video/{}", pool_index); - server.add_file(url_path.clone(), PathBuf::from(&path)); - let http_url = server.get_url(&url_path); - eprintln!("[Video] Browser-compatible, serving at: {}", http_url); - Some(http_url) - } else { - None - }; + let decoder = VideoDecoder::new(path.clone(), video_state_guard.cache_size, Some(1920), Some(1080))?; let metadata = VideoFileMetadata { pool_index, @@ -460,152 +408,13 @@ pub async fn video_load_file( audio_sample_rate, audio_channels, audio_waveform, - codec_name, - is_browser_compatible, - http_url, - transcoding: !is_browser_compatible, }; video_state_guard.pool.push(Arc::new(Mutex::new(decoder))); - // Start background transcoding if not browser-compatible - if !is_browser_compatible { - eprintln!("[Video Transcode] Starting background transcode for pool_index {}", pool_index); - let jobs = video_state_guard.transcode_jobs.clone(); - let input_path = path.clone(); - let pool_idx = pool_index; - let server = video_server.inner().clone(); - - tauri::async_runtime::spawn(async move { - if let Err(e) = start_transcode(jobs, pool_idx, input_path, server).await { - eprintln!("[Video Transcode] Failed: {}", e); - } - }); - } - Ok(metadata) } -// Background transcode to WebM/VP9 for browser compatibility -async fn start_transcode( - jobs: Arc>>, - pool_index: usize, - input_path: String, - video_server: Arc>, -) -> Result<(), String> { - use std::process::Command; - - // Generate output path in system cache directory - let cache_dir = std::env::temp_dir().join("lightningbeam_transcoded"); - std::fs::create_dir_all(&cache_dir).map_err(|e| e.to_string())?; - - let input_file = PathBuf::from(&input_path); - let file_stem = input_file.file_stem() - .ok_or("Invalid input path")? - .to_string_lossy(); - let output_path = cache_dir.join(format!("{}_{}.webm", file_stem, pool_index)); - - // Create job entry - { - let mut jobs_guard = jobs.lock().unwrap(); - jobs_guard.insert(pool_index, TranscodeJob { - pool_index, - input_path: input_path.clone(), - output_path: output_path.to_string_lossy().to_string(), - http_url: None, - progress: 0.0, - completed: false, - }); - } - - eprintln!("[Video Transcode] Output: {}", output_path.display()); - - // Run FFmpeg transcode command - // Using VP9 codec with CRF 30 (good quality/size balance) and fast encoding - let output = Command::new("ffmpeg") - .args(&[ - "-i", &input_path, - "-c:v", "libvpx-vp9", // VP9 video codec - "-crf", "30", // Quality (lower = better, 23-32 recommended) - "-b:v", "0", // Use CRF mode - "-threads", "4", // Use 4 threads - "-row-mt", "1", // Enable row-based multithreading - "-speed", "4", // Encoding speed (0=slowest/best, 4=good balance) - "-c:a", "libopus", // Opus audio codec (best for WebM) - "-b:a", "128k", // Audio bitrate - "-y", // Overwrite output - output_path.to_str().ok_or("Invalid output path")?, - ]) - .output() - .map_err(|e| format!("Failed to spawn ffmpeg: {}", e))?; - - if output.status.success() { - eprintln!("[Video Transcode] Completed: {}", output_path.display()); - - // Add transcoded file to HTTP server - let server = video_server.lock().unwrap(); - let url_path = format!("/video/{}", pool_index); - server.add_file(url_path.clone(), output_path.clone()); - let http_url = server.get_url(&url_path); - eprintln!("[Video Transcode] Serving at: {}", http_url); - drop(server); - - // Mark as completed and store HTTP URL - let mut jobs_guard = jobs.lock().unwrap(); - if let Some(job) = jobs_guard.get_mut(&pool_index) { - job.progress = 1.0; - job.completed = true; - job.http_url = Some(http_url); - } - eprintln!("[Video Transcode] Job completed for pool_index {}", pool_index); - Ok(()) - } else { - let stderr = String::from_utf8_lossy(&output.stderr); - eprintln!("[Video Transcode] FFmpeg error: {}", stderr); - Err(format!("FFmpeg failed: {}", stderr)) - } -} - -// Get transcode status for a pool index -#[tauri::command] -pub async fn video_get_transcode_status( - video_state: tauri::State<'_, Arc>>, - pool_index: usize, -) -> Result)>, String> { - let state = video_state.lock().unwrap(); - let jobs = state.transcode_jobs.lock().unwrap(); - - if let Some(job) = jobs.get(&pool_index) { - Ok(Some((job.output_path.clone(), job.progress, job.completed, job.http_url.clone()))) - } else { - Ok(None) - } -} - -// Add a video file to asset protocol scope so browser can access it -#[tauri::command] -pub async fn video_allow_asset( - app: tauri::AppHandle, - path: String, -) -> Result<(), String> { - use tauri_plugin_fs::FsExt; - - let file_path = PathBuf::from(&path); - - // Add to FS scope - let fs_scope = app.fs_scope(); - fs_scope.allow_file(&file_path) - .map_err(|e| format!("Failed to allow file in fs scope: {}", e))?; - - // Add to asset protocol scope - let asset_scope = app.asset_protocol_scope(); - asset_scope.allow_file(&file_path) - .map_err(|e| format!("Failed to allow file in asset scope: {}", e))?; - - eprintln!("[Video] Added to asset scope: {}", path); - Ok(()) -} - fn generate_waveform(audio_data: &[f32], channels: u32, target_peaks: usize) -> Vec { let total_samples = audio_data.len(); let samples_per_channel = total_samples / channels as usize; @@ -648,91 +457,6 @@ fn generate_waveform(audio_data: &[f32], channels: u32, target_peaks: usize) -> waveform } -#[tauri::command] -pub async fn video_get_frame( - state: tauri::State<'_, Arc>>, - pool_index: usize, - timestamp: f64, - use_jpeg: bool, - channel: tauri::ipc::Channel, -) -> Result<(), String> { - use std::time::Instant; - - let t_total_start = Instant::now(); - - let t_lock_start = Instant::now(); - let video_state = state.lock().unwrap(); - - let decoder = video_state.pool.get(pool_index) - .ok_or("Invalid pool index")? - .clone(); - - drop(video_state); - - let mut decoder = decoder.lock().unwrap(); - let t_lock_end = Instant::now(); - - let t_decode_start = Instant::now(); - let frame_data = decoder.get_frame(timestamp)?; - let t_decode_end = Instant::now(); - - let t_compress_start = Instant::now(); - let data_to_send = if use_jpeg { - // Get frame dimensions from decoder - let width = decoder.output_width; - let height = decoder.output_height; - - // Create image from raw RGBA data - let img = RgbaImage::from_raw(width, height, frame_data) - .ok_or("Failed to create image from frame data")?; - - // Convert RGBA to RGB (JPEG doesn't support alpha) - let rgb_img = image::DynamicImage::ImageRgba8(img).to_rgb8(); - - // Encode to JPEG with quality 85 (good balance of size/quality) - let mut jpeg_data = Vec::new(); - let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut jpeg_data, 85); - encoder.encode( - rgb_img.as_raw(), - rgb_img.width(), - rgb_img.height(), - image::ColorType::Rgb8 - ).map_err(|e| format!("JPEG encoding failed: {}", e))?; - - jpeg_data - } else { - frame_data - }; - let t_compress_end = Instant::now(); - - // Drop decoder lock before sending to avoid blocking - drop(decoder); - - let t_send_start = Instant::now(); - // Send binary data through channel (bypasses JSON serialization) - // InvokeResponseBody::Raw sends raw binary data without JSON encoding - channel.send(tauri::ipc::InvokeResponseBody::Raw(data_to_send.clone())) - .map_err(|e| format!("Channel send error: {}", e))?; - let t_send_end = Instant::now(); - - let t_total_end = Instant::now(); - - // Detailed profiling - let lock_time = t_lock_end.duration_since(t_lock_start).as_micros(); - let decode_time = t_decode_end.duration_since(t_decode_start).as_micros(); - let compress_time = t_compress_end.duration_since(t_compress_start).as_micros(); - let send_time = t_send_end.duration_since(t_send_start).as_micros(); - let total_time = t_total_end.duration_since(t_total_start).as_micros(); - - let size_kb = data_to_send.len() / 1024; - let mode = if use_jpeg { "JPEG" } else { "RAW" }; - - eprintln!("[Video Profile {}] Size: {}KB | Lock: {}μs | Decode: {}μs | Compress: {}μs | Send: {}μs | Total: {}μs", - mode, size_kb, lock_time, decode_time, compress_time, send_time, total_time); - - Ok(()) -} - #[tauri::command] pub async fn video_set_cache_size( state: tauri::State<'_, Arc>>, @@ -760,123 +484,6 @@ pub async fn video_get_pool_info( )) } -// Benchmark command to test IPC performance with various payload sizes -#[tauri::command] -pub async fn video_ipc_benchmark( - size_bytes: usize, - channel: tauri::ipc::Channel, -) -> Result<(), String> { - use std::time::Instant; - - let t_start = Instant::now(); - - // Create dummy data of requested size - let data = vec![0u8; size_bytes]; - - let t_after_alloc = Instant::now(); - - // Send through channel - channel.send(tauri::ipc::InvokeResponseBody::Raw(data)) - .map_err(|e| format!("Channel send error: {}", e))?; - - let t_after_send = Instant::now(); - - let alloc_time = t_after_alloc.duration_since(t_start).as_micros(); - let send_time = t_after_send.duration_since(t_after_alloc).as_micros(); - let total_time = t_after_send.duration_since(t_start).as_micros(); - - eprintln!("[IPC Benchmark Rust] Size: {}KB | Alloc: {}μs | Send: {}μs | Total: {}μs", - size_bytes / 1024, alloc_time, send_time, total_time); - - Ok(()) -} - -// Batch frame request - get multiple frames in one IPC call -#[tauri::command] -pub async fn video_get_frames_batch( - state: tauri::State<'_, Arc>>, - pool_index: usize, - timestamps: Vec, - use_jpeg: bool, - channel: tauri::ipc::Channel, -) -> Result<(), String> { - use std::time::Instant; - - let t_total_start = Instant::now(); - - let video_state = state.lock().unwrap(); - let decoder = video_state.pool.get(pool_index) - .ok_or("Invalid pool index")? - .clone(); - drop(video_state); - - let mut decoder = decoder.lock().unwrap(); - - // Decode all frames - let mut all_frames = Vec::new(); - let mut total_decode_time = 0u128; - let mut total_compress_time = 0u128; - - for timestamp in ×tamps { - let t_decode_start = Instant::now(); - let frame_data = decoder.get_frame(*timestamp)?; - let t_decode_end = Instant::now(); - total_decode_time += t_decode_end.duration_since(t_decode_start).as_micros(); - - let t_compress_start = Instant::now(); - let data = if use_jpeg { - let width = decoder.output_width; - let height = decoder.output_height; - let img = RgbaImage::from_raw(width, height, frame_data) - .ok_or("Failed to create image from frame data")?; - let rgb_img = image::DynamicImage::ImageRgba8(img).to_rgb8(); - let mut jpeg_data = Vec::new(); - let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut jpeg_data, 85); - encoder.encode( - rgb_img.as_raw(), - rgb_img.width(), - rgb_img.height(), - image::ColorType::Rgb8 - ).map_err(|e| format!("JPEG encoding failed: {}", e))?; - jpeg_data - } else { - frame_data - }; - let t_compress_end = Instant::now(); - total_compress_time += t_compress_end.duration_since(t_compress_start).as_micros(); - - all_frames.push(data); - } - - drop(decoder); - - // Pack all frames into one buffer with metadata - // Format: [frame_count: u32][frame1_size: u32][frame1_data...][frame2_size: u32][frame2_data...] - let mut packed_data = Vec::new(); - packed_data.extend_from_slice(&(all_frames.len() as u32).to_le_bytes()); - - for frame in &all_frames { - packed_data.extend_from_slice(&(frame.len() as u32).to_le_bytes()); - packed_data.extend_from_slice(frame); - } - - let total_size_kb = packed_data.len() / 1024; - - let t_send_start = Instant::now(); - channel.send(tauri::ipc::InvokeResponseBody::Raw(packed_data)) - .map_err(|e| format!("Channel send error: {}", e))?; - let t_send_end = Instant::now(); - - let send_time = t_send_end.duration_since(t_send_start).as_micros(); - let total_time = t_send_end.duration_since(t_total_start).as_micros(); - - let mode = if use_jpeg { "JPEG" } else { "RAW" }; - eprintln!("[Video Batch {}] Frames: {} | Size: {}KB | Decode: {}μs | Compress: {}μs | Send: {}μs | Total: {}μs", - mode, timestamps.len(), total_size_kb, total_decode_time, total_compress_time, send_time, total_time); - - Ok(()) -} - /// Stream a decoded video frame over WebSocket (zero-copy performance testing) #[tauri::command] pub async fn video_stream_frame( diff --git a/src-tauri/src/video_server.rs b/src-tauri/src/video_server.rs deleted file mode 100644 index 7258221..0000000 --- a/src-tauri/src/video_server.rs +++ /dev/null @@ -1,203 +0,0 @@ -use std::sync::{Arc, Mutex}; -use std::collections::HashMap; -use std::fs::File; -use std::io::{Read, Seek, SeekFrom}; -use std::path::PathBuf; -use tiny_http::{Server, Response, Request, Header, StatusCode}; - -pub struct VideoServer { - port: u16, - allowed_files: Arc>>, // URL path -> file path -} - -impl VideoServer { - pub fn new() -> Result { - // Bind to localhost on a random available port - let server = Server::http("127.0.0.1:0") - .map_err(|e| format!("Failed to create HTTP server: {}", e))?; - - let port = match server.server_addr() { - tiny_http::ListenAddr::IP(addr) => addr.port(), - _ => return Err("Unexpected server address type".to_string()), - }; - let allowed_files = Arc::new(Mutex::new(HashMap::new())); - - eprintln!("[Video Server] Started on port {}", port); - - // Spawn server thread - let files = allowed_files.clone(); - std::thread::spawn(move || { - for request in server.incoming_requests() { - handle_request(request, &files); - } - }); - - Ok(Self { - port, - allowed_files, - }) - } - - pub fn port(&self) -> u16 { - self.port - } - - pub fn add_file(&self, url_path: String, file_path: PathBuf) { - eprintln!("[Video Server] Adding file: {} -> {:?}", url_path, file_path); - let mut files = self.allowed_files.lock().unwrap(); - files.insert(url_path, file_path); - } - - pub fn get_url(&self, url_path: &str) -> String { - format!("http://127.0.0.1:{}{}", self.port, url_path) - } -} - -fn handle_request(request: Request, allowed_files: &Arc>>) { - let url = request.url(); - let method = request.method(); - - eprintln!("[Video Server] {} {}", method, url); - - // Get file path - let files = allowed_files.lock().unwrap(); - let file_path = match files.get(url) { - Some(path) => path.clone(), - None => { - eprintln!("[Video Server] File not found: {}", url); - let response = Response::from_string("Not Found") - .with_status_code(StatusCode(404)); - let _ = request.respond(response); - return; - } - }; - drop(files); - - // Open file - let mut file = match File::open(&file_path) { - Ok(f) => f, - Err(e) => { - eprintln!("[Video Server] Failed to open file: {}", e); - let response = Response::from_string("Internal Server Error") - .with_status_code(StatusCode(500)); - let _ = request.respond(response); - return; - } - }; - - // Get file size - let file_size = match file.metadata() { - Ok(meta) => meta.len(), - Err(e) => { - eprintln!("[Video Server] Failed to get file metadata: {}", e); - let response = Response::from_string("Internal Server Error") - .with_status_code(StatusCode(500)); - let _ = request.respond(response); - return; - } - }; - - // Check for Range header - convert to owned String to avoid borrow issues - let range_header = request.headers().iter() - .find(|h| h.field.equiv("Range")) - .map(|h| h.value.as_str().to_string()); - - match range_header { - Some(range) if range.starts_with("bytes=") => { - // Parse range request - eprintln!("[Video Server] Range request: {}", range); - handle_range_request(request, file, file_size, &range, &file_path); - } - _ => { - // Serve entire file - eprintln!("[Video Server] Full file request"); - handle_full_request(request, file, file_size, &file_path); - } - } -} - -fn handle_range_request( - request: Request, - mut file: File, - file_size: u64, - range: &str, - file_path: &PathBuf, -) { - // Parse "bytes=start-end" - let range = range.trim_start_matches("bytes="); - let parts: Vec<&str> = range.split('-').collect(); - - let start = parts[0].parse::().unwrap_or(0); - let end = if parts.len() > 1 && !parts[1].is_empty() { - parts[1].parse::().unwrap_or(file_size - 1) - } else { - file_size - 1 - }; - - let length = end - start + 1; - - // Seek to start position - if let Err(e) = file.seek(SeekFrom::Start(start)) { - eprintln!("[Video Server] Failed to seek: {}", e); - let response = Response::from_string("Internal Server Error") - .with_status_code(StatusCode(500)); - let _ = request.respond(response); - return; - } - - // Read the requested range - let mut buffer = vec![0u8; length as usize]; - if let Err(e) = file.read_exact(&mut buffer) { - eprintln!("[Video Server] Failed to read range: {}", e); - let response = Response::from_string("Internal Server Error") - .with_status_code(StatusCode(500)); - let _ = request.respond(response); - return; - } - - // Determine content type - let content_type = get_content_type(file_path); - - // Send 206 Partial Content response - let content_range = format!("bytes {}-{}/{}", start, end, file_size); - let response = Response::from_data(buffer) - .with_status_code(StatusCode(206)) - .with_header(Header::from_bytes(&b"Content-Type"[..], content_type.as_bytes()).unwrap()) - .with_header(Header::from_bytes(&b"Content-Length"[..], length.to_string().as_bytes()).unwrap()) - .with_header(Header::from_bytes(&b"Content-Range"[..], content_range.as_bytes()).unwrap()) - .with_header(Header::from_bytes(&b"Accept-Ranges"[..], &b"bytes"[..]).unwrap()) - .with_header(Header::from_bytes(&b"Access-Control-Allow-Origin"[..], &b"*"[..]).unwrap()); - - let _ = request.respond(response); -} - -fn handle_full_request( - request: Request, - file: File, - file_size: u64, - file_path: &PathBuf, -) { - // Determine content type - let content_type = get_content_type(file_path); - - // Send 200 OK response using from_file to avoid chunked encoding - let response = Response::from_file(file) - .with_status_code(StatusCode(200)) - .with_chunked_threshold(usize::MAX) // Force Content-Length instead of chunked - .with_header(Header::from_bytes(&b"Content-Type"[..], content_type.as_bytes()).unwrap()) - .with_header(Header::from_bytes(&b"Accept-Ranges"[..], &b"bytes"[..]).unwrap()) - .with_header(Header::from_bytes(&b"Access-Control-Allow-Origin"[..], &b"*"[..]).unwrap()); - - let _ = request.respond(response); -} - -fn get_content_type(path: &PathBuf) -> String { - match path.extension().and_then(|s| s.to_str()) { - Some("webm") => "video/webm".to_string(), - Some("mp4") => "video/mp4".to_string(), - Some("mkv") => "video/x-matroska".to_string(), - Some("avi") => "video/x-msvideo".to_string(), - Some("mov") => "video/quicktime".to_string(), - _ => "application/octet-stream".to_string(), - } -} diff --git a/src/frame-receiver.js b/src/frame-receiver.js index 187f120..014f654 100644 --- a/src/frame-receiver.js +++ b/src/frame-receiver.js @@ -66,14 +66,10 @@ export class FrameReceiver { const dataLength = width * height * 4; const rgbaData = new Uint8ClampedArray(arrayBuffer, 16, dataLength); - // Debug: Log received data - console.log(`[FrameReceiver RECV] pool=${poolIndex}, ${width}x${height}, total buffer len=${arrayBuffer.byteLength}, data len=${dataLength}, first 20 RGBA bytes:`, Array.from(rgbaData.slice(0, 20))); // Create ImageData directly from the view (zero-copy!) const imageData = new ImageData(rgbaData, width, height); - // Debug: Log ImageData properties - console.log(`[FrameReceiver RECV] ImageData: ${imageData.width}x${imageData.height}, data len=${imageData.data.length}, first 20 bytes:`, Array.from(imageData.data.slice(0, 20))); // Call subscriber with frame data const timestamp = timestampMs / 1000.0;