@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.
Files changed (82) hide show
  1. package/NOTICE +26 -0
  2. package/README.md +134 -56
  3. package/bin/desklink-host.mjs +13 -5
  4. package/dist/bridge.d.ts +1 -0
  5. package/dist/bridge.d.ts.map +1 -1
  6. package/dist/bridge.js +12 -4
  7. package/dist/bridge.js.map +1 -1
  8. package/dist/engineProcess.d.ts +6 -1
  9. package/dist/engineProcess.d.ts.map +1 -1
  10. package/dist/engineProcess.js +35 -4
  11. package/dist/engineProcess.js.map +1 -1
  12. package/dist/protocol.d.ts +58 -0
  13. package/dist/protocol.d.ts.map +1 -1
  14. package/dist/protocol.js.map +1 -1
  15. package/dist/resolveEngine.d.ts +7 -1
  16. package/dist/resolveEngine.d.ts.map +1 -1
  17. package/dist/resolveEngine.js +66 -14
  18. package/dist/resolveEngine.js.map +1 -1
  19. package/docs/BOUNDARY.md +15 -7
  20. package/docs/PROTOCOL.md +267 -27
  21. package/engine/Cargo.lock +47 -2
  22. package/engine/Cargo.toml +18 -13
  23. package/engine/build.rs +86 -4
  24. package/engine/native/agent_overlay_mac.m +51 -4
  25. package/engine/native/agent_overlay_wayland.c +145 -12
  26. package/engine/native/agent_overlay_x11.c +72 -0
  27. package/engine/native/nvenc_shim.c +453 -0
  28. package/engine/native/vaapi_shim.c +727 -0
  29. package/engine/native/vpx_shim.c +54 -4
  30. package/engine/native/vt_shim.c +448 -0
  31. package/engine/src/capture.rs +167 -58
  32. package/engine/src/capture_mac.rs +24 -14
  33. package/engine/src/capture_win.rs +23 -0
  34. package/engine/src/clipboard_win.rs +8 -0
  35. package/engine/src/convert.rs +286 -61
  36. package/engine/src/cursor.rs +118 -0
  37. package/engine/src/encoder.rs +122 -13
  38. package/engine/src/h264.rs +188 -0
  39. package/engine/src/h264_nvenc.rs +308 -0
  40. package/engine/src/h264_openh264.rs +735 -0
  41. package/engine/src/h264_vaapi.rs +263 -0
  42. package/engine/src/h264_vt.rs +368 -0
  43. package/engine/src/indicator.rs +104 -3
  44. package/engine/src/input_win.rs +91 -0
  45. package/engine/src/keymap_win.rs +39 -0
  46. package/engine/src/mac.rs +34 -9
  47. package/engine/src/main.rs +186 -26
  48. package/engine/src/peer.rs +466 -28
  49. package/engine/src/portal.rs +42 -4
  50. package/engine/src/portal_win.rs +37 -0
  51. package/engine/src/protocol.rs +64 -2
  52. package/engine/src/session.rs +1252 -105
  53. package/engine/src/win.rs +110 -0
  54. package/engine/src/x11.rs +225 -9
  55. package/engine/src/x11_mac.rs +12 -0
  56. package/engine/vendor/libva/COPYING +19 -0
  57. package/engine/vendor/libva/va/va.h +5450 -0
  58. package/engine/vendor/libva/va/va_dec_av1.h +695 -0
  59. package/engine/vendor/libva/va/va_dec_hevc.h +616 -0
  60. package/engine/vendor/libva/va/va_dec_jpeg.h +157 -0
  61. package/engine/vendor/libva/va/va_dec_vp8.h +249 -0
  62. package/engine/vendor/libva/va/va_dec_vp9.h +312 -0
  63. package/engine/vendor/libva/va/va_dec_vvc.h +688 -0
  64. package/engine/vendor/libva/va/va_drm.h +61 -0
  65. package/engine/vendor/libva/va/va_enc_av1.h +1010 -0
  66. package/engine/vendor/libva/va/va_enc_h264.h +659 -0
  67. package/engine/vendor/libva/va/va_enc_hevc.h +958 -0
  68. package/engine/vendor/libva/va/va_enc_jpeg.h +162 -0
  69. package/engine/vendor/libva/va/va_enc_mpeg2.h +308 -0
  70. package/engine/vendor/libva/va/va_enc_vp8.h +350 -0
  71. package/engine/vendor/libva/va/va_enc_vp9.h +639 -0
  72. package/engine/vendor/libva/va/va_fei.h +170 -0
  73. package/engine/vendor/libva/va/va_fei_h264.h +505 -0
  74. package/engine/vendor/libva/va/va_fei_hevc.h +254 -0
  75. package/engine/vendor/libva/va/va_prot.h +525 -0
  76. package/engine/vendor/libva/va/va_version.h +87 -0
  77. package/engine/vendor/libva/va/va_vpp.h +1590 -0
  78. package/engine/vendor/nv-codec-headers/LICENSE +24 -0
  79. package/engine/vendor/nv-codec-headers/README +7 -0
  80. package/engine/vendor/nv-codec-headers/include/ffnvcodec/nvEncodeAPI.h +3825 -0
  81. package/examples/reference-client.html +1 -1
  82. package/package.json +4 -3
@@ -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
- //! only on request, and a refinement pass. While the desktop moves, frames are
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(encoder: *mut NativeEncoder, i420: *const u8, force_keyframe: c_int) -> c_int;
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. 8 codes a 4K desktop frame well
51
- /// inside a 30 fps budget on a desktop CPU.
52
- const CPU_USED: c_int = 8;
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
- pub fn encode(&mut self, frame: &I420, force_keyframe: bool) -> Result<EncodedFrame> {
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
- unsafe { dl_vpx_encode(self.native, frame.data.as_ptr(), force_keyframe as c_int) };
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.encode(&blank(w, h), true).unwrap().data.is_empty());
201
- assert!(!encoder.encode(&blank(w, h), false).unwrap().data.is_empty());
202
- assert!(encoder.encode(&blank(32, 32), false).is_err());
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
+ }