@desklink/host 0.1.0
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/LICENSE +202 -0
- package/NOTICE +44 -0
- package/README.md +266 -0
- package/bin/desklink-host.mjs +113 -0
- package/dist/bridge.d.ts +57 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/bridge.js +208 -0
- package/dist/bridge.js.map +1 -0
- package/dist/engineProcess.d.ts +82 -0
- package/dist/engineProcess.d.ts.map +1 -0
- package/dist/engineProcess.js +243 -0
- package/dist/engineProcess.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol.d.ts +169 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +18 -0
- package/dist/protocol.js.map +1 -0
- package/dist/resolveEngine.d.ts +34 -0
- package/dist/resolveEngine.d.ts.map +1 -0
- package/dist/resolveEngine.js +103 -0
- package/dist/resolveEngine.js.map +1 -0
- package/docs/BOUNDARY.md +145 -0
- package/docs/PROTOCOL.md +363 -0
- package/engine/Cargo.lock +3077 -0
- package/engine/Cargo.toml +37 -0
- package/engine/build.rs +107 -0
- package/engine/native/inputtino_shim.cpp +11 -0
- package/engine/native/vpx_shim.c +184 -0
- package/engine/native/wrapper.h +7 -0
- package/engine/src/capture.rs +510 -0
- package/engine/src/clipboard.rs +132 -0
- package/engine/src/convert.rs +299 -0
- package/engine/src/encoder.rs +204 -0
- package/engine/src/input.rs +355 -0
- package/engine/src/keymap.rs +338 -0
- package/engine/src/main.rs +614 -0
- package/engine/src/peer.rs +775 -0
- package/engine/src/portal.rs +115 -0
- package/engine/src/protocol.rs +331 -0
- package/engine/src/session.rs +2159 -0
- package/engine/src/x11.rs +244 -0
- package/engine/vendor/inputtino/.clang-format +166 -0
- package/engine/vendor/inputtino/.clang-tidy +30 -0
- package/engine/vendor/inputtino/.dockerignore +11 -0
- package/engine/vendor/inputtino/.github/workflows/mirror-repo.yml +14 -0
- package/engine/vendor/inputtino/.github/workflows/python-lint-format-test-typecheck.yml +124 -0
- package/engine/vendor/inputtino/.github/workflows/python-test-install.yaml +49 -0
- package/engine/vendor/inputtino/.github/workflows/rust.yml +40 -0
- package/engine/vendor/inputtino/CMakeLists.txt +187 -0
- package/engine/vendor/inputtino/LICENSE +21 -0
- package/engine/vendor/inputtino/README.md +110 -0
- package/engine/vendor/inputtino/bindings/python/.devcontainer/Dockerfile +23 -0
- package/engine/vendor/inputtino/bindings/python/.devcontainer/devcontainer.json +28 -0
- package/engine/vendor/inputtino/bindings/python/.python-version +1 -0
- package/engine/vendor/inputtino/bindings/python/CMakeLists.txt +23 -0
- package/engine/vendor/inputtino/bindings/python/CPPLINT.cfg +2 -0
- package/engine/vendor/inputtino/bindings/python/Makefile +49 -0
- package/engine/vendor/inputtino/bindings/python/README.md +215 -0
- package/engine/vendor/inputtino/bindings/python/pyproject.toml +178 -0
- package/engine/vendor/inputtino/bindings/python/src/inputtino/__init__.py +43 -0
- package/engine/vendor/inputtino/bindings/python/src/inputtino/_core.pyi +533 -0
- package/engine/vendor/inputtino/bindings/python/src/inputtino/base.py +63 -0
- package/engine/vendor/inputtino/bindings/python/src/inputtino/joypad.py +248 -0
- package/engine/vendor/inputtino/bindings/python/src/inputtino/keyboard.py +256 -0
- package/engine/vendor/inputtino/bindings/python/src/inputtino/mouse.py +106 -0
- package/engine/vendor/inputtino/bindings/python/src/inputtino/pentablet.py +73 -0
- package/engine/vendor/inputtino/bindings/python/src/inputtino/py.typed +0 -0
- package/engine/vendor/inputtino/bindings/python/src/inputtino/touchscreen.py +61 -0
- package/engine/vendor/inputtino/bindings/python/src/inputtino/trackpad.py +74 -0
- package/engine/vendor/inputtino/bindings/python/src/main.cpp +217 -0
- package/engine/vendor/inputtino/bindings/python/tests/__init__.py +0 -0
- package/engine/vendor/inputtino/bindings/python/tests/helpers.py +3 -0
- package/engine/vendor/inputtino/bindings/python/tests/test_base.py +60 -0
- package/engine/vendor/inputtino/bindings/python/tests/test_inputtino.py +5 -0
- package/engine/vendor/inputtino/bindings/python/tests/test_joypad.py +332 -0
- package/engine/vendor/inputtino/bindings/python/tests/test_keyboard.py +164 -0
- package/engine/vendor/inputtino/bindings/python/tests/test_mouse.py +185 -0
- package/engine/vendor/inputtino/bindings/python/tests/test_pentablet.py +198 -0
- package/engine/vendor/inputtino/bindings/python/tests/test_touchscreen.py +144 -0
- package/engine/vendor/inputtino/bindings/python/tests/test_trackpad.py +146 -0
- package/engine/vendor/inputtino/bindings/python/uv.lock +390 -0
- package/engine/vendor/inputtino/bindings/rust/Cargo.toml +6 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/.dockerignore +1 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/Cargo.toml +18 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/examples/keyboard.rs +33 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/examples/mouse.rs +17 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/src/common.rs +116 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/src/error.rs +17 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/src/joypad.rs +74 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/src/joypad_nintendo.rs +135 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/src/joypad_ps5.rs +280 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/src/joypad_xbox.rs +134 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/src/keyboard.rs +76 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/src/lib.rs +32 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/src/mouse.rs +178 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/tests/common.rs +38 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/tests/joypads.rs +150 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/tests/keyboard.rs +73 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/tests/mouse.rs +139 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino-sys/.dockerignore +1 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino-sys/Cargo.toml +12 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino-sys/build.rs +48 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino-sys/src/lib.rs +5 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino-sys/wrapper.h +1 -0
- package/engine/vendor/inputtino/cmake/utils.cmake +9 -0
- package/engine/vendor/inputtino/docker/Dockerfile +70 -0
- package/engine/vendor/inputtino/include/inputtino/input.h +324 -0
- package/engine/vendor/inputtino/include/inputtino/input.hpp +466 -0
- package/engine/vendor/inputtino/include/inputtino/result.hpp +37 -0
- package/engine/vendor/inputtino/share/pkgconfig/libinputtino.pc.in +17 -0
- package/engine/vendor/inputtino/src/c-bindings/helpers.hpp +19 -0
- package/engine/vendor/inputtino/src/c-bindings/joypad_ps5.cpp +115 -0
- package/engine/vendor/inputtino/src/c-bindings/joypad_switch.cpp +63 -0
- package/engine/vendor/inputtino/src/c-bindings/joypad_xone.cpp +62 -0
- package/engine/vendor/inputtino/src/c-bindings/keyboard.cpp +42 -0
- package/engine/vendor/inputtino/src/c-bindings/mouse.cpp +67 -0
- package/engine/vendor/inputtino/src/c-bindings/pen_tablet.cpp +50 -0
- package/engine/vendor/inputtino/src/c-bindings/touchscreen.cpp +43 -0
- package/engine/vendor/inputtino/src/c-bindings/trackpad.cpp +49 -0
- package/engine/vendor/inputtino/src/uhid/README.adoc +63 -0
- package/engine/vendor/inputtino/src/uhid/include/crc32.hpp +38 -0
- package/engine/vendor/inputtino/src/uhid/include/uhid/protected_types.hpp +36 -0
- package/engine/vendor/inputtino/src/uhid/include/uhid/ps5.hpp +550 -0
- package/engine/vendor/inputtino/src/uhid/include/uhid/uhid.hpp +168 -0
- package/engine/vendor/inputtino/src/uhid/joypad_ps5.cpp +580 -0
- package/engine/vendor/inputtino/src/uinput/include/inputtino/keyboard.hpp +84 -0
- package/engine/vendor/inputtino/src/uinput/include/inputtino/protected_ps5_types.hpp +7 -0
- package/engine/vendor/inputtino/src/uinput/include/inputtino/protected_types.hpp +110 -0
- package/engine/vendor/inputtino/src/uinput/joypad_nintendo.cpp +190 -0
- package/engine/vendor/inputtino/src/uinput/joypad_ps.cpp +203 -0
- package/engine/vendor/inputtino/src/uinput/joypad_utils.hpp +246 -0
- package/engine/vendor/inputtino/src/uinput/joypad_xbox.cpp +190 -0
- package/engine/vendor/inputtino/src/uinput/keyboard.cpp +120 -0
- package/engine/vendor/inputtino/src/uinput/mouse.cpp +199 -0
- package/engine/vendor/inputtino/src/uinput/pentablet.cpp +162 -0
- package/engine/vendor/inputtino/src/uinput/touchscreen.cpp +140 -0
- package/engine/vendor/inputtino/src/uinput/trackpad.cpp +192 -0
- package/examples/reference-client.html +428 -0
- package/package.json +38 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
//! Pixel conversion and downscale into the I420 plane layout libvpx encodes.
|
|
2
|
+
//!
|
|
3
|
+
//! Deliberately dependency-free: the source format a compositor actually offers
|
|
4
|
+
//! over PipeWire is a packed 4-byte one (`BGRx`/`RGBx`/`BGRA`/`RGBA`), and a box
|
|
5
|
+
//! downscale is the correct filter when the encoded surface is smaller than the
|
|
6
|
+
//! source — it is the only one that does not alias the desktop's text.
|
|
7
|
+
|
|
8
|
+
use crate::capture::PixelFormat;
|
|
9
|
+
|
|
10
|
+
/// A tightly packed I420 frame; the plane strides are the frame width/2 so the
|
|
11
|
+
/// buffer is exactly what `vpx_image_t` wants when we hand libvpx three planes.
|
|
12
|
+
pub struct I420 {
|
|
13
|
+
pub width: usize,
|
|
14
|
+
pub height: usize,
|
|
15
|
+
pub data: Vec<u8>,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
impl I420 {
|
|
19
|
+
fn new(width: usize, height: usize) -> Self {
|
|
20
|
+
let y = width * height;
|
|
21
|
+
let c = (width / 2) * (height / 2);
|
|
22
|
+
Self {
|
|
23
|
+
width,
|
|
24
|
+
height,
|
|
25
|
+
data: vec![0u8; y + 2 * c],
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
pub fn y_plane(&self) -> &[u8] {
|
|
30
|
+
&self.data[..self.width * self.height]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
pub fn u_plane(&self) -> &[u8] {
|
|
34
|
+
let y = self.width * self.height;
|
|
35
|
+
&self.data[y..y + (self.width / 2) * (self.height / 2)]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
pub fn v_plane(&self) -> &[u8] {
|
|
39
|
+
let y = self.width * self.height;
|
|
40
|
+
let c = (self.width / 2) * (self.height / 2);
|
|
41
|
+
&self.data[y + c..y + 2 * c]
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#[inline]
|
|
46
|
+
fn luma(r: i32, g: i32, b: i32) -> u8 {
|
|
47
|
+
// BT.601 limited range, integer weights (the classic libyuv-equivalent form).
|
|
48
|
+
(((66 * r + 129 * g + 25 * b + 128) >> 8) + 16).clamp(0, 255) as u8
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
#[inline]
|
|
52
|
+
fn chroma(r: i32, g: i32, b: i32) -> (u8, u8) {
|
|
53
|
+
let u = ((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128;
|
|
54
|
+
let v = ((112 * r - 94 * g - 18 * b + 128) >> 8) + 128;
|
|
55
|
+
(u.clamp(0, 255) as u8, v.clamp(0, 255) as u8)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// Fit a source into a box without upscaling and with even dimensions, because
|
|
59
|
+
/// I420 chroma is subsampled and the encoder refuses a frame whose size is not
|
|
60
|
+
/// the one it was built for. This is the single definition of that rule: the
|
|
61
|
+
/// encoder's geometry, the portal frames and the X11 frames all call it.
|
|
62
|
+
pub fn fit(width: usize, height: usize, max_width: usize, max_height: usize) -> (usize, usize) {
|
|
63
|
+
if max_width == 0 || max_height == 0 || (width <= max_width && height <= max_height) {
|
|
64
|
+
return (width & !1, height & !1);
|
|
65
|
+
}
|
|
66
|
+
let scale = f64::min(
|
|
67
|
+
max_width as f64 / width as f64,
|
|
68
|
+
max_height as f64 / height as f64,
|
|
69
|
+
);
|
|
70
|
+
(
|
|
71
|
+
(((width as f64 * scale) as usize) & !1).max(2),
|
|
72
|
+
(((height as f64 * scale) as usize) & !1).max(2),
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// Which bytes of a source pixel are red, green and blue, and how wide it is.
|
|
77
|
+
fn layout(format: PixelFormat) -> Option<(usize, [usize; 3])> {
|
|
78
|
+
match format {
|
|
79
|
+
PixelFormat::Bgrx | PixelFormat::Bgra => Some((4, [2, 1, 0])),
|
|
80
|
+
PixelFormat::Rgbx | PixelFormat::Rgba => Some((4, [0, 1, 2])),
|
|
81
|
+
PixelFormat::Bgr => Some((3, [2, 1, 0])),
|
|
82
|
+
PixelFormat::Rgb => Some((3, [0, 1, 2])),
|
|
83
|
+
PixelFormat::Unsupported => None,
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/// How many threads convert one frame. A 4K desktop is 8 million pixels, which
|
|
88
|
+
/// one core converts in tens of milliseconds, the whole budget of a frame; a few
|
|
89
|
+
/// bands bring that under the encoder's own cost without taking the machine.
|
|
90
|
+
fn bands() -> usize {
|
|
91
|
+
std::thread::available_parallelism()
|
|
92
|
+
.map(|n| n.get())
|
|
93
|
+
.unwrap_or(4)
|
|
94
|
+
.clamp(1, 8)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// Convert `src` (packed RGB of some byte order) to `dst_w`x`dst_h` I420.
|
|
98
|
+
///
|
|
99
|
+
/// At the source's own size every pixel is converted as it is, which is what
|
|
100
|
+
/// keeps text sharp; smaller, each destination pixel is the box average of the
|
|
101
|
+
/// source pixels it covers, the only downscale filter that does not alias text;
|
|
102
|
+
/// larger (a stream a few pixels short of the encoder's size), the nearest
|
|
103
|
+
/// source pixel.
|
|
104
|
+
/// Chroma is the average of each 2x2 block. `src_stride` is in bytes. Returns
|
|
105
|
+
/// `None` for a format we cannot read, so the caller reports an unsupported
|
|
106
|
+
/// source instead of encoding noise.
|
|
107
|
+
pub fn to_i420(
|
|
108
|
+
src: &[u8],
|
|
109
|
+
src_w: usize,
|
|
110
|
+
src_h: usize,
|
|
111
|
+
src_stride: usize,
|
|
112
|
+
format: PixelFormat,
|
|
113
|
+
dst_w: usize,
|
|
114
|
+
dst_h: usize,
|
|
115
|
+
) -> Option<I420> {
|
|
116
|
+
if src_w == 0 || src_h == 0 || dst_w == 0 || dst_h == 0 {
|
|
117
|
+
return None;
|
|
118
|
+
}
|
|
119
|
+
// I420 chroma is subsampled 2x2, so an odd destination would leave a
|
|
120
|
+
// half-populated chroma plane. Even dimensions are the caller's contract and
|
|
121
|
+
// the encoder's; enforcing it here keeps the buffer self-consistent.
|
|
122
|
+
let dst_w = dst_w & !1;
|
|
123
|
+
let dst_h = dst_h & !1;
|
|
124
|
+
if dst_w < 2 || dst_h < 2 {
|
|
125
|
+
return None;
|
|
126
|
+
}
|
|
127
|
+
let (bytes_per_pixel, order) = layout(format)?;
|
|
128
|
+
if src_stride < src_w * bytes_per_pixel
|
|
129
|
+
|| src.len() < src_stride * (src_h - 1) + src_w * bytes_per_pixel
|
|
130
|
+
{
|
|
131
|
+
return None;
|
|
132
|
+
}
|
|
133
|
+
let mut out = I420::new(dst_w, dst_h);
|
|
134
|
+
let (y_plane, chroma) = out.data.split_at_mut(dst_w * dst_h);
|
|
135
|
+
let (u_plane, v_plane) = chroma.split_at_mut((dst_w / 2) * (dst_h / 2));
|
|
136
|
+
|
|
137
|
+
// Bands of whole row pairs, so each thread owns disjoint luma and chroma rows.
|
|
138
|
+
let pairs = dst_h / 2;
|
|
139
|
+
let per_band = pairs.div_ceil(bands());
|
|
140
|
+
let cw = dst_w / 2;
|
|
141
|
+
let source = Source {
|
|
142
|
+
data: src,
|
|
143
|
+
width: src_w,
|
|
144
|
+
height: src_h,
|
|
145
|
+
stride: src_stride,
|
|
146
|
+
bytes_per_pixel,
|
|
147
|
+
order,
|
|
148
|
+
};
|
|
149
|
+
std::thread::scope(|scope| {
|
|
150
|
+
let y_bands = y_plane.chunks_mut(per_band * 2 * dst_w);
|
|
151
|
+
let u_bands = u_plane.chunks_mut(per_band * cw);
|
|
152
|
+
let v_bands = v_plane.chunks_mut(per_band * cw);
|
|
153
|
+
for (band, ((y, u), v)) in y_bands.zip(u_bands).zip(v_bands).enumerate() {
|
|
154
|
+
let source = &source;
|
|
155
|
+
scope.spawn(move || {
|
|
156
|
+
let first_pair = band * per_band;
|
|
157
|
+
for pair in 0..u.len() / cw {
|
|
158
|
+
let dy = (first_pair + pair) * 2;
|
|
159
|
+
let (top, bottom) =
|
|
160
|
+
y[pair * 2 * dst_w..(pair + 1) * 2 * dst_w].split_at_mut(dst_w);
|
|
161
|
+
let u_row = &mut u[pair * cw..(pair + 1) * cw];
|
|
162
|
+
let v_row = &mut v[pair * cw..(pair + 1) * cw];
|
|
163
|
+
source.row_pair(dy, dst_w, dst_h, top, bottom, u_row, v_row);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
Some(out)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
struct Source<'a> {
|
|
172
|
+
data: &'a [u8],
|
|
173
|
+
width: usize,
|
|
174
|
+
height: usize,
|
|
175
|
+
stride: usize,
|
|
176
|
+
bytes_per_pixel: usize,
|
|
177
|
+
order: [usize; 3],
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
impl Source<'_> {
|
|
181
|
+
#[inline]
|
|
182
|
+
fn rgb(&self, x: usize, y: usize) -> (i32, i32, i32) {
|
|
183
|
+
let i = y * self.stride + x * self.bytes_per_pixel;
|
|
184
|
+
let p = &self.data[i..i + 3];
|
|
185
|
+
(
|
|
186
|
+
p[self.order[0]] as i32,
|
|
187
|
+
p[self.order[1]] as i32,
|
|
188
|
+
p[self.order[2]] as i32,
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/// The average colour of the source pixels destination pixel (dx, dy) covers.
|
|
193
|
+
#[inline]
|
|
194
|
+
fn sample(&self, dx: usize, dy: usize, dst_w: usize, dst_h: usize) -> (i32, i32, i32) {
|
|
195
|
+
if dst_w == self.width && dst_h == self.height {
|
|
196
|
+
return self.rgb(dx, dy);
|
|
197
|
+
}
|
|
198
|
+
let x0 = dx * self.width / dst_w;
|
|
199
|
+
let x1 = ((dx + 1) * self.width)
|
|
200
|
+
.div_ceil(dst_w)
|
|
201
|
+
.clamp(x0 + 1, self.width);
|
|
202
|
+
let y0 = dy * self.height / dst_h;
|
|
203
|
+
let y1 = ((dy + 1) * self.height)
|
|
204
|
+
.div_ceil(dst_h)
|
|
205
|
+
.clamp(y0 + 1, self.height);
|
|
206
|
+
let (mut r, mut g, mut b) = (0i32, 0i32, 0i32);
|
|
207
|
+
for y in y0..y1 {
|
|
208
|
+
for x in x0..x1 {
|
|
209
|
+
let (pr, pg, pb) = self.rgb(x, y);
|
|
210
|
+
r += pr;
|
|
211
|
+
g += pg;
|
|
212
|
+
b += pb;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
let n = ((x1 - x0) * (y1 - y0)) as i32;
|
|
216
|
+
(r / n, g / n, b / n)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/// Two destination rows and the chroma row they share.
|
|
220
|
+
#[allow(clippy::too_many_arguments)]
|
|
221
|
+
fn row_pair(
|
|
222
|
+
&self,
|
|
223
|
+
dy: usize,
|
|
224
|
+
dst_w: usize,
|
|
225
|
+
dst_h: usize,
|
|
226
|
+
top: &mut [u8],
|
|
227
|
+
bottom: &mut [u8],
|
|
228
|
+
u: &mut [u8],
|
|
229
|
+
v: &mut [u8],
|
|
230
|
+
) {
|
|
231
|
+
for cx in 0..dst_w / 2 {
|
|
232
|
+
let dx = cx * 2;
|
|
233
|
+
let a = self.sample(dx, dy, dst_w, dst_h);
|
|
234
|
+
let b = self.sample(dx + 1, dy, dst_w, dst_h);
|
|
235
|
+
let c = self.sample(dx, dy + 1, dst_w, dst_h);
|
|
236
|
+
let d = self.sample(dx + 1, dy + 1, dst_w, dst_h);
|
|
237
|
+
top[dx] = luma(a.0, a.1, a.2);
|
|
238
|
+
top[dx + 1] = luma(b.0, b.1, b.2);
|
|
239
|
+
bottom[dx] = luma(c.0, c.1, c.2);
|
|
240
|
+
bottom[dx + 1] = luma(d.0, d.1, d.2);
|
|
241
|
+
let (cu, cv) = chroma(
|
|
242
|
+
(a.0 + b.0 + c.0 + d.0 + 2) >> 2,
|
|
243
|
+
(a.1 + b.1 + c.1 + d.1 + 2) >> 2,
|
|
244
|
+
(a.2 + b.2 + c.2 + d.2 + 2) >> 2,
|
|
245
|
+
);
|
|
246
|
+
u[cx] = cu;
|
|
247
|
+
v[cx] = cv;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
#[cfg(test)]
|
|
253
|
+
mod tests {
|
|
254
|
+
use super::*;
|
|
255
|
+
|
|
256
|
+
#[test]
|
|
257
|
+
fn downscale_keeps_average_colour_and_plane_sizes() {
|
|
258
|
+
// 4x4 opaque red desktop in BGRx, downscaled to 2x2.
|
|
259
|
+
let mut src = vec![0u8; 4 * 16];
|
|
260
|
+
for px in src.chunks_exact_mut(4) {
|
|
261
|
+
px[0] = 0; // B
|
|
262
|
+
px[1] = 0; // G
|
|
263
|
+
px[2] = 255; // R
|
|
264
|
+
px[3] = 255; // X
|
|
265
|
+
}
|
|
266
|
+
let out = to_i420(&src, 4, 4, 16, PixelFormat::Bgrx, 2, 2).unwrap();
|
|
267
|
+
assert_eq!(out.data.len(), 4 + 2 * 1);
|
|
268
|
+
assert!(
|
|
269
|
+
out.y_plane().iter().all(|&y| y > 70),
|
|
270
|
+
"red luma should be high"
|
|
271
|
+
);
|
|
272
|
+
assert!(out.u_plane().iter().all(|&u| u < 128), "red has low Cb");
|
|
273
|
+
assert!(out.v_plane().iter().all(|&v| v > 200), "red has high Cr");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
#[test]
|
|
277
|
+
fn odd_destination_is_clamped_to_whole_chroma_blocks() {
|
|
278
|
+
let src = vec![255u8; 4 * 16];
|
|
279
|
+
let out = to_i420(&src, 4, 4, 16, PixelFormat::Bgrx, 3, 3).unwrap();
|
|
280
|
+
assert_eq!((out.width, out.height), (2, 2));
|
|
281
|
+
assert_eq!(out.data.len(), 4 + 2 * 1);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
#[test]
|
|
285
|
+
fn a_stream_a_few_pixels_off_the_encoders_size_is_converted_to_exactly_that_size() {
|
|
286
|
+
// A fractional-scaled display reports one size to the portal and
|
|
287
|
+
// streams another; the encoder only accepts its own.
|
|
288
|
+
let src = vec![200u8; 4 * 1704 * 1066];
|
|
289
|
+
let out = to_i420(&src, 1704, 1066, 1704 * 4, PixelFormat::Bgrx, 1706, 1066).unwrap();
|
|
290
|
+
assert_eq!((out.width, out.height), (1706, 1066));
|
|
291
|
+
let out = to_i420(&src, 1704, 1066, 1704 * 4, PixelFormat::Bgrx, 1702, 1064).unwrap();
|
|
292
|
+
assert_eq!((out.width, out.height), (1702, 1064));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
#[test]
|
|
296
|
+
fn unsupported_format_is_reported_not_guessed() {
|
|
297
|
+
assert!(to_i420(&vec![0u8; 64], 4, 4, 16, PixelFormat::Unsupported, 2, 2).is_none());
|
|
298
|
+
}
|
|
299
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
//! VP9 encoding, wrapping libvpx's encoder through the C shim in
|
|
2
|
+
//! `native/vpx_shim.c`.
|
|
3
|
+
//!
|
|
4
|
+
//! Real-time, single pass, zero lookahead: one frame in, one packet out, so the
|
|
5
|
+
//! far end never waits on a frame the encoder is still holding. Hardware
|
|
6
|
+
//! encoders are deliberately not used — VP9 hardware encode is vendor-specific
|
|
7
|
+
//! and would put a driver dependency on the machine class.
|
|
8
|
+
//!
|
|
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
|
|
11
|
+
//! coded against the rate target; once it stops, the last frame is coded once
|
|
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.
|
|
14
|
+
|
|
15
|
+
use crate::convert::I420;
|
|
16
|
+
use anyhow::Result;
|
|
17
|
+
use std::os::raw::c_int;
|
|
18
|
+
|
|
19
|
+
#[repr(C)]
|
|
20
|
+
struct NativeEncoder {
|
|
21
|
+
_private: [u8; 0],
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
extern "C" {
|
|
25
|
+
fn dl_vpx_create(
|
|
26
|
+
width: c_int,
|
|
27
|
+
height: c_int,
|
|
28
|
+
bitrate_kbps: c_int,
|
|
29
|
+
fps: c_int,
|
|
30
|
+
threads: c_int,
|
|
31
|
+
cpu_used: c_int,
|
|
32
|
+
) -> *mut NativeEncoder;
|
|
33
|
+
fn dl_vpx_encode(encoder: *mut NativeEncoder, i420: *const u8, force_keyframe: c_int) -> c_int;
|
|
34
|
+
fn dl_vpx_reconfigure(
|
|
35
|
+
encoder: *mut NativeEncoder,
|
|
36
|
+
bitrate_kbps: c_int,
|
|
37
|
+
max_quantizer: c_int,
|
|
38
|
+
) -> c_int;
|
|
39
|
+
fn dl_vpx_motion_max_q() -> c_int;
|
|
40
|
+
fn dl_vpx_packet_data(encoder: *const NativeEncoder) -> *const u8;
|
|
41
|
+
fn dl_vpx_packet_size(encoder: *const NativeEncoder) -> usize;
|
|
42
|
+
fn dl_vpx_packet_is_key(encoder: *const NativeEncoder) -> c_int;
|
|
43
|
+
fn dl_vpx_destroy(encoder: *mut NativeEncoder);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// The quantizer ceiling of a refinement pass (VP9's 0–63 scale). Low enough
|
|
47
|
+
/// that small text is crisp; the pass happens once per still, not per frame.
|
|
48
|
+
const REFINE_MAX_Q: c_int = 10;
|
|
49
|
+
|
|
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;
|
|
53
|
+
|
|
54
|
+
/// One encoded VP9 frame as libvpx produced it.
|
|
55
|
+
pub struct EncodedFrame {
|
|
56
|
+
pub data: Vec<u8>,
|
|
57
|
+
/// A key frame: decodable on its own. The transport has to say so.
|
|
58
|
+
pub keyframe: bool,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
pub struct Encoder {
|
|
62
|
+
native: *mut NativeEncoder,
|
|
63
|
+
width: usize,
|
|
64
|
+
height: usize,
|
|
65
|
+
pub bitrate_kbps: u32,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// The encoder is moved to the pipeline thread and only used there; libvpx has
|
|
69
|
+
// no cross-thread requirements of its own beyond not calling into one context
|
|
70
|
+
// concurrently, which the session's single writer guarantees.
|
|
71
|
+
unsafe impl Send for Encoder {}
|
|
72
|
+
|
|
73
|
+
impl Encoder {
|
|
74
|
+
pub fn new(
|
|
75
|
+
width: usize,
|
|
76
|
+
height: usize,
|
|
77
|
+
bitrate_kbps: u32,
|
|
78
|
+
fps: u32,
|
|
79
|
+
threads: u32,
|
|
80
|
+
) -> Result<Self> {
|
|
81
|
+
let native = unsafe {
|
|
82
|
+
dl_vpx_create(
|
|
83
|
+
width as c_int,
|
|
84
|
+
height as c_int,
|
|
85
|
+
bitrate_kbps as c_int,
|
|
86
|
+
fps.max(1) as c_int,
|
|
87
|
+
threads.max(1) as c_int,
|
|
88
|
+
CPU_USED,
|
|
89
|
+
)
|
|
90
|
+
};
|
|
91
|
+
if native.is_null() {
|
|
92
|
+
anyhow::bail!("libvpx refused to initialise a VP9 encoder for {width}x{height}");
|
|
93
|
+
}
|
|
94
|
+
Ok(Self {
|
|
95
|
+
native,
|
|
96
|
+
width,
|
|
97
|
+
height,
|
|
98
|
+
bitrate_kbps,
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/// Change the rate target for the frames that follow.
|
|
103
|
+
pub fn set_bitrate(&mut self, bitrate_kbps: u32) -> Result<()> {
|
|
104
|
+
let status = unsafe {
|
|
105
|
+
dl_vpx_reconfigure(
|
|
106
|
+
self.native,
|
|
107
|
+
bitrate_kbps.max(1) as c_int,
|
|
108
|
+
dl_vpx_motion_max_q(),
|
|
109
|
+
)
|
|
110
|
+
};
|
|
111
|
+
if status != 0 {
|
|
112
|
+
anyhow::bail!("libvpx refused a {bitrate_kbps} kbps target");
|
|
113
|
+
}
|
|
114
|
+
self.bitrate_kbps = bitrate_kbps.max(1);
|
|
115
|
+
Ok(())
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/// Code `frame` again, unchanged, under the refinement ceiling. The result is
|
|
119
|
+
/// an inter frame that carries only the detail the last one lacked.
|
|
120
|
+
pub fn refine(&mut self, frame: &I420) -> Result<EncodedFrame> {
|
|
121
|
+
let bitrate = self.bitrate_kbps.max(1) as c_int;
|
|
122
|
+
if unsafe { dl_vpx_reconfigure(self.native, bitrate, REFINE_MAX_Q) } != 0 {
|
|
123
|
+
anyhow::bail!("libvpx refused the refinement ceiling");
|
|
124
|
+
}
|
|
125
|
+
let packet = self.encode(frame, false);
|
|
126
|
+
let restored = unsafe { dl_vpx_reconfigure(self.native, bitrate, dl_vpx_motion_max_q()) };
|
|
127
|
+
if restored != 0 {
|
|
128
|
+
anyhow::bail!("libvpx refused to restore the motion ceiling");
|
|
129
|
+
}
|
|
130
|
+
packet
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
pub fn encode(&mut self, frame: &I420, force_keyframe: bool) -> Result<EncodedFrame> {
|
|
134
|
+
if frame.width != self.width || frame.height != self.height {
|
|
135
|
+
anyhow::bail!(
|
|
136
|
+
"encoder is {}x{} but was given {}x{}",
|
|
137
|
+
self.width,
|
|
138
|
+
self.height,
|
|
139
|
+
frame.width,
|
|
140
|
+
frame.height
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
let status =
|
|
144
|
+
unsafe { dl_vpx_encode(self.native, frame.data.as_ptr(), force_keyframe as c_int) };
|
|
145
|
+
if status < 0 {
|
|
146
|
+
anyhow::bail!("libvpx rejected a {}x{} frame", self.width, self.height);
|
|
147
|
+
}
|
|
148
|
+
if status == 0 {
|
|
149
|
+
anyhow::bail!("libvpx produced no packet for a frame");
|
|
150
|
+
}
|
|
151
|
+
let size = unsafe { dl_vpx_packet_size(self.native) };
|
|
152
|
+
let data = unsafe { dl_vpx_packet_data(self.native) };
|
|
153
|
+
if data.is_null() || size == 0 {
|
|
154
|
+
anyhow::bail!("libvpx reported a zero-length packet");
|
|
155
|
+
}
|
|
156
|
+
let bytes = unsafe { std::slice::from_raw_parts(data, size) }.to_vec();
|
|
157
|
+
let keyframe = unsafe { dl_vpx_packet_is_key(self.native) } != 0;
|
|
158
|
+
Ok(EncodedFrame {
|
|
159
|
+
data: bytes,
|
|
160
|
+
keyframe,
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
impl Drop for Encoder {
|
|
166
|
+
fn drop(&mut self) {
|
|
167
|
+
if !self.native.is_null() {
|
|
168
|
+
unsafe { dl_vpx_destroy(self.native) };
|
|
169
|
+
self.native = std::ptr::null_mut();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
impl std::fmt::Debug for Encoder {
|
|
175
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
176
|
+
write!(
|
|
177
|
+
f,
|
|
178
|
+
"Encoder({}x{}, {}kbps)",
|
|
179
|
+
self.width, self.height, self.bitrate_kbps
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
#[cfg(test)]
|
|
185
|
+
mod tests {
|
|
186
|
+
use super::*;
|
|
187
|
+
|
|
188
|
+
fn blank(width: usize, height: usize) -> I420 {
|
|
189
|
+
I420 {
|
|
190
|
+
width,
|
|
191
|
+
height,
|
|
192
|
+
data: vec![128u8; width * height + 2 * (width / 2) * (height / 2)],
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
#[test]
|
|
197
|
+
fn encodes_successive_frames_and_refuses_a_mismatched_geometry() {
|
|
198
|
+
let (w, h) = (64usize, 64usize);
|
|
199
|
+
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());
|
|
203
|
+
}
|
|
204
|
+
}
|