Skip to content

FCE-3602: Create mobile custom source high level API#560

Open
MiloszFilimowski wants to merge 17 commits into
mainfrom
feat/vision-camera-source
Open

FCE-3602: Create mobile custom source high level API#560
MiloszFilimowski wants to merge 17 commits into
mainfrom
feat/vision-camera-source

Conversation

@MiloszFilimowski

Copy link
Copy Markdown
Collaborator

No description provided.

VisionCamera v5 frame outputs that publish camera frames to Fishjam —
zero-copy forwarding and a WebGPU-rendered video source.

- New package packages/react-native-vision-camera-source (useVisionCameraSource
  + /webgpu tier, forward/pooled track management, frame timestamp + orientation
  helpers).
- react-client/mobile-client: useCustomSource gains a UseCustomSourceOptions
  argument so a source can publish its video as a regular camera track
  (videoType: 'camera' — a "virtual camera").
- webrtc-client: route createDataChannel through createNewConnection so a
  data-channel-first connection still wires ICE/track managers (tracks added
  later were silently never published).
- Bump packages/react-native-webrtc to the custom-video-track native support.
- Add the package to the release workflow + workspaces.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@MiloszFilimowski MiloszFilimowski force-pushed the feat/vision-camera-source branch from d0356df to 8572830 Compare July 9, 2026 08:33
MiloszFilimowski and others added 15 commits July 9, 2026 10:35
A custom source is custom by definition — publishing its video as a
`camera` track was a footgun. Drop UseCustomSourceOptions /
CustomSourceTrackTypes and the videoType threading across react-client,
mobile-client, and react-native-vision-camera-source.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
setStream was referentially unstable because its useCallback closed over
the sources state, so it changed identity on every publish. Read sources
through a ref instead; setStream is now stable.

With that fixed, the useManagedCustomSource adapter (which existed only to
work around the instability via a ref) is redundant — inline its publish
effect into useVisionCameraSource and useVisionCameraWebGpuSource.

Add useCustomSourceManager tests pinning setStream stability and the
publish/unpublish lifecycle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Prep for the TGSL shader port: JS-body TGSL needs the unplugin-typegpu
build transform, which tsc can't run. Move the package build to bob with
a babel.config.js that runs @babel/preset-typescript + unplugin-typegpu.

