@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
@@ -0,0 +1,735 @@
1
+ //! H.264 through Cisco's prebuilt openh264 library, the software fallback for
2
+ //! a Linux machine with no hardware encoder.
3
+ //!
4
+ //! Cisco's patent licence covers only its own binary, downloaded onto the
5
+ //! machine that runs it: so the library is never built from source and never
6
+ //! shipped here. [`fetch_once`] downloads the pinned release from Cisco into
7
+ //! the user's cache, and [`Encoder::open`] loads it with `dlopen` only after
8
+ //! its sha256 matches the pin — a distribution's own libopenh264 is a source
9
+ //! build and is never looked at.
10
+ //!
11
+ //! `DESKLINK_OPENH264=off` disables the backend; any other value names the
12
+ //! library file to load instead of the cached one (still hash-checked).
13
+
14
+ use crate::convert::I420;
15
+ use crate::encoder::EncodedFrame;
16
+ use anyhow::{Context, Result};
17
+ use openh264_sys2::{
18
+ videoFormatI420, videoFrameTypeIDR, DynamicAPI, ISVCEncoder, ISVCEncoderVtbl, SBitrateInfo,
19
+ SEncParamExt, SEncoderStatistics, SFrameBSInfo, SSourcePicture, API, CAMERA_VIDEO_REAL_TIME,
20
+ CONSTANT_ID, ENCODER_OPTION_BITRATE, ENCODER_OPTION_GET_STATISTICS, ENCODER_OPTION_RC_MODE,
21
+ ENCODER_OPTION_TRACE_LEVEL, PRO_BASELINE, RC_BITRATE_MODE, RC_MODES, RC_OFF_MODE,
22
+ SM_FIXEDSLCNUM_SLICE, SM_SINGLE_SLICE, SPATIAL_LAYER_ALL, WELS_LOG_QUIET,
23
+ };
24
+ use sha2::{Digest, Sha256};
25
+ use std::io::{Read, Write};
26
+ use std::os::raw::{c_int, c_void};
27
+ use std::path::PathBuf;
28
+ use std::ptr::NonNull;
29
+ use std::sync::OnceLock;
30
+ use std::time::{Duration, Instant};
31
+
32
+ /// The Cisco release the bindings were generated against, as (file name,
33
+ /// sha256 of the decompressed library). Hashes checked against Cisco's
34
+ /// download; the `openh264-sys2` crate lists the same ones.
35
+ #[cfg(target_arch = "x86_64")]
36
+ const BLOB: Option<(&str, &str)> = Some((
37
+ "libopenh264-2.6.0-linux64.8.so",
38
+ "2f0cde7c6a6abcf5cae76942894ea42897fa677bce4ed6c91a24dd1b041d5f04",
39
+ ));
40
+ #[cfg(target_arch = "aarch64")]
41
+ const BLOB: Option<(&str, &str)> = Some((
42
+ "libopenh264-2.6.0-linux-arm64.8.so",
43
+ "12e7b33623667cdab0e575170c147b1b36eadb77d0d2aa7ceb5afd3e58902140",
44
+ ));
45
+ #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
46
+ const BLOB: Option<(&str, &str)> = None;
47
+
48
+ /// Plain HTTP: the pinned hash is the integrity check, so TLS would add a large
49
+ /// dependency and no assurance. Cisco serves the same files over both.
50
+ const HOST: &str = "ciscobinary.openh264.org";
51
+
52
+ pub struct Encoder {
53
+ native: NonNull<ISVCEncoder>,
54
+ /// Copy of the encoder's vtable, validated at open. Every call goes
55
+ /// through this, so no path dereferences the library's pointers unchecked.
56
+ vtable: ISVCEncoderVtbl,
57
+ width: usize,
58
+ height: usize,
59
+ fps: u32,
60
+ frames: u64,
61
+ /// The last frame was a refinement, coded with rate control off.
62
+ refined: bool,
63
+ bitstream: Box<SFrameBSInfo>,
64
+ /// Keeps the library mapped while `native` lives; dropped after it.
65
+ _api: DynamicAPI,
66
+ }
67
+
68
+ // Moved to the pipeline thread and used only there; openh264 asks nothing more
69
+ // of a caller than not entering one encoder concurrently.
70
+ unsafe impl Send for Encoder {}
71
+
72
+ impl Encoder {
73
+ pub fn open(width: usize, height: usize, bitrate_kbps: u32, fps: u32) -> Result<Self> {
74
+ let path = library()?;
75
+ let (_, sha256) =
76
+ BLOB.context("Cisco publishes no openh264 build for this architecture")?;
77
+ let bytes = std::fs::read(&path)
78
+ .with_context(|| format!("openh264 is not in place at {}", path.display()))?;
79
+ if hex(&Sha256::digest(&bytes)) != sha256 {
80
+ anyhow::bail!("{} is not Cisco's openh264 2.6.0", path.display());
81
+ }
82
+ drop(bytes);
83
+ let api = DynamicAPI::from_blob_path(&path)
84
+ .map_err(|e| anyhow::anyhow!("cannot load {}: {e}", path.display()))?;
85
+
86
+ let mut raw: *mut ISVCEncoder = std::ptr::null_mut();
87
+ if unsafe { api.WelsCreateSVCEncoder(&mut raw) } != 0 {
88
+ anyhow::bail!("openh264 could not create an encoder");
89
+ }
90
+ let native = NonNull::new(raw).context("openh264 returned a null encoder")?;
91
+ // The handle's first word is its vtable pointer; reject a null one
92
+ // here so no later call dereferences an unchecked pointer.
93
+ let vtable = NonNull::new(unsafe { *native.as_ptr() } as *mut ISVCEncoderVtbl)
94
+ .context("openh264 returned an encoder with no vtable")?;
95
+ let vtable = unsafe { *vtable.as_ptr() };
96
+ let mut encoder = Self {
97
+ native,
98
+ vtable,
99
+ width,
100
+ height,
101
+ fps: fps.max(1),
102
+ frames: 0,
103
+ refined: false,
104
+ bitstream: Box::default(),
105
+ _api: api,
106
+ };
107
+ // Before initialising, so its warnings never reach the terminal either.
108
+ let mut quiet = WELS_LOG_QUIET;
109
+ encoder.option(ENCODER_OPTION_TRACE_LEVEL, &mut quiet)?;
110
+
111
+ let threads = std::thread::available_parallelism().map_or(1, |n| n.get().min(4));
112
+ let mut params = SEncParamExt::default();
113
+ let vt = encoder.vtable();
114
+ let get_defaults = vt
115
+ .GetDefaultParams
116
+ .context("openh264 lacks GetDefaultParams")?;
117
+ let initialize = vt.InitializeExt.context("openh264 lacks InitializeExt")?;
118
+ unsafe { get_defaults(encoder.native.as_ptr(), &mut params) };
119
+ let bps = (bitrate_kbps.max(1) as c_int).saturating_mul(1000);
120
+ // Camera mode, not screen content: the screen mode switches scene
121
+ // change detection back on whatever it is told, and codes an unasked
122
+ // IDR on every large change — every window switch, and every frame of
123
+ // a full-screen scroll. Adaptive quantization is off in both modes.
124
+ params.iUsageType = CAMERA_VIDEO_REAL_TIME;
125
+ params.bEnableSceneChangeDetect = false;
126
+ params.bEnableBackgroundDetection = false;
127
+ params.iPicWidth = width as c_int;
128
+ params.iPicHeight = height as c_int;
129
+ params.iTargetBitrate = bps;
130
+ params.iRCMode = RC_BITRATE_MODE;
131
+ params.fMaxFrameRate = encoder.fps as f32;
132
+ // Every frame must come out: a skipped frame is a frame the far end
133
+ // never gets. The rate is then held by the quantizer alone.
134
+ params.bEnableFrameSkip = false;
135
+ // The whole quantizer range: without frame skipping it is all that
136
+ // holds a low rate. 12 is openh264's own floor.
137
+ params.iMinQp = 12;
138
+ params.iMaxQp = 51;
139
+ params.uiIntraPeriod = 0;
140
+ params.eSpsPpsIdStrategy = CONSTANT_ID;
141
+ params.iEntropyCodingModeFlag = 0;
142
+ params.iTemporalLayerNum = 1;
143
+ params.iSpatialLayerNum = 1;
144
+ params.iMultipleThreadIdc = threads as u16;
145
+ let layer = &mut params.sSpatialLayers[0];
146
+ layer.iVideoWidth = width as c_int;
147
+ layer.iVideoHeight = height as c_int;
148
+ layer.fFrameRate = encoder.fps as f32;
149
+ layer.iSpatialBitrate = bps;
150
+ layer.uiProfileIdc = PRO_BASELINE;
151
+ // The fixed quantizer of the rate-control-off mode `refine` switches to.
152
+ layer.iDLayerQp = crate::h264::REFINE_MAX_QP as c_int;
153
+ // openh264 threads by slice: one slice per thread.
154
+ layer.sSliceArgument.uiSliceMode = if threads > 1 {
155
+ SM_FIXEDSLCNUM_SLICE
156
+ } else {
157
+ SM_SINGLE_SLICE
158
+ };
159
+ layer.sSliceArgument.uiSliceNum = threads as u32;
160
+ if unsafe { initialize(encoder.native.as_ptr(), &params) } != 0 {
161
+ anyhow::bail!("openh264 refused a {width}x{height} encoder at {bitrate_kbps} kbps");
162
+ }
163
+ Ok(encoder)
164
+ }
165
+
166
+ fn vtable(&self) -> &ISVCEncoderVtbl {
167
+ &self.vtable
168
+ }
169
+
170
+ fn option<T>(&mut self, id: openh264_sys2::ENCODER_OPTION, value: &mut T) -> Result<()> {
171
+ let set = self
172
+ .vtable()
173
+ .SetOption
174
+ .context("openh264 lacks SetOption")?;
175
+ if unsafe { set(self.native.as_ptr(), id, value as *mut T as *mut c_void) } != 0 {
176
+ anyhow::bail!("openh264 refused option {id}");
177
+ }
178
+ Ok(())
179
+ }
180
+
181
+ fn rc_mode(&mut self, mut mode: RC_MODES) -> Result<()> {
182
+ self.option(ENCODER_OPTION_RC_MODE, &mut mode)
183
+ }
184
+
185
+ /// The average quantizer of the frame coded last.
186
+ fn last_qp(&self) -> Result<u32> {
187
+ let get = self
188
+ .vtable()
189
+ .GetOption
190
+ .context("openh264 lacks GetOption")?;
191
+ let mut stats = SEncoderStatistics::default();
192
+ let status = unsafe {
193
+ get(
194
+ self.native.as_ptr(),
195
+ ENCODER_OPTION_GET_STATISTICS,
196
+ &mut stats as *mut SEncoderStatistics as *mut c_void,
197
+ )
198
+ };
199
+ if status != 0 {
200
+ anyhow::bail!("openh264 refused its statistics");
201
+ }
202
+ Ok(stats.uiAverageFrameQP)
203
+ }
204
+ }
205
+
206
+ impl crate::h264::Backend for Encoder {
207
+ fn encode(&mut self, frame: &I420, keyframe: bool) -> Result<EncodedFrame> {
208
+ self.refined = false;
209
+ if frame.width != self.width || frame.height != self.height {
210
+ anyhow::bail!(
211
+ "encoder is {}x{} but was given {}x{}",
212
+ self.width,
213
+ self.height,
214
+ frame.width,
215
+ frame.height
216
+ );
217
+ }
218
+ let (w, h) = (self.width, self.height);
219
+ let luma = w * h;
220
+ let chroma = (w / 2) * (h / 2);
221
+ if frame.data.len() < luma + 2 * chroma {
222
+ anyhow::bail!("a {w}x{h} frame needs {} bytes", luma + 2 * chroma);
223
+ }
224
+ let vt = self.vtable();
225
+ let encode = vt.EncodeFrame.context("openh264 lacks EncodeFrame")?;
226
+ let force = vt
227
+ .ForceIntraFrame
228
+ .context("openh264 lacks ForceIntraFrame")?;
229
+ if keyframe && unsafe { force(self.native.as_ptr(), true) } != 0 {
230
+ anyhow::bail!("openh264 refused to force an IDR");
231
+ }
232
+ // The encoder only reads the planes; the API is not const-correct.
233
+ let base = frame.data.as_ptr() as *mut u8;
234
+ let source = SSourcePicture {
235
+ iColorFormat: videoFormatI420 as c_int,
236
+ iStride: [w as c_int, (w / 2) as c_int, (w / 2) as c_int, 0],
237
+ pData: unsafe {
238
+ [
239
+ base,
240
+ base.add(luma),
241
+ base.add(luma + chroma),
242
+ std::ptr::null_mut(),
243
+ ]
244
+ },
245
+ iPicWidth: w as c_int,
246
+ iPicHeight: h as c_int,
247
+ uiTimeStamp: (self.frames * 1000 / self.fps as u64) as i64,
248
+ bPsnrY: false,
249
+ bPsnrU: false,
250
+ bPsnrV: false,
251
+ };
252
+ self.frames += 1;
253
+ if unsafe { encode(self.native.as_ptr(), &source, &mut *self.bitstream) } != 0 {
254
+ anyhow::bail!("openh264 rejected a {w}x{h} frame");
255
+ }
256
+ let info = &*self.bitstream;
257
+ let sizes = validated_layer_sizes(info)?;
258
+ let mut data = Vec::with_capacity(info.iFrameSizeInBytes.max(0) as usize);
259
+ for (layer, &size) in info.sLayerInfo[..sizes.len()].iter().zip(&sizes) {
260
+ if size == 0 {
261
+ continue;
262
+ }
263
+ // Non-null with this length by validated_layer_sizes; copied into
264
+ // the owned Vec so no borrow of the library's buffers escapes.
265
+ data.extend_from_slice(unsafe { std::slice::from_raw_parts(layer.pBsBuf, size) });
266
+ }
267
+ if data.is_empty() {
268
+ anyhow::bail!("openh264 produced no access unit for a frame");
269
+ }
270
+ Ok(EncodedFrame {
271
+ data,
272
+ keyframe: info.eFrameType == videoFrameTypeIDR,
273
+ })
274
+ }
275
+
276
+ /// openh264 cannot change its quantizer ceiling after initialisation, so
277
+ /// the pass runs with rate control off at the fixed quantizer set at open,
278
+ /// [`crate::h264::REFINE_MAX_QP`] (screen content has no adaptive
279
+ /// quantization, so every macroblock gets it), then rate control resumes
280
+ /// without having seen the pass's bits. When rate control is already
281
+ /// coding finer than the ceiling, the pass is an ordinary frame instead.
282
+ fn refine(&mut self, frame: &I420) -> Result<EncodedFrame> {
283
+ // After a refinement the last QP is the refinement's own, which says
284
+ // nothing about where rate control would put this one.
285
+ let packet = if !self.refined && self.last_qp()? <= crate::h264::REFINE_MAX_QP {
286
+ self.encode(frame, false)
287
+ } else {
288
+ self.rc_mode(RC_OFF_MODE)?;
289
+ let packet = self.encode(frame, false);
290
+ self.rc_mode(RC_BITRATE_MODE)?;
291
+ packet
292
+ };
293
+ self.refined = true;
294
+ packet
295
+ }
296
+
297
+ fn set_bitrate(&mut self, bitrate_kbps: u32) -> Result<()> {
298
+ let mut info = SBitrateInfo {
299
+ iLayer: SPATIAL_LAYER_ALL,
300
+ iBitrate: (bitrate_kbps.max(1) as c_int).saturating_mul(1000),
301
+ };
302
+ self.option(ENCODER_OPTION_BITRATE, &mut info)
303
+ }
304
+ }
305
+
306
+ /// Each layer's bitstream size, validated before any slice is built from
307
+ /// what the library handed back: the layer count is clamped to the array,
308
+ /// NAL counts and lengths must be non-negative with their length pointers
309
+ /// present, sums use checked arithmetic, a non-empty layer needs its buffer,
310
+ /// and the running total stays within the reported frame size. Pure over the
311
+ /// header, so tests cover it without the library.
312
+ fn validated_layer_sizes(info: &SFrameBSInfo) -> Result<Vec<usize>> {
313
+ let layers = info.iLayerNum.clamp(0, info.sLayerInfo.len() as c_int) as usize;
314
+ let mut sizes = Vec::with_capacity(layers);
315
+ let mut total = 0usize;
316
+ for layer in &info.sLayerInfo[..layers] {
317
+ if layer.iNalCount < 0 {
318
+ anyhow::bail!("openh264 reported a negative NAL count");
319
+ }
320
+ let count = layer.iNalCount as usize;
321
+ if count > 0 && layer.pNalLengthInByte.is_null() {
322
+ anyhow::bail!("openh264 reported NALs with no lengths");
323
+ }
324
+ let lengths = if count == 0 {
325
+ &[]
326
+ } else {
327
+ unsafe { std::slice::from_raw_parts(layer.pNalLengthInByte, count) }
328
+ };
329
+ let mut size = 0usize;
330
+ for &len in lengths {
331
+ if len < 0 {
332
+ anyhow::bail!("openh264 reported a negative NAL length");
333
+ }
334
+ size = size
335
+ .checked_add(len as usize)
336
+ .context("openh264 NAL lengths overflow")?;
337
+ }
338
+ if size > 0 && layer.pBsBuf.is_null() {
339
+ anyhow::bail!("openh264 reported bytes with no buffer");
340
+ }
341
+ total = total
342
+ .checked_add(size)
343
+ .context("openh264 layer sizes overflow")?;
344
+ if info.iFrameSizeInBytes > 0 && total > info.iFrameSizeInBytes as usize {
345
+ anyhow::bail!("openh264 reported more bytes than the frame holds");
346
+ }
347
+ sizes.push(size);
348
+ }
349
+ Ok(sizes)
350
+ }
351
+
352
+ impl Drop for Encoder {
353
+ fn drop(&mut self) {
354
+ if let Some(uninitialize) = self.vtable().Uninitialize {
355
+ unsafe { uninitialize(self.native.as_ptr()) };
356
+ }
357
+ unsafe { self._api.WelsDestroySVCEncoder(self.native.as_ptr()) };
358
+ }
359
+ }
360
+
361
+ /// Where the library is loaded from, or why it is not.
362
+ fn library() -> Result<PathBuf> {
363
+ match std::env::var_os("DESKLINK_OPENH264") {
364
+ Some(v) if v == "off" => anyhow::bail!("disabled by DESKLINK_OPENH264=off"),
365
+ Some(v) if !v.is_empty() => Ok(PathBuf::from(v)),
366
+ _ => cached().context("no cache directory (neither XDG_CACHE_HOME nor HOME is set)"),
367
+ }
368
+ }
369
+
370
+ /// `$XDG_CACHE_HOME/desklink/openh264/<file>`, falling back to `~/.cache`.
371
+ fn cached() -> Option<PathBuf> {
372
+ let (file, _) = BLOB?;
373
+ let base = std::env::var_os("XDG_CACHE_HOME")
374
+ .map(PathBuf::from)
375
+ .filter(|p| p.is_absolute())
376
+ .or_else(|| Some(PathBuf::from(std::env::var_os("HOME")?).join(".cache")))?;
377
+ Some(base.join("desklink/openh264").join(file))
378
+ }
379
+
380
+ /// Fetch Cisco's library once per process; true when it is now in place.
381
+ pub fn fetch_once() -> bool {
382
+ static FETCHED: OnceLock<bool> = OnceLock::new();
383
+ *FETCHED.get_or_init(|| {
384
+ // An explicit path or `off` is the operator's choice; never override it.
385
+ if std::env::var_os("DESKLINK_OPENH264").is_some_and(|v| !v.is_empty()) {
386
+ return false;
387
+ }
388
+ match fetch() {
389
+ Ok(()) => true,
390
+ Err(error) => {
391
+ eprintln!("openh264: {error:#}");
392
+ false
393
+ }
394
+ }
395
+ })
396
+ }
397
+
398
+ fn fetch() -> Result<()> {
399
+ let (file, sha256) = BLOB.context("Cisco publishes no openh264 build for this architecture")?;
400
+ let target = cached().context("no cache directory (neither XDG_CACHE_HOME nor HOME is set)")?;
401
+ if std::fs::read(&target).is_ok_and(|b| hex(&Sha256::digest(&b)) == sha256) {
402
+ return Ok(());
403
+ }
404
+ let compressed = http_get(&format!("/{file}.bz2"))?;
405
+ let mut library = Vec::new();
406
+ bzip2_rs::DecoderReader::new(compressed.as_slice())
407
+ .take(16 << 20)
408
+ .read_to_end(&mut library)
409
+ .context("Cisco's download is not valid bzip2")?;
410
+ if hex(&Sha256::digest(&library)) != sha256 {
411
+ anyhow::bail!("Cisco's {file} does not match its pinned sha256");
412
+ }
413
+ let dir = target.parent().context("cache path has no parent")?;
414
+ std::fs::create_dir_all(dir).with_context(|| format!("cannot create {}", dir.display()))?;
415
+ let temp = dir.join(format!(".{file}.{}", std::process::id()));
416
+ let written = (|| {
417
+ use std::os::unix::fs::OpenOptionsExt;
418
+ let mut out = std::fs::OpenOptions::new()
419
+ .write(true)
420
+ .create(true)
421
+ .truncate(true)
422
+ .mode(0o644)
423
+ .open(&temp)?;
424
+ out.write_all(&library)?;
425
+ out.sync_all()?;
426
+ std::fs::rename(&temp, &target)
427
+ })();
428
+ if written.is_err() {
429
+ let _ = std::fs::remove_file(&temp);
430
+ }
431
+ written.with_context(|| format!("cannot write {}", target.display()))
432
+ }
433
+
434
+ /// A bounded HTTP/1.0 GET of `path` from [`HOST`] (1.0, so never chunked): 5 s
435
+ /// to connect, 20 s in all. Name resolution is the system resolver's own bound.
436
+ fn http_get(path: &str) -> Result<Vec<u8>> {
437
+ use std::net::{TcpStream, ToSocketAddrs};
438
+ let deadline = Instant::now() + Duration::from_secs(20);
439
+ let addrs = (HOST, 80)
440
+ .to_socket_addrs()
441
+ .with_context(|| format!("cannot resolve {HOST}"))?;
442
+ let mut stream = addrs
443
+ .into_iter()
444
+ .find_map(|a| TcpStream::connect_timeout(&a, Duration::from_secs(5)).ok())
445
+ .with_context(|| format!("cannot connect to {HOST}"))?;
446
+ stream.set_write_timeout(Some(Duration::from_secs(5)))?;
447
+ write!(
448
+ stream,
449
+ "GET {path} HTTP/1.0\r\nHost: {HOST}\r\nConnection: close\r\nUser-Agent: desklink\r\n\r\n"
450
+ )?;
451
+ let mut response = Vec::new();
452
+ let mut chunk = [0u8; 64 * 1024];
453
+ loop {
454
+ let left = deadline.saturating_duration_since(Instant::now());
455
+ if left.is_zero() || response.len() > 8 << 20 {
456
+ anyhow::bail!("download from {HOST} took too long or grew too large");
457
+ }
458
+ stream.set_read_timeout(Some(left))?;
459
+ match stream.read(&mut chunk) {
460
+ Ok(0) => break,
461
+ Ok(n) => response.extend_from_slice(&chunk[..n]),
462
+ Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {}
463
+ Err(e) => return Err(e).context(format!("download from {HOST} failed")),
464
+ }
465
+ }
466
+ let end = response
467
+ .windows(4)
468
+ .position(|w| w == b"\r\n\r\n")
469
+ .context("malformed HTTP response")?;
470
+ let head = String::from_utf8_lossy(&response[..end]).into_owned();
471
+ let status = head.lines().next().unwrap_or_default();
472
+ if status.split_whitespace().nth(1) != Some("200") {
473
+ anyhow::bail!("{HOST} answered {status}");
474
+ }
475
+ let body = response.split_off(end + 4);
476
+ // A truncated body would only fail the hash later; say so plainly here.
477
+ let length = head.lines().find_map(|l| {
478
+ let (name, value) = l.split_once(':')?;
479
+ name.eq_ignore_ascii_case("content-length")
480
+ .then(|| value.trim().parse::<usize>().ok())?
481
+ });
482
+ if length.is_some_and(|n| n != body.len()) {
483
+ anyhow::bail!("download from {HOST} was cut short");
484
+ }
485
+ Ok(body)
486
+ }
487
+
488
+ fn hex(bytes: &[u8]) -> String {
489
+ bytes.iter().map(|b| format!("{b:02x}")).collect()
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
+ }