Address webcam sidecar review feedback

This commit is contained in:
EtienneLescot
2026-05-22 21:20:51 +02:00
parent ef5855f1f4
commit 10614c2950
5 changed files with 86 additions and 5 deletions
@@ -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];
@@ -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;
}
+25
View File
@@ -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<void>((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, {
@@ -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);
}
});
});
@@ -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,
});