mediaway-device 0.2.1

Device capture: camera, microphone, screen/window, audio playback + hotplug, with Windows/Linux/Web backends as #[cfg]-gated modules
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
#![cfg(test)]
#![allow(
    clippy::unwrap_used,
    clippy::expect_used,
    clippy::print_stderr,
    reason = "unit tests"
)]

use super::{Crop, plan_crop, resized_geometry};
use crate::CaptureError;
use crate::desktop::CaptureRegion;
use crate::windows_desktop::FrameDimensions;
use mediaway_common::VideoGeometry;

#[cfg(windows)]
mod hardware {
    #![allow(
        unsafe_code,
        reason = "real win32 window creation for a real-hardware WGC capture smoke test"
    )]

    use crate::desktop::{
        CaptureOutputPreference, CursorCapture, DesktopVideoCapture, DesktopVideoCaptureConfig,
    };
    use crate::windows::{GpuDevice, GpuDeviceOptions};
    use crate::windows_desktop::{
        CaptureBorder, FrameDimensions, WindowCaptureOptions, WindowsWindowCapture,
    };
    use mediaway_common::{GpuBufferHandle, NativeHandle, Rational, VideoFrameStorage};
    use windows::Win32::Foundation::{HWND, LPARAM, LRESULT, WPARAM};
    use windows::Win32::System::LibraryLoader::GetModuleHandleW;
    use windows::Win32::UI::WindowsAndMessaging::{
        CW_USEDEFAULT, CreateWindowExW, DefWindowProcW, DestroyWindow, RegisterClassW,
        SW_SHOWNORMAL, ShowWindow, UnregisterClassW, WINDOW_EX_STYLE, WNDCLASSW,
        WS_OVERLAPPEDWINDOW,
    };
    use windows::core::PCWSTR;

    /// Minimal `WNDPROC` — this test never needs custom message handling.
    unsafe extern "system" fn wndproc(
        hwnd: HWND,
        msg: u32,
        wparam: WPARAM,
        lparam: LPARAM,
    ) -> LRESULT {
        // SAFETY: forwards to the default window procedure, exactly as any minimal win32
        // window does when it has no custom message handling of its own.
        unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }
    }

    /// Real, visible top-level window this test creates/destroys — a real capture target
    /// for WGC, which cannot capture a nonexistent/minimized/off-screen window.
    struct TestWindow {
        hwnd: HWND,
        class_name: Vec<u16>,
    }

    impl TestWindow {
        fn create(width: i32, height: i32) -> Option<Self> {
            let class_name: Vec<u16> = "MediawayWgcSmokeTestWindow\0".encode_utf16().collect();
            let instance = unsafe { GetModuleHandleW(None) }.ok()?;
            let class = WNDCLASSW {
                lpfnWndProc: Some(wndproc),
                hInstance: instance.into(),
                lpszClassName: PCWSTR(class_name.as_ptr()),
                ..Default::default()
            };
            // SAFETY: `class_name`/`class` are both live for the duration of this call.
            if unsafe { RegisterClassW(&raw const class) } == 0 {
                return None;
            }
            let title: Vec<u16> = "mediaway wgc smoke test\0".encode_utf16().collect();
            // SAFETY: standard CreateWindowExW call with a registered class name and no
            // parent/menu; every out-param is `None`/default, matching a plain top-level window.
            let hwnd = unsafe {
                CreateWindowExW(
                    WINDOW_EX_STYLE::default(),
                    PCWSTR(class_name.as_ptr()),
                    PCWSTR(title.as_ptr()),
                    WS_OVERLAPPEDWINDOW,
                    CW_USEDEFAULT,
                    CW_USEDEFAULT,
                    width,
                    height,
                    None,
                    None,
                    Some(instance.into()),
                    None,
                )
            }
            .ok()?;
            if hwnd.is_invalid() {
                return None;
            }
            // SAFETY: hwnd is a live window just created above.
            let _ = unsafe { ShowWindow(hwnd, SW_SHOWNORMAL) };
            Some(Self { hwnd, class_name })
        }
    }

    impl Drop for TestWindow {
        fn drop(&mut self) {
            // SAFETY: hwnd/class_name were created by this same struct's `create`.
            unsafe {
                let _ = DestroyWindow(self.hwnd);
                let _ = UnregisterClassW(PCWSTR(self.class_name.as_ptr()), None);
            }
        }
    }

    /// Real hardware smoke test: a real win32 window, a real D3D11 device, a real WGC
    /// session — proves `WindowsWindowCapture`'s already-Zero-Copy code path (no
    /// `CopyResource`/`memcpy` anywhere in `poll_frame`, see `wgc.rs`) actually delivers a
    /// real `GpuBufferHandle::DirectX11` frame end to end. Per `adr/windows/0004`'s own
    /// acceptance criterion ("README Window cell can move toward 🆗/⚡ once CI machines
    /// prove capture") — this is that proof.
    ///
    /// Skips gracefully (never fails the default suite) at any missing capability: no WGC
    /// support, window creation failure, or no frame delivered within the bounded poll
    /// window (WGC delivery is async and this test cannot control compositor timing).
    ///
    /// # Why this is `#[ignore]`d
    ///
    /// It calls `ShowWindow(SW_SHOWNORMAL)`, so it pops a real window onto whatever desktop
    /// the suite runs on. Needing real hardware would not by itself justify opting out —
    /// this crate's other hardware tests stay in the default suite because they only open a
    /// device and leave the machine alone. The line, per
    /// [`docs/conventions/testing.md`](../../../../docs/conventions/testing.md) § Tests that
    /// manipulate the desktop, is whether a test touches input or the screen. This one does.
    ///
    /// Run it explicitly:
    ///
    /// ```text
    /// cargo nextest run -p mediaway-device --run-ignored all -E 'test(wgc_window_capture)'
    /// ```
    #[test]
    #[ignore = "opens a visible window on the desktop; run explicitly with --run-ignored all"]
    fn wgc_window_capture_delivers_zero_copy_frame_or_skip() {
        let _guard = crate::windows_desktop::HARDWARE_TEST_LOCK
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        let Some(window) = TestWindow::create(320, 240) else {
            eprintln!("skip: could not create a real test window");
            return;
        };
        let device = match GpuDevice::create(GpuDeviceOptions::default()) {
            Ok(d) => d,
            Err(e) => {
                eprintln!("skip: GpuDevice::create failed ({e:?})");
                return;
            }
        };
        let Some(window_handle) = NativeHandle::new(window.hwnd.0 as usize) else {
            eprintln!("skip: null test window handle");
            return;
        };

        let mut cfg = DesktopVideoCaptureConfig::window(window_handle, Rational::new(1, 30));
        cfg.output = CaptureOutputPreference::ZeroCopyGpu;
        cfg.gpu_device = Some(device.handle());

        let mut capture = match WindowsWindowCapture::open(&cfg) {
            Ok(c) => c,
            Err(e) => {
                eprintln!("skip: WindowsWindowCapture::open failed ({e:?})");
                return;
            }
        };

        // WGC frame delivery is async — poll with a bounded retry loop rather than a single
        // attempt (same shape this crate's own poll-based backends already use elsewhere).
        let mut delivered = None;
        for _ in 0..50 {
            match capture.poll_frame() {
                Ok(Some(frame)) => {
                    delivered = Some(frame);
                    break;
                }
                Ok(None) => std::thread::sleep(std::time::Duration::from_millis(20)),
                Err(e) => {
                    eprintln!("skip: poll_frame failed ({e:?})");
                    return;
                }
            }
        }
        let Some(frame) = delivered else {
            eprintln!("skip: no frame delivered within the bounded poll window");
            return;
        };

        assert!(
            frame.width > 0 && frame.height > 0,
            "expected a real frame size"
        );
        assert!(
            matches!(
                frame.storage,
                VideoFrameStorage::Gpu(GpuBufferHandle::DirectX11 { .. })
            ),
            "expected a Zero-Copy DirectX11 handle, got {:?}",
            frame.storage
        );
        let _ = capture.release_frame();
        eprintln!(
            "wgc window capture: real Zero-Copy frame delivered ({}x{})",
            frame.width, frame.height
        );
    }

    /// The cursor, border and even-crop options, applied to a real WGC session.
    ///
    /// Opening with [`CursorCapture::Included`] succeeding is the cursor check: `open` fails
    /// when `SetIsCursorCaptureEnabled` does, in either direction. The border is read back from
    /// the session. **Needs Windows 11 (build 22000+)**, where borderless capture exists, and
    /// asserts it rather than skipping, because a skip is how the border went unhidden before.
    /// The delivered frame must be even on both axes whatever size the test window came up at.
    ///
    /// `#[ignore]`d for the same reason as the test above: it shows a real window.
    ///
    /// ```text
    /// cargo nextest run -p mediaway-device --run-ignored all -E 'test(wgc_window_capture)'
    /// ```
    #[test]
    #[ignore = "opens a visible window on the desktop; run explicitly with --run-ignored all"]
    fn wgc_window_capture_applies_cursor_border_and_even_crop() {
        let _guard = crate::windows_desktop::HARDWARE_TEST_LOCK
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        // Chosen so WGC's capture size comes out odd on both axes. WGC captures the window
        // without its invisible resize borders, which on Windows 11 at 100% scale is 14 px
        // narrower and 7 px shorter than the window rect, so 335x250 captures as 321x243. The
        // assertion below checks that rather than trusting it, since the offset depends on
        // scale and theme.
        let Some(window) = TestWindow::create(335, 250) else {
            eprintln!("skip: could not create a real test window");
            return;
        };
        let device = GpuDevice::create(GpuDeviceOptions::default()).expect("D3D11 device");
        let window_handle = NativeHandle::new(window.hwnd.0 as usize).expect("test window");

        let mut cfg = DesktopVideoCaptureConfig::window(window_handle, Rational::new(1, 30));
        cfg.output = CaptureOutputPreference::ZeroCopyGpu;
        cfg.gpu_device = Some(device.handle());
        cfg.cursor = CursorCapture::Included;

        // The size WGC would deliver uncropped, from a plain session.
        let native = WindowsWindowCapture::open(&cfg)
            .expect("WGC open")
            .stream_info()
            .geometry()
            .expect("video geometry");
        assert!(
            native.width % 2 == 1 || native.height % 2 == 1,
            "the test window captured at {}x{}, even on both axes, so this run proves nothing \
             about cropping; change the window size in this test",
            native.width,
            native.height
        );

        let options = WindowCaptureOptions {
            border: CaptureBorder::Hidden,
            dimensions: FrameDimensions::EvenCropped,
        };
        let mut hidden = WindowsWindowCapture::open_with(&cfg, options)
            .expect("WGC open with the cursor included");
        assert!(
            hidden.border_hidden(),
            "Windows 11 grants an unpackaged process borderless capture; the border is still on"
        );
        let mut frame = None;
        for _ in 0..50 {
            if let Some(f) = hidden.poll_frame().expect("poll") {
                frame = Some(f);
                break;
            }
            std::thread::sleep(std::time::Duration::from_millis(20));
        }
        let frame = frame.expect("a frame within one second");
        // Spelled out rather than taken from `FrameDimensions::pool_size`: an expectation
        // computed by the code under test passes however that code is broken. (Measured —
        // that version survived `pool_size` being turned into a no-op.)
        let expected = (
            native.width - native.width % 2,
            native.height - native.height % 2,
        );
        assert_eq!(
            (frame.width, frame.height),
            expected,
            "a {}x{} window should even-crop to {expected:?}",
            native.width,
            native.height
        );
        let _ = hidden.release_frame();
        drop(hidden);

        let shown = WindowsWindowCapture::open(&cfg).expect("WGC open with the default border");
        assert!(
            !shown.border_hidden(),
            "`open` must leave the border at the OS default"
        );
    }
}

