From 10614c2950fd750faf87de1da1217071773572f3 Mon Sep 17 00:00:00 2001 From: EtienneLescot Date: Fri, 22 May 2026 21:20:51 +0200 Subject: [PATCH] Address webcam sidecar review feedback --- .../wgc-capture/src/dshow_webcam_capture.cpp | 7 ++- .../native/wgc-capture/src/mf_encoder.cpp | 7 ++- src/hooks/useScreenRecorder.ts | 25 +++++++++++ .../timestampedVideoFrameQueue.test.ts | 45 ++++++++++++++++++- .../exporter/timestampedVideoFrameQueue.ts | 7 ++- 5 files changed, 86 insertions(+), 5 deletions(-) diff --git a/electron/native/wgc-capture/src/dshow_webcam_capture.cpp b/electron/native/wgc-capture/src/dshow_webcam_capture.cpp index 436bdea..7e3f8b7 100644 --- a/electron/native/wgc-capture/src/dshow_webcam_capture.cpp +++ b/electron/native/wgc-capture/src/dshow_webcam_capture.cpp @@ -379,8 +379,11 @@ void DirectShowWebcamCapture::storeFrame(const BYTE* buffer, long length) { } if (width_ % 2 == 1) { const int x = width_ - 1; - const BYTE* pair = source + (x - 1) * 2; - const auto color = yuvToBgr(pair[2], pair[1], pair[3]); + const int previousPairStart = ((x - 1) / 2) * 4; + const BYTE y = source[x * 2]; + const BYTE u = source[previousPairStart + 1]; + const BYTE v = source[previousPairStart + 3]; + const auto color = yuvToBgr(y, u, v); BYTE* pixel = destination + x * 4; pixel[0] = color[0]; pixel[1] = color[1]; diff --git a/electron/native/wgc-capture/src/mf_encoder.cpp b/electron/native/wgc-capture/src/mf_encoder.cpp index c04353a..18bc4cc 100644 --- a/electron/native/wgc-capture/src/mf_encoder.cpp +++ b/electron/native/wgc-capture/src/mf_encoder.cpp @@ -267,7 +267,12 @@ bool MFEncoder::copyBgraFrameToBuffer(const BgraFrameView& frame, BYTE* destinat } if (frame.width == width_ && frame.height == height_) { - std::memcpy(destination, frame.data, requiredBytes); + for (DWORD i = 0; i < requiredBytes; i += 4) { + destination[i] = frame.data[i]; + destination[i + 1] = frame.data[i + 1]; + destination[i + 2] = frame.data[i + 2]; + destination[i + 3] = 255; + } return true; } diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts index f64eec4..cdd4088 100644 --- a/src/hooks/useScreenRecorder.ts +++ b/src/hooks/useScreenRecorder.ts @@ -764,6 +764,25 @@ export function useScreenRecorder(): UseScreenRecorderReturn { const isCountdownRunActive = (runId?: number) => runId === undefined || countdownRunId.current === runId; + const waitForWebcamReady = async () => { + if (webcamReady.current) { + return; + } + + await new Promise((resolve) => { + const interval = setInterval(() => { + if (webcamReady.current) { + clearInterval(interval); + resolve(); + } + }, 50); + setTimeout(() => { + clearInterval(interval); + resolve(); + }, 5000); + }); + }; + const startNativeWindowsRecordingIfAvailable = async ( selectedSource: ProcessedDesktopSource, countdownRunToken?: number, @@ -795,6 +814,12 @@ export function useScreenRecorder(): UseScreenRecorderReturn { const displayId = Number(selectedSource.display_id); const sourceType = selectedSource.id.startsWith("window:") ? "window" : "display"; const windowHandle = parseWindowHandleFromSourceId(selectedSource.id); + if (webcamEnabled) { + await waitForWebcamReady(); + if (!isCountdownRunActive(countdownRunToken)) { + return true; + } + } const browserWebcamRecorder = webcamEnabled && webcamStream.current ? createRecorderHandle(webcamStream.current, { diff --git a/src/lib/exporter/timestampedVideoFrameQueue.test.ts b/src/lib/exporter/timestampedVideoFrameQueue.test.ts index 24d625d..9645c98 100644 --- a/src/lib/exporter/timestampedVideoFrameQueue.test.ts +++ b/src/lib/exporter/timestampedVideoFrameQueue.test.ts @@ -14,6 +14,15 @@ class MockVideoFrame { } } +function restoreVideoFrame(originalVideoFrame: typeof globalThis.VideoFrame | undefined) { + if (originalVideoFrame === undefined) { + delete (globalThis as { VideoFrame?: typeof globalThis.VideoFrame }).VideoFrame; + return; + } + + vi.stubGlobal("VideoFrame", originalVideoFrame); +} + describe("TimestampedVideoFrameQueue", () => { it("samples the latest webcam frame at or before the requested source timestamp", async () => { const originalVideoFrame = globalThis.VideoFrame; @@ -27,6 +36,7 @@ describe("TimestampedVideoFrameQueue", () => { queue.enqueue(frame0, 0); queue.enqueue(frame33, 33); queue.enqueue(frame66, 66); + queue.close(); const sampled0 = await queue.frameAt(0); const sampled20 = await queue.frameAt(20); @@ -44,7 +54,40 @@ describe("TimestampedVideoFrameQueue", () => { sampled80?.close(); queue.destroy(); } finally { - vi.stubGlobal("VideoFrame", originalVideoFrame); + restoreVideoFrame(originalVideoFrame); + } + }); + + it("waits for a newer frame before falling back to the held frame while open", async () => { + const originalVideoFrame = globalThis.VideoFrame; + vi.stubGlobal("VideoFrame", MockVideoFrame); + try { + const queue = new TimestampedVideoFrameQueue(); + const frame0 = new MockVideoFrame(0) as unknown as VideoFrame; + const frame33 = new MockVideoFrame(33_000) as unknown as VideoFrame; + + queue.enqueue(frame0, 0); + const sampled0 = await queue.frameAt(0); + let resolved = false; + const pending = queue.frameAt(33).then((frame) => { + resolved = true; + return frame; + }); + + await Promise.resolve(); + expect(resolved).toBe(false); + + queue.enqueue(frame33, 33); + const sampled33 = await pending; + + expect(sampled0?.timestamp).toBe(0); + expect(sampled33?.timestamp).toBe(33_000); + + sampled0?.close(); + sampled33?.close(); + queue.destroy(); + } finally { + restoreVideoFrame(originalVideoFrame); } }); }); diff --git a/src/lib/exporter/timestampedVideoFrameQueue.ts b/src/lib/exporter/timestampedVideoFrameQueue.ts index 336b053..86c0fe9 100644 --- a/src/lib/exporter/timestampedVideoFrameQueue.ts +++ b/src/lib/exporter/timestampedVideoFrameQueue.ts @@ -64,7 +64,12 @@ export class TimestampedVideoFrameQueue { continue; } - if (this.heldFrame) { + if ( + this.heldFrame && + (next || + this.closed || + this.heldFrame.sourceTimestampMs >= sourceTimestampMs - TIMESTAMP_EPSILON_MS) + ) { return new VideoFrame(this.heldFrame.frame, { timestamp: this.heldFrame.frame.timestamp, });