@desklink/host 0.5.15 → 0.5.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/PROTOCOL.md CHANGED
@@ -187,7 +187,9 @@ video itself (see *Encoded sources*): a consumer reads it rather than inferring
187
187
  support from the engine's version string.
188
188
 
189
189
  `input.mechanism` names the build's portal input path, `inputtino/uinput`,
190
- where the engine creates its own virtual devices. A session opened against an X
190
+ where the engine creates its own virtual devices, or `portal-remotedesktop` only when
191
+ `/dev/uinput` is unavailable and the session portal advertises a keyboard (keys
192
+ only; `pointer` and `wheel` are then `false`). A session opened against an X
191
193
  display applies input through XTest instead, where the X server applies the
192
194
  events and nothing the engine does can reach another session's keyboard.
193
195
  `input.layout` is the keymap the engine compiled for typing, taken from the
package/engine/Cargo.lock CHANGED
@@ -536,7 +536,7 @@ checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
536
536
 
537
537
  [[package]]
538
538
  name = "desklink-host"
539
- version = "0.5.15"
539
+ version = "0.5.16"
540
540
  dependencies = [
541
541
  "anyhow",
542
542
  "ashpd",
package/engine/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "desklink-host"
3
- version = "0.5.15"
3
+ version = "0.5.16"
4
4
  edition = "2021"
5
5
  rust-version = "1.97.1"
6
6
  license = "Apache-2.0"
@@ -606,82 +606,3 @@ struct StreamState {
606
606
 
607
607
  // PipeWire callbacks only touch this from the capture thread.
608
608
  unsafe impl Send for StreamState {}
609
-
610
- #[cfg(test)]
611
- mod tests {
612
- use super::*;
613
- use std::time::Duration;
614
-
615
- #[test]
616
- fn layer_mask_keeps_click_center_and_removes_ring_and_edge() {
617
- let position = Arc::new(Mutex::new(Some((50, 50))));
618
- let mut mask = IndicatorMask::new(position.clone());
619
- mask.apply(&mut vec![1; 100 * 100 * 4], 100, 100, 100, 100);
620
- let mut frame = vec![2; 100 * 100 * 4];
621
- mask.apply(&mut frame, 100, 100, 100, 100);
622
- assert_eq!(frame[(50 * 100 + 50) * 4], 2);
623
- assert_eq!(frame[(50 * 100 + 60) * 4], 1);
624
- assert_eq!(frame[0], 1);
625
- *position.lock().unwrap() = Some((75, 50));
626
- let mut moved = vec![3; 100 * 100 * 4];
627
- mask.apply(&mut moved, 100, 100, 100, 100);
628
- assert_eq!(moved[(50 * 100 + 60) * 4], 1, "eased path is masked");
629
- assert_eq!(moved[(50 * 100 + 75) * 4], 3, "click center remains live");
630
- }
631
-
632
- #[test]
633
- fn dropping_capture_stops_and_joins_its_live_pipewire_loop() {
634
- // Exercise the real event-loop/channel lifetime without a portal,
635
- // compositor, screen capture or input device.
636
- let (quit, stop) = pw::channel::channel();
637
- let (ready_tx, ready_rx) = mpsc::channel();
638
- let (ended_tx, ended_rx) = mpsc::channel();
639
- let thread = std::thread::spawn(move || {
640
- pw::init();
641
- let main_loop = pw::main_loop::MainLoopRc::new(None).unwrap();
642
- let _stop = stop.attach(main_loop.loop_(), {
643
- let main_loop = main_loop.clone();
644
- move |_| main_loop.quit()
645
- });
646
- let timer = main_loop.loop_().add_timer(move |_| {
647
- let _ = ready_tx.send(());
648
- });
649
- timer
650
- .update_timer(Some(Duration::from_millis(1)), None)
651
- .into_result()
652
- .unwrap();
653
- main_loop.run();
654
- });
655
- let capture = Capture {
656
- quit,
657
- thread: Some(thread),
658
- source: SelectedSource {
659
- node_id: 0,
660
- width: 2,
661
- height: 2,
662
- position: None,
663
- source_type: None,
664
- origin_x: 0,
665
- origin_y: 0,
666
- },
667
- cursor: None,
668
- geometry: Arc::new(Mutex::new(None)),
669
- frames: Arc::new(AtomicU64::new(0)),
670
- dropped: Arc::new(AtomicU64::new(0)),
671
- encoded_width: 2,
672
- encoded_height: 2,
673
- };
674
- ready_rx
675
- .recv_timeout(Duration::from_secs(2))
676
- .expect("the loop is running");
677
- // Keep a broken join bounded so a missing stop is a failed check rather
678
- // than a test process that hangs forever.
679
- std::thread::spawn(move || {
680
- drop(capture);
681
- ended_tx.send(()).unwrap();
682
- });
683
- ended_rx
684
- .recv_timeout(Duration::from_secs(2))
685
- .expect("capture stopped and joined");
686
- }
687
- }
@@ -263,51 +263,3 @@ impl std::fmt::Debug for Encoder {
263
263
  )
264
264
  }
265
265
  }
