Skip to content

Commit 1f5d9d8

Browse files
committed
perf(windows): skip the webcam frame copy when its sequence has not changed
copyLatestFrame now takes the caller's last seen sequence and returns false without copying when nothing new arrived. Both capture backends share the rule through snapshotWebcamFrame, covered by webcam_snapshot_test.
1 parent 38c1239 commit 1f5d9d8

8 files changed

Lines changed: 106 additions & 26 deletions

File tree

‎electron/native/wgc-capture/CMakeLists.txt‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,3 +157,16 @@ target_compile_definitions(frame_visibility_test PRIVATE
157157
)
158158

159159
target_compile_options(frame_visibility_test PRIVATE /EHsc /W4 /utf-8)
160+
161+
add_executable(webcam_snapshot_test
162+
src/dshow_webcam_capture.h
163+
src/webcam_snapshot_test.cpp
164+
)
165+
166+
target_compile_definitions(webcam_snapshot_test PRIVATE
167+
NOMINMAX
168+
WIN32_LEAN_AND_MEAN
169+
_WIN32_WINNT=0x0A00
170+
)
171+
172+
target_compile_options(webcam_snapshot_test PRIVATE /EHsc /W4 /utf-8)

‎electron/native/wgc-capture/src/dshow_webcam_capture.cpp‎

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -576,17 +576,10 @@ void DirectShowWebcamCapture::storeFrame(const BYTE* buffer, long length) {
576576
latestFrameSequence_ += 1;
577577
}
578578

