video: reuse the swscale context + gate per-frame decode tracing
get_frame rebuilt the RGBA swscale context on every decoded frame and printed [Video Timing] lines unconditionally. A stream's frames share one input format/size, so build the scaler once (keyed on format+dims, rebuilt only if they change) and reuse it; gate the per-frame traces behind LB_VIDEO_DEBUG. Cuts export wall time ~10% on a 1080p video clip (the scaler rebuild was the bulk of the per-frame "scale" cost; it's now ~0ms). SwsContext is !Send, so the cached scaler is wrapped in a SendScaler — sound because a VideoDecoder is only ever touched under the VideoManager mutex (same invariant as its decoder/input).
This commit is contained in:
parent
e7c239f203
commit
7525a604f2
|
|
@ -97,6 +97,22 @@ pub struct VideoDecoder {
|
||||||
decoder: Option<ffmpeg::decoder::Video>,
|
decoder: Option<ffmpeg::decoder::Video>,
|
||||||
last_decoded_ts: i64, // Track the last decoded frame timestamp
|
last_decoded_ts: i64, // Track the last decoded frame timestamp
|
||||||
keyframe_positions: Vec<i64>, // Index of keyframe timestamps for fast seeking
|
keyframe_positions: Vec<i64>, // Index of keyframe timestamps for fast seeking
|
||||||
|
/// Reused RGBA scaler, keyed by the input (format, width, height). Building an swscale
|
||||||
|
/// context is not free; a stream's frames share one format/size, so build it once.
|
||||||
|
scaler: Option<(ffmpeg::format::Pixel, u32, u32, SendScaler)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `SwsContext` is `!Send` in ffmpeg-next, but a `VideoDecoder` (like its decoder/input) is only
|
||||||
|
/// ever accessed under the `VideoManager` mutex — never concurrently — so moving it between
|
||||||
|
/// threads is sound. The decoder/input fields rely on the same invariant.
|
||||||
|
struct SendScaler(ffmpeg::software::scaling::context::Context);
|
||||||
|
unsafe impl Send for SendScaler {}
|
||||||
|
|
||||||
|
/// Per-frame video decode tracing, gated behind `LB_VIDEO_DEBUG` (checked once). Off by
|
||||||
|
/// default — at export frame rates these prints are a lot of locked stderr writes.
|
||||||
|
fn video_debug() -> bool {
|
||||||
|
static V: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
|
||||||
|
*V.get_or_init(|| std::env::var("LB_VIDEO_DEBUG").is_ok())
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VideoDecoder {
|
impl VideoDecoder {
|
||||||
|
|
@ -183,6 +199,7 @@ impl VideoDecoder {
|
||||||
decoder: None,
|
decoder: None,
|
||||||
last_decoded_ts: -1,
|
last_decoded_ts: -1,
|
||||||
keyframe_positions,
|
keyframe_positions,
|
||||||
|
scaler: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -270,7 +287,9 @@ impl VideoDecoder {
|
||||||
|
|
||||||
// Check cache
|
// Check cache
|
||||||
if let Some(cached_frame) = self.frame_cache.get(&frame_ts) {
|
if let Some(cached_frame) = self.frame_cache.get(&frame_ts) {
|
||||||
eprintln!("[Video Timing] Cache hit for ts={:.3}s ({}ms)", timestamp, t_start.elapsed().as_millis());
|
if video_debug() {
|
||||||
|
eprintln!("[Video Timing] Cache hit for ts={:.3}s ({}ms)", timestamp, t_start.elapsed().as_millis());
|
||||||
|
}
|
||||||
return Ok(cached_frame.clone());
|
return Ok(cached_frame.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -292,8 +311,10 @@ impl VideoDecoder {
|
||||||
let keyframe_seconds = keyframe_ts_stream as f64 * self.time_base;
|
let keyframe_seconds = keyframe_ts_stream as f64 * self.time_base;
|
||||||
let keyframe_ts_av = (keyframe_seconds * 1_000_000.0) as i64; // AV_TIME_BASE = 1000000
|
let keyframe_ts_av = (keyframe_seconds * 1_000_000.0) as i64; // AV_TIME_BASE = 1000000
|
||||||
|
|
||||||
eprintln!("[Video Seek] Target: {} | Keyframe(stream): {} | Keyframe(AV): {} | Index size: {}",
|
if video_debug() {
|
||||||
frame_ts, keyframe_ts_stream, keyframe_ts_av, self.keyframe_positions.len());
|
eprintln!("[Video Seek] Target: {} | Keyframe(stream): {} | Keyframe(AV): {} | Index size: {}",
|
||||||
|
frame_ts, keyframe_ts_stream, keyframe_ts_av, self.keyframe_positions.len());
|
||||||
|
}
|
||||||
|
|
||||||
// Reopen input (a fresh BlobReader for packed sources).
|
// Reopen input (a fresh BlobReader for packed sources).
|
||||||
let mut owned = self.source.open()
|
let mut owned = self.source.open()
|
||||||
|
|
@ -306,7 +327,9 @@ impl VideoDecoder {
|
||||||
input.seek(keyframe_ts_av, keyframe_ts_av..(keyframe_ts_av + 1))
|
input.seek(keyframe_ts_av, keyframe_ts_av..(keyframe_ts_av + 1))
|
||||||
.map_err(|e| format!("Seek failed: {}", e))?;
|
.map_err(|e| format!("Seek failed: {}", e))?;
|
||||||
|
|
||||||
eprintln!("[Video Timing] Seek call took {}ms", t_seek_start.elapsed().as_millis());
|
if video_debug() {
|
||||||
|
eprintln!("[Video Timing] Seek call took {}ms", t_seek_start.elapsed().as_millis());
|
||||||
|
}
|
||||||
|
|
||||||
let context_decoder = ffmpeg::codec::context::Context::from_parameters(
|
let context_decoder = ffmpeg::codec::context::Context::from_parameters(
|
||||||
input.streams().best(ffmpeg::media::Type::Video).unwrap().parameters()
|
input.streams().best(ffmpeg::media::Type::Video).unwrap().parameters()
|
||||||
|
|
@ -354,16 +377,27 @@ impl VideoDecoder {
|
||||||
if is_better {
|
if is_better {
|
||||||
let t_scale_start = Instant::now();
|
let t_scale_start = Instant::now();
|
||||||
|
|
||||||
// Convert to RGBA and scale to output size
|
// Reuse the RGBA scaler across frames; rebuild only if the input
|
||||||
let mut scaler = ffmpeg::software::scaling::context::Context::get(
|
// format/size changes (it won't within a stream).
|
||||||
frame.format(),
|
let need_new = match &self.scaler {
|
||||||
frame.width(),
|
Some((fmt, w, h, _)) => {
|
||||||
frame.height(),
|
*fmt != frame.format() || *w != frame.width() || *h != frame.height()
|
||||||
ffmpeg::format::Pixel::RGBA,
|
}
|
||||||
self.output_width,
|
None => true,
|
||||||
self.output_height,
|
};
|
||||||
ffmpeg::software::scaling::flag::Flags::BILINEAR,
|
if need_new {
|
||||||
).map_err(|e| e.to_string())?;
|
let ctx = ffmpeg::software::scaling::context::Context::get(
|
||||||
|
frame.format(),
|
||||||
|
frame.width(),
|
||||||
|
frame.height(),
|
||||||
|
ffmpeg::format::Pixel::RGBA,
|
||||||
|
self.output_width,
|
||||||
|
self.output_height,
|
||||||
|
ffmpeg::software::scaling::flag::Flags::BILINEAR,
|
||||||
|
).map_err(|e| e.to_string())?;
|
||||||
|
self.scaler = Some((frame.format(), frame.width(), frame.height(), SendScaler(ctx)));
|
||||||
|
}
|
||||||
|
let scaler = &mut self.scaler.as_mut().unwrap().3.0;
|
||||||
|
|
||||||
let mut rgb_frame = ffmpeg::util::frame::Video::empty();
|
let mut rgb_frame = ffmpeg::util::frame::Video::empty();
|
||||||
scaler.run(&frame, &mut rgb_frame)
|
scaler.run(&frame, &mut rgb_frame)
|
||||||
|
|
@ -392,10 +426,12 @@ impl VideoDecoder {
|
||||||
if current_frame_ts >= frame_ts {
|
if current_frame_ts >= frame_ts {
|
||||||
// Found our frame, cache and return it
|
// Found our frame, cache and return it
|
||||||
if let Some(data) = best_frame_data {
|
if let Some(data) = best_frame_data {
|
||||||
let total_time = t_start.elapsed().as_millis();
|
if video_debug() {
|
||||||
let decode_time = t_decode_start.elapsed().as_millis();
|
let total_time = t_start.elapsed().as_millis();
|
||||||
eprintln!("[Video Timing] ts={:.3}s | Decoded {} frames in {}ms | Scale: {}ms | Total: {}ms",
|
let decode_time = t_decode_start.elapsed().as_millis();
|
||||||
timestamp, decode_count, decode_time, scale_time_ms, total_time);
|
eprintln!("[Video Timing] ts={:.3}s | Decoded {} frames in {}ms | Scale: {}ms | Total: {}ms",
|
||||||
|
timestamp, decode_count, decode_time, scale_time_ms, total_time);
|
||||||
|
}
|
||||||
self.frame_cache.put(frame_ts, data.clone());
|
self.frame_cache.put(frame_ts, data.clone());
|
||||||
return Ok(data);
|
return Ok(data);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue