mediaway-device 0.1.8

Device capture: camera, microphone, screen/window, audio playback + hotplug, with Windows/Linux/Web backends as #[cfg]-gated modules
#![cfg(test)]
#![allow(
    clippy::unwrap_used,
    clippy::expect_used,
    clippy::print_stderr,
    reason = "unit tests"
)]

use super::resized_geometry;
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, DesktopVideoCapture, DesktopVideoCaptureConfig};
    use crate::windows::{GpuDevice, GpuDeviceOptions};
    use crate::windows_desktop::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() -> 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,
                    320,
                    240,
                    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).
    #[test]
    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() 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
        );
    }
}

/// 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,
        })
    );
}