1use crate::io::ByteSource;
8use crate::{
9 CancellationToken, CodecSupport, CpuFrameSource, DecodeStatistics, EncodedSample,
10 EncodedVideoSample, Error, ErrorKind, ExactFrameReader, FrameIndex, FrameSource, Limits,
11 Mp4Demuxer, Mp4DemuxerOptions, Orientation, PixelFormat, Result, VideoDecoderConfig,
12 VideoDecoderFactory, VideoEncoderConfig, VideoEncoderFactory, VideoFrame,
13};
14use std::collections::{HashMap, HashSet};
15
16#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
18pub struct FrameDigest(pub [u8; 32]);
19
20impl FrameDigest {
21 pub fn from_frame(frame: &VideoFrame) -> Result<Self> {
24 let mut bytes = Vec::new();
25 bytes.extend_from_slice(&frame.dimensions.width.to_be_bytes());
26 bytes.extend_from_slice(&frame.dimensions.height.to_be_bytes());
27 bytes.push(pixel_format_tag(frame.pixel_format));
28 bytes.push(match frame.color_range {
29 crate::ColorRange::Full => 0,
30 crate::ColorRange::Limited => 1,
31 _ => u8::MAX,
32 });
33 bytes.push(u8::try_from(frame.planes.len()).map_err(|_| {
34 Error::new(ErrorKind::ResourceLimit, "video frame has too many planes")
35 })?);
36 append_active_pixels(frame, &mut bytes)?;
37 Ok(Self(sha256(&bytes)))
38 }
39
40 pub fn to_hex(self) -> String {
41 const HEX: &[u8; 16] = b"0123456789abcdef";
42 let mut output = String::with_capacity(64);
43 for byte in self.0 {
44 output.push(char::from(HEX[usize::from(byte >> 4)]));
45 output.push(char::from(HEX[usize::from(byte & 0x0f)]));
46 }
47 output
48 }
49
50 pub fn from_hex(value: &str) -> Result<Self> {
51 if value.len() != 64 || !value.is_ascii() {
52 return Err(invalid(
53 "frame fingerprints must contain 64 hexadecimal digits",
54 ));
55 }
56 let mut digest = [0_u8; 32];
57 for (index, byte) in digest.iter_mut().enumerate() {
58 let start = index * 2;
59 *byte = u8::from_str_radix(&value[start..start + 2], 16)
60 .map_err(|_| invalid("frame fingerprints must contain hexadecimal digits"))?;
61 }
62 Ok(Self(digest))
63 }
64}
65
66#[derive(Clone, Copy, Debug, Eq, PartialEq)]
68pub struct ExpectedVideoFrame {
69 pub presentation_index: FrameIndex,
70 pub digest: FrameDigest,
71}
72
73#[derive(Clone, Debug, Eq, PartialEq)]
75pub struct VideoDecoderConformanceVector {
76 pub name: String,
77 pub configuration: VideoDecoderConfig,
78 pub samples: Vec<EncodedVideoSample>,
79 pub expected_frames: Vec<ExpectedVideoFrame>,
80}
81
82impl VideoDecoderConformanceVector {
83 pub async fn from_mp4<S: ByteSource + ?Sized>(
87 name: impl Into<String>,
88 source: &S,
89 options: Mp4DemuxerOptions,
90 track_id: u32,
91 mut configuration: VideoDecoderConfig,
92 expected_digests: &[FrameDigest],
93 ) -> Result<Self> {
94 let limits = options.limits;
95 let movie = Mp4Demuxer::open(source, options).await?;
96 let track = movie.track(track_id).ok_or_else(|| {
97 Error::new(
98 ErrorKind::InvalidInput,
99 "codec vector MP4 track does not exist",
100 )
101 })?;
102 if track.codec != configuration.codec
103 || track.dimensions != Some(configuration.coded_dimensions)
104 {
105 return Err(invalid(
106 "codec vector MP4 track does not match its decoder configuration",
107 ));
108 }
109 let samples = track.to_encoded_video_samples(source, &limits).await?;
110 if samples.len() != expected_digests.len() {
111 return Err(invalid(
112 "codec vector MP4 requires one fingerprint per presentation frame",
113 ));
114 }
115 configuration.configuration = track.decoder_config.clone();
116 let expected_frames = expected_digests
117 .iter()
118 .copied()
119 .enumerate()
120 .map(|(index, digest)| ExpectedVideoFrame {
121 presentation_index: FrameIndex(index as u64),
122 digest,
123 })
124 .collect();
125 Ok(Self {
126 name: name.into(),
127 configuration,
128 samples,
129 expected_frames,
130 })
131 }
132}
133
134#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
136pub struct VideoDecoderConformanceReport {
137 pub frames_verified: u64,
138 pub access_patterns_verified: u8,
139 pub statistics: DecodeStatistics,
140}
141
142pub fn verify_video_decoder_conformance(
144 factory: &dyn VideoDecoderFactory,
145 vector: &VideoDecoderConformanceVector,
146 limits: Limits,
147) -> Result<VideoDecoderConformanceReport> {
148 validate_decoder_vector(vector)?;
149 let mut sequential: Vec<_> = vector
150 .expected_frames
151 .iter()
152 .map(|frame| frame.presentation_index)
153 .collect();
154 sequential.sort_by_key(|index| index.0);
155 let mut reverse = sequential.clone();
156 reverse.reverse();
157 let mut seek_heavy = Vec::with_capacity(sequential.len());
158 let (mut low, mut high) = (0_usize, sequential.len());
159 while low < high {
160 high -= 1;
161 seek_heavy.push(sequential[high]);
162 if low < high {
163 seek_heavy.push(sequential[low]);
164 low += 1;
165 }
166 }
167
168 let expected: HashMap<_, _> = vector
169 .expected_frames
170 .iter()
171 .map(|frame| (frame.presentation_index, frame.digest))
172 .collect();
173 let mut report = VideoDecoderConformanceReport::default();
174 for pattern in [&sequential, &reverse, &seek_heavy] {
175 let mut reader = ExactFrameReader::new(
176 factory,
177 vector.configuration.clone(),
178 vector.samples.clone(),
179 limits,
180 )?;
181 let cancellation = CancellationToken::new();
182 for &index in pattern {
183 let frame = reader.get(index, &cancellation)?;
184 let actual = FrameDigest::from_frame(&frame)?;
185 let wanted = expected[&index];
186 if actual != wanted {
187 return Err(Error::new(
188 ErrorKind::Codec,
189 format!(
190 "codec vector '{}' frame {} fingerprint mismatch: expected {}, got {}",
191 vector.name,
192 index.0,
193 wanted.to_hex(),
194 actual.to_hex()
195 ),
196 ));
197 }
198 report.frames_verified = report.frames_verified.saturating_add(1);
199 }
200 report.statistics = add_statistics(report.statistics, reader.statistics());
201 report.access_patterns_verified = report.access_patterns_verified.saturating_add(1);
202 }
203 Ok(report)
204}
205
206#[derive(Clone, Debug, PartialEq)]
208pub struct VideoEncoderConformanceVector {
209 pub name: String,
210 pub configuration: VideoEncoderConfig,
211 pub decoder_configuration: VideoDecoderConfig,
214 pub frames: Vec<VideoFrame>,
215 pub minimum_psnr_db: f64,
216}
217
218#[derive(Clone, Copy, Debug, Default, PartialEq)]
220pub struct VideoEncoderConformanceReport {
221 pub frames_encoded: u64,
222 pub packets_emitted: u64,
223 pub minimum_observed_psnr_db: f64,
224}
225
226pub async fn verify_video_encoder_conformance(
229 encoder_factory: &dyn VideoEncoderFactory,
230 decoder_factory: &dyn VideoDecoderFactory,
231 vector: &VideoEncoderConformanceVector,
232 limits: Limits,
233) -> Result<VideoEncoderConformanceReport> {
234 validate_encoder_vector(vector)?;
235 require_supported(
236 encoder_factory.capability(&vector.configuration),
237 "encoder does not support the conformance vector",
238 )?;
239 let mut encoder = encoder_factory.create(&vector.configuration, &limits)?;
240 let format = encoder.format();
241 if format.dimensions != vector.configuration.coded_dimensions
242 || format.pixel_format != vector.configuration.input_format
243 {
244 return Err(invalid(
245 "encoder format does not match its conformance configuration",
246 ));
247 }
248
249 let mut packets = Vec::new();
250 for (index, frame) in vector.frames.iter().enumerate() {
251 let index =
252 FrameIndex(u64::try_from(index).map_err(|_| {
253 Error::new(ErrorKind::ResourceLimit, "encoder frame index overflow")
254 })?);
255 packets.extend(
256 encoder
257 .encode(
258 index,
259 FrameSource::Cpu(CpuFrameSource {
260 frame,
261 orientation: Orientation::TopLeft,
262 }),
263 )
264 .await?,
265 );
266 }
267 packets.extend(encoder.finish().await?);
268 let encoder_config = encoder.config().clone();
269 validate_encoded_packets(&packets, vector.frames.len(), &encoder_config, vector)?;
270 let packet_count = packets.len();
271
272 let mut decoder_configuration = vector.decoder_configuration.clone();
273 decoder_configuration.configuration = encoder_config.decoder_config;
274 let samples = packets_to_video_samples(packets)?;
275 let mut reader =
276 ExactFrameReader::new(decoder_factory, decoder_configuration, samples, limits)?;
277 let cancellation = CancellationToken::new();
278 let mut minimum_observed = f64::INFINITY;
279 for (index, source) in vector.frames.iter().enumerate() {
280 let index = FrameIndex(index as u64);
281 let decoded = reader.get(index, &cancellation)?;
282 let psnr = frame_psnr(source, &decoded)?;
283 minimum_observed = minimum_observed.min(psnr);
284 if psnr < vector.minimum_psnr_db {
285 return Err(Error::new(
286 ErrorKind::Codec,
287 format!(
288 "codec vector '{}' frame {} PSNR {psnr:.3} dB is below {:.3} dB",
289 vector.name, index.0, vector.minimum_psnr_db
290 ),
291 ));
292 }
293 }
294 Ok(VideoEncoderConformanceReport {
295 frames_encoded: vector.frames.len() as u64,
296 packets_emitted: packet_count as u64,
297 minimum_observed_psnr_db: minimum_observed,
298 })
299}
300
301fn validate_decoder_vector(vector: &VideoDecoderConformanceVector) -> Result<()> {
302 if vector.name.trim().is_empty() || vector.samples.is_empty() {
303 return Err(invalid(
304 "decoder conformance vectors require a name and samples",
305 ));
306 }
307 if vector.expected_frames.len() != vector.samples.len() {
308 return Err(invalid(
309 "decoder conformance vectors require one expected frame per sample",
310 ));
311 }
312 let sample_indexes: HashSet<_> = vector
313 .samples
314 .iter()
315 .map(|sample| sample.presentation_index)
316 .collect();
317 let expected_indexes: HashSet<_> = vector
318 .expected_frames
319 .iter()
320 .map(|frame| frame.presentation_index)
321 .collect();
322 if sample_indexes.len() != vector.samples.len()
323 || expected_indexes.len() != vector.expected_frames.len()
324 || sample_indexes != expected_indexes
325 {
326 return Err(invalid(
327 "decoder conformance frame identities must be unique and complete",
328 ));
329 }
330 Ok(())
331}
332
333fn validate_encoder_vector(vector: &VideoEncoderConformanceVector) -> Result<()> {
334 if vector.name.trim().is_empty() || vector.frames.is_empty() {
335 return Err(invalid(
336 "encoder conformance vectors require a name and frames",
337 ));
338 }
339 if !vector.minimum_psnr_db.is_finite() || vector.minimum_psnr_db < 0.0 {
340 return Err(invalid(
341 "encoder conformance PSNR must be finite and nonnegative",
342 ));
343 }
344 if vector.configuration.timescale == 0 || vector.configuration.frame_duration == 0 {
345 return Err(invalid(
346 "encoder conformance requires a nonzero timescale and frame duration",
347 ));
348 }
349 if vector.decoder_configuration.codec != vector.configuration.codec
350 || vector.decoder_configuration.coded_dimensions != vector.configuration.coded_dimensions
351 || vector.decoder_configuration.output_format != vector.configuration.input_format
352 || vector.decoder_configuration.color_range != vector.configuration.color_range
353 {
354 return Err(invalid(
355 "encoder and decoder conformance configurations must describe the same codec and frame format",
356 ));
357 }
358 for frame in &vector.frames {
359 if frame.dimensions != vector.configuration.coded_dimensions
360 || frame.pixel_format != vector.configuration.input_format
361 || frame.color_range != vector.configuration.color_range
362 {
363 return Err(invalid("encoder conformance source frame format changed"));
364 }
365 }
366 Ok(())
367}
368
369fn require_supported(support: CodecSupport, message: &str) -> Result<()> {
370 if support.is_supported() {
371 Ok(())
372 } else {
373 Err(Error::new(
374 ErrorKind::Unsupported,
375 format!("{message}: {support:?}"),
376 ))
377 }
378}
379
380fn validate_encoded_packets(
381 packets: &[EncodedSample],
382 frame_count: usize,
383 configuration: &crate::EncoderConfig,
384 vector: &VideoEncoderConformanceVector,
385) -> Result<()> {
386 if configuration.codec != vector.configuration.codec
387 || configuration.timescale != vector.configuration.timescale
388 || configuration.timescale == 0
389 {
390 return Err(Error::new(
391 ErrorKind::Codec,
392 "encoder emitted invalid codec configuration",
393 ));
394 }
395 if packets.len() != frame_count {
396 return Err(Error::new(
397 ErrorKind::Codec,
398 "encoder must emit exactly one video access unit per conformance frame",
399 ));
400 }
401 if packets.first().is_none_or(|packet| !packet.is_sync) {
402 return Err(Error::new(
403 ErrorKind::Codec,
404 "encoder conformance output must begin with a random-access sample",
405 ));
406 }
407 let mut pts = HashSet::new();
408 for (decode_index, packet) in packets.iter().enumerate() {
409 let expected_dts = i64::try_from(decode_index)
410 .ok()
411 .and_then(|value| value.checked_mul(i64::from(vector.configuration.frame_duration)))
412 .ok_or_else(|| Error::new(ErrorKind::ResourceLimit, "encoder DTS overflow"))?;
413 if packet.data.is_empty()
414 || packet.duration != vector.configuration.frame_duration
415 || packet.dts != expected_dts
416 || !pts.insert(packet.pts)
417 {
418 return Err(Error::new(
419 ErrorKind::Codec,
420 "encoder emitted invalid data, duration, DTS, or duplicate PTS",
421 ));
422 }
423 let duration = i64::from(vector.configuration.frame_duration);
424 if packet.pts < 0 || packet.pts % duration != 0 {
425 return Err(Error::new(
426 ErrorKind::Codec,
427 "encoder sample PTS is outside the configured frame clock",
428 ));
429 }
430 }
431 for presentation_index in 0..frame_count {
432 let expected_pts = i64::try_from(presentation_index)
433 .ok()
434 .and_then(|value| value.checked_mul(i64::from(vector.configuration.frame_duration)))
435 .ok_or_else(|| Error::new(ErrorKind::ResourceLimit, "encoder PTS overflow"))?;
436 if !pts.contains(&expected_pts) {
437 return Err(Error::new(
438 ErrorKind::Codec,
439 "encoder sample PTS values do not cover the input timeline",
440 ));
441 }
442 }
443 Ok(())
444}
445
446fn packets_to_video_samples(packets: Vec<EncodedSample>) -> Result<Vec<EncodedVideoSample>> {
447 let mut presentation = (0..packets.len()).collect::<Vec<_>>();
448 presentation.sort_by_key(|&index| (packets[index].pts, packets[index].dts, index));
449 let mut indexes = vec![FrameIndex(0); packets.len()];
450 for (presentation_index, decode_index) in presentation.into_iter().enumerate() {
451 indexes[decode_index] =
452 FrameIndex(u64::try_from(presentation_index).map_err(|_| {
453 Error::new(ErrorKind::ResourceLimit, "presentation index overflow")
454 })?);
455 }
456 Ok(packets
457 .into_iter()
458 .enumerate()
459 .map(|(decode_index, packet)| EncodedVideoSample {
460 presentation_index: indexes[decode_index],
461 random_access: packet.is_sync,
462 data: packet.data,
463 })
464 .collect())
465}
466
467fn frame_psnr(reference: &VideoFrame, candidate: &VideoFrame) -> Result<f64> {
468 if reference.dimensions != candidate.dimensions
469 || reference.pixel_format != candidate.pixel_format
470 || reference.color_range != candidate.color_range
471 {
472 return Err(Error::new(
473 ErrorKind::Codec,
474 "round-trip decoder changed frame metadata",
475 ));
476 }
477 let mut a = Vec::new();
478 let mut b = Vec::new();
479 append_active_pixels(reference, &mut a)?;
480 append_active_pixels(candidate, &mut b)?;
481 if a.len() != b.len() || a.is_empty() {
482 return Err(Error::new(
483 ErrorKind::Codec,
484 "round-trip frame layout changed",
485 ));
486 }
487 let squared_error: f64 = a
488 .iter()
489 .zip(&b)
490 .map(|(&left, &right)| {
491 let difference = f64::from(left) - f64::from(right);
492 difference * difference
493 })
494 .sum();
495 if squared_error == 0.0 {
496 return Ok(f64::INFINITY);
497 }
498 let mse = squared_error / a.len() as f64;
499 Ok(10.0 * ((255.0 * 255.0) / mse).log10())
500}
501
502fn append_active_pixels(frame: &VideoFrame, output: &mut Vec<u8>) -> Result<()> {
503 let layouts = plane_layouts(frame)?;
504 for (plane, (row_bytes, rows)) in frame.planes.iter().zip(layouts) {
505 for row in 0..rows {
506 let start = row.checked_mul(plane.stride).ok_or_else(|| {
507 Error::new(ErrorKind::ResourceLimit, "video plane offset overflow")
508 })?;
509 let end = start
510 .checked_add(row_bytes)
511 .ok_or_else(|| Error::new(ErrorKind::ResourceLimit, "video plane row overflow"))?;
512 let bytes = plane.data.get(start..end).ok_or_else(|| {
513 Error::new(
514 ErrorKind::InvalidInput,
515 "video plane is shorter than its active rows",
516 )
517 })?;
518 output.extend_from_slice(bytes);
519 }
520 }
521 Ok(())
522}
523
524fn plane_layouts(frame: &VideoFrame) -> Result<Vec<(usize, usize)>> {
525 let width = usize::try_from(frame.dimensions.width)
526 .map_err(|_| Error::new(ErrorKind::ResourceLimit, "video width is too large"))?;
527 let height = usize::try_from(frame.dimensions.height)
528 .map_err(|_| Error::new(ErrorKind::ResourceLimit, "video height is too large"))?;
529 let packed = |components: usize| {
530 width
531 .checked_mul(components)
532 .map(|row_bytes| vec![(row_bytes, height)])
533 };
534 let layouts = match frame.pixel_format {
535 PixelFormat::Rgba8 | PixelFormat::Bgra8 => packed(4),
536 PixelFormat::Rgb8 => packed(3),
537 PixelFormat::Gray8 => packed(1),
538 PixelFormat::Yuv420p8 => {
539 let chroma_width = width.div_ceil(2);
540 let chroma_height = height.div_ceil(2);
541 Some(vec![
542 (width, height),
543 (chroma_width, chroma_height),
544 (chroma_width, chroma_height),
545 ])
546 }
547 _ => {
548 return Err(Error::new(
549 ErrorKind::Unsupported,
550 "unsupported pixel format",
551 ));
552 }
553 }
554 .ok_or_else(|| Error::new(ErrorKind::ResourceLimit, "video row size overflow"))?;
555 if layouts.len() != frame.planes.len() {
556 return Err(invalid("video plane count does not match its pixel format"));
557 }
558 Ok(layouts)
559}
560
561fn pixel_format_tag(format: PixelFormat) -> u8 {
562 match format {
563 PixelFormat::Rgba8 => 0,
564 PixelFormat::Bgra8 => 1,
565 PixelFormat::Rgb8 => 2,
566 PixelFormat::Gray8 => 3,
567 PixelFormat::Yuv420p8 => 4,
568 _ => u8::MAX,
569 }
570}
571
572fn add_statistics(left: DecodeStatistics, right: DecodeStatistics) -> DecodeStatistics {
573 DecodeStatistics {
574 samples_submitted: left
575 .samples_submitted
576 .saturating_add(right.samples_submitted),
577 samples_skipped: left.samples_skipped.saturating_add(right.samples_skipped),
578 cache_hits: left.cache_hits.saturating_add(right.cache_hits),
579 resets: left.resets.saturating_add(right.resets),
580 drains: left.drains.saturating_add(right.drains),
581 }
582}
583
584fn invalid(message: &str) -> Error {
585 Error::new(ErrorKind::InvalidInput, message)
586}
587
588#[doc(hidden)]
589pub fn sha256(input: &[u8]) -> [u8; 32] {
590 const INITIAL: [u32; 8] = [
591 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab,
592 0x5be0cd19,
593 ];
594 const K: [u32; 64] = [
595 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4,
596 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe,
597 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f,
598 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
599 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc,
600 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
601 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116,
602 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
603 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
604 0xc67178f2,
605 ];
606 let bit_length = (input.len() as u64).wrapping_mul(8);
607 let mut padded = input.to_vec();
608 padded.push(0x80);
609 while padded.len() % 64 != 56 {
610 padded.push(0);
611 }
612 padded.extend_from_slice(&bit_length.to_be_bytes());
613 let mut state = INITIAL;
614 for chunk in padded.chunks_exact(64) {
615 let mut words = [0_u32; 64];
616 for (index, word) in words[..16].iter_mut().enumerate() {
617 *word = u32::from_be_bytes(chunk[index * 4..index * 4 + 4].try_into().unwrap());
618 }
619 for index in 16..64 {
620 let s0 = words[index - 15].rotate_right(7)
621 ^ words[index - 15].rotate_right(18)
622 ^ (words[index - 15] >> 3);
623 let s1 = words[index - 2].rotate_right(17)
624 ^ words[index - 2].rotate_right(19)
625 ^ (words[index - 2] >> 10);
626 words[index] = words[index - 16]
627 .wrapping_add(s0)
628 .wrapping_add(words[index - 7])
629 .wrapping_add(s1);
630 }
631 let [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut h] = state;
632 for index in 0..64 {
633 let upper = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
634 let choice = (e & f) ^ ((!e) & g);
635 let first = h
636 .wrapping_add(upper)
637 .wrapping_add(choice)
638 .wrapping_add(K[index])
639 .wrapping_add(words[index]);
640 let lower = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
641 let majority = (a & b) ^ (a & c) ^ (b & c);
642 let second = lower.wrapping_add(majority);
643 h = g;
644 g = f;
645 f = e;
646 e = d.wrapping_add(first);
647 d = c;
648 c = b;
649 b = a;
650 a = first.wrapping_add(second);
651 }
652 for (target, value) in state.iter_mut().zip([a, b, c, d, e, f, g, h]) {
653 *target = target.wrapping_add(value);
654 }
655 }
656 let mut output = [0_u8; 32];
657 for (chunk, value) in output.chunks_exact_mut(4).zip(state) {
658 chunk.copy_from_slice(&value.to_be_bytes());
659 }
660 output
661}
662
663#[cfg(test)]
664mod tests {
665 use super::*;
666 use crate::{
667 Codec, CodecImplementation, CodecProfile, ColorRange, HardwarePreference, Plane,
668 SampleDependency, VideoDimensions, VideoEncoder, VideoEncoderFormat,
669 uncompressed_video_decoder_factory,
670 };
671 use std::future::Future;
672 use std::pin::Pin;
673 use std::task::{Context, Poll, Waker};
674
675 fn block_on<T>(future: impl Future<Output = T>) -> T {
676 let waker = Waker::noop();
677 let mut context = Context::from_waker(waker);
678 let mut future = Box::pin(future);
679 loop {
680 match Pin::new(&mut future).poll(&mut context) {
681 Poll::Ready(value) => return value,
682 Poll::Pending => std::thread::yield_now(),
683 }
684 }
685 }
686
687 fn config() -> VideoDecoderConfig {
688 VideoDecoderConfig {
689 codec: Codec::UncompressedVideo,
690 profile: CodecProfile::UncompressedGray8,
691 coded_dimensions: VideoDimensions::new(2, 1, &Limits::default()).unwrap(),
692 output_format: PixelFormat::Gray8,
693 color_range: ColorRange::Full,
694 hardware: HardwarePreference::Avoid,
695 configuration: Vec::new(),
696 }
697 }
698
699 fn frame(value: u8) -> VideoFrame {
700 VideoFrame::new(
701 config().coded_dimensions,
702 PixelFormat::Gray8,
703 ColorRange::Full,
704 vec![Plane {
705 data: vec![value, value + 1],
706 stride: 2,
707 }],
708 &Limits::default(),
709 )
710 .unwrap()
711 }
712
713 fn decoder_vector() -> VideoDecoderConformanceVector {
714 let values = [10_u8, 30, 20];
715 VideoDecoderConformanceVector {
716 name: "uncompressed reorder".into(),
717 configuration: config(),
718 samples: values
719 .iter()
720 .enumerate()
721 .map(|(decode_index, &value)| EncodedVideoSample {
722 presentation_index: FrameIndex([0, 2, 1][decode_index]),
723 random_access: decode_index == 0,
724 data: vec![value, value + 1],
725 })
726 .collect(),
727 expected_frames: [10_u8, 20, 30]
728 .iter()
729 .enumerate()
730 .map(|(index, &value)| ExpectedVideoFrame {
731 presentation_index: FrameIndex(index as u64),
732 digest: FrameDigest::from_frame(&frame(value)).unwrap(),
733 })
734 .collect(),
735 }
736 }
737
738 #[test]
739 fn sha256_matches_published_empty_string_vector() {
740 assert_eq!(
741 FrameDigest(sha256(b"")).to_hex(),
742 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
743 );
744 }
745
746 #[test]
747 fn frame_digest_hex_round_trips_and_rejects_bad_input() {
748 let digest = FrameDigest::from_frame(&frame(11)).unwrap();
749 assert_eq!(FrameDigest::from_hex(&digest.to_hex()).unwrap(), digest);
750 assert_eq!(
751 FrameDigest::from_hex("xyz").unwrap_err().kind(),
752 ErrorKind::InvalidInput
753 );
754 }
755
756 #[test]
757 fn frame_digest_ignores_stride_padding() {
758 let mut padded = frame(7);
759 padded.planes[0] = Plane {
760 data: vec![7, 8, 99, 99],
761 stride: 4,
762 };
763 assert_eq!(
764 FrameDigest::from_frame(&frame(7)).unwrap(),
765 FrameDigest::from_frame(&padded).unwrap()
766 );
767 }
768
769 #[test]
770 fn decoder_runner_checks_three_access_patterns() {
771 let report = verify_video_decoder_conformance(
772 &uncompressed_video_decoder_factory(),
773 &decoder_vector(),
774 Limits::default(),
775 )
776 .unwrap();
777 assert_eq!(report.frames_verified, 9);
778 assert_eq!(report.access_patterns_verified, 3);
779 assert!(report.statistics.resets > 0);
780 }
781
782 struct PassThroughEncoder {
783 config: crate::EncoderConfig,
784 format: VideoEncoderFormat,
785 frame_duration: u32,
786 }
787
788 impl VideoEncoder for PassThroughEncoder {
789 fn config(&self) -> &crate::EncoderConfig {
790 &self.config
791 }
792
793 fn format(&self) -> VideoEncoderFormat {
794 self.format
795 }
796
797 fn encode<'a>(
798 &'a mut self,
799 index: FrameIndex,
800 source: FrameSource<'a>,
801 ) -> crate::EncoderFuture<'a, Vec<EncodedSample>> {
802 Box::pin(async move {
803 let FrameSource::Cpu(source) = source else {
804 return Err(invalid("fixture requires CPU frames"));
805 };
806 Ok(vec![EncodedSample {
807 data: source.frame.planes[0].data.clone(),
808 dts: i64::try_from(index.0)
809 .map_err(|_| invalid("fixture timestamp overflow"))?
810 * i64::from(self.frame_duration),
811 pts: i64::try_from(index.0)
812 .map_err(|_| invalid("fixture timestamp overflow"))?
813 * i64::from(self.frame_duration),
814 duration: self.frame_duration,
815 is_sync: true,
816 dependency: SampleDependency::INDEPENDENT,
817 }])
818 })
819 }
820
821 fn finish<'a>(&'a mut self) -> crate::EncoderFuture<'a, Vec<EncodedSample>> {
822 Box::pin(async { Ok(Vec::new()) })
823 }
824 }
825
826 struct PassThroughEncoderFactory;
827
828 impl VideoEncoderFactory for PassThroughEncoderFactory {
829 fn capability(&self, configuration: &VideoEncoderConfig) -> CodecSupport {
830 if configuration.codec == Codec::UncompressedVideo {
831 CodecSupport::Supported {
832 implementation: CodecImplementation::Software,
833 }
834 } else {
835 CodecSupport::UnsupportedCodec
836 }
837 }
838
839 fn create(
840 &self,
841 configuration: &VideoEncoderConfig,
842 _limits: &Limits,
843 ) -> Result<Box<dyn VideoEncoder>> {
844 Ok(Box::new(PassThroughEncoder {
845 config: crate::EncoderConfig {
846 codec: Codec::UncompressedVideo,
847 timescale: configuration.timescale,
848 decoder_config: Vec::new(),
849 },
850 format: VideoEncoderFormat {
851 dimensions: configuration.coded_dimensions,
852 pixel_format: configuration.input_format,
853 },
854 frame_duration: configuration.frame_duration,
855 }))
856 }
857 }
858
859 #[test]
860 fn encoder_runner_validates_independent_round_trip() {
861 let decoder = config();
862 let vector = VideoEncoderConformanceVector {
863 name: "pass through".into(),
864 configuration: VideoEncoderConfig {
865 codec: Codec::UncompressedVideo,
866 profile: CodecProfile::UncompressedGray8,
867 coded_dimensions: decoder.coded_dimensions,
868 input_format: PixelFormat::Gray8,
869 color_range: ColorRange::Full,
870 hardware: HardwarePreference::Avoid,
871 timescale: 1,
872 frame_duration: 1,
873 configuration: Vec::new(),
874 },
875 decoder_configuration: decoder,
876 frames: vec![frame(1), frame(3)],
877 minimum_psnr_db: 60.0,
878 };
879 let report = block_on(verify_video_encoder_conformance(
880 &PassThroughEncoderFactory,
881 &uncompressed_video_decoder_factory(),
882 &vector,
883 Limits::default(),
884 ))
885 .unwrap();
886 assert_eq!(report.frames_encoded, 2);
887 assert!(report.minimum_observed_psnr_db.is_infinite());
888 }
889}