492 lines
22 KiB
Rust
492 lines
22 KiB
Rust
//! VA-API decoding against our software decoders (Linux): every picture of H.264 and HEVC fixtures
|
|
//! covering the stream features the hardware path takes (H.264: CAVLC and CABAC, B-pyramids,
|
|
//! several references, weighted prediction, temporal direct, custom scaling matrices, several
|
|
//! slices per picture; HEVC Main and Main 10: open GOP with RASL pictures, scaling lists, several
|
|
//! slices with wavefront parallel processing, weighted prediction, transform skip and AMP; 1080p /
|
|
//! 2160p) must be identical (bit-exact planes, colour, pixel aspect, pts and presentation
|
|
//! order), also after `reset` + reseek and a mid-stream `flush`; a forced mid-stream failure must
|
|
//! continue in software with the software decoder's exact output; what the hardware path does not
|
|
//! take (interlaced and 10-bit H.264, 4:2:2 HEVC) is declined; damaged samples and parameter sets give
|
|
//! errors or fall back, never crash or hang. Skips without ffmpeg (fixture generator) or without a
|
|
//! VA-API driver.
|
|
#![cfg(target_os = "linux")]
|
|
|
|
mod common;
|
|
|
|
use std::time::Duration;
|
|
|
|
use common::*;
|
|
use filmcraft_codecs::VideoDecoder;
|
|
use filmcraft_codecs::hw::NalStreamInfo;
|
|
use filmcraft_platform::HybridDecoder;
|
|
use filmcraft_platform::vaapi::VaDecoder;
|
|
|
|
/// The hardware decoder for a stream, or `None` (skip) when this machine has none for it.
|
|
fn hardware(s: &Stream) -> Option<Box<dyn VideoDecoder>> {
|
|
match filmcraft_platform::vaapi_factory(&s.entry) {
|
|
Some(Ok(d)) => {
|
|
assert!(d.name().starts_with("VA-API"), "{}", d.name());
|
|
Some(d)
|
|
}
|
|
Some(Err(e)) => panic!("factory error: {e}"),
|
|
None => {
|
|
eprintln!("SKIPPED: no VA-API hardware decoder for {}", s.entry.codec.name());
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
fn software(s: &Stream) -> Box<dyn VideoDecoder> {
|
|
filmcraft_codecs::software_video_decoder(&s.entry).unwrap()
|
|
}
|
|
|
|
/// H.264 fixtures for the stream features the hardware path takes: (file, x264 profile, x264
|
|
/// parameters). All 640x360 (coded 368: cropping), three seconds at 24 fps, keyframes every second.
|
|
const H264: &[(&str, &str, &str)] = &[
|
|
("va_h264_cbp_slices.mp4", "baseline", "keyint=24:min-keyint=24:scenecut=0:slices=3:ref=3"),
|
|
("va_h264_main_weighted.mp4", "main", "bframes=3:b-pyramid=normal:keyint=24:min-keyint=24:scenecut=0:ref=4:weightp=2:weightb=1"),
|
|
("va_h264_temporal_direct.mp4", "high", "bframes=2:b-pyramid=strict:direct=temporal:keyint=24:min-keyint=24:scenecut=0:ref=3:weightb=0"),
|
|
("va_h264_cqm_jvt.mp4", "high", "bframes=3:keyint=24:min-keyint=24:scenecut=0:cqm=jvt:8x8dct=1:slices=2"),
|
|
("va_h264_open_gop.mp4", "high", "bframes=3:b-pyramid=normal:keyint=24:min-keyint=24:scenecut=0:open-gop=1"),
|
|
];
|
|
|
|
fn h264_fixture(ff: &std::path::Path, name: &str, profile: &str, params: &str) -> Option<std::path::PathBuf> {
|
|
let src = "testsrc2=s=640x360:r=24:d=3,noise=alls=12:allf=t";
|
|
fixture(ff, name, &["-f", "lavfi", "-i", src, "-c:v", "libx264", "-profile:v", profile, "-preset", "fast", "-x264-params", params, "-pix_fmt", "yuv420p"])
|
|
}
|
|
|
|
/// HEVC fixtures for the stream features the hardware path takes: (file, pixel format, x265
|
|
/// parameters). 640x360, three seconds at 24 fps, keyframes every second.
|
|
const HEVC: &[(&str, &str, &str)] = &[
|
|
("va_hevc_scaling_lists.mp4", "yuv420p", "log-level=error:bframes=3:keyint=24:min-keyint=24:scenecut=0:scaling-list=default"),
|
|
("va_hevc_slices_wpp.mp4", "yuv420p10le", "log-level=error:bframes=3:keyint=24:min-keyint=24:scenecut=0:slices=3:wpp=1"),
|
|
("va_hevc_weighted.mp4", "yuv420p", "log-level=error:bframes=3:keyint=24:min-keyint=24:scenecut=0:weightp=1:weightb=1:ref=4"),
|
|
("va_hevc_tskip_amp.mp4", "yuv420p10le", "log-level=error:bframes=4:b-pyramid=1:keyint=24:min-keyint=24:scenecut=0:tskip=1:amp=1:rect=1:open-gop=1"),
|
|
];
|
|
|
|
fn hevc_fixture(ff: &std::path::Path, name: &str, pix_fmt: &str, params: &str) -> Option<std::path::PathBuf> {
|
|
let src = "testsrc2=s=640x360:r=24:d=3,noise=alls=12:allf=t";
|
|
let profile = if pix_fmt.contains("10") { "main10" } else { "main" };
|
|
fixture(
|
|
ff,
|
|
name,
|
|
&["-f", "lavfi", "-i", src, "-c:v", "libx265", "-preset", "fast", "-profile:v", profile, "-tag:v", "hvc1", "-x265-params", params, "-pix_fmt", pix_fmt],
|
|
)
|
|
}
|
|
|
|
/// The small fixtures: the shared H.264 High, HEVC Main (open GOP) and Main 10 ones and the
|
|
/// feature fixtures above.
|
|
fn small_streams(ff: &std::path::Path) -> Vec<(String, Stream)> {
|
|
let mut out = Vec::new();
|
|
for name in ["h264_high.mp4", "hevc_main.mp4", "hevc_main10.mp4"] {
|
|
if let Some(path) = named(ff, name) {
|
|
out.push((name.to_string(), read_stream(&path)));
|
|
}
|
|
}
|
|
for (name, profile, params) in H264 {
|
|
if let Some(path) = h264_fixture(ff, name, profile, params) {
|
|
out.push((name.to_string(), read_stream(&path)));
|
|
}
|
|
}
|
|
for (name, pix_fmt, params) in HEVC {
|
|
if let Some(path) = hevc_fixture(ff, name, pix_fmt, params) {
|
|
out.push((name.to_string(), read_stream(&path)));
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
/// Every picture of `s` from the hardware decoder equals the software decoder's: whole stream,
|
|
/// after `reset` + reseek to each later sync sample, after a mid-stream `flush`, and a full pass
|
|
/// after all the resets.
|
|
fn parity(name: &str, s: &Stream) {
|
|
let Some(mut hw) = hardware(s) else { return };
|
|
let mut sw = software(s);
|
|
let a = decode_all(hw.as_mut(), &s.samples);
|
|
let b = decode_all(sw.as_mut(), &s.samples);
|
|
assert!(a.len() >= 40, "{name}: {} pictures", a.len());
|
|
assert_same(name, &a, &b);
|
|
assert!(hw.name().starts_with("VA-API"), "{name}: stayed in hardware ({})", hw.name());
|
|
|
|
// reset + reseek to each later sync sample, decode a stretch. After a seek to a non-IDR I picture
|
|
// (open GOP), the pictures shown before it are predicted from frames that were never decoded:
|
|
// both decoders conceal them (the software decoder from mid-gray stand-ins, the hardware also
|
|
// from motion data those stand-ins do not have), so only the pictures from the seek point on
|
|
// must match, as they must.
|
|
let info = NalStreamInfo::from_entry(&s.entry).unwrap().unwrap();
|
|
let syncs: Vec<usize> = (1..s.samples.len()).filter(|&i| s.sync[i]).collect();
|
|
assert!(!syncs.is_empty(), "{name}: more than one GOP");
|
|
for &k in syncs.iter().rev() {
|
|
let end = (k + 17).min(s.samples.len());
|
|
for d in [&mut hw, &mut sw] {
|
|
d.reset();
|
|
}
|
|
let mut a = decode_all(hw.as_mut(), &s.samples[k..end]);
|
|
let mut b = decode_all(sw.as_mut(), &s.samples[k..end]);
|
|
assert_eq!(a.len(), b.len(), "{name} from sample {k}: pictures");
|
|
if info.codec == filmcraft_codecs::hw::NalCodec::H264 && !info.nal_types(&s.samples[k].0).contains(&5) {
|
|
let start = s.samples[k].1;
|
|
a.retain(|f| f.pts >= start);
|
|
b.retain(|f| f.pts >= start);
|
|
}
|
|
assert!(!a.is_empty(), "{name}: pictures after seeking to {k}");
|
|
assert_same(&format!("{name} from sample {k}"), &a, &b);
|
|
}
|
|
// a flush in the middle of a GOP, then carrying on: H.264 only, where references survive a
|
|
// flush (an HEVC flush ends the stream; the GOP cache always seeks after one)
|
|
if info.codec == filmcraft_codecs::hw::NalCodec::H264 {
|
|
for d in [&mut hw, &mut sw] {
|
|
d.reset();
|
|
}
|
|
let mid = s.samples.len() / 2;
|
|
let mut a = Vec::new();
|
|
let mut b = Vec::new();
|
|
for (d, out) in [(&mut hw, &mut a), (&mut sw, &mut b)] {
|
|
for (smp, pts) in &s.samples[..mid] {
|
|
out.extend(d.decode(smp, *pts).unwrap());
|
|
}
|
|
out.extend(d.flush());
|
|
out.extend(decode_all(d.as_mut(), &s.samples[mid..]));
|
|
}
|
|
assert_same(&format!("{name} with a flush at {mid}"), &a, &b);
|
|
}
|
|
// back to the start after seeking around: the whole stream again
|
|
hw.reset();
|
|
sw.reset();
|
|
assert_same(&format!("{name} after resets"), &decode_all(hw.as_mut(), &s.samples), &decode_all(sw.as_mut(), &s.samples));
|
|
assert!(hw.name().starts_with("VA-API"), "{name}: stayed in hardware ({})", hw.name());
|
|
// random-access / disposable answers match the software decoder's
|
|
for (smp, _) in &s.samples {
|
|
assert_eq!(hw.is_random_access(smp), sw.is_random_access(smp), "{name}");
|
|
assert_eq!(hw.is_disposable(smp), sw.is_disposable(smp), "{name}");
|
|
}
|
|
}
|
|
|
|
/// [`parity`] for large streams, with little memory: the two decoders run side by side and each
|
|
/// picture is compared and dropped as soon as both have output it (a 4K 10-bit stream held whole
|
|
/// takes gigabytes). The whole stream, then from the last sync sample after a reset.
|
|
fn parity_streaming(name: &str, s: &Stream) {
|
|
let Some(mut hw) = hardware(s) else { return };
|
|
let mut sw = software(s);
|
|
let last_sync = (1..s.samples.len()).rev().find(|&i| s.sync[i]).unwrap_or(0);
|
|
for start in [0, last_sync] {
|
|
hw.reset();
|
|
sw.reset();
|
|
let (mut a, mut b) = (std::collections::VecDeque::new(), std::collections::VecDeque::new());
|
|
let mut compared = 0usize;
|
|
let mut compare = |a: &mut std::collections::VecDeque<filmcraft_codecs::DecodedFrame>,
|
|
b: &mut std::collections::VecDeque<filmcraft_codecs::DecodedFrame>| {
|
|
while !a.is_empty() && !b.is_empty() {
|
|
let (Some(x), Some(y)) = (a.pop_front(), b.pop_front()) else { break };
|
|
assert_same(&format!("{name} from sample {start}"), &[x], &[y]);
|
|
compared += 1;
|
|
}
|
|
};
|
|
for (smp, pts) in &s.samples[start..] {
|
|
a.extend(hw.decode(smp, *pts).unwrap());
|
|
b.extend(sw.decode(smp, *pts).unwrap());
|
|
compare(&mut a, &mut b);
|
|
}
|
|
a.extend(hw.flush());
|
|
b.extend(sw.flush());
|
|
compare(&mut a, &mut b);
|
|
assert!(a.is_empty() && b.is_empty(), "{name} from sample {start}: picture counts differ");
|
|
assert!(compared >= s.samples.len().saturating_sub(start) / 2, "{name} from sample {start}: {compared} pictures");
|
|
assert!(hw.name().starts_with("VA-API"), "{name}: stayed in hardware ({})", hw.name());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn bit_exact_with_the_software_decoder() {
|
|
let ff = filmcraft_testkit::require_ffmpeg!();
|
|
let streams = small_streams(&ff);
|
|
assert!(streams.len() > H264.len(), "fixtures");
|
|
for (name, s) in streams {
|
|
parity(&name, &s);
|
|
}
|
|
}
|
|
|
|
fn large(size: &str) {
|
|
let ff = filmcraft_testkit::require_ffmpeg!();
|
|
let (_, h) = size.split_once('x').unwrap();
|
|
let src = format!("testsrc2=s={size}:r=24:d=2,noise=alls=12:allf=t");
|
|
let args = [
|
|
"-f",
|
|
"lavfi",
|
|
"-i",
|
|
&src,
|
|
"-c:v",
|
|
"libx264",
|
|
"-profile:v",
|
|
"high",
|
|
"-preset",
|
|
"veryfast",
|
|
"-x264-params",
|
|
"bframes=3:b-pyramid=normal:keyint=24:min-keyint=24:scenecut=0",
|
|
"-pix_fmt",
|
|
"yuv420p",
|
|
];
|
|
let Some(path) = fixture(&ff, &format!("h264_{h}.mp4"), &args) else { return };
|
|
parity_streaming(&format!("h264 {size}"), &read_stream(&path));
|
|
}
|
|
|
|
#[test]
|
|
fn h264_1080p_and_2160p_are_bit_exact() {
|
|
large("1920x1080");
|
|
large("3840x2160");
|
|
}
|
|
|
|
fn large_hevc(size: &str, pix_fmt: &str) {
|
|
let ff = filmcraft_testkit::require_ffmpeg!();
|
|
let (_, h) = size.split_once('x').unwrap();
|
|
let src = format!("testsrc2=s={size}:r=24:d=2,noise=alls=12:allf=t");
|
|
let ten = pix_fmt.contains("10");
|
|
let args = [
|
|
"-f",
|
|
"lavfi",
|
|
"-i",
|
|
&src,
|
|
"-c:v",
|
|
"libx265",
|
|
"-preset",
|
|
"ultrafast",
|
|
"-profile:v",
|
|
if ten { "main10" } else { "main" },
|
|
"-tag:v",
|
|
"hvc1",
|
|
"-x265-params",
|
|
"log-level=error:bframes=4:keyint=24:min-keyint=24:scenecut=0:open-gop=1",
|
|
"-pix_fmt",
|
|
pix_fmt,
|
|
];
|
|
let name = format!("va_hevc_{}_{h}.mp4", if ten { "main10" } else { "main" });
|
|
let Some(path) = fixture(&ff, &name, &args) else { return };
|
|
parity_streaming(&name, &read_stream(&path));
|
|
}
|
|
|
|
#[test]
|
|
fn hevc_main_and_main10_1080p_and_2160p_are_bit_exact() {
|
|
for size in ["1920x1080", "3840x2160"] {
|
|
large_hevc(size, "yuv420p");
|
|
large_hevc(size, "yuv420p10le");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn mid_stream_failure_continues_in_software() {
|
|
let ff = filmcraft_testkit::require_ffmpeg!();
|
|
for (name, s) in small_streams(&ff) {
|
|
let name = name.as_str();
|
|
let info = NalStreamInfo::from_entry(&s.entry).unwrap().unwrap();
|
|
let Ok(_) = VaDecoder::new(info.clone()) else {
|
|
eprintln!("SKIPPED: no VA-API hardware decoder for {name}");
|
|
continue;
|
|
};
|
|
let reference = decode_all(software(&s).as_mut(), &s.samples);
|
|
assert!(!reference.is_empty(), "{name}");
|
|
// fail at the first sample, inside the first GOP, at a sync sample and just after one
|
|
let k = (1..s.samples.len()).find(|&i| s.sync[i]).unwrap();
|
|
for fail_at in [0, 5, k, k + 1, k + 9] {
|
|
let mut va = VaDecoder::new(info.clone()).unwrap();
|
|
va.fail_after(fail_at as u64);
|
|
let mut d = HybridDecoder::new(Box::new(va), s.entry.clone(), info.clone());
|
|
let before = filmcraft_codecs::hw::hw_stats().fallbacks;
|
|
let out = decode_all(&mut d, &s.samples);
|
|
assert!(!d.is_hardware(), "{name}: switched to software");
|
|
assert!(filmcraft_codecs::hw::hw_stats().fallbacks > before, "{name}: fallback counted");
|
|
assert_same(&format!("{name} failing at sample {fail_at}"), &out, &reference);
|
|
d.reset();
|
|
assert_same(&format!("{name} after the fallback and a reset"), &decode_all(&mut d, &s.samples), &reference);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// What the hardware path does not take is declined (the software decoders keep it): interlaced
|
|
/// and 10-bit H.264, and HEVC (not decoded through VA-API yet).
|
|
#[test]
|
|
fn declines_what_the_hardware_path_does_not_take() {
|
|
let ff = filmcraft_testkit::require_ffmpeg!();
|
|
let src = "testsrc2=s=640x360:r=24:d=1";
|
|
let cases: [(&str, Vec<&str>); 2] = [
|
|
("va_h264_interlaced.mp4", vec!["-f", "lavfi", "-i", src, "-c:v", "libx264", "-x264-params", "interlaced=1:keyint=12", "-pix_fmt", "yuv420p"]),
|
|
("va_h264_10bit.mp4", vec!["-f", "lavfi", "-i", src, "-c:v", "libx264", "-profile:v", "high10", "-pix_fmt", "yuv420p10le"]),
|
|
];
|
|
for (name, args) in cases {
|
|
// an ffmpeg whose x264 lacks 10-bit leaves the fixture out
|
|
let Some(path) = fixture(&ff, name, &args) else { continue };
|
|
let s = read_stream(&path);
|
|
assert!(filmcraft_platform::vaapi_factory(&s.entry).is_none(), "{name}: declined");
|
|
}
|
|
let args = ["-f", "lavfi", "-i", src, "-c:v", "libx265", "-tag:v", "hvc1", "-x265-params", "log-level=error:keyint=12", "-pix_fmt", "yuv422p10le"];
|
|
if let Some(path) = fixture(&ff, "va_hevc_422.mp4", &args) {
|
|
let s = read_stream(&path);
|
|
assert!(filmcraft_platform::vaapi_factory(&s.entry).is_none(), "HEVC 4:2:2: declined");
|
|
}
|
|
// Settings ▸ Playback ▸ Hardware decoding Off
|
|
let Some(path) = named(&ff, "h264_high.mp4") else { return };
|
|
let s = read_stream(&path);
|
|
filmcraft_codecs::hw::set_hardware_decoding(false);
|
|
let off = filmcraft_platform::vaapi_factory(&s.entry).is_none();
|
|
filmcraft_codecs::hw::set_hardware_decoding(true);
|
|
assert!(off, "declined while hardware decoding is Off");
|
|
}
|
|
|
|
/// Run `f` on a thread; fail if it panics or takes longer than `limit`.
|
|
fn bounded(what: &str, limit: Duration, f: impl FnOnce() + Send + 'static) {
|
|
let (tx, rx) = std::sync::mpsc::channel();
|
|
std::thread::spawn(move || {
|
|
let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
|
|
let _ = tx.send(r.is_ok());
|
|
});
|
|
match rx.recv_timeout(limit) {
|
|
Ok(true) => {}
|
|
Ok(false) => panic!("{what}: panicked"),
|
|
Err(_) => panic!("{what}: hung"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn damaged_samples_and_parameter_sets_never_crash_or_hang() {
|
|
let ff = filmcraft_testkit::require_ffmpeg!();
|
|
for (name, s) in small_streams(&ff) {
|
|
let name = name.as_str();
|
|
if hardware(&s).is_none() {
|
|
continue;
|
|
}
|
|
// damaged samples (bit flips, truncation, corrupt length prefixes): through the hybrid, and
|
|
// straight into the hardware decoder (no software to fall back to)
|
|
for seed in 1..=8u64 {
|
|
let samples = s.samples.clone();
|
|
let entry = s.entry.clone();
|
|
bounded(&format!("{name} seed {seed}"), Duration::from_secs(60), move || {
|
|
let mut seed = seed.wrapping_mul(0x9E37_79B9_7F4A_7C15) | 1;
|
|
let mut d = filmcraft_platform::vaapi_factory(&entry).unwrap().unwrap();
|
|
let info = NalStreamInfo::from_entry(&entry).unwrap().unwrap();
|
|
let mut raw = VaDecoder::new(info).unwrap();
|
|
for (i, (smp, pts)) in samples.iter().enumerate().take(40) {
|
|
let mut m = smp.clone();
|
|
match xorshift(&mut seed) % 4 {
|
|
0 => {
|
|
for _ in 0..1 + xorshift(&mut seed) % 8 {
|
|
let at = (xorshift(&mut seed) as usize) % m.len().max(1);
|
|
if let Some(b) = m.get_mut(at) {
|
|
*b ^= 1 << (xorshift(&mut seed) % 8);
|
|
}
|
|
}
|
|
}
|
|
1 => m.truncate((xorshift(&mut seed) as usize) % m.len().max(1)),
|
|
2 if m.len() > 4 => m[..4].copy_from_slice(&(xorshift(&mut seed) as u32).to_be_bytes()),
|
|
_ => {}
|
|
}
|
|
let _ = d.decode(&m, *pts);
|
|
let _ = raw.decode(&m, *pts);
|
|
if i % 13 == 12 {
|
|
d.reset();
|
|
raw.reset();
|
|
}
|
|
}
|
|
let _ = d.flush();
|
|
let _ = raw.flush();
|
|
});
|
|
}
|
|
// damaged parameter sets: the decoder declines, errors or decodes; never crashes
|
|
let filmcraft_isobmff::CodecConfig::Avc(a) = &s.entry.codec else { continue };
|
|
let rec = a.to_bytes();
|
|
let mut seed = 0x1234_5678u64;
|
|
for round in 0..24 {
|
|
let mut r = rec.clone();
|
|
if round % 3 == 2 {
|
|
r.truncate(r.len() * (round + 1) / 30);
|
|
} else {
|
|
for _ in 0..1 + round % 4 {
|
|
let at = 6 + (xorshift(&mut seed) as usize) % r.len().saturating_sub(6).max(1);
|
|
if let Some(b) = r.get_mut(at) {
|
|
*b ^= 1 << (xorshift(&mut seed) % 8);
|
|
}
|
|
}
|
|
}
|
|
let samples: Vec<_> = s.samples.iter().take(12).cloned().collect();
|
|
bounded(&format!("{name} record round {round}"), Duration::from_secs(60), move || {
|
|
let Ok(info) = NalStreamInfo::from_avcc(&r) else { return };
|
|
let Ok(mut va) = VaDecoder::new(info) else { return };
|
|
for (smp, pts) in &samples {
|
|
if va.decode(smp, *pts).is_err() {
|
|
break;
|
|
}
|
|
}
|
|
let _ = va.flush();
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Creating and dropping decoders again and again (every clip on a timeline makes one) neither
|
|
/// fails nor leaks VA-API objects into the next one.
|
|
#[test]
|
|
fn repeated_initialisation() {
|
|
let ff = filmcraft_testkit::require_ffmpeg!();
|
|
let Some(path) = named(&ff, "h264_high.mp4") else { return };
|
|
let s = read_stream(&path);
|
|
let reference = decode_all(software(&s).as_mut(), &s.samples[..13]);
|
|
for round in 0..40 {
|
|
let Some(mut hw) = hardware(&s) else { return };
|
|
if round % 8 == 0 {
|
|
assert_same(&format!("round {round}"), &decode_all(hw.as_mut(), &s.samples[..13]), &reference);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Several decoders at once on different threads (the frame workers) give the software decoder's
|
|
/// pictures.
|
|
#[test]
|
|
fn concurrent_decoders() {
|
|
let ff = filmcraft_testkit::require_ffmpeg!();
|
|
let mut paths: Vec<_> = named(&ff, "h264_high.mp4").into_iter().collect();
|
|
for (name, profile, params) in H264.iter().take(3) {
|
|
paths.extend(h264_fixture(&ff, name, profile, params));
|
|
}
|
|
let jobs: Vec<_> = paths
|
|
.into_iter()
|
|
.chain(named(&ff, "h264_high.mp4"))
|
|
.map(|path| {
|
|
std::thread::spawn(move || {
|
|
let s = read_stream(&path);
|
|
let Some(mut hw) = hardware(&s) else { return };
|
|
let n = s.samples.len().min(40);
|
|
let a = decode_all(hw.as_mut(), &s.samples[..n]);
|
|
hw.reset();
|
|
let b = decode_all(hw.as_mut(), &s.samples[..n]);
|
|
let c = decode_all(software(&s).as_mut(), &s.samples[..n]);
|
|
assert_same("concurrent", &a, &c);
|
|
assert_same("concurrent, after a reset", &b, &c);
|
|
})
|
|
})
|
|
.collect();
|
|
for j in jobs {
|
|
j.join().unwrap();
|
|
}
|
|
}
|
|
|
|
/// A decoder used and moved between threads (decoders migrate between frame workers).
|
|
#[test]
|
|
fn decoder_moves_between_threads() {
|
|
let ff = filmcraft_testkit::require_ffmpeg!();
|
|
let Some(path) = named(&ff, "h264_high.mp4") else { return };
|
|
let s = read_stream(&path);
|
|
let Some(mut hw) = hardware(&s) else { return };
|
|
let reference = decode_all(software(&s).as_mut(), &s.samples[..30]);
|
|
let samples = s.samples.clone();
|
|
let (hw, out) = std::thread::spawn(move || {
|
|
let out = decode_all(hw.as_mut(), &samples[..30]);
|
|
(hw, out)
|
|
})
|
|
.join()
|
|
.unwrap();
|
|
assert_same("first thread", &out, &reference);
|
|
let mut hw = hw;
|
|
hw.reset();
|
|
let samples = s.samples.clone();
|
|
let out = std::thread::spawn(move || decode_all(hw.as_mut(), &samples[..30])).join().unwrap();
|
|
assert_same("second thread", &out, &reference);
|
|
}
|