-
Notifications
You must be signed in to change notification settings - Fork 108
fix(recording): stop a camera name from matching a different camera #405
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
ef1b78a
fix(recording): stop a camera name from matching a different camera
EtienneLescot 8d40b37
fix(recording): match camera names on word boundaries, and keep non-L…
EtienneLescot d59eca9
test(recording): let a camera case say when it proved nothing
EtienneLescot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
| import { normalizeDeviceName, scoreDeviceNameMatch } from "./deviceNameMatching"; | ||
|
|
||
| describe("normalizeDeviceName", () => { | ||
| it("reduces punctuation and case to single-spaced lowercase", () => { | ||
| expect(normalizeDeviceName("Camera (NVIDIA Broadcast)")).toBe("camera nvidia broadcast"); | ||
| expect(normalizeDeviceName("Logitech StreamCam (046d:0893)")).toBe( | ||
| "logitech streamcam 046d 0893", | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| describe("scoreDeviceNameMatch", () => { | ||
| it("scores an exact name highest", () => { | ||
| expect( | ||
| scoreDeviceNameMatch("Camera (NVIDIA Broadcast)", "{clsid}", "Camera (NVIDIA Broadcast)"), | ||
| ).toBe(1000); | ||
| }); | ||
|
|
||
| // What Chromium hands over is the driver's name plus USB ids, so one side | ||
| // being the other with decoration is the ordinary case, not a near miss. | ||
| it("matches through the USB ids Chromium appends", () => { | ||
| expect( | ||
| scoreDeviceNameMatch("Logitech StreamCam", "{clsid}", "Logitech StreamCam (046d:0893)"), | ||
| ).toBe(900); | ||
| }); | ||
|
|
||
| it("matches when the platform name is the longer of the two", () => { | ||
| expect( | ||
| scoreDeviceNameMatch("Logitech HD Pro Webcam C920", "{clsid}", "HD Pro Webcam C920"), | ||
| ).toBe(900); | ||
| }); | ||
|
|
||
| /** | ||
| * The bug this module exists to end. "Logi Capture" and "Logitech StreamCam" | ||
| * are two different real devices sharing no word — but "logi" is inside | ||
| * "logitech", and the word-scoring tier took that for a match, opening the | ||
| * wrong camera instead of letting the caller fall through to the provider | ||
| * that had the right one. | ||
| */ | ||
| it("refuses a word that is merely inside another word", () => { | ||
| expect(scoreDeviceNameMatch("Logitech StreamCam", "{clsid}", "Logi Capture")).toBe(0); | ||
| }); | ||
|
|
||
| it("refuses the microphone form of the same mistake", () => { | ||
| expect(scoreDeviceNameMatch("Microphone (Logitech PRO X)", "{id}", "Micro Studio")).toBe(0); | ||
| }); | ||
|
|
||
| /** | ||
| * Sharing one distinctive word is no longer enough on its own. Two Logitech | ||
| * devices are still two devices, and answering "some Logitech thing" is how | ||
| * the wrong one got opened. | ||
| */ | ||
| it("refuses a partial match on a shared brand", () => { | ||
| expect(scoreDeviceNameMatch("Logitech StreamCam", "{clsid}", "Logitech BRIO")).toBe(0); | ||
| }); | ||
|
|
||
| it("scores nothing when no name was requested", () => { | ||
| expect(scoreDeviceNameMatch("Logitech StreamCam", "{clsid}", undefined)).toBe(0); | ||
| expect(scoreDeviceNameMatch("Logitech StreamCam", "{clsid}", " ")).toBe(0); | ||
| }); | ||
|
|
||
| /** | ||
| * "Micro" is inside "Microphone", "Logi" inside "Logitech" — spelled there, | ||
| * but not as a word. Containment used to take either for a match and resolve | ||
| * a device nobody asked for. | ||
| */ | ||
| it("refuses a request merely spelled inside a longer word", () => { | ||
| expect(scoreDeviceNameMatch("Microphone (Logitech StreamCam)", "{id}", "Micro")).toBe(0); | ||
| expect(scoreDeviceNameMatch("Logitech StreamCam", "{clsid}", "Logi")).toBe(0); | ||
| }); | ||
|
|
||
| /** | ||
| * `[^a-z0-9]` stripped every non-Latin letter, so two different Japanese | ||
| * cameras both normalized to "a" and matched each other at 1000. | ||
| */ | ||
| it("keeps non-Latin names apart", () => { | ||
| expect(scoreDeviceNameMatch("カメラ A", "{clsid}", "ウェブカメラ A")).toBe(0); | ||
| expect(scoreDeviceNameMatch("Веб-камера 1", "{clsid}", "Веб-камера 2")).toBe(0); | ||
| }); | ||
|
|
||
| it("still matches identical non-Latin names", () => { | ||
| expect(scoreDeviceNameMatch("カメラ A", "{clsid}", "カメラ A")).toBe(1000); | ||
| expect(scoreDeviceNameMatch("摄像头(罗技)", "{clsid}", "摄像头(罗技)")).toBe(1000); | ||
| }); | ||
|
|
||
| it("falls back to the identifier when the friendly name says nothing", () => { | ||
| expect(scoreDeviceNameMatch("", "usb elgato facecam 0fd9", "Elgato Facecam")).toBe(800); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| /** | ||
| * Matching a device the user picked in the browser against one Windows can open. | ||
| * | ||
| * The two sides name the same hardware differently — Chromium appends USB ids | ||
| * ("Logitech StreamCam (046d:0893)") while DirectShow, Media Foundation and | ||
| * WASAPI report the driver's friendly name — so the match cannot be equality. | ||
| * It can, however, be *decisive*: every real pairing observed is one name | ||
| * containing the other, and nothing weaker is trusted. | ||
| * | ||
| * There used to be a further tier that scored shared WORDS, meant to bridge | ||
| * names that differ more than that. It bridged names that were not the same | ||
| * device at all. "Logi Capture" and "Logitech StreamCam" share no word, but | ||
| * "logi" is inside "logitech" — enough to win, so choosing a camera Media | ||
| * Foundation cannot see opened a different camera instead of falling through to | ||
| * the provider that would have found the right one. The microphone side reached | ||
| * the same place by the same road, "micro" sitting inside the "microphone" that | ||
| * opens nearly every Windows endpoint name (getopenscreen/openscreen#404, #405). | ||
| * | ||
| * Dropping that tier costs nothing measurable: on the reporter's machine every | ||
| * camera and microphone resolves at 800 or above without it. What it buys is | ||
| * that "I could not find it" is now reachable — and a caller that hears it can | ||
| * try another provider, or say so, instead of recording the wrong device. | ||
| * | ||
| * This lives here rather than in `electron/ipc/handlers.ts` because that module | ||
| * calls `app.getPath()` while being imported and cannot be loaded from a test. | ||
| * The C++ helpers carry their own copy of these rules, covered by the | ||
| * Windows-only scripts in `scripts/` that drive the real binary. | ||
| */ | ||
|
|
||
| /** | ||
| * Lowercase, letters and digits only, single-spaced — the shape both sides | ||
| * compare in. | ||
| * | ||
| * Unicode-aware, and not `[^a-z0-9]`: that stripped every non-Latin letter, so a | ||
| * Japanese "カメラ A" and "ウェブカメラ A" both collapsed to "a" and matched each | ||
| * other exactly, at the highest score there is. The C++ helpers use | ||
| * `std::iswalnum` on wide characters and never had that flaw; this is the copy | ||
| * that did. | ||
| */ | ||
| export function normalizeDeviceName(value: string) { | ||
| return value | ||
| .toLowerCase() | ||
| .replace(/[^\p{L}\p{N}]+/gu, " ") | ||
| .trim(); | ||
| } | ||
|
|
||
| /** | ||
| * Does `needle` appear in `haystack` as whole words? | ||
| * | ||
| * Plain containment answers for devices that merely share a spelling: a | ||
| * requested "Micro" is inside "Microphone (Logitech StreamCam)", and "Logi" | ||
| * inside "Logitech", neither of them as a word. Both resolved a device nobody | ||
| * asked for — and resolving one is precisely what stops the caller from falling | ||
| * through to the provider that had the right one. | ||
| * | ||
| * Both sides are normalized, so a boundary is the start of the string, its end, | ||
| * or a space. | ||
| */ | ||
| function containsAsWords(haystack: string, needle: string) { | ||
| if (!haystack || !needle) { | ||
| return false; | ||
| } | ||
| for (let at = haystack.indexOf(needle); at !== -1; at = haystack.indexOf(needle, at + 1)) { | ||
| const startsOnBoundary = at === 0 || haystack[at - 1] === " "; | ||
| const after = at + needle.length; | ||
| const endsOnBoundary = after === haystack.length || haystack[after] === " "; | ||
| if (startsOnBoundary && endsOnBoundary) { | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| /** | ||
| * How well a candidate device answers a requested name, or 0 for "not this one" | ||
| * — which callers must treat as a real answer rather than a weak match. | ||
| * | ||
| * @param candidateName The device's own name, as the platform reports it. | ||
| * @param candidateId Its stable identifier — a CLSID, a symbolic link — which | ||
| * sometimes carries the model name when the friendly name does not. | ||
| * @param requestedName What the user picked, as the browser labelled it. | ||
| */ | ||
| export function scoreDeviceNameMatch( | ||
| candidateName: string, | ||
| candidateId: string, | ||
| requestedName?: string, | ||
| ) { | ||
| const candidate = normalizeDeviceName(candidateName); | ||
| const id = normalizeDeviceName(candidateId); | ||
| const requested = normalizeDeviceName(requestedName ?? ""); | ||
| if (!requested) { | ||
| return 0; | ||
| } | ||
| if (candidate === requested) { | ||
| return 1000; | ||
| } | ||
| // One name being the other plus decoration is the ordinary case, and the only | ||
| // inexact match worth trusting — provided the shared part is whole words. | ||
| if (containsAsWords(candidate, requested) || containsAsWords(requested, candidate)) { | ||
| return 900; | ||
| } | ||
| if (containsAsWords(id, requested) || containsAsWords(requested, id)) { | ||
| return 800; | ||
| } | ||
| return 0; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.