1#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum Format {
12 Webp,
14}
15
16impl Format {
17 fn code(self) -> u8 {
18 match self {
19 Format::Webp => 1,
20 }
21 }
22
23 fn from_code(code: u8) -> Option<Self> {
24 match code {
25 1 => Some(Format::Webp),
26 _ => None,
27 }
28 }
29}
30
31pub const HEADER_LEN: usize = 18;
33
34#[derive(Clone, Debug, PartialEq, Eq)]
36pub struct Frame {
37 pub profile: u16,
39 pub tab: u16,
41 pub seq: u32,
43 pub width: u16,
44 pub height: u16,
45 pub format: Format,
46 pub key: bool,
48 pub page_width: u16,
52 pub page_height: u16,
53 pub bytes: Vec<u8>,
54}
55
56impl Frame {
57 pub fn encode(&self) -> Vec<u8> {
59 let mut out = Vec::with_capacity(HEADER_LEN + self.bytes.len());
60 out.extend_from_slice(&self.profile.to_le_bytes());
61 out.extend_from_slice(&self.tab.to_le_bytes());
62 out.extend_from_slice(&self.seq.to_le_bytes());
63 out.extend_from_slice(&self.width.to_le_bytes());
64 out.extend_from_slice(&self.height.to_le_bytes());
65 out.push(self.format.code());
66 out.push(u8::from(self.key));
67 out.extend_from_slice(&self.page_width.to_le_bytes());
68 out.extend_from_slice(&self.page_height.to_le_bytes());
69 out.extend_from_slice(&self.bytes);
70 out
71 }
72
73 pub fn decode(bytes: &[u8]) -> Result<Self, String> {
75 if bytes.len() < HEADER_LEN {
76 return Err(format!("a frame is at least {HEADER_LEN} bytes, got {}", bytes.len()));
77 }
78 let profile = u16::from_le_bytes([bytes[0], bytes[1]]);
79 let tab = u16::from_le_bytes([bytes[2], bytes[3]]);
80 let seq = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
81 let width = u16::from_le_bytes([bytes[8], bytes[9]]);
82 let height = u16::from_le_bytes([bytes[10], bytes[11]]);
83 let format = Format::from_code(bytes[12])
84 .ok_or_else(|| format!("unknown frame format {}", bytes[12]))?;
85 let key = match bytes[13] {
86 0 => false,
87 1 => true,
88 other => return Err(format!("the key flag is 0 or 1, got {other}")),
89 };
90 let page_width = u16::from_le_bytes([bytes[14], bytes[15]]);
91 let page_height = u16::from_le_bytes([bytes[16], bytes[17]]);
92 let payload = bytes[HEADER_LEN..].to_vec();
93 if payload.is_empty() {
94 return Err("a frame carries no encoded pixels".to_owned());
95 }
96 if width == 0 || height == 0 {
97 return Err(format!("a frame has a size, got {width}x{height}"));
98 }
99 if page_width == 0 || page_height == 0 {
100 return Err(format!(
101 "a frame names the page it was laid out at, got {page_width}x{page_height}"
102 ));
103 }
104 Ok(Frame {
105 profile,
106 tab,
107 seq,
108 width,
109 height,
110 format,
111 key,
112 page_width,
113 page_height,
114 bytes: payload,
115 })
116 }
117}
118
119#[cfg(target_os = "macos")]
133fn config(quality: u8, key: bool) -> Option<webp::WebPConfig> {
134 const LOSSY_EFFORT: i32 = 0;
137 let mut config = webp::WebPConfig::new().ok()?;
138 config.lossless = i32::from(key);
139 config.alpha_compression = i32::from(!key);
140 config.quality = f32::from(quality);
141 if !key {
142 config.method = LOSSY_EFFORT;
143 }
144 Some(config)
145}
146
147#[cfg(target_os = "macos")]
155pub fn encode_webp(
156 bgra: &[u8],
157 width: u32,
158 height: u32,
159 quality: u8,
160 key: bool,
161) -> Option<Vec<u8>> {
162 if width == 0 || height == 0 {
163 return None;
164 }
165 let mut rgba = bgra.to_vec();
166 for pixel in rgba.chunks_mut(4) {
167 pixel.swap(0, 2);
168 }
169 let Some(config) = config(quality, key) else {
170 eprintln!(
171 "cef host: a frame was not encoded (event_name cef_frame_encode_failed, \
172 {width}x{height}): libwebp refused the encoder config"
173 );
174 return None;
175 };
176 let encoder = webp::Encoder::from_rgba(&rgba, width, height);
177 match encoder.encode_advanced(&config) {
178 Ok(memory) => Some(memory.to_vec()),
179 Err(why) => {
180 eprintln!(
181 "cef host: a frame was not encoded (event_name cef_frame_encode_failed, \
182 {width}x{height}): {why:?}"
183 );
184 None
185 }
186 }
187}
188
189#[cfg(target_os = "macos")]
198pub struct Sink {
199 profile: u16,
200 tab: u16,
201 path: std::path::PathBuf,
202 fps: u16,
203 quality: u8,
204 stream: Option<std::os::unix::net::UnixStream>,
205 last_attempt: Option<std::time::Instant>,
206 last_sent: Option<std::time::Instant>,
207 size: Option<(u32, u32)>,
208 seq: u32,
209 logged_failure: bool,
210 forced_key: bool,
212 paused: bool,
214}
215
216#[cfg(target_os = "macos")]
217impl Sink {
218 pub fn new(profile: u16, tab: u16, path: std::path::PathBuf, fps: u16, quality: u8) -> Self {
219 Self {
220 profile,
221 tab,
222 path,
223 fps: fps.clamp(1, 120),
224 quality: quality.min(100),
225 stream: None,
226 last_attempt: None,
227 last_sent: None,
228 size: None,
229 seq: 0,
230 logged_failure: false,
231 forced_key: false,
232 paused: false,
233 }
234 }
235
236 pub fn force_key(&mut self) {
239 self.forced_key = true;
240 }
241
242 pub fn offer_audio(&mut self, audio: &crate::channel::Audio) {
248 if self.paused {
249 return;
250 }
251 if self.stream.is_none() && !self.reconnect() {
252 return;
253 }
254 if let Some(stream) = self.stream.as_mut()
255 && let Err(why) = crate::channel::write_audio(stream, audio)
256 {
257 eprintln!(
258 "cef host: an audio packet was not written (event_name \
259 cef_audio_write_failed, profile {}): {why}",
260 self.profile
261 );
262 self.stream = None;
264 }
265 }
266
267 pub fn set_paused(&mut self, paused: bool) {
270 if paused == self.paused {
271 return;
272 }
273 self.paused = paused;
274 if !paused {
275 self.forced_key = true;
276 }
277 }
278
279 pub fn interval_remaining(&self) -> std::time::Duration {
284 let interval = std::time::Duration::from_millis(1000 / u64::from(self.fps));
285 self.last_sent
286 .map_or(std::time::Duration::ZERO, |last| interval.saturating_sub(last.elapsed()))
287 }
288
289 pub fn offer(&mut self, bgra: &[u8], width: u32, height: u32, page: (u32, u32)) {
293 if self.paused {
297 return;
298 }
299 let key = self.forced_key || self.size != Some((width, height));
302 if self.stream.is_none() && !self.reconnect() {
303 return;
304 }
305 let Some(bytes) = encode_webp(bgra, width, height, self.quality, key) else {
306 return;
307 };
308 let (Ok(width16), Ok(height16)) = (u16::try_from(width), u16::try_from(height)) else {
309 return;
310 };
311 let (Ok(page_width), Ok(page_height)) =
312 (u16::try_from(page.0.max(1)), u16::try_from(page.1.max(1)))
313 else {
314 return;
315 };
316 let frame = Frame {
317 profile: self.profile,
318 tab: self.tab,
319 seq: self.seq,
320 width: width16,
321 height: height16,
322 format: Format::Webp,
323 key,
324 page_width,
325 page_height,
326 bytes,
327 };
328 if let Some(stream) = self.stream.as_mut() {
329 match crate::channel::write(stream, &frame) {
330 Ok(()) => {
331 self.seq = self.seq.wrapping_add(1);
332 self.last_sent = Some(std::time::Instant::now());
333 self.size = Some((width, height));
334 self.logged_failure = false;
335 self.forced_key = false;
336 }
337 Err(why) => {
338 if !self.logged_failure {
343 eprintln!(
344 "cef host: a frame was dropped (event_name cef_frame_dropped, \
345 profile {}): {why}",
346 self.profile
347 );
348 self.logged_failure = true;
349 }
350 self.stream = None;
351 }
352 }
353 }
354 }
355
356 fn reconnect(&mut self) -> bool {
359 let now = std::time::Instant::now();
360 if self.last_attempt.is_some_and(|last| now.duration_since(last) < ONE_SECOND) {
361 return false;
362 }
363 self.last_attempt = Some(now);
364 match crate::channel::connect(&self.path) {
365 Ok(stream) => {
366 self.stream = Some(stream);
367 self.logged_failure = false;
368 true
369 }
370 Err(why) => {
371 if !self.logged_failure {
372 eprintln!(
373 "cef host: the frame socket is not taken yet (event_name \
374 cef_frames_unconnected, profile {}): {why}",
375 self.profile
376 );
377 self.logged_failure = true;
378 }
379 false
380 }
381 }
382 }
383}
384
385#[cfg(target_os = "macos")]
386const ONE_SECOND: std::time::Duration = std::time::Duration::from_secs(1);
387
388#[cfg(test)]
389mod tests {
390 use super::{Format, Frame, HEADER_LEN};
391
392 fn frame(seq: u32, profile: u16) -> Frame {
393 Frame {
394 profile,
395 tab: 1,
396 seq,
397 width: 800,
398 height: 600,
399 format: Format::Webp,
400 key: seq == 0,
401 page_width: 400,
402 page_height: 300,
403 bytes: vec![1, 2, 3, 4],
404 }
405 }
406
407 #[test]
410 fn a_frame_round_trips_with_its_header() {
411 let original = frame(7, 3);
412 let bytes = original.encode();
413 assert_eq!(bytes.len(), HEADER_LEN + original.bytes.len());
414 assert_eq!(Frame::decode(&bytes), Ok(original));
415 }
416
417 #[test]
420 fn a_frame_names_its_profile_and_its_tab() {
421 let frame = Frame {
422 profile: 3,
423 tab: 7,
424 seq: 9,
425 width: 800,
426 height: 600,
427 format: Format::Webp,
428 key: true,
429 page_width: 400,
430 page_height: 300,
431 bytes: vec![1, 2, 3],
432 };
433 let encoded = frame.encode();
434 assert_eq!(u16::from_le_bytes([encoded[0], encoded[1]]), 3);
435 assert_eq!(u16::from_le_bytes([encoded[2], encoded[3]]), 7);
436 assert_eq!(Frame::decode(&encoded).expect("a frame decodes"), frame);
437
438 let mut old = encoded.clone();
439 old[2..4].copy_from_slice(&0_u16.to_le_bytes());
440 old.truncate(12);
441 assert_eq!(
442 Frame::decode(&old).expect_err("a short header is refused"),
443 "a frame is at least 18 bytes, got 12",
444 );
445 }
446
447 #[test]
452 fn a_frame_names_the_page_its_pixels_were_painted_from() {
453 let mut frame = frame(1, 0);
454 frame.width = 2560;
455 frame.height = 1440;
456 frame.page_width = 1280;
457 frame.page_height = 720;
458 let encoded = frame.encode();
459 assert_eq!(u16::from_le_bytes([encoded[14], encoded[15]]), 1280, "the header's page width");
460 assert_eq!(u16::from_le_bytes([encoded[16], encoded[17]]), 720, "and its height");
461 let read = Frame::decode(&encoded).expect("a frame decodes");
462 assert_eq!((read.page_width, read.page_height), (1280, 720));
463 assert_eq!((read.width, read.height), (2560, 1440), "and its pixels are their own");
464
465 let mut unnamed = encoded.clone();
466 unnamed[14..16].copy_from_slice(&0_u16.to_le_bytes());
467 assert_eq!(
468 Frame::decode(&unnamed).expect_err("a frame with no page is refused"),
469 "a frame names the page it was laid out at, got 0x720",
470 );
471 }
472
473 #[test]
477 fn a_frame_that_does_not_fit_its_header_is_refused() {
478 let whole = frame(1, 0).encode();
479 assert!(Frame::decode(&whole[..HEADER_LEN - 1]).is_err(), "a short header");
480 assert!(Frame::decode(&whole[..HEADER_LEN]).is_err(), "no pixels");
481 let mut unknown = whole.clone();
482 unknown[12] = 9;
483 assert!(Frame::decode(&unknown).is_err(), "an unknown format");
484 let mut flag = whole;
485 flag[13] = 2;
486 assert!(Frame::decode(&flag).is_err(), "a key flag that is not a flag");
487 }
488
489 #[cfg(target_os = "macos")]
493 #[test]
494 fn a_paused_sink_writes_nothing_and_resumes_with_a_key_frame() {
495 let dir = std::env::temp_dir().join(format!("forge-sink-pause-{}", std::process::id()));
496 std::fs::create_dir_all(&dir).expect("a temp dir");
497 let path = dir.join("frames.sock");
498 let listener = std::os::unix::net::UnixListener::bind(&path).expect("a listener");
499 let bgra = [255_u8, 0, 0, 255, 0, 0, 255, 255];
500
501 let mut sink = super::Sink::new(1, 1, path.clone(), 120, 80);
502 sink.set_paused(true);
503 sink.offer(&bgra, 2, 1, (1, 1));
504 listener.set_nonblocking(true).expect("a non-blocking listener");
505 assert!(listener.accept().is_err(), "a paused sink writes no frame");
506
507 sink.set_paused(false);
508 sink.offer(&bgra, 2, 1, (1, 1));
509 let (mut stream, _) = listener.accept().expect("a frame connection");
510 stream.set_read_timeout(Some(super::ONE_SECOND)).expect("a read bound");
511 let crate::channel::Reception::Frame(frame) =
512 crate::channel::read(&mut stream).expect("a frame")
513 else {
514 panic!("a resumed sink sends a frame");
515 };
516 assert!(frame.key, "the first frame after a pause is a key frame");
517 std::fs::remove_dir_all(&dir).expect("the temp dir goes");
518 }
519
520 #[cfg(target_os = "macos")]
524 #[test]
525 fn a_sink_writes_every_frame_and_a_resize_is_a_key() {
526 let dir = std::env::temp_dir().join(format!("forge-sink-resize-{}", std::process::id()));
527 std::fs::create_dir_all(&dir).expect("a temp dir");
528 let path = dir.join("frames.sock");
529 let listener = std::os::unix::net::UnixListener::bind(&path).expect("a listener");
530 let two = [255_u8, 0, 0, 255, 0, 0, 255, 255];
531 let four = [1_u8; 16];
532
533 let mut sink = super::Sink::new(1, 1, path.clone(), 1, 80);
536 sink.offer(&two, 2, 1, (1, 1));
537 let (mut stream, _) = listener.accept().expect("the first connection");
538 stream.set_read_timeout(Some(super::ONE_SECOND)).expect("a read bound");
539 let crate::channel::Reception::Frame(first) =
540 crate::channel::read(&mut stream).expect("the first frame")
541 else {
542 panic!("a painted frame arrives");
543 };
544 assert_eq!((first.width, first.height), (2, 1));
545
546 sink.offer(&four, 4, 1, (2, 1));
547 let crate::channel::Reception::Frame(resized) =
548 crate::channel::read(&mut stream).expect("the resized frame")
549 else {
550 panic!("a resize sends inside the interval");
551 };
552 assert_eq!((resized.width, resized.height), (4, 1));
553 assert!(resized.key, "and the resize is a key frame");
554
555 sink.offer(&four, 4, 1, (2, 1));
556 assert!(
557 matches!(
558 crate::channel::read(&mut stream).expect("a same-size frame"),
559 crate::channel::Reception::Frame(_)
560 ),
561 "a same-size frame inside the interval is written, not dropped"
562 );
563 std::fs::remove_dir_all(&dir).expect("the temp dir goes");
564 }
565
566 #[cfg(target_os = "macos")]
573 #[test]
574 fn a_lossy_frame_takes_the_fastest_effort_and_a_key_frame_does_not() {
575 let lossy = super::config(83, false).expect("a config");
578 assert_eq!(lossy.method, 0, "a lossy frame takes the fastest effort");
579 assert_eq!(lossy.quality, 83.0, "the configured quality is the one encoded");
580 assert_eq!(lossy.lossless, 0, "a frame between key frames is lossy");
581 assert_eq!(lossy.alpha_compression, 1, "a lossy frame compresses alpha, as it always did");
582 let key = super::config(83, true).expect("a config");
583 let libwebp = webp::WebPConfig::new().expect("a config");
584 assert_eq!(key.lossless, 1, "a surface's first frame is lossless");
585 assert_eq!(key.method, libwebp.method, "a key frame keeps libwebp's default effort");
586 assert_eq!(
587 key.quality, 83.0,
588 "a key frame keeps the configured quality: VP8L searches on it"
589 );
590 assert_eq!(
594 key.alpha_compression, 0,
595 "a lossless frame keeps encode_simple's alpha setting"
596 );
597 }
598
599 #[cfg(target_os = "macos")]
612 #[test]
613 fn a_bgra_view_encodes_to_webp() {
614 const WIDTH: u32 = 128;
619 const HEIGHT: u32 = 80;
620 let mut bgra = vec![0_u8; (WIDTH * HEIGHT * 4) as usize];
621 for (at, pixel) in bgra.chunks_mut(4).enumerate() {
622 let x = u32::try_from(at).expect("an index") % WIDTH;
623 let y = u32::try_from(at).expect("an index") / WIDTH;
624 let base = if (x / 8 + y / 8).is_multiple_of(2) { 24 } else { 220 };
625 let value = if y.is_multiple_of(7) { 40 } else { base };
626 pixel[0] = value;
627 pixel[1] = value / 2;
628 pixel[2] = 255 - value;
629 pixel[3] = 255;
630 }
631 let mut rgba = bgra.clone();
632 for pixel in rgba.chunks_mut(4) {
633 pixel.swap(0, 2);
634 }
635 let at_effort = |method: i32| {
638 let mut reference = webp::WebPConfig::new().expect("a config");
639 reference.lossless = 0;
640 reference.alpha_compression = 1;
641 reference.quality = 83.0;
642 reference.method = method;
643 webp::Encoder::from_rgba(&rgba, WIDTH, HEIGHT)
644 .encode_advanced(&reference)
645 .expect("a reference encode")
646 .to_vec()
647 };
648 let fast = at_effort(0);
649 let default = at_effort(4);
650 assert!(
655 fast != default,
656 "the control: the efforts must disagree on this buffer, or the oracle is blind \
657 ({} B fast, {} B default)",
658 fast.len(),
659 default.len(),
660 );
661
662 let lossy = super::encode_webp(&bgra, WIDTH, HEIGHT, 83, false).expect("a lossy frame");
663 assert_eq!(&lossy[..4], b"RIFF");
664 assert_eq!(&lossy[8..12], b"WEBP");
665 assert!(lossy.windows(4).any(|window| window == b"VP8 "), "lossy uses VP8");
666 assert!(
667 lossy == fast,
668 "the lossy frame is the fast-effort encode, not the default's \
669 ({} B encoded, {} B fast, {} B default)",
670 lossy.len(),
671 fast.len(),
672 default.len(),
673 );
674
675 let key = super::encode_webp(&bgra, WIDTH, HEIGHT, 83, true).expect("a key frame");
676 assert!(key.windows(4).any(|window| window == b"VP8L"), "key frames are lossless");
677 assert!(super::encode_webp(&bgra, 0, 1, 83, false).is_none(), "no view, no frame");
678 }
679}