1use crate::media::{ColorRange, PixelFormat, Plane, VideoFrame};
18use crate::mp4::{CoverArt, CoverArtFormat};
19use crate::transfer::{FrameSource, Orientation};
20use crate::{Error, ErrorKind, Result};
21
22pub const DEFAULT_COVER_FRAME: u64 = 4;
25
26pub const COVER_THUMBNAIL_MAX_EDGE: u32 = 512;
29
30#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
34pub enum CoverSource {
35 None,
37 Frame(u64),
42}
43
44impl Default for CoverSource {
45 fn default() -> Self {
46 Self::Frame(DEFAULT_COVER_FRAME)
47 }
48}
49
50impl CoverArt {
51 pub fn from_video_frame(frame: &VideoFrame) -> Result<Self> {
56 Ok(Thumbnail::new(frame, Orientation::TopLeft)?.to_cover_art())
57 }
58}
59
60#[derive(Debug)]
62#[doc(hidden)]
63pub struct CoverCapture {
64 source: CoverSource,
65 thumbnail: Option<Thumbnail>,
66}
67
68impl CoverCapture {
69 #[doc(hidden)]
70 pub fn new(source: CoverSource) -> Self {
71 Self {
72 source,
73 thumbnail: None,
74 }
75 }
76
77 #[doc(hidden)]
80 pub fn offer(&mut self, index: u64, frame: FrameSource<'_>) {
81 let CoverSource::Frame(target) = self.source else {
82 return;
83 };
84 if index > target {
85 return;
86 }
87 self.thumbnail = match frame {
90 FrameSource::Cpu(source) => Thumbnail::new(source.frame, source.orientation).ok(),
91 FrameSource::Graphics(_) => None,
92 };
93 }
94
95 #[doc(hidden)]
97 pub fn cover_art(&self) -> Option<CoverArt> {
98 self.thumbnail.as_ref().map(Thumbnail::to_cover_art)
99 }
100}
101
102#[derive(Debug)]
104struct Thumbnail {
105 width: usize,
106 height: usize,
107 rgb: Vec<u8>,
108}
109
110impl Thumbnail {
111 fn new(frame: &VideoFrame, orientation: Orientation) -> Result<Self> {
112 let source_width = frame.dimensions.width as usize;
113 let source_height = frame.dimensions.height as usize;
114 let longest = frame.dimensions.width.max(frame.dimensions.height);
115 let scale = longest.div_ceil(COVER_THUMBNAIL_MAX_EDGE).max(1) as usize;
116 let width = source_width.div_ceil(scale);
117 let height = source_height.div_ceil(scale);
118 let reader = PixelReader::new(frame)?;
119 let mut rgb = vec![0_u8; width * height * 3];
120 for y in 0..height {
121 for x in 0..width {
122 let mut totals = [0_u32; 3];
123 let mut count = 0_u32;
124 for source_y in y * scale..((y + 1) * scale).min(source_height) {
125 let row = match orientation {
126 Orientation::TopLeft => source_y,
127 Orientation::BottomLeft => source_height - 1 - source_y,
128 };
129 for source_x in x * scale..((x + 1) * scale).min(source_width) {
130 let pixel = reader.rgb(source_x, row);
131 for (total, channel) in totals.iter_mut().zip(pixel) {
132 *total += u32::from(channel);
133 }
134 count += 1;
135 }
136 }
137 let offset = (y * width + x) * 3;
138 for (channel, total) in rgb[offset..offset + 3].iter_mut().zip(totals) {
139 *channel = ((total + count / 2) / count) as u8;
140 }
141 }
142 }
143 Ok(Self { width, height, rgb })
144 }
145
146 fn to_cover_art(&self) -> CoverArt {
147 CoverArt {
148 format: CoverArtFormat::Png,
149 data: encode_png_rgb(self.width, self.height, &self.rgb),
150 }
151 }
152}
153
154struct PixelReader<'a> {
156 format: PixelFormat,
157 range: ColorRange,
158 planes: &'a [Plane],
159}
160
161impl<'a> PixelReader<'a> {
162 fn new(frame: &'a VideoFrame) -> Result<Self> {
163 let planes_needed = match frame.pixel_format {
164 PixelFormat::Yuv420p8 => 3,
165 PixelFormat::Rgba8 | PixelFormat::Bgra8 | PixelFormat::Rgb8 | PixelFormat::Gray8 => 1,
166 _ => {
167 return Err(Error::new(
168 ErrorKind::Unsupported,
169 "cover art cannot be made from this pixel format",
170 ));
171 }
172 };
173 if frame.planes.len() < planes_needed {
174 return Err(Error::new(
175 ErrorKind::InvalidInput,
176 "video frame is missing a plane",
177 ));
178 }
179 Ok(Self {
180 format: frame.pixel_format,
181 range: frame.color_range,
182 planes: &frame.planes,
183 })
184 }
185
186 fn rgb(&self, x: usize, y: usize) -> [u8; 3] {
187 let plane = &self.planes[0];
188 let row = &plane.data[y * plane.stride..];
189 let rgb = match self.format {
190 PixelFormat::Rgba8 => [row[x * 4], row[x * 4 + 1], row[x * 4 + 2]],
191 PixelFormat::Bgra8 => [row[x * 4 + 2], row[x * 4 + 1], row[x * 4]],
192 PixelFormat::Rgb8 => [row[x * 3], row[x * 3 + 1], row[x * 3 + 2]],
193 PixelFormat::Gray8 => [row[x]; 3],
194 PixelFormat::Yuv420p8 => {
195 let chroma = |plane: &Plane| plane.data[(y / 2) * plane.stride + x / 2];
196 return yuv_to_rgb(
197 row[x],
198 chroma(&self.planes[1]),
199 chroma(&self.planes[2]),
200 self.range,
201 );
202 }
203 _ => unreachable!("PixelReader::new rejects every other pixel format"),
204 };
205 match self.range {
206 ColorRange::Limited => rgb.map(|component| {
207 ((u32::from(component.saturating_sub(16)) * 255 + 109) / 219).min(255) as u8
208 }),
209 _ => rgb,
210 }
211 }
212}
213
214fn yuv_to_rgb(y: u8, u: u8, v: u8, range: ColorRange) -> [u8; 3] {
217 let (y, u, v) = (i32::from(y), i32::from(u) - 128, i32::from(v) - 128);
218 let (luma, r, g_u, g_v, b) = match range {
219 ColorRange::Limited => ((y - 16) * 76_309, 104_597, -25_675, -53_279, 132_201),
220 _ => (y << 16, 91_881, -22_554, -46_802, 116_130),
221 };
222 let clip = |value: i32| ((value + (1 << 15)) >> 16).clamp(0, 255) as u8;
223 [
224 clip(luma + r * v),
225 clip(luma + g_u * u + g_v * v),
226 clip(luma + b * u),
227 ]
228}
229
230pub(crate) fn encode_png_rgb(width: usize, height: usize, rgb: &[u8]) -> Vec<u8> {
234 let row_bytes = width * 3;
235 let mut filtered = Vec::with_capacity((row_bytes + 1) * height);
236 let mut candidate = vec![0_u8; row_bytes];
237 let mut best = vec![0_u8; row_bytes];
238 for y in 0..height {
239 let row = &rgb[y * row_bytes..(y + 1) * row_bytes];
240 let previous = (y > 0).then(|| &rgb[(y - 1) * row_bytes..y * row_bytes]);
241 let mut best_filter = 0_u8;
242 let mut best_cost = u64::MAX;
243 for filter in 0..5_u8 {
244 for x in 0..row_bytes {
245 let left = if x >= 3 { row[x - 3] } else { 0 };
246 let up = previous.map_or(0, |previous| previous[x]);
247 let up_left = if x >= 3 {
248 previous.map_or(0, |previous| previous[x - 3])
249 } else {
250 0
251 };
252 let prediction = match filter {
253 0 => 0,
254 1 => left,
255 2 => up,
256 3 => ((u16::from(left) + u16::from(up)) / 2) as u8,
257 _ => paeth(left, up, up_left),
258 };
259 candidate[x] = row[x].wrapping_sub(prediction);
260 }
261 let cost = candidate
262 .iter()
263 .map(|&value| u64::from((value as i8).unsigned_abs()))
264 .sum();
265 if cost < best_cost {
266 best_cost = cost;
267 best_filter = filter;
268 best.copy_from_slice(&candidate);
269 }
270 }
271 filtered.push(best_filter);
272 filtered.extend_from_slice(&best);
273 }
274
275 let mut ihdr = Vec::with_capacity(13);
276 ihdr.extend_from_slice(&(width as u32).to_be_bytes());
277 ihdr.extend_from_slice(&(height as u32).to_be_bytes());
278 ihdr.extend_from_slice(&[8, 2, 0, 0, 0]);
280
281 let mut png = b"\x89PNG\r\n\x1a\n".to_vec();
282 png_chunk(&mut png, b"IHDR", &ihdr);
283 png_chunk(&mut png, b"IDAT", &zlib(&filtered));
284 png_chunk(&mut png, b"IEND", &[]);
285 png
286}
287
288fn paeth(left: u8, up: u8, up_left: u8) -> u8 {
289 let estimate = i16::from(left) + i16::from(up) - i16::from(up_left);
290 let distance_left = (estimate - i16::from(left)).abs();
291 let distance_up = (estimate - i16::from(up)).abs();
292 let distance_up_left = (estimate - i16::from(up_left)).abs();
293 if distance_left <= distance_up && distance_left <= distance_up_left {
294 left
295 } else if distance_up <= distance_up_left {
296 up
297 } else {
298 up_left
299 }
300}
301
302fn png_chunk(png: &mut Vec<u8>, kind: &[u8; 4], data: &[u8]) {
303 png.extend_from_slice(&(data.len() as u32).to_be_bytes());
304 let start = png.len();
305 png.extend_from_slice(kind);
306 png.extend_from_slice(data);
307 let crc = crc32(&png[start..]);
308 png.extend_from_slice(&crc.to_be_bytes());
309}
310
311fn crc32(bytes: &[u8]) -> u32 {
312 let mut crc = !0_u32;
313 for &byte in bytes {
314 crc ^= u32::from(byte);
315 for _ in 0..8 {
316 crc = (crc >> 1) ^ (0xedb8_8320 & (crc & 1).wrapping_neg());
317 }
318 }
319 !crc
320}
321
322fn adler32(bytes: &[u8]) -> u32 {
323 let (mut a, mut b) = (1_u32, 0_u32);
324 for chunk in bytes.chunks(5_552) {
325 for &byte in chunk {
326 a += u32::from(byte);
327 b += a;
328 }
329 a %= 65_521;
330 b %= 65_521;
331 }
332 (b << 16) | a
333}
334
335fn zlib(data: &[u8]) -> Vec<u8> {
336 let mut output = vec![0x78, 0x01];
338 let compressed = deflate_fixed(data);
339 if compressed.len() < stored_len(data.len()) {
340 output.extend_from_slice(&compressed);
341 } else {
342 deflate_stored(data, &mut output);
343 }
344 output.extend_from_slice(&adler32(data).to_be_bytes());
345 output
346}
347
348fn stored_len(length: usize) -> usize {
349 length + 5 * length.div_ceil(65_535).max(1)
350}
351
352fn deflate_stored(data: &[u8], output: &mut Vec<u8>) {
353 let mut blocks = data.chunks(65_535).peekable();
354 if blocks.peek().is_none() {
355 output.extend_from_slice(&[1, 0, 0, 0xff, 0xff]);
356 }
357 while let Some(block) = blocks.next() {
358 output.push(u8::from(blocks.peek().is_none()));
359 let length = block.len() as u16;
360 output.extend_from_slice(&length.to_le_bytes());
361 output.extend_from_slice(&(!length).to_le_bytes());
362 output.extend_from_slice(block);
363 }
364}
365
366const LENGTH_BASE: [u16; 29] = [
367 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131,
368 163, 195, 227, 258,
369];
370const LENGTH_EXTRA: [u8; 29] = [
371 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0,
372];
373const DISTANCE_BASE: [u16; 30] = [
374 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537,
375 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577,
376];
377const DISTANCE_EXTRA: [u8; 30] = [
378 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13,
379 13,
380];
381const WINDOW: usize = 32_768;
382const MIN_MATCH: usize = 3;
383const MAX_MATCH: usize = 258;
384const HASH_BITS: u32 = 15;
385const MAX_CHAIN: usize = 64;
386
387struct BitWriter {
389 bytes: Vec<u8>,
390 buffer: u64,
391 bits: u32,
392}
393
394impl BitWriter {
395 fn bits(&mut self, value: u32, count: u32) {
396 self.buffer |= u64::from(value) << self.bits;
397 self.bits += count;
398 while self.bits >= 8 {
399 self.bytes.push(self.buffer as u8);
400 self.buffer >>= 8;
401 self.bits -= 8;
402 }
403 }
404
405 fn code(&mut self, code: u32, length: u32) {
407 self.bits(code.reverse_bits() >> (32 - length), length);
408 }
409
410 fn literal_length(&mut self, symbol: u16) {
411 let symbol = u32::from(symbol);
412 match symbol {
413 0..=143 => self.code(0x30 + symbol, 8),
414 144..=255 => self.code(0x190 + symbol - 144, 9),
415 256..=279 => self.code(symbol - 256, 7),
416 _ => self.code(0xc0 + symbol - 280, 8),
417 }
418 }
419
420 fn finish(mut self) -> Vec<u8> {
421 if self.bits > 0 {
422 self.bytes.push(self.buffer as u8);
423 }
424 self.bytes
425 }
426}
427
428fn deflate_fixed(data: &[u8]) -> Vec<u8> {
429 let mut writer = BitWriter {
430 bytes: Vec::with_capacity(data.len() / 2),
431 buffer: 0,
432 bits: 0,
433 };
434 writer.bits(1, 1);
436 writer.bits(1, 2);
437
438 let hash = |position: usize| {
439 let value = u32::from(data[position])
440 | u32::from(data[position + 1]) << 8
441 | u32::from(data[position + 2]) << 16;
442 (value.wrapping_mul(0x9e37_79b1) >> (32 - HASH_BITS)) as usize
443 };
444 let mut head = vec![usize::MAX; 1 << HASH_BITS];
445 let mut previous = vec![usize::MAX; data.len()];
446 let insert = |position: usize, head: &mut [usize], previous: &mut [usize]| {
447 if position + MIN_MATCH <= data.len() {
448 let key = hash(position);
449 previous[position] = head[key];
450 head[key] = position;
451 }
452 };
453
454 let mut position = 0;
455 while position < data.len() {
456 let mut best_length = 0;
457 let mut best_distance = 0;
458 if position + MIN_MATCH <= data.len() {
459 let limit = (data.len() - position).min(MAX_MATCH);
460 let mut candidate = head[hash(position)];
461 let mut chain = 0;
462 while candidate != usize::MAX && position - candidate <= WINDOW && chain < MAX_CHAIN {
463 let length = data[candidate..]
464 .iter()
465 .zip(&data[position..position + limit])
466 .take_while(|(a, b)| a == b)
467 .count();
468 if length > best_length {
469 best_length = length;
470 best_distance = position - candidate;
471 if length == limit {
472 break;
473 }
474 }
475 candidate = previous[candidate];
476 chain += 1;
477 }
478 }
479 if best_length >= MIN_MATCH {
480 let length_code = LENGTH_BASE
481 .iter()
482 .rposition(|&base| usize::from(base) <= best_length)
483 .unwrap_or(0);
484 writer.literal_length(257 + length_code as u16);
485 writer.bits(
486 (best_length - usize::from(LENGTH_BASE[length_code])) as u32,
487 u32::from(LENGTH_EXTRA[length_code]),
488 );
489 let distance_code = DISTANCE_BASE
490 .iter()
491 .rposition(|&base| usize::from(base) <= best_distance)
492 .unwrap_or(0);
493 writer.code(distance_code as u32, 5);
494 writer.bits(
495 (best_distance - usize::from(DISTANCE_BASE[distance_code])) as u32,
496 u32::from(DISTANCE_EXTRA[distance_code]),
497 );
498 for offset in 0..best_length {
499 insert(position + offset, &mut head, &mut previous);
500 }
501 position += best_length;
502 } else {
503 writer.literal_length(u16::from(data[position]));
504 insert(position, &mut head, &mut previous);
505 position += 1;
506 }
507 }
508 writer.literal_length(256);
509 writer.finish()
510}
511
512#[cfg(test)]
513mod tests {
514 use super::*;
515 use crate::api::Limits;
516 use crate::media::VideoDimensions;
517 use crate::transfer::CpuFrameSource;
518
519 fn inflate(zlib: &[u8]) -> Vec<u8> {
522 assert_eq!(&zlib[..2], &[0x78, 0x01]);
523 let stream = &zlib[2..zlib.len() - 4];
524 let mut output = Vec::new();
525 let mut bit = 0_usize;
526 let read = |bit: &mut usize, count: usize| {
527 let mut value = 0_u32;
528 for index in 0..count {
529 let byte = stream[(*bit + index) / 8];
530 value |= u32::from((byte >> ((*bit + index) % 8)) & 1) << index;
531 }
532 *bit += count;
533 value
534 };
535 let read_code = |bit: &mut usize, count: usize| {
536 let mut value = 0_u32;
537 for _ in 0..count {
538 value = (value << 1) | read(bit, 1);
539 }
540 value
541 };
542 loop {
543 let last = read(&mut bit, 1);
544 match read(&mut bit, 2) {
545 0 => {
546 bit = bit.div_ceil(8) * 8;
547 let at = bit / 8;
548 let length = usize::from(u16::from_le_bytes([stream[at], stream[at + 1]]));
549 output.extend_from_slice(&stream[at + 4..at + 4 + length]);
550 bit = (at + 4 + length) * 8;
551 }
552 1 => loop {
553 let mut code = read_code(&mut bit, 7);
554 let symbol = if code <= 0x17 {
555 code + 256
556 } else {
557 code = (code << 1) | read(&mut bit, 1);
558 if (0x30..=0xbf).contains(&code) {
559 code - 0x30
560 } else if (0xc0..=0xc7).contains(&code) {
561 code - 0xc0 + 280
562 } else {
563 code = (code << 1) | read(&mut bit, 1);
564 code - 0x190 + 144
565 }
566 } as usize;
567 if symbol < 256 {
568 output.push(symbol as u8);
569 continue;
570 }
571 if symbol == 256 {
572 break;
573 }
574 let index = symbol - 257;
575 let length = usize::from(LENGTH_BASE[index])
576 + read(&mut bit, usize::from(LENGTH_EXTRA[index])) as usize;
577 let distance_code = read_code(&mut bit, 5) as usize;
578 let distance = usize::from(DISTANCE_BASE[distance_code])
579 + read(&mut bit, usize::from(DISTANCE_EXTRA[distance_code])) as usize;
580 for _ in 0..length {
581 output.push(output[output.len() - distance]);
582 }
583 },
584 other => panic!("unexpected block type {other}"),
585 }
586 if last == 1 {
587 break;
588 }
589 }
590 assert_eq!(
591 u32::from_be_bytes(zlib[zlib.len() - 4..].try_into().unwrap()),
592 adler32(&output)
593 );
594 output
595 }
596
597 fn decode_png(png: &[u8]) -> (usize, usize, Vec<u8>) {
599 assert_eq!(&png[..8], b"\x89PNG\r\n\x1a\n");
600 let mut at = 8;
601 let (mut width, mut height, mut idat) = (0, 0, Vec::new());
602 while at < png.len() {
603 let length = u32::from_be_bytes(png[at..at + 4].try_into().unwrap()) as usize;
604 let body = &png[at + 4..at + 8 + length];
605 let crc =
606 u32::from_be_bytes(png[at + 8 + length..at + 12 + length].try_into().unwrap());
607 assert_eq!(crc, crc32(body));
608 let data = &body[4..];
609 match &body[..4] {
610 b"IHDR" => {
611 width = u32::from_be_bytes(data[..4].try_into().unwrap()) as usize;
612 height = u32::from_be_bytes(data[4..8].try_into().unwrap()) as usize;
613 assert_eq!(&data[8..], &[8, 2, 0, 0, 0]);
614 }
615 b"IDAT" => idat.extend_from_slice(data),
616 b"IEND" => break,
617 _ => {}
618 }
619 at += 12 + length;
620 }
621 let filtered = inflate(&idat);
622 let row_bytes = width * 3;
623 let mut rgb = vec![0_u8; row_bytes * height];
624 for y in 0..height {
625 let line = &filtered[y * (row_bytes + 1)..(y + 1) * (row_bytes + 1)];
626 for x in 0..row_bytes {
627 let left = if x >= 3 {
628 rgb[y * row_bytes + x - 3]
629 } else {
630 0
631 };
632 let up = if y > 0 {
633 rgb[(y - 1) * row_bytes + x]
634 } else {
635 0
636 };
637 let up_left = if x >= 3 && y > 0 {
638 rgb[(y - 1) * row_bytes + x - 3]
639 } else {
640 0
641 };
642 let prediction = match line[0] {
643 0 => 0,
644 1 => left,
645 2 => up,
646 3 => ((u16::from(left) + u16::from(up)) / 2) as u8,
647 4 => paeth(left, up, up_left),
648 other => panic!("unexpected filter {other}"),
649 };
650 rgb[y * row_bytes + x] = line[x + 1].wrapping_add(prediction);
651 }
652 }
653 (width, height, rgb)
654 }
655
656 fn frame(width: u32, height: u32, format: PixelFormat, planes: Vec<Plane>) -> VideoFrame {
657 let limits = Limits::default();
658 VideoFrame::new(
659 VideoDimensions::new(width, height, &limits).unwrap(),
660 format,
661 ColorRange::Full,
662 planes,
663 &limits,
664 )
665 .unwrap()
666 }
667
668 fn pattern(width: usize, height: usize, seed: u8) -> Vec<u8> {
669 (0..width * height * 3)
670 .map(|index| {
671 let pixel = index / 3;
672 let (x, y) = (pixel % width, pixel / width);
673 ((x * 7 + y * 13 + (index % 3) * 50) as u8).wrapping_add(seed)
674 })
675 .collect()
676 }
677
678 #[test]
679 fn png_round_trips_patterned_flat_and_noisy_images() {
680 let mut noise = 0x1234_5678_u32;
681 let noisy: Vec<u8> = (0..97 * 31 * 3)
682 .map(|_| {
683 noise ^= noise << 13;
684 noise ^= noise >> 17;
685 noise ^= noise << 5;
686 noise as u8
687 })
688 .collect();
689 for (width, height, rgb) in [
690 (1, 1, vec![1, 2, 3]),
691 (64, 48, pattern(64, 48, 9)),
692 (300, 200, vec![77; 300 * 200 * 3]),
693 (97, 31, noisy),
694 ] {
695 let png = encode_png_rgb(width, height, &rgb);
696 assert_eq!(decode_png(&png), (width, height, rgb));
697 }
698 }
699
700 #[test]
701 fn stored_blocks_are_used_when_huffman_coding_would_grow_the_data() {
702 let mut noise = 0x9e37_79b9_u32;
703 let data: Vec<u8> = (0..70_000)
704 .map(|_| {
705 noise ^= noise << 13;
706 noise ^= noise >> 17;
707 noise ^= noise << 5;
708 noise as u8
709 })
710 .collect();
711 let stream = zlib(&data);
712 assert_eq!(stream.len(), 2 + stored_len(data.len()) + 4);
713 assert_eq!(inflate(&stream), data);
714 assert_eq!(inflate(&zlib(&[])), Vec::<u8>::new());
715 }
716
717 #[test]
718 fn a_flat_frame_compresses_far_below_its_raw_size() {
719 let png = encode_png_rgb(512, 288, &vec![40; 512 * 288 * 3]);
720 assert!(png.len() < 4_096, "{} bytes", png.len());
721 }
722
723 #[test]
724 fn large_frames_shrink_to_the_thumbnail_edge_by_averaging() {
725 let (width, height) = (1030_usize, 6_usize);
727 let mut rgba = vec![0_u8; width * height * 4];
728 for (pixel, value) in rgba.chunks_mut(4).enumerate() {
729 let (x, y) = (pixel % width, pixel / width);
730 let level = if x % 3 == 0 { (y % 2 * 30) as u8 } else { 60 };
732 value.copy_from_slice(&[level, level / 2, 255 - level, 0]);
733 }
734 let source = frame(
735 width as u32,
736 height as u32,
737 PixelFormat::Rgba8,
738 vec![Plane {
739 data: rgba,
740 stride: width * 4,
741 }],
742 );
743 let thumbnail = Thumbnail::new(&source, Orientation::TopLeft).unwrap();
744 assert_eq!((thumbnail.width, thumbnail.height), (344, 2));
745 assert_eq!(&thumbnail.rgb[..3], &[43, 22, 212]);
748 assert_eq!(&thumbnail.rgb[344 * 3..344 * 3 + 3], &[47, 23, 208]);
749 assert_eq!(&thumbnail.rgb[343 * 3..344 * 3], &[10, 5, 245]);
751 }
752
753 #[test]
754 fn every_pixel_format_and_orientation_reads_as_top_down_rgb() {
755 let rgb = [[200_u8, 100, 50], [10, 20, 30]];
756 let packed = |bytes: &dyn Fn([u8; 3]) -> Vec<u8>| -> Vec<u8> {
757 rgb.iter().flat_map(|&pixel| bytes(pixel)).collect()
758 };
759 let one_by_two = |format, data: Vec<u8>| {
760 let stride = data.len() / 2;
761 frame(1, 2, format, vec![Plane { data, stride }])
762 };
763 for source in [
764 one_by_two(PixelFormat::Rgba8, packed(&|[r, g, b]| vec![r, g, b, 255])),
765 one_by_two(PixelFormat::Bgra8, packed(&|[r, g, b]| vec![b, g, r, 255])),
766 one_by_two(PixelFormat::Rgb8, packed(&|pixel| pixel.to_vec())),
767 ] {
768 let thumbnail = Thumbnail::new(&source, Orientation::TopLeft).unwrap();
769 assert_eq!(thumbnail.rgb, rgb.concat());
770 let flipped = Thumbnail::new(&source, Orientation::BottomLeft).unwrap();
771 assert_eq!(flipped.rgb, [rgb[1], rgb[0]].concat());
772 }
773
774 let gray = one_by_two(PixelFormat::Gray8, vec![9, 250]);
775 let thumbnail = Thumbnail::new(&gray, Orientation::TopLeft).unwrap();
776 assert_eq!(thumbnail.rgb, [9, 9, 9, 250, 250, 250]);
777
778 let mut limited = one_by_two(PixelFormat::Gray8, vec![16, 235]);
779 limited.color_range = ColorRange::Limited;
780 let thumbnail = Thumbnail::new(&limited, Orientation::TopLeft).unwrap();
781 assert_eq!(thumbnail.rgb, [0, 0, 0, 255, 255, 255]);
782 }
783
784 #[test]
785 fn yuv420_converts_with_bt601_in_either_range() {
786 let yuv = |y: u8, u: u8, v: u8, range| {
787 let mut source = frame(
788 2,
789 2,
790 PixelFormat::Yuv420p8,
791 vec![
792 Plane {
793 data: vec![y; 4],
794 stride: 2,
795 },
796 Plane {
797 data: vec![u],
798 stride: 1,
799 },
800 Plane {
801 data: vec![v],
802 stride: 1,
803 },
804 ],
805 );
806 source.color_range = range;
807 Thumbnail::new(&source, Orientation::TopLeft).unwrap().rgb[..3].to_vec()
808 };
809 assert_eq!(yuv(16, 128, 128, ColorRange::Limited), [0, 0, 0]);
810 assert_eq!(yuv(235, 128, 128, ColorRange::Limited), [255, 255, 255]);
811 assert_eq!(yuv(81, 90, 240, ColorRange::Limited), [254, 0, 0]);
813 assert_eq!(yuv(145, 54, 34, ColorRange::Limited), [0, 255, 1]);
814 assert_eq!(yuv(41, 240, 110, ColorRange::Limited), [0, 0, 255]);
815 assert_eq!(yuv(128, 128, 128, ColorRange::Full), [128, 128, 128]);
816 assert_eq!(yuv(76, 85, 255, ColorRange::Full), [254, 0, 0]);
817 }
818
819 #[test]
820 fn capture_keeps_the_chosen_frame_or_the_last_one_before_it() {
821 let solid = |value: u8| {
822 frame(
823 2,
824 2,
825 PixelFormat::Gray8,
826 vec![Plane {
827 data: vec![value; 4],
828 stride: 2,
829 }],
830 )
831 };
832 let frames: Vec<VideoFrame> = (0..8).map(|index| solid(index * 10)).collect();
833 let offer = |capture: &mut CoverCapture, count: usize| {
834 for (index, frame) in frames.iter().take(count).enumerate() {
835 capture.offer(
836 index as u64,
837 FrameSource::Cpu(CpuFrameSource {
838 frame,
839 orientation: Orientation::TopLeft,
840 }),
841 );
842 }
843 };
844 let first_pixel = |capture: &CoverCapture| {
845 capture
846 .cover_art()
847 .map(|cover| decode_png(&cover.data).2[0])
848 };
849
850 let mut default = CoverCapture::new(CoverSource::default());
851 offer(&mut default, 8);
852 assert_eq!(first_pixel(&default), Some(40));
853
854 let mut short = CoverCapture::new(CoverSource::Frame(6));
855 offer(&mut short, 3);
856 assert_eq!(first_pixel(&short), Some(20));
857
858 let mut none = CoverCapture::new(CoverSource::None);
859 offer(&mut none, 8);
860 assert_eq!(first_pixel(&none), None);
861
862 let empty = CoverCapture::new(CoverSource::default());
863 assert_eq!(first_pixel(&empty), None);
864 }
865}