/// Full hardware-driven resize (actually resizing a captured window/monitor mid-session
/// and observing `Direct3D11CaptureFramePool::Recreate` take effect) is not practically
/// automatable in this suite — it needs a real WGC session plus a window an external
/// actor resizes on a timeline this test can't control. Instead, this exercises the pure
/// decision logic `poll_frame` uses to detect a size change, extracted so it is testable
/// without `WinRT` calls.

#[test]
fn resized_geometry_none_when_size_unchanged() {
    let current = VideoGeometry {
        width: 1920,
        height: 1080,
    };
    assert_eq!(resized_geometry(current, 1920, 1080), None);
}

#[test]
fn resized_geometry_some_when_width_changes() {
    let current = VideoGeometry {
        width: 1920,
        height: 1080,
    };
    assert_eq!(
        resized_geometry(current, 1280, 1080),
        Some(VideoGeometry {
            width: 1280,
            height: 1080,
        })
    );
}

#[test]
fn resized_geometry_some_when_height_changes() {
    let current = VideoGeometry {
        width: 1920,
        height: 1080,
    };
    assert_eq!(
        resized_geometry(current, 1920, 720),
        Some(VideoGeometry {
            width: 1920,
            height: 720,
        })
    );
}

#[test]
fn resized_geometry_some_on_first_frame_from_zero_geometry() {
    // `stream_info.geometry()` starts non-zero at `open()` in practice, but the closed/
    // uninitialized path uses a `0x0` placeholder — confirm that is always treated as a
    // mismatch (never accidentally suppresses a legitimate first Recreate).
    let current = VideoGeometry {
        width: 0,
        height: 0,
    };
    assert_eq!(
        resized_geometry(current, 800, 600),
        Some(VideoGeometry {
            width: 800,
            height: 600,
        })
    );
}