266
-
267
- #[cfg(test)]
268
- mod tests {
269
- use super::*;
270
-
271
- fn blank(width: usize, height: usize) -> I420 {
272
- I420 {
273
- width,
274
- height,
275
- data: vec![128u8; width * height + 2 * (width / 2) * (height / 2)],
276
- }
277
- }
278
-
279
- #[test]
280
- fn encodes_successive_frames_and_refuses_a_mismatched_geometry() {
281
- let (w, h) = (64usize, 64usize);
282
- let mut encoder = Encoder::new(w, h, 500, 30, 2).expect("encoder should start");
283
- assert!(!encoder
284
- .encode(&blank(w, h), true, false)
285
- .unwrap()
286
- .data
287
- .is_empty());
288
- assert!(!encoder
289
- .encode(&blank(w, h), false, false)
290
- .unwrap()
291
- .data
292
- .is_empty());
293
- assert!(encoder.encode(&blank(32, 32), false, false).is_err());
294
- }
295
-
296
- #[test]
297
- fn a_half_size_run_starts_and_ends_on_a_key_frame() {
298
- let (w, h) = (1280usize, 720usize);
299
- let mut encoder = Encoder::new(w, h, 2000, 60, 2).expect("encoder should start");
300
- assert!(encoder.encode(&blank(w, h), true, false).unwrap().keyframe);
301
- assert!(!encoder.encode(&blank(w, h), false, false).unwrap().keyframe);
302
- assert!(
303
- encoder.encode(&blank(w, h), false, true).unwrap().keyframe,
304
- "size down"
305
- );
306
- assert!(!encoder.encode(&blank(w, h), false, true).unwrap().keyframe);
307
- assert!(
308
- encoder.refine(&blank(w, h)).unwrap().keyframe,
309
- "size back up"
310
- );
311
- assert!(!encoder.encode(&blank(w, h), false, false).unwrap().keyframe);
312
- }
313
- }
@@ -141,168 +141,3 @@ impl Drop for Encoder {
141
141
  unsafe { dl_nvenc_destroy(self.native) };
142
142
  }
143
143
  }
