From a55d67b6aadc752e70648891f3f72bc76ce491ee Mon Sep 17 00:00:00 2001 From: turbocrime Date: Wed, 29 Apr 2026 12:28:40 -0700 Subject: [PATCH 1/5] claim alternate interface --- driver/src/camera/worker/worker.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/driver/src/camera/worker/worker.ts b/driver/src/camera/worker/worker.ts index 987921e..56555ca 100644 --- a/driver/src/camera/worker/worker.ts +++ b/driver/src/camera/worker/worker.ts @@ -57,6 +57,8 @@ const getDevice = async ( await dev.claimInterface(usbInterface); } + await dev.selectAlternateInterface(usbInterface, 0); + return dev; }); From ede9f6c70c83e9137100ba66aa5abb779c6ef23b Mon Sep 17 00:00:00 2001 From: turbocrime Date: Wed, 29 Apr 2026 12:50:10 -0700 Subject: [PATCH 2/5] identify alternate by descriptor --- driver/src/camera/worker/get-device.ts | 28 ++++++++++++++++++++++++++ driver/src/camera/worker/worker.ts | 22 ++------------------ 2 files changed, 30 insertions(+), 20 deletions(-) create mode 100644 driver/src/camera/worker/get-device.ts diff --git a/driver/src/camera/worker/get-device.ts b/driver/src/camera/worker/get-device.ts new file mode 100644 index 0000000..fc038aa --- /dev/null +++ b/driver/src/camera/worker/get-device.ts @@ -0,0 +1,28 @@ +import type { CamIsoInterface } from "../stream/enum.js"; + +export async function prepareCamDevice( + dev: USBDevice, + usbInterface: CamIsoInterface, +): Promise { + if (!dev.opened) { + await dev.open(); + } + + const iface = dev.configuration?.interfaces.find( + (i) => i.interfaceNumber === usbInterface, + ); + if (!iface) { + throw new ReferenceError(`Interface ${usbInterface} not found`); + } + + if (!iface.claimed) { + await dev.claimInterface(usbInterface); + } + + await dev.selectAlternateInterface( + usbInterface, + iface.alternate.alternateSetting, + ); + + return dev; +} diff --git a/driver/src/camera/worker/worker.ts b/driver/src/camera/worker/worker.ts index 56555ca..fefaaac 100644 --- a/driver/src/camera/worker/worker.ts +++ b/driver/src/camera/worker/worker.ts @@ -9,6 +9,7 @@ import { import { CamIsoStream } from "../stream/iso-parser.js"; import { UnderlyingIsochronousTransferSource } from "../stream/transfer-source.js"; import { ISOCHRONOUS_BATCH_SIZE } from "./constants.js"; +import { prepareCamDevice } from "./get-device.js"; import { type IsoWorkerRequest, type IsoWorkerResponse, @@ -40,26 +41,7 @@ const getDevice = async ( `Device with serial number ${serialNumber} not found`, ); } - - if (!dev.opened) { - await dev.open(); - } - - const iface = dev.configuration?.interfaces.find( - (iface) => iface.interfaceNumber === usbInterface, - ); - - if (!iface) { - throw new ReferenceError(`Interface ${usbInterface} not found`); - } - - if (!iface.claimed) { - await dev.claimInterface(usbInterface); - } - - await dev.selectAlternateInterface(usbInterface, 0); - - return dev; + return prepareCamDevice(dev, usbInterface); }); return activeDevice; From 8fd3d3202dab736635c5b3036cba7bc4d8506076 Mon Sep 17 00:00:00 2001 From: turbocrime Date: Wed, 29 Apr 2026 12:50:35 -0700 Subject: [PATCH 3/5] add regression test --- driver/test/prepare-cam-device.test.ts | 142 +++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 driver/test/prepare-cam-device.test.ts diff --git a/driver/test/prepare-cam-device.test.ts b/driver/test/prepare-cam-device.test.ts new file mode 100644 index 0000000..66a798a --- /dev/null +++ b/driver/test/prepare-cam-device.test.ts @@ -0,0 +1,142 @@ +/// +/** biome-ignore-all lint/style/noNonNullAssertion: test code */ + +import { describe, expect, test, vi } from "vitest"; +import { CamIsoInterface } from "../src/camera/stream/enum.js"; +import { prepareCamDevice } from "../src/camera/worker/get-device.js"; + +const makeAlt = (alternateSetting: number): USBAlternateInterface => + ({ + alternateSetting, + interfaceClass: 0xff, + interfaceSubclass: 0, + interfaceProtocol: 0, + interfaceName: null, + endpoints: [], + }) as unknown as USBAlternateInterface; + +const makeIface = ( + interfaceNumber: number, + { claimed = false, alternateSetting = 0 } = {}, +): USBInterface => { + const alt = makeAlt(alternateSetting); + return { + interfaceNumber, + alternate: alt, + alternates: [alt], + claimed, + } as unknown as USBInterface; +}; + +const makeDevice = ( + iface: USBInterface, + { opened = true } = {}, +): { + device: USBDevice; + open: ReturnType>; + claimInterface: ReturnType>; + selectAlternateInterface: ReturnType< + typeof vi.fn + >; +} => { + const open = vi.fn().mockResolvedValue(undefined); + const claimInterface = vi + .fn() + .mockResolvedValue(undefined); + const selectAlternateInterface = vi + .fn() + .mockResolvedValue(undefined); + const device: Partial = { + opened, + configuration: { + interfaces: [iface], + } as unknown as USBConfiguration, + open, + claimInterface, + selectAlternateInterface, + }; + return { + device: device as USBDevice, + open, + claimInterface, + selectAlternateInterface, + }; +}; + +describe("prepareCamDevice", () => { + test("claims and selects alt setting in order when interface is unclaimed", async () => { + const iface = makeIface(CamIsoInterface.CAMERA, { + claimed: false, + alternateSetting: 0, + }); + const { device, claimInterface, selectAlternateInterface } = + makeDevice(iface); + + await prepareCamDevice(device, CamIsoInterface.CAMERA); + + expect(claimInterface).toHaveBeenCalledWith(CamIsoInterface.CAMERA); + expect(selectAlternateInterface).toHaveBeenCalledWith( + CamIsoInterface.CAMERA, + 0, + ); + + const claimOrder = claimInterface.mock.invocationCallOrder[0]!; + const selectOrder = selectAlternateInterface.mock.invocationCallOrder[0]!; + expect(claimOrder).toBeLessThan(selectOrder); + }); + + test("skips claim when interface is already claimed but still selects alt", async () => { + const iface = makeIface(CamIsoInterface.CAMERA, { + claimed: true, + alternateSetting: 0, + }); + const { device, claimInterface, selectAlternateInterface } = + makeDevice(iface); + + await prepareCamDevice(device, CamIsoInterface.CAMERA); + + expect(claimInterface).not.toHaveBeenCalled(); + expect(selectAlternateInterface).toHaveBeenCalledWith( + CamIsoInterface.CAMERA, + 0, + ); + }); + + test("forwards the descriptor's alternateSetting verbatim", async () => { + const iface = makeIface(CamIsoInterface.CAMERA, { + claimed: false, + alternateSetting: 3, + }); + const { device, selectAlternateInterface } = makeDevice(iface); + + await prepareCamDevice(device, CamIsoInterface.CAMERA); + + expect(selectAlternateInterface).toHaveBeenCalledWith( + CamIsoInterface.CAMERA, + 3, + ); + }); + + test("throws ReferenceError when the requested interface is not present", async () => { + const iface = makeIface(7); + const { device } = makeDevice(iface); + + await expect( + prepareCamDevice(device, CamIsoInterface.CAMERA), + ).rejects.toBeInstanceOf(ReferenceError); + }); + + test("opens the device before claiming when not yet open", async () => { + const iface = makeIface(CamIsoInterface.CAMERA, { claimed: false }); + const { device, open, claimInterface } = makeDevice(iface, { + opened: false, + }); + + await prepareCamDevice(device, CamIsoInterface.CAMERA); + + expect(open).toHaveBeenCalled(); + const openOrder = open.mock.invocationCallOrder[0]!; + const claimOrder = claimInterface.mock.invocationCallOrder[0]!; + expect(openOrder).toBeLessThan(claimOrder); + }); +}); From b2924aac623e1f51a00ff45fdade2b7f6836db77 Mon Sep 17 00:00:00 2001 From: turbocrime Date: Wed, 29 Apr 2026 12:50:43 -0700 Subject: [PATCH 4/5] changeset --- .changeset/silver-llamas-fly.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/silver-llamas-fly.md diff --git a/.changeset/silver-llamas-fly.md b/.changeset/silver-llamas-fly.md new file mode 100644 index 0000000..861510e --- /dev/null +++ b/.changeset/silver-llamas-fly.md @@ -0,0 +1,5 @@ +--- +"@webnect/driver": patch +--- + +Fix Linux Chromium iso transfer crash on `Camera.setMode`. From cdd9ac65e3d37887c1851e6eae603caf13ea5b4c Mon Sep 17 00:00:00 2001 From: turbocrime Date: Wed, 29 Apr 2026 17:33:23 -0700 Subject: [PATCH 5/5] only select conditionally --- driver/src/camera/stream/enum.ts | 4 ++++ driver/src/camera/worker/get-device.ts | 9 ++++---- driver/test/prepare-cam-device.test.ts | 30 ++++++++++++++------------ 3 files changed, 24 insertions(+), 19 deletions(-) diff --git a/driver/src/camera/stream/enum.ts b/driver/src/camera/stream/enum.ts index b5a4b07..7b0abde 100644 --- a/driver/src/camera/stream/enum.ts +++ b/driver/src/camera/stream/enum.ts @@ -15,6 +15,10 @@ export enum CamIsoInterface { CAMERA = 0, } +export enum CamIsoAltSetting { + CAMERA = 0, +} + export enum CamIsoStreamFlag { VIDEO = 0b1000, DEPTH = 0b0111, diff --git a/driver/src/camera/worker/get-device.ts b/driver/src/camera/worker/get-device.ts index fc038aa..07d2a50 100644 --- a/driver/src/camera/worker/get-device.ts +++ b/driver/src/camera/worker/get-device.ts @@ -1,4 +1,4 @@ -import type { CamIsoInterface } from "../stream/enum.js"; +import { CamIsoAltSetting, type CamIsoInterface } from "../stream/enum.js"; export async function prepareCamDevice( dev: USBDevice, @@ -19,10 +19,9 @@ export async function prepareCamDevice( await dev.claimInterface(usbInterface); } - await dev.selectAlternateInterface( - usbInterface, - iface.alternate.alternateSetting, - ); + if (iface.alternate.alternateSetting !== CamIsoAltSetting.CAMERA) { + await dev.selectAlternateInterface(usbInterface, CamIsoAltSetting.CAMERA); + } return dev; } diff --git a/driver/test/prepare-cam-device.test.ts b/driver/test/prepare-cam-device.test.ts index 66a798a..1a2176d 100644 --- a/driver/test/prepare-cam-device.test.ts +++ b/driver/test/prepare-cam-device.test.ts @@ -2,7 +2,10 @@ /** biome-ignore-all lint/style/noNonNullAssertion: test code */ import { describe, expect, test, vi } from "vitest"; -import { CamIsoInterface } from "../src/camera/stream/enum.js"; +import { + CamIsoAltSetting, + CamIsoInterface, +} from "../src/camera/stream/enum.js"; import { prepareCamDevice } from "../src/camera/worker/get-device.js"; const makeAlt = (alternateSetting: number): USBAlternateInterface => @@ -64,10 +67,10 @@ const makeDevice = ( }; describe("prepareCamDevice", () => { - test("claims and selects alt setting in order when interface is unclaimed", async () => { + test("claims then selects target alt when interface is unclaimed and on a different alt", async () => { const iface = makeIface(CamIsoInterface.CAMERA, { claimed: false, - alternateSetting: 0, + alternateSetting: 1, }); const { device, claimInterface, selectAlternateInterface } = makeDevice(iface); @@ -77,7 +80,7 @@ describe("prepareCamDevice", () => { expect(claimInterface).toHaveBeenCalledWith(CamIsoInterface.CAMERA); expect(selectAlternateInterface).toHaveBeenCalledWith( CamIsoInterface.CAMERA, - 0, + CamIsoAltSetting.CAMERA, ); const claimOrder = claimInterface.mock.invocationCallOrder[0]!; @@ -85,10 +88,10 @@ describe("prepareCamDevice", () => { expect(claimOrder).toBeLessThan(selectOrder); }); - test("skips claim when interface is already claimed but still selects alt", async () => { + test("skips claim when already claimed but still selects target alt if currently on a different alt", async () => { const iface = makeIface(CamIsoInterface.CAMERA, { claimed: true, - alternateSetting: 0, + alternateSetting: 1, }); const { device, claimInterface, selectAlternateInterface } = makeDevice(iface); @@ -98,23 +101,22 @@ describe("prepareCamDevice", () => { expect(claimInterface).not.toHaveBeenCalled(); expect(selectAlternateInterface).toHaveBeenCalledWith( CamIsoInterface.CAMERA, - 0, + CamIsoAltSetting.CAMERA, ); }); - test("forwards the descriptor's alternateSetting verbatim", async () => { + test("skips selectAlternateInterface when device is already on the target alt", async () => { const iface = makeIface(CamIsoInterface.CAMERA, { claimed: false, - alternateSetting: 3, + alternateSetting: CamIsoAltSetting.CAMERA, }); - const { device, selectAlternateInterface } = makeDevice(iface); + const { device, claimInterface, selectAlternateInterface } = + makeDevice(iface); await prepareCamDevice(device, CamIsoInterface.CAMERA); - expect(selectAlternateInterface).toHaveBeenCalledWith( - CamIsoInterface.CAMERA, - 3, - ); + expect(claimInterface).toHaveBeenCalledWith(CamIsoInterface.CAMERA); + expect(selectAlternateInterface).not.toHaveBeenCalled(); }); test("throws ReferenceError when the requested interface is not present", async () => {