From 4cf7b7abf4704ba51c531a8292bb94c78b43c177 Mon Sep 17 00:00:00 2001 From: Gal Sasson Date: Tue, 30 Jun 2026 11:05:40 +0300 Subject: [PATCH 1/3] feat(video): support cameraFacing on VideoSourceInput Add optional `cameraFacing` ("user" | "environment") to VideoSourceInput, passed to `createVideoSource` as `cameraType`. The camera type propagates to the lens (DeviceCamera front/back-facing) and engine tracking, so back-facing / world video sources can be flagged correctly. Defaults to "user" (front). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/internal/sourceUtils.test.ts | 37 ++++++++++++++++++++++++++++++++ src/internal/sourceUtils.ts | 9 +++++++- src/types.ts | 6 ++++++ 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/internal/sourceUtils.test.ts b/src/internal/sourceUtils.test.ts index 6e60b76..1dc70e9 100644 --- a/src/internal/sourceUtils.test.ts +++ b/src/internal/sourceUtils.test.ts @@ -351,6 +351,43 @@ describe("sourceUtils", () => { expect(mockCreateVideoSource).toHaveBeenCalledWith(videoElement, undefined); }); + it("should pass cameraFacing through as cameraType", async () => { + const source = { + kind: "video" as const, + url: "https://example.com/video.mp4", + cameraFacing: "environment" as const, + }; + + const promise = createCameraKitSource(source); + videoElement.dispatchEvent(new Event("canplay")); + await promise; + + expect(mockCreateVideoSource).toHaveBeenCalledWith(videoElement, { cameraType: "environment" }); + }); + + it("should combine cameraFacing and tracking data", async () => { + const buffer = new ArrayBuffer(8); + (globalThis as { fetch?: typeof fetch }).fetch = jest + .fn() + .mockResolvedValue({ ok: true, arrayBuffer: () => Promise.resolve(buffer) }) as typeof fetch; + + const source = { + kind: "video" as const, + url: "https://example.com/video.mp4", + trackingDataUrl: "https://example.com/clip.td", + cameraFacing: "environment" as const, + }; + + const promise = createCameraKitSource(source); + videoElement.dispatchEvent(new Event("canplay")); + await promise; + + expect(mockCreateVideoSource).toHaveBeenCalledWith(videoElement, { + trackingData: buffer, + cameraType: "environment", + }); + }); + it("should reject when tracking data fails to load", async () => { (globalThis as { fetch?: typeof fetch }).fetch = jest .fn() diff --git a/src/internal/sourceUtils.ts b/src/internal/sourceUtils.ts index 2233c7a..ce055e4 100644 --- a/src/internal/sourceUtils.ts +++ b/src/internal/sourceUtils.ts @@ -44,6 +44,7 @@ export async function createCameraKitSource(source: SourceInput): Promise((res, rej) => { autoplay = autoplay ?? true; @@ -135,8 +138,12 @@ function createCameraKitVideoSource({ // before any playback has started, so there is no orphaned playing video to clean up. const trackingData = trackingDataUrl ? await fetchTrackingData(trackingDataUrl) : undefined; if (autoplay) await videoInput.play(); + const videoSourceOptions = + trackingData || cameraFacing + ? { ...(trackingData ? { trackingData } : {}), ...(cameraFacing ? { cameraType: cameraFacing } : {}) } + : undefined; res({ - cameraKitSource: createVideoSource(videoInput, trackingData ? { trackingData } : undefined), + cameraKitSource: createVideoSource(videoInput, videoSourceOptions), transform: Transform2D.Identity, inputSize: [videoInput.videoWidth, videoInput.videoHeight], initializedSourceInput: { diff --git a/src/types.ts b/src/types.ts index 32732d5..3b92436 100644 --- a/src/types.ts +++ b/src/types.ts @@ -55,6 +55,12 @@ export type VideoSourceInput = { * previewing world-facing lenses from a recorded environment. */ trackingDataUrl?: string; + /** + * Camera the video should be treated as coming from. Passed to `createVideoSource` as `cameraType`, + * which surfaces to the lens (e.g. front/back-facing behavior) and engine tracking. Defaults to + * `"user"` (front). Set `"environment"` for back-facing / world content. `"user"` ↔ front, `"environment"` ↔ back. + */ + cameraFacing?: CameraFacing; }; export type ImageSourceInput = { kind: "image"; url: string }; From 4f000867289a9ccafc525ef2708ad53cd56e9256 Mon Sep 17 00:00:00 2001 From: Gal Sasson Date: Tue, 30 Jun 2026 17:18:33 +0300 Subject: [PATCH 2/3] Address review: add fpsLimit, simplify options, reword cameraFacing doc - VideoSourceInput gains optional fpsLimit, passed to createVideoSource. - Pass options object directly (createVideoSource defaults undefined fields). - Reword cameraFacing doc per review. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/internal/sourceUtils.test.ts | 14 +++++++++++++- src/internal/sourceUtils.ts | 9 ++++----- src/types.ts | 11 ++++++++--- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/internal/sourceUtils.test.ts b/src/internal/sourceUtils.test.ts index 1dc70e9..0faf433 100644 --- a/src/internal/sourceUtils.test.ts +++ b/src/internal/sourceUtils.test.ts @@ -348,7 +348,19 @@ describe("sourceUtils", () => { videoElement.dispatchEvent(new Event("canplay")); await promise; - expect(mockCreateVideoSource).toHaveBeenCalledWith(videoElement, undefined); + // createVideoSource is always called with an options object; trackingData is just undefined here. + const options = mockCreateVideoSource.mock.calls[0]?.[1]; + expect(options?.trackingData).toBeUndefined(); + }); + + it("should pass fpsLimit through", async () => { + const source = { kind: "video" as const, url: "https://example.com/video.mp4", fpsLimit: 30 }; + + const promise = createCameraKitSource(source); + videoElement.dispatchEvent(new Event("canplay")); + await promise; + + expect(mockCreateVideoSource.mock.calls[0]?.[1]?.fpsLimit).toBe(30); }); it("should pass cameraFacing through as cameraType", async () => { diff --git a/src/internal/sourceUtils.ts b/src/internal/sourceUtils.ts index ce055e4..a232f9d 100644 --- a/src/internal/sourceUtils.ts +++ b/src/internal/sourceUtils.ts @@ -45,6 +45,7 @@ export async function createCameraKitSource(source: SourceInput): Promise((res, rej) => { autoplay = autoplay ?? true; @@ -138,12 +141,8 @@ function createCameraKitVideoSource({ // before any playback has started, so there is no orphaned playing video to clean up. const trackingData = trackingDataUrl ? await fetchTrackingData(trackingDataUrl) : undefined; if (autoplay) await videoInput.play(); - const videoSourceOptions = - trackingData || cameraFacing - ? { ...(trackingData ? { trackingData } : {}), ...(cameraFacing ? { cameraType: cameraFacing } : {}) } - : undefined; res({ - cameraKitSource: createVideoSource(videoInput, videoSourceOptions), + cameraKitSource: createVideoSource(videoInput, { trackingData, cameraType: cameraFacing, fpsLimit }), transform: Transform2D.Identity, inputSize: [videoInput.videoWidth, videoInput.videoHeight], initializedSourceInput: { diff --git a/src/types.ts b/src/types.ts index 3b92436..15dbce5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -56,11 +56,16 @@ export type VideoSourceInput = { */ trackingDataUrl?: string; /** - * Camera the video should be treated as coming from. Passed to `createVideoSource` as `cameraType`, - * which surfaces to the lens (e.g. front/back-facing behavior) and engine tracking. Defaults to - * `"user"` (front). Set `"environment"` for back-facing / world content. `"user"` ↔ front, `"environment"` ↔ back. + * Front/rear camera semantic the video should be treated as. Passed to CameraKit as `cameraType`, + * which affects Lens feature behavior such as surface tracking. Defaults to "user". + * "user" is front-facing; "environment" is rear-facing. */ cameraFacing?: CameraFacing; + /** + * Optional cap on frames per second processed from the video. Passed to `createVideoSource` as + * `fpsLimit`. Defaults to the video's native frame rate when omitted. + */ + fpsLimit?: number; }; export type ImageSourceInput = { kind: "image"; url: string }; From dc0baded307a02469af96f27abf51deaf3cd3b53 Mon Sep 17 00:00:00 2001 From: Gal Sasson Date: Tue, 30 Jun 2026 17:22:03 +0300 Subject: [PATCH 3/3] chore: bump version to 0.5.0 Co-Authored-By: Claude Opus 4.8 (1M context) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index d897954..4664eed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@snap/react-camera-kit", - "version": "0.4.0", + "version": "0.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@snap/react-camera-kit", - "version": "0.4.0", + "version": "0.5.0", "license": "MIT", "dependencies": { "stable-hash": "^0.0.6" diff --git a/package.json b/package.json index 21bd925..ea68024 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@snap/react-camera-kit", - "version": "0.4.0", + "version": "0.5.0", "description": "React Camera Kit for web applications", "type": "module", "main": "./dist/cjs/index.js",