@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
package/engine/src/encoder.rs
CHANGED
|
@@ -7,10 +7,12 @@
|
|
|
7
7
|
//! and would put a driver dependency on the machine class.
|
|
8
8
|
//!
|
|
9
9
|
//! Tuned for a desktop rather than a camera: screen-content mode, key frames
|
|
10
|
-
//!
|
|
10
|
+
//! on request and when the coded size changes, and a refinement pass. While the desktop moves, frames are
|
|
11
11
|
//! coded against the rate target; once it stops, the last frame is coded once
|
|
12
12
|
//! more under a low quantizer ceiling, so what the user reads is sharp without
|
|
13
|
-
//! paying for sharpness on every frame of a scroll.
|
|
13
|
+
//! paying for sharpness on every frame of a scroll. Motion the encoder cannot
|
|
14
|
+
//! code within a frame interval may be coded at half size; the refinement pass
|
|
15
|
+
//! is always full size.
|
|
14
16
|
|
|
15
17
|
use crate::convert::I420;
|
|
16
18
|
use anyhow::Result;
|
|
@@ -30,7 +32,12 @@ extern "C" {
|
|
|
30
32
|
threads: c_int,
|
|
31
33
|
cpu_used: c_int,
|
|
32
34
|
) -> *mut NativeEncoder;
|
|
33
|
-
fn dl_vpx_encode(
|
|
35
|
+
fn dl_vpx_encode(
|
|
36
|
+
encoder: *mut NativeEncoder,
|
|
37
|
+
i420: *const u8,
|
|
38
|
+
force_keyframe: c_int,
|
|
39
|
+
half: c_int,
|
|
40
|
+
) -> c_int;
|
|
34
41
|
fn dl_vpx_reconfigure(
|
|
35
42
|
encoder: *mut NativeEncoder,
|
|
36
43
|
bitrate_kbps: c_int,
|
|
@@ -47,9 +54,9 @@ extern "C" {
|
|
|
47
54
|
/// that small text is crisp; the pass happens once per still, not per frame.
|
|
48
55
|
const REFINE_MAX_Q: c_int = 10;
|
|
49
56
|
|
|
50
|
-
/// libvpx's speed/quality trade for real time.
|
|
51
|
-
///
|
|
52
|
-
const CPU_USED: c_int =
|
|
57
|
+
/// libvpx's speed/quality trade for real time. 9 codes full-screen motion at
|
|
58
|
+
/// 1080p in about half the CPU time of 8, and a refined still reads the same.
|
|
59
|
+
const CPU_USED: c_int = 9;
|
|
53
60
|
|
|
54
61
|
/// One encoded VP9 frame as libvpx produced it.
|
|
55
62
|
pub struct EncodedFrame {
|
|
@@ -122,7 +129,7 @@ impl Encoder {
|
|
|
122
129
|
if unsafe { dl_vpx_reconfigure(self.native, bitrate, REFINE_MAX_Q) } != 0 {
|
|
123
130
|
anyhow::bail!("libvpx refused the refinement ceiling");
|
|
124
131
|
}
|
|
125
|
-
let packet = self.encode(frame, false);
|
|
132
|
+
let packet = self.encode(frame, false, false);
|
|
126
133
|
let restored = unsafe { dl_vpx_reconfigure(self.native, bitrate, dl_vpx_motion_max_q()) };
|
|
127
134
|
if restored != 0 {
|
|
128
135
|
anyhow::bail!("libvpx refused to restore the motion ceiling");
|
|
@@ -130,7 +137,14 @@ impl Encoder {
|
|
|
130
137
|
packet
|
|
131
138
|
}
|
|
132
139
|
|
|
133
|
-
|
|
140
|
+
/// Code `frame`; with `half`, at half its width and height. Changing the
|
|
141
|
+
/// coded size makes this frame a key frame.
|
|
142
|
+
pub fn encode(
|
|
143
|
+
&mut self,
|
|
144
|
+
frame: &I420,
|
|
145
|
+
force_keyframe: bool,
|
|
146
|
+
half: bool,
|
|
147
|
+
) -> Result<EncodedFrame> {
|
|
134
148
|
if frame.width != self.width || frame.height != self.height {
|
|
135
149
|
anyhow::bail!(
|
|
136
150
|
"encoder is {}x{} but was given {}x{}",
|
|
@@ -140,8 +154,14 @@ impl Encoder {
|
|
|
140
154
|
frame.height
|
|
141
155
|
);
|
|
142
156
|
}
|
|
143
|
-
let status =
|
|
144
|
-
|
|
157
|
+
let status = unsafe {
|
|
158
|
+
dl_vpx_encode(
|
|
159
|
+
self.native,
|
|
160
|
+
frame.data.as_ptr(),
|
|
161
|
+
force_keyframe as c_int,
|
|
162
|
+
half as c_int,
|
|
163
|
+
)
|
|
164
|
+
};
|
|
145
165
|
if status < 0 {
|
|
146
166
|
anyhow::bail!("libvpx rejected a {}x{} frame", self.width, self.height);
|
|
147
167
|
}
|
|
@@ -162,6 +182,69 @@ impl Encoder {
|
|
|
162
182
|
}
|
|
163
183
|
}
|
|
164
184
|
|
|
185
|
+
/// Whichever encoder a session's answer chose: VP9, which every session
|
|
186
|
+
/// starts with, or H.264 once a receiver prefers it (see `crate::h264`).
|
|
187
|
+
pub enum VideoEncoder {
|
|
188
|
+
Vp9(Encoder),
|
|
189
|
+
H264 {
|
|
190
|
+
backend: Box<dyn crate::h264::Backend>,
|
|
191
|
+
size: (usize, usize),
|
|
192
|
+
},
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
impl VideoEncoder {
|
|
196
|
+
/// `half` is VP9's: H.264 codes every frame at full size.
|
|
197
|
+
pub fn encode(&mut self, frame: &I420, keyframe: bool, half: bool) -> Result<EncodedFrame> {
|
|
198
|
+
match self {
|
|
199
|
+
Self::Vp9(encoder) => encoder.encode(frame, keyframe, half),
|
|
200
|
+
Self::H264 { backend, size } => {
|
|
201
|
+
fits(frame, *size)?;
|
|
202
|
+
backend.encode(frame, keyframe)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
pub fn refine(&mut self, frame: &I420) -> Result<EncodedFrame> {
|
|
208
|
+
match self {
|
|
209
|
+
Self::Vp9(encoder) => encoder.refine(frame),
|
|
210
|
+
Self::H264 { backend, size } => {
|
|
211
|
+
fits(frame, *size)?;
|
|
212
|
+
backend.refine(frame)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
pub fn set_bitrate(&mut self, bitrate_kbps: u32) -> Result<()> {
|
|
218
|
+
match self {
|
|
219
|
+
Self::Vp9(encoder) => encoder.set_bitrate(bitrate_kbps),
|
|
220
|
+
Self::H264 { backend, .. } => backend.set_bitrate(bitrate_kbps.max(1)),
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
pub fn size(&self) -> (usize, usize) {
|
|
225
|
+
match self {
|
|
226
|
+
Self::Vp9(encoder) => (encoder.width, encoder.height),
|
|
227
|
+
Self::H264 { size, .. } => *size,
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/// The H.264 backends hand `frame.data` to C, which reads a whole I420 frame
|
|
233
|
+
/// of the size they were opened at: refuse anything else here, once.
|
|
234
|
+
fn fits(frame: &I420, (width, height): (usize, usize)) -> Result<()> {
|
|
235
|
+
if (frame.width, frame.height) != (width, height) {
|
|
236
|
+
anyhow::bail!(
|
|
237
|
+
"encoder is {width}x{height} but was given {}x{}",
|
|
238
|
+
frame.width,
|
|
239
|
+
frame.height
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
if frame.data.len() < width * height + 2 * (width / 2) * (height / 2) {
|
|
243
|
+
anyhow::bail!("a {width}x{height} frame is short of its planes");
|
|
244
|
+
}
|
|
245
|
+
Ok(())
|
|
246
|
+
}
|
|
247
|
+
|
|
165
248
|
impl Drop for Encoder {
|
|
166
249
|
fn drop(&mut self) {
|
|
167
250
|
if !self.native.is_null() {
|
|
@@ -197,8 +280,34 @@ mod tests {
|
|
|
197
280
|
fn encodes_successive_frames_and_refuses_a_mismatched_geometry() {
|
|
198
281
|
let (w, h) = (64usize, 64usize);
|
|
199
282
|
let mut encoder = Encoder::new(w, h, 500, 30, 2).expect("encoder should start");
|
|
200
|
-
assert!(!encoder
|
|
201
|
-
|
|
202
|
-
|
|
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);
|
|
203
312
|
}
|
|
204
313
|
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
//! H.264 encoding of captured desktops, offered alongside VP9.
|
|
2
|
+
//!
|
|
3
|
+
//! VP9 stays the default: a receiver that states no preference gets it, as it
|
|
4
|
+
//! always has. H.264 exists for receivers whose platform decodes it in
|
|
5
|
+
//! hardware and VP9 only in software — an iPhone's WebRTC stack is one — so the
|
|
6
|
+
//! engine offers both and codes whichever the receiver's answer puts first.
|
|
7
|
+
//!
|
|
8
|
+
//! Platform encoders come first: VideoToolbox on macOS, NVENC or VA-API on
|
|
9
|
+
//! Linux. Where a Linux machine has neither, the engine loads Cisco's prebuilt
|
|
10
|
+
//! openh264 library, fetched at runtime from Cisco and checked against its
|
|
11
|
+
//! published hash — never a copy built from source, and never one shipped in
|
|
12
|
+
//! this package, because Cisco's patent licence covers only its own binary.
|
|
13
|
+
//!
|
|
14
|
+
//! Every backend emits the same stream: Constrained Baseline, Annex-B, no
|
|
15
|
+
//! B-frames and no lookahead (one frame in, one access unit out), an IDR only
|
|
16
|
+
//! when asked for, and the SPS and PPS in front of every IDR so a decoder can
|
|
17
|
+
//! start on any of them. Coded size never changes within a session: the VP9
|
|
18
|
+
//! path's half-size motion has no H.264 counterpart here, since a size change
|
|
19
|
+
//! costs H.264 a new sequence, not just a key frame.
|
|
20
|
+
|
|
21
|
+
use crate::convert::I420;
|
|
22
|
+
use crate::encoder::EncodedFrame;
|
|
23
|
+
use anyhow::Result;
|
|
24
|
+
use std::sync::Mutex;
|
|
25
|
+
|
|
26
|
+
/// One H.264 encoder backend. All take I420 at the size they were opened with.
|
|
27
|
+
pub trait Backend: Send {
|
|
28
|
+
/// Code `frame`; `keyframe` forces an IDR. The access unit is Annex-B, and
|
|
29
|
+
/// an IDR carries its SPS and PPS in front of the slice.
|
|
30
|
+
fn encode(&mut self, frame: &I420, keyframe: bool) -> Result<EncodedFrame>;
|
|
31
|
+
/// Code `frame` again, unchanged, as an inter frame under
|
|
32
|
+
/// [`REFINE_MAX_QP`]: the detail the motion frames could not afford.
|
|
33
|
+
fn refine(&mut self, frame: &I420) -> Result<EncodedFrame>;
|
|
34
|
+
/// Change the rate target, in kilobits per second, for the frames that follow.
|
|
35
|
+
fn set_bitrate(&mut self, bitrate_kbps: u32) -> Result<()>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// The quantizer ceiling of a refinement pass, on H.264's 0–51 scale: small
|
|
39
|
+
/// text reads sharp, and the pass happens once per still, not per frame.
|
|
40
|
+
pub const REFINE_MAX_QP: u32 = 20;
|
|
41
|
+
|
|
42
|
+
/// The `profile-level-id` every backend's stream satisfies: Constrained
|
|
43
|
+
/// Baseline (`42e0`) at level 5.2, the bound for a 4K desktop at 60 fps. A
|
|
44
|
+
/// receiver matches on the profile; the level is an upper bound, and the
|
|
45
|
+
/// offer's `level-asymmetry-allowed=1` lets each side keep its own.
|
|
46
|
+
pub const PROFILE_LEVEL_ID: &str = "42e034";
|
|
47
|
+
|
|
48
|
+
/// Which encoder a session would get.
|
|
49
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
50
|
+
pub struct Info {
|
|
51
|
+
/// `videotoolbox`, `nvenc`, `vaapi` or `openh264`.
|
|
52
|
+
pub encoder: &'static str,
|
|
53
|
+
pub hardware: bool,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type Opener = fn(usize, usize, u32, u32) -> Result<Box<dyn Backend>>;
|
|
57
|
+
|
|
58
|
+
/// Level 5.2's frame size limit, in 16x16 macroblocks: the most the offer's
|
|
59
|
+
/// `PROFILE_LEVEL_ID` promises, and more than a 4096x2160 desktop needs.
|
|
60
|
+
const MAX_MACROBLOCKS: usize = 36_864;
|
|
61
|
+
|
|
62
|
+
/// `DESKLINK_H264_ENCODER`: one backend to use alone, `off` for none.
|
|
63
|
+
fn only() -> Option<String> {
|
|
64
|
+
std::env::var("DESKLINK_H264_ENCODER")
|
|
65
|
+
.ok()
|
|
66
|
+
.filter(|only| !only.is_empty())
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// The backends this build has, best first.
|
|
70
|
+
fn candidates() -> Vec<(Info, Opener)> {
|
|
71
|
+
let mut all: Vec<(Info, Opener)> = Vec::new();
|
|
72
|
+
#[cfg(target_os = "macos")]
|
|
73
|
+
all.push((
|
|
74
|
+
Info {
|
|
75
|
+
encoder: "videotoolbox",
|
|
76
|
+
hardware: true,
|
|
77
|
+
},
|
|
78
|
+
|w, h, kbps, fps| Ok(Box::new(crate::h264_vt::Encoder::open(w, h, kbps, fps)?)),
|
|
79
|
+
));
|
|
80
|
+
#[cfg(target_os = "linux")]
|
|
81
|
+
{
|
|
82
|
+
all.push((
|
|
83
|
+
Info {
|
|
84
|
+
encoder: "nvenc",
|
|
85
|
+
hardware: true,
|
|
86
|
+
},
|
|
87
|
+
|w, h, kbps, fps| Ok(Box::new(crate::h264_nvenc::Encoder::open(w, h, kbps, fps)?)),
|
|
88
|
+
));
|
|
89
|
+
all.push((
|
|
90
|
+
Info {
|
|
91
|
+
encoder: "vaapi",
|
|
92
|
+
hardware: true,
|
|
93
|
+
},
|
|
94
|
+
|w, h, kbps, fps| Ok(Box::new(crate::h264_vaapi::Encoder::open(w, h, kbps, fps)?)),
|
|
95
|
+
));
|
|
96
|
+
all.push((
|
|
97
|
+
Info {
|
|
98
|
+
encoder: "openh264",
|
|
99
|
+
hardware: false,
|
|
100
|
+
},
|
|
101
|
+
|w, h, kbps, fps| {
|
|
102
|
+
Ok(Box::new(crate::h264_openh264::Encoder::open(
|
|
103
|
+
w, h, kbps, fps,
|
|
104
|
+
)?))
|
|
105
|
+
},
|
|
106
|
+
));
|
|
107
|
+
}
|
|
108
|
+
if let Some(only) = only() {
|
|
109
|
+
all.retain(|(info, _)| info.encoder == only);
|
|
110
|
+
}
|
|
111
|
+
all
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// Whether H.264 is offered for a surface this size. Some encoders open at
|
|
115
|
+
/// sizes they then refuse to code, so a session is refused H.264 at its offer
|
|
116
|
+
/// rather than ended by its first frame.
|
|
117
|
+
pub fn fits(width: usize, height: usize) -> bool {
|
|
118
|
+
width >= 16 && height >= 16 && width.div_ceil(16) * height.div_ceil(16) <= MAX_MACROBLOCKS
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// Open the best backend that starts for this size.
|
|
122
|
+
pub fn open(
|
|
123
|
+
width: usize,
|
|
124
|
+
height: usize,
|
|
125
|
+
bitrate_kbps: u32,
|
|
126
|
+
fps: u32,
|
|
127
|
+
) -> Result<(Box<dyn Backend>, Info)> {
|
|
128
|
+
if !fits(width, height) {
|
|
129
|
+
anyhow::bail!("H.264 is not offered for a {width}x{height} surface");
|
|
130
|
+
}
|
|
131
|
+
// One backend opens at a time: driver libraries initialize on first use,
|
|
132
|
+
// and two GPU stacks coming up together in one process is where drivers
|
|
133
|
+
// have been seen to deadlock.
|
|
134
|
+
static OPENING: Mutex<()> = Mutex::new(());
|
|
135
|
+
let _opening = OPENING.lock().unwrap_or_else(|p| p.into_inner());
|
|
136
|
+
let mut refusals = Vec::new();
|
|
137
|
+
for (info, open) in candidates() {
|
|
138
|
+
match open(width, height, bitrate_kbps.max(1), fps.max(1)) {
|
|
139
|
+
Ok(backend) => return Ok((backend, info)),
|
|
140
|
+
Err(error) => refusals.push(format!("{}: {error:#}", info.encoder)),
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
anyhow::bail!(
|
|
144
|
+
"no H.264 encoder starts for {width}x{height} ({})",
|
|
145
|
+
if refusals.is_empty() {
|
|
146
|
+
String::from("none enabled")
|
|
147
|
+
} else {
|
|
148
|
+
refusals.join("; ")
|
|
149
|
+
}
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/// What the last probe found; `None` until one has run.
|
|
154
|
+
static PROBED: Mutex<Option<Option<Info>>> = Mutex::new(None);
|
|
155
|
+
|
|
156
|
+
/// The backend a session would get, found by starting a small encoder and
|
|
157
|
+
/// coding one frame with it — a driver that lists H.264 but cannot open a
|
|
158
|
+
/// session is common enough that listing is no evidence. Cached: opening a
|
|
159
|
+
/// hardware session is not free. Never fetches anything.
|
|
160
|
+
pub fn probe() -> Option<Info> {
|
|
161
|
+
let mut probed = PROBED.lock().unwrap_or_else(|p| p.into_inner());
|
|
162
|
+
*probed.get_or_insert_with(|| {
|
|
163
|
+
let (width, height) = (320, 240);
|
|
164
|
+
let blank = I420 {
|
|
165
|
+
width,
|
|
166
|
+
height,
|
|
167
|
+
data: vec![128u8; width * height * 3 / 2],
|
|
168
|
+
};
|
|
169
|
+
let (mut backend, info) = open(width, height, 500, 30).ok()?;
|
|
170
|
+
backend.encode(&blank, true).ok()?;
|
|
171
|
+
Some(info)
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/// The backend for a session about to be offered: [`probe`], and on Linux,
|
|
176
|
+
/// when no encoder is present, fetch Cisco's openh264 once and probe again.
|
|
177
|
+
/// Blocks for the fetch; call it off the async runtime.
|
|
178
|
+
pub fn prepare() -> Option<Info> {
|
|
179
|
+
if let Some(info) = probe() {
|
|
180
|
+
return Some(info);
|
|
181
|
+
}
|
|
182
|
+
#[cfg(target_os = "linux")]
|
|
183
|
+
if only().is_none_or(|only| only == "openh264") && crate::h264_openh264::fetch_once() {
|
|
184
|
+
*PROBED.lock().unwrap_or_else(|p| p.into_inner()) = None;
|
|
185
|
+
return probe();
|
|
186
|
+
}
|
|
187
|
+
None
|
|
188
|
+
}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
//! H.264 on NVIDIA GPUs through NVENC, wrapped by the C shim in
|
|
2
|
+
//! `native/nvenc_shim.c`.
|
|
3
|
+
//!
|
|
4
|
+
//! The driver's libraries are opened at runtime, so the engine starts the same
|
|
5
|
+
//! on a machine without an NVIDIA GPU; there [`Encoder::open`] just fails with
|
|
6
|
+
//! the reason. Synchronous, P frames only, ultra-low-latency tuning: the access
|
|
7
|
+
//! unit is complete when `encode` returns.
|
|
8
|
+
|
|
9
|
+
use crate::convert::I420;
|
|
10
|
+
use crate::encoder::EncodedFrame;
|
|
11
|
+
use crate::h264::REFINE_MAX_QP;
|
|
12
|
+
use anyhow::Result;
|
|
13
|
+
use std::ffi::CStr;
|
|
14
|
+
use std::os::raw::{c_char, c_int};
|
|
15
|
+
|
|
16
|
+
#[repr(C)]
|
|
17
|
+
struct NativeEncoder {
|
|
18
|
+
_private: [u8; 0],
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
extern "C" {
|
|
22
|
+
fn dl_nvenc_create(
|
|
23
|
+
width: c_int,
|
|
24
|
+
height: c_int,
|
|
25
|
+
bitrate_kbps: c_int,
|
|
26
|
+
fps: c_int,
|
|
27
|
+
error: *mut c_char,
|
|
28
|
+
error_len: usize,
|
|
29
|
+
) -> *mut NativeEncoder;
|
|
30
|
+
fn dl_nvenc_encode(encoder: *mut NativeEncoder, i420: *const u8, force_idr: c_int) -> c_int;
|
|
31
|
+
fn dl_nvenc_reconfigure(
|
|
32
|
+
encoder: *mut NativeEncoder,
|
|
33
|
+
bitrate_kbps: c_int,
|
|
34
|
+
max_qp: c_int,
|
|
35
|
+
) -> c_int;
|
|
36
|
+
fn dl_nvenc_packet_data(encoder: *const NativeEncoder) -> *const u8;
|
|
37
|
+
fn dl_nvenc_packet_size(encoder: *const NativeEncoder) -> usize;
|
|
38
|
+
fn dl_nvenc_packet_is_key(encoder: *const NativeEncoder) -> c_int;
|
|
39
|
+
fn dl_nvenc_error(encoder: *const NativeEncoder) -> *const c_char;
|
|
40
|
+
fn dl_nvenc_destroy(encoder: *mut NativeEncoder);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
pub struct Encoder {
|
|
44
|
+
native: *mut NativeEncoder,
|
|
45
|
+
width: usize,
|
|
46
|
+
height: usize,
|
|
47
|
+
bitrate_kbps: u32,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Moved to the pipeline thread and only used there; the shim makes its CUDA
|
|
51
|
+
// context current around each call rather than binding it to a thread.
|
|
52
|
+
unsafe impl Send for Encoder {}
|
|
53
|
+
|
|
54
|
+
impl Encoder {
|
|
55
|
+
pub fn open(width: usize, height: usize, bitrate_kbps: u32, fps: u32) -> Result<Self> {
|
|
56
|
+
let mut error = [0 as c_char; 256];
|
|
57
|
+
let native = unsafe {
|
|
58
|
+
dl_nvenc_create(
|
|
59
|
+
width as c_int,
|
|
60
|
+
height as c_int,
|
|
61
|
+
bitrate_kbps.max(1) as c_int,
|
|
62
|
+
fps.max(1) as c_int,
|
|
63
|
+
error.as_mut_ptr(),
|
|
64
|
+
error.len(),
|
|
65
|
+
)
|
|
66
|
+
};
|
|
67
|
+
if native.is_null() {
|
|
68
|
+
let reason = unsafe { CStr::from_ptr(error.as_ptr()) }.to_string_lossy();
|
|
69
|
+
anyhow::bail!("{reason}");
|
|
70
|
+
}
|
|
71
|
+
Ok(Self {
|
|
72
|
+
native,
|
|
73
|
+
width,
|
|
74
|
+
height,
|
|
75
|
+
bitrate_kbps: bitrate_kbps.max(1),
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
fn failure(&self, what: &str) -> anyhow::Error {
|
|
80
|
+
let reason = unsafe { CStr::from_ptr(dl_nvenc_error(self.native)) }.to_string_lossy();
|
|
81
|
+
anyhow::anyhow!("NVENC {what}: {reason}")
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
impl crate::h264::Backend for Encoder {
|
|
86
|
+
fn encode(&mut self, frame: &I420, keyframe: bool) -> Result<EncodedFrame> {
|
|
87
|
+
if frame.width != self.width || frame.height != self.height {
|
|
88
|
+
anyhow::bail!(
|
|
89
|
+
"encoder is {}x{} but was given {}x{}",
|
|
90
|
+
self.width,
|
|
91
|
+
self.height,
|
|
92
|
+
frame.width,
|
|
93
|
+
frame.height
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
if frame.data.len() < self.width * self.height * 3 / 2 {
|
|
97
|
+
anyhow::bail!("an I420 frame of {} bytes is short", frame.data.len());
|
|
98
|
+
}
|
|
99
|
+
if unsafe { dl_nvenc_encode(self.native, frame.data.as_ptr(), keyframe as c_int) } != 1 {
|
|
100
|
+
return Err(self.failure("could not code a frame"));
|
|
101
|
+
}
|
|
102
|
+
let (data, size) = unsafe {
|
|
103
|
+
(
|
|
104
|
+
dl_nvenc_packet_data(self.native),
|
|
105
|
+
dl_nvenc_packet_size(self.native),
|
|
106
|
+
)
|
|
107
|
+
};
|
|
108
|
+
Ok(EncodedFrame {
|
|
109
|
+
data: unsafe { std::slice::from_raw_parts(data, size) }.to_vec(),
|
|
110
|
+
keyframe: unsafe { dl_nvenc_packet_is_key(self.native) } != 0,
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// NVENC caps the quantizer through its rate-control config, which it
|
|
115
|
+
/// changes in place between frames without an IDR: the ceiling goes down
|
|
116
|
+
/// for this one frame and comes back off after it.
|
|
117
|
+
fn refine(&mut self, frame: &I420) -> Result<EncodedFrame> {
|
|
118
|
+
let bitrate = self.bitrate_kbps as c_int;
|
|
119
|
+
if unsafe { dl_nvenc_reconfigure(self.native, bitrate, REFINE_MAX_QP as c_int) } != 0 {
|
|
120
|
+
return Err(self.failure("refused the refinement ceiling"));
|
|
121
|
+
}
|
|
122
|
+
let coded = self.encode(frame, false);
|
|
123
|
+
if unsafe { dl_nvenc_reconfigure(self.native, bitrate, 0) } != 0 {
|
|
124
|
+
return Err(self.failure("refused to lift the refinement ceiling"));
|
|
125
|
+
}
|
|
126
|
+
coded
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
fn set_bitrate(&mut self, bitrate_kbps: u32) -> Result<()> {
|
|
130
|
+
let bitrate_kbps = bitrate_kbps.max(1);
|
|
131
|
+
if unsafe { dl_nvenc_reconfigure(self.native, bitrate_kbps as c_int, 0) } != 0 {
|
|
132
|
+
return Err(self.failure(&format!("refused a {bitrate_kbps} kbps target")));
|
|
133
|
+
}
|
|
134
|
+
self.bitrate_kbps = bitrate_kbps;
|
|
135
|
+
Ok(())
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
impl Drop for Encoder {
|
|
140
|
+
fn drop(&mut self) {
|
|
141
|
+
unsafe { dl_nvenc_destroy(self.native) };
|
|
142
|
+
}
|
|
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
|
+
}
|