1use std::time::Instant;
14
15use anyhow::{Context, Result};
16use bytes::Bytes;
17
18use codec::frame::{ColorMetadata, PixelFormat, VideoFrame};
19use codec::{colorspace, decode};
20use container::streaming;
21
22#[derive(Clone)]
24pub struct DecodePumpConfig {
25 pub codec_name: String,
27 pub info_for_decoder: codec::frame::StreamInfo,
29 pub source_color_metadata: ColorMetadata,
31 pub source_pixel_format: PixelFormat,
33 pub needs_downsample: bool,
35 pub tonemap_to_sdr: bool,
41 pub gpu_index: Option<u32>,
43 pub filters: std::sync::Arc<codec::filter::FilterChain>,
48}
49
50#[derive(Clone)]
57pub struct ClipSource {
58 pub cfg: DecodePumpConfig,
59 pub input: Bytes,
60 pub start_frame: u64,
61 pub end_frame: Option<u64>,
62}
63
64impl ClipSource {
65 pub fn whole(cfg: DecodePumpConfig, input: Bytes) -> Self {
67 Self { cfg, input, start_frame: 0, end_frame: None }
68 }
69}
70
71pub fn run_shared_decode_pump_blocking(
74 cfg: DecodePumpConfig,
75 input_data: Bytes,
76 senders: Vec<tokio::sync::mpsc::Sender<VideoFrame>>,
77 rt: tokio::runtime::Handle,
78) -> Result<u64> {
79 run_spliced_decode_pump_blocking(vec![ClipSource::whole(cfg, input_data)], senders, rt)
80}
81
82pub fn run_spliced_decode_pump_blocking(
93 clips: Vec<ClipSource>,
94 senders: Vec<tokio::sync::mpsc::Sender<VideoFrame>>,
95 rt: tokio::runtime::Handle,
96) -> Result<u64> {
97 let mut total: u64 = 0;
98 let result = (|| {
99 for (clip_idx, clip) in clips.iter().enumerate() {
100 match decode_clip(clip, &senders, &rt, &mut total)
101 .with_context(|| format!("decoding splice clip {clip_idx}"))?
102 {
103 Flow::Continue => {}
104 Flow::AllReceiversClosed => break,
105 }
106 }
107 Ok(total)
108 })();
109 drop(senders);
111 result
112}
113
114enum Flow {
115 Continue,
116 AllReceiversClosed,
117}
118
119fn decode_clip(
122 clip: &ClipSource,
123 senders: &[tokio::sync::mpsc::Sender<VideoFrame>],
124 rt: &tokio::runtime::Handle,
125 total: &mut u64,
126) -> Result<Flow> {
127 let cfg = &clip.cfg;
128 let mut demuxer =
129 streaming::demux_streaming(&clip.input).context("demuxing clip for decode pump")?;
130 let mut decoder =
131 decode::create_decoder_on(&cfg.codec_name, cfg.info_for_decoder.clone(), cfg.gpu_index)
132 .context("creating decoder for decode pump")?;
133
134 let mut src_idx: u64 = 0;
136 loop {
137 match demuxer
138 .next_video_sample()
139 .context("demuxing next video sample in decode pump")?
140 {
141 Some(sample) => {
142 decoder
143 .push_sample(&sample.data)
144 .context("pushing sample to decode pump decoder")?;
145 while let Some(frame) =
146 decoder.decode_next().context("decoding frame in decode pump")?
147 {
148 match handle_frame(clip, cfg, frame, senders, rt, &mut src_idx, total)? {
149 FrameAction::Continue => {}
150 FrameAction::ClipDone => return Ok(Flow::Continue),
151 FrameAction::StopAll => return Ok(Flow::AllReceiversClosed),
152 }
153 }
154 }
155 None => {
156 decoder.finish().context("decoder finish in decode pump")?;
157 while let Some(frame) = decoder
158 .decode_next()
159 .context("decoding frame after finish in decode pump")?
160 {
161 match handle_frame(clip, cfg, frame, senders, rt, &mut src_idx, total)? {
162 FrameAction::Continue => {}
163 FrameAction::ClipDone => return Ok(Flow::Continue),
164 FrameAction::StopAll => return Ok(Flow::AllReceiversClosed),
165 }
166 }
167 break;
168 }
169 }
170 }
171 Ok(Flow::Continue)
172}
173
174enum FrameAction {
175 Continue,
176 ClipDone,
177 StopAll,
178}
179
180fn handle_frame(
183 clip: &ClipSource,
184 cfg: &DecodePumpConfig,
185 frame: VideoFrame,
186 senders: &[tokio::sync::mpsc::Sender<VideoFrame>],
187 rt: &tokio::runtime::Handle,
188 src_idx: &mut u64,
189 total: &mut u64,
190) -> Result<FrameAction> {
191 if clip.end_frame.is_some_and(|end| *src_idx >= end) {
192 return Ok(FrameAction::ClipDone); }
194 if *src_idx >= clip.start_frame {
195 let normalized = normalize_frame(cfg, frame)?;
196 if !fan_out(senders, normalized, rt)? {
197 return Ok(FrameAction::StopAll);
198 }
199 *total += 1;
200 }
201 *src_idx += 1;
202 Ok(FrameAction::Continue)
203}
204
205fn normalize_frame(cfg: &DecodePumpConfig, frame: VideoFrame) -> Result<VideoFrame> {
211 let downsampled = if cfg.needs_downsample {
212 colorspace::downsample_444_to_420_frame(&frame)
213 .context("shared decode pump 4:4:4 → 4:2:0 downsample")?
214 } else {
215 frame
216 };
217 let normalized = if !cfg.tonemap_to_sdr {
218 downsampled
220 } else {
221 colorspace::convert_to_sdr_bt709(&downsampled, &cfg.source_color_metadata)
222 .context("shared decode pump colorspace convert (HDR-aware)")?
223 };
224 if cfg.filters.is_empty() {
227 Ok(normalized)
228 } else {
229 cfg.filters.apply(normalized).context("shared decode pump video filters")
230 }
231}
232
233pub const DECODE_BENCH_FRAMES: usize = 120;
237
238pub fn fastest_decode_gpu(
246 codec_name: &str,
247 info: &codec::frame::StreamInfo,
248 input: &Bytes,
249 candidates: &[u32],
250 measure_frames: usize,
251) -> Option<u32> {
252 if candidates.len() < 2 {
253 return candidates.first().copied();
254 }
255 let mut best: Option<(u32, f64)> = None;
256 for &gpu in candidates {
257 match bench_decode_gpu(codec_name, info, input, gpu, measure_frames) {
258 Ok(Some(fps)) => {
259 tracing::info!(
260 gpu_index = gpu,
261 fps = format!("{fps:.1}"),
262 "decode-with-fastest: benchmarked candidate"
263 );
264 if best.is_none_or(|(_, b)| fps > b) {
265 best = Some((gpu, fps));
266 }
267 }
268 Ok(None) => {
269 tracing::warn!(gpu_index = gpu, "decode-with-fastest: no frames; skipping candidate")
270 }
271 Err(e) => tracing::warn!(
272 gpu_index = gpu,
273 error = %e,
274 "decode-with-fastest: bench failed; skipping candidate"
275 ),
276 }
277 }
278 if let Some((gpu, fps)) = best {
279 tracing::info!(
280 gpu_index = gpu,
281 fps = format!("{fps:.1}"),
282 "decode-with-fastest: selected fastest decode GPU"
283 );
284 }
285 best.map(|(g, _)| g)
286}
287
288fn bench_decode_gpu(
291 codec_name: &str,
292 info: &codec::frame::StreamInfo,
293 input: &Bytes,
294 gpu: u32,
295 measure_frames: usize,
296) -> Result<Option<f64>> {
297 const WARMUP: usize = 8;
298 let target = WARMUP + measure_frames;
299 let mut demuxer = streaming::demux_streaming(input).context("demux for decode bench")?;
300 let mut decoder = decode::create_decoder_on(codec_name, info.clone(), Some(gpu))
301 .context("create decoder for bench")?;
302 let mut decoded = 0usize;
303 let mut clock: Option<Instant> = None;
304 'outer: loop {
305 match demuxer.next_video_sample().context("bench next sample")? {
306 Some(s) => {
307 decoder.push_sample(&s.data).context("bench push")?;
308 while decoder.decode_next().context("bench decode")?.is_some() {
309 decoded += 1;
310 if decoded == WARMUP {
311 clock = Some(Instant::now());
312 }
313 if decoded >= target {
314 break 'outer;
315 }
316 }
317 }
318 None => {
319 decoder.finish().context("bench finish")?;
320 while decoder.decode_next().context("bench drain")?.is_some() {
321 decoded += 1;
322 if decoded == WARMUP {
323 clock = Some(Instant::now());
324 }
325 if decoded >= target {
326 break 'outer;
327 }
328 }
329 break;
330 }
331 }
332 }
333 let measured = decoded.saturating_sub(WARMUP);
334 Ok(match clock {
335 Some(t) if measured > 0 => {
336 let secs = t.elapsed().as_secs_f64();
337 (secs > 0.0).then_some(measured as f64 / secs)
338 }
339 _ => (decoded > 0).then_some(decoded as f64),
342 })
343}
344
345fn fan_out(
348 senders: &[tokio::sync::mpsc::Sender<VideoFrame>],
349 frame: VideoFrame,
350 rt: &tokio::runtime::Handle,
351) -> Result<bool> {
352 let mut any_alive = false;
353 for (idx, sender) in senders.iter().enumerate() {
354 let frame_clone = frame.clone();
355 let sender = sender.clone();
356 let accepted = rt.block_on(async move { sender.send(frame_clone).await });
357 match accepted {
358 Ok(()) => any_alive = true,
359 Err(_) => {
360 tracing::warn!(rung_idx = idx, "shared decode pump: rung dropped its receiver");
361 }
362 }
363 }
364 Ok(any_alive)
365}