144
-
145
- #[cfg(test)]
146
- mod tests {
147
- use super::*;
148
- use crate::h264::Backend;
149
-
150
- /// Fine detail, like text, scrolled sideways by `t`.
151
- fn pattern(width: usize, height: usize, t: usize) -> I420 {
152
- let mut data = vec![128u8; width * height * 3 / 2];
153
- for y in 0..height {
154
- for x in 0..width {
155
- data[y * width + x] = ((x + 4 * t) ^ (3 * y)) as u8;
156
- }
157
- }
158
- I420 {
159
- width,
160
- height,
161
- data,
162
- }
163
- }
164
-
165
- fn nal_types(au: &[u8]) -> Vec<u8> {
166
- let mut types = Vec::new();
167
- let mut i = 0;
168
- while i + 3 < au.len() {
169
- if au[i..i + 3] == [0, 0, 1] {
170
- types.push(au[i + 3] & 0x1f);
171
- i += 3;
172
- } else {
173
- i += 1;
174
- }
175
- }
176
- types
177
- }
178
-
179
- fn open(width: usize, height: usize) -> Option<Encoder> {
180
- match Encoder::open(width, height, 4000, 30) {
181
- Ok(encoder) => Some(encoder),
182
- Err(error) => {
183
- eprintln!("skipping: NVENC unavailable: {error:#}");
184
- None
185
- }
186
- }
187
- }
188
-
189
- #[test]
190
- fn idr_on_request_only_and_refine_is_inter() {
191
- let (w, h) = (640, 480);
192
- let Some(mut encoder) = open(w, h) else {
193
- return;
194
- };
195
- let first = encoder.encode(&pattern(w, h, 0), false).unwrap();
196
- assert!(first.data.starts_with(&[0, 0, 0, 1]), "Annex-B");
197
- assert_eq!(nal_types(&first.data), [7, 8, 5]);
198
- assert!(first.keyframe);
199
-
200
- let next = encoder.encode(&pattern(w, h, 1), false).unwrap();
201
- assert_eq!(nal_types(&next.data), [1]);
202
- assert!(!next.keyframe);
203
-
204
- let forced = encoder.encode(&pattern(w, h, 2), true).unwrap();
205
- assert_eq!(nal_types(&forced.data), [7, 8, 5]);
206
- assert!(forced.keyframe);
207
-
208
- let still = pattern(w, h, 3);
209
- let motion = encoder.encode(&still, false).unwrap();
210
- let refined = encoder.refine(&still).unwrap();
211
- assert_eq!(nal_types(&refined.data), [1]);
212
- assert!(!refined.keyframe);
213
- eprintln!(
214
- "motion frame {} bytes, refinement {} bytes",
215
- motion.data.len(),
216
- refined.data.len()
217
- );
218
-
219
- encoder.set_bitrate(1500).unwrap();
220
- let after = encoder.encode(&pattern(w, h, 4), false).unwrap();
221
- assert_eq!(nal_types(&after.data), [1]);
222
- assert!(!after.keyframe);
223
-
224
- assert!(encoder.encode(&pattern(320, 240, 0), false).is_err());
225
- }
226
-
227
- #[test]
228
- fn the_refinement_ceiling_is_lifted_for_the_motion_after_it() {
229
- let (w, h) = (640, 480);
230
- let Ok(mut encoder) = Encoder::open(w, h, 300, 30) else {
231
- return;
232
- };
233
- // Fresh detail every frame, which no reference predicts: the rate
234
- // control, not motion search, decides these sizes.
235
- let noise = |t: usize| {
236
- let mut frame = pattern(w, h, 0);
237
- for (i, pixel) in frame.data[..w * h].iter_mut().enumerate() {
238
- *pixel = ((i ^ t).wrapping_mul(2_654_435_761) >> 13) as u8;
239
- }
240
- frame
241
- };
242
- let coded = |encoder: &mut Encoder, from: usize| -> usize {
243
- (from..from + 10)
244
- .map(|t| encoder.encode(&noise(t), false).unwrap().data.len())
245
- .sum()
246
- };
247
- coded(&mut encoder, 0);
248
- let before = coded(&mut encoder, 10);
249
- encoder.refine(&noise(19)).unwrap();
250
- let after = coded(&mut encoder, 20);
251
- assert!(
252
- after < before * 2,
253
- "motion after a refinement is back under the rate target: {before} then {after} bytes"
254
- );
255
- }
256
-
257
- #[test]
258
- fn sizes_off_the_macroblock_grid_open_and_code() {
259
- for (w, h) in [(1366, 768), (1918, 1078)] {
260
- let Some(mut encoder) = open(w, h) else {
261
- return;
262
- };
263
- assert!(encoder.encode(&pattern(w, h, 0), false).unwrap().keyframe);
264
- assert!(!encoder.encode(&pattern(w, h, 1), false).unwrap().keyframe);
265
- }
266
- }
267
-
268
- /// Not a check: writes `$DESKLINK_NVENC_DUMP` (60 frames at 1282x722, an
269
- /// IDR at 30 and a refinement at 45) for an external decoder, and prints
270
- /// the mean encode time at 1080p and 4K.
271
- #[test]
272
- #[ignore]
273
- fn dump_and_time() {
274
- use std::time::Instant;
275
- if let Ok(path) = std::env::var("DESKLINK_NVENC_DUMP") {
276
- let (w, h) = (1282, 722);
277
- let Some(mut encoder) = Encoder::open(w, h, 1500, 30).ok() else {
278
- return;
279
- };
280
- let mut out = Vec::new();
281
- for t in 0..60 {
282
- let frame = pattern(w, h, t.min(44));
283
- let coded = match t {
284
- 45 => encoder.refine(&frame),
285
- _ => encoder.encode(&frame, t == 30),
286
- }
287
- .unwrap();
288
- if (44..=46).contains(&t) {
289
- eprintln!("frame {t}: {} bytes", coded.data.len());
290
- }
291
- out.extend_from_slice(&coded.data);
292
- }
293
- std::fs::write(path, out).unwrap();
294
- }
295
- for (w, h) in [(1920, 1080), (3840, 2160)] {
296
- let Some(mut encoder) = Encoder::open(w, h, 20_000, 60).ok() else {
297
- return;
298
- };
299
- let frames: Vec<_> = (0..4).map(|t| pattern(w, h, t)).collect();
300
- encoder.encode(&frames[0], true).unwrap();
301
- let start = Instant::now();
302
- for t in 0..60 {
303
- encoder.encode(&frames[t % 4], false).unwrap();
304
- }
305
- eprintln!("{w}x{h}: {:?} per frame", start.elapsed() / 60);
306
- }
307
- }
308
- }
@@ -488,248 +488,3 @@ fn http_get(path: &str) -> Result<Vec<u8>> {
488
488
  fn hex(bytes: &[u8]) -> String {
489
489
  bytes.iter().map(|b| format!("{b:02x}")).collect()
490
490
  }
491
-
492
- #[cfg(test)]
493
- mod tests {
494
- use super::*;
495
- use crate::h264::Backend;
496
- use openh264_sys2::SLayerBSInfo;
497
-
498
- #[test]
499
- fn malformed_bitstream_headers_are_rejected_before_any_slice() {
500
- fn info(layer_count: c_int, layers: &[SLayerBSInfo], frame_size: c_int) -> SFrameBSInfo {
501
- let mut info = SFrameBSInfo::default();
502
- info.iLayerNum = layer_count;
503
- info.sLayerInfo[..layers.len()].copy_from_slice(layers);
504
- info.iFrameSizeInBytes = frame_size;
505
- info
506
- }
507
- fn layer(nals: &mut [c_int], buf: *mut u8) -> SLayerBSInfo {
508
- SLayerBSInfo {
509
- iNalCount: nals.len() as c_int,
510
- pNalLengthInByte: nals.as_mut_ptr(),
511
- pBsBuf: buf,
512
- ..Default::default()
513
- }
514
- }
515
- let some_buf = NonNull::<u8>::dangling().as_ptr();
516
- // Well-formed headers pass through with their sizes.
517
- let (mut a, mut b) = ([10, 20], [5]);
518
- let ok = info(
519
- 2,
520
- &[layer(&mut a, some_buf), layer(&mut b, some_buf)],
521
- 36,
522
- );
523
- assert_eq!(validated_layer_sizes(&ok).unwrap(), [30, 5]);
524
- // A count past the array is clamped, not read past.
525
- assert_eq!(validated_layer_sizes(&info(500, &[], 0)).unwrap().len(), 128);
526
- assert!(validated_layer_sizes(&info(-1, &[], 0)).unwrap().is_empty());
527
- // Every malformation fails instead of building a slice.
528
- let mut bad = [-1];
529
- assert!(validated_layer_sizes(&info(1, &[layer(&mut bad, some_buf)], 100)).is_err());
530
- let null_lengths = SLayerBSInfo {
531
- iNalCount: 2,
532
- pNalLengthInByte: std::ptr::null_mut(),
533
- pBsBuf: some_buf,
534
- ..Default::default()
535
- };
536
- assert!(validated_layer_sizes(&info(1, &[null_lengths], 100)).is_err());
537
- let mut negative = [10, -3];
538
- assert!(validated_layer_sizes(&info(1, &[layer(&mut negative, some_buf)], 100)).is_err());
539
- let mut no_buf = [10];
540
- assert!(
541
- validated_layer_sizes(&info(1, &[layer(&mut no_buf, std::ptr::null_mut())], 100)).is_err()
542
- );
543
- let mut over = [60, 50];
544
- assert!(validated_layer_sizes(&info(1, &[layer(&mut over, some_buf)], 100)).is_err());
545
- }
546
-
547
- /// A diagonal gradient that moves with `t`, so inter frames carry motion.
548
- fn pattern(width: usize, height: usize, t: usize) -> I420 {
549
- let mut data = vec![128u8; width * height * 3 / 2];
550
- for y in 0..height {
551
- for x in 0..width {
552
- data[y * width + x] = ((x + y + t * 4) % 256) as u8;
553
- }
554
- }
555
- // A moving box, for edges the encoder has to follow.
556
- let (bx, by) = ((t * 7) % (width.saturating_sub(64).max(1)), height / 3);
557
- for y in by..(by + 64).min(height) {
558
- for x in bx..(bx + 64).min(width) {
559
- data[y * width + x] = 16;
560
- }
561
- }
562
- I420 {
563
- width,
564
- height,
565
- data,
566
- }
567
- }
568
-
569
- /// Each NAL unit of an Annex-B access unit, from its header byte on.
570
- fn nals(au: &[u8]) -> Vec<&[u8]> {
571
- let starts: Vec<usize> = (0..au.len().saturating_sub(3))
572
- .filter(|&i| au[i..i + 3] == [0, 0, 1])
573
- .map(|i| i + 3)
574
- .collect();
575
- starts
576
- .iter()
577
- .enumerate()
578
- .map(|(k, &s)| &au[s..starts.get(k + 1).map_or(au.len(), |&n| n - 3)])
579
- .collect()
580
- }
581
-
582
- fn nal_types(au: &[u8]) -> Vec<u8> {
583
- nals(au).iter().map(|n| n[0] & 0x1f).collect()
584
- }
585
-
586
- fn open(width: usize, height: usize) -> Option<Encoder> {
587
- match Encoder::open(width, height, 4000, 30) {
588
- Ok(encoder) => Some(encoder),
589
- Err(error) => {
590
- eprintln!("skipping: openh264 unavailable ({error:#})");
591
- None
592
- }
593
- }
594
- }
595
-
596
- #[test]
597
- fn meets_the_stream_contract() {
598
- let Some(mut encoder) = open(640, 480) else {
599
- return;
600
- };
601
- let first = encoder.encode(&pattern(640, 480, 0), false).unwrap();
602
- assert!(first.keyframe);
603
- assert!(first.data.starts_with(&[0, 0, 0, 1]));
604
- let types = nal_types(&first.data);
605
- let (sps, pps, idr) = (
606
- types.iter().position(|&t| t == 7).unwrap(),
607
- types.iter().position(|&t| t == 8).unwrap(),
608
- types.iter().position(|&t| t == 5).unwrap(),
609
- );
610
- assert!(sps < pps && pps < idr, "{types:?}");
611
- let sps = nals(&first.data)[sps];
612
- assert_eq!(sps[1], 66, "profile_idc must be Baseline");
613
- assert!(sps[2] & 0x40 != 0, "constraint_set1: Constrained Baseline");
614
-
615
- let next = encoder.encode(&pattern(640, 480, 1), false).unwrap();
616
- assert!(!next.keyframe);
617
- assert!(
618
- nal_types(&next.data).iter().all(|&t| t == 1),
619
- "{:?}",
620
- nal_types(&next.data)
621
- );
622
-
623
- let forced = encoder.encode(&pattern(640, 480, 2), true).unwrap();
624
- assert!(forced.keyframe);
625
- let types = nal_types(&forced.data);
626
- assert!(
627
- types.contains(&7) && types.contains(&8) && types.contains(&5),
628
- "{types:?}"
629
- );
630
-
631
- let refined = encoder.refine(&pattern(640, 480, 2)).unwrap();
632
- assert!(!refined.keyframe);
633
- assert!(!nal_types(&refined.data).contains(&5));
634
-
635
- encoder.set_bitrate(1000).unwrap();
636
- let after = encoder.encode(&pattern(640, 480, 3), false).unwrap();
637
- assert!(!after.keyframe);
638
-
639
- // A still frame still produces an access unit: frame skipping is off.
640
- for _ in 0..5 {
641
- assert!(!encoder
642
- .encode(&pattern(640, 480, 3), false)
643
- .unwrap()
644
- .data
645
- .is_empty());
646
- }
647
- assert!(encoder.encode(&pattern(320, 240, 0), false).is_err());
648
- }
649
-
650
- #[test]
651
- fn a_window_switch_is_coded_without_an_unasked_idr() {
652
- let (w, h) = (1280, 720);
653
- let Some(mut encoder) = open(w, h) else {
654
- return;
655
- };
656
- // Four different text-like windows, ten frames each.
657
- let window = |seed: usize| {
658
- let mut frame = pattern(w, h, 0);
659
- for y in 0..h {
660
- for x in 0..w {
661
- let on = (x / (3 + seed) + y / (5 + seed)) % 3 == 0;
662
- frame.data[y * w + x] = if on { 20 } else { 235 };
663
- }
664
- }
665
- frame
666
- };
667
- let idrs: Vec<usize> = (0..40)
668
- .filter(|&t| encoder.encode(&window(t / 10), false).unwrap().keyframe)
669
- .collect();
670
- assert_eq!(idrs, [0], "an IDR only for the first frame");
671
- }
672
-
673
- #[test]
674
- fn codes_sizes_that_are_not_macroblock_multiples() {
675
- for (w, h) in [(1366, 768), (1918, 1078)] {
676
- let Some(mut encoder) = open(w, h) else {
677
- return;
678
- };
679
- assert!(encoder.encode(&pattern(w, h, 0), false).unwrap().keyframe);
680
- assert!(!encoder.encode(&pattern(w, h, 1), false).unwrap().keyframe);
681
- }
682
- }
683
-
684
- /// Writes a clip for out-of-band decoding: `DESKLINK_OPENH264_CLIP=path`.
685
- #[test]
686
- #[ignore]
687
- fn writes_a_clip() {
688
- let path = std::env::var("DESKLINK_OPENH264_CLIP").unwrap();
689
- let (w, h) = (1366, 768);
690
- // Low enough that rate control codes above the refinement ceiling.
691
- let mut encoder = Encoder::open(w, h, 300, 30).expect("openh264 must be in place");
692
- let mut out = Vec::new();
693
- for t in 0..60 {
694
- let frame = pattern(w, h, t);
695
- let au = if t == 40 {
696
- encoder.refine(&pattern(w, h, 39)).unwrap()
697
- } else {
698
- encoder.encode(&frame, t == 30).unwrap()
699
- };
700
- out.extend_from_slice(&au.data);
701
- }
702
- std::fs::write(path, out).unwrap();
703
- }
704
-
705
- /// Average encode time over 60 moving frames.
706
- #[test]
707
- #[ignore]
708
- fn times_large_frames() {
709
- for (w, h) in [(1920, 1080), (3840, 2160)] {
710
- let mut encoder = open(w, h).expect("openh264 must be in place");
711
- let frames: Vec<_> = (0..60).map(|t| pattern(w, h, t)).collect();
712
- let start = Instant::now();
713
- let mut bytes = 0;
714
- for (t, frame) in frames.iter().enumerate() {
715
- bytes += encoder.encode(frame, t == 0).unwrap().data.len();
716
- }
717
- eprintln!(
718
- "{w}x{h}: {:.1} ms/frame, {} KiB/frame",
719
- start.elapsed().as_secs_f64() * 1000.0 / 60.0,
720
- bytes / 60 / 1024
721
- );
722
- }
723
- }
724
-
725
- /// Hits the network: `cargo test fetches -- --ignored` with a scratch
726
- /// `XDG_CACHE_HOME`.
727
- #[test]
728
- #[ignore]
729
- fn fetches_from_cisco() {
730
- assert!(fetch_once());
731
- let path = cached().unwrap();
732
- assert!(path.exists());
733
- assert!(Encoder::open(320, 240, 500, 30).is_ok());
734
- }
735
- }
@@ -164,100 +164,3 @@ impl Drop for Encoder {
164
164
  unsafe { dl_vaapi_destroy(self.native) };
165
165
  }
166
166
  }
