improve video import performance a bit
This commit is contained in:
parent
caba4305d8
commit
2dea1eab9e
|
|
@ -15,6 +15,7 @@ crossterm = "0.27"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
pathdiff = "0.2"
|
pathdiff = "0.2"
|
||||||
|
rayon = "1.10"
|
||||||
|
|
||||||
# Audio export
|
# Audio export
|
||||||
hound = "3.5"
|
hound = "3.5"
|
||||||
|
|
|
||||||
|
|
@ -1523,7 +1523,7 @@ impl Engine {
|
||||||
pool_index, detail_level, chunk_indices);
|
pool_index, detail_level, chunk_indices);
|
||||||
// Get audio file data from pool
|
// Get audio file data from pool
|
||||||
if let Some(audio_file) = self.audio_pool.get_file(pool_index) {
|
if let Some(audio_file) = self.audio_pool.get_file(pool_index) {
|
||||||
println!("✅ [ENGINE] Found audio file in pool, spawning background thread");
|
println!("✅ [ENGINE] Found audio file in pool, queuing work in thread pool");
|
||||||
// Clone necessary data for background thread
|
// Clone necessary data for background thread
|
||||||
let data = audio_file.data.clone();
|
let data = audio_file.data.clone();
|
||||||
let channels = audio_file.channels;
|
let channels = audio_file.channels;
|
||||||
|
|
@ -1531,8 +1531,8 @@ impl Engine {
|
||||||
let path = audio_file.path.clone();
|
let path = audio_file.path.clone();
|
||||||
let chunk_tx = self.chunk_generation_tx.clone();
|
let chunk_tx = self.chunk_generation_tx.clone();
|
||||||
|
|
||||||
// Generate chunks in background thread to avoid blocking audio thread
|
// Generate chunks using rayon's thread pool to avoid spawning thousands of threads
|
||||||
std::thread::spawn(move || {
|
rayon::spawn(move || {
|
||||||
// Create temporary AudioFile for chunk generation
|
// Create temporary AudioFile for chunk generation
|
||||||
let temp_audio_file = crate::audio::pool::AudioFile::with_format(
|
let temp_audio_file = crate::audio::pool::AudioFile::with_format(
|
||||||
path,
|
path,
|
||||||
|
|
@ -1563,6 +1563,9 @@ impl Engine {
|
||||||
chunks: event_chunks,
|
chunks: event_chunks,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Yield to other threads to reduce CPU contention with video playback
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(1));
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
eprintln!("❌ [ENGINE] Pool index {} not found for waveform generation", pool_index);
|
eprintln!("❌ [ENGINE] Pool index {} not found for waveform generation", pool_index);
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ pub struct VideoMetadata {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Video decoder with LRU frame caching
|
/// Video decoder with LRU frame caching
|
||||||
struct VideoDecoder {
|
pub struct VideoDecoder {
|
||||||
path: String,
|
path: String,
|
||||||
width: u32, // Original video width
|
width: u32, // Original video width
|
||||||
height: u32, // Original video height
|
height: u32, // Original video height
|
||||||
|
|
@ -43,7 +43,9 @@ impl VideoDecoder {
|
||||||
///
|
///
|
||||||
/// `max_width` and `max_height` specify the maximum output dimensions.
|
/// `max_width` and `max_height` specify the maximum output dimensions.
|
||||||
/// Video will be scaled down if larger, preserving aspect ratio.
|
/// Video will be scaled down if larger, preserving aspect ratio.
|
||||||
fn new(path: String, cache_size: usize, max_width: Option<u32>, max_height: Option<u32>) -> Result<Self, String> {
|
/// `build_keyframes` controls whether to build the keyframe index immediately (slow)
|
||||||
|
/// or defer it for async building later.
|
||||||
|
fn new(path: String, cache_size: usize, max_width: Option<u32>, max_height: Option<u32>, build_keyframes: bool) -> Result<Self, String> {
|
||||||
ffmpeg::init().map_err(|e| e.to_string())?;
|
ffmpeg::init().map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
let input = ffmpeg::format::input(&path)
|
let input = ffmpeg::format::input(&path)
|
||||||
|
|
@ -92,11 +94,16 @@ impl VideoDecoder {
|
||||||
|
|
||||||
let fps = f64::from(video_stream.avg_frame_rate());
|
let fps = f64::from(video_stream.avg_frame_rate());
|
||||||
|
|
||||||
// Build keyframe index for fast seeking
|
// Optionally build keyframe index for fast seeking
|
||||||
// This scans the video once to find all keyframe positions
|
let keyframe_positions = if build_keyframes {
|
||||||
eprintln!("[Video Decoder] Building keyframe index for {}", path);
|
eprintln!("[Video Decoder] Building keyframe index for {}", path);
|
||||||
let keyframe_positions = Self::build_keyframe_index(&path, stream_index)?;
|
let positions = Self::build_keyframe_index(&path, stream_index)?;
|
||||||
eprintln!("[Video Decoder] Found {} keyframes", keyframe_positions.len());
|
eprintln!("[Video Decoder] Found {} keyframes", positions.len());
|
||||||
|
positions
|
||||||
|
} else {
|
||||||
|
eprintln!("[Video Decoder] Deferring keyframe index building for {}", path);
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
path,
|
path,
|
||||||
|
|
@ -118,6 +125,31 @@ impl VideoDecoder {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build keyframe index for this decoder
|
||||||
|
/// This can be called asynchronously after decoder creation
|
||||||
|
fn build_and_set_keyframe_index(&mut self) -> Result<(), String> {
|
||||||
|
eprintln!("[Video Decoder] Building keyframe index for {}", self.path);
|
||||||
|
let positions = Self::build_keyframe_index(&self.path, self.stream_index)?;
|
||||||
|
eprintln!("[Video Decoder] Found {} keyframes", positions.len());
|
||||||
|
self.keyframe_positions = positions;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the output width (scaled dimensions)
|
||||||
|
pub fn get_output_width(&self) -> u32 {
|
||||||
|
self.output_width
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the output height (scaled dimensions)
|
||||||
|
pub fn get_output_height(&self) -> u32 {
|
||||||
|
self.output_height
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode a frame at the specified timestamp (public wrapper)
|
||||||
|
pub fn decode_frame(&mut self, timestamp: f64) -> Result<Vec<u8>, String> {
|
||||||
|
self.get_frame(timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
/// Build an index of all keyframe positions in the video
|
/// Build an index of all keyframe positions in the video
|
||||||
/// This enables fast seeking by knowing exactly where keyframes are
|
/// This enables fast seeking by knowing exactly where keyframes are
|
||||||
fn build_keyframe_index(path: &str, stream_index: usize) -> Result<Vec<i64>, String> {
|
fn build_keyframe_index(path: &str, stream_index: usize) -> Result<Vec<i64>, String> {
|
||||||
|
|
@ -407,6 +439,9 @@ impl VideoManager {
|
||||||
///
|
///
|
||||||
/// `target_width` and `target_height` specify the maximum dimensions
|
/// `target_width` and `target_height` specify the maximum dimensions
|
||||||
/// for decoded frames. Video will be scaled down if larger.
|
/// for decoded frames. Video will be scaled down if larger.
|
||||||
|
///
|
||||||
|
/// The keyframe index is NOT built during this call - use `build_keyframe_index_async`
|
||||||
|
/// in a background thread to build it asynchronously.
|
||||||
pub fn load_video(
|
pub fn load_video(
|
||||||
&mut self,
|
&mut self,
|
||||||
clip_id: Uuid,
|
clip_id: Uuid,
|
||||||
|
|
@ -417,12 +452,13 @@ impl VideoManager {
|
||||||
// First probe the video for metadata
|
// First probe the video for metadata
|
||||||
let metadata = probe_video(&path)?;
|
let metadata = probe_video(&path)?;
|
||||||
|
|
||||||
// Create decoder with target dimensions
|
// Create decoder with target dimensions, without building keyframe index
|
||||||
let decoder = VideoDecoder::new(
|
let decoder = VideoDecoder::new(
|
||||||
path,
|
path,
|
||||||
self.cache_size,
|
self.cache_size,
|
||||||
Some(target_width),
|
Some(target_width),
|
||||||
Some(target_height),
|
Some(target_height),
|
||||||
|
false, // Don't build keyframe index synchronously
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Store decoder in pool
|
// Store decoder in pool
|
||||||
|
|
@ -431,6 +467,20 @@ impl VideoManager {
|
||||||
Ok(metadata)
|
Ok(metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build keyframe index for a loaded video asynchronously
|
||||||
|
///
|
||||||
|
/// This should be called from a background thread after load_video()
|
||||||
|
/// to avoid blocking the UI during import.
|
||||||
|
pub fn build_keyframe_index(&self, clip_id: &Uuid) -> Result<(), String> {
|
||||||
|
let decoder_arc = self.decoders.get(clip_id)
|
||||||
|
.ok_or_else(|| format!("Video clip {} not found", clip_id))?;
|
||||||
|
|
||||||
|
let mut decoder = decoder_arc.lock()
|
||||||
|
.map_err(|e| format!("Failed to lock decoder: {}", e))?;
|
||||||
|
|
||||||
|
decoder.build_and_set_keyframe_index()
|
||||||
|
}
|
||||||
|
|
||||||
/// Get a decoded frame for a specific clip at a specific timestamp
|
/// Get a decoded frame for a specific clip at a specific timestamp
|
||||||
///
|
///
|
||||||
/// Returns None if the clip is not loaded or decoding fails.
|
/// Returns None if the clip is not loaded or decoding fails.
|
||||||
|
|
@ -467,10 +517,14 @@ impl VideoManager {
|
||||||
Some(frame)
|
Some(frame)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate thumbnails for a video clip
|
/// Generate thumbnails for a video clip (single batch version - use generate_thumbnails_progressive instead)
|
||||||
///
|
///
|
||||||
/// Thumbnails are generated every 5 seconds at 64px width.
|
/// Thumbnails are generated every 5 seconds at 128px width.
|
||||||
/// This should be called in a background thread to avoid blocking.
|
/// This should be called in a background thread to avoid blocking.
|
||||||
|
/// Thumbnails are inserted into the cache progressively as they're generated,
|
||||||
|
/// allowing the UI to display them immediately.
|
||||||
|
///
|
||||||
|
/// DEPRECATED: Use generate_thumbnails_progressive which releases the lock between thumbnails.
|
||||||
pub fn generate_thumbnails(&mut self, clip_id: &Uuid, duration: f64) -> Result<(), String> {
|
pub fn generate_thumbnails(&mut self, clip_id: &Uuid, duration: f64) -> Result<(), String> {
|
||||||
let decoder_arc = self.decoders.get(clip_id)
|
let decoder_arc = self.decoders.get(clip_id)
|
||||||
.ok_or("Clip not loaded")?
|
.ok_or("Clip not loaded")?
|
||||||
|
|
@ -479,7 +533,9 @@ impl VideoManager {
|
||||||
let mut decoder = decoder_arc.lock()
|
let mut decoder = decoder_arc.lock()
|
||||||
.map_err(|e| format!("Failed to lock decoder: {}", e))?;
|
.map_err(|e| format!("Failed to lock decoder: {}", e))?;
|
||||||
|
|
||||||
let mut thumbnails = Vec::new();
|
// Initialize thumbnail cache entry with empty vec
|
||||||
|
self.thumbnail_cache.insert(*clip_id, Vec::new());
|
||||||
|
|
||||||
let interval = 5.0; // Generate thumbnail every 5 seconds
|
let interval = 5.0; // Generate thumbnail every 5 seconds
|
||||||
let mut t = 0.0;
|
let mut t = 0.0;
|
||||||
|
|
||||||
|
|
@ -505,18 +561,32 @@ impl VideoManager {
|
||||||
thumb_height,
|
thumb_height,
|
||||||
);
|
);
|
||||||
|
|
||||||
thumbnails.push((t, Arc::new(thumb_data)));
|
// Insert thumbnail into cache immediately so UI can display it
|
||||||
|
if let Some(thumbnails) = self.thumbnail_cache.get_mut(clip_id) {
|
||||||
|
thumbnails.push((t, Arc::new(thumb_data)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
t += interval;
|
t += interval;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store thumbnails in cache
|
|
||||||
self.thumbnail_cache.insert(*clip_id, thumbnails);
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the decoder Arc for a clip (for external thumbnail generation)
|
||||||
|
/// This allows external code to decode frames without holding the VideoManager lock
|
||||||
|
pub fn get_decoder(&self, clip_id: &Uuid) -> Option<Arc<Mutex<VideoDecoder>>> {
|
||||||
|
self.decoders.get(clip_id).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert a thumbnail into the cache (for external thumbnail generation)
|
||||||
|
pub fn insert_thumbnail(&mut self, clip_id: &Uuid, timestamp: f64, data: Arc<Vec<u8>>) {
|
||||||
|
self.thumbnail_cache
|
||||||
|
.entry(*clip_id)
|
||||||
|
.or_insert_with(Vec::new)
|
||||||
|
.push((timestamp, data));
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the thumbnail closest to the specified timestamp
|
/// Get the thumbnail closest to the specified timestamp
|
||||||
///
|
///
|
||||||
/// Returns None if no thumbnails have been generated for this clip.
|
/// Returns None if no thumbnails have been generated for this clip.
|
||||||
|
|
@ -582,6 +652,17 @@ impl Default for VideoManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Simple nearest-neighbor downsampling for RGBA images
|
/// Simple nearest-neighbor downsampling for RGBA images
|
||||||
|
pub fn downsample_rgba_public(
|
||||||
|
src: &[u8],
|
||||||
|
src_width: u32,
|
||||||
|
src_height: u32,
|
||||||
|
dst_width: u32,
|
||||||
|
dst_height: u32,
|
||||||
|
) -> Vec<u8> {
|
||||||
|
downsample_rgba(src, src_width, src_height, dst_width, dst_height)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simple nearest-neighbor downsampling for RGBA images (internal)
|
||||||
fn downsample_rgba(
|
fn downsample_rgba(
|
||||||
src: &[u8],
|
src: &[u8],
|
||||||
src_width: u32,
|
src_width: u32,
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ lightningcss = "1.0.0-alpha.68"
|
||||||
clap = { version = "4.5", features = ["derive"] }
|
clap = { version = "4.5", features = ["derive"] }
|
||||||
uuid = { version = "1.0", features = ["v4", "serde"] }
|
uuid = { version = "1.0", features = ["v4", "serde"] }
|
||||||
petgraph = "0.6"
|
petgraph = "0.6"
|
||||||
|
rayon = "1.10"
|
||||||
|
|
||||||
# Native file dialogs
|
# Native file dialogs
|
||||||
rfd = "0.15"
|
rfd = "0.15"
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,19 @@ struct Args {
|
||||||
fn main() -> eframe::Result {
|
fn main() -> eframe::Result {
|
||||||
println!("🚀 Starting Lightningbeam Editor...");
|
println!("🚀 Starting Lightningbeam Editor...");
|
||||||
|
|
||||||
|
// Configure rayon thread pool to use fewer threads, leaving cores free for video playback
|
||||||
|
let num_cpus = std::thread::available_parallelism()
|
||||||
|
.map(|n| n.get())
|
||||||
|
.unwrap_or(4);
|
||||||
|
let waveform_threads = (num_cpus.saturating_sub(2)).max(2); // Leave 2 cores free, minimum 2 threads
|
||||||
|
rayon::ThreadPoolBuilder::new()
|
||||||
|
.num_threads(waveform_threads)
|
||||||
|
.thread_name(|i| format!("waveform-{}", i))
|
||||||
|
.build_global()
|
||||||
|
.expect("Failed to build rayon thread pool");
|
||||||
|
println!("✅ Configured waveform generation to use {} threads (leaving {} cores for video)",
|
||||||
|
waveform_threads, num_cpus - waveform_threads);
|
||||||
|
|
||||||
// Parse command line arguments
|
// Parse command line arguments
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
|
||||||
|
|
@ -2095,7 +2108,7 @@ impl EditorApp {
|
||||||
|
|
||||||
let clip_id = clip.id;
|
let clip_id = clip.id;
|
||||||
|
|
||||||
// Load video into VideoManager
|
// Load video into VideoManager (without building keyframe index)
|
||||||
let doc_width = self.action_executor.document().width as u32;
|
let doc_width = self.action_executor.document().width as u32;
|
||||||
let doc_height = self.action_executor.document().height as u32;
|
let doc_height = self.action_executor.document().height as u32;
|
||||||
|
|
||||||
|
|
@ -2106,6 +2119,18 @@ impl EditorApp {
|
||||||
}
|
}
|
||||||
drop(video_mgr);
|
drop(video_mgr);
|
||||||
|
|
||||||
|
// Spawn background thread to build keyframe index asynchronously
|
||||||
|
let video_manager_clone = Arc::clone(&self.video_manager);
|
||||||
|
let keyframe_clip_id = clip_id;
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let video_mgr = video_manager_clone.lock().unwrap();
|
||||||
|
if let Err(e) = video_mgr.build_keyframe_index(&keyframe_clip_id) {
|
||||||
|
eprintln!("Failed to build keyframe index: {}", e);
|
||||||
|
} else {
|
||||||
|
println!(" Built keyframe index for video clip {}", keyframe_clip_id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Spawn background thread for audio extraction if video has audio
|
// Spawn background thread for audio extraction if video has audio
|
||||||
if metadata.has_audio {
|
if metadata.has_audio {
|
||||||
if let Some(ref audio_controller) = self.audio_controller {
|
if let Some(ref audio_controller) = self.audio_controller {
|
||||||
|
|
@ -2180,15 +2205,61 @@ impl EditorApp {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spawn background thread for thumbnail generation
|
// Spawn background thread for thumbnail generation
|
||||||
|
// Get decoder once, then generate thumbnails without holding VideoManager lock
|
||||||
let video_manager_clone = Arc::clone(&self.video_manager);
|
let video_manager_clone = Arc::clone(&self.video_manager);
|
||||||
let duration = metadata.duration;
|
let duration = metadata.duration;
|
||||||
|
let thumb_clip_id = clip_id;
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
let mut video_mgr = video_manager_clone.lock().unwrap();
|
// Get decoder Arc with brief lock
|
||||||
if let Err(e) = video_mgr.generate_thumbnails(&clip_id, duration) {
|
let decoder_arc = {
|
||||||
eprintln!("Failed to generate video thumbnails: {}", e);
|
let video_mgr = video_manager_clone.lock().unwrap();
|
||||||
} else {
|
match video_mgr.get_decoder(&thumb_clip_id) {
|
||||||
println!(" Generated thumbnails for video clip {}", clip_id);
|
Some(arc) => arc,
|
||||||
|
None => {
|
||||||
|
eprintln!("Failed to get decoder for thumbnail generation");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// VideoManager lock released - video can now be displayed!
|
||||||
|
|
||||||
|
let interval = 5.0;
|
||||||
|
let mut t = 0.0;
|
||||||
|
let mut thumbnail_count = 0;
|
||||||
|
|
||||||
|
while t < duration {
|
||||||
|
// Decode frame WITHOUT holding VideoManager lock
|
||||||
|
let thumb_opt = {
|
||||||
|
let mut decoder = decoder_arc.lock().unwrap();
|
||||||
|
match decoder.decode_frame(t) {
|
||||||
|
Ok(rgba_data) => {
|
||||||
|
let w = decoder.get_output_width();
|
||||||
|
let h = decoder.get_output_height();
|
||||||
|
Some((rgba_data, w, h))
|
||||||
|
}
|
||||||
|
Err(_) => None,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Downsample without any locks
|
||||||
|
if let Some((rgba_data, w, h)) = thumb_opt {
|
||||||
|
use lightningbeam_core::video::downsample_rgba_public;
|
||||||
|
let thumb_w = 128u32;
|
||||||
|
let thumb_h = (h as f32 / w as f32 * thumb_w as f32) as u32;
|
||||||
|
let thumb_data = downsample_rgba_public(&rgba_data, w, h, thumb_w, thumb_h);
|
||||||
|
|
||||||
|
// Brief lock just to insert
|
||||||
|
{
|
||||||
|
let mut video_mgr = video_manager_clone.lock().unwrap();
|
||||||
|
video_mgr.insert_thumbnail(&thumb_clip_id, t, Arc::new(thumb_data));
|
||||||
|
}
|
||||||
|
thumbnail_count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
t += interval;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
println!(" Generated {} thumbnails for video clip {}", thumbnail_count, thumb_clip_id);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add clip to document
|
// Add clip to document
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue