mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-15 06:52:23 +00:00
craft-video: FilmCraft's later H.264 encoder work (High 10, threads, sub-pel diamond) and its H.264 and ProRes tests * craft-video: H.264 High 10 and High 10 Intra in the encoder The encoder is generic over the sample type (u8 at 8 bits, u16 at 9-10 bits): QP' = QP + QpBdOffset for quantisation and lambdas, mb_qp_delta range and wrap, chroma QP mapping, deblocking thresholds scaled to the bit depth, clipping in prediction, interpolation and reconstruction. SPS bit depths, profile_idc 110, constraint_set3 for High 10 Intra, avcC high-profile fields, the High 10 cpbBrVclFactor for level choice. New YuvFrame16 / Encoder::encode16, EncoderConfig::bit_depth / intra_only. 8-bit output is unchanged. Merged onto craft-video's copy: its input stride checks, B-frame timestamp check, configuration validation, level 6.2 refusal, motion search start candidates and codec_params are kept. Ported from storytold/filmcraft#1580 (commit d24ce15e). * craft-video: the H.264 encoder honours threads: 1 and reuses MbInfo buffers; raw-YUV bench - A pool is built for every thread count, including 1: rayon calls outside install run on the global pool (all cores), so threads: 1 used to run on every core. A pool that cannot be built is an InvalidConfig error. - Up to eight macroblock-info buffers are recycled between pictures and cleared on reuse, instead of allocating one per picture. - examples/h264_h264enc_bench.rs encodes a raw YUV file and reports speed; its argument checks run as tests. Ported from storytold/filmcraft#3205 (merge 08f8075a). * craft-video: H.264 encoder sub-pel refinement on a 4-point diamond; 16x8 and 8x16 only after 8x8 beat 16x16 Sub-pel refinement tries the four axis neighbours instead of all eight, and the 16x8 / 8x16 partitions are searched only when 8x8 beat 16x16: fewer SATD evaluations for about the same decisions. craft-video's own motion search start candidates are kept. Ported from storytold/filmcraft#3207 (merge 8db5334c). * craft-video: FilmCraft's H.264 encoder tests; EncoderConfig::writer names the program in the SEI The encoder had no integration tests here. FilmCraft's come along as the h264_encode test target: ffmpeg/ffprobe oracle decodes (8-bit and High 10, bit-exact against the reconstruction), pinned decisions, CRF level choice and the settings SEI, thread counts and VBR overshoot. EncoderConfig::writer is the program and version the identification SEI names (craft-h264enc and this crate's version by default), so an application's files keep naming it. Two expectations differ from FilmCraft's copy: the pinned slice hashes (this encoder also starts the 16x16 search from the skip, zero and co-located vectors; with those left out and FilmCraft's writer name, it gives FilmCraft's four hashes exactly) and a CRF that is not a number, which this encoder refuses. * craft-video: FilmCraft's ProRes tests (oracle decode and encode, round trip, fuzz, perf) craft-video's ProRes copy came with only its hostile-input test. FilmCraft's ffmpeg oracle decode and encode tests, round trips, mutation fuzz and the ignored throughput tests come along; craft-video's own fuzz_hostile stays. Frame::new returns a Result here, so the tests unwrap it. --------- Co-authored-by: JordyUSA <75485565+JordyUSA@users.noreply.github.com> Co-authored-by: vangel76 <vangel1976@googlemail.com>
61 lines
2.4 KiB
Rust
61 lines
2.4 KiB
Rust
//! Constant quality has no bitrate cap, so the level must allow what low factors spend; and every
|
|
//! stream says which settings made it.
|
|
|
|
use crate::common;
|
|
|
|
use common::synth;
|
|
use craft_video::h264::*;
|
|
|
|
fn crf(w: u32, h: u32, fps: u32, f: f32) -> Encoder {
|
|
let mut c = EncoderConfig::new(w, h, fps, 1);
|
|
c.rate = RateControl::Crf(f);
|
|
c.format = PacketFormat::LengthPrefixed;
|
|
Encoder::new(c).unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn crf_level_allows_the_bitrate_of_the_factor() {
|
|
let level = |w, h, fps, f| crf(w, h, fps, f).avcc()[3];
|
|
// 1080p30: the default and visually lossless factors stay at 4; CRF 0 (measured at 105 Mb/s
|
|
// average, 147 Mb/s peak on camera footage) needs more than 4.2's 62.5 Mb/s
|
|
assert_eq!(level(1920, 1080, 30, 23.0), 40);
|
|
assert_eq!(level(1920, 1080, 30, 18.0), 40);
|
|
assert_eq!(level(1920, 1080, 30, 0.0), 51);
|
|
// never below what the picture size and rate need
|
|
assert_eq!(level(3840, 2160, 30, 51.0), 51);
|
|
// a factor out of range is clamped; one that is not a number is refused
|
|
assert_eq!(level(1920, 1080, 30, -100.0), 51);
|
|
let mut c = EncoderConfig::new(1920, 1080, 30, 1);
|
|
c.rate = RateControl::Crf(f32::NAN);
|
|
assert!(matches!(Encoder::new(c), Err(Error::InvalidConfig(_))));
|
|
}
|
|
|
|
#[test]
|
|
fn first_access_unit_carries_the_settings() {
|
|
let (w, h) = (64, 48);
|
|
let mut enc = crf(w as u32, h as u32, 30, 18.0);
|
|
let f = synth(w, h, 0);
|
|
let mut packets = enc.encode(&f.frame(), 0).unwrap();
|
|
packets.extend(enc.flush());
|
|
let text = |p: &Packet| String::from_utf8_lossy(&p.data).into_owned();
|
|
let first = text(&packets[0]);
|
|
for want in ["craft-h264enc", "rc=crf crf=18.0", "profile=high", "level=", "bframes=2", "cabac=1"] {
|
|
assert!(first.contains(want), "{want} missing from {first:?}");
|
|
}
|
|
// once per stream
|
|
assert!(packets.iter().skip(1).all(|p| !text(p).contains("craft-h264enc")));
|
|
}
|
|
|
|
#[test]
|
|
fn the_writer_named_in_the_stream_is_the_configured_one() {
|
|
let (w, h) = (64, 48);
|
|
let mut cfg = EncoderConfig::new(w as u32, h as u32, 30, 1);
|
|
cfg.writer = "SomeApp 1.2.3".into();
|
|
let mut enc = Encoder::new(cfg).unwrap();
|
|
let mut packets = enc.encode(&synth(w, h, 0).frame(), 0).unwrap();
|
|
packets.extend(enc.flush());
|
|
let first = String::from_utf8_lossy(&packets[0].data).into_owned();
|
|
assert!(first.contains("SomeApp 1.2.3 - clean-room H.264 encoder - options: "), "{first:?}");
|
|
assert!(!first.contains("craft-h264enc"), "{first:?}");
|
|
}
|