oximedia_codec/error_concealment.rs
1//! Error concealment for lost or corrupted video frames.
2//!
3//! When a video frame is missing due to packet loss, decoding error, or
4//! transmission failure, the decoder must produce a substitute frame to
5//! maintain temporal continuity. This module provides simple and effective
6//! concealment strategies based on reference frame data.
7//!
8//! ## Strategies
9//!
10//! - **Frame copy** (`conceal_missing_frame`): Copies the previous reference
11//! frame verbatim. This is the simplest and fastest strategy; effective
12//! when motion between frames is small.
13//! - **Temporal blend** (`conceal_blend`): Blends between the previous and a
14//! fallback (e.g. grey or next available) frame at a configurable ratio.
15//! - **Motion-compensated** (`conceal_motion_compensated`): Applies a simple
16//! global translation offset to the previous frame, useful when a dominant
17//! camera-pan motion is known.
18
19#![allow(dead_code)]
20#![allow(clippy::cast_precision_loss)]
21#![allow(clippy::cast_possible_truncation)]
22#![allow(clippy::cast_sign_loss)]
23
24// ---------------------------------------------------------------------------
25// Frame copy (primary API)
26// ---------------------------------------------------------------------------
27
28/// Conceal a missing frame by copying the previous reference frame.
29///
30/// This is the simplest error-concealment strategy: the decoder re-uses the
31/// most recently decoded frame as a substitute for the lost frame. For low-
32/// motion content the result is indistinguishable; for high-motion content
33/// it may produce a brief freeze.
34///
35/// # Parameters
36/// - `prev` – pixel buffer of the previous (reference) frame, interleaved
37/// YUV or RGB u8, length `3 * w * h`.
38/// - `w` – frame width in pixels.
39/// - `h` – frame height in pixels.
40///
41/// # Returns
42/// A `Vec<u8>` containing the concealed frame (identical to `prev`).
43///
44/// # Panics
45/// Panics if `prev.len() != 3 * w * h`.
46#[must_use]
47pub fn conceal_missing_frame(prev: &[u8], w: u32, h: u32) -> Vec<u8> {
48 let expected = 3 * (w as usize) * (h as usize);
49 assert_eq!(
50 prev.len(),
51 expected,
52 "conceal_missing_frame: prev length {len} != 3*w*h {expected}",
53 len = prev.len()
54 );
55 prev.to_vec()
56}
57
58// ---------------------------------------------------------------------------
59// Temporal blend
60// ---------------------------------------------------------------------------
61
62/// Conceal a missing frame by blending `prev` with a background colour.
63///
64/// When no future reference frame is available this replaces the missing
65/// frame with a fade toward `bg_color`:
66/// ```text
67/// out[i] = alpha * prev[i] + (1 - alpha) * bg_color
68/// ```
69///
70/// # Parameters
71/// - `prev` – previous frame pixel buffer, interleaved RGB u8, length
72/// `3 * w * h`.
73/// - `w` – frame width in pixels.
74/// - `h` – frame height in pixels.
75/// - `alpha` – blending weight for `prev` in [0.0, 1.0]. 1.0 = pure copy;
76/// 0.0 = pure `bg_color`.
77/// - `bg_color` – background RGB colour to blend toward, as `[R, G, B]`.
78///
79/// # Panics
80/// Panics if `prev.len() != 3 * w * h`.
81#[must_use]
82pub fn conceal_blend(prev: &[u8], w: u32, h: u32, alpha: f32, bg_color: [u8; 3]) -> Vec<u8> {
83 let expected = 3 * (w as usize) * (h as usize);
84 assert_eq!(
85 prev.len(),
86 expected,
87 "conceal_blend: prev length {len} != 3*w*h {expected}",
88 len = prev.len()
89 );
90
91 let alpha = alpha.clamp(0.0, 1.0);
92 let one_minus_alpha = 1.0 - alpha;
93
94 let mut out = Vec::with_capacity(expected);
95 let n_pixels = (w * h) as usize;
96
97 for i in 0..n_pixels {
98 for ch in 0..3usize {
99 let pv = prev[i * 3 + ch] as f32;
100 let bg = bg_color[ch] as f32;
101 let blended = alpha * pv + one_minus_alpha * bg;
102 out.push(blended.round().clamp(0.0, 255.0) as u8);
103 }
104 }
105
106 out
107}
108
109// ---------------------------------------------------------------------------
110// Motion-compensated concealment
111// ---------------------------------------------------------------------------
112
113/// Conceal a missing frame using a constant global motion offset.
114///
115/// Applies a pixel-level translation `(dx, dy)` to `prev` to approximate
116/// camera-pan motion. Pixels that are shifted out-of-bounds are filled from
117/// `prev` at the clamped border (clamp-to-edge).
118///
119/// # Parameters
120/// - `prev` – previous frame pixel buffer, interleaved RGB u8, length
121/// `3 * w * h`.
122/// - `w` – frame width in pixels.
123/// - `h` – frame height in pixels.
124/// - `dx` – horizontal translation (positive = shift right).
125/// - `dy` – vertical translation (positive = shift down).
126///
127/// # Panics
128/// Panics if `prev.len() != 3 * w * h`.
129#[must_use]
130pub fn conceal_motion_compensated(prev: &[u8], w: u32, h: u32, dx: i32, dy: i32) -> Vec<u8> {
131 let expected = 3 * (w as usize) * (h as usize);
132 assert_eq!(
133 prev.len(),
134 expected,
135 "conceal_motion_compensated: prev length {len} != 3*w*h {expected}",
136 len = prev.len()
137 );
138
139 let w_i = w as i32;
140 let h_i = h as i32;
141 let mut out = vec![0u8; expected];
142
143 for y in 0..h as i32 {
144 for x in 0..w_i {
145 // Source coordinate in previous frame
146 let src_x = (x - dx).clamp(0, w_i - 1) as usize;
147 let src_y = (y - dy).clamp(0, h_i - 1) as usize;
148
149 let src_idx = (src_y * w as usize + src_x) * 3;
150 let dst_idx = (y as usize * w as usize + x as usize) * 3;
151
152 out[dst_idx] = prev[src_idx];
153 out[dst_idx + 1] = prev[src_idx + 1];
154 out[dst_idx + 2] = prev[src_idx + 2];
155 }
156 }
157
158 out
159}
160
161// ---------------------------------------------------------------------------
162// Tests
163// ---------------------------------------------------------------------------
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 fn make_frame(w: u32, h: u32, fill: u8) -> Vec<u8> {
170 vec![fill; (3 * w * h) as usize]
171 }
172
173 #[test]
174 fn conceal_missing_frame_copies_prev() {
175 let prev = vec![42u8; 3 * 8 * 6];
176 let out = conceal_missing_frame(&prev, 8, 6);
177 assert_eq!(out, prev);
178 }
179
180 #[test]
181 fn conceal_missing_frame_correct_length() {
182 let prev = make_frame(16, 9, 100);
183 let out = conceal_missing_frame(&prev, 16, 9);
184 assert_eq!(out.len(), prev.len());
185 }
186
187 #[test]
188 #[should_panic(expected = "3*w*h")]
189 fn conceal_missing_frame_panics_on_wrong_size() {
190 let _ = conceal_missing_frame(&[0u8; 10], 4, 4);
191 }
192
193 #[test]
194 fn conceal_blend_alpha_one_returns_prev() {
195 let prev = vec![200u8; 3 * 4 * 4];
196 let out = conceal_blend(&prev, 4, 4, 1.0, [0, 0, 0]);
197 for (&p, &o) in prev.iter().zip(out.iter()) {
198 assert_eq!(p, o, "alpha=1.0 should return prev unchanged");
199 }
200 }
201
202 #[test]
203 fn conceal_blend_alpha_zero_returns_bg() {
204 let prev = vec![255u8; 3 * 4 * 4];
205 let bg = [128u8, 64, 32];
206 let out = conceal_blend(&prev, 4, 4, 0.0, bg);
207 for i in 0..(4 * 4) as usize {
208 assert_eq!(out[i * 3], bg[0]);
209 assert_eq!(out[i * 3 + 1], bg[1]);
210 assert_eq!(out[i * 3 + 2], bg[2]);
211 }
212 }
213
214 #[test]
215 fn conceal_blend_half_alpha_midpoint() {
216 let prev = vec![200u8; 3 * 2 * 2];
217 let out = conceal_blend(&prev, 2, 2, 0.5, [0, 0, 0]);
218 for &v in &out {
219 assert_eq!(v, 100, "half blend of 200 and 0 should be 100");
220 }
221 }
222
223 #[test]
224 fn conceal_blend_all_values_in_range() {
225 let prev: Vec<u8> = (0..3 * 8 * 8).map(|i| (i * 3 % 256) as u8).collect();
226 let out = conceal_blend(&prev, 8, 8, 0.7, [128, 128, 128]);
227 // All values are u8, so they are always in [0, 255].
228 assert!(!out.is_empty());
229 }
230
231 #[test]
232 fn conceal_motion_compensated_zero_offset_copies_prev() {
233 let prev: Vec<u8> = (0..3 * 8 * 8).map(|i| (i % 256) as u8).collect();
234 let out = conceal_motion_compensated(&prev, 8, 8, 0, 0);
235 assert_eq!(out, prev);
236 }
237
238 #[test]
239 fn conceal_motion_compensated_correct_length() {
240 let prev = make_frame(10, 10, 50);
241 let out = conceal_motion_compensated(&prev, 10, 10, 2, -1);
242 assert_eq!(out.len(), prev.len());
243 }
244
245 #[test]
246 fn conceal_motion_compensated_all_values_valid() {
247 let prev = make_frame(8, 6, 77);
248 let out = conceal_motion_compensated(&prev, 8, 6, 3, 2);
249 // All values are u8, so they are always in [0, 255].
250 assert!(!out.is_empty());
251 }
252
253 #[test]
254 fn conceal_motion_compensated_shift_right_fills_left_edge() {
255 // A frame with gradient 0..w-1 in the horizontal direction
256 let w = 8u32;
257 let h = 4u32;
258 let mut prev = vec![0u8; (3 * w * h) as usize];
259 for y in 0..h as usize {
260 for x in 0..w as usize {
261 let v = x as u8;
262 prev[(y * w as usize + x) * 3] = v;
263 prev[(y * w as usize + x) * 3 + 1] = v;
264 prev[(y * w as usize + x) * 3 + 2] = v;
265 }
266 }
267
268 let out = conceal_motion_compensated(&prev, w, h, 2, 0);
269
270 // After shifting right by 2, the leftmost 2 columns should be clamped
271 // to the source x=0 value (0)
272 assert_eq!(out[0], 0, "left-edge clamped pixel should be 0");
273 assert_eq!(out[3], 0, "second-pixel clamped should be 0");
274 }
275}