167
-
168
- #[cfg(test)]
169
- mod tests {
170
- use super::*;
171
- use crate::h264::Backend;
172
-
173
- /// A diagonal gradient shifted by `t`, so successive frames differ.
174
- fn pattern(width: usize, height: usize, t: usize) -> I420 {
175
- let mut data = vec![128u8; width * height * 3 / 2];
176
- for y in 0..height {
177
- for x in 0..width {
178
- data[y * width + x] = ((x + y + 4 * t) & 0xff) as u8;
179
- }
180
- }
181
- I420 {
182
- width,
183
- height,
184
- data,
185
- }
186
- }
187
-
188
- /// NAL unit types in Annex-B order.
189
- fn nal_types(data: &[u8]) -> Vec<u8> {
190
- let mut types = Vec::new();
191
- let mut i = 0;
192
- while i + 3 < data.len() {
193
- if data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1 {
194
- types.push(data[i + 3] & 0x1f);
195
- i += 3;
196
- } else {
197
- i += 1;
198
- }
199
- }
200
- types
201
- }
202
-
203
- fn open(width: usize, height: usize) -> Option<Encoder> {
204
- match Encoder::open(width, height, 4000, 30) {
205
- Ok(encoder) => Some(encoder),
206
- Err(error) => {
207
- eprintln!("skipping: VA-API H.264 unavailable: {error:#}");
208
- None
209
- }
210
- }
211
- }
212
-
213
- fn slices(types: &[u8]) -> Vec<u8> {
214
- types
215
- .iter()
216
- .copied()
217
- .filter(|t| !matches!(t, 6 | 9))
218
- .collect()
219
- }
220
-
221
- #[test]
222
- fn codes_idr_then_p_frames_and_refines() {
223
- let (w, h) = (640, 480);
224
- let Some(mut encoder) = open(w, h) else {
225
- return;
226
- };
227
- let first = encoder.encode(&pattern(w, h, 0), false).unwrap();
228
- assert!(first.keyframe);
229
- assert!(first.data.starts_with(&[0, 0, 0, 1]));
230
- assert_eq!(slices(&nal_types(&first.data))[..3], [7, 8, 5]);
231
- let next = encoder.encode(&pattern(w, h, 1), false).unwrap();
232
- assert!(!next.keyframe);
233
- assert_eq!(slices(&nal_types(&next.data)), [1]);
234
- let forced = encoder.encode(&pattern(w, h, 2), true).unwrap();
235
- assert!(forced.keyframe);
236
- assert_eq!(slices(&nal_types(&forced.data))[..3], [7, 8, 5]);
237
- let motion = encoder.encode(&pattern(w, h, 3), false).unwrap();
238
- let refined = encoder.refine(&pattern(w, h, 3)).unwrap();
239
- assert!(!refined.keyframe);
240
- assert_eq!(slices(&nal_types(&refined.data)), [1]);
241
- eprintln!(
242
- "motion frame {} bytes, refinement {} bytes",
243
- motion.data.len(),
244
- refined.data.len()
245
- );
246
- encoder.set_bitrate(1000).unwrap();
247
- let slower = encoder.encode(&pattern(w, h, 4), false).unwrap();
248
- assert!(!slower.keyframe);
249
- assert_eq!(slices(&nal_types(&slower.data)), [1]);
250
- assert!(encoder.encode(&pattern(320, 240, 5), false).is_err());
251
- }
252
-
253
- #[test]
254
- fn codes_sizes_off_the_macroblock_grid() {
255
- for (w, h) in [(1366, 768), (1918, 1078)] {
256
- let Some(mut encoder) = open(w, h) else {
257
- return;
258
- };
259
- assert!(encoder.encode(&pattern(w, h, 0), false).unwrap().keyframe);
260
- assert!(!encoder.encode(&pattern(w, h, 1), false).unwrap().keyframe);
261
- }
262
- }
263
- }