From 81c38399376df95f9da8e30eb8faa321ef84d769 Mon Sep 17 00:00:00 2001 From: Mike Solar Date: Sat, 29 Aug 2026 19:52:28 +0800 Subject: [PATCH] codec: cap FFmpeg decoder frame threads at 4 threads=auto spawns one decode thread per logical core per decoder (16 observed on a 24-core box); with cores-2 render workers decoding concurrently the machine is oversubscribed several times over and the playback-audio render thread gets starved (late chunks are dropped). 4 threads decode 1080p h264 far past real-time; preview throughput comes from the worker pool, not per-decoder threading. Applies to both the software open path and the hwaccel open path. --- crates/oak-codec/src/ffmpeg.rs | 17 ++++++++++++++++- crates/oak-codec/src/hwdecode.rs | 2 +- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/crates/oak-codec/src/ffmpeg.rs b/crates/oak-codec/src/ffmpeg.rs index 66c182eba..68fda7f7f 100644 --- a/crates/oak-codec/src/ffmpeg.rs +++ b/crates/oak-codec/src/ffmpeg.rs @@ -559,6 +559,21 @@ struct AudioDecodeState { carry_format: (u32, u64), } +/// The frame-thread count handed to every FFmpeg decoder's `threads` +/// option. `auto` lets libavcodec spawn one decode thread per logical +/// core (16 observed on a 24-core box) PER DECODER; with ~`cores - 2` +/// render workers decoding concurrently that oversubscribes the machine +/// several times over and starves the playback-audio thread (late chunks +/// are dropped -> pops). 4 threads decode 1080p h264 far past real-time -- +/// preview throughput comes from the worker pool, not from per-decoder +/// threading. +pub(crate) fn decoder_threads() -> String { + let cores = std::thread::available_parallelism() + .map(|n| n.get()) + .unwrap_or(4); + cores.min(4).to_string() +} + /// A swresample conversion context. struct AudioResampler { ctx: resampling::Context, @@ -635,7 +650,7 @@ impl DecoderState { let codec = ffmpeg::decoder::find(codec_id) .ok_or_else(|| fail(format!("no decoder for codec {codec_id:?}")))?; let mut open_opts = Dictionary::new(); - open_opts.set("threads", "auto"); + open_opts.set("threads", &decoder_threads()); ffmpeg::codec::Context::from_parameters(params) .map_err(ffmpeg_err)? .decoder() diff --git a/crates/oak-codec/src/hwdecode.rs b/crates/oak-codec/src/hwdecode.rs index ae3271189..47c9c7213 100644 --- a/crates/oak-codec/src/hwdecode.rs +++ b/crates/oak-codec/src/hwdecode.rs @@ -195,7 +195,7 @@ pub fn open_hw_accel( // context frees it with the context. unsafe { (*context.as_mut_ptr()).hw_device_ctx = device }; let mut opts = Dictionary::new(); - opts.set("threads", "auto"); + opts.set("threads", &crate::ffmpeg::decoder_threads()); context .decoder() .open_as_with(codec, opts)