@desklink/host 0.2.0 → 0.3.1
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/NOTICE +26 -0
- package/README.md +134 -56
- package/bin/desklink-host.mjs +13 -5
- package/dist/bridge.d.ts +1 -0
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js +12 -4
- package/dist/bridge.js.map +1 -1
- package/dist/engineProcess.d.ts +6 -1
- package/dist/engineProcess.d.ts.map +1 -1
- package/dist/engineProcess.js +35 -4
- package/dist/engineProcess.js.map +1 -1
- package/dist/protocol.d.ts +58 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js.map +1 -1
- package/dist/resolveEngine.d.ts +7 -1
- package/dist/resolveEngine.d.ts.map +1 -1
- package/dist/resolveEngine.js +66 -14
- package/dist/resolveEngine.js.map +1 -1
- package/docs/BOUNDARY.md +15 -7
- package/docs/PROTOCOL.md +267 -27
- package/engine/Cargo.lock +47 -2
- package/engine/Cargo.toml +18 -13
- package/engine/build.rs +86 -4
- package/engine/native/agent_overlay_mac.m +51 -4
- package/engine/native/agent_overlay_wayland.c +145 -12
- package/engine/native/agent_overlay_x11.c +72 -0
- package/engine/native/nvenc_shim.c +453 -0
- package/engine/native/vaapi_shim.c +727 -0
- package/engine/native/vpx_shim.c +54 -4
- package/engine/native/vt_shim.c +448 -0
- package/engine/src/capture.rs +167 -58
- package/engine/src/capture_mac.rs +24 -14
- package/engine/src/capture_win.rs +23 -0
- package/engine/src/clipboard_win.rs +8 -0
- package/engine/src/convert.rs +286 -61
- package/engine/src/cursor.rs +118 -0
- package/engine/src/encoder.rs +122 -13
- package/engine/src/h264.rs +188 -0
- package/engine/src/h264_nvenc.rs +308 -0
- package/engine/src/h264_openh264.rs +735 -0
- package/engine/src/h264_vaapi.rs +263 -0
- package/engine/src/h264_vt.rs +368 -0
- package/engine/src/indicator.rs +104 -3
- package/engine/src/input_win.rs +91 -0
- package/engine/src/keymap_win.rs +39 -0
- package/engine/src/mac.rs +34 -9
- package/engine/src/main.rs +186 -26
- package/engine/src/peer.rs +466 -28
- package/engine/src/portal.rs +42 -4
- package/engine/src/portal_win.rs +37 -0
- package/engine/src/protocol.rs +64 -2
- package/engine/src/session.rs +1252 -105
- package/engine/src/win.rs +110 -0
- package/engine/src/x11.rs +225 -9
- package/engine/src/x11_mac.rs +12 -0
- package/engine/vendor/libva/COPYING +19 -0
- package/engine/vendor/libva/va/va.h +5450 -0
- package/engine/vendor/libva/va/va_dec_av1.h +695 -0
- package/engine/vendor/libva/va/va_dec_hevc.h +616 -0
- package/engine/vendor/libva/va/va_dec_jpeg.h +157 -0
- package/engine/vendor/libva/va/va_dec_vp8.h +249 -0
- package/engine/vendor/libva/va/va_dec_vp9.h +312 -0
- package/engine/vendor/libva/va/va_dec_vvc.h +688 -0
- package/engine/vendor/libva/va/va_drm.h +61 -0
- package/engine/vendor/libva/va/va_enc_av1.h +1010 -0
- package/engine/vendor/libva/va/va_enc_h264.h +659 -0
- package/engine/vendor/libva/va/va_enc_hevc.h +958 -0
- package/engine/vendor/libva/va/va_enc_jpeg.h +162 -0
- package/engine/vendor/libva/va/va_enc_mpeg2.h +308 -0
- package/engine/vendor/libva/va/va_enc_vp8.h +350 -0
- package/engine/vendor/libva/va/va_enc_vp9.h +639 -0
- package/engine/vendor/libva/va/va_fei.h +170 -0
- package/engine/vendor/libva/va/va_fei_h264.h +505 -0
- package/engine/vendor/libva/va/va_fei_hevc.h +254 -0
- package/engine/vendor/libva/va/va_prot.h +525 -0
- package/engine/vendor/libva/va/va_version.h +87 -0
- package/engine/vendor/libva/va/va_vpp.h +1590 -0
- package/engine/vendor/nv-codec-headers/LICENSE +24 -0
- package/engine/vendor/nv-codec-headers/README +7 -0
- package/engine/vendor/nv-codec-headers/include/ffnvcodec/nvEncodeAPI.h +3825 -0
- package/examples/reference-client.html +1 -1
- package/package.json +4 -3
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
//! H.264 through VA-API, the Linux encode interface of Intel and AMD GPUs,
|
|
2
|
+
//! wrapping the slice encoder through the C shim in `native/vaapi_shim.c`.
|
|
3
|
+
//!
|
|
4
|
+
//! libva is loaded at runtime, so a machine without it (or without a GPU that
|
|
5
|
+
//! encodes H.264) still starts the engine and this backend just declines. The
|
|
6
|
+
//! first DRM render node whose driver has a Constrained Baseline encoder wins —
|
|
7
|
+
//! a machine may also carry a GPU whose VA driver only decodes — and
|
|
8
|
+
//! `DESKLINK_VAAPI_DEVICE` names one node to use instead.
|
|
9
|
+
|
|
10
|
+
use crate::convert::I420;
|
|
11
|
+
use crate::encoder::EncodedFrame;
|
|
12
|
+
use crate::h264::REFINE_MAX_QP;
|
|
13
|
+
use anyhow::Result;
|
|
14
|
+
use std::ffi::CString;
|
|
15
|
+
use std::os::raw::{c_char, c_int};
|
|
16
|
+
|
|
17
|
+
#[repr(C)]
|
|
18
|
+
struct NativeEncoder {
|
|
19
|
+
_private: [u8; 0],
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
extern "C" {
|
|
23
|
+
fn dl_vaapi_create(
|
|
24
|
+
device: *const c_char,
|
|
25
|
+
width: c_int,
|
|
26
|
+
height: c_int,
|
|
27
|
+
bitrate_kbps: c_int,
|
|
28
|
+
fps: c_int,
|
|
29
|
+
error: *mut c_char,
|
|
30
|
+
error_size: usize,
|
|
31
|
+
) -> *mut NativeEncoder;
|
|
32
|
+
fn dl_vaapi_encode(
|
|
33
|
+
encoder: *mut NativeEncoder,
|
|
34
|
+
i420: *const u8,
|
|
35
|
+
force_keyframe: c_int,
|
|
36
|
+
max_qp: c_int,
|
|
37
|
+
) -> c_int;
|
|
38
|
+
fn dl_vaapi_set_bitrate(encoder: *mut NativeEncoder, bitrate_kbps: c_int);
|
|
39
|
+
fn dl_vaapi_motion_max_qp() -> c_int;
|
|
40
|
+
fn dl_vaapi_packet_data(encoder: *const NativeEncoder) -> *const u8;
|
|
41
|
+
fn dl_vaapi_packet_size(encoder: *const NativeEncoder) -> usize;
|
|
42
|
+
fn dl_vaapi_packet_is_key(encoder: *const NativeEncoder) -> c_int;
|
|
43
|
+
fn dl_vaapi_destroy(encoder: *mut NativeEncoder);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
pub struct Encoder {
|
|
47
|
+
native: *mut NativeEncoder,
|
|
48
|
+
width: usize,
|
|
49
|
+
height: usize,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Moved to the pipeline thread and only used there; a VA display has no
|
|
53
|
+
// thread affinity, only a ban on concurrent calls into one context.
|
|
54
|
+
unsafe impl Send for Encoder {}
|
|
55
|
+
|
|
56
|
+
/// The render nodes to try, in order.
|
|
57
|
+
fn devices() -> Vec<String> {
|
|
58
|
+
if let Some(device) = std::env::var_os("DESKLINK_VAAPI_DEVICE").filter(|d| !d.is_empty()) {
|
|
59
|
+
return vec![device.to_string_lossy().into_owned()];
|
|
60
|
+
}
|
|
61
|
+
let mut nodes: Vec<String> = std::fs::read_dir("/dev/dri")
|
|
62
|
+
.into_iter()
|
|
63
|
+
.flatten()
|
|
64
|
+
.flatten()
|
|
65
|
+
.filter(|entry| entry.file_name().to_string_lossy().starts_with("renderD"))
|
|
66
|
+
// NVIDIA's VA driver only decodes, and loading it brings up CUDA,
|
|
67
|
+
// which was seen to deadlock against an NVENC session opening in
|
|
68
|
+
// another thread; NVENC is the encoder for that GPU.
|
|
69
|
+
.filter(|entry| {
|
|
70
|
+
let driver = std::path::Path::new("/sys/class/drm")
|
|
71
|
+
.join(entry.file_name())
|
|
72
|
+
.join("device/driver");
|
|
73
|
+
std::fs::read_link(driver).map_or(true, |driver| !driver.ends_with("nvidia"))
|
|
74
|
+
})
|
|
75
|
+
.map(|entry| entry.path().to_string_lossy().into_owned())
|
|
76
|
+
.collect();
|
|
77
|
+
nodes.sort();
|
|
78
|
+
nodes
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
impl Encoder {
|
|
82
|
+
pub fn open(width: usize, height: usize, bitrate_kbps: u32, fps: u32) -> Result<Self> {
|
|
83
|
+
let mut refusals = Vec::new();
|
|
84
|
+
for device in devices() {
|
|
85
|
+
let Ok(path) = CString::new(device.as_str()) else {
|
|
86
|
+
continue;
|
|
87
|
+
};
|
|
88
|
+
let mut error = [0 as c_char; 256];
|
|
89
|
+
let native = unsafe {
|
|
90
|
+
dl_vaapi_create(
|
|
91
|
+
path.as_ptr(),
|
|
92
|
+
width as c_int,
|
|
93
|
+
height as c_int,
|
|
94
|
+
bitrate_kbps.max(1) as c_int,
|
|
95
|
+
fps.max(1) as c_int,
|
|
96
|
+
error.as_mut_ptr(),
|
|
97
|
+
error.len(),
|
|
98
|
+
)
|
|
99
|
+
};
|
|
100
|
+
if !native.is_null() {
|
|
101
|
+
return Ok(Self {
|
|
102
|
+
native,
|
|
103
|
+
width,
|
|
104
|
+
height,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
let reason = unsafe { std::ffi::CStr::from_ptr(error.as_ptr()) };
|
|
108
|
+
refusals.push(reason.to_string_lossy().into_owned());
|
|
109
|
+
}
|
|
110
|
+
if refusals.is_empty() {
|
|
111
|
+
anyhow::bail!("no DRM render node");
|
|
112
|
+
}
|
|
113
|
+
anyhow::bail!("{}", refusals.join("; "))
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
fn code(&mut self, frame: &I420, keyframe: bool, max_qp: c_int) -> Result<EncodedFrame> {
|
|
117
|
+
if frame.width != self.width || frame.height != self.height {
|
|
118
|
+
anyhow::bail!(
|
|
119
|
+
"encoder is {}x{} but was given {}x{}",
|
|
120
|
+
self.width,
|
|
121
|
+
self.height,
|
|
122
|
+
frame.width,
|
|
123
|
+
frame.height
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
let status =
|
|
127
|
+
unsafe { dl_vaapi_encode(self.native, frame.data.as_ptr(), keyframe as c_int, max_qp) };
|
|
128
|
+
if status != 1 {
|
|
129
|
+
anyhow::bail!("VA-API rejected a {}x{} frame", self.width, self.height);
|
|
130
|
+
}
|
|
131
|
+
let data = unsafe {
|
|
132
|
+
std::slice::from_raw_parts(
|
|
133
|
+
dl_vaapi_packet_data(self.native),
|
|
134
|
+
dl_vaapi_packet_size(self.native),
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
.to_vec();
|
|
138
|
+
Ok(EncodedFrame {
|
|
139
|
+
data,
|
|
140
|
+
keyframe: unsafe { dl_vaapi_packet_is_key(self.native) } != 0,
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
impl crate::h264::Backend for Encoder {
|
|
146
|
+
fn encode(&mut self, frame: &I420, keyframe: bool) -> Result<EncodedFrame> {
|
|
147
|
+
self.code(frame, keyframe, unsafe { dl_vaapi_motion_max_qp() })
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/// The ceiling rides in this frame's rate-control parameters; the next
|
|
151
|
+
/// frame carries the motion ceiling again.
|
|
152
|
+
fn refine(&mut self, frame: &I420) -> Result<EncodedFrame> {
|
|
153
|
+
self.code(frame, false, REFINE_MAX_QP as c_int)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
fn set_bitrate(&mut self, bitrate_kbps: u32) -> Result<()> {
|
|
157
|
+
unsafe { dl_vaapi_set_bitrate(self.native, bitrate_kbps.max(1) as c_int) };
|
|
158
|
+
Ok(())
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
impl Drop for Encoder {
|
|
163
|
+
fn drop(&mut self) {
|
|
164
|
+
unsafe { dl_vaapi_destroy(self.native) };
|
|
165
|
+
}
|
|
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
|
+
}
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
//! H.264 through VideoToolbox, the macOS host's hardware encoder, wrapped by
|
|
2
|
+
//! the C shim in `native/vt_shim.c`.
|
|
3
|
+
//!
|
|
4
|
+
//! Low-latency rate control where the machine's encoder offers it for
|
|
5
|
+
//! Constrained Baseline, the ordinary real-time mode otherwise, or once the rate
|
|
6
|
+
//! is too low for low-latency mode to code every frame; either way no
|
|
7
|
+
//! reordering and no lookahead, and the shim flushes each frame before it
|
|
8
|
+
//! returns, so one frame in is one access unit out. VideoToolbox is a system
|
|
9
|
+
//! framework present on every supported macOS, so it is linked, not loaded.
|
|
10
|
+
|
|
11
|
+
use crate::convert::I420;
|
|
12
|
+
use crate::encoder::EncodedFrame;
|
|
13
|
+
use crate::h264::REFINE_MAX_QP;
|
|
14
|
+
use anyhow::Result;
|
|
15
|
+
use std::os::raw::c_int;
|
|
16
|
+
|
|
17
|
+
#[repr(C)]
|
|
18
|
+
struct NativeEncoder {
|
|
19
|
+
_private: [u8; 0],
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
extern "C" {
|
|
23
|
+
fn dl_vt_create(
|
|
24
|
+
width: c_int,
|
|
25
|
+
height: c_int,
|
|
26
|
+
bitrate_kbps: c_int,
|
|
27
|
+
fps: c_int,
|
|
28
|
+
out: *mut *mut NativeEncoder,
|
|
29
|
+
) -> i32;
|
|
30
|
+
fn dl_vt_encode(
|
|
31
|
+
encoder: *mut NativeEncoder,
|
|
32
|
+
i420: *const u8,
|
|
33
|
+
force_keyframe: c_int,
|
|
34
|
+
max_qp: c_int,
|
|
35
|
+
) -> i32;
|
|
36
|
+
fn dl_vt_set_bitrate(encoder: *mut NativeEncoder, bitrate_kbps: c_int) -> i32;
|
|
37
|
+
#[cfg(test)]
|
|
38
|
+
fn dl_vt_low_latency(encoder: *const NativeEncoder) -> c_int;
|
|
39
|
+
fn dl_vt_out_data(encoder: *const NativeEncoder) -> *const u8;
|
|
40
|
+
fn dl_vt_out_size(encoder: *const NativeEncoder) -> usize;
|
|
41
|
+
fn dl_vt_out_is_key(encoder: *const NativeEncoder) -> c_int;
|
|
42
|
+
fn dl_vt_destroy(encoder: *mut NativeEncoder);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// The shim's report of a frame the rate control skipped on every attempt,
|
|
46
|
+
/// even in the ordinary real-time mode (`DL_VT_DROPPED`).
|
|
47
|
+
const DROPPED: i32 = 1;
|
|
48
|
+
|
|
49
|
+
/// H.264's own quantizer ceiling: no cap outside a refinement pass.
|
|
50
|
+
const MOTION_MAX_QP: c_int = 51;
|
|
51
|
+
|
|
52
|
+
pub struct Encoder {
|
|
53
|
+
native: *mut NativeEncoder,
|
|
54
|
+
width: usize,
|
|
55
|
+
height: usize,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Moved to the pipeline thread and only used there. The output callback runs
|
|
59
|
+
// inside the encode call, which waits for it.
|
|
60
|
+
unsafe impl Send for Encoder {}
|
|
61
|
+
|
|
62
|
+
impl Encoder {
|
|
63
|
+
pub fn open(width: usize, height: usize, bitrate_kbps: u32, fps: u32) -> Result<Self> {
|
|
64
|
+
let mut native = std::ptr::null_mut();
|
|
65
|
+
let status = unsafe {
|
|
66
|
+
dl_vt_create(
|
|
67
|
+
width as c_int,
|
|
68
|
+
height as c_int,
|
|
69
|
+
bitrate_kbps.max(1) as c_int,
|
|
70
|
+
fps.max(1) as c_int,
|
|
71
|
+
&mut native,
|
|
72
|
+
)
|
|
73
|
+
};
|
|
74
|
+
if status != 0 || native.is_null() {
|
|
75
|
+
anyhow::bail!(
|
|
76
|
+
"VideoToolbox refused an H.264 session for {width}x{height} (OSStatus {status})"
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
Ok(Self {
|
|
80
|
+
native,
|
|
81
|
+
width,
|
|
82
|
+
height,
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// Whether the session runs VideoToolbox's low-latency rate control.
|
|
87
|
+
#[cfg(test)]
|
|
88
|
+
fn low_latency(&self) -> bool {
|
|
89
|
+
unsafe { dl_vt_low_latency(self.native) != 0 }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
fn code(&mut self, frame: &I420, keyframe: bool, max_qp: c_int) -> Result<EncodedFrame> {
|
|
93
|
+
if frame.width != self.width || frame.height != self.height {
|
|
94
|
+
anyhow::bail!(
|
|
95
|
+
"encoder is {}x{} but was given {}x{}",
|
|
96
|
+
self.width,
|
|
97
|
+
self.height,
|
|
98
|
+
frame.width,
|
|
99
|
+
frame.height
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
let status =
|
|
103
|
+
unsafe { dl_vt_encode(self.native, frame.data.as_ptr(), keyframe as c_int, max_qp) };
|
|
104
|
+
if status == DROPPED {
|
|
105
|
+
anyhow::bail!("VideoToolbox skipped the same frame on every attempt");
|
|
106
|
+
}
|
|
107
|
+
if status != 0 {
|
|
108
|
+
anyhow::bail!("VideoToolbox failed to code a frame (OSStatus {status})");
|
|
109
|
+
}
|
|
110
|
+
let size = unsafe { dl_vt_out_size(self.native) };
|
|
111
|
+
let data = unsafe { dl_vt_out_data(self.native) };
|
|
112
|
+
if data.is_null() || size == 0 {
|
|
113
|
+
anyhow::bail!("VideoToolbox produced an empty access unit");
|
|
114
|
+
}
|
|
115
|
+
Ok(EncodedFrame {
|
|
116
|
+
data: unsafe { std::slice::from_raw_parts(data, size) }.to_vec(),
|
|
117
|
+
keyframe: unsafe { dl_vt_out_is_key(self.native) } != 0,
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
impl crate::h264::Backend for Encoder {
|
|
123
|
+
fn encode(&mut self, frame: &I420, keyframe: bool) -> Result<EncodedFrame> {
|
|
124
|
+
self.code(frame, keyframe, MOTION_MAX_QP)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/// Low-latency mode lowers `MaxAllowedFrameQP` for this one frame. The
|
|
128
|
+
/// ordinary mode accepts that property and ignores it, so there the frame
|
|
129
|
+
/// gets a rate target many times the session's instead, which buys about 5
|
|
130
|
+
/// QP below the frames around it: [`REFINE_MAX_QP`] only when those sit
|
|
131
|
+
/// near 25 (`DL_VT_REFINE_RATE_SCALE` in the shim).
|
|
132
|
+
fn refine(&mut self, frame: &I420) -> Result<EncodedFrame> {
|
|
133
|
+
self.code(frame, false, REFINE_MAX_QP as c_int)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
fn set_bitrate(&mut self, bitrate_kbps: u32) -> Result<()> {
|
|
137
|
+
let status = unsafe { dl_vt_set_bitrate(self.native, bitrate_kbps.max(1) as c_int) };
|
|
138
|
+
if status != 0 {
|
|
139
|
+
anyhow::bail!("VideoToolbox refused a {bitrate_kbps} kbps target (OSStatus {status})");
|
|
140
|
+
}
|
|
141
|
+
Ok(())
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
impl Drop for Encoder {
|
|
146
|
+
fn drop(&mut self) {
|
|
147
|
+
unsafe { dl_vt_destroy(self.native) };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
#[cfg(test)]
|
|
152
|
+
mod tests {
|
|
153
|
+
use super::*;
|
|
154
|
+
use crate::h264::Backend;
|
|
155
|
+
|
|
156
|
+
/// A coloured box that moves with `t` over a grey gradient: something to
|
|
157
|
+
/// predict from, and colour a decoder could get wrong.
|
|
158
|
+
fn pattern(width: usize, height: usize, t: usize) -> I420 {
|
|
159
|
+
let mut data = vec![128u8; width * height * 3 / 2];
|
|
160
|
+
let inside = |x: usize, y: usize| {
|
|
161
|
+
(x + width - (t * 8) % width) % width < width / 4
|
|
162
|
+
&& y > height / 3
|
|
163
|
+
&& y < height * 2 / 3
|
|
164
|
+
};
|
|
165
|
+
for y in 0..height {
|
|
166
|
+
for x in 0..width {
|
|
167
|
+
data[y * width + x] = if inside(x, y) {
|
|
168
|
+
160
|
|
169
|
+
} else {
|
|
170
|
+
((x + y + t * 3) % 200) as u8 + 16
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
let (cw, ch) = (width / 2, height / 2);
|
|
175
|
+
for y in 0..ch {
|
|
176
|
+
for x in 0..cw {
|
|
177
|
+
if inside(x * 2, y * 2) {
|
|
178
|
+
data[width * height + y * cw + x] = 90;
|
|
179
|
+
data[width * height + cw * ch + y * cw + x] = 200;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
I420 {
|
|
184
|
+
width,
|
|
185
|
+
height,
|
|
186
|
+
data,
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/// NAL unit types of an Annex-B access unit, in order.
|
|
191
|
+
fn nal_types(au: &[u8]) -> Vec<u8> {
|
|
192
|
+
let mut types = Vec::new();
|
|
193
|
+
let mut i = 0;
|
|
194
|
+
while i + 3 < au.len() {
|
|
195
|
+
if au[i] == 0 && au[i + 1] == 0 && au[i + 2] == 1 {
|
|
196
|
+
types.push(au[i + 3] & 0x1f);
|
|
197
|
+
i += 3;
|
|
198
|
+
} else {
|
|
199
|
+
i += 1;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
types
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
fn open(width: usize, height: usize, kbps: u32, fps: u32) -> Option<Encoder> {
|
|
206
|
+
match Encoder::open(width, height, kbps, fps) {
|
|
207
|
+
Ok(encoder) => Some(encoder),
|
|
208
|
+
Err(error) => {
|
|
209
|
+
eprintln!("skipping: {error:#}");
|
|
210
|
+
None
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
fn assert_idr(frame: &EncodedFrame) {
|
|
216
|
+
let types = nal_types(&frame.data);
|
|
217
|
+
assert!(frame.keyframe, "{types:?}");
|
|
218
|
+
let sps = types.iter().position(|&t| t == 7).expect("SPS");
|
|
219
|
+
let pps = types.iter().position(|&t| t == 8).expect("PPS");
|
|
220
|
+
let idr = types.iter().position(|&t| t == 5).expect("IDR slice");
|
|
221
|
+
assert!(sps < pps && pps < idr, "{types:?}");
|
|
222
|
+
assert!(frame.data.starts_with(&[0, 0, 0, 1]));
|
|
223
|
+
// Constrained Baseline: profile_idc 66, constraint_set1.
|
|
224
|
+
let at = frame
|
|
225
|
+
.data
|
|
226
|
+
.windows(4)
|
|
227
|
+
.position(|w| w[..3] == [0, 0, 1] && w[3] & 0x1f == 7)
|
|
228
|
+
.unwrap()
|
|
229
|
+
+ 4;
|
|
230
|
+
assert_eq!(frame.data[at], 66, "profile_idc");
|
|
231
|
+
assert!(frame.data[at + 1] & 0x40 != 0, "constraint_set1_flag");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
fn assert_inter(frame: &EncodedFrame) {
|
|
235
|
+
let types = nal_types(&frame.data);
|
|
236
|
+
assert!(!frame.keyframe, "{types:?}");
|
|
237
|
+
assert!(types.contains(&1) && !types.contains(&5), "{types:?}");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
#[test]
|
|
241
|
+
fn h264_vt_meets_the_stream_contract() {
|
|
242
|
+
let (w, h) = (640, 360);
|
|
243
|
+
let Some(mut encoder) = open(w, h, 2000, 30) else {
|
|
244
|
+
return;
|
|
245
|
+
};
|
|
246
|
+
eprintln!("low latency rate control: {}", encoder.low_latency());
|
|
247
|
+
assert_idr(&encoder.encode(&pattern(w, h, 0), false).unwrap());
|
|
248
|
+
for t in 1..5 {
|
|
249
|
+
assert_inter(&encoder.encode(&pattern(w, h, t), false).unwrap());
|
|
250
|
+
}
|
|
251
|
+
assert_idr(&encoder.encode(&pattern(w, h, 5), true).unwrap());
|
|
252
|
+
assert_inter(&encoder.encode(&pattern(w, h, 6), false).unwrap());
|
|
253
|
+
let still = encoder.encode(&pattern(w, h, 6), false).unwrap();
|
|
254
|
+
assert_inter(&still);
|
|
255
|
+
let refined = encoder.refine(&pattern(w, h, 6)).unwrap();
|
|
256
|
+
assert_inter(&refined);
|
|
257
|
+
eprintln!(
|
|
258
|
+
"static frame {} bytes, refined {} bytes",
|
|
259
|
+
still.data.len(),
|
|
260
|
+
refined.data.len()
|
|
261
|
+
);
|
|
262
|
+
encoder.set_bitrate(500).unwrap();
|
|
263
|
+
assert_inter(&encoder.encode(&pattern(w, h, 7), false).unwrap());
|
|
264
|
+
assert!(encoder.encode(&pattern(320, 240, 0), false).is_err());
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
#[test]
|
|
268
|
+
fn h264_vt_codes_sizes_that_are_not_macroblock_multiples() {
|
|
269
|
+
for (w, h) in [(1366, 768), (1918, 1078)] {
|
|
270
|
+
let Some(mut encoder) = open(w, h, 4000, 30) else {
|
|
271
|
+
return;
|
|
272
|
+
};
|
|
273
|
+
assert_idr(&encoder.encode(&pattern(w, h, 0), false).unwrap());
|
|
274
|
+
assert_inter(&encoder.encode(&pattern(w, h, 1), false).unwrap());
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/// The lowest rates a session sets at each size: low-latency rate control
|
|
279
|
+
/// skips frames there, and a skip must never reach the session as an error.
|
|
280
|
+
#[test]
|
|
281
|
+
fn h264_vt_codes_every_frame_at_the_lowest_rates() {
|
|
282
|
+
for (w, h, kbps) in [
|
|
283
|
+
(640, 360, 100),
|
|
284
|
+
(1366, 768, 200),
|
|
285
|
+
(1920, 1080, 300),
|
|
286
|
+
(1920, 1080, 654),
|
|
287
|
+
(3840, 2160, 1500),
|
|
288
|
+
] {
|
|
289
|
+
let Some(mut encoder) = open(w, h, kbps, 30) else {
|
|
290
|
+
return;
|
|
291
|
+
};
|
|
292
|
+
// Few distinct pictures: building a 4K one is slow in a debug build.
|
|
293
|
+
let frames: Vec<_> = (0..8).map(|t| pattern(w, h, t)).collect();
|
|
294
|
+
for t in 0..40 {
|
|
295
|
+
let frame = &frames[t % 8];
|
|
296
|
+
let coded = match t {
|
|
297
|
+
0 | 20 => encoder.encode(frame, true),
|
|
298
|
+
30 => encoder.refine(&frames[29 % 8]),
|
|
299
|
+
_ => encoder.encode(frame, false),
|
|
300
|
+
}
|
|
301
|
+
.unwrap_or_else(|error| panic!("{w}x{h} at {kbps} kbps, frame {t}: {error:#}"));
|
|
302
|
+
let types = nal_types(&coded.data);
|
|
303
|
+
assert_eq!(coded.keyframe, types.contains(&5), "frame {t}: {types:?}");
|
|
304
|
+
if t == 0 || t == 20 {
|
|
305
|
+
assert_idr(&coded);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
eprintln!(
|
|
309
|
+
"{w}x{h} at {kbps} kbps: low latency {}",
|
|
310
|
+
encoder.low_latency()
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/// Writes `$DESKLINK_VT_DUMP` (60 frames of motion, an IDR and a refine in
|
|
316
|
+
/// the middle) for an external decoder, and times 1080p and 4K. The rate is
|
|
317
|
+
/// low so motion frames sit above the refinement ceiling.
|
|
318
|
+
#[test]
|
|
319
|
+
#[ignore]
|
|
320
|
+
fn h264_vt_dump_and_time() {
|
|
321
|
+
let Ok(path) = std::env::var("DESKLINK_VT_DUMP") else {
|
|
322
|
+
return;
|
|
323
|
+
};
|
|
324
|
+
let (w, h) = (1366, 768);
|
|
325
|
+
let Some(mut encoder) = open(w, h, 1000, 30) else {
|
|
326
|
+
return;
|
|
327
|
+
};
|
|
328
|
+
let mut out = Vec::new();
|
|
329
|
+
for t in 0..60 {
|
|
330
|
+
let frame = pattern(w, h, t);
|
|
331
|
+
let coded = match t {
|
|
332
|
+
30 => encoder.encode(&frame, true),
|
|
333
|
+
45 => encoder.refine(&pattern(w, h, 44)),
|
|
334
|
+
_ => encoder.encode(&frame, false),
|
|
335
|
+
}
|
|
336
|
+
.unwrap_or_else(|error| panic!("frame {t}: {error:#}"));
|
|
337
|
+
out.extend_from_slice(&coded.data);
|
|
338
|
+
}
|
|
339
|
+
std::fs::write(&path, out).unwrap();
|
|
340
|
+
std::fs::write(format!("{path}.yuv"), pattern(w, h, 0).data).unwrap();
|
|
341
|
+
// The ordinary mode, reached through a rate low-latency mode cannot
|
|
342
|
+
// hold, then raised: its refinement's quantizer for an external parser.
|
|
343
|
+
let mut encoder = open(w, h, 100, 30).unwrap();
|
|
344
|
+
let mut out = Vec::new();
|
|
345
|
+
for t in 0..60 {
|
|
346
|
+
if t == 10 {
|
|
347
|
+
assert!(!encoder.low_latency(), "still in low-latency mode");
|
|
348
|
+
encoder.set_bitrate(1000).unwrap();
|
|
349
|
+
}
|
|
350
|
+
let coded = match t {
|
|
351
|
+
45 => encoder.refine(&pattern(w, h, 44)),
|
|
352
|
+
_ => encoder.encode(&pattern(w, h, t), t == 0),
|
|
353
|
+
}
|
|
354
|
+
.unwrap_or_else(|error| panic!("fallback frame {t}: {error:#}"));
|
|
355
|
+
out.extend_from_slice(&coded.data);
|
|
356
|
+
}
|
|
357
|
+
std::fs::write(format!("{path}.fallback"), out).unwrap();
|
|
358
|
+
for (w, h, kbps) in [(1920, 1080, 8000), (3840, 2160, 20000)] {
|
|
359
|
+
let frames: Vec<_> = (0..8).map(|t| pattern(w, h, t)).collect();
|
|
360
|
+
let mut encoder = open(w, h, kbps, 60).unwrap();
|
|
361
|
+
let start = std::time::Instant::now();
|
|
362
|
+
for t in 0..60 {
|
|
363
|
+
encoder.encode(&frames[t % 8], t == 0).unwrap();
|
|
364
|
+
}
|
|
365
|
+
eprintln!("{w}x{h}: {:?} per frame", start.elapsed() / 60);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|