use std::time::Instant;
use anyhow::{Context, Result};
use bytes::Bytes;
use codec::frame::{ColorMetadata, PixelFormat, VideoFrame};
use codec::{colorspace, decode};
use container::streaming;
#[derive(Clone)]
pub struct DecodePumpConfig {
pub codec_name: String,
pub info_for_decoder: codec::frame::StreamInfo,
pub source_color_metadata: ColorMetadata,
pub source_pixel_format: PixelFormat,
pub needs_downsample: bool,
pub tonemap_to_sdr: bool,
pub gpu_index: Option<u32>,
pub filters: std::sync::Arc<codec::filter::FilterChain>,
}
#[derive(Clone)]
pub struct ClipSource {
pub cfg: DecodePumpConfig,
pub input: Bytes,
pub start_frame: u64,
pub end_frame: Option<u64>,
}
impl ClipSource {
pub fn whole(cfg: DecodePumpConfig, input: Bytes) -> Self {
Self { cfg, input, start_frame: 0, end_frame: None }
}
}
pub fn run_shared_decode_pump_blocking(
cfg: DecodePumpConfig,
input_data: Bytes,
senders: Vec<tokio::sync::mpsc::Sender<VideoFrame>>,
rt: tokio::runtime::Handle,
) -> Result<u64> {
run_spliced_decode_pump_blocking(vec![ClipSource::whole(cfg, input_data)], senders, rt)
}
pub fn run_spliced_decode_pump_blocking(
clips: Vec<ClipSource>,
senders: Vec<tokio::sync::mpsc::Sender<VideoFrame>>,
rt: tokio::runtime::Handle,
) -> Result<u64> {
let mut total: u64 = 0;
let result = (|| {
for (clip_idx, clip) in clips.iter().enumerate() {
match decode_clip(clip, &senders, &rt, &mut total)
.with_context(|| format!("decoding splice clip {clip_idx}"))?
{
Flow::Continue => {}
Flow::AllReceiversClosed => break,
}
}
Ok(total)
})();
drop(senders);
result
}
enum Flow {
Continue,
AllReceiversClosed,
}
fn decode_clip(
clip: &ClipSource,
senders: &[tokio::sync::mpsc::Sender<VideoFrame>],
rt: &tokio::runtime::Handle,
total: &mut u64,
) -> Result<Flow> {
let cfg = &clip.cfg;
let mut demuxer =
streaming::demux_streaming(&clip.input).context("demuxing clip for decode pump")?;
let mut decoder =
decode::create_decoder_on(&cfg.codec_name, cfg.info_for_decoder.clone(), cfg.gpu_index)
.context("creating decoder for decode pump")?;
let mut src_idx: u64 = 0;
loop {
match demuxer
.next_video_sample()
.context("demuxing next video sample in decode pump")?
{
Some(sample) => {
decoder
.push_sample(&sample.data)
.context("pushing sample to decode pump decoder")?;
while let Some(frame) =
decoder.decode_next().context("decoding frame in decode pump")?
{
match handle_frame(clip, cfg, frame, senders, rt, &mut src_idx, total)? {
FrameAction::Continue => {}
FrameAction::ClipDone => return Ok(Flow::Continue),
FrameAction::StopAll => return Ok(Flow::AllReceiversClosed),
}
}
}
None => {
decoder.finish().context("decoder finish in decode pump")?;
while let Some(frame) = decoder
.decode_next()
.context("decoding frame after finish in decode pump")?
{
match handle_frame(clip, cfg, frame, senders, rt, &mut src_idx, total)? {
FrameAction::Continue => {}
FrameAction::ClipDone => return Ok(Flow::Continue),
FrameAction::StopAll => return Ok(Flow::AllReceiversClosed),
}
}
break;
}
}
}
Ok(Flow::Continue)
}
enum FrameAction {
Continue,
ClipDone,
StopAll,
}
fn handle_frame(
clip: &ClipSource,
cfg: &DecodePumpConfig,
frame: VideoFrame,
senders: &[tokio::sync::mpsc::Sender<VideoFrame>],
rt: &tokio::runtime::Handle,
src_idx: &mut u64,
total: &mut u64,
) -> Result<FrameAction> {
if clip.end_frame.is_some_and(|end| *src_idx >= end) {
return Ok(FrameAction::ClipDone); }
if *src_idx >= clip.start_frame {
let normalized = normalize_frame(cfg, frame)?;
if !fan_out(senders, normalized, rt)? {
return Ok(FrameAction::StopAll);
}
*total += 1;
}
*src_idx += 1;
Ok(FrameAction::Continue)
}
fn normalize_frame(cfg: &DecodePumpConfig, frame: VideoFrame) -> Result<VideoFrame> {
let downsampled = if cfg.needs_downsample {
colorspace::downsample_444_to_420_frame(&frame)
.context("shared decode pump 4:4:4 → 4:2:0 downsample")?
} else {
frame
};
let normalized = if !cfg.tonemap_to_sdr {
downsampled
} else {
colorspace::convert_to_sdr_bt709(&downsampled, &cfg.source_color_metadata)
.context("shared decode pump colorspace convert (HDR-aware)")?
};
if cfg.filters.is_empty() {
Ok(normalized)
} else {
cfg.filters.apply(normalized).context("shared decode pump video filters")
}
}
pub const DECODE_BENCH_FRAMES: usize = 120;
pub fn fastest_decode_gpu(
codec_name: &str,
info: &codec::frame::StreamInfo,
input: &Bytes,
candidates: &[u32],
measure_frames: usize,
) -> Option<u32> {
if candidates.len() < 2 {
return candidates.first().copied();
}
let mut best: Option<(u32, f64)> = None;
for &gpu in candidates {
match bench_decode_gpu(codec_name, info, input, gpu, measure_frames) {
Ok(Some(fps)) => {
tracing::info!(
gpu_index = gpu,
fps = format!("{fps:.1}"),
"decode-with-fastest: benchmarked candidate"
);
if best.is_none_or(|(_, b)| fps > b) {
best = Some((gpu, fps));
}
}
Ok(None) => {
tracing::warn!(gpu_index = gpu, "decode-with-fastest: no frames; skipping candidate")
}
Err(e) => tracing::warn!(
gpu_index = gpu,
error = %e,
"decode-with-fastest: bench failed; skipping candidate"
),
}
}
if let Some((gpu, fps)) = best {
tracing::info!(
gpu_index = gpu,
fps = format!("{fps:.1}"),
"decode-with-fastest: selected fastest decode GPU"
);
}
best.map(|(g, _)| g)
}
fn bench_decode_gpu(
codec_name: &str,
info: &codec::frame::StreamInfo,
input: &Bytes,
gpu: u32,
measure_frames: usize,
) -> Result<Option<f64>> {
const WARMUP: usize = 8;
let target = WARMUP + measure_frames;
let mut demuxer = streaming::demux_streaming(input).context("demux for decode bench")?;
let mut decoder = decode::create_decoder_on(codec_name, info.clone(), Some(gpu))
.context("create decoder for bench")?;
let mut decoded = 0usize;
let mut clock: Option<Instant> = None;
'outer: loop {
match demuxer.next_video_sample().context("bench next sample")? {
Some(s) => {
decoder.push_sample(&s.data).context("bench push")?;
while decoder.decode_next().context("bench decode")?.is_some() {
decoded += 1;
if decoded == WARMUP {
clock = Some(Instant::now());
}
if decoded >= target {
break 'outer;
}
}
}
None => {
decoder.finish().context("bench finish")?;
while decoder.decode_next().context("bench drain")?.is_some() {
decoded += 1;
if decoded == WARMUP {
clock = Some(Instant::now());
}
if decoded >= target {
break 'outer;
}
}
break;
}
}
}
let measured = decoded.saturating_sub(WARMUP);
Ok(match clock {
Some(t) if measured > 0 => {
let secs = t.elapsed().as_secs_f64();
(secs > 0.0).then_some(measured as f64 / secs)
}
_ => (decoded > 0).then_some(decoded as f64),
})
}
fn fan_out(
senders: &[tokio::sync::mpsc::Sender<VideoFrame>],
frame: VideoFrame,
rt: &tokio::runtime::Handle,
) -> Result<bool> {
let mut any_alive = false;
for (idx, sender) in senders.iter().enumerate() {
let frame_clone = frame.clone();
let sender = sender.clone();
let accepted = rt.block_on(async move { sender.send(frame_clone).await });
match accepted {
Ok(()) => any_alive = true,
Err(_) => {
tracing::warn!(rung_idx = idx, "shared decode pump: rung dropped its receiver");
}
}
}
Ok(any_alive)
}