makepad/platform/video/examples/stream_decode_check.rs
2026-10-26 02:43:09 +01:00

104 lines
4.2 KiB
Rust

//! stream_decode_check <in.h264|in.h265>... [--cpu]: the platform's stream decoder (as the Desk Link
//! viewer drives it: GPU pictures, one access unit per push) over Annex-B files, without a
//! window. Prints the pictures decoded, their size and chroma, whether the session runs on the
//! hardware decoder and the decode time per picture; exits 1 unless every picture decoded.
//! `.h265` / `.hevc` (or `--hevc`): H.265, else H.264.
//!
//! Several files (a file may repeat): each decodes on its own thread at once, as the viewer
//! decodes a banded stream (a decoder thread per stream); the last line is the wall time per
//! picture of them all together, what bounds the viewer's picture rate.
struct Run {
line: String,
pictures: i64,
ok: bool,
}
fn decode(path: &str, hevc_flag: bool, gpu: bool) -> Run {
use makepad_video::annex_b;
use makepad_video::{StreamVideoCodec, VideoStreamDecoder};
let data = std::fs::read(path).unwrap_or_else(|e| {
eprintln!("{path}: {e}");
std::process::exit(2)
});
let hevc = path.ends_with(".h265") || path.ends_with(".hevc") || hevc_flag;
let codec = if hevc { StreamVideoCodec::Hevc } else { StreamVideoCodec::H264 };
let mut decoder = (if gpu { VideoStreamDecoder::new_gpu(codec) } else { VideoStreamDecoder::new(codec) }).unwrap_or_else(|e| {
eprintln!("decoder: {e:?}");
std::process::exit(1)
});
let (mut unit, mut pictures, mut decoded, mut failed, mut full_chroma) = (Vec::new(), 0i64, 0i64, 0u32, None);
let mut size = (0, 0);
let started = std::time::Instant::now();
for nal in annex_b::split_annex_b(&data) {
unit.extend_from_slice(&[0, 0, 0, 1]);
unit.extend_from_slice(nal);
let vcl = if hevc { annex_b::hevc_nal_unit_type(nal) < 32 } else { matches!(annex_b::nal_unit_type(nal), 1 | 5) };
if !vcl {
continue;
}
match decoder.push_packet(&unit, pictures) {
Ok(frames) => {
for f in frames {
decoded += 1;
size = (f.width, f.height);
if let Some(g) = &f.gpu {
full_chroma = Some(g.full_chroma);
}
}
}
Err(e) => {
failed += 1;
if failed <= 3 {
eprintln!("picture {pictures}: {e:?}");
}
}
}
pictures += 1;
unit.clear();
}
if let Ok(frames) = decoder.flush() {
decoded += frames.len() as i64;
}
let line = format!(
"{} {}: {pictures} pictures, {decoded} decoded, {failed} failed, {}x{}, chroma {}, {:.2} ms per picture, hardware decoder: {}",
if hevc { "H.265" } else { "H.264" },
path,
size.0,
size.1,
match full_chroma {
Some(true) => "4:4:4",
Some(false) => "4:2:0",
None => "(cpu nv12)",
},
started.elapsed().as_secs_f64() * 1000.0 / pictures.max(1) as f64,
match decoder.hardware() {
Some(true) => "yes",
Some(false) => "NO (software)",
None => "not reported",
}
);
Run { line, pictures, ok: decoded > 0 && decoded == pictures }
}
fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
let files: Vec<String> = args.iter().filter(|a| !a.starts_with("--")).cloned().collect();
if files.is_empty() {
eprintln!("usage: stream_decode_check <in.h264|in.h265>... [--hevc] [--cpu]");
std::process::exit(2);
}
let hevc = args.iter().any(|a| a == "--hevc");
let gpu = !args.iter().any(|a| a == "--cpu");
let started = std::time::Instant::now();
let threads: Vec<_> = files.iter().cloned().map(|f| std::thread::spawn(move || decode(&f, hevc, gpu))).collect();
let runs: Vec<Run> = threads.into_iter().map(|t| t.join().unwrap()).collect();
for r in &runs {
println!("{}", r.line);
}
if runs.len() > 1 {
let most = runs.iter().map(|r| r.pictures).max().unwrap_or(0);
println!("together ({} streams at once): {:.2} ms per picture", runs.len(), started.elapsed().as_secs_f64() * 1000.0 / most.max(1) as f64);
}
std::process::exit(if runs.iter().all(|r| r.ok) { 0 } else { 1 });
}