- module target (configFile:true) so bob defers to babel.config.js;
  verified the built output preserves 'worklet' directives (for the
  consumer's react-native-worklets plugin) and resolves TGSL to WGSL.
- typescript target emits .d.ts to dist/typescript.
- exports/main/types repointed to dist/module + dist/typescript.
- add typegpu, unplugin-typegpu, react-native-builder-bob, babel deps.

No shader code changed yet; shaders still ship as raw WGSL strings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Port the /webgpu passthrough shader from hand-written WGSL strings to
TypeGPU:

- FrameCropParams is now a d.struct — one source of truth for the WGSL
  struct decl and the 40-byte worklet packer (size derived via d.sizeOf).
- vertexMain / fragmentMain are TGSL functions (tgpu.vertexFn/fragmentFn);
  the crop uniform is a tgpu.bindGroupLayout.
- sampleCamera is a WGSL-bodied tgpu.fn the TGSL fragment calls. It stays
  WGSL because typegpu 0.11 can't resolve a texture_external inside a TGSL
  function; its texture+sampler bindings are declared as raw WGSL too. This
  is the only part not authored in TGSL.
- Public API now exposes the sampleCamera TgpuFn + bindingDeclarations
  instead of a prebuilt shaderCode string (breaking).

The per-frame worklet path stays raw react-native-webgpu — TGSL resolves
to WGSL only at pipeline setup, so no tgpu proxy runs per frame.

Verified: generated WGSL is functionally identical to the previous
hand-written shader across ios/android x mirror (Android YUV decode
byte-identical; only cosmetic naming + an intentional bind-group index
swap differ), so GPU work is unchanged. CPU packing is untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
tgpu['~unstable'].vertexFn/fragmentFn are @deprecated aliases (the feature
graduated to stable); switch to the top-level tgpu.vertexFn/tgpu.fragmentFn.
Same function reference, so the generated WGSL is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Spell out that the WebGPU entry needs react-native-webgpu, the
unplugin-typegpu Babel plugin (for the TGSL shaders), and iOS 17+ for the
Metal camera-import path — and that the base camera-publishing tier
requires none of these. The package sets no deployment target itself
(pure JS); the app chooses its own.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This PR had bumped the mobile-client Expo plugin's default
IPHONEOS_DEPLOYMENT_TARGET from 15.1 to 17.0, which forces iOS 17 on
every @fishjam-cloud/react-native-client consumer — not just those using
the WebGPU tier. Restore 15.1; apps that need a higher floor (e.g. for
the /webgpu camera-import path) can still set it via
options.ios.iphoneDeploymentTarget.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add warnIfIosVersionUnsupported(): a one-time console.warn when the
/webgpu camera tier runs on iOS < 17, where Metal external-texture camera
import may fail. Surfaces a clear, actionable reason before the cryptic
device/feature error. Called automatically by useCameraWebGpuDevice (the
choke point both shared and bring-your-own-device paths pass through) and
exported for advanced callers that skip the hook. No-op off iOS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remove the custom-source manager test added for the setStream stability
fix, per request.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The effect mixed a (async()=>{})().catch() IIFE, mutable pool/created vars
reassigned in three places, and cancellation re-checked after every await.

Extract the two-step allocation into allocatePooledTrack() (which owns the
"free the orphaned pool if the track fails" case), collapse the effect to a
single disposed flag + one allocation holder disposed exactly once, and pull
out named helpers (toWorkletBufferDescriptor, toError, disposePool,
disposeAllocation).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
useManagedForwardTrack was already clean, but tidy it for consistency with
the pooled sibling: guard `disposed` (was `cancelled`) and only store the
created track when not torn down, mirroring useManagedPooledTrack.

Extract the repeated `cause instanceof Error ? cause : new Error(String(cause))`
into a shared internal toError() (4 call sites across the two managed-track
hooks and useCameraWebGpuDevice).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The vision-camera dep was used in only 3 files; the rest (the custom-video
track lifecycle hooks + the WebGPU camera toolkit) is source-agnostic.
Extract it into a new package @fishjam-cloud/react-native-custom-video-source
that publishes your own frames to Fishjam from any source, and make the
VisionCamera package a thin adapter on top of it.

- new package: internal/{useManagedForwardTrack,useManagedPooledTrack,toError}
  + webgpu/* (camera shader bindings, passthrough, crop, render context,
  required features, device, texture resolver, runtime). Owns typegpu +
  unplugin-typegpu (the TGSL shaders live here now).
- vision-camera-source keeps only the VC-coupled files (useVisionCameraSource,
  useVisionCameraWebGpuSource, frameTimestamp, orientation), depends on the
  generic package (workspace:*), and its /webgpu re-exports the generic
  toolkit. Dropped its now-unused typegpu/unplugin-typegpu deps + babel plugin.
- register the new workspace in the root package.json.

Both packages typecheck, build (bob), and lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- release: publish + version-bump react-native-custom-video-source, gate
  vision-camera-source publish on it
- vision-camera-source: react-native-worklets as peer dep (native module)
- webgpu: guard shared-device slot against late device-lost callbacks
- docs: fix stale package name in RNWebGPU warning, README outputView
  example, encodeCameraPassthrough single-crop constraint, timestamp
  fallback quirk, drop untrue "asserted in tests" claim
- extract shared usePublishedStream + stopStreamTracks helpers
- log frame-processing failures with the cause object

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@MiloszFilimowski MiloszFilimowski changed the title WIP: Custom Source FCE-3602: Create mobile custom source high level API Jul 10, 2026
@linear

linear Bot commented Jul 10, 2026

Copy link
Copy Markdown

FCE-3602

@MiloszFilimowski MiloszFilimowski requested review from Magmusacy and czerwiukk and removed request for czerwiukk July 10, 2026 10:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant