@desklink/host 0.5.14 → 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/README.md +6 -1
- package/bin/desklink-host.mjs +9 -3
- package/dist/doctor.d.ts +2 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +220 -0
- package/dist/doctor.js.map +1 -0
- package/dist/engineProcess.d.ts +5 -0
- package/dist/engineProcess.d.ts.map +1 -1
- package/dist/engineProcess.js +15 -5
- package/dist/engineProcess.js.map +1 -1
- package/docs/PROTOCOL.md +4 -2
- package/engine/Cargo.lock +1 -1
- package/engine/Cargo.toml +1 -1
- package/engine/native/mac_stream.mm +4 -1
- package/engine/src/capture.rs +0 -79
- package/engine/src/diagnose.rs +99 -0
- package/engine/src/encoder.rs +0 -48
- package/engine/src/h264_nvenc.rs +0 -165
- package/engine/src/h264_openh264.rs +0 -245
- package/engine/src/h264_vaapi.rs +0 -97
- package/engine/src/indicator.rs +0 -101
- package/engine/src/input_portal.rs +337 -0
- package/engine/src/mac.rs +229 -6
- package/engine/src/main.rs +7 -88
- package/engine/src/observe.rs +0 -33
- package/engine/src/peer.rs +0 -433
- package/engine/src/portal.rs +0 -75
- package/engine/src/session.rs +112 -1085
- package/examples/reference-client.html +13 -0
- package/package.json +3 -3
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
//! `desklink-host diagnose`: the machine facts behind `desklink-host doctor`.
|
|
2
|
+
//!
|
|
3
|
+
//! Every check here is a question, not an action: the X server is connected to
|
|
4
|
+
//! and read, the portal's version properties are fetched, and an encoder codes
|
|
5
|
+
//! one blank 320x240 frame. Nothing opens a portal session, asks for consent,
|
|
6
|
+
//! captures a desktop or creates an input device. `doctor` in the Node CLI adds
|
|
7
|
+
//! the package facts and decides which answer a person needs to act on.
|
|
8
|
+
|
|
9
|
+
use anyhow::Result;
|
|
10
|
+
use serde_json::{json, Value};
|
|
11
|
+
|
|
12
|
+
pub async fn run() -> Result<()> {
|
|
13
|
+
let session = if std::env::var_os("WAYLAND_DISPLAY").is_some() {
|
|
14
|
+
"wayland"
|
|
15
|
+
} else if std::env::var_os("DISPLAY").is_some() {
|
|
16
|
+
"x11"
|
|
17
|
+
} else {
|
|
18
|
+
"none"
|
|
19
|
+
};
|
|
20
|
+
let x11 = if session == "x11" { probe_x11() } else { Value::Null };
|
|
21
|
+
let wayland = if session == "wayland" {
|
|
22
|
+
probe_wayland().await
|
|
23
|
+
} else {
|
|
24
|
+
Value::Null
|
|
25
|
+
};
|
|
26
|
+
// `prepare` rather than `probe`: on a machine with no hardware encoder the
|
|
27
|
+
// engine fetches Cisco's openh264 once at first need, so that is the answer
|
|
28
|
+
// a session would get. It is a bounded download, not a session or a prompt.
|
|
29
|
+
let encoder = crate::h264::prepare().map(|info| {
|
|
30
|
+
json!({ "name": info.encoder, "hardware": info.hardware })
|
|
31
|
+
});
|
|
32
|
+
println!(
|
|
33
|
+
"{}",
|
|
34
|
+
serde_json::to_string(&json!({
|
|
35
|
+
"version": env!("CARGO_PKG_VERSION"),
|
|
36
|
+
"session": session,
|
|
37
|
+
"x11": x11,
|
|
38
|
+
"wayland": wayland,
|
|
39
|
+
"encoder": encoder,
|
|
40
|
+
"uinput": crate::input::probe().is_ok(),
|
|
41
|
+
}))?
|
|
42
|
+
);
|
|
43
|
+
Ok(())
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// Connect exactly as the capture backend would, then report the two
|
|
47
|
+
/// extensions it wants and XTest for input.
|
|
48
|
+
fn probe_x11() -> Value {
|
|
49
|
+
match crate::x11::X11Desktop::connect(None) {
|
|
50
|
+
Ok(desktop) => json!({
|
|
51
|
+
"connected": true,
|
|
52
|
+
"display": std::env::var("DISPLAY").ok(),
|
|
53
|
+
"width": desktop.width,
|
|
54
|
+
"height": desktop.height,
|
|
55
|
+
"shm": desktop.capture_path() == "shm",
|
|
56
|
+
"damage": desktop.damage_armed(),
|
|
57
|
+
"xtest": desktop.input_available(),
|
|
58
|
+
}),
|
|
59
|
+
Err(error) => json!({ "connected": false, "error": format!("{error:#}") }),
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// Ask the session portal whether its ScreenCast and RemoteDesktop interfaces
|
|
64
|
+
/// are there. Building these proxies reads the interface's `version` property;
|
|
65
|
+
/// it creates no session and cannot prompt. The PipeWire socket is the capture
|
|
66
|
+
/// backend's own half of a screen share.
|
|
67
|
+
async fn probe_wayland() -> Value {
|
|
68
|
+
let screencast = ashpd::desktop::screencast::Screencast::new().await;
|
|
69
|
+
let remote_desktop = ashpd::desktop::remote_desktop::RemoteDesktop::new().await;
|
|
70
|
+
json!({
|
|
71
|
+
"screencast": screencast.is_ok(),
|
|
72
|
+
"screencast_error": screencast.err().map(|error| error.to_string()),
|
|
73
|
+
"remote_desktop": remote_desktop.is_ok(),
|
|
74
|
+
"remote_desktop_error": remote_desktop.err().map(|error| error.to_string()),
|
|
75
|
+
"pipewire": pipewire_socket(),
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// Whether the session PipeWire socket is present. A portal screencast hands
|
|
80
|
+
/// back a remote on this socket, so a portal without one cannot deliver frames.
|
|
81
|
+
fn pipewire_socket() -> bool {
|
|
82
|
+
let runtime = std::env::var_os("PIPEWIRE_RUNTIME_DIR")
|
|
83
|
+
.filter(|path| !path.is_empty())
|
|
84
|
+
.or_else(|| std::env::var_os("XDG_RUNTIME_DIR"))
|
|
85
|
+
.map(std::path::PathBuf::from);
|
|
86
|
+
let Some(directory) = runtime else {
|
|
87
|
+
return false;
|
|
88
|
+
};
|
|
89
|
+
let remote = std::env::var("PIPEWIRE_REMOTE")
|
|
90
|
+
.ok()
|
|
91
|
+
.filter(|remote| !remote.is_empty())
|
|
92
|
+
.unwrap_or_else(|| String::from("pipewire-0"));
|
|
93
|
+
let remote = std::path::Path::new(&remote);
|
|
94
|
+
if remote.is_absolute() {
|
|
95
|
+
remote.exists()
|
|
96
|
+
} else {
|
|
97
|
+
directory.join(remote).exists()
|
|
98
|
+
}
|
|
99
|
+
}
|
package/engine/src/encoder.rs
CHANGED
|
@@ -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
|
-
}
|
package/engine/src/h264_nvenc.rs
CHANGED
|
@@ -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
|
-
}
|
package/engine/src/h264_vaapi.rs
CHANGED
|
@@ -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
|
-
}
|