@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/capture.rs
CHANGED
|
@@ -13,7 +13,7 @@ use pw::spa;
|
|
|
13
13
|
use pw::spa::pod::{serialize::PodSerializer, Pod};
|
|
14
14
|
use std::io::Cursor;
|
|
15
15
|
use std::os::fd::OwnedFd;
|
|
16
|
-
use std::sync::atomic::{AtomicU64, Ordering};
|
|
16
|
+
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
|
17
17
|
use std::sync::{mpsc, Arc, Mutex};
|
|
18
18
|
use std::thread::JoinHandle;
|
|
19
19
|
use std::time::{Duration, Instant};
|
|
@@ -44,13 +44,18 @@ pub struct StreamGeometry {
|
|
|
44
44
|
pub buffer_type: String,
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
/// Receives each converted frame, its sequence number and a way to produce the
|
|
48
|
+
/// frame's packed BGRX pixels, which only a local frame consumer needs; the
|
|
49
|
+
/// sink calls it at most once, and only then are the pixels copied.
|
|
50
|
+
pub type FrameSink = Box<dyn Fn(I420, u64, Pixels) + Send + 'static>;
|
|
51
|
+
pub type Pixels<'a> = &'a mut dyn FnMut() -> Option<Vec<u8>>;
|
|
48
52
|
|
|
49
53
|
/// A running capture. Dropping it quits the PipeWire loop and joins its thread.
|
|
50
54
|
pub struct Capture {
|
|
51
55
|
quit: pw::channel::Sender<()>,
|
|
52
56
|
thread: Option<JoinHandle<()>>,
|
|
53
57
|
pub source: SelectedSource,
|
|
58
|
+
cursor_positions: Arc<AtomicBool>,
|
|
54
59
|
geometry: Arc<Mutex<Option<StreamGeometry>>>,
|
|
55
60
|
frames: Arc<AtomicU64>,
|
|
56
61
|
dropped: Arc<AtomicU64>,
|
|
@@ -59,6 +64,9 @@ pub struct Capture {
|
|
|
59
64
|
}
|
|
60
65
|
|
|
61
66
|
impl Capture {
|
|
67
|
+
pub fn cursor_positions(&self) -> bool {
|
|
68
|
+
self.cursor_positions.load(Ordering::Relaxed)
|
|
69
|
+
}
|
|
62
70
|
pub fn geometry(&self) -> Option<StreamGeometry> {
|
|
63
71
|
self.geometry.lock().ok().and_then(|g| g.clone())
|
|
64
72
|
}
|
|
@@ -189,16 +197,29 @@ fn buffer_pod(buffer: &mut Vec<u8>) -> Result<()> {
|
|
|
189
197
|
}
|
|
190
198
|
|
|
191
199
|
/// Start capturing `session`'s node, delivering I420 frames to `sink`.
|
|
200
|
+
#[allow(clippy::too_many_arguments)]
|
|
192
201
|
pub fn start(
|
|
193
202
|
session: PortalSession,
|
|
194
203
|
encoded_width: usize,
|
|
195
204
|
encoded_height: usize,
|
|
196
205
|
max_fps: u32,
|
|
206
|
+
cursor_sink: Option<crate::cursor::CursorSink>,
|
|
197
207
|
sink: FrameSink,
|
|
198
208
|
indicator_position: Option<Arc<Mutex<Option<(i64, i64)>>>>,
|
|
199
209
|
on_stop: Box<dyn Fn(String) + Send>,
|
|
200
210
|
) -> Result<Capture> {
|
|
201
|
-
let PortalSession {
|
|
211
|
+
let PortalSession {
|
|
212
|
+
fd,
|
|
213
|
+
source,
|
|
214
|
+
cursor_mode,
|
|
215
|
+
..
|
|
216
|
+
} = session;
|
|
217
|
+
let cursor_positions = Arc::new(AtomicBool::new(false));
|
|
218
|
+
let cursor_sink = if cursor_mode == "metadata" {
|
|
219
|
+
cursor_sink
|
|
220
|
+
} else {
|
|
221
|
+
None
|
|
222
|
+
};
|
|
202
223
|
let geometry = Arc::new(Mutex::new(None));
|
|
203
224
|
let frames = Arc::new(AtomicU64::new(0));
|
|
204
225
|
let dropped = Arc::new(AtomicU64::new(0));
|
|
@@ -206,7 +227,9 @@ pub fn start(
|
|
|
206
227
|
let (ready_tx, ready_rx) = mpsc::channel::<Result<(), String>>();
|
|
207
228
|
|
|
208
229
|
let node_id = source.node_id;
|
|
230
|
+
let source_size = (source.width.max(1) as usize, source.height.max(1) as usize);
|
|
209
231
|
let thread = {
|
|
232
|
+
let cursor_positions = cursor_positions.clone();
|
|
210
233
|
let geometry = geometry.clone();
|
|
211
234
|
let frames = frames.clone();
|
|
212
235
|
let dropped = dropped.clone();
|
|
@@ -217,9 +240,12 @@ pub fn start(
|
|
|
217
240
|
let result = run_loop(
|
|
218
241
|
fd,
|
|
219
242
|
node_id,
|
|
243
|
+
source_size,
|
|
220
244
|
encoded_width,
|
|
221
245
|
encoded_height,
|
|
222
246
|
max_fps,
|
|
247
|
+
cursor_sink,
|
|
248
|
+
cursor_positions,
|
|
223
249
|
sink,
|
|
224
250
|
indicator_position,
|
|
225
251
|
on_stop,
|
|
@@ -246,6 +272,7 @@ pub fn start(
|
|
|
246
272
|
quit,
|
|
247
273
|
thread: Some(thread),
|
|
248
274
|
source,
|
|
275
|
+
cursor_positions,
|
|
249
276
|
geometry,
|
|
250
277
|
frames,
|
|
251
278
|
dropped,
|
|
@@ -264,9 +291,12 @@ pub fn start(
|
|
|
264
291
|
fn run_loop(
|
|
265
292
|
fd: OwnedFd,
|
|
266
293
|
node_id: u32,
|
|
294
|
+
source_size: (usize, usize),
|
|
267
295
|
encoded_width: usize,
|
|
268
296
|
encoded_height: usize,
|
|
269
297
|
max_fps: u32,
|
|
298
|
+
cursor_sink: Option<crate::cursor::CursorSink>,
|
|
299
|
+
cursor_positions: Arc<AtomicBool>,
|
|
270
300
|
sink: FrameSink,
|
|
271
301
|
indicator_position: Option<Arc<Mutex<Option<(i64, i64)>>>>,
|
|
272
302
|
on_stop: Box<dyn Fn(String) + Send>,
|
|
@@ -298,6 +328,8 @@ fn run_loop(
|
|
|
298
328
|
let stream = pw::stream::StreamBox::new(&core, "desklink-capture", props)
|
|
299
329
|
.context("pw_stream_new failed")?;
|
|
300
330
|
|
|
331
|
+
let wants_cursor_meta = cursor_sink.is_some();
|
|
332
|
+
let mut cursor = cursor_sink.map(crate::cursor::Reporter::new);
|
|
301
333
|
let mut state = StreamState {
|
|
302
334
|
box_w: encoded_width,
|
|
303
335
|
box_h: encoded_height,
|
|
@@ -316,14 +348,26 @@ fn run_loop(
|
|
|
316
348
|
let mut ready = Some(ready);
|
|
317
349
|
let _listener = stream
|
|
318
350
|
.add_local_listener_with_user_data(&mut state)
|
|
319
|
-
.state_changed({
|
|
320
|
-
let
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
351
|
+
.state_changed({
|
|
352
|
+
let main_loop = main_loop.clone();
|
|
353
|
+
move |_, state, old, new| {
|
|
354
|
+
let reason = match new {
|
|
355
|
+
pw::stream::StreamState::Error(message) => {
|
|
356
|
+
Some(format!("PipeWire stream stopped: {message}"))
|
|
357
|
+
}
|
|
358
|
+
pw::stream::StreamState::Unconnected
|
|
359
|
+
if old == pw::stream::StreamState::Streaming =>
|
|
360
|
+
{
|
|
361
|
+
Some(String::from("PipeWire stream disconnected"))
|
|
362
|
+
}
|
|
363
|
+
_ => None,
|
|
364
|
+
};
|
|
365
|
+
if let Some(reason) = reason {
|
|
366
|
+
(state.on_stop)(reason);
|
|
367
|
+
main_loop.quit();
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
})
|
|
327
371
|
.param_changed(|_, state, id, param| {
|
|
328
372
|
if id != pw::spa::param::ParamType::Format.as_raw() {
|
|
329
373
|
return;
|
|
@@ -351,6 +395,31 @@ fn run_loop(
|
|
|
351
395
|
let Some(mut buffer) = stream.dequeue_buffer() else {
|
|
352
396
|
return;
|
|
353
397
|
};
|
|
398
|
+
if let Some(cursor) = cursor.as_mut() {
|
|
399
|
+
if let Some(meta) = buffer.find_meta::<spa::buffer::meta::MetaCursor>() {
|
|
400
|
+
cursor_positions.store(true, Ordering::Relaxed);
|
|
401
|
+
// The wrapper's size check bounds the bitmap header;
|
|
402
|
+
// never follow an unchecked bitmap offset.
|
|
403
|
+
let bitmap = buffer
|
|
404
|
+
.find_meta::<CursorWithBitmap>()
|
|
405
|
+
.filter(|m| {
|
|
406
|
+
m.cursor.bitmap_offset as usize
|
|
407
|
+
== std::mem::size_of::<spa::sys::spa_meta_cursor>()
|
|
408
|
+
})
|
|
409
|
+
.map(|m| &m.bitmap);
|
|
410
|
+
if state.logical_w == 0 || state.logical_h == 0 {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
let mut position = *meta.as_raw();
|
|
414
|
+
position.position.x = (i64::from(position.position.x) * source_size.0 as i64)
|
|
415
|
+
.div_euclid(state.logical_w as i64)
|
|
416
|
+
as i32;
|
|
417
|
+
position.position.y = (i64::from(position.position.y) * source_size.1 as i64)
|
|
418
|
+
.div_euclid(state.logical_h as i64)
|
|
419
|
+
as i32;
|
|
420
|
+
cursor.update_spa(&position, bitmap, source_size.0, source_size.1);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
354
423
|
let Some(format) = state.format else { return };
|
|
355
424
|
let datas = buffer.datas_mut();
|
|
356
425
|
if datas.is_empty() {
|
|
@@ -383,54 +452,58 @@ fn run_loop(
|
|
|
383
452
|
// second time and hand the encoder a frame it refuses whenever the
|
|
384
453
|
// stream's size differs from the one the portal reported.
|
|
385
454
|
let seq = state.frames.load(Ordering::Relaxed);
|
|
386
|
-
let
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
mask.apply(&mut raw, state.box_w, state.box_h, w, h);
|
|
398
|
-
to_i420(
|
|
399
|
-
&raw,
|
|
400
|
-
state.box_w,
|
|
401
|
-
state.box_h,
|
|
402
|
-
state.box_w * 4,
|
|
403
|
-
PixelFormat::Bgrx,
|
|
404
|
-
state.box_w,
|
|
405
|
-
state.box_h,
|
|
406
|
-
)
|
|
407
|
-
} else {
|
|
408
|
-
to_i420(chunk_bytes, w, h, stride, format, state.box_w, state.box_h)
|
|
455
|
+
let (box_w, box_h) = (state.box_w, state.box_h);
|
|
456
|
+
// The capture indicator is masked out of BGRX pixels, so a masked
|
|
457
|
+
// frame is converted from that copy; otherwise straight from the
|
|
458
|
+
// stream, and BGRX is only made if the sink asks for it.
|
|
459
|
+
let mut masked = None;
|
|
460
|
+
let frame = if let Some(mask) = state.mask.as_mut() {
|
|
461
|
+
let Some(mut raw) =
|
|
462
|
+
crate::convert::to_bgrx(chunk_bytes, w, h, stride, format, box_w, box_h)
|
|
463
|
+
else {
|
|
464
|
+
state.dropped.fetch_add(1, Ordering::Relaxed);
|
|
465
|
+
return;
|
|
409
466
|
};
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
467
|
+
mask.apply(&mut raw, box_w, box_h, w, h);
|
|
468
|
+
let frame = to_i420(
|
|
469
|
+
&raw,
|
|
470
|
+
box_w,
|
|
471
|
+
box_h,
|
|
472
|
+
box_w * 4,
|
|
473
|
+
PixelFormat::Bgrx,
|
|
474
|
+
box_w,
|
|
475
|
+
box_h,
|
|
476
|
+
);
|
|
477
|
+
masked = Some(raw);
|
|
478
|
+
frame
|
|
479
|
+
} else {
|
|
480
|
+
to_i420(chunk_bytes, w, h, stride, format, box_w, box_h)
|
|
481
|
+
};
|
|
482
|
+
if let Some(i420) = frame {
|
|
483
|
+
if state.geometry.lock().map(|g| g.is_none()).unwrap_or(false) {
|
|
484
|
+
if let Ok(mut g) = state.geometry.lock() {
|
|
485
|
+
*g = Some(StreamGeometry {
|
|
486
|
+
source_width: w,
|
|
487
|
+
source_height: h,
|
|
488
|
+
origin_x: 0,
|
|
489
|
+
origin_y: 0,
|
|
490
|
+
format: format!("{format:?}").to_lowercase(),
|
|
491
|
+
buffer_type: state.buffer_type.clone(),
|
|
492
|
+
});
|
|
431
493
|
}
|
|
432
|
-
}
|
|
433
|
-
|
|
494
|
+
}
|
|
495
|
+
state.frames.fetch_add(1, Ordering::Relaxed);
|
|
496
|
+
(state.sink)(i420, seq, &mut || {
|
|
497
|
+
masked.take().or_else(|| {
|
|
498
|
+
crate::convert::to_bgrx(chunk_bytes, w, h, stride, format, box_w, box_h)
|
|
499
|
+
})
|
|
500
|
+
});
|
|
501
|
+
// The first frame the loop actually delivers is the only
|
|
502
|
+
// proof capture started; sending this when `run_loop`
|
|
503
|
+
// returns would be after the main loop quits, too late for
|
|
504
|
+
// `start` to wait on.
|
|
505
|
+
if let Some(sender) = ready.take() {
|
|
506
|
+
let _ = sender.send(Ok(()));
|
|
434
507
|
}
|
|
435
508
|
} else {
|
|
436
509
|
state.dropped.fetch_add(1, Ordering::Relaxed);
|
|
@@ -443,10 +516,15 @@ fn run_loop(
|
|
|
443
516
|
format_pod(&mut format_buf, max_fps)?;
|
|
444
517
|
let mut buffer_buf = Vec::new();
|
|
445
518
|
buffer_pod(&mut buffer_buf)?;
|
|
446
|
-
let mut
|
|
519
|
+
let mut cursor_buf = Vec::new();
|
|
520
|
+
cursor_pod(&mut cursor_buf)?;
|
|
521
|
+
let mut params = vec![
|
|
447
522
|
Pod::from_bytes(&format_buf).context("invalid EnumFormat pod")?,
|
|
448
523
|
Pod::from_bytes(&buffer_buf).context("invalid Buffers pod")?,
|
|
449
524
|
];
|
|
525
|
+
if wants_cursor_meta {
|
|
526
|
+
params.push(Pod::from_bytes(&cursor_buf).context("invalid Cursor Meta pod")?);
|
|
527
|
+
}
|
|
450
528
|
|
|
451
529
|
stream
|
|
452
530
|
.connect(
|
|
@@ -638,6 +716,7 @@ mod tests {
|
|
|
638
716
|
origin_x: 0,
|
|
639
717
|
origin_y: 0,
|
|
640
718
|
},
|
|
719
|
+
cursor_positions: Arc::new(AtomicBool::new(false)),
|
|
641
720
|
geometry: Arc::new(Mutex::new(None)),
|
|
642
721
|
frames: Arc::new(AtomicU64::new(0)),
|
|
643
722
|
dropped: Arc::new(AtomicU64::new(0)),
|
|
@@ -658,3 +737,33 @@ mod tests {
|
|
|
658
737
|
.expect("capture stopped and joined");
|
|
659
738
|
}
|
|
660
739
|
}
|
|
740
|
+
|
|
741
|
+
fn cursor_pod(buffer: &mut Vec<u8>) -> Result<()> {
|
|
742
|
+
use spa::pod::{object, property, Value};
|
|
743
|
+
let obj = object!(
|
|
744
|
+
spa::utils::SpaTypes::ObjectParamMeta,
|
|
745
|
+
spa::param::ParamType::Meta,
|
|
746
|
+
property!(
|
|
747
|
+
RawKey(spa::sys::SPA_PARAM_META_type),
|
|
748
|
+
Id,
|
|
749
|
+
RawKey(spa::sys::SPA_META_Cursor)
|
|
750
|
+
),
|
|
751
|
+
property!(
|
|
752
|
+
RawKey(spa::sys::SPA_PARAM_META_size),
|
|
753
|
+
Int,
|
|
754
|
+
(std::mem::size_of::<CursorWithBitmap>() + 256 * 256 * 4) as i32
|
|
755
|
+
)
|
|
756
|
+
);
|
|
757
|
+
PodSerializer::serialize(Cursor::new(buffer), &Value::Object(obj))
|
|
758
|
+
.map(|_| ())
|
|
759
|
+
.map_err(|e| anyhow::anyhow!("failed to build Cursor Meta pod: {e}"))
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
#[repr(C)]
|
|
763
|
+
struct CursorWithBitmap {
|
|
764
|
+
cursor: spa::sys::spa_meta_cursor,
|
|
765
|
+
bitmap: spa::sys::spa_meta_bitmap,
|
|
766
|
+
}
|
|
767
|
+
impl spa::buffer::meta::Metadata for CursorWithBitmap {
|
|
768
|
+
const META_TYPE: u32 = spa::sys::SPA_META_Cursor;
|
|
769
|
+
}
|
|
@@ -4,7 +4,10 @@ use anyhow::{bail, Result};
|
|
|
4
4
|
use std::ffi::{c_char, c_void, CStr};
|
|
5
5
|
use std::sync::Mutex;
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
/// See the Linux `capture::FrameSink`: the closure makes the BGRX copy, only
|
|
8
|
+
/// when the sink asks for it.
|
|
9
|
+
pub type FrameSink = Box<dyn Fn(I420, u64, Pixels) + Send + 'static>;
|
|
10
|
+
pub type Pixels<'a> = &'a mut dyn FnMut() -> Option<Vec<u8>>;
|
|
8
11
|
|
|
9
12
|
struct State {
|
|
10
13
|
sink: FrameSink,
|
|
@@ -41,8 +44,13 @@ unsafe extern "C" {
|
|
|
41
44
|
extern "C" fn stream_status(context: *mut c_void, reason: *const c_char, running: bool) {
|
|
42
45
|
let state = unsafe { &*(context as *const Mutex<State>) };
|
|
43
46
|
if let Ok(state) = state.lock() {
|
|
44
|
-
let reason = if reason.is_null() {
|
|
45
|
-
|
|
47
|
+
let reason = if reason.is_null() {
|
|
48
|
+
String::from("stream stopped")
|
|
49
|
+
} else {
|
|
50
|
+
unsafe { CStr::from_ptr(reason) }
|
|
51
|
+
.to_string_lossy()
|
|
52
|
+
.into_owned()
|
|
53
|
+
};
|
|
46
54
|
(state.status)(running, reason);
|
|
47
55
|
}
|
|
48
56
|
}
|
|
@@ -67,15 +75,6 @@ extern "C" fn receive_frame(
|
|
|
67
75
|
let state = unsafe { &*(context as *const Mutex<State>) };
|
|
68
76
|
let Ok(mut state) = state.lock() else { return };
|
|
69
77
|
let source = unsafe { std::slice::from_raw_parts(pixels, length) };
|
|
70
|
-
let raw = to_bgrx(
|
|
71
|
-
source,
|
|
72
|
-
source_width,
|
|
73
|
-
source_height,
|
|
74
|
-
stride,
|
|
75
|
-
PixelFormat::Bgra,
|
|
76
|
-
state.width,
|
|
77
|
-
state.height,
|
|
78
|
-
);
|
|
79
78
|
let frame = to_i420(
|
|
80
79
|
source,
|
|
81
80
|
source_width,
|
|
@@ -85,8 +84,19 @@ extern "C" fn receive_frame(
|
|
|
85
84
|
state.width,
|
|
86
85
|
state.height,
|
|
87
86
|
);
|
|
88
|
-
if let
|
|
89
|
-
(state.
|
|
87
|
+
if let Some(frame) = frame {
|
|
88
|
+
let (width, height) = (state.width, state.height);
|
|
89
|
+
(state.sink)(frame, state.sequence, &mut || {
|
|
90
|
+
to_bgrx(
|
|
91
|
+
source,
|
|
92
|
+
source_width,
|
|
93
|
+
source_height,
|
|
94
|
+
stride,
|
|
95
|
+
PixelFormat::Bgra,
|
|
96
|
+
width,
|
|
97
|
+
height,
|
|
98
|
+
)
|
|
99
|
+
});
|
|
90
100
|
state.sequence += 1;
|
|
91
101
|
}
|
|
92
102
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//! Windows desktop capture is deferred; encoded sources never call this adapter.
|
|
2
|
+
use crate::convert::I420;
|
|
3
|
+
use crate::portal::PortalSession;
|
|
4
|
+
use anyhow::{bail, Result};
|
|
5
|
+
|
|
6
|
+
pub type FrameSink = Box<dyn Fn(I420, u64, Pixels) + Send + 'static>;
|
|
7
|
+
pub type Pixels<'a> = &'a mut dyn FnMut() -> Option<Vec<u8>>;
|
|
8
|
+
pub struct Capture;
|
|
9
|
+
|
|
10
|
+
pub fn unavailable_reason() -> &'static str {
|
|
11
|
+
"Windows display capture is not implemented in this build."
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
pub fn start(
|
|
15
|
+
_session: PortalSession,
|
|
16
|
+
_width: usize,
|
|
17
|
+
_height: usize,
|
|
18
|
+
_fps: u32,
|
|
19
|
+
_sink: FrameSink,
|
|
20
|
+
_status: Box<dyn Fn(bool, String) + Send>,
|
|
21
|
+
) -> Result<Capture> {
|
|
22
|
+
bail!("{}", unavailable_reason())
|
|
23
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
//! Windows clipboard seam. This build never reads or writes the clipboard.
|
|
2
|
+
pub const MAX_CLIPBOARD_BYTES: usize = 256 * 1024;
|
|
3
|
+
pub fn read_or_explain() -> Result<(String, bool), String> {
|
|
4
|
+
Err("Windows clipboard transfer is not implemented in this build.".into())
|
|
5
|
+
}
|
|
6
|
+
pub fn write(_text: &str) -> anyhow::Result<()> {
|
|
7
|
+
anyhow::bail!("Windows clipboard transfer is not implemented in this build.")
|
|
8
|
+
}
|