video_frame_hasher/
lib.rs

1// Copyright 2020 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! Hashes video frames.
6
7use async_trait::async_trait;
8use fidl_fuchsia_images2 as images2;
9use fidl_fuchsia_media::*;
10use fuchsia_image_format::images2_image_format_from_sysmem_image_format;
11use hex::encode;
12use mundane::hash::{Digest, Hasher, Sha256};
13use std::convert::*;
14use std::fmt;
15use stream_processor_test::{ExpectedDigest, FatalError, Output, OutputPacket, OutputValidator};
16use thiserror::Error;
17
18#[derive(Debug, Error)]
19pub enum Error {
20    DataTooSmallToBeFrame { expected_size: usize, actual_size: usize },
21    FormatDetailsNotUncompressedVideo,
22    UnsupportedPixelFormat(images2::PixelFormat),
23    FormatHasNoPlane { plane_requested: usize, planes_in_format: usize },
24}
25
26impl fmt::Display for Error {
27    fn fmt(&self, w: &mut fmt::Formatter<'_>) -> fmt::Result {
28        fmt::Debug::fmt(&self, w)
29    }
30}
31
32struct Frame<'a> {
33    data: &'a [u8],
34    format: images2::ImageFormat,
35}
36
37trait Subsample420 {
38    const CHROMA_SUBSAMPLE_RATIO: usize = 2;
39
40    fn frame_size(format: &images2::ImageFormat) -> usize {
41        // For 4:2:0 YUV, the UV data is 1/2 the size of the Y data,
42        // so the size of the frame is 3/2 the size of the Y plane.
43        *format.bytes_per_row.as_ref().unwrap() as usize
44            * format.size.as_ref().unwrap().height as usize
45            * 3
46            / 2
47    }
48}
49
50struct Yv12Frame<'a> {
51    frame: Frame<'a>,
52}
53
54impl<'a> Subsample420 for Yv12Frame<'a> {}
55
56impl<'a> Yv12Frame<'a> {
57    /// Returns an iterator over the display data in Yv12 format.
58    fn yv12_iter(&self) -> impl Iterator<Item = u8> + 'a {
59        let luminance_plane_stride = *self.frame.format.bytes_per_row.as_ref().unwrap() as usize;
60        let luminance_plane_display_height =
61            self.frame.format.display_rect.as_ref().unwrap().height as usize;
62        let luminance_plane_display_width =
63            self.frame.format.display_rect.as_ref().unwrap().width as usize;
64        let luminance = self
65            .frame
66            .data
67            .chunks(luminance_plane_stride)
68            .take(luminance_plane_display_height)
69            .flat_map(move |row| row.iter().take(luminance_plane_display_width));
70
71        let luminance_plane_coded_size =
72            luminance_plane_stride * self.frame.format.size.as_ref().unwrap().height as usize;
73        let chroma_plane_display_height =
74            luminance_plane_display_height / Self::CHROMA_SUBSAMPLE_RATIO;
75        let chroma_plane_display_width =
76            luminance_plane_display_width / Self::CHROMA_SUBSAMPLE_RATIO;
77
78        // V rows followed by U rows.
79        let chroma_rows = self.frame.data[luminance_plane_coded_size..]
80            .chunks(luminance_plane_stride / Self::CHROMA_SUBSAMPLE_RATIO);
81
82        let chroma_v = chroma_rows
83            .clone()
84            .take(chroma_plane_display_height)
85            .flat_map(move |row| row.iter().take(chroma_plane_display_width));
86
87        let chroma_plane_coded_height =
88            self.frame.format.size.as_ref().unwrap().height as usize / Self::CHROMA_SUBSAMPLE_RATIO;
89        let chroma_u = chroma_rows
90            .clone()
91            .skip(chroma_plane_coded_height)
92            .take(chroma_plane_display_height)
93            .flat_map(move |row| row.iter().take(chroma_plane_display_width));
94
95        luminance.chain(chroma_v).chain(chroma_u).cloned()
96    }
97}
98
99impl<'a> TryFrom<Frame<'a>> for Yv12Frame<'a> {
100    type Error = Error;
101    fn try_from(frame: Frame<'a>) -> Result<Self, Self::Error> {
102        let expected_size = Self::frame_size(&frame.format);
103        if frame.data.len() < expected_size {
104            return Err(Error::DataTooSmallToBeFrame {
105                actual_size: frame.data.len(),
106                expected_size,
107            });
108        }
109
110        Ok(Yv12Frame { frame })
111    }
112}
113
114struct Nv12Frame<'a> {
115    frame: Frame<'a>,
116}
117
118impl<'a> Subsample420 for Nv12Frame<'a> {}
119
120impl<'a> Nv12Frame<'a> {
121    /// Returns an iterator over the display data in Yv12 format.
122    fn yv12_iter(&self) -> impl Iterator<Item = u8> + 'a {
123        let rows =
124            self.frame.data.chunks(*self.frame.format.bytes_per_row.as_ref().unwrap() as usize);
125        let luminance_row_count = self.frame.format.display_rect.as_ref().unwrap().height as usize;
126        let chroma_row_count = luminance_row_count / Self::CHROMA_SUBSAMPLE_RATIO;
127        let row_length = self.frame.format.display_rect.as_ref().unwrap().width as usize;
128
129        let luminance =
130            rows.clone().take(luminance_row_count).flat_map(move |row| row.iter().take(row_length));
131        let chroma_rows = rows
132            .skip(self.frame.format.size.as_ref().unwrap().height as usize)
133            .take(chroma_row_count);
134        let chroma_u =
135            chroma_rows.clone().flat_map(move |row| row.iter().take(row_length).step_by(2));
136        let chroma_v =
137            chroma_rows.flat_map(move |row| row.iter().take(row_length).skip(1).step_by(2));
138
139        luminance.chain(chroma_v).chain(chroma_u).cloned()
140    }
141}
142
143impl<'a> TryFrom<Frame<'a>> for Nv12Frame<'a> {
144    type Error = Error;
145    fn try_from(frame: Frame<'a>) -> Result<Self, Self::Error> {
146        let expected_size = Self::frame_size(&frame.format);
147        if frame.data.len() < expected_size {
148            return Err(Error::DataTooSmallToBeFrame {
149                actual_size: frame.data.len(),
150                expected_size,
151            });
152        }
153
154        Ok(Nv12Frame { frame })
155    }
156}
157
158fn packet_display_data<'a>(
159    src: &'a OutputPacket,
160) -> Result<Box<dyn Iterator<Item = u8> + 'a>, Error> {
161    let sysmem_format = src
162        .format
163        .format_details
164        .domain
165        .as_ref()
166        .and_then(|domain| match domain {
167            DomainFormat::Video(VideoFormat::Uncompressed(uncompressed_format)) => {
168                Some(uncompressed_format.image_format)
169            }
170            _ => None,
171        })
172        .ok_or(Error::FormatDetailsNotUncompressedVideo)?;
173    let format = images2_image_format_from_sysmem_image_format(&sysmem_format).unwrap();
174
175    Ok(match format.pixel_format.as_ref().unwrap() {
176        images2::PixelFormat::Yv12 => {
177            Box::new(Yv12Frame::try_from(Frame { data: src.data.as_slice(), format })?.yv12_iter())
178        }
179        images2::PixelFormat::Nv12 => {
180            Box::new(Nv12Frame::try_from(Frame { data: src.data.as_slice(), format })?.yv12_iter())
181        }
182        _ => Err(Error::UnsupportedPixelFormat(*format.pixel_format.as_ref().unwrap()))?,
183    })
184}
185
186pub struct VideoFrameHasher {
187    pub expected_digest: ExpectedDigest,
188}
189
190#[async_trait(?Send)]
191impl OutputValidator for VideoFrameHasher {
192    async fn validate(&self, output: &[Output]) -> Result<(), anyhow::Error> {
193        let mut hasher = Sha256::default();
194        let mut frame_ordinal = 0;
195
196        if let Some(per_frame_bytes) = &self.expected_digest.per_frame_bytes {
197            let packet_count = output
198                .iter()
199                .filter(|item| {
200                    if let Output::Packet(ref _packet) = item {
201                        return true;
202                    }
203                    false
204                })
205                .count();
206            if per_frame_bytes.len() != packet_count {
207                return Err(FatalError(format!(
208                    "per_frame_bytes.len() != output.len() - {} vs {}",
209                    per_frame_bytes.len(),
210                    output.len()
211                ))
212                .into());
213            }
214        }
215
216        output
217            .iter()
218            .map(|output| {
219                if let Output::Packet(ref packet) = output {
220                    packet_display_data(packet)?.for_each(|b| hasher.update(&[b]));
221                    let tmp_hasher = hasher.clone();
222                    let frame_digest = tmp_hasher.finish().bytes();
223                    if let Some(per_frame_bytes) = &self.expected_digest.per_frame_bytes {
224                        if per_frame_bytes[frame_ordinal] != frame_digest {
225                            return Err(FatalError(format!(
226                                "frame_ordinal {} digest mismatch - expected {}; got {}",
227                                frame_ordinal,
228                                encode(per_frame_bytes[frame_ordinal]),
229                                encode(frame_digest)
230                            ))
231                            .into());
232                        }
233                    }
234
235                    frame_ordinal += 1;
236                }
237                Ok(())
238            })
239            .collect::<Result<(), anyhow::Error>>()?;
240
241        let digest = hasher.finish().bytes();
242        if self.expected_digest.bytes != digest {
243            return Err(FatalError(format!(
244                "Expected {}; got {}",
245                self.expected_digest,
246                encode(digest)
247            ))
248            .into());
249        }
250
251        Ok(())
252    }
253}
254
255#[cfg(test)]
256mod test {
257    use super::*;
258    use fidl_fuchsia_images2::{self as fimages2, *};
259    use fidl_fuchsia_math::{RectU, SizeU};
260    use fuchsia_image_format::sysmem1_image_format_from_images2_image_format;
261    use fuchsia_stream_processors::{ValidPacket, ValidPacketHeader};
262    use rand::prelude::*;
263    use std::rc::Rc;
264    use stream_processor_test::ValidStreamOutputFormat;
265
266    #[derive(Debug, Copy, Clone)]
267    struct TestSpec {
268        pixel_format: PixelFormat,
269        coded_width: usize, // coded_height() is in the impl
270        display_width: usize,
271        display_height: usize,
272        bytes_per_row: usize,
273    }
274
275    impl TestSpec {
276        fn coded_height(&self) -> usize {
277            // This just makes sure that coded_height() is > height.
278            self.display_height * 2
279        }
280    }
281
282    impl Into<OutputPacket> for TestSpec {
283        fn into(self) -> OutputPacket {
284            let mut format_details = FormatDetails::default();
285            format_details.domain = Some(DomainFormat::Video(VideoFormat::Uncompressed(
286                new_video_uncompressed_format(ImageFormat {
287                    pixel_format: Some(self.pixel_format),
288                    size: Some(SizeU {
289                        width: self.coded_width.try_into().unwrap(),
290                        height: self.coded_height().try_into().unwrap(),
291                    }),
292                    bytes_per_row: Some(self.bytes_per_row.try_into().unwrap()),
293                    display_rect: Some(RectU {
294                        x: 0,
295                        y: 0,
296                        width: self.display_width.try_into().unwrap(),
297                        height: self.display_height.try_into().unwrap(),
298                    }),
299                    color_space: Some(fimages2::ColorSpace::Rec709),
300                    ..Default::default()
301                }),
302            )));
303            OutputPacket {
304                data: vec![0; (self.bytes_per_row * self.coded_height() * 3 / 2) as usize],
305                format: Rc::new(ValidStreamOutputFormat {
306                    stream_lifetime_ordinal: 0,
307                    format_details,
308                }),
309                packet: ValidPacket {
310                    header: ValidPacketHeader { buffer_lifetime_ordinal: 0, packet_index: 0 },
311                    buffer_index: 0,
312                    stream_lifetime_ordinal: 0,
313                    start_offset: 0,
314                    valid_length_bytes: 0,
315                    timestamp_ish: None,
316                    start_access_unit: false,
317                    known_end_access_unit: false,
318                },
319            }
320        }
321    }
322
323    fn new_video_uncompressed_format(image_format: ImageFormat) -> VideoUncompressedFormat {
324        VideoUncompressedFormat {
325            image_format: sysmem1_image_format_from_images2_image_format(&image_format).unwrap(),
326            fourcc: 0,
327            primary_width_pixels: 0,
328            primary_height_pixels: 0,
329            secondary_width_pixels: 0,
330            secondary_height_pixels: 0,
331            planar: false,
332            swizzled: false,
333            primary_line_stride_bytes: 0,
334            secondary_line_stride_bytes: 0,
335            primary_start_offset: 0,
336            secondary_start_offset: 0,
337            tertiary_start_offset: 0,
338            primary_pixel_stride: 0,
339            secondary_pixel_stride: 0,
340            primary_display_width_pixels: 0,
341            primary_display_height_pixels: 0,
342            has_pixel_aspect_ratio: false,
343            pixel_aspect_ratio_width: 0,
344            pixel_aspect_ratio_height: 0,
345        }
346    }
347
348    fn specs(pixel_format: PixelFormat) -> impl Iterator<Item = TestSpec> {
349        vec![
350            TestSpec {
351                pixel_format,
352                coded_width: 16,
353                display_width: 10,
354                display_height: 16,
355                bytes_per_row: 20,
356            },
357            TestSpec {
358                pixel_format,
359                coded_width: 16,
360                display_width: 14,
361                display_height: 8,
362                bytes_per_row: 16,
363            },
364            TestSpec {
365                pixel_format,
366                coded_width: 32,
367                display_width: 12,
368                display_height: 4,
369                bytes_per_row: 54,
370            },
371            TestSpec {
372                pixel_format,
373                coded_width: 16,
374                display_width: 2,
375                display_height: 100,
376                bytes_per_row: 1200,
377            },
378        ]
379        .into_iter()
380    }
381
382    #[fuchsia::test]
383    fn packets_of_different_formats_hash_same_with_matching_data() -> Result<(), Error> {
384        let mut rng = StdRng::seed_from_u64(45);
385
386        for (nv12_spec, yv12_spec) in specs(PixelFormat::Nv12).zip(specs(PixelFormat::Yv12)) {
387            // Initialize two packets with random data. Then set every display byte to
388            // `global_index % 256`, where `global_index` is a count of every display byte so far
389            // in the frame.
390
391            let mut nv12_packet: OutputPacket = nv12_spec.into();
392            rng.fill(nv12_packet.data.as_mut_slice());
393
394            let mut nv12_global_index: usize = 0;
395            let mut assign_index = |b: &mut u8| {
396                *b = (nv12_global_index % 256) as u8;
397                nv12_global_index += 1;
398            };
399
400            // NV12 Luminance plane.
401            for row in
402                nv12_packet.data.chunks_mut(nv12_spec.bytes_per_row).take(nv12_spec.display_height)
403            {
404                row.iter_mut().take(nv12_spec.display_width).for_each(&mut assign_index);
405            }
406
407            // NV12 Chroma V plane.
408            for row in nv12_packet
409                .data
410                .chunks_mut(nv12_spec.bytes_per_row)
411                .skip(nv12_spec.coded_height() as usize)
412                .take(nv12_spec.display_height / Nv12Frame::CHROMA_SUBSAMPLE_RATIO)
413            {
414                row.iter_mut()
415                    .take(nv12_spec.display_width)
416                    .skip(1)
417                    .step_by(2)
418                    .for_each(&mut assign_index)
419            }
420
421            // NV12 Chroma U plane.
422            for row in nv12_packet
423                .data
424                .chunks_mut(nv12_spec.bytes_per_row)
425                .skip(nv12_spec.coded_height() as usize)
426                .take(nv12_spec.display_height / Nv12Frame::CHROMA_SUBSAMPLE_RATIO)
427            {
428                row.iter_mut().take(nv12_spec.display_width).step_by(2).for_each(&mut assign_index)
429            }
430
431            let mut yv12_packet: OutputPacket = yv12_spec.into();
432            rng.fill(yv12_packet.data.as_mut_slice());
433
434            let mut yv12_global_index: usize = 0;
435            let mut assign_index = |b: &mut u8| {
436                *b = (yv12_global_index % 256) as u8;
437                yv12_global_index += 1;
438            };
439
440            // YV12 Luminance plane.
441            for row in
442                yv12_packet.data.chunks_mut(yv12_spec.bytes_per_row).take(yv12_spec.display_height)
443            {
444                row.iter_mut().take(yv12_spec.display_width).for_each(&mut assign_index);
445            }
446
447            // YV12 Chroma V plane.
448            let luminance_plane_size = yv12_spec.bytes_per_row * yv12_spec.coded_height();
449            for row in yv12_packet.data[luminance_plane_size..]
450                .chunks_mut(yv12_spec.bytes_per_row / Yv12Frame::CHROMA_SUBSAMPLE_RATIO)
451                .take(yv12_spec.display_height / Yv12Frame::CHROMA_SUBSAMPLE_RATIO)
452            {
453                row.iter_mut()
454                    .take(yv12_spec.display_width / Yv12Frame::CHROMA_SUBSAMPLE_RATIO)
455                    .for_each(&mut assign_index)
456            }
457
458            // YV12 Chroma U plane.
459            let chrominance_plane_size =
460                luminance_plane_size / Yv12Frame::CHROMA_SUBSAMPLE_RATIO.pow(2);
461            for row in yv12_packet.data[(luminance_plane_size + chrominance_plane_size)..]
462                .chunks_mut(yv12_spec.bytes_per_row / Yv12Frame::CHROMA_SUBSAMPLE_RATIO)
463                .take(yv12_spec.display_height / Yv12Frame::CHROMA_SUBSAMPLE_RATIO)
464            {
465                row.iter_mut()
466                    .take(yv12_spec.display_width / Yv12Frame::CHROMA_SUBSAMPLE_RATIO)
467                    .for_each(&mut assign_index)
468            }
469
470            let from_yv12 = packet_display_data(&yv12_packet)?;
471            let from_nv12 = packet_display_data(&nv12_packet)?;
472
473            let yv12_hash = Sha256::hash(&from_yv12.collect::<Vec<u8>>().as_slice());
474            let nv12_hash = Sha256::hash(&from_nv12.collect::<Vec<u8>>().as_slice());
475
476            assert_eq!(yv12_hash, nv12_hash);
477        }
478
479        Ok(())
480    }
481
482    #[fuchsia::test]
483    fn sanity_test_that_different_packets_hash_differently() -> Result<(), Error> {
484        let mut rng = StdRng::seed_from_u64(45);
485
486        for (nv12_spec, yv12_spec) in specs(PixelFormat::Nv12).zip(specs(PixelFormat::Yv12)) {
487            let mut nv12_packet: OutputPacket = nv12_spec.into();
488            rng.fill(nv12_packet.data.as_mut_slice());
489
490            let mut yv12_packet: OutputPacket = yv12_spec.into();
491            yv12_packet.data.iter_mut().enumerate().for_each(|(i, b)| *b = nv12_packet.data[i]);
492
493            // Change a random display byte.
494            let x = rng.gen_range(0..nv12_spec.display_width);
495            let y = {
496                let y_range = [
497                    // Luminance plane rows.
498                    (0..nv12_spec.display_height),
499                    // Chrominance plane rows.
500                    {
501                        let start = nv12_spec.coded_height();
502                        let end = start + (nv12_spec.display_height / 2);
503                        start..end
504                    },
505                ]
506                .choose(&mut rng)
507                .expect("Sampling from nonempty slice")
508                .clone();
509                rng.gen_range(y_range)
510            };
511            let idx = y * nv12_spec.bytes_per_row + x;
512            nv12_packet.data[idx] = nv12_packet.data[idx].overflowing_add(1).0;
513
514            let from_yv12 = packet_display_data(&yv12_packet)?;
515            let from_nv12 = packet_display_data(&nv12_packet)?;
516
517            let yv12_hash = Sha256::hash(&from_yv12.collect::<Vec<u8>>().as_slice());
518            let nv12_hash = Sha256::hash(&from_nv12.collect::<Vec<u8>>().as_slice());
519
520            assert_ne!(yv12_hash, nv12_hash);
521        }
522
523        Ok(())
524    }
525}