const fn region(x: u32, y: u32, width: u32, height: u32) -> CaptureRegion {
    CaptureRegion {
        x,
        y,
        width,
        height,
    }
}

#[test]
fn without_a_region_the_pool_follows_the_window() {
    let plan = plan_crop(None, FrameDimensions::Native, 1137, 636).expect("plan");
    assert!(matches!(plan.crop, Crop::None));
    assert_eq!((plan.frame, plan.pool), ((1137, 636), (1137, 636)));
    let plan = plan_crop(None, FrameDimensions::EvenCropped, 1137, 636).expect("plan");
    assert_eq!((plan.frame, plan.pool), ((1136, 636), (1136, 636)));
}

#[test]
fn a_region_at_the_origin_is_cropped_by_the_pool_alone() {
    let plan = plan_crop(
        Some(region(0, 0, 800, 600)),
        FrameDimensions::Native,
        1920,
        1080,
    )
    .expect("plan");
    assert!(
        matches!(plan.crop, Crop::Pool { .. }),
        "no copy at the origin"
    );
    assert_eq!((plan.frame, plan.pool), ((800, 600), (800, 600)));
}

#[test]
fn a_region_elsewhere_copies_and_the_pool_stops_at_its_far_corner() {
    let plan = plan_crop(
        Some(region(100, 50, 800, 600)),
        FrameDimensions::Native,
        1920,
        1080,
    )
    .expect("plan");
    assert!(matches!(plan.crop, Crop::Copy { .. }));
    assert_eq!(plan.frame, (800, 600));
    // Not the whole window: WGC clips from the top-left, so nothing past (900, 650) is needed.
    assert_eq!(plan.pool, (900, 650));
}

#[test]
fn even_cropping_applies_to_the_region_not_the_window() {
    let plan = plan_crop(
        Some(region(10, 10, 801, 601)),
        FrameDimensions::EvenCropped,
        1137,
        636,
    )
    .expect("plan");
    assert_eq!(plan.frame, (800, 600));
    assert_eq!(plan.pool, (810, 610));
}

#[test]
fn a_region_past_the_window_is_refused_with_its_numbers() {
    let err = plan_crop(
        Some(region(1000, 0, 200, 100)),
        FrameDimensions::Native,
        1137,
        636,
    )
    .err()
    .expect("out of bounds");
    assert_eq!(
        err,
        CaptureError::RegionOutOfBounds {
            x: 1000,
            y: 0,
            width: 200,
            height: 100,
            surface_width: 1137,
            surface_height: 636,
        }
    );
}

#[test]
fn a_region_that_even_crops_to_nothing_is_invalid_input() {
    let err = plan_crop(
        Some(region(0, 0, 1, 100)),
        FrameDimensions::EvenCropped,
        640,
        480,
    )
    .err()
    .expect("empty after cropping");
    assert_eq!(err, CaptureError::InvalidInput);
}