mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-13 22:12:12 +00:00
craft-video: MPEG-4 Part 2 (Xvid/DivX) Simple profile encoder
* craft-video: MPEG-4 Part 2 Simple profile encoder Adds a clean-room MPEG-4 Visual (ISO/IEC 14496-2) encoder behind the new `mpeg4` / `mpeg4-encode` features: the stream type Xvid and DivX write, which craft-media already identifies (CodecId::Mpeg4Video) in MP4, Matroska, AVI and MXF but which no crate could produce. - I- and P-VOPs, 4:2:0 8-bit, H.263 quantisation, half-sample motion with a predictor-seeded diamond search, skipped and intra macroblocks in P-VOPs, intra refresh within the 132 codings of §7.4.5. - Constant quantiser or 1-pass average bitrate. - Headers in band (raw .m4v, AVI) or as extradata / CodecParams (MP4, Matroska). Tests use ffmpeg/ffprobe as external decoders: every coefficient table entry and escape mode 1/2 event decodes like the fixed-length escape, and encoded streams decode without diagnostics, close to the encoder's reconstruction. --------- Co-authored-by: alehel <git@ornumhelgaker.no>
This commit is contained in:
1 parent
373c242ad7
commit
77a3ddbf26
12 files changed
+2385
-1
No files matched your search
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "craft-video"
|
||||
description = "Clean-room pure-Rust video codecs, one feature per codec: DNxHD/DNxHR, VP8, ProRes, H.264, HEVC, VP9, AV1."
|
||||
description = "Clean-room pure-Rust video codecs, one feature per codec: DNxHD/DNxHR, VP8, ProRes, H.264, HEVC, VP9, AV1, MPEG-4 Part 2."
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
@@ -27,6 +27,9 @@ vp8-decode = []
|
||||
vp8-encode = []
|
||||
# MPEG-2 video (ITU-T H.262) and MPEG-1 video (ISO/IEC 11172-2): decoder.
|
||||
mpeg2 = []
|
||||
# MPEG-4 Part 2 video (ISO/IEC 14496-2, the Xvid/DivX format): Simple profile encoder.
|
||||
mpeg4 = ["mpeg4-encode"]
|
||||
mpeg4-encode = ["craft-media/bitstream"]
|
||||
# Apple ProRes (SMPTE RDD 36): decoder and encoder.
|
||||
prores = ["craft-media/bitstream"]
|
||||
# H.264 / AVC: decoder and encoder; `h264-decode` or `h264-encode` for one of them.
|
||||
@@ -121,6 +124,10 @@ required-features = ["vp9-decode"]
|
||||
name = "vp9_vp9dec"
|
||||
required-features = ["vp9-decode"]
|
||||
|
||||
[[test]]
|
||||
name = "mpeg4_encode"
|
||||
required-features = ["mpeg4-encode"]
|
||||
|
||||
[[test]]
|
||||
name = "mpeg2"
|
||||
required-features = ["mpeg2"]
|
||||
|
||||
@@ -22,6 +22,7 @@ No codec feature is on by default.
|
||||
| `apv` | [`apv`](src/apv/README.md) | APV (Advanced Professional Video, RFC 9924): decoder (`apv-decode` alone) and encoder (`apv-encode` alone) |
|
||||
| `vp8` | `vp8` | VP8 (RFC 6386): decoder |
|
||||
| `mpeg2` | [`mpeg2`](src/mpeg2/README.md) | MPEG-2 video (ITU-T H.262) and MPEG-1 video (ISO/IEC 11172-2): decoder |
|
||||
| `mpeg4` | [`mpeg4`](src/mpeg4/README.md) | MPEG-4 Part 2 video (ISO/IEC 14496-2, the Xvid/DivX format): Simple profile encoder (`mpeg4-encode`) |
|
||||
| `prores` | `prores` | Apple ProRes (SMPTE RDD 36): decoder and encoder |
|
||||
| `h264` | [`h264`](src/h264/decode/README.md) | ITU-T H.264 / AVC: decoder (`h264-decode` alone) and encoder (`h264-encode` alone) |
|
||||
| `hevc` | [`hevc`](src/hevc/decode/README.md) | ITU-T H.265 / HEVC: decoder (`hevc-decode` alone) and encoder (`hevc-encode` alone) |
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
//! | `apv` | [`apv`] | APV (RFC 9924): decoder (`apv-decode`) and encoder (`apv-encode`) |
|
||||
//! | `vp8` | [`vp8`] | VP8 (RFC 6386): decoder (`vp8-decode`) and key-frame encoder (`vp8-encode`) |
|
||||
//! | `mpeg2` | [`mpeg2`] | MPEG-2 video (ITU-T H.262) and MPEG-1 video: decoder |
|
||||
//! | `mpeg4` | [`mpeg4`] | MPEG-4 Part 2 video (ISO/IEC 14496-2, Xvid/DivX): Simple profile encoder (`mpeg4-encode`) |
|
||||
//! | `prores` | [`prores`] | Apple ProRes (SMPTE RDD 36): decoder and encoder |
|
||||
//! | `h264` | [`h264`] | ITU-T H.264 / AVC: decoder (`h264-decode`) and encoder (`h264-encode`) |
|
||||
//! | `hevc` | [`hevc`] | ITU-T H.265 / HEVC: decoder (`hevc-decode`) and encoder (`hevc-encode`) |
|
||||
@@ -37,6 +38,9 @@ pub mod vp8;
|
||||
#[cfg(feature = "mpeg2")]
|
||||
pub mod mpeg2;
|
||||
|
||||
#[cfg(feature = "mpeg4-encode")]
|
||||
pub mod mpeg4;
|
||||
|
||||
#[cfg(feature = "prores")]
|
||||
pub mod prores;
|
||||
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
# MPEG-4 Part 2 video encoder (`craft_video::mpeg4`, feature `mpeg4`)
|
||||
|
||||
Clean-room MPEG-4 Visual (ISO/IEC 14496-2) encoder in pure Rust: the format Xvid and DivX write.
|
||||
It writes Simple profile streams. Depends on `craft-media` (`bitstream`, `CodecParams`) and
|
||||
`thiserror`. No `unsafe`, no panics. No Xvid, DivX, FFmpeg or other MPEG-4 implementation was
|
||||
read. ffmpeg and ffprobe are used only as external decoders in the tests.
|
||||
|
||||
There is no MPEG-4 Part 2 decoder in this crate. `craft-media` names the stream (`CodecId::Mpeg4Video`)
|
||||
in MP4, Matroska, AVI and MXF.
|
||||
|
||||
## Specifications
|
||||
|
||||
| Document | Used for |
|
||||
|---|---|
|
||||
| ISO/IEC 14496-2 | visual object sequence, visual object, VOL and VOP headers (§6.2), macroblock and block syntax, the VLC tables of Annex B, intra DC prediction (§7.4.3), the second inverse quantisation method (§7.4.2), the IDCT (Annex A) and its saturation, motion vector coding and prediction (§7.6), the intra refresh rule (§7.4.5) |
|
||||
|
||||
## API
|
||||
|
||||
```rust
|
||||
use craft_video::mpeg4::{Encoder, EncoderConfig, RateControl, YuvFrame};
|
||||
|
||||
let mut cfg = EncoderConfig::new(1280, 720, 30000, 1001);
|
||||
cfg.rate = RateControl::Bitrate { kbps: 2500 }; // or RateControl::Quant(1..=31)
|
||||
cfg.keyint = 250;
|
||||
cfg.headers_in_band = false; // MP4 / Matroska: headers in the track
|
||||
let mut enc = Encoder::new(cfg)?;
|
||||
let params = enc.codec_params(); // Mpeg4Video, `mp4v`, size, extradata
|
||||
for frame in frames {
|
||||
let packet = enc.encode(&YuvFrame { y, u, v, y_stride, uv_stride })?;
|
||||
// packet.data: one VOP; packet.keyframe; packet.pts (frame number); packet.quant
|
||||
}
|
||||
```
|
||||
|
||||
- Input is 8-bit 4:2:0. Width and height are even, 2 to 8190.
|
||||
- One picture in, one VOP out, in order. There are no B-VOPs and no delay.
|
||||
- `extradata()` holds `VisualObjectSequence` through `VideoObjectLayer`. That is the MP4 `esds`
|
||||
`DecoderSpecificInfo` and the Matroska `CodecPrivate`. With `headers_in_band` (the default),
|
||||
the extradata also starts every I-VOP packet. A raw `.m4v` stream or an AVI file needs this.
|
||||
- `reconstruction()` returns the picture a decoder reconstructs from the last packet.
|
||||
- `request_keyframe()` codes the next picture as an I-VOP.
|
||||
|
||||
## Coding
|
||||
|
||||
- Headers:
|
||||
- Simple Object Type.
|
||||
- `profile_and_level_indication` is the smallest Simple profile level whose picture size holds
|
||||
the picture (L3, L4a, L5, L6). Bitrate and VBV limits are not checked. Pictures over 3600
|
||||
macroblocks are signalled as L6, which is larger than that level allows.
|
||||
- Rectangular, progressive, 4:2:0, `low_delay`, H.263 quantisation (`quant_type` 0).
|
||||
- No resync markers, no data partitioning.
|
||||
- Fixed VOP rate when the reduced frame rate allows it.
|
||||
- Pixel aspect ratio: a table code where one fits, otherwise the extended form.
|
||||
- Optional colour description.
|
||||
- Time code: `vop_time_increment_resolution` is the reduced frame rate numerator (16 bits at
|
||||
most). `modulo_time_base` counts the seconds between VOPs.
|
||||
- I-VOPs: every macroblock is intra. DC is predicted from the left or upper block (§7.4.3) and
|
||||
coded with the DC size tables. AC prediction is off (`ac_pred_flag` 0), so the scan is always
|
||||
zigzag.
|
||||
- P-VOPs:
|
||||
- One half-sample vector per 16×16 macroblock (no 4MV), with `fcode` 1 or 2 per VOP.
|
||||
- The motion search tries the median predictor, the left and upper vectors and zero, then
|
||||
runs a diamond search in whole samples (cost: SAD plus quantiser × vector bits), then
|
||||
refines to the 8 half-sample positions around the best.
|
||||
- Vectors are limited so that the part of a macroblock inside the picture reads only reference
|
||||
samples inside the picture. The prediction therefore does not depend on how a decoder pads
|
||||
its reference.
|
||||
- `vop_rounding_type` alternates between P-VOPs.
|
||||
- Mode decision in P-VOPs:
|
||||
- Intra when the macroblock's deviation from its mean, plus 500, is below the best SAD.
|
||||
- Skipped (`not_coded`) when the vector is zero and nothing quantises to non-zero.
|
||||
- Every macroblock is coded intra again after 128 inter codings. §7.4.5 requires this within
|
||||
132, to bound IDCT mismatch with the decoder.
|
||||
- Quantisation:
|
||||
- Intra AC: `|c| / 2q`.
|
||||
- Inter: `(|c| - q/2) / 2q`.
|
||||
- Intra DC: rounded to `dc_scaler` (Table 7-1).
|
||||
- Levels are capped at 2047.
|
||||
- Coefficients use Tables B-16 (intra) and B-17 (inter). Escape modes 1 (level offset) and 2
|
||||
(run offset) are used where they fit; otherwise the fixed-length mode 3.
|
||||
- DCT and IDCT are separable in `f64`. The IDCT rounds and saturates to [-256, 255].
|
||||
- Rate control:
|
||||
- `Quant(q)`: every VOP at quantiser `q`.
|
||||
- `Bitrate`: a 1-pass average. Each VOP's quantiser (2–31) comes from a bits × quantiser model
|
||||
kept separately for I and P, aiming at the frame budget minus 1/25 of the running overshoot.
|
||||
I-VOPs aim at 4 frame budgets. The quantiser changes by at most 4 between VOPs of a type.
|
||||
|
||||
## Not supported
|
||||
|
||||
- B-VOPs, quarter-sample motion, global motion compensation (Advanced Simple profile).
|
||||
- AC prediction, 4MV, `dquant` within a VOP.
|
||||
- MPEG quantisation matrices.
|
||||
- Interlace, shapes, sprites, scalability.
|
||||
- Resync markers, data partitioning, reversible VLCs.
|
||||
|
||||
## Tests
|
||||
|
||||
- Unit tests:
|
||||
- The coefficient tables are prefix codes and the intra and inter tables use the same codes.
|
||||
- Every other VLC table is prefix-free.
|
||||
- DC scaler, dequantisation, chroma vector rounding, motion vector wrapping, and the predictor
|
||||
rules at picture edges.
|
||||
- DCT round trip.
|
||||
- Code table oracle (`encode/tests.rs`): every table entry, every escape mode 1 and mode 2
|
||||
event, intra and inter. The table-coded stream must decode in ffmpeg byte for byte like the
|
||||
same coefficients sent with the fixed-length escape. Swapping two table entries fails it.
|
||||
- Integration tests (`tests/mpeg4_encode`) decode with ffmpeg and check:
|
||||
- no diagnostics and the right frame count;
|
||||
- every frame within 48 dB of the encoder's reconstruction, and above a PSNR floor against the
|
||||
source;
|
||||
- quantisers 1 to 31, partial macroblocks (18×34, 98×66, 2×2);
|
||||
- a 150-frame GOP, which exercises the intra refresh and the time code across seconds;
|
||||
- extreme content at quantiser 1;
|
||||
- out-of-band headers given as extradata;
|
||||
- the bitrate mode within 15% of its target.
|
||||
- ffprobe reads the size, aspect ratio, colour description and profile.
|
||||
|
||||
The oracle tests skip when ffmpeg is absent, unless `CRAFT_MEDIA_REQUIRE_ORACLES=1` is set.
|
||||
@@ -0,0 +1,100 @@
|
||||
//! The 8×8 forward and inverse DCT of ISO/IEC 14496-2 Annex A, evaluated separably in `f64`.
|
||||
//! The inverse rounds to the nearest integer and saturates to [-256, 255] (§7.4.4), well inside
|
||||
//! the IEEE Std 1180-1990 accuracy the standard asks of a decoder's IDCT, so a conforming
|
||||
//! decoder's reconstruction stays within a sample value of the encoder's.
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
/// `M[x][u] = C(u)/2 · cos((2x+1)uπ/16)`, `C(0) = 1/√2`, `C(u>0) = 1`.
|
||||
fn basis() -> &'static [[f64; 8]; 8] {
|
||||
static B: OnceLock<[[f64; 8]; 8]> = OnceLock::new();
|
||||
B.get_or_init(|| {
|
||||
let mut m = [[0f64; 8]; 8];
|
||||
for (x, row) in m.iter_mut().enumerate() {
|
||||
for (u, v) in row.iter_mut().enumerate() {
|
||||
let c = if u == 0 { std::f64::consts::FRAC_1_SQRT_2 } else { 1.0 };
|
||||
*v = c / 2.0 * (((2 * x + 1) * u) as f64 * std::f64::consts::PI / 16.0).cos();
|
||||
}
|
||||
}
|
||||
m
|
||||
})
|
||||
}
|
||||
|
||||
/// Forward DCT of 64 samples (raster order) into rounded coefficients.
|
||||
pub(super) fn fdct(px: &[i32; 64], out: &mut [i32; 64]) {
|
||||
let m = basis();
|
||||
// rows: tmp[y][u] = Σ_x M[x][u] f[y][x]
|
||||
let mut tmp = [[0f64; 8]; 8];
|
||||
for y in 0..8 {
|
||||
for u in 0..8 {
|
||||
tmp[y][u] = (0..8).map(|x| m[x][u] * f64::from(px[y * 8 + x])).sum();
|
||||
}
|
||||
}
|
||||
for v in 0..8 {
|
||||
for u in 0..8 {
|
||||
let s: f64 = (0..8).map(|y| m[y][v] * tmp[y][u]).sum();
|
||||
out[v * 8 + u] = s.round() as i32;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// In-place inverse DCT of dequantised coefficients (raster order), saturated to [-256, 255].
|
||||
pub(super) fn idct(blk: &mut [i32; 64]) {
|
||||
if blk[1..].iter().all(|&c| c == 0) {
|
||||
// F[0][0]/8 everywhere, rounded half away from zero.
|
||||
let d = blk[0];
|
||||
let v = if d >= 0 { (d + 4) >> 3 } else { -((-d + 4) >> 3) };
|
||||
blk.fill(v.clamp(-256, 255));
|
||||
return;
|
||||
}
|
||||
let m = basis();
|
||||
// rows: tmp[v][x] = Σ_u M[x][u] F[v][u]
|
||||
let mut tmp = [[0f64; 8]; 8];
|
||||
for v in 0..8 {
|
||||
let r = &blk[v * 8..v * 8 + 8];
|
||||
if r.iter().all(|&c| c == 0) {
|
||||
continue;
|
||||
}
|
||||
for x in 0..8 {
|
||||
tmp[v][x] = (0..8).map(|u| m[x][u] * f64::from(r[u])).sum();
|
||||
}
|
||||
}
|
||||
for y in 0..8 {
|
||||
for x in 0..8 {
|
||||
let s: f64 = (0..8).map(|v| m[y][v] * tmp[v][x]).sum();
|
||||
blk[y * 8 + x] = (s.round() as i32).clamp(-256, 255);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn inverse_undoes_forward() {
|
||||
let mut x = 0x1234_5678u32;
|
||||
for _ in 0..200 {
|
||||
let px: [i32; 64] = std::array::from_fn(|_| {
|
||||
x ^= x << 13;
|
||||
x ^= x >> 17;
|
||||
x ^= x << 5;
|
||||
(x % 256) as i32
|
||||
});
|
||||
let mut c = [0; 64];
|
||||
fdct(&px, &mut c);
|
||||
idct(&mut c);
|
||||
for (a, b) in px.iter().zip(&c) {
|
||||
assert!((a - b).abs() <= 1, "{a} vs {b}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flat_block_has_only_dc() {
|
||||
let mut c = [0; 64];
|
||||
fdct(&[100; 64], &mut c);
|
||||
assert_eq!(c[0], 800);
|
||||
assert!(c[1..].iter().all(|&v| v == 0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
//! Start codes and headers (ISO/IEC 14496-2 §6.2): visual object sequence, visual object, video
|
||||
//! object, video object layer and video object plane.
|
||||
|
||||
use craft_media::bitstream::BitWriter;
|
||||
|
||||
use super::ColorConfig;
|
||||
|
||||
const VISUAL_OBJECT_SEQUENCE: u32 = 0x1B0;
|
||||
const VISUAL_OBJECT: u32 = 0x1B5;
|
||||
const VIDEO_OBJECT: u32 = 0x100;
|
||||
const VIDEO_OBJECT_LAYER: u32 = 0x120;
|
||||
const VOP: u32 = 0x1B6;
|
||||
|
||||
/// `visual_object_type`: video.
|
||||
const VIDEO_ID: u32 = 1;
|
||||
/// `video_object_type_indication`: Simple Object Type.
|
||||
const SIMPLE_OBJECT: u32 = 1;
|
||||
|
||||
fn start_code(w: &mut BitWriter, code: u32) {
|
||||
w.write_bits(code, 32);
|
||||
}
|
||||
|
||||
/// `next_start_code()`: a zero bit, then ones to the byte boundary.
|
||||
pub(super) fn stuffing(w: &mut BitWriter) {
|
||||
w.write_bit(false);
|
||||
while !w.is_byte_aligned() {
|
||||
w.write_bit(true);
|
||||
}
|
||||
}
|
||||
|
||||
fn marker(w: &mut BitWriter) {
|
||||
w.write_bit(true);
|
||||
}
|
||||
|
||||
/// The stream's time base.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(super) struct Timing {
|
||||
/// `vop_time_increment_resolution`: ticks per second.
|
||||
pub resolution: u32,
|
||||
/// Ticks per frame.
|
||||
pub increment: u32,
|
||||
}
|
||||
|
||||
impl Timing {
|
||||
/// Bits of `vop_time_increment` (enough for `resolution - 1`, at least 1).
|
||||
pub fn bits(self) -> u32 {
|
||||
(32 - self.resolution.saturating_sub(1).leading_zeros()).max(1)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct Sequence {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub timing: Timing,
|
||||
pub profile_level: u8,
|
||||
pub sar: (u8, u8),
|
||||
pub color: Option<ColorConfig>,
|
||||
}
|
||||
|
||||
/// Table 6-12 `aspect_ratio_info` codes with a fixed pixel aspect ratio.
|
||||
const ASPECT: [(u8, (u8, u8)); 5] = [(1, (1, 1)), (2, (12, 11)), (3, (10, 11)), (4, (16, 11)), (5, (40, 33))];
|
||||
|
||||
/// `VisualObjectSequence()` through `VideoObjectLayer()`: what a decoder needs before the first
|
||||
/// VOP, and the `DecoderSpecificInfo` of an MP4 `esds`.
|
||||
pub(super) fn sequence_headers(s: &Sequence) -> Vec<u8> {
|
||||
let mut w = BitWriter::new();
|
||||
start_code(&mut w, VISUAL_OBJECT_SEQUENCE);
|
||||
w.write_bits(u32::from(s.profile_level), 8);
|
||||
|
||||
start_code(&mut w, VISUAL_OBJECT);
|
||||
w.write_bit(false); // is_visual_object_identifier
|
||||
w.write_bits(VIDEO_ID, 4);
|
||||
w.write_bit(s.color.is_some()); // video_signal_type
|
||||
if let Some(c) = s.color {
|
||||
w.write_bits(5, 3); // video_format: unspecified
|
||||
w.write_bit(c.full_range);
|
||||
w.write_bit(true); // colour_description
|
||||
w.write_bits(u32::from(c.primaries), 8);
|
||||
w.write_bits(u32::from(c.transfer), 8);
|
||||
w.write_bits(u32::from(c.matrix), 8);
|
||||
}
|
||||
stuffing(&mut w);
|
||||
|
||||
start_code(&mut w, VIDEO_OBJECT);
|
||||
|
||||
start_code(&mut w, VIDEO_OBJECT_LAYER);
|
||||
w.write_bit(false); // random_accessible_vol
|
||||
w.write_bits(SIMPLE_OBJECT, 8);
|
||||
w.write_bit(false); // is_object_layer_identifier
|
||||
match ASPECT.iter().find(|(_, r)| *r == s.sar) {
|
||||
Some(&(code, _)) => w.write_bits(u32::from(code), 4),
|
||||
None => {
|
||||
w.write_bits(0xF, 4); // extended PAR
|
||||
w.write_bits(u32::from(s.sar.0), 8);
|
||||
w.write_bits(u32::from(s.sar.1), 8);
|
||||
}
|
||||
}
|
||||
w.write_bit(true); // vol_control_parameters
|
||||
w.write_bits(1, 2); // chroma_format 4:2:0
|
||||
w.write_bit(true); // low_delay: no B-VOPs
|
||||
w.write_bit(false); // vbv_parameters
|
||||
w.write_bits(0, 2); // video_object_layer_shape: rectangular
|
||||
marker(&mut w);
|
||||
w.write_bits(s.timing.resolution, 16);
|
||||
marker(&mut w);
|
||||
let fixed = s.timing.increment < s.timing.resolution;
|
||||
w.write_bit(fixed); // fixed_vop_rate
|
||||
if fixed {
|
||||
w.write_bits(s.timing.increment, s.timing.bits());
|
||||
}
|
||||
marker(&mut w);
|
||||
w.write_bits(s.width, 13);
|
||||
marker(&mut w);
|
||||
w.write_bits(s.height, 13);
|
||||
marker(&mut w);
|
||||
w.write_bit(false); // interlaced
|
||||
w.write_bit(true); // obmc_disable
|
||||
w.write_bit(false); // sprite_enable
|
||||
w.write_bit(false); // not_8_bit
|
||||
w.write_bit(false); // quant_type: H.263 quantisation
|
||||
w.write_bit(true); // complexity_estimation_disable
|
||||
w.write_bit(true); // resync_marker_disable
|
||||
w.write_bit(false); // data_partitioned
|
||||
w.write_bit(false); // scalability
|
||||
stuffing(&mut w);
|
||||
w.finish()
|
||||
}
|
||||
|
||||
/// `VideoObjectPlane()` up to the first macroblock.
|
||||
pub(super) struct VopHeader {
|
||||
pub intra: bool,
|
||||
/// Whole seconds since the previous VOP (`modulo_time_base`).
|
||||
pub seconds: u32,
|
||||
pub time_increment: u32,
|
||||
pub rounding: bool,
|
||||
pub quant: u8,
|
||||
pub fcode: u8,
|
||||
}
|
||||
|
||||
pub(super) fn vop_header(w: &mut BitWriter, h: &VopHeader, timing: Timing) {
|
||||
start_code(w, VOP);
|
||||
w.write_bits(if h.intra { 0 } else { 1 }, 2); // vop_coding_type
|
||||
for _ in 0..h.seconds {
|
||||
w.write_bit(true);
|
||||
}
|
||||
w.write_bit(false);
|
||||
marker(w);
|
||||
w.write_bits(h.time_increment, timing.bits());
|
||||
marker(w);
|
||||
w.write_bit(true); // vop_coded
|
||||
if !h.intra {
|
||||
w.write_bit(h.rounding);
|
||||
}
|
||||
w.write_bits(0, 3); // intra_dc_vlc_thr: DC always with its own VLC
|
||||
w.write_bits(u32::from(h.quant), 5);
|
||||
if !h.intra {
|
||||
w.write_bits(u32::from(h.fcode), 3);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn time_increment_bits() {
|
||||
let t = |resolution| Timing { resolution, increment: 1 }.bits();
|
||||
assert_eq!(t(1), 1);
|
||||
assert_eq!(t(2), 1);
|
||||
assert_eq!(t(25), 5);
|
||||
assert_eq!(t(30000), 15);
|
||||
assert_eq!(t(65535), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn headers_are_byte_aligned_start_codes() {
|
||||
let s = Sequence { width: 352, height: 288, timing: Timing { resolution: 25, increment: 1 }, profile_level: 3, sar: (1, 1), color: None };
|
||||
let h = sequence_headers(&s);
|
||||
assert_eq!(&h[..5], &[0, 0, 1, 0xB0, 3]);
|
||||
assert_eq!(&h[5..9], &[0, 0, 1, 0xB5]);
|
||||
assert!(h.windows(4).any(|w| w == [0, 0, 1, 0x20]));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,372 @@
|
||||
//! Clean-room MPEG-4 Part 2 (ISO/IEC 14496-2, MPEG-4 Visual) encoder, written from the
|
||||
//! standard. It writes the Simple profile: the kind of stream Xvid and DivX produce and that
|
||||
//! MP4 (`mp4v`), Matroska (`V_MPEG4/ISO/SP`) and AVI (`XVID`, `DIVX`, `FMP4`) carry.
|
||||
//!
|
||||
//! - I-VOPs and P-VOPs, 4:2:0 8-bit, rectangular, progressive, H.263 quantisation.
|
||||
//! - One motion vector a macroblock at half-sample precision (`fcode` 1–2), found with a
|
||||
//! predictor-seeded diamond search; skipped macroblocks; intra macroblocks in P-VOPs when they
|
||||
//! predict worse than they code on their own, and every 128 inter codings (§7.4.5 asks for one
|
||||
//! within 132).
|
||||
//! - Intra DC prediction, no AC prediction; all three coefficient escape modes.
|
||||
//! - Rate control: a constant quantiser, or a 1-pass average bitrate.
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use craft_video::mpeg4::{Encoder, EncoderConfig, RateControl, YuvFrame};
|
||||
//! let mut cfg = EncoderConfig::new(640, 360, 25, 1);
|
||||
//! cfg.rate = RateControl::Quant(5);
|
||||
//! let mut enc = Encoder::new(cfg).unwrap();
|
||||
//! let (y, u, v) = (vec![128u8; 640 * 360], vec![128u8; 320 * 180], vec![128u8; 320 * 180]);
|
||||
//! let frame = YuvFrame { y: &y, u: &u, v: &v, y_stride: 640, uv_stride: 320 };
|
||||
//! let packet = enc.encode(&frame).unwrap();
|
||||
//! ```
|
||||
|
||||
#![allow(clippy::needless_range_loop)]
|
||||
|
||||
mod dct;
|
||||
mod headers;
|
||||
mod picture;
|
||||
mod tables;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use craft_media::bitstream::BitWriter;
|
||||
use craft_media::{CodecId, CodecParams};
|
||||
|
||||
use headers::{Sequence, Timing, VopHeader};
|
||||
use picture::{Frame, PictureCoder, Plane};
|
||||
|
||||
/// Largest width or height (the VOL stores 13 bits; sizes are even).
|
||||
pub const MAX_SIDE: u32 = 8190;
|
||||
|
||||
/// Encoder errors.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("invalid dimensions {0}x{1} (must be even, non-zero and at most {MAX_SIDE})")]
|
||||
InvalidDimensions(u32, u32),
|
||||
#[error("invalid frame rate {0}/{1} (the reduced numerator must fit in 16 bits)")]
|
||||
InvalidFrameRate(u32, u32),
|
||||
#[error("invalid configuration: {0}")]
|
||||
InvalidConfig(String),
|
||||
#[error("input frame planes too small for the configured size")]
|
||||
BadFrame,
|
||||
}
|
||||
|
||||
/// An 8-bit 4:2:0 input picture: chroma planes are half the width and height.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct YuvFrame<'a> {
|
||||
pub y: &'a [u8],
|
||||
pub u: &'a [u8],
|
||||
pub v: &'a [u8],
|
||||
pub y_stride: usize,
|
||||
pub uv_stride: usize,
|
||||
}
|
||||
|
||||
/// Colour description written to the visual object header (ITU-T H.273 code points).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct ColorConfig {
|
||||
pub primaries: u8,
|
||||
pub transfer: u8,
|
||||
pub matrix: u8,
|
||||
pub full_range: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum RateControl {
|
||||
/// Every VOP at this quantiser (1–31, lower is better).
|
||||
Quant(u8),
|
||||
/// An average of `kbps` kilobits a second, the quantiser chosen a frame at a time (2–31).
|
||||
Bitrate { kbps: u32 },
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct EncoderConfig {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub fps_num: u32,
|
||||
pub fps_den: u32,
|
||||
pub rate: RateControl,
|
||||
/// Largest distance between I-VOPs (1 = all intra).
|
||||
pub keyint: u32,
|
||||
/// Pixel aspect ratio (each 1–255).
|
||||
pub sar: (u8, u8),
|
||||
/// Colour description; `None` writes no `video_signal_type`.
|
||||
pub color: Option<ColorConfig>,
|
||||
/// Repeat the configuration headers ([`Encoder::extradata`]) before every I-VOP, as a raw
|
||||
/// `.m4v` elementary stream or an AVI file has them. Off for MP4 and Matroska, which carry
|
||||
/// them in the track header.
|
||||
pub headers_in_band: bool,
|
||||
}
|
||||
|
||||
impl EncoderConfig {
|
||||
/// Quantiser 4, an I-VOP at least every 250 frames, square pixels, headers in band.
|
||||
pub fn new(width: u32, height: u32, fps_num: u32, fps_den: u32) -> Self {
|
||||
EncoderConfig { width, height, fps_num, fps_den, rate: RateControl::Quant(4), keyint: 250, sar: (1, 1), color: None, headers_in_band: true }
|
||||
}
|
||||
}
|
||||
|
||||
/// One coded VOP.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Packet {
|
||||
pub data: Vec<u8>,
|
||||
/// An I-VOP (with the configuration headers in front when they are in band).
|
||||
pub keyframe: bool,
|
||||
/// The frame number, in units of `fps_den / fps_num` seconds.
|
||||
pub pts: i64,
|
||||
/// The quantiser the VOP was coded with.
|
||||
pub quant: u8,
|
||||
}
|
||||
|
||||
/// The picture a decoder reconstructs from the last packet (to within the IDCT accuracy a
|
||||
/// conforming decoder is held to).
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Reconstruction {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub y: Vec<u8>,
|
||||
pub u: Vec<u8>,
|
||||
pub v: Vec<u8>,
|
||||
}
|
||||
|
||||
/// `profile_and_level_indication` (Simple profile) for a picture of `mbs` macroblocks: the
|
||||
/// smallest level whose picture size holds it. Bitrate and buffer limits are not checked, and
|
||||
/// pictures over 3600 macroblocks are larger than any Simple profile level allows.
|
||||
fn simple_level(mbs: usize) -> u8 {
|
||||
match mbs {
|
||||
0..=396 => 0x03,
|
||||
397..=1200 => 0x04,
|
||||
1201..=1620 => 0x05,
|
||||
_ => 0x06,
|
||||
}
|
||||
}
|
||||
|
||||
fn gcd(a: u32, b: u32) -> u32 {
|
||||
if b == 0 { a } else { gcd(b, a % b) }
|
||||
}
|
||||
|
||||
struct RateState {
|
||||
/// Bits a frame at the target rate.
|
||||
per_frame: f64,
|
||||
/// Bits sent minus bits budgeted so far.
|
||||
fullness: f64,
|
||||
/// Bits × quantiser of the last I and P VOP (the model: bits fall as 1/quantiser).
|
||||
complexity: [Option<f64>; 2],
|
||||
last_qp: [u8; 2],
|
||||
}
|
||||
|
||||
impl RateState {
|
||||
fn qp(&self, intra: bool) -> u8 {
|
||||
let k = usize::from(!intra);
|
||||
let share = if intra { 4.0 } else { 1.0 };
|
||||
let target = (share * self.per_frame - self.fullness / 25.0).max(0.25 * share * self.per_frame);
|
||||
let Some(c) = self.complexity[k].or_else(|| self.complexity[0].map(|c| c / 4.0)) else {
|
||||
// No history: about 0.15 bits a sample at quantiser 8 for detailed content.
|
||||
return 8;
|
||||
};
|
||||
let q = (c / target).round().clamp(2.0, 31.0) as u8;
|
||||
let last = self.last_qp[k];
|
||||
if self.complexity[k].is_some() { q.clamp(last.saturating_sub(4).max(2), (last + 4).min(31)) } else { q }
|
||||
}
|
||||
|
||||
fn update(&mut self, intra: bool, bits: usize, qp: u8) {
|
||||
let k = usize::from(!intra);
|
||||
let c = bits as f64 * f64::from(qp);
|
||||
self.complexity[k] = Some(self.complexity[k].map_or(c, |old| 0.5 * old + 0.5 * c));
|
||||
self.last_qp[k] = qp;
|
||||
self.fullness += bits as f64 - self.per_frame;
|
||||
}
|
||||
}
|
||||
|
||||
/// The encoder: one picture in, one VOP out (no B-VOPs, so no reordering or delay).
|
||||
pub struct Encoder {
|
||||
cfg: EncoderConfig,
|
||||
timing: Timing,
|
||||
mb_w: usize,
|
||||
mb_h: usize,
|
||||
headers: Vec<u8>,
|
||||
reference: Option<Frame>,
|
||||
frame: u64,
|
||||
since_key: u32,
|
||||
force_key: bool,
|
||||
last_seconds: u64,
|
||||
rounding: bool,
|
||||
refresh: Vec<u8>,
|
||||
rate: Option<RateState>,
|
||||
}
|
||||
|
||||
impl Encoder {
|
||||
pub fn new(cfg: EncoderConfig) -> Result<Encoder, Error> {
|
||||
let (w, h) = (cfg.width, cfg.height);
|
||||
if w == 0 || h == 0 || w % 2 != 0 || h % 2 != 0 || w > MAX_SIDE || h > MAX_SIDE {
|
||||
return Err(Error::InvalidDimensions(w, h));
|
||||
}
|
||||
if cfg.fps_num == 0 || cfg.fps_den == 0 {
|
||||
return Err(Error::InvalidFrameRate(cfg.fps_num, cfg.fps_den));
|
||||
}
|
||||
let g = gcd(cfg.fps_num, cfg.fps_den);
|
||||
let timing = Timing { resolution: cfg.fps_num / g, increment: cfg.fps_den / g };
|
||||
if timing.resolution > 0xFFFF {
|
||||
return Err(Error::InvalidFrameRate(cfg.fps_num, cfg.fps_den));
|
||||
}
|
||||
if cfg.keyint == 0 {
|
||||
return Err(Error::InvalidConfig("keyint must be at least 1".into()));
|
||||
}
|
||||
if cfg.sar.0 == 0 || cfg.sar.1 == 0 {
|
||||
return Err(Error::InvalidConfig("sample aspect ratio terms must be non-zero".into()));
|
||||
}
|
||||
let rate = match cfg.rate {
|
||||
RateControl::Quant(q) if (1..=31).contains(&q) => None,
|
||||
RateControl::Quant(q) => return Err(Error::InvalidConfig(format!("quantiser {q} is outside 1–31"))),
|
||||
RateControl::Bitrate { kbps: 0 } => return Err(Error::InvalidConfig("bitrate must be non-zero".into())),
|
||||
RateControl::Bitrate { kbps } => Some(RateState {
|
||||
per_frame: f64::from(kbps) * 1000.0 * f64::from(timing.increment) / f64::from(timing.resolution),
|
||||
fullness: 0.0,
|
||||
complexity: [None; 2],
|
||||
last_qp: [8; 2],
|
||||
}),
|
||||
};
|
||||
let (mb_w, mb_h) = (w.div_ceil(16) as usize, h.div_ceil(16) as usize);
|
||||
let headers = headers::sequence_headers(&Sequence {
|
||||
width: w,
|
||||
height: h,
|
||||
timing,
|
||||
profile_level: simple_level(mb_w * mb_h),
|
||||
sar: cfg.sar,
|
||||
color: cfg.color,
|
||||
});
|
||||
Ok(Encoder {
|
||||
cfg,
|
||||
timing,
|
||||
mb_w,
|
||||
mb_h,
|
||||
headers,
|
||||
reference: None,
|
||||
frame: 0,
|
||||
since_key: 0,
|
||||
force_key: false,
|
||||
last_seconds: 0,
|
||||
rounding: false,
|
||||
refresh: vec![0; mb_w * mb_h],
|
||||
rate,
|
||||
})
|
||||
}
|
||||
|
||||
/// The configuration headers (`VisualObjectSequence` to `VideoObjectLayer`): the MP4 `esds`
|
||||
/// `DecoderSpecificInfo` and the Matroska `CodecPrivate`.
|
||||
pub fn extradata(&self) -> &[u8] {
|
||||
&self.headers
|
||||
}
|
||||
|
||||
/// Track description for muxers (`CodecId::Mpeg4Video`, `mp4v` tag, picture size, extradata).
|
||||
pub fn codec_params(&self) -> CodecParams {
|
||||
CodecParams::new(CodecId::Mpeg4Video).with_tag(*b"mp4v").with_size(self.cfg.width, self.cfg.height).with_extradata(self.headers.clone())
|
||||
}
|
||||
|
||||
/// Code the next picture as an I-VOP.
|
||||
pub fn request_keyframe(&mut self) {
|
||||
self.force_key = true;
|
||||
}
|
||||
|
||||
/// The decoder's picture for the last packet, cropped to the configured size.
|
||||
pub fn reconstruction(&self) -> Option<Reconstruction> {
|
||||
let r = self.reference.as_ref()?;
|
||||
let (w, h) = (self.cfg.width as usize, self.cfg.height as usize);
|
||||
let crop = |p: &Plane, w: usize, h: usize| (0..h).flat_map(|y| p.data[y * p.w..][..w].iter().copied()).collect();
|
||||
Some(Reconstruction {
|
||||
width: self.cfg.width,
|
||||
height: self.cfg.height,
|
||||
y: crop(&r.y, w, h),
|
||||
u: crop(&r.u, w / 2, h / 2),
|
||||
v: crop(&r.v, w / 2, h / 2),
|
||||
})
|
||||
}
|
||||
|
||||
/// Copy the input into macroblock-aligned planes, repeating the last column and row.
|
||||
fn load(&self, f: &YuvFrame) -> Result<Frame, Error> {
|
||||
let (w, h) = (self.cfg.width as usize, self.cfg.height as usize);
|
||||
let fits = |p: &[u8], stride: usize, w: usize, h: usize| {
|
||||
stride >= w && (h - 1).checked_mul(stride).and_then(|n| n.checked_add(w)).is_some_and(|n| n <= p.len())
|
||||
};
|
||||
if !fits(f.y, f.y_stride, w, h) || !fits(f.u, f.uv_stride, w / 2, h / 2) || !fits(f.v, f.uv_stride, w / 2, h / 2) {
|
||||
return Err(Error::BadFrame);
|
||||
}
|
||||
let mut out = Frame::new(self.mb_w, self.mb_h);
|
||||
let fill = |dst: &mut Plane, src: &[u8], stride: usize, w: usize, h: usize| {
|
||||
for y in 0..dst.h {
|
||||
let row = &src[y.min(h - 1) * stride..][..w];
|
||||
let d = &mut dst.data[y * dst.w..][..dst.w];
|
||||
d[..w].copy_from_slice(row);
|
||||
let edge = row[w - 1];
|
||||
d[w..].fill(edge);
|
||||
}
|
||||
};
|
||||
fill(&mut out.y, f.y, f.y_stride, w, h);
|
||||
fill(&mut out.u, f.u, f.uv_stride, w / 2, h / 2);
|
||||
fill(&mut out.v, f.v, f.uv_stride, w / 2, h / 2);
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Encode one picture.
|
||||
pub fn encode(&mut self, frame: &YuvFrame) -> Result<Packet, Error> {
|
||||
let src = self.load(frame)?;
|
||||
let intra = self.reference.is_none() || self.force_key || self.since_key >= self.cfg.keyint;
|
||||
let qp = match (&self.rate, self.cfg.rate) {
|
||||
(Some(r), _) => r.qp(intra),
|
||||
(None, RateControl::Quant(q)) => q,
|
||||
(None, RateControl::Bitrate { .. }) => 8,
|
||||
};
|
||||
if !intra {
|
||||
self.rounding = !self.rounding;
|
||||
}
|
||||
let mut refresh = std::mem::take(&mut self.refresh);
|
||||
if intra {
|
||||
refresh.fill(0);
|
||||
}
|
||||
let coded = PictureCoder {
|
||||
src: &src,
|
||||
reference: if intra { None } else { self.reference.as_ref() },
|
||||
width: self.cfg.width as usize,
|
||||
height: self.cfg.height as usize,
|
||||
mb_w: self.mb_w,
|
||||
mb_h: self.mb_h,
|
||||
qp: i32::from(qp),
|
||||
rounding: self.rounding,
|
||||
refresh: &mut refresh,
|
||||
}
|
||||
.code();
|
||||
self.refresh = refresh;
|
||||
|
||||
let ticks = self.frame * u64::from(self.timing.increment);
|
||||
let res = u64::from(self.timing.resolution);
|
||||
let seconds = ticks / res;
|
||||
let mut w = BitWriter::new();
|
||||
if intra && self.cfg.headers_in_band {
|
||||
w.write_bytes(&self.headers);
|
||||
}
|
||||
let header = VopHeader {
|
||||
intra,
|
||||
seconds: (seconds - self.last_seconds).min(u64::from(u32::MAX)) as u32,
|
||||
time_increment: (ticks % res) as u32,
|
||||
rounding: self.rounding,
|
||||
quant: qp,
|
||||
fcode: coded.fcode,
|
||||
};
|
||||
headers::vop_header(&mut w, &header, self.timing);
|
||||
for mb in &coded.mbs {
|
||||
picture::write_mb(&mut w, mb, intra, coded.fcode, false);
|
||||
}
|
||||
headers::stuffing(&mut w);
|
||||
let data = w.finish();
|
||||
|
||||
if let Some(r) = &mut self.rate {
|
||||
r.update(intra, data.len() * 8, qp);
|
||||
}
|
||||
self.reference = Some(coded.recon);
|
||||
self.last_seconds = seconds;
|
||||
let pts = self.frame as i64;
|
||||
self.frame += 1;
|
||||
self.since_key = if intra { 1 } else { self.since_key + 1 };
|
||||
self.force_key = false;
|
||||
Ok(Packet { data, keyframe: intra, pts, quant: qp })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,677 @@
|
||||
//! One video object plane: mode decision, motion estimation, transform and quantisation, the
|
||||
//! reconstruction a decoder makes, and the macroblock syntax (ISO/IEC 14496-2 §6.2.6–6.2.8,
|
||||
//! §7.4–7.6).
|
||||
|
||||
use craft_media::bitstream::BitWriter;
|
||||
|
||||
use super::dct::{fdct, idct};
|
||||
use super::tables::{self, CBPY, DC_SIZE_CHROMA, DC_SIZE_LUMA, ESCAPE, MCBPC_INTER, MCBPC_INTRA, MVD, Tcoef, ZIGZAG};
|
||||
|
||||
/// An 8-bit sample plane, `w` samples a row.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) struct Plane {
|
||||
pub w: usize,
|
||||
pub h: usize,
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Plane {
|
||||
pub fn new(w: usize, h: usize) -> Plane {
|
||||
Plane { w, h, data: vec![0; w * h] }
|
||||
}
|
||||
|
||||
/// The sample at `(x, y)`, coordinates clamped to the plane.
|
||||
#[inline]
|
||||
fn at(&self, x: i32, y: i32) -> i32 {
|
||||
let x = x.clamp(0, self.w as i32 - 1) as usize;
|
||||
let y = y.clamp(0, self.h as i32 - 1) as usize;
|
||||
i32::from(self.data[y * self.w + x])
|
||||
}
|
||||
}
|
||||
|
||||
/// A picture at macroblock-aligned size.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) struct Frame {
|
||||
pub y: Plane,
|
||||
pub u: Plane,
|
||||
pub v: Plane,
|
||||
}
|
||||
|
||||
impl Frame {
|
||||
pub fn new(mb_w: usize, mb_h: usize) -> Frame {
|
||||
Frame { y: Plane::new(mb_w * 16, mb_h * 16), u: Plane::new(mb_w * 8, mb_h * 8), v: Plane::new(mb_w * 8, mb_h * 8) }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(super) enum Mode {
|
||||
Intra,
|
||||
Inter,
|
||||
/// `not_coded`: the co-located macroblock of the reference, no residual.
|
||||
Skip,
|
||||
}
|
||||
|
||||
/// A coded macroblock.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) struct Mb {
|
||||
pub mode: Mode,
|
||||
/// Motion vector in half samples (zero unless inter).
|
||||
pub mv: (i32, i32),
|
||||
/// Its predictor (§7.6.5).
|
||||
pub pred: (i32, i32),
|
||||
/// Coded block pattern, block `i` in bit `5 - i` (Y0–Y3, Cb, Cr). Intra: the AC coefficients.
|
||||
pub cbp: u8,
|
||||
/// Intra: `dct_dc_differential` of each block.
|
||||
pub dc_diff: [i32; 6],
|
||||
/// Quantised levels in raster order (intra: index 0 is unused).
|
||||
pub levels: [[i16; 64]; 6],
|
||||
}
|
||||
|
||||
impl Mb {
|
||||
pub fn skip() -> Mb {
|
||||
Mb { mode: Mode::Skip, mv: (0, 0), pred: (0, 0), cbp: 0, dc_diff: [0; 6], levels: [[0; 64]; 6] }
|
||||
}
|
||||
}
|
||||
|
||||
/// Table 7-1: `dc_scaler` of luminance and chrominance blocks.
|
||||
pub(super) fn dc_scaler(qp: i32, luma: bool) -> i32 {
|
||||
match (luma, qp) {
|
||||
(_, ..=4) => 8,
|
||||
(true, 5..=8) => 2 * qp,
|
||||
(true, 9..=24) => qp + 8,
|
||||
(true, _) => 2 * qp - 16,
|
||||
(false, 5..=24) => (qp + 13) / 2,
|
||||
(false, _) => qp - 6,
|
||||
}
|
||||
}
|
||||
|
||||
/// The second inverse quantisation method (H.263 style, §7.4.2.2), saturated to [-2048, 2047].
|
||||
#[inline]
|
||||
fn dequant(level: i32, qp: i32) -> i32 {
|
||||
if level == 0 {
|
||||
return 0;
|
||||
}
|
||||
let mag = qp * (2 * level.abs() + 1) - if qp & 1 == 0 { 1 } else { 0 };
|
||||
(if level < 0 { -mag } else { mag }).clamp(-2048, 2047)
|
||||
}
|
||||
|
||||
/// The largest level the fixed-length escape carries.
|
||||
const MAX_LEVEL: i32 = 2047;
|
||||
|
||||
/// The value of an intra DC prediction neighbour outside the VOP or not intra (`2^(8+2)`).
|
||||
const DC_DEFAULT: i32 = 1024;
|
||||
|
||||
/// Reconstructed intra DC values (`F[0][0]`) of the blocks coded so far, for DC prediction.
|
||||
struct DcStore {
|
||||
/// Luminance, one per 8×8 block.
|
||||
y: Vec<i32>,
|
||||
yw: usize,
|
||||
/// Chrominance, one per macroblock.
|
||||
c: [Vec<i32>; 2],
|
||||
cw: usize,
|
||||
}
|
||||
|
||||
impl DcStore {
|
||||
fn new(mb_w: usize, mb_h: usize) -> DcStore {
|
||||
let n = mb_w * mb_h;
|
||||
DcStore { y: vec![DC_DEFAULT; 4 * n], yw: 2 * mb_w, c: [vec![DC_DEFAULT; n], vec![DC_DEFAULT; n]], cw: mb_w }
|
||||
}
|
||||
|
||||
/// The grid, its width and the block's position for block `b` of macroblock `(mx, my)`.
|
||||
fn locate(&mut self, b: usize, mx: usize, my: usize) -> (&mut Vec<i32>, usize, usize, usize) {
|
||||
match b {
|
||||
0..=3 => (&mut self.y, self.yw, 2 * mx + (b & 1), 2 * my + (b >> 1)),
|
||||
_ => (&mut self.c[b - 4], self.cw, mx, my),
|
||||
}
|
||||
}
|
||||
|
||||
/// The prediction `F_P` of block `b` (§7.4.3.1): from the block above when the horizontal
|
||||
/// gradient is the smaller, else from the block to the left.
|
||||
fn predict(&mut self, b: usize, mx: usize, my: usize) -> i32 {
|
||||
let (g, w, bx, by) = self.locate(b, mx, my);
|
||||
let get = |x: usize, y: usize, dx: bool, dy: bool| -> i32 {
|
||||
if (dx && x == 0) || (dy && y == 0) {
|
||||
return DC_DEFAULT;
|
||||
}
|
||||
g[(y - usize::from(dy)) * w + x - usize::from(dx)]
|
||||
};
|
||||
let a = get(bx, by, true, false);
|
||||
let bb = get(bx, by, true, true);
|
||||
let c = get(bx, by, false, true);
|
||||
if (a - bb).abs() < (bb - c).abs() { c } else { a }
|
||||
}
|
||||
|
||||
fn set(&mut self, b: usize, mx: usize, my: usize, v: i32) {
|
||||
let (g, w, bx, by) = self.locate(b, mx, my);
|
||||
g[by * w + bx] = v;
|
||||
}
|
||||
}
|
||||
|
||||
/// Motion-compensated prediction of a `size`×`size` block at `(x, y)` with a half-sample vector
|
||||
/// (§7.6.2), samples outside the plane taken from its edge.
|
||||
fn predict(plane: &Plane, x: i32, y: i32, mv: (i32, i32), size: usize, rounding: bool, out: &mut [u8]) {
|
||||
let (ix, iy) = (x + (mv.0 >> 1), y + (mv.1 >> 1));
|
||||
let (hx, hy) = (mv.0 & 1, mv.1 & 1);
|
||||
let r = i32::from(rounding);
|
||||
for j in 0..size {
|
||||
for i in 0..size {
|
||||
let (sx, sy) = (ix + i as i32, iy + j as i32);
|
||||
let a = plane.at(sx, sy);
|
||||
let v = match (hx, hy) {
|
||||
(0, 0) => a,
|
||||
(1, 0) => (a + plane.at(sx + 1, sy) + 1 - r) >> 1,
|
||||
(0, _) => (a + plane.at(sx, sy + 1) + 1 - r) >> 1,
|
||||
_ => (a + plane.at(sx + 1, sy) + plane.at(sx, sy + 1) + plane.at(sx + 1, sy + 1) + 2 - r) >> 2,
|
||||
};
|
||||
out[j * size + i] = v as u8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The chrominance vector of a 16×16 luminance vector: halved, with every fraction to a half
|
||||
/// sample (§7.6.2.2).
|
||||
#[inline]
|
||||
fn chroma_mv(mv: (i32, i32)) -> (i32, i32) {
|
||||
((mv.0 >> 1) | (mv.0 & 1), (mv.1 >> 1) | (mv.1 & 1))
|
||||
}
|
||||
|
||||
/// The median of three.
|
||||
#[inline]
|
||||
fn median(a: i32, b: i32, c: i32) -> i32 {
|
||||
a.max(b).min(a.min(b).max(c))
|
||||
}
|
||||
|
||||
/// Bits of a motion vector difference at `fcode` 1 (an estimate for the motion search).
|
||||
fn mvd_bits(d: i32) -> u32 {
|
||||
let a = d.unsigned_abs().min(32) as usize;
|
||||
u32::from(MVD[a].1) + u32::from(a != 0)
|
||||
}
|
||||
|
||||
/// The largest vector component the search tries, in half samples (`fcode` 2 covers it).
|
||||
const SEARCH_LIMIT: i32 = 63;
|
||||
|
||||
/// Encoder-side state for one VOP.
|
||||
pub(super) struct PictureCoder<'a> {
|
||||
pub src: &'a Frame,
|
||||
pub reference: Option<&'a Frame>,
|
||||
/// Picture size in samples (even).
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
pub mb_w: usize,
|
||||
pub mb_h: usize,
|
||||
pub qp: i32,
|
||||
pub rounding: bool,
|
||||
/// Per macroblock: inter codings since its last intra coding (refresh before 132, §7.4.5).
|
||||
pub refresh: &'a mut [u8],
|
||||
}
|
||||
|
||||
/// Inter codings after which a macroblock is coded intra again: IDCT mismatch between encoder and
|
||||
/// decoder must not build up over more than 132 (§7.4.5).
|
||||
const REFRESH: u8 = 128;
|
||||
|
||||
/// What [`PictureCoder::code`] made.
|
||||
pub(super) struct Coded {
|
||||
pub mbs: Vec<Mb>,
|
||||
pub recon: Frame,
|
||||
pub fcode: u8,
|
||||
}
|
||||
|
||||
impl PictureCoder<'_> {
|
||||
/// Decide, transform and reconstruct every macroblock.
|
||||
pub fn code(&mut self) -> Coded {
|
||||
let mut recon = Frame::new(self.mb_w, self.mb_h);
|
||||
let mut dc = DcStore::new(self.mb_w, self.mb_h);
|
||||
let mut mbs: Vec<Mb> = Vec::with_capacity(self.mb_w * self.mb_h);
|
||||
for my in 0..self.mb_h {
|
||||
for mx in 0..self.mb_w {
|
||||
let i = my * self.mb_w + mx;
|
||||
let mb = match self.reference {
|
||||
None => self.intra(mx, my, &mut dc, &mut recon),
|
||||
Some(r) => {
|
||||
let pred = mv_pred(&mbs, self.mb_w, mx, my);
|
||||
self.inter_or_intra(r, mx, my, pred, &mbs, &mut dc, &mut recon)
|
||||
}
|
||||
};
|
||||
match mb.mode {
|
||||
Mode::Intra => self.refresh[i] = 0,
|
||||
Mode::Inter => self.refresh[i] = self.refresh[i].saturating_add(1),
|
||||
Mode::Skip => {}
|
||||
}
|
||||
mbs.push(mb);
|
||||
}
|
||||
}
|
||||
let reach = mbs.iter().map(|m| m.mv.0.max(-m.mv.0 - 1).max(m.mv.1).max(-m.mv.1 - 1)).max().unwrap_or(0);
|
||||
let fcode = if reach < 32 { 1 } else { 2 };
|
||||
Coded { mbs, recon, fcode }
|
||||
}
|
||||
|
||||
/// 8×8 source block `b` of macroblock `(mx, my)`.
|
||||
fn source_block(&self, b: usize, mx: usize, my: usize) -> [i32; 64] {
|
||||
let (plane, x, y) = block_origin(self.src, b, mx, my);
|
||||
std::array::from_fn(|k| i32::from(plane.data[(y + k / 8) * plane.w + x + k % 8]))
|
||||
}
|
||||
|
||||
fn intra(&self, mx: usize, my: usize, dc: &mut DcStore, recon: &mut Frame) -> Mb {
|
||||
let qp = self.qp;
|
||||
let mut mb = Mb { mode: Mode::Intra, ..Mb::skip() };
|
||||
for b in 0..6 {
|
||||
let px = self.source_block(b, mx, my);
|
||||
let mut coef = [0; 64];
|
||||
fdct(&px, &mut coef);
|
||||
let scaler = dc_scaler(qp, b < 4);
|
||||
let qf = ((coef[0].max(0) + scaler / 2) / scaler).min(2047 / scaler);
|
||||
let fp = dc.predict(b, mx, my);
|
||||
mb.dc_diff[b] = qf - (fp + scaler / 2) / scaler;
|
||||
dc.set(b, mx, my, qf * scaler);
|
||||
let mut rec = [0; 64];
|
||||
rec[0] = qf * scaler;
|
||||
for k in 1..64 {
|
||||
let c = coef[k];
|
||||
let l = (c.abs() / (2 * qp)).min(MAX_LEVEL);
|
||||
let l = if c < 0 { -l } else { l };
|
||||
mb.levels[b][k] = l as i16;
|
||||
rec[k] = dequant(l, qp);
|
||||
if l != 0 {
|
||||
mb.cbp |= 1 << (5 - b);
|
||||
}
|
||||
}
|
||||
idct(&mut rec);
|
||||
let (plane, x, y) = block_origin_mut(recon, b, mx, my);
|
||||
for k in 0..64 {
|
||||
plane.data[(y + k / 8) * plane.w + x + k % 8] = rec[k].clamp(0, 255) as u8;
|
||||
}
|
||||
}
|
||||
mb
|
||||
}
|
||||
|
||||
/// Whether a vector reads only samples inside the picture for the part of the macroblock that
|
||||
/// is inside it, so the prediction does not depend on how a decoder pads its reference.
|
||||
fn mv_ok(&self, mx: usize, my: usize, mv: (i32, i32)) -> bool {
|
||||
let range = -SEARCH_LIMIT - 1..=SEARCH_LIMIT;
|
||||
if !range.contains(&mv.0) || !range.contains(&mv.1) {
|
||||
return false;
|
||||
}
|
||||
let fits = |x0: usize, size: usize, extent: usize, v: i32| {
|
||||
let inside = size.min(extent - x0) as i32;
|
||||
let start = x0 as i32 + (v >> 1);
|
||||
start >= 0 && start + inside - 1 + (v & 1) < extent as i32
|
||||
};
|
||||
let c = chroma_mv(mv);
|
||||
fits(mx * 16, 16, self.width, mv.0)
|
||||
&& fits(my * 16, 16, self.height, mv.1)
|
||||
&& fits(mx * 8, 8, self.width / 2, c.0)
|
||||
&& fits(my * 8, 8, self.height / 2, c.1)
|
||||
}
|
||||
|
||||
/// Sum of absolute differences of the 16×16 luminance prediction with `mv`.
|
||||
fn sad(&self, r: &Frame, mx: usize, my: usize, mv: (i32, i32)) -> u32 {
|
||||
let mut p = [0u8; 256];
|
||||
predict(&r.y, (mx * 16) as i32, (my * 16) as i32, mv, 16, self.rounding, &mut p);
|
||||
let s = &self.src.y;
|
||||
let mut sad = 0u32;
|
||||
for j in 0..16 {
|
||||
let row = &s.data[(my * 16 + j) * s.w + mx * 16..][..16];
|
||||
for i in 0..16 {
|
||||
sad += u32::from(row[i].abs_diff(p[j * 16 + i]));
|
||||
}
|
||||
}
|
||||
sad
|
||||
}
|
||||
|
||||
/// Motion search: the best of the predictor, the neighbours' vectors and zero, a small
|
||||
/// diamond search in whole samples, then the eight half-sample positions around it.
|
||||
fn search(&self, r: &Frame, mx: usize, my: usize, pred: (i32, i32), cands: &[(i32, i32)]) -> ((i32, i32), u32) {
|
||||
let lambda = self.qp as u32;
|
||||
let cost = |mv: (i32, i32)| -> Option<(u32, u32)> {
|
||||
if !self.mv_ok(mx, my, mv) {
|
||||
return None;
|
||||
}
|
||||
let sad = self.sad(r, mx, my, mv);
|
||||
Some((sad + lambda * (mvd_bits(mv.0 - pred.0) + mvd_bits(mv.1 - pred.1)), sad))
|
||||
};
|
||||
let mut best = ((0, 0), cost((0, 0)).unwrap_or((u32::MAX, u32::MAX)));
|
||||
for &c in cands {
|
||||
let c = (c.0 & !1, c.1 & !1);
|
||||
if let Some(k) = cost(c).filter(|k| k.0 < best.1.0) {
|
||||
best = (c, k);
|
||||
}
|
||||
}
|
||||
for _ in 0..32 {
|
||||
let centre = best.0;
|
||||
for d in [(2, 0), (-2, 0), (0, 2), (0, -2)] {
|
||||
let mv = (centre.0 + d.0, centre.1 + d.1);
|
||||
if let Some(k) = cost(mv).filter(|k| k.0 < best.1.0) {
|
||||
best = (mv, k);
|
||||
}
|
||||
}
|
||||
if best.0 == centre {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let centre = best.0;
|
||||
for d in [(-1, -1), (0, -1), (1, -1), (-1, 0), (1, 0), (-1, 1), (0, 1), (1, 1)] {
|
||||
let mv = (centre.0 + d.0, centre.1 + d.1);
|
||||
if let Some(k) = cost(mv).filter(|k| k.0 < best.1.0) {
|
||||
best = (mv, k);
|
||||
}
|
||||
}
|
||||
(best.0, best.1.1)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn inter_or_intra(&self, r: &Frame, mx: usize, my: usize, pred: (i32, i32), mbs: &[Mb], dc: &mut DcStore, recon: &mut Frame) -> Mb {
|
||||
let i = my * self.mb_w + mx;
|
||||
let mut cands = vec![pred];
|
||||
if mx > 0 {
|
||||
cands.push(mbs[i - 1].mv);
|
||||
}
|
||||
if my > 0 {
|
||||
cands.push(mbs[i - self.mb_w].mv);
|
||||
}
|
||||
let (mv, sad) = self.search(r, mx, my, pred, &cands);
|
||||
// Intra when the macroblock's own variation is clearly below the prediction error
|
||||
// (the usual H.263 test), or when it is due for refresh.
|
||||
let s = &self.src.y;
|
||||
let row = |j: usize| &s.data[(my * 16 + j) * s.w + mx * 16..][..16];
|
||||
let sum: u32 = (0..16).map(|j| row(j).iter().map(|&v| u32::from(v)).sum::<u32>()).sum();
|
||||
let mean = (sum + 128) / 256;
|
||||
let dev: u32 = (0..16).map(|j| row(j).iter().map(|&v| u32::from(v).abs_diff(mean)).sum::<u32>()).sum();
|
||||
if self.refresh[i] >= REFRESH || dev + 500 < sad {
|
||||
return self.intra(mx, my, dc, recon);
|
||||
}
|
||||
for b in 0..6 {
|
||||
dc.set(b, mx, my, DC_DEFAULT);
|
||||
}
|
||||
let mut mb = Mb { mode: Mode::Inter, mv, pred, ..Mb::skip() };
|
||||
let qp = self.qp;
|
||||
let cmv = chroma_mv(mv);
|
||||
let mut py = [0u8; 256];
|
||||
predict(&r.y, (mx * 16) as i32, (my * 16) as i32, mv, 16, self.rounding, &mut py);
|
||||
let mut pc = [[0u8; 64]; 2];
|
||||
predict(&r.u, (mx * 8) as i32, (my * 8) as i32, cmv, 8, self.rounding, &mut pc[0]);
|
||||
predict(&r.v, (mx * 8) as i32, (my * 8) as i32, cmv, 8, self.rounding, &mut pc[1]);
|
||||
let mut rec = [[0i32; 64]; 6];
|
||||
for b in 0..6 {
|
||||
let p = |k: usize| -> i32 {
|
||||
match b {
|
||||
0..=3 => i32::from(py[((b >> 1) * 8 + k / 8) * 16 + (b & 1) * 8 + k % 8]),
|
||||
_ => i32::from(pc[b - 4][k]),
|
||||
}
|
||||
};
|
||||
let src = self.source_block(b, mx, my);
|
||||
let resid: [i32; 64] = std::array::from_fn(|k| src[k] - p(k));
|
||||
let mut coef = [0; 64];
|
||||
fdct(&resid, &mut coef);
|
||||
let mut deq = [0; 64];
|
||||
for k in 0..64 {
|
||||
let c = coef[k];
|
||||
let l = ((c.abs() - qp / 2) / (2 * qp)).clamp(0, MAX_LEVEL);
|
||||
let l = if c < 0 { -l } else { l };
|
||||
mb.levels[b][k] = l as i16;
|
||||
deq[k] = dequant(l, qp);
|
||||
if l != 0 {
|
||||
mb.cbp |= 1 << (5 - b);
|
||||
}
|
||||
}
|
||||
if mb.cbp & (1 << (5 - b)) != 0 {
|
||||
idct(&mut deq);
|
||||
}
|
||||
rec[b] = std::array::from_fn(|k| (p(k) + deq[k]).clamp(0, 255));
|
||||
}
|
||||
if mv == (0, 0) && mb.cbp == 0 {
|
||||
mb = Mb::skip();
|
||||
}
|
||||
for (b, rec) in rec.iter().enumerate() {
|
||||
let (plane, x, y) = block_origin_mut(recon, b, mx, my);
|
||||
for k in 0..64 {
|
||||
plane.data[(y + k / 8) * plane.w + x + k % 8] = rec[k] as u8;
|
||||
}
|
||||
}
|
||||
mb
|
||||
}
|
||||
}
|
||||
|
||||
/// The plane and top-left sample of block `b` (0–3 luminance in raster order, 4 Cb, 5 Cr).
|
||||
fn block_origin(f: &Frame, b: usize, mx: usize, my: usize) -> (&Plane, usize, usize) {
|
||||
match b {
|
||||
0..=3 => (&f.y, mx * 16 + (b & 1) * 8, my * 16 + (b >> 1) * 8),
|
||||
4 => (&f.u, mx * 8, my * 8),
|
||||
_ => (&f.v, mx * 8, my * 8),
|
||||
}
|
||||
}
|
||||
|
||||
fn block_origin_mut(f: &mut Frame, b: usize, mx: usize, my: usize) -> (&mut Plane, usize, usize) {
|
||||
match b {
|
||||
0..=3 => (&mut f.y, mx * 16 + (b & 1) * 8, my * 16 + (b >> 1) * 8),
|
||||
4 => (&mut f.u, mx * 8, my * 8),
|
||||
_ => (&mut f.v, mx * 8, my * 8),
|
||||
}
|
||||
}
|
||||
|
||||
/// The motion vector predictor of macroblock `(mx, my)` (§7.6.5): the median of the vectors to
|
||||
/// the left, above and above right. A candidate outside the VOP is zero when it is the only one;
|
||||
/// when two are outside, the third is the predictor. Intra and skipped macroblocks count as zero.
|
||||
pub(super) fn mv_pred(mbs: &[Mb], mb_w: usize, mx: usize, my: usize) -> (i32, i32) {
|
||||
let i = my * mb_w + mx;
|
||||
let a = (mx > 0).then(|| mbs[i - 1].mv);
|
||||
let b = (my > 0).then(|| mbs[i - mb_w].mv);
|
||||
let c = (my > 0 && mx + 1 < mb_w).then(|| mbs[i - mb_w + 1].mv);
|
||||
match (a, b, c) {
|
||||
(None, None, None) => (0, 0),
|
||||
(Some(v), None, None) | (None, Some(v), None) | (None, None, Some(v)) => v,
|
||||
_ => {
|
||||
let [a, b, c] = [a, b, c].map(|v| v.unwrap_or((0, 0)));
|
||||
(median(a.0, b.0, c.0), median(a.1, b.1, c.1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn put(w: &mut BitWriter, (code, len): (u16, u8)) {
|
||||
w.write_bits(u32::from(code), u32::from(len));
|
||||
}
|
||||
|
||||
/// One `motion_code` and `motion_residual` (§7.6.3), the difference wrapped into the range of
|
||||
/// `fcode`.
|
||||
fn write_mvd(w: &mut BitWriter, d: i32, fcode: u8) {
|
||||
let r_size = u32::from(fcode - 1);
|
||||
let f = 1i32 << r_size;
|
||||
let (low, high, range) = (-32 * f, 32 * f - 1, 64 * f);
|
||||
let d = if d < low {
|
||||
d + range
|
||||
} else if d > high {
|
||||
d - range
|
||||
} else {
|
||||
d
|
||||
};
|
||||
if d == 0 {
|
||||
put(w, MVD[0]);
|
||||
return;
|
||||
}
|
||||
let a = d.abs() - 1;
|
||||
let code = (a >> r_size) + 1;
|
||||
put(w, MVD[code as usize]);
|
||||
w.write_bit(d < 0);
|
||||
if r_size > 0 {
|
||||
w.write_bits((a & (f - 1)) as u32, r_size);
|
||||
}
|
||||
}
|
||||
|
||||
/// The run-length events of a block from scan position `start`, as `(run, level)`.
|
||||
fn events(levels: &[i16; 64], start: usize) -> Vec<(usize, i32)> {
|
||||
let mut out = Vec::new();
|
||||
let mut run = 0;
|
||||
for &z in &ZIGZAG[start..] {
|
||||
let l = i32::from(levels[z]);
|
||||
if l == 0 {
|
||||
run += 1;
|
||||
} else {
|
||||
out.push((run, l));
|
||||
run = 0;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// One TCOEF event (§6.3.8 / Table B-16, B-17, with the three escape modes of §7.4.1.3).
|
||||
fn write_event(w: &mut BitWriter, t: &Tcoef, last: bool, run: usize, level: i32, escape_only: bool) {
|
||||
let mag = level.unsigned_abs();
|
||||
let sign = level < 0;
|
||||
if !escape_only {
|
||||
if let Some((code, len)) = t.code(last, run, mag) {
|
||||
w.write_bits(code, len);
|
||||
w.write_bit(sign);
|
||||
return;
|
||||
}
|
||||
put(w, ESCAPE);
|
||||
let lmax = t.lmax(last, run);
|
||||
if let Some((code, len)) = mag.checked_sub(lmax).filter(|_| lmax > 0).and_then(|m| t.code(last, run, m)) {
|
||||
w.write_bit(false);
|
||||
w.write_bits(code, len);
|
||||
w.write_bit(sign);
|
||||
return;
|
||||
}
|
||||
if let Some((code, len)) = t.rmax(last, mag).and_then(|rmax| run.checked_sub(rmax + 1)).and_then(|r| t.code(last, r, mag)) {
|
||||
w.write_bits(0b10, 2);
|
||||
w.write_bits(code, len);
|
||||
w.write_bit(sign);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
put(w, ESCAPE);
|
||||
}
|
||||
w.write_bits(0b11, 2);
|
||||
w.write_bit(last);
|
||||
w.write_bits(run as u32, 6);
|
||||
w.write_bit(true);
|
||||
w.write_bits((level as u32) & 0xFFF, 12);
|
||||
w.write_bit(true);
|
||||
}
|
||||
|
||||
fn write_coeffs(w: &mut BitWriter, levels: &[i16; 64], start: usize, t: &Tcoef, escape_only: bool) {
|
||||
let ev = events(levels, start);
|
||||
let n = ev.len();
|
||||
for (k, &(run, level)) in ev.iter().enumerate() {
|
||||
write_event(w, t, k + 1 == n, run, level, escape_only);
|
||||
}
|
||||
}
|
||||
|
||||
fn write_intra_blocks(w: &mut BitWriter, mb: &Mb, escape_only: bool) {
|
||||
for b in 0..6 {
|
||||
let d = mb.dc_diff[b];
|
||||
let size = 32 - d.unsigned_abs().leading_zeros();
|
||||
put(w, if b < 4 { DC_SIZE_LUMA[size as usize] } else { DC_SIZE_CHROMA[size as usize] });
|
||||
if size > 0 {
|
||||
let bits = if d > 0 { d } else { d + (1 << size) - 1 };
|
||||
w.write_bits(bits as u32, size);
|
||||
if size > 8 {
|
||||
w.write_bit(true);
|
||||
}
|
||||
}
|
||||
if mb.cbp & (1 << (5 - b)) != 0 {
|
||||
write_coeffs(w, &mb.levels[b], 1, tables::intra(), escape_only);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One `macroblock()` of an I-VOP (`intra_vop`) or a P-VOP. `escape_only` codes every coefficient
|
||||
/// with the fixed-length escape (to check the code tables against a decoder).
|
||||
pub(super) fn write_mb(w: &mut BitWriter, mb: &Mb, intra_vop: bool, fcode: u8, escape_only: bool) {
|
||||
let cbpc = usize::from(mb.cbp & 3);
|
||||
let cbpy = usize::from(mb.cbp >> 2);
|
||||
if intra_vop {
|
||||
put(w, MCBPC_INTRA[cbpc]);
|
||||
w.write_bit(false); // ac_pred_flag
|
||||
put(w, CBPY[cbpy]);
|
||||
write_intra_blocks(w, mb, escape_only);
|
||||
return;
|
||||
}
|
||||
match mb.mode {
|
||||
Mode::Skip => w.write_bit(true),
|
||||
Mode::Intra => {
|
||||
w.write_bit(false);
|
||||
put(w, MCBPC_INTER[3 * 4 + cbpc]);
|
||||
w.write_bit(false); // ac_pred_flag
|
||||
put(w, CBPY[cbpy]);
|
||||
write_intra_blocks(w, mb, escape_only);
|
||||
}
|
||||
Mode::Inter => {
|
||||
w.write_bit(false);
|
||||
put(w, MCBPC_INTER[cbpc]);
|
||||
put(w, CBPY[cbpy ^ 15]);
|
||||
write_mvd(w, mb.mv.0 - mb.pred.0, fcode);
|
||||
write_mvd(w, mb.mv.1 - mb.pred.1, fcode);
|
||||
for b in 0..6 {
|
||||
if mb.cbp & (1 << (5 - b)) != 0 {
|
||||
write_coeffs(w, &mb.levels[b], 0, tables::inter(), escape_only);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn dc_scaler_follows_table_7_1() {
|
||||
assert_eq!((1..=31).map(|q| dc_scaler(q, true)).collect::<Vec<_>>()[..10], [8, 8, 8, 8, 10, 12, 14, 16, 17, 18]);
|
||||
assert_eq!(dc_scaler(24, true), 32);
|
||||
assert_eq!(dc_scaler(25, true), 34);
|
||||
assert_eq!(dc_scaler(31, true), 46);
|
||||
assert_eq!(dc_scaler(5, false), 9);
|
||||
assert_eq!(dc_scaler(24, false), 18);
|
||||
assert_eq!(dc_scaler(25, false), 19);
|
||||
assert_eq!(dc_scaler(31, false), 25);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dequant_is_h263_style() {
|
||||
assert_eq!(dequant(1, 3), 9);
|
||||
assert_eq!(dequant(-1, 4), -11);
|
||||
assert_eq!(dequant(2000, 31), 2047);
|
||||
assert_eq!(dequant(-2000, 31), -2048);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chroma_vectors_round_to_half_samples() {
|
||||
let c = |v| chroma_mv((v, 0)).0;
|
||||
assert_eq!([c(0), c(1), c(2), c(3), c(4), c(5)], [0, 1, 1, 1, 2, 3]);
|
||||
assert_eq!([c(-1), c(-2), c(-3), c(-4), c(-5)], [-1, -1, -1, -2, -3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mvd_wraps_and_splits_residual() {
|
||||
// fcode 2: difference 5 -> motion_code 3 (|5|-1 = 4 = 2·(3-1) + 0), residual 0.
|
||||
let mut w = BitWriter::new();
|
||||
write_mvd(&mut w, 5, 2);
|
||||
let mut want = BitWriter::new();
|
||||
put(&mut want, MVD[3]);
|
||||
want.write_bit(false);
|
||||
want.write_bits(0, 1);
|
||||
assert_eq!(w.finish(), want.finish());
|
||||
// fcode 1: 40 wraps to -24.
|
||||
let mut w = BitWriter::new();
|
||||
write_mvd(&mut w, 40, 1);
|
||||
let mut want = BitWriter::new();
|
||||
put(&mut want, MVD[24]);
|
||||
want.write_bit(true);
|
||||
assert_eq!(w.finish(), want.finish());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn predictor_rules_at_the_edges() {
|
||||
let mut mbs = vec![Mb::skip(); 6];
|
||||
mbs[0].mv = (4, -2);
|
||||
mbs[1].mv = (6, 2);
|
||||
mbs[2].mv = (8, 0);
|
||||
// first row: the left vector
|
||||
assert_eq!(mv_pred(&mbs, 3, 1, 0), (4, -2));
|
||||
assert_eq!(mv_pred(&mbs, 3, 0, 0), (0, 0));
|
||||
// left column: left is zero, median of 0, above, above right
|
||||
assert_eq!(mv_pred(&mbs, 3, 0, 1), (4, 0));
|
||||
// right column: above right is zero
|
||||
mbs[4].mv = (2, 2);
|
||||
assert_eq!(mv_pred(&mbs, 3, 2, 1), (2, 0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,460 @@
|
||||
//! Variable-length code tables of ISO/IEC 14496-2 Annex B, as `(code, length)` pairs (the code
|
||||
//! in the low `length` bits, most significant bit first).
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
/// Table B-6, `mcbpc` of an I-VOP macroblock, indexed by `(mb_type - 3) * 4 + cbpc`.
|
||||
pub(super) const MCBPC_INTRA: [(u16, u8); 8] = [(1, 1), (1, 3), (2, 3), (3, 3), (1, 4), (1, 6), (2, 6), (3, 6)];
|
||||
|
||||
/// Table B-7, `mcbpc` of a P-VOP macroblock, indexed by `mb_type * 4 + cbpc`
|
||||
/// (`mb_type` 0 inter, 1 inter+q, 2 inter4v, 3 intra, 4 intra+q).
|
||||
pub(super) const MCBPC_INTER: [(u16, u8); 20] = [
|
||||
(1, 1),
|
||||
(3, 4),
|
||||
(2, 4),
|
||||
(5, 6),
|
||||
(3, 3),
|
||||
(7, 7),
|
||||
(6, 7),
|
||||
(5, 9),
|
||||
(2, 3),
|
||||
(5, 7),
|
||||
(4, 7),
|
||||
(5, 8),
|
||||
(3, 5),
|
||||
(4, 8),
|
||||
(3, 8),
|
||||
(3, 7),
|
||||
(4, 6),
|
||||
(4, 9),
|
||||
(3, 9),
|
||||
(2, 9),
|
||||
];
|
||||
|
||||
/// Table B-8, `cbpy` indexed by the coded block pattern of an intra macroblock (block 0 in the
|
||||
/// most significant of the four bits). An inter macroblock sends the code of the inverted pattern.
|
||||
pub(super) const CBPY: [(u16, u8); 16] =
|
||||
[(3, 4), (5, 5), (4, 5), (9, 4), (3, 5), (7, 4), (2, 6), (11, 4), (2, 5), (3, 6), (5, 4), (10, 4), (4, 4), (8, 4), (6, 4), (3, 2)];
|
||||
|
||||
/// Table B-12, `motion_code` by magnitude 0–32; a sign bit (1 = negative) follows every code but 0.
|
||||
pub(super) const MVD: [(u16, u8); 33] = [
|
||||
(1, 1),
|
||||
(1, 2),
|
||||
(1, 3),
|
||||
(1, 4),
|
||||
(3, 6),
|
||||
(5, 7),
|
||||
(4, 7),
|
||||
(3, 7),
|
||||
(11, 9),
|
||||
(10, 9),
|
||||
(9, 9),
|
||||
(17, 10),
|
||||
(16, 10),
|
||||
(15, 10),
|
||||
(14, 10),
|
||||
(13, 10),
|
||||
(12, 10),
|
||||
(11, 10),
|
||||
(10, 10),
|
||||
(9, 10),
|
||||
(8, 10),
|
||||
(7, 10),
|
||||
(6, 10),
|
||||
(5, 10),
|
||||
(4, 10),
|
||||
(7, 11),
|
||||
(6, 11),
|
||||
(5, 11),
|
||||
(4, 11),
|
||||
(3, 11),
|
||||
(2, 11),
|
||||
(3, 12),
|
||||
(2, 12),
|
||||
];
|
||||
|
||||
/// Table B-13, `dct_dc_size_luminance` 0–12.
|
||||
pub(super) const DC_SIZE_LUMA: [(u16, u8); 13] =
|
||||
[(3, 3), (3, 2), (2, 2), (2, 3), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8), (1, 9), (1, 10), (1, 11)];
|
||||
|
||||
/// Table B-14, `dct_dc_size_chrominance` 0–12.
|
||||
pub(super) const DC_SIZE_CHROMA: [(u16, u8); 13] =
|
||||
[(3, 2), (2, 2), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8), (1, 9), (1, 10), (1, 11), (1, 12)];
|
||||
|
||||
/// The TCOEF escape code (`0000 011`), shared by both coefficient tables.
|
||||
pub(super) const ESCAPE: (u16, u8) = (3, 7);
|
||||
|
||||
/// Table B-16, intra TCOEF codes, ordered by `last`, then `run`, then `level` (as
|
||||
/// [`INTRA_LMAX`] lays them out).
|
||||
const INTRA_VLC: [(u16, u8); 102] = [
|
||||
// last 0, run 0, levels 1–27
|
||||
(0x2, 2),
|
||||
(0x6, 3),
|
||||
(0xF, 4),
|
||||
(0xD, 5),
|
||||
(0xC, 5),
|
||||
(0x15, 6),
|
||||
(0x13, 6),
|
||||
(0x12, 6),
|
||||
(0x17, 7),
|
||||
(0x1F, 8),
|
||||
(0x1E, 8),
|
||||
(0x1D, 8),
|
||||
(0x25, 9),
|
||||
(0x24, 9),
|
||||
(0x23, 9),
|
||||
(0x21, 9),
|
||||
(0x21, 10),
|
||||
(0x20, 10),
|
||||
(0xF, 10),
|
||||
(0xE, 10),
|
||||
(0x7, 11),
|
||||
(0x6, 11),
|
||||
(0x20, 11),
|
||||
(0x21, 11),
|
||||
(0x50, 12),
|
||||
(0x51, 12),
|
||||
(0x52, 12),
|
||||
// last 0, run 1, levels 1–10
|
||||
(0xE, 4),
|
||||
(0x14, 6),
|
||||
(0x16, 7),
|
||||
(0x1C, 8),
|
||||
(0x20, 9),
|
||||
(0x1F, 9),
|
||||
(0xD, 10),
|
||||
(0x22, 11),
|
||||
(0x53, 12),
|
||||
(0x55, 12),
|
||||
// last 0, run 2, levels 1–5
|
||||
(0xB, 5),
|
||||
(0x15, 7),
|
||||
(0x1E, 9),
|
||||
(0xC, 10),
|
||||
(0x56, 12),
|
||||
// last 0, run 3, levels 1–4
|
||||
(0x11, 6),
|
||||
(0x1B, 8),
|
||||
(0x1D, 9),
|
||||
(0xB, 10),
|
||||
// last 0, runs 4–7, levels 1–3
|
||||
(0x10, 6),
|
||||
(0x22, 9),
|
||||
(0xA, 10),
|
||||
(0xD, 6),
|
||||
(0x1C, 9),
|
||||
(0x8, 10),
|
||||
(0x12, 7),
|
||||
(0x1B, 9),
|
||||
(0x54, 12),
|
||||
(0x14, 7),
|
||||
(0x1A, 9),
|
||||
(0x57, 12),
|
||||
// last 0, runs 8–9, levels 1–2
|
||||
(0x19, 8),
|
||||
(0x9, 10),
|
||||
(0x18, 8),
|
||||
(0x23, 11),
|
||||
// last 0, runs 10–14, level 1
|
||||
(0x17, 8),
|
||||
(0x19, 9),
|
||||
(0x18, 9),
|
||||
(0x7, 10),
|
||||
(0x58, 12),
|
||||
// last 1, run 0, levels 1–8
|
||||
(0x7, 4),
|
||||
(0xC, 6),
|
||||
(0x16, 8),
|
||||
(0x17, 9),
|
||||
(0x6, 10),
|
||||
(0x5, 11),
|
||||
(0x4, 11),
|
||||
(0x59, 12),
|
||||
// last 1, run 1, levels 1–3
|
||||
(0xF, 6),
|
||||
(0x16, 9),
|
||||
(0x5, 10),
|
||||
// last 1, runs 2–6, levels 1–2
|
||||
(0xE, 6),
|
||||
(0x4, 10),
|
||||
(0x11, 7),
|
||||
(0x24, 11),
|
||||
(0x10, 7),
|
||||
(0x25, 11),
|
||||
(0x13, 7),
|
||||
(0x5A, 12),
|
||||
(0x15, 8),
|
||||
(0x5B, 12),
|
||||
// last 1, runs 7–20, level 1
|
||||
(0x14, 8),
|
||||
(0x13, 8),
|
||||
(0x1A, 8),
|
||||
(0x15, 9),
|
||||
(0x14, 9),
|
||||
(0x13, 9),
|
||||
(0x12, 9),
|
||||
(0x11, 9),
|
||||
(0x26, 11),
|
||||
(0x27, 11),
|
||||
(0x5C, 12),
|
||||
(0x5D, 12),
|
||||
(0x5E, 12),
|
||||
(0x5F, 12),
|
||||
];
|
||||
|
||||
/// The largest level of each run in [`INTRA_VLC`]: `[last][run]`, 0 past the last run.
|
||||
const INTRA_LMAX: [&[u8]; 2] = [&[27, 10, 5, 4, 3, 3, 3, 3, 2, 2, 1, 1, 1, 1, 1], &[8, 3, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]];
|
||||
|
||||
/// Table B-17, inter TCOEF codes, in the order of [`INTER_LMAX`].
|
||||
const INTER_VLC: [(u16, u8); 102] = [
|
||||
// last 0, run 0, levels 1–12
|
||||
(0x2, 2),
|
||||
(0xF, 4),
|
||||
(0x15, 6),
|
||||
(0x17, 7),
|
||||
(0x1F, 8),
|
||||
(0x25, 9),
|
||||
(0x24, 9),
|
||||
(0x21, 10),
|
||||
(0x20, 10),
|
||||
(0x7, 11),
|
||||
(0x6, 11),
|
||||
(0x20, 11),
|
||||
// last 0, run 1, levels 1–6
|
||||
(0x6, 3),
|
||||
(0x14, 6),
|
||||
(0x1E, 8),
|
||||
(0xF, 10),
|
||||
(0x21, 11),
|
||||
(0x50, 12),
|
||||
// last 0, run 2, levels 1–4
|
||||
(0xE, 4),
|
||||
(0x1D, 8),
|
||||
(0xE, 10),
|
||||
(0x51, 12),
|
||||
// last 0, runs 3–6, levels 1–3
|
||||
(0xD, 5),
|
||||
(0x23, 9),
|
||||
(0xD, 10),
|
||||
(0xC, 5),
|
||||
(0x22, 9),
|
||||
(0x52, 12),
|
||||
(0xB, 5),
|
||||
(0xC, 10),
|
||||
(0x53, 12),
|
||||
(0x13, 6),
|
||||
(0xB, 10),
|
||||
(0x54, 12),
|
||||
// last 0, runs 7–10, levels 1–2
|
||||
(0x12, 6),
|
||||
(0xA, 10),
|
||||
(0x11, 6),
|
||||
(0x9, 10),
|
||||
(0x10, 6),
|
||||
(0x8, 10),
|
||||
(0x16, 7),
|
||||
(0x55, 12),
|
||||
// last 0, runs 11–26, level 1
|
||||
(0x15, 7),
|
||||
(0x14, 7),
|
||||
(0x1C, 8),
|
||||
(0x1B, 8),
|
||||
(0x21, 9),
|
||||
(0x20, 9),
|
||||
(0x1F, 9),
|
||||
(0x1E, 9),
|
||||
(0x1D, 9),
|
||||
(0x1C, 9),
|
||||
(0x1B, 9),
|
||||
(0x1A, 9),
|
||||
(0x22, 11),
|
||||
(0x23, 11),
|
||||
(0x56, 12),
|
||||
(0x57, 12),
|
||||
// last 1, run 0, levels 1–3
|
||||
(0x7, 4),
|
||||
(0x19, 9),
|
||||
(0x5, 11),
|
||||
// last 1, run 1, levels 1–2
|
||||
(0xF, 6),
|
||||
(0x4, 11),
|
||||
// last 1, runs 2–40, level 1
|
||||
(0xE, 6),
|
||||
(0xD, 6),
|
||||
(0xC, 6),
|
||||
(0x13, 7),
|
||||
(0x12, 7),
|
||||
(0x11, 7),
|
||||
(0x10, 7),
|
||||
(0x1A, 8),
|
||||
(0x19, 8),
|
||||
(0x18, 8),
|
||||
(0x17, 8),
|
||||
(0x16, 8),
|
||||
(0x15, 8),
|
||||
(0x14, 8),
|
||||
(0x13, 8),
|
||||
(0x18, 9),
|
||||
(0x17, 9),
|
||||
(0x16, 9),
|
||||
(0x15, 9),
|
||||
(0x14, 9),
|
||||
(0x13, 9),
|
||||
(0x12, 9),
|
||||
(0x11, 9),
|
||||
(0x7, 10),
|
||||
(0x6, 10),
|
||||
(0x5, 10),
|
||||
(0x4, 10),
|
||||
(0x24, 11),
|
||||
(0x25, 11),
|
||||
(0x26, 11),
|
||||
(0x27, 11),
|
||||
(0x58, 12),
|
||||
(0x59, 12),
|
||||
(0x5A, 12),
|
||||
(0x5B, 12),
|
||||
(0x5C, 12),
|
||||
(0x5D, 12),
|
||||
(0x5E, 12),
|
||||
(0x5F, 12),
|
||||
];
|
||||
|
||||
/// The largest level of each run in [`INTER_VLC`]: `[last][run]`.
|
||||
const INTER_LMAX: [&[u8]; 2] = [
|
||||
&[12, 6, 4, 3, 3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
&[3, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
];
|
||||
|
||||
/// A TCOEF table indexed by `(last, run, level)`, with the `LMAX` and `RMAX` of the escape modes.
|
||||
pub(super) struct Tcoef {
|
||||
/// `code << 8 | length`, 0 where the table has no code; `[last][run][level]`.
|
||||
codes: [[[u32; 28]; 64]; 2],
|
||||
/// The largest level of a run (`LMAX`, 0 = none): `[last][run]`.
|
||||
lmax: [[u8; 64]; 2],
|
||||
/// The largest run of a level (`RMAX`, -1 = none): `[last][level]`.
|
||||
rmax: [[i8; 28]; 2],
|
||||
}
|
||||
|
||||
impl Tcoef {
|
||||
fn build(vlc: &[(u16, u8); 102], lmax: [&[u8]; 2]) -> Tcoef {
|
||||
let mut t = Tcoef { codes: [[[0; 28]; 64]; 2], lmax: [[0; 64]; 2], rmax: [[-1; 28]; 2] };
|
||||
let mut codes = vlc.iter();
|
||||
for (last, runs) in lmax.iter().enumerate() {
|
||||
for (run, &max) in runs.iter().enumerate() {
|
||||
t.lmax[last][run] = max;
|
||||
for level in 1..=usize::from(max) {
|
||||
let (code, len) = codes.next().copied().unwrap_or((0, 0));
|
||||
t.codes[last][run][level] = u32::from(code) << 8 | u32::from(len);
|
||||
t.rmax[last][level] = t.rmax[last][level].max(run as i8);
|
||||
}
|
||||
}
|
||||
}
|
||||
t
|
||||
}
|
||||
|
||||
/// The code of `(last, run, level)` (`level` ≥ 1), if the table has one.
|
||||
pub(super) fn code(&self, last: bool, run: usize, level: u32) -> Option<(u32, u32)> {
|
||||
let row = self.codes[usize::from(last)].get(run)?;
|
||||
let c = *row.get(level as usize)?;
|
||||
(c != 0).then_some((c >> 8, c & 0xFF))
|
||||
}
|
||||
|
||||
/// `LMAX(last, run)`, 0 when the table has no such run.
|
||||
pub(super) fn lmax(&self, last: bool, run: usize) -> u32 {
|
||||
self.lmax[usize::from(last)].get(run).map_or(0, |&m| u32::from(m))
|
||||
}
|
||||
|
||||
/// `RMAX(last, level)`, `None` when the table has no such level.
|
||||
pub(super) fn rmax(&self, last: bool, level: u32) -> Option<usize> {
|
||||
let r = *self.rmax[usize::from(last)].get(level as usize)?;
|
||||
usize::try_from(r).ok()
|
||||
}
|
||||
}
|
||||
|
||||
/// The intra (B-16) table.
|
||||
pub(super) fn intra() -> &'static Tcoef {
|
||||
static T: OnceLock<Tcoef> = OnceLock::new();
|
||||
T.get_or_init(|| Tcoef::build(&INTRA_VLC, INTRA_LMAX))
|
||||
}
|
||||
|
||||
/// The inter (B-17) table.
|
||||
pub(super) fn inter() -> &'static Tcoef {
|
||||
static T: OnceLock<Tcoef> = OnceLock::new();
|
||||
T.get_or_init(|| Tcoef::build(&INTER_VLC, INTER_LMAX))
|
||||
}
|
||||
|
||||
/// The zigzag scan: `ZIGZAG[i]` is the raster position of the `i`-th coefficient.
|
||||
pub(super) const ZIGZAG: [usize; 64] = [
|
||||
0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, 12, 19, 26, 33, 40, 48, 41, 34, 27, 20, 13, 6, 7, 14, 21, 28, 35, 42, 49, 56, 57, 50, 43,
|
||||
36, 29, 22, 15, 23, 30, 37, 44, 51, 58, 59, 52, 45, 38, 31, 39, 46, 53, 60, 61, 54, 47, 55, 62, 63,
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Fails if a code is a prefix of another (each read as a bit string); returns the Kraft sum
|
||||
/// of the lengths (1 for a complete code).
|
||||
fn prefix_free(name: &str, codes: &[(u16, u8)]) -> f64 {
|
||||
for (i, &(a, la)) in codes.iter().enumerate() {
|
||||
for &(b, lb) in &codes[i + 1..] {
|
||||
let n = la.min(lb);
|
||||
assert!(a >> (la - n) != b >> (lb - n), "{name}: {a:#b}/{la} and {b:#b}/{lb} share a prefix");
|
||||
}
|
||||
}
|
||||
codes.iter().map(|&(_, l)| 0.5f64.powi(i32::from(l))).sum()
|
||||
}
|
||||
|
||||
/// Both TCOEF tables use the same codes (Table B-16 reassigns B-17's), they form a prefix
|
||||
/// code with the escape, and one 9-bit code is left unused.
|
||||
#[test]
|
||||
fn tcoef_tables_are_prefix_codes() {
|
||||
let mut a = INTRA_VLC.to_vec();
|
||||
let mut b = INTER_VLC.to_vec();
|
||||
a.sort_unstable();
|
||||
b.sort_unstable();
|
||||
assert_eq!(a, b);
|
||||
let mut codes = INTER_VLC.to_vec();
|
||||
codes.push(ESCAPE);
|
||||
let kraft = prefix_free("tcoef", &codes);
|
||||
assert!((kraft - (1.0 - 0.5f64.powi(9))).abs() < 1e-12, "Kraft sum {kraft}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tcoef_layout_matches_the_code_count() {
|
||||
for (name, lmax) in [("intra", INTRA_LMAX), ("inter", INTER_LMAX)] {
|
||||
let n: usize = lmax.iter().flat_map(|r| r.iter()).map(|&m| usize::from(m)).sum();
|
||||
assert_eq!(n, 102, "{name}");
|
||||
}
|
||||
let t = intra();
|
||||
assert_eq!(t.code(false, 0, 1), Some((0x2, 2)));
|
||||
assert_eq!(t.code(true, 0, 1), Some((0x7, 4)));
|
||||
assert_eq!(t.code(true, 20, 1), Some((0x5F, 12)));
|
||||
assert_eq!(t.lmax(false, 0), 27);
|
||||
assert_eq!(t.rmax(true, 1), Some(20));
|
||||
assert_eq!(t.rmax(false, 27), Some(0));
|
||||
assert_eq!(t.rmax(false, 28), None);
|
||||
let t = inter();
|
||||
assert_eq!(t.code(false, 1, 1), Some((0x6, 3)));
|
||||
assert_eq!(t.code(true, 40, 1), Some((0x5F, 12)));
|
||||
assert_eq!(t.rmax(true, 1), Some(40));
|
||||
assert_eq!(t.code(false, 27, 1), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn small_tables_are_prefix_free() {
|
||||
let stuffed = |t: &[(u16, u8)]| [t, &[(1, 9)]].concat();
|
||||
assert!(prefix_free("mcbpc intra", &stuffed(&MCBPC_INTRA)) <= 1.0);
|
||||
assert!(prefix_free("mcbpc inter", &stuffed(&MCBPC_INTER)) <= 1.0);
|
||||
assert!(prefix_free("cbpy", &CBPY) <= 1.0);
|
||||
assert!(prefix_free("dc luma", &DC_SIZE_LUMA) <= 1.0);
|
||||
assert!(prefix_free("dc chroma", &DC_SIZE_CHROMA) <= 1.0);
|
||||
assert!(prefix_free("mvd", &MVD) <= 1.0);
|
||||
let mut seen = [false; 64];
|
||||
for &z in &ZIGZAG {
|
||||
seen[z] = true;
|
||||
}
|
||||
assert!(seen.iter().all(|&s| s));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
//! Every entry of the coefficient code tables, and the first two escape modes, against ffmpeg:
|
||||
//! a stream coded with the tables must decode exactly as the same coefficients sent with the
|
||||
//! fixed-length escape. Skipped when ffmpeg is absent.
|
||||
|
||||
use std::process::Command;
|
||||
|
||||
use craft_media::bitstream::BitWriter;
|
||||
|
||||
use super::headers::{self, Sequence, Timing, VopHeader};
|
||||
use super::picture::{Mb, Mode, write_mb};
|
||||
use super::tables::{self, Tcoef, ZIGZAG};
|
||||
|
||||
/// One `(last, run, level)` event of interest for each block: every table entry, every
|
||||
/// `LMAX + 1` (escape mode 1) and every `RMAX + 1` (escape mode 2), with alternating signs.
|
||||
fn events(t: &Tcoef) -> Vec<(bool, usize, i32)> {
|
||||
let mut out = Vec::new();
|
||||
for last in [false, true] {
|
||||
for run in 0..64 {
|
||||
let lmax = t.lmax(last, run);
|
||||
for level in 1..=lmax {
|
||||
out.push((last, run, level as i32));
|
||||
}
|
||||
if lmax > 0 {
|
||||
out.push((last, run, lmax as i32 + 1));
|
||||
}
|
||||
}
|
||||
for level in 1..28 {
|
||||
if let Some(rmax) = t.rmax(last, level) {
|
||||
out.push((last, rmax + 1, level as i32));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (i, e) in out.iter_mut().enumerate() {
|
||||
if i % 2 == 1 {
|
||||
e.2 = -e.2;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// A block holding `event` from scan position `start`; an event with `last` clear is followed by
|
||||
/// a closing level at scan position 63.
|
||||
fn block(event: (bool, usize, i32), start: usize) -> [i16; 64] {
|
||||
let (last, run, level) = event;
|
||||
let mut b = [0i16; 64];
|
||||
b[ZIGZAG[start + run]] = level as i16;
|
||||
if !last {
|
||||
b[ZIGZAG[63]] = 1;
|
||||
}
|
||||
b
|
||||
}
|
||||
|
||||
/// Macroblocks carrying `events` six to a macroblock, padded to `n`.
|
||||
fn macroblocks(events: &[(bool, usize, i32)], intra: bool, n: usize) -> Vec<Mb> {
|
||||
let mut mbs = Vec::new();
|
||||
for chunk in events.chunks(6) {
|
||||
let mut mb = Mb { mode: if intra { Mode::Intra } else { Mode::Inter }, ..Mb::skip() };
|
||||
for (b, &e) in chunk.iter().enumerate() {
|
||||
mb.levels[b] = block(e, usize::from(intra));
|
||||
mb.cbp |= 1 << (5 - b);
|
||||
}
|
||||
mbs.push(mb);
|
||||
}
|
||||
while mbs.len() < n {
|
||||
mbs.push(Mb::skip());
|
||||
}
|
||||
mbs
|
||||
}
|
||||
|
||||
/// An I-VOP with the intra events, then a P-VOP with the inter events (zero vectors).
|
||||
fn stream(width: u32, height: u32, intra: &[Mb], inter: &[Mb], escape_only: bool) -> Vec<u8> {
|
||||
let timing = Timing { resolution: 25, increment: 1 };
|
||||
let mut w = BitWriter::new();
|
||||
w.write_bytes(&headers::sequence_headers(&Sequence { width, height, timing, profile_level: 3, sar: (1, 1), color: None }));
|
||||
for (k, mbs) in [intra, inter].into_iter().enumerate() {
|
||||
let i = k == 0;
|
||||
let h = VopHeader { intra: i, seconds: 0, time_increment: k as u32, rounding: false, quant: 1, fcode: 1 };
|
||||
headers::vop_header(&mut w, &h, timing);
|
||||
for mb in mbs {
|
||||
let mut mb = mb.clone();
|
||||
if i {
|
||||
mb.mode = Mode::Intra;
|
||||
}
|
||||
write_mb(&mut w, &mb, i, 1, escape_only);
|
||||
}
|
||||
headers::stuffing(&mut w);
|
||||
}
|
||||
w.finish()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn code_tables_decode_like_the_fixed_length_escape() {
|
||||
let Some(ff) = craft_media_testkit::ffmpeg_or_skip("mpeg4 code tables") else { return };
|
||||
let (ei, ep) = (events(tables::intra()), events(tables::inter()));
|
||||
let mb_w = 8usize;
|
||||
let n = ei.len().max(ep.len()).div_ceil(6);
|
||||
let mb_h = n.div_ceil(mb_w);
|
||||
let (w, h) = ((mb_w * 16) as u32, (mb_h * 16) as u32);
|
||||
let mut intra = macroblocks(&ei, true, mb_w * mb_h);
|
||||
for mb in &mut intra {
|
||||
mb.mode = Mode::Intra;
|
||||
}
|
||||
let inter = macroblocks(&ep, false, mb_w * mb_h);
|
||||
let dir = craft_media_testkit::private_dir("mpeg4-tables");
|
||||
let decode = |name: &str, escape_only: bool| -> Vec<u8> {
|
||||
let path = dir.join(name);
|
||||
std::fs::write(&path, stream(w, h, &intra, &inter, escape_only)).expect("write");
|
||||
let out = Command::new(&ff)
|
||||
.args(["-hide_banner", "-nostdin", "-v", "warning", "-f", "m4v", "-i"])
|
||||
.arg(&path)
|
||||
.args(["-f", "rawvideo", "-pix_fmt", "yuv420p", "-"])
|
||||
.output()
|
||||
.expect("run ffmpeg");
|
||||
let diag = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(out.status.success() && diag.trim().is_empty(), "{name}: {diag}");
|
||||
out.stdout
|
||||
};
|
||||
let vlc = decode("vlc.m4v", false);
|
||||
let esc = decode("escape.m4v", true);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
assert_eq!(vlc.len(), (w * h * 3 / 2 * 2) as usize, "two pictures");
|
||||
assert!(vlc.iter().any(|&v| v != vlc[0]), "the pictures carry the coefficients");
|
||||
if let Some(i) = vlc.iter().zip(&esc).position(|(a, b)| a != b) {
|
||||
let frame = i / (w * h * 3 / 2) as usize;
|
||||
panic!("the table-coded stream decodes differently from the escape-coded one (picture {frame}, byte {i})");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
//! MPEG-4 Part 2 video (ISO/IEC 14496-2, MPEG-4 Visual), the format Xvid and DivX write.
|
||||
//!
|
||||
//! | Feature | What |
|
||||
//! |---|---|
|
||||
//! | `mpeg4-encode` | Encoder: Simple profile, I- and P-VOPs ([`Encoder`]) |
|
||||
//!
|
||||
//! Feature `mpeg4` turns on the encoder.
|
||||
|
||||
#[cfg(feature = "mpeg4-encode")]
|
||||
mod encode;
|
||||
#[cfg(feature = "mpeg4-encode")]
|
||||
pub use encode::*;
|
||||
@@ -0,0 +1,324 @@
|
||||
//! The MPEG-4 Part 2 encoder against ffmpeg / ffprobe as external decoder oracles (skipped when
|
||||
//! absent), plus API and robustness checks that need no oracle.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
use craft_video::mpeg4::{ColorConfig, Encoder, EncoderConfig, Error, Packet, RateControl, YuvFrame};
|
||||
|
||||
struct Yuv {
|
||||
w: usize,
|
||||
y: Vec<u8>,
|
||||
u: Vec<u8>,
|
||||
v: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Yuv {
|
||||
fn frame(&self) -> YuvFrame<'_> {
|
||||
YuvFrame { y: &self.y, u: &self.u, v: &self.v, y_stride: self.w, uv_stride: self.w / 2 }
|
||||
}
|
||||
}
|
||||
|
||||
fn hash(mut x: u32) -> u32 {
|
||||
x ^= x >> 16;
|
||||
x = x.wrapping_mul(0x7feb_352d);
|
||||
x ^= x >> 15;
|
||||
x = x.wrapping_mul(0x846c_a68b);
|
||||
x ^= x >> 16;
|
||||
x
|
||||
}
|
||||
|
||||
/// Picture `t` of a moving test sequence: a panning texture (sub-sample motion), a drifting
|
||||
/// gradient, hard-edged blocks moving a whole number of samples a frame, and a little noise.
|
||||
fn synth(w: usize, h: usize, t: usize) -> Yuv {
|
||||
let tf = t as f32;
|
||||
let mut y = vec![0u8; w * h];
|
||||
for yy in 0..h {
|
||||
for xx in 0..w {
|
||||
let (px, py) = (xx as f32 + tf * 1.5, yy as f32 + tf * 0.5);
|
||||
let tex = ((px / 9.0).sin() + (py / 7.0).cos() + ((px + py) / 23.0).sin()) * 35.0;
|
||||
let grad = 40.0 * ((xx as f32 / w as f32) * 4.0 + tf * 0.05).sin();
|
||||
let bx = (xx + 3 * t) / 24;
|
||||
let by = yy / 24;
|
||||
let block = if (bx + by).is_multiple_of(3) { 50.0 } else { -20.0 };
|
||||
let noise = (hash((xx as u32) ^ (yy as u32) << 12 ^ (t as u32) << 24) % 9) as f32 - 4.0;
|
||||
y[yy * w + xx] = (128.0 + tex + grad + block + noise).clamp(0.0, 255.0) as u8;
|
||||
}
|
||||
}
|
||||
let (cw, ch) = (w / 2, h / 2);
|
||||
let mut u = vec![0u8; cw * ch];
|
||||
let mut v = vec![0u8; cw * ch];
|
||||
for yy in 0..ch {
|
||||
for xx in 0..cw {
|
||||
let fx = xx as f32 / cw as f32;
|
||||
let fy = yy as f32 / ch as f32;
|
||||
u[yy * cw + xx] = (128.0 + 60.0 * (fx * 3.0 + tf * 0.03).sin()) as u8;
|
||||
v[yy * cw + xx] = (128.0 + 50.0 * (fy * 5.0 - tf * 0.04).cos() * fx) as u8;
|
||||
}
|
||||
}
|
||||
Yuv { w, y, u, v }
|
||||
}
|
||||
|
||||
fn ffmpeg() -> Option<PathBuf> {
|
||||
craft_media_testkit::ffmpeg_or_skip("mpeg4 encoder oracle")
|
||||
}
|
||||
|
||||
fn out_dir(name: &str) -> PathBuf {
|
||||
craft_media_testkit::fixtures_dir(&format!("mpeg4enc/{name}"))
|
||||
}
|
||||
|
||||
fn write_stream(path: &Path, packets: &[Packet]) {
|
||||
let bytes: Vec<u8> = packets.iter().flat_map(|p| p.data.iter().copied()).collect();
|
||||
std::fs::write(path, bytes).expect("write stream");
|
||||
}
|
||||
|
||||
/// ffmpeg's decode of a raw MPEG-4 Part 2 stream: (diagnostics, yuv420p frames).
|
||||
fn ffmpeg_decode(ff: &Path, path: &Path) -> (String, Vec<u8>) {
|
||||
let out = Command::new(ff)
|
||||
.args(["-hide_banner", "-nostdin", "-v", "warning", "-f", "m4v", "-i"])
|
||||
.arg(path)
|
||||
.args(["-f", "rawvideo", "-pix_fmt", "yuv420p", "-"])
|
||||
.output()
|
||||
.expect("run ffmpeg");
|
||||
assert!(out.status.success(), "ffmpeg failed: {}", String::from_utf8_lossy(&out.stderr));
|
||||
(String::from_utf8_lossy(&out.stderr).into_owned(), out.stdout)
|
||||
}
|
||||
|
||||
fn psnr(a: &[u8], b: &[u8]) -> f64 {
|
||||
assert_eq!(a.len(), b.len());
|
||||
let se: f64 = a.iter().zip(b).map(|(&x, &y)| (f64::from(x) - f64::from(y)).powi(2)).sum();
|
||||
if se == 0.0 {
|
||||
return 99.0;
|
||||
}
|
||||
10.0 * (255.0f64 * 255.0 * a.len() as f64 / se).log10()
|
||||
}
|
||||
|
||||
fn planes(y: &Yuv) -> Vec<u8> {
|
||||
[&y.y[..], &y.u[..], &y.v[..]].concat()
|
||||
}
|
||||
|
||||
/// An encoded test sequence: the packets, and for each frame the encoder's reconstruction and the
|
||||
/// source (planes Y, U, V back to back).
|
||||
struct Encoded {
|
||||
enc: Encoder,
|
||||
packets: Vec<Packet>,
|
||||
recon: Vec<Vec<u8>>,
|
||||
src: Vec<Vec<u8>>,
|
||||
}
|
||||
|
||||
/// Encode `n` frames, keeping the encoder's reconstruction of each.
|
||||
fn encode(cfg: EncoderConfig, n: usize) -> Encoded {
|
||||
let (w, h) = (cfg.width as usize, cfg.height as usize);
|
||||
let mut enc = Encoder::new(cfg).expect("encoder");
|
||||
let (mut packets, mut recon, mut src) = (Vec::new(), Vec::new(), Vec::new());
|
||||
for t in 0..n {
|
||||
let pic = synth(w, h, t);
|
||||
packets.push(enc.encode(&pic.frame()).expect("encode"));
|
||||
let r = enc.reconstruction().expect("reconstruction");
|
||||
recon.push([r.y, r.u, r.v].concat());
|
||||
src.push(planes(&pic));
|
||||
}
|
||||
Encoded { enc, packets, recon, src }
|
||||
}
|
||||
|
||||
/// ffmpeg decodes the stream without a diagnostic, frame for frame, to the encoder's own
|
||||
/// reconstruction (within IDCT rounding) and close to the source.
|
||||
fn check_against_ffmpeg(name: &str, cfg: EncoderConfig, n: usize, min_psnr: f64) {
|
||||
let Some(ff) = ffmpeg() else { return };
|
||||
let (w, h) = (cfg.width as usize, cfg.height as usize);
|
||||
let Encoded { packets, recon, src, .. } = encode(cfg, n);
|
||||
let path = out_dir(name).join("stream.m4v");
|
||||
write_stream(&path, &packets);
|
||||
let (diag, dec) = ffmpeg_decode(&ff, &path);
|
||||
assert!(diag.trim().is_empty(), "{name}: ffmpeg reported: {diag}");
|
||||
let size = w * h * 3 / 2;
|
||||
assert_eq!(dec.len(), size * n, "{name}: decoded frame count");
|
||||
for (i, frame) in dec.chunks(size).enumerate() {
|
||||
let drift = psnr(&recon[i], frame);
|
||||
assert!(drift >= 48.0, "{name}: frame {i} differs from the encoder's reconstruction ({drift:.1} dB)");
|
||||
let q = psnr(&src[i], frame);
|
||||
assert!(q >= min_psnr, "{name}: frame {i} at {q:.1} dB against the source (want {min_psnr})");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ffmpeg_decodes_constant_quantiser_streams() {
|
||||
for (w, h, q, min) in [(176, 144, 2, 38.0), (320, 240, 5, 31.0), (200, 120, 12, 26.0), (64, 48, 31, 20.0)] {
|
||||
let mut cfg = EncoderConfig::new(w, h, 25, 1);
|
||||
cfg.rate = RateControl::Quant(q);
|
||||
cfg.keyint = 12;
|
||||
check_against_ffmpeg(&format!("cq_{w}x{h}_q{q}"), cfg, 30, min);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sizes that are not a multiple of 16 leave partial macroblocks at the right and bottom edges.
|
||||
#[test]
|
||||
fn ffmpeg_decodes_partial_macroblocks() {
|
||||
for (w, h) in [(18, 34), (98, 66), (2, 2)] {
|
||||
let mut cfg = EncoderConfig::new(w, h, 30000, 1001);
|
||||
cfg.rate = RateControl::Quant(3);
|
||||
check_against_ffmpeg(&format!("edge_{w}x{h}"), cfg, 12, 30.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// More than 128 P-VOPs: every macroblock is refreshed with an intra coding before IDCT
|
||||
/// mismatch can build up, and the time code crosses several seconds.
|
||||
#[test]
|
||||
fn long_gop_stays_in_step_with_the_decoder() {
|
||||
let mut cfg = EncoderConfig::new(96, 64, 10, 1);
|
||||
cfg.keyint = 1000;
|
||||
check_against_ffmpeg("long_gop", cfg, 150, 30.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bitrate_mode_lands_near_the_target() {
|
||||
let kbps = 400;
|
||||
let mut cfg = EncoderConfig::new(320, 240, 25, 1);
|
||||
cfg.rate = RateControl::Bitrate { kbps };
|
||||
cfg.keyint = 50;
|
||||
let n = 100;
|
||||
let Encoded { packets, .. } = encode(cfg.clone(), n);
|
||||
let bits: usize = packets.iter().map(|p| p.data.len() * 8).sum();
|
||||
let got = bits as f64 / (n as f64 / 25.0) / 1000.0;
|
||||
assert!((got - f64::from(kbps)).abs() < 0.15 * f64::from(kbps), "{got:.0} kbps for a {kbps} kbps target");
|
||||
check_against_ffmpeg("abr", cfg, 40, 25.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ffprobe_reads_the_configuration() {
|
||||
let Some(ff) = craft_media_testkit::ffprobe() else { return };
|
||||
let mut cfg = EncoderConfig::new(352, 288, 30000, 1001);
|
||||
cfg.sar = (12, 11);
|
||||
cfg.color = Some(ColorConfig { primaries: 6, transfer: 6, matrix: 6, full_range: false });
|
||||
let Encoded { packets, .. } = encode(cfg, 3);
|
||||
let path = out_dir("probe").join("stream.m4v");
|
||||
write_stream(&path, &packets);
|
||||
let out = Command::new(ff)
|
||||
.args([
|
||||
"-v",
|
||||
"error",
|
||||
"-show_entries",
|
||||
"stream=codec_name,profile,width,height,sample_aspect_ratio,color_primaries,color_space",
|
||||
"-of",
|
||||
"default=nw=1",
|
||||
])
|
||||
.arg(&path)
|
||||
.output()
|
||||
.expect("run ffprobe");
|
||||
let text = String::from_utf8_lossy(&out.stdout);
|
||||
for want in ["codec_name=mpeg4", "width=352", "height=288", "sample_aspect_ratio=12:11", "color_primaries=smpte170m", "color_space=smpte170m"] {
|
||||
assert!(text.lines().any(|l| l == want), "ffprobe: missing {want} in\n{text}");
|
||||
}
|
||||
assert!(text.contains("Simple"), "ffprobe profile:\n{text}");
|
||||
}
|
||||
|
||||
/// With the headers out of band, a decoder given the extradata (as MP4 or Matroska would)
|
||||
/// decodes the VOPs.
|
||||
#[test]
|
||||
fn out_of_band_headers_are_the_extradata() {
|
||||
let Some(ff) = ffmpeg() else { return };
|
||||
let mut cfg = EncoderConfig::new(160, 96, 25, 1);
|
||||
cfg.headers_in_band = false;
|
||||
let Encoded { enc, packets, .. } = encode(cfg, 5);
|
||||
assert!(!packets[0].data.starts_with(&[0, 0, 1, 0xB0]));
|
||||
let mut bytes = enc.extradata().to_vec();
|
||||
bytes.extend(packets.iter().flat_map(|p| p.data.iter().copied()));
|
||||
let path = out_dir("oob").join("stream.m4v");
|
||||
std::fs::write(&path, bytes).expect("write");
|
||||
let (diag, dec) = ffmpeg_decode(&ff, &path);
|
||||
assert!(diag.trim().is_empty(), "{diag}");
|
||||
assert_eq!(dec.len(), 160 * 96 * 3 / 2 * 5);
|
||||
let p = enc.codec_params();
|
||||
assert_eq!(p.extradata, enc.extradata());
|
||||
assert_eq!(p.tag, Some(*b"mp4v"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keyframes_follow_keyint_and_requests() {
|
||||
let mut cfg = EncoderConfig::new(32, 32, 25, 1);
|
||||
cfg.keyint = 4;
|
||||
let mut enc = Encoder::new(cfg).expect("encoder");
|
||||
let pic = synth(32, 32, 0);
|
||||
let mut keys = Vec::new();
|
||||
for i in 0..10 {
|
||||
if i == 6 {
|
||||
enc.request_keyframe();
|
||||
}
|
||||
keys.push(enc.encode(&pic.frame()).expect("encode").keyframe);
|
||||
}
|
||||
assert_eq!(keys, [true, false, false, false, true, false, true, false, false, false]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_configurations_and_frames_are_errors() {
|
||||
let new = |w, h| Encoder::new(EncoderConfig::new(w, h, 25, 1)).err();
|
||||
assert_eq!(new(0, 16), Some(Error::InvalidDimensions(0, 16)));
|
||||
assert_eq!(new(17, 16), Some(Error::InvalidDimensions(17, 16)));
|
||||
assert_eq!(new(8192, 16), Some(Error::InvalidDimensions(8192, 16)));
|
||||
assert!(matches!(Encoder::new(EncoderConfig::new(16, 16, 0, 1)), Err(Error::InvalidFrameRate(0, 1))));
|
||||
assert!(matches!(Encoder::new(EncoderConfig::new(16, 16, 70000, 1)), Err(Error::InvalidFrameRate(..))));
|
||||
assert!(Encoder::new(EncoderConfig::new(16, 16, 140000, 2)).is_err());
|
||||
let mut cfg = EncoderConfig::new(16, 16, 25, 1);
|
||||
cfg.rate = RateControl::Quant(0);
|
||||
assert!(matches!(Encoder::new(cfg.clone()), Err(Error::InvalidConfig(_))));
|
||||
cfg.rate = RateControl::Quant(32);
|
||||
assert!(matches!(Encoder::new(cfg.clone()), Err(Error::InvalidConfig(_))));
|
||||
cfg.rate = RateControl::Quant(4);
|
||||
cfg.keyint = 0;
|
||||
assert!(matches!(Encoder::new(cfg), Err(Error::InvalidConfig(_))));
|
||||
|
||||
let mut enc = Encoder::new(EncoderConfig::new(32, 16, 25, 1)).expect("encoder");
|
||||
let y = vec![0u8; 32 * 16];
|
||||
let c = vec![0u8; 16 * 8];
|
||||
assert_eq!(enc.encode(&YuvFrame { y: &y[1..], u: &c, v: &c, y_stride: 32, uv_stride: 16 }).err(), Some(Error::BadFrame));
|
||||
assert_eq!(enc.encode(&YuvFrame { y: &y, u: &c, v: &c, y_stride: 31, uv_stride: 16 }).err(), Some(Error::BadFrame));
|
||||
assert_eq!(enc.encode(&YuvFrame { y: &y, u: &c[..100], v: &c, y_stride: 32, uv_stride: 16 }).err(), Some(Error::BadFrame));
|
||||
assert_eq!(enc.encode(&YuvFrame { y: &y, u: &c, v: &c, y_stride: usize::MAX, uv_stride: 16 }).err(), Some(Error::BadFrame));
|
||||
// A failed frame leaves the encoder usable.
|
||||
assert!(enc.encode(&YuvFrame { y: &y, u: &c, v: &c, y_stride: 32, uv_stride: 16 }).is_ok());
|
||||
}
|
||||
|
||||
/// Extreme pictures (flat black and white, full-range noise, hard alternation between them) at
|
||||
/// the finest quantiser hit the largest levels and DC differences; ffmpeg still decodes them.
|
||||
#[test]
|
||||
fn extreme_content_at_the_finest_quantiser() {
|
||||
let (w, h) = (48, 32);
|
||||
let mut cfg = EncoderConfig::new(w as u32, h as u32, 25, 1);
|
||||
cfg.rate = RateControl::Quant(1);
|
||||
let mut enc = Encoder::new(cfg).expect("encoder");
|
||||
let mut packets = Vec::new();
|
||||
let mut recon = Vec::new();
|
||||
let mut seed = 1u32;
|
||||
for t in 0..8 {
|
||||
let y: Vec<u8> = (0..w * h)
|
||||
.map(|i| match t % 4 {
|
||||
0 => 0,
|
||||
1 => 255,
|
||||
2 => {
|
||||
seed = hash(seed.wrapping_add(i as u32));
|
||||
seed as u8
|
||||
}
|
||||
_ => {
|
||||
if (i % w + i / w) % 2 == 0 {
|
||||
0
|
||||
} else {
|
||||
255
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let c = vec![if t % 2 == 0 { 0 } else { 255 }; w * h / 4];
|
||||
packets.push(enc.encode(&YuvFrame { y: &y, u: &c, v: &c, y_stride: w, uv_stride: w / 2 }).expect("encode"));
|
||||
let r = enc.reconstruction().expect("reconstruction");
|
||||
recon.push([r.y, r.u, r.v].concat());
|
||||
}
|
||||
let Some(ff) = ffmpeg() else { return };
|
||||
let path = out_dir("extreme").join("stream.m4v");
|
||||
write_stream(&path, &packets);
|
||||
let (diag, dec) = ffmpeg_decode(&ff, &path);
|
||||
assert!(diag.trim().is_empty(), "{diag}");
|
||||
for (i, frame) in dec.chunks(w * h * 3 / 2).enumerate() {
|
||||
let d = psnr(&recon[i], frame);
|
||||
assert!(d >= 45.0, "frame {i}: {d:.1} dB from the reconstruction");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user