579-
bool DirectShowWebcamCapture::copyLatestFrame(WebcamFrameSnapshot& destination) {
579+
bool DirectShowWebcamCapture::copyLatestFrame(WebcamFrameSnapshot& destination, uint64_t lastSeenSequence) {
580580
std::scoped_lock lock(frameMutex_);
581-
if (latestFrame_.empty() || width_ <= 0 || height_ <= 0) {
582-
return false;
583-
}
584-
585-
destination.data = latestFrame_;
586-
destination.width = width_;
587-
destination.height = height_;
588-
destination.sequence = latestFrameSequence_;
589-
return true;
581+
return snapshotWebcamFrame(
582+
latestFrame_, width_, height_, latestFrameSequence_, lastSeenSequence, destination);
590583
}
591584

592585
int DirectShowWebcamCapture::width() const {

‎electron/native/wgc-capture/src/dshow_webcam_capture.h‎

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,33 @@ struct WebcamFrameSnapshot {
1616
uint64_t sequence = 0;
1717
};
1818

19+
/**
20+
* Copies the latest camera frame into `destination`, unless the caller already has it.
21+
*
22+
* The video writer asks on every screen tick, 60 times a second, while a camera
23+
* delivers 30: copying first and comparing sequences after cost a full frame
24+
* copied for nothing on every other tick. Returns false, leaving `destination`
25+
* untouched, when there is no frame yet or its sequence is `lastSeenSequence`.
26+
* Both capture backends route `copyLatestFrame` through this.
27+
*/
28+
inline bool snapshotWebcamFrame(
29+
const std::vector<BYTE>& frame,
30+
int width,
31+
int height,
32+
uint64_t sequence,
33+
uint64_t lastSeenSequence,
34+
WebcamFrameSnapshot& destination) {
35+
if (frame.empty() || width <= 0 || height <= 0 || sequence == lastSeenSequence) {
36+
return false;
37+
}
38+
39+
destination.data = frame;
40+
destination.width = width;
41+
destination.height = height;
42+
destination.sequence = sequence;
43+
return true;
44+
}
45+
1946
class DirectShowWebcamCapture {
2047
public:
2148
DirectShowWebcamCapture() = default;
@@ -33,7 +60,7 @@ class DirectShowWebcamCapture {
3360
int requestedFps);
3461
bool start();
3562
void stop();
36-
bool copyLatestFrame(WebcamFrameSnapshot& destination);
63+
bool copyLatestFrame(WebcamFrameSnapshot& destination, uint64_t lastSeenSequence);
3764

3865
int width() const;
3966
int height() const;

‎electron/native/wgc-capture/src/main.cpp‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1175,8 +1175,7 @@ int wmain(int argc, wchar_t* argv[]) {
11751175
}
11761176
if (webcamActive) {
11771177
WebcamFrameSnapshot candidateWebcamFrame;
1178-
if (webcamCapture.copyLatestFrame(candidateWebcamFrame) &&
1179-
candidateWebcamFrame.sequence != latestWebcamSequence &&
1178+
if (webcamCapture.copyLatestFrame(candidateWebcamFrame, latestWebcamSequence) &&
11801179
hasVisibleWebcamContent(candidateWebcamFrame.data, webcamCapture.deliversNv12())) {
11811180
latestWebcamFrame = std::move(candidateWebcamFrame.data);
11821181
latestWebcamWidth = candidateWebcamFrame.width;
@@ -1470,7 +1469,7 @@ int wmain(int argc, wchar_t* argv[]) {
14701469
const auto webcamDeadline = std::chrono::steady_clock::now() + std::chrono::seconds(3);
14711470
while (std::chrono::steady_clock::now() < webcamDeadline && !hasVisibleWebcamFrame) {
14721471
WebcamFrameSnapshot candidateWebcamFrame;
1473-
if (webcamCapture.copyLatestFrame(candidateWebcamFrame) &&
1472+
if (webcamCapture.copyLatestFrame(candidateWebcamFrame, latestWebcamSequence) &&
14741473
hasVisibleWebcamContent(candidateWebcamFrame.data, webcamCapture.deliversNv12())) {
14751474
latestWebcamFrame = std::move(candidateWebcamFrame.data);
14761475
latestWebcamWidth = candidateWebcamFrame.width;

‎electron/native/wgc-capture/src/webcam_capture.cpp‎

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -621,20 +621,13 @@ void WebcamCapture::captureLoop() {
621621
CoUninitialize();
622622
}
623623

624-
bool WebcamCapture::copyLatestFrame(WebcamFrameSnapshot& destination) {
624+
bool WebcamCapture::copyLatestFrame(WebcamFrameSnapshot& destination, uint64_t lastSeenSequence) {
625625
if (usingDirectShow_) {
626-
return directShowCapture_.copyLatestFrame(destination);
626+
return directShowCapture_.copyLatestFrame(destination, lastSeenSequence);
627627
}
628628
std::scoped_lock lock(frameMutex_);
629-
if (latestFrame_.empty() || width_ <= 0 || height_ <= 0) {
630-
return false;
631-
}
632-
633-
destination.data = latestFrame_;
634-
destination.width = width_;
635-
destination.height = height_;
636-
destination.sequence = latestFrameSequence_;
637-
return true;
629+
return snapshotWebcamFrame(
630+
latestFrame_, width_, height_, latestFrameSequence_, lastSeenSequence, destination);
638631
}
639632

640633
bool WebcamCapture::deliversNv12() const {

‎electron/native/wgc-capture/src/webcam_capture.h‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class WebcamCapture {
3232
bool preferNv12);
3333
bool start();
3434
void stop();
35-
bool copyLatestFrame(WebcamFrameSnapshot& destination);
35+
bool copyLatestFrame(WebcamFrameSnapshot& destination, uint64_t lastSeenSequence);
3636

3737
int width() const;
3838
int height() const;
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#include "dshow_webcam_capture.h"
2+
3+
#include <cstdio>
4+
5+
namespace {
6+
7+
int failures = 0;
8+
9+
void expect(const char* label, bool condition) {
10+
if (condition) {
11+
return;
12+
}
13+
std::printf("FAIL %s\n", label);
14+
++failures;
15+
}
16+
17+
} // namespace
18+
19+
int main() {
20+
const std::vector<BYTE> frame(4 * 2 * 2, 0x80);
21+
22+
// A new sequence is copied, with the size and sequence it was stored with.
23+
WebcamFrameSnapshot fresh;
24+
expect("new sequence is copied", snapshotWebcamFrame(frame, 2, 2, 7, 6, fresh));
25+
expect("new sequence carries its data", fresh.data == frame);
26+
expect("new sequence carries its size", fresh.width == 2 && fresh.height == 2);
27+
expect("new sequence carries its number", fresh.sequence == 7);
28+
29+
// The regression this file exists for: the writer polls at the screen's
30+
// rate, twice the camera's, and a frame it already holds must cost nothing.
31+
WebcamFrameSnapshot unchanged;
32+
expect("unchanged sequence reports nothing new", !snapshotWebcamFrame(frame, 2, 2, 7, 7, unchanged));
33+
expect("unchanged sequence copies nothing", unchanged.data.empty() && unchanged.sequence == 0);
34+
35+
// No frame stored yet: nothing to hand out, whatever the caller last saw.
36+
WebcamFrameSnapshot empty;
37+
expect("empty frame is not copied", !snapshotWebcamFrame({}, 2, 2, 1, 0, empty));
38+
expect("unknown size is not copied", !snapshotWebcamFrame(frame, 0, 2, 1, 0, empty));
39+
40+
if (failures > 0) {
41+
std::printf("%d webcam snapshot check(s) failed\n", failures);
42+
return 1;
43+
}
44+
std::printf("webcam snapshot checks passed\n");
45+
return 0;
46+
}

‎scripts/build-windows-wgc-helper.mjs‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,12 @@ if (!fs.existsSync(frameVisibilityTestPath)) {
125125
// reads every black frame as content.
126126
await run(frameVisibilityTestPath, [], { cwd: BUILD_DIR });
127127
console.log(`Passed ${frameVisibilityTestPath}`);
128+
129+
const webcamSnapshotTestPath = path.join(BUILD_DIR, "webcam_snapshot_test.exe");
130+
if (!fs.existsSync(webcamSnapshotTestPath)) {
131+
throw new Error(`WGC helper build completed but ${webcamSnapshotTestPath} was not found.`);
132+
}
133+
// Guards the per-tick webcam poll. The writer asks at the screen's rate, twice
134+
// the camera's, so a frame it already holds must not be copied again.
135+
await run(webcamSnapshotTestPath, [], { cwd: BUILD_DIR });
136+
console.log(`Passed ${webcamSnapshotTestPath}`);

0 commit comments

Comments
 (0)