@fishjam-cloud/react-native-custom-video-source 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +70 -0
  2. package/dist/module/index.js +22 -0
  3. package/dist/module/index.js.map +1 -0
  4. package/dist/module/internal/stopStreamTracks.js +9 -0
  5. package/dist/module/internal/stopStreamTracks.js.map +1 -0
  6. package/dist/module/internal/toError.js +5 -0
  7. package/dist/module/internal/toError.js.map +1 -0
  8. package/dist/module/internal/useManagedForwardTrack.js +63 -0
  9. package/dist/module/internal/useManagedForwardTrack.js.map +1 -0
  10. package/dist/module/internal/useManagedPooledTrack.js +120 -0
  11. package/dist/module/internal/useManagedPooledTrack.js.map +1 -0
  12. package/dist/module/webgpu/cameraPassthroughPipeline.js +237 -0
  13. package/dist/module/webgpu/cameraPassthroughPipeline.js.map +1 -0
  14. package/dist/module/webgpu/cameraShaderBindings.js +126 -0
  15. package/dist/module/webgpu/cameraShaderBindings.js.map +1 -0
  16. package/dist/module/webgpu/cameraTextureResolver.js +52 -0
  17. package/dist/module/webgpu/cameraTextureResolver.js.map +1 -0
  18. package/dist/module/webgpu/cropUtilities.js +121 -0
  19. package/dist/module/webgpu/cropUtilities.js.map +1 -0
  20. package/dist/module/webgpu/frameRenderContext.js +2 -0
  21. package/dist/module/webgpu/frameRenderContext.js.map +1 -0
  22. package/dist/module/webgpu/requiredFeatures.js +46 -0
  23. package/dist/module/webgpu/requiredFeatures.js.map +1 -0
  24. package/dist/module/webgpu/useCameraWebGpuDevice.js +114 -0
  25. package/dist/module/webgpu/useCameraWebGpuDevice.js.map +1 -0
  26. package/dist/module/webgpu/webGpuRuntime.js +25 -0
  27. package/dist/module/webgpu/webGpuRuntime.js.map +1 -0
  28. package/dist/module/webgpu.js +25 -0
  29. package/dist/module/webgpu.js.map +1 -0
  30. package/dist/typescript/index.d.ts +21 -0
  31. package/dist/typescript/index.d.ts.map +1 -0
  32. package/dist/typescript/internal/stopStreamTracks.d.ts +4 -0
  33. package/dist/typescript/internal/stopStreamTracks.d.ts.map +1 -0
  34. package/dist/typescript/internal/toError.d.ts +3 -0
  35. package/dist/typescript/internal/toError.d.ts.map +1 -0
  36. package/dist/typescript/internal/useManagedForwardTrack.d.ts +17 -0
  37. package/dist/typescript/internal/useManagedForwardTrack.d.ts.map +1 -0
  38. package/dist/typescript/internal/useManagedPooledTrack.d.ts +26 -0
  39. package/dist/typescript/internal/useManagedPooledTrack.d.ts.map +1 -0
  40. package/dist/typescript/webgpu/cameraPassthroughPipeline.d.ts +46 -0
  41. package/dist/typescript/webgpu/cameraPassthroughPipeline.d.ts.map +1 -0
  42. package/dist/typescript/webgpu/cameraShaderBindings.d.ts +69 -0
  43. package/dist/typescript/webgpu/cameraShaderBindings.d.ts.map +1 -0
  44. package/dist/typescript/webgpu/cameraTextureResolver.d.ts +39 -0
  45. package/dist/typescript/webgpu/cameraTextureResolver.d.ts.map +1 -0
  46. package/dist/typescript/webgpu/cropUtilities.d.ts +67 -0
  47. package/dist/typescript/webgpu/cropUtilities.d.ts.map +1 -0
  48. package/dist/typescript/webgpu/frameRenderContext.d.ts +61 -0
  49. package/dist/typescript/webgpu/frameRenderContext.d.ts.map +1 -0
  50. package/dist/typescript/webgpu/requiredFeatures.d.ts +25 -0
  51. package/dist/typescript/webgpu/requiredFeatures.d.ts.map +1 -0
  52. package/dist/typescript/webgpu/useCameraWebGpuDevice.d.ts +27 -0
  53. package/dist/typescript/webgpu/useCameraWebGpuDevice.d.ts.map +1 -0
  54. package/dist/typescript/webgpu/webGpuRuntime.d.ts +14 -0
  55. package/dist/typescript/webgpu/webGpuRuntime.d.ts.map +1 -0
  56. package/dist/typescript/webgpu.d.ts +24 -0
  57. package/dist/typescript/webgpu.d.ts.map +1 -0
  58. package/package.json +109 -0
package/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # `@fishjam-cloud/react-native-custom-video-source`
2
+
3
+ Publish your own video frames to Fishjam from **any** frame source. This is the generic layer
4
+ under [`@fishjam-cloud/react-native-vision-camera-source`](../react-native-vision-camera-source) —
5
+ use that package if your source is [VisionCamera](https://react-native-vision-camera.com); use this
6
+ one directly for any other source (a native ML pipeline, a compositor, your own renderer).
7
+
8
+ There are two modes, picked by **how you produce frames**:
9
+
10
+ - **Forwarding** — you already have finished native buffers. `useManagedForwardTrack()` creates and
11
+ owns the track; hand each buffer pointer to `forwardFrame` from `@fishjam-cloud/react-native-webrtc`.
12
+ - **Render-target (pooled)** — you render the frames yourself. `useManagedPooledTrack()` allocates a
13
+ surface pool; draw into it and hand each frame back with `pushFrame`. The
14
+ [`/webgpu`](#webgpu-camera-toolkit) entry point provides a WebGPU camera-rendering toolkit for this
15
+ mode.
16
+
17
+ Each hook owns the track's async lifecycle — creation, the published `stream`, and teardown — so you
18
+ only feed frames. Must be used under `FishjamProvider` from `@fishjam-cloud/react-native-client`.
19
+
20
+ ## Prerequisites
21
+
22
+ - `@fishjam-cloud/react-native-webrtc` and `@fishjam-cloud/react-native-client` (with your app wrapped
23
+ in `FishjamProvider`)
24
+ - New Architecture (custom video tracks require it)
25
+ - For the `/webgpu` entry only: `react-native-webgpu` ≥ 0.5.15, plus `unplugin-typegpu` in your app's
26
+ Babel config (the shaders are authored in [TypeGPU](https://docs.swmansion.com/TypeGPU/)), and
27
+ **iOS 17+** for the Metal external-texture camera-import path.
28
+
29
+ ## Forward finished buffers
30
+
31
+ ```tsx
32
+ import { useManagedForwardTrack } from '@fishjam-cloud/react-native-custom-video-source';
33
+ import { forwardFrame } from '@fishjam-cloud/react-native-webrtc';
34
+ import { useCustomSource } from '@fishjam-cloud/react-native-client';
35
+
36
+ const { track, stream } = useManagedForwardTrack(/* enabled */ true);
37
+ // publish `stream` (e.g. via useCustomSource), then per frame:
38
+ forwardFrame(track, { nativeBuffer /* bigint CVPixelBufferRef / AHardwareBuffer* */ });
39
+ ```
40
+
41
+ ## Render your own frames
42
+
43
+ ```tsx
44
+ import { useManagedPooledTrack } from '@fishjam-cloud/react-native-custom-video-source';
45
+ import { pushFrame } from '@fishjam-cloud/react-native-webrtc';
46
+
47
+ const { track, bufferDescriptors } = useManagedPooledTrack(true, 720, 1280, /* poolSize */ 3);
48
+ // import bufferDescriptors[i].surfaceHandle into your GPU, render into slot i, then:
49
+ pushFrame(track, { bufferIndex: i, timestampNs });
50
+ ```
51
+
52
+ ## WebGPU camera toolkit
53
+
54
+ `@fishjam-cloud/react-native-custom-video-source/webgpu` samples the live camera and helps you render
55
+ into the pooled surfaces:
56
+
57
+ - `createCameraShaderBindings` / `sampleCamera` — sample the camera from your own shaders, YUV decode
58
+ handled per platform
59
+ - `createCameraPassthroughPipeline` / `encodeCameraPassthrough` — a ready-made camera→output pass
60
+ - `useCameraWebGpuDevice` — the shared, camera-import-capable `GPUDevice`
61
+ - `computeAspectFillCrop` / `computeSquareCrop` / `packFrameCropParams` — crop + orientation helpers
62
+
63
+ > Verify camera + WebGPU behavior on **physical devices** — the iOS Simulator cannot import the
64
+ > camera's YUV textures.
65
+
66
+ ## Development
67
+
68
+ Part of the [web-client-sdk](https://github.com/fishjam-cloud/web-client-sdk) monorepo. `yarn build`
69
+ compiles `src/` to `dist/` with `react-native-builder-bob` (Babel + `unplugin-typegpu` for the TGSL
70
+ shaders, `tsc` for type definitions).
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Publish your own video frames to Fishjam.
3
+ *
4
+ * Source-lifecycle hooks that create a custom video track, publish it, and clean it up — you
5
+ * supply the frames from any source. Two modes, picked by how you produce frames:
6
+ *
7
+ * - {@link useManagedForwardTrack} — you already have finished native buffers (a camera, a native
8
+ * ML pipeline, a compositor); forward each buffer pointer with `forwardFrame` from
9
+ * `@fishjam-cloud/react-native-webrtc`.
10
+ * - {@link useManagedPooledTrack} — you render the frames yourself; allocate a surface pool, draw
11
+ * into it, and hand each frame back with `pushFrame`. The `@fishjam-cloud/react-native-custom-video-source/webgpu`
12
+ * entry point provides a WebGPU camera-rendering toolkit for this mode.
13
+ *
14
+ * For a ready-made VisionCamera integration on top of this, use
15
+ * `@fishjam-cloud/react-native-vision-camera-source`.
16
+ *
17
+ * @packageDocumentation
18
+ */
19
+
20
+ export { useManagedForwardTrack } from './internal/useManagedForwardTrack';
21
+ export { useManagedPooledTrack } from './internal/useManagedPooledTrack';
22
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["useManagedForwardTrack","useManagedPooledTrack"],"sources":["index.ts"],"sourcesContent":["/**\n * Publish your own video frames to Fishjam.\n *\n * Source-lifecycle hooks that create a custom video track, publish it, and clean it up — you\n * supply the frames from any source. Two modes, picked by how you produce frames:\n *\n * - {@link useManagedForwardTrack} — you already have finished native buffers (a camera, a native\n * ML pipeline, a compositor); forward each buffer pointer with `forwardFrame` from\n * `@fishjam-cloud/react-native-webrtc`.\n * - {@link useManagedPooledTrack} — you render the frames yourself; allocate a surface pool, draw\n * into it, and hand each frame back with `pushFrame`. The `@fishjam-cloud/react-native-custom-video-source/webgpu`\n * entry point provides a WebGPU camera-rendering toolkit for this mode.\n *\n * For a ready-made VisionCamera integration on top of this, use\n * `@fishjam-cloud/react-native-vision-camera-source`.\n *\n * @packageDocumentation\n */\n\nexport { type ManagedForwardTrack, useManagedForwardTrack } from './internal/useManagedForwardTrack';\nexport {\n type ManagedPooledTrack,\n useManagedPooledTrack,\n type WorkletBufferDescriptor,\n} from './internal/useManagedPooledTrack';\n"],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA,SAAmCA,sBAAsB,QAAQ,mCAAmC;AACpG,SAEEC,qBAAqB,QAEhB,kCAAkC","ignoreList":[]}
@@ -0,0 +1,9 @@
1
+ /** Stops every track of `stream`, downgrading failures to a warning tagged with `ownerLabel`. */
2
+ export function stopStreamTracks(stream, ownerLabel) {
3
+ try {
4
+ stream.getTracks().forEach(mediaTrack => mediaTrack.stop());
5
+ } catch (cause) {
6
+ console.warn(`${ownerLabel}: stopping tracks failed`, cause);
7
+ }
8
+ }
9
+ //# sourceMappingURL=stopStreamTracks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["stopStreamTracks","stream","ownerLabel","getTracks","forEach","mediaTrack","stop","cause","console","warn"],"sources":["stopStreamTracks.ts"],"sourcesContent":["import type { MediaStream } from '@fishjam-cloud/react-native-webrtc';\n\n/** Stops every track of `stream`, downgrading failures to a warning tagged with `ownerLabel`. */\nexport function stopStreamTracks(stream: MediaStream, ownerLabel: string): void {\n try {\n stream.getTracks().forEach((mediaTrack) => mediaTrack.stop());\n } catch (cause) {\n console.warn(`${ownerLabel}: stopping tracks failed`, cause);\n }\n}\n"],"mappings":"AAEA;AACA,OAAO,SAASA,gBAAgBA,CAACC,MAAmB,EAAEC,UAAkB,EAAQ;EAC9E,IAAI;IACFD,MAAM,CAACE,SAAS,CAAC,CAAC,CAACC,OAAO,CAAEC,UAAU,IAAKA,UAAU,CAACC,IAAI,CAAC,CAAC,CAAC;EAC/D,CAAC,CAAC,OAAOC,KAAK,EAAE;IACdC,OAAO,CAACC,IAAI,CAAC,GAAGP,UAAU,0BAA0B,EAAEK,KAAK,CAAC;EAC9D;AACF","ignoreList":[]}
@@ -0,0 +1,5 @@
1
+ /** Coerce an unknown thrown value into an Error, leaving real Errors untouched. */
2
+ export function toError(cause) {
3
+ return cause instanceof Error ? cause : new Error(String(cause));
4
+ }
5
+ //# sourceMappingURL=toError.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["toError","cause","Error","String"],"sources":["toError.ts"],"sourcesContent":["/** Coerce an unknown thrown value into an Error, leaving real Errors untouched. */\nexport function toError(cause: unknown): Error {\n return cause instanceof Error ? cause : new Error(String(cause));\n}\n"],"mappings":"AAAA;AACA,OAAO,SAASA,OAAOA,CAACC,KAAc,EAAS;EAC7C,OAAOA,KAAK,YAAYC,KAAK,GAAGD,KAAK,GAAG,IAAIC,KAAK,CAACC,MAAM,CAACF,KAAK,CAAC,CAAC;AAClE","ignoreList":[]}
@@ -0,0 +1,63 @@
1
+ import { createCustomVideoTrack } from '@fishjam-cloud/react-native-webrtc';
2
+ import { useEffect, useState } from 'react';
3
+ import { stopStreamTracks } from './stopStreamTracks';
4
+ import { toError } from './toError';
5
+
6
+ /**
7
+ * State of a forwarding custom video track managed by {@link useManagedForwardTrack}.
8
+ * While the track is being created (or after an error) `track` and `stream` are `null`.
9
+ */
10
+
11
+ const INITIAL_STATE = {
12
+ track: null,
13
+ stream: null,
14
+ error: null
15
+ };
16
+
17
+ /**
18
+ * Owns the async lifecycle of a forwarding custom video track: creates it while `enabled`,
19
+ * exposes the handle + stream once ready, and stops the tracks on disable/unmount (also when
20
+ * creation resolves after the owner already unmounted).
21
+ */
22
+ export function useManagedForwardTrack(enabled) {
23
+ const [managedTrack, setManagedTrack] = useState(INITIAL_STATE);
24
+ useEffect(() => {
25
+ // The disabled case needs no work: the previous run's cleanup already reset the state.
26
+ if (!enabled) {
27
+ return;
28
+ }
29
+ // Creation is async, so the effect may be torn down before it resolves. `disposed` records
30
+ // that; `created` holds the result once it exists so cleanup can stop it exactly once.
31
+ let disposed = false;
32
+ let created = null;
33
+ createCustomVideoTrack().then(result => {
34
+ if (disposed) {
35
+ // Torn down while creating — throw the just-built track away.
36
+ stopStreamTracks(result.stream, 'useManagedForwardTrack');
37
+ return;
38
+ }
39
+ created = result;
40
+ setManagedTrack({
41
+ track: result.track,
42
+ stream: result.stream,
43
+ error: null
44
+ });
45
+ }).catch(cause => {
46
+ if (!disposed) {
47
+ setManagedTrack({
48
+ ...INITIAL_STATE,
49
+ error: toError(cause)
50
+ });
51
+ }
52
+ });
53
+ return () => {
54
+ disposed = true;
55
+ if (created != null) {
56
+ stopStreamTracks(created.stream, 'useManagedForwardTrack');
57
+ }
58
+ setManagedTrack(INITIAL_STATE);
59
+ };
60
+ }, [enabled]);
61
+ return managedTrack;
62
+ }
63
+ //# sourceMappingURL=useManagedForwardTrack.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["createCustomVideoTrack","useEffect","useState","stopStreamTracks","toError","INITIAL_STATE","track","stream","error","useManagedForwardTrack","enabled","managedTrack","setManagedTrack","disposed","created","then","result","catch","cause"],"sources":["useManagedForwardTrack.ts"],"sourcesContent":["import {\n createCustomVideoTrack,\n type CustomVideoTrackResult,\n type ForwardTrack,\n type MediaStream,\n} from '@fishjam-cloud/react-native-webrtc';\nimport { useEffect, useState } from 'react';\n\nimport { stopStreamTracks } from './stopStreamTracks';\nimport { toError } from './toError';\n\n/**\n * State of a forwarding custom video track managed by {@link useManagedForwardTrack}.\n * While the track is being created (or after an error) `track` and `stream` are `null`.\n */\nexport interface ManagedForwardTrack {\n track: ForwardTrack | null;\n stream: MediaStream | null;\n error: Error | null;\n}\n\nconst INITIAL_STATE: ManagedForwardTrack = { track: null, stream: null, error: null };\n\n/**\n * Owns the async lifecycle of a forwarding custom video track: creates it while `enabled`,\n * exposes the handle + stream once ready, and stops the tracks on disable/unmount (also when\n * creation resolves after the owner already unmounted).\n */\nexport function useManagedForwardTrack(enabled: boolean): ManagedForwardTrack {\n const [managedTrack, setManagedTrack] = useState<ManagedForwardTrack>(INITIAL_STATE);\n\n useEffect(() => {\n // The disabled case needs no work: the previous run's cleanup already reset the state.\n if (!enabled) {\n return;\n }\n // Creation is async, so the effect may be torn down before it resolves. `disposed` records\n // that; `created` holds the result once it exists so cleanup can stop it exactly once.\n let disposed = false;\n let created: CustomVideoTrackResult<ForwardTrack> | null = null;\n\n createCustomVideoTrack()\n .then((result) => {\n if (disposed) {\n // Torn down while creating — throw the just-built track away.\n stopStreamTracks(result.stream, 'useManagedForwardTrack');\n return;\n }\n created = result;\n setManagedTrack({ track: result.track, stream: result.stream, error: null });\n })\n .catch((cause: unknown) => {\n if (!disposed) {\n setManagedTrack({ ...INITIAL_STATE, error: toError(cause) });\n }\n });\n\n return () => {\n disposed = true;\n if (created != null) {\n stopStreamTracks(created.stream, 'useManagedForwardTrack');\n }\n setManagedTrack(INITIAL_STATE);\n };\n }, [enabled]);\n\n return managedTrack;\n}\n"],"mappings":"AAAA,SACEA,sBAAsB,QAIjB,oCAAoC;AAC3C,SAASC,SAAS,EAAEC,QAAQ,QAAQ,OAAO;AAE3C,SAASC,gBAAgB,QAAQ,oBAAoB;AACrD,SAASC,OAAO,QAAQ,WAAW;;AAEnC;AACA;AACA;AACA;;AAOA,MAAMC,aAAkC,GAAG;EAAEC,KAAK,EAAE,IAAI;EAAEC,MAAM,EAAE,IAAI;EAAEC,KAAK,EAAE;AAAK,CAAC;;AAErF;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,sBAAsBA,CAACC,OAAgB,EAAuB;EAC5E,MAAM,CAACC,YAAY,EAAEC,eAAe,CAAC,GAAGV,QAAQ,CAAsBG,aAAa,CAAC;EAEpFJ,SAAS,CAAC,MAAM;IACd;IACA,IAAI,CAACS,OAAO,EAAE;MACZ;IACF;IACA;IACA;IACA,IAAIG,QAAQ,GAAG,KAAK;IACpB,IAAIC,OAAoD,GAAG,IAAI;IAE/Dd,sBAAsB,CAAC,CAAC,CACrBe,IAAI,CAAEC,MAAM,IAAK;MAChB,IAAIH,QAAQ,EAAE;QACZ;QACAV,gBAAgB,CAACa,MAAM,CAACT,MAAM,EAAE,wBAAwB,CAAC;QACzD;MACF;MACAO,OAAO,GAAGE,MAAM;MAChBJ,eAAe,CAAC;QAAEN,KAAK,EAAEU,MAAM,CAACV,KAAK;QAAEC,MAAM,EAAES,MAAM,CAACT,MAAM;QAAEC,KAAK,EAAE;MAAK,CAAC,CAAC;IAC9E,CAAC,CAAC,CACDS,KAAK,CAAEC,KAAc,IAAK;MACzB,IAAI,CAACL,QAAQ,EAAE;QACbD,eAAe,CAAC;UAAE,GAAGP,aAAa;UAAEG,KAAK,EAAEJ,OAAO,CAACc,KAAK;QAAE,CAAC,CAAC;MAC9D;IACF,CAAC,CAAC;IAEJ,OAAO,MAAM;MACXL,QAAQ,GAAG,IAAI;MACf,IAAIC,OAAO,IAAI,IAAI,EAAE;QACnBX,gBAAgB,CAACW,OAAO,CAACP,MAAM,EAAE,wBAAwB,CAAC;MAC5D;MACAK,eAAe,CAACP,aAAa,CAAC;IAChC,CAAC;EACH,CAAC,EAAE,CAACK,OAAO,CAAC,CAAC;EAEb,OAAOC,YAAY;AACrB","ignoreList":[]}
@@ -0,0 +1,120 @@
1
+ import { createCustomVideoBufferPool, createCustomVideoTrack } from '@fishjam-cloud/react-native-webrtc';
2
+ import { useEffect, useState } from 'react';
3
+ import { stopStreamTracks } from './stopStreamTracks';
4
+ import { toError } from './toError';
5
+
6
+ /** One pooled output surface as plain values the frame worklet can capture and import itself. */
7
+
8
+ /**
9
+ * State of a pooled custom video track managed by {@link useManagedPooledTrack}.
10
+ * While the pool and track are being created (or after an error) all fields are `null`.
11
+ */
12
+
13
+ const INITIAL_STATE = {
14
+ track: null,
15
+ stream: null,
16
+ bufferDescriptors: null,
17
+ error: null
18
+ };
19
+
20
+ /** A fully-allocated pool + its track, owned and torn down as a single unit. */
21
+
22
+ function toWorkletBufferDescriptor(buffer) {
23
+ return {
24
+ index: buffer.index,
25
+ surfaceHandle: buffer.surfaceHandle,
26
+ width: buffer.width,
27
+ height: buffer.height
28
+ };
29
+ }
30
+ function disposePool(pool) {
31
+ void pool.dispose().catch(cause => {
32
+ console.warn('useManagedPooledTrack: disposing the buffer pool failed', cause);
33
+ });
34
+ }
35
+
36
+ /** Tears an allocation down in the required order: stop the track's frames, then free its pool. */
37
+ function disposeAllocation({
38
+ pool,
39
+ stream
40
+ }) {
41
+ stopStreamTracks(stream, 'useManagedPooledTrack');
42
+ disposePool(pool);
43
+ }
44
+
45
+ /** Allocates the surface pool and a track bound to it. If the track fails, frees the orphan pool. */
46
+ async function allocatePooledTrack(width, height, poolSize) {
47
+ const pool = await createCustomVideoBufferPool({
48
+ width,
49
+ height,
50
+ poolSize
51
+ });
52
+ try {
53
+ const {
54
+ track,
55
+ stream
56
+ } = await createCustomVideoTrack({
57
+ pool
58
+ });
59
+ return {
60
+ pool,
61
+ track,
62
+ stream,
63
+ bufferDescriptors: pool.buffers.map(toWorkletBufferDescriptor)
64
+ };
65
+ } catch (cause) {
66
+ disposePool(pool);
67
+ throw cause;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Owns the async lifecycle of a surface pool + pooled custom video track: allocates both while
73
+ * `enabled` (re-allocates when the dimensions change), exposes worklet-ready descriptors, and
74
+ * tears down in the correct order (stop tracks, then dispose the pool) on disable/unmount.
75
+ */
76
+ export function useManagedPooledTrack(enabled, width, height, poolSize) {
77
+ const [managedTrack, setManagedTrack] = useState(INITIAL_STATE);
78
+ useEffect(() => {
79
+ // The disabled case needs no work: the previous run's cleanup already reset the state (so
80
+ // consumers never hold descriptors of a disposed pool).
81
+ if (!enabled) {
82
+ return;
83
+ }
84
+
85
+ // Allocation is async, so the effect may be torn down before it resolves. `disposed` records
86
+ // that; `allocation` holds the result once it exists so cleanup can free it exactly once.
87
+ let disposed = false;
88
+ let allocation = null;
89
+ allocatePooledTrack(width, height, poolSize).then(result => {
90
+ if (disposed) {
91
+ // Torn down while allocating — throw the just-built resources away.
92
+ disposeAllocation(result);
93
+ return;
94
+ }
95
+ allocation = result;
96
+ setManagedTrack({
97
+ track: result.track,
98
+ stream: result.stream,
99
+ bufferDescriptors: result.bufferDescriptors,
100
+ error: null
101
+ });
102
+ }).catch(cause => {
103
+ if (!disposed) {
104
+ setManagedTrack({
105
+ ...INITIAL_STATE,
106
+ error: toError(cause)
107
+ });
108
+ }
109
+ });
110
+ return () => {
111
+ disposed = true;
112
+ if (allocation) {
113
+ disposeAllocation(allocation);
114
+ }
115
+ setManagedTrack(INITIAL_STATE);
116
+ };
117
+ }, [enabled, width, height, poolSize]);
118
+ return managedTrack;
119
+ }
120
+ //# sourceMappingURL=useManagedPooledTrack.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["createCustomVideoBufferPool","createCustomVideoTrack","useEffect","useState","stopStreamTracks","toError","INITIAL_STATE","track","stream","bufferDescriptors","error","toWorkletBufferDescriptor","buffer","index","surfaceHandle","width","height","disposePool","pool","dispose","catch","cause","console","warn","disposeAllocation","allocatePooledTrack","poolSize","buffers","map","useManagedPooledTrack","enabled","managedTrack","setManagedTrack","disposed","allocation","then","result"],"sources":["useManagedPooledTrack.ts"],"sourcesContent":["import {\n createCustomVideoBufferPool,\n createCustomVideoTrack,\n type CustomVideoBuffer,\n type CustomVideoBufferPool,\n type MediaStream,\n type PooledTrack,\n} from '@fishjam-cloud/react-native-webrtc';\nimport { useEffect, useState } from 'react';\n\nimport { stopStreamTracks } from './stopStreamTracks';\nimport { toError } from './toError';\n\n/** One pooled output surface as plain values the frame worklet can capture and import itself. */\nexport interface WorkletBufferDescriptor {\n index: number;\n surfaceHandle: bigint;\n width: number;\n height: number;\n}\n\n/**\n * State of a pooled custom video track managed by {@link useManagedPooledTrack}.\n * While the pool and track are being created (or after an error) all fields are `null`.\n */\nexport interface ManagedPooledTrack {\n track: PooledTrack | null;\n stream: MediaStream | null;\n /** Plain per-surface descriptors (the pool object itself is not worklet-serializable). */\n bufferDescriptors: WorkletBufferDescriptor[] | null;\n error: Error | null;\n}\n\nconst INITIAL_STATE: ManagedPooledTrack = { track: null, stream: null, bufferDescriptors: null, error: null };\n\n/** A fully-allocated pool + its track, owned and torn down as a single unit. */\ninterface PooledTrackAllocation {\n pool: CustomVideoBufferPool;\n track: PooledTrack;\n stream: MediaStream;\n bufferDescriptors: WorkletBufferDescriptor[];\n}\n\nfunction toWorkletBufferDescriptor(buffer: CustomVideoBuffer): WorkletBufferDescriptor {\n return { index: buffer.index, surfaceHandle: buffer.surfaceHandle, width: buffer.width, height: buffer.height };\n}\n\nfunction disposePool(pool: CustomVideoBufferPool): void {\n void pool.dispose().catch((cause: unknown) => {\n console.warn('useManagedPooledTrack: disposing the buffer pool failed', cause);\n });\n}\n\n/** Tears an allocation down in the required order: stop the track's frames, then free its pool. */\nfunction disposeAllocation({ pool, stream }: PooledTrackAllocation): void {\n stopStreamTracks(stream, 'useManagedPooledTrack');\n disposePool(pool);\n}\n\n/** Allocates the surface pool and a track bound to it. If the track fails, frees the orphan pool. */\nasync function allocatePooledTrack(width: number, height: number, poolSize: number): Promise<PooledTrackAllocation> {\n const pool = await createCustomVideoBufferPool({ width, height, poolSize });\n try {\n const { track, stream } = await createCustomVideoTrack({ pool });\n return { pool, track, stream, bufferDescriptors: pool.buffers.map(toWorkletBufferDescriptor) };\n } catch (cause) {\n disposePool(pool);\n throw cause;\n }\n}\n\n/**\n * Owns the async lifecycle of a surface pool + pooled custom video track: allocates both while\n * `enabled` (re-allocates when the dimensions change), exposes worklet-ready descriptors, and\n * tears down in the correct order (stop tracks, then dispose the pool) on disable/unmount.\n */\nexport function useManagedPooledTrack(\n enabled: boolean,\n width: number,\n height: number,\n poolSize: number,\n): ManagedPooledTrack {\n const [managedTrack, setManagedTrack] = useState<ManagedPooledTrack>(INITIAL_STATE);\n\n useEffect(() => {\n // The disabled case needs no work: the previous run's cleanup already reset the state (so\n // consumers never hold descriptors of a disposed pool).\n if (!enabled) {\n return;\n }\n\n // Allocation is async, so the effect may be torn down before it resolves. `disposed` records\n // that; `allocation` holds the result once it exists so cleanup can free it exactly once.\n let disposed = false;\n let allocation: PooledTrackAllocation | null = null;\n\n allocatePooledTrack(width, height, poolSize)\n .then((result) => {\n if (disposed) {\n // Torn down while allocating — throw the just-built resources away.\n disposeAllocation(result);\n return;\n }\n allocation = result;\n setManagedTrack({\n track: result.track,\n stream: result.stream,\n bufferDescriptors: result.bufferDescriptors,\n error: null,\n });\n })\n .catch((cause: unknown) => {\n if (!disposed) {\n setManagedTrack({ ...INITIAL_STATE, error: toError(cause) });\n }\n });\n\n return () => {\n disposed = true;\n if (allocation) {\n disposeAllocation(allocation);\n }\n setManagedTrack(INITIAL_STATE);\n };\n }, [enabled, width, height, poolSize]);\n\n return managedTrack;\n}\n"],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,237 @@
1
+ import { GPUBufferUsage, GPUShaderStage } from 'react-native-webgpu';
2
+ import tgpu from 'typegpu';
3
+ import * as d from 'typegpu/data';
4
+ import { add, div, mul, sub } from 'typegpu/std';
5
+ import { createCameraBindGroup, createCameraShaderBindings, sampleCamera } from './cameraShaderBindings';
6
+ import { FrameCropParams, packFrameCropParams } from './cropUtilities';
7
+ import { getOutputSurfaceFormat } from './requiredFeatures';
8
+
9
+ // Crop is a plain uniform, so it is authored in TGSL and TypeGPU assigns it @group(0). The camera
10
+ // is a `texture_external`, which TypeGPU cannot resolve in a shader, so its bindings are raw WGSL
11
+ // and go at @group(1) (see cameraShaderBindings). This is a group swap from the pre-TGSL shader
12
+ // (which had camera at 0, crop at 1) but is otherwise identical.
13
+ const CROP_BIND_GROUP_INDEX = 0;
14
+ const CAMERA_BIND_GROUP_INDEX = 1;
15
+ const FRAME_CROP_BUFFER_BYTES = d.sizeOf(FrameCropParams);
16
+
17
+ // TypeGPU bind group layout for the crop uniform — the source of truth for the WGSL declaration
18
+ // and for the fragment's typed access to `cropParams`.
19
+ const cropLayout = /*#__PURE__*/(globalThis.__TYPEGPU_AUTONAME__ ?? (a => a))(tgpu.bindGroupLayout({
20
+ cropParams: {
21
+ uniform: FrameCropParams
22
+ }
23
+ }), "cropLayout");
24
+
25
+ // One oversized triangle covering the viewport; uv spans [0,1] top-left origin over the visible
26
+ // area.
27
+ const vertexMain = tgpu.vertexFn({
28
+ in: {
29
+ vertexIndex: d.builtin.vertexIndex
30
+ },
31
+ out: {
32
+ position: d.builtin.position,
33
+ uv: d.location(0, d.vec2f)
34
+ }
35
+ })(/*#__PURE__*/($ => (globalThis.__TYPEGPU_META__ ??= new WeakMap()).set($.f = input => {
36
+ const positions = [d.vec2f(-1, -1), d.vec2f(3, -1), d.vec2f(-1, 3)];
37
+ const position = positions[input.vertexIndex];
38
+ return {
39
+ position: d.vec4f(position.x, position.y, 0, 1),
40
+ uv: d.vec2f((position.x + 1) * 0.5, 1 - (position.y + 1) * 0.5)
41
+ };
42
+ }, {
43
+ v: 1,
44
+ name: undefined,
45
+ ast: {
46
+ params: [{
47
+ type: "i",
48
+ name: "input"
49
+ }],
50
+ body: [0, [[13, "positions", [100, [[6, [7, "d", "vec2f"], [[4, "-", [5, "1"]], [4, "-", [5, "1"]]]], [6, [7, "d", "vec2f"], [[5, "3"], [4, "-", [5, "1"]]]], [6, [7, "d", "vec2f"], [[4, "-", [5, "1"]], [5, "3"]]]]]], [13, "position", [8, "positions", [7, "input", "vertexIndex"]]], [10, [104, {
51
+ position: [6, [7, "d", "vec4f"], [[7, "position", "x"], [7, "position", "y"], [5, "0"], [5, "1"]]],
52
+ uv: [6, [7, "d", "vec2f"], [[1, [1, [7, "position", "x"], "+", [5, "1"]], "*", [5, "0.5"]], [1, [5, "1"], "-", [1, [1, [7, "position", "y"], "+", [5, "1"]], "*", [5, "0.5"]]]]]
53
+ }]]]],
54
+ externalNames: ["d"]
55
+ },
56
+ externals: () => {
57
+ return {
58
+ d
59
+ };
60
+ }
61
+ }) && $.f)({})).$name('vertexMain');
62
+
63
+ // The fragment applies the FrameCropParams crop + orientation transform and samples the camera via
64
+ // sampleCamera(). Mirroring is folded into a build-time sign/offset on uv.x (mirror → 1 - x) so the
65
+ // shader stays branch-free.
66
+ function makeFragmentMain(mirror) {
67
+ const mirrorSign = mirror ? -1 : 1;
68
+ const mirrorOffset = mirror ? 1 : 0;
69
+ return tgpu.fragmentFn({
70
+ in: {
71
+ screenUv: d.location(0, d.vec2f)
72
+ },
73
+ out: d.vec4f
74
+ })(/*#__PURE__*/($ => (globalThis.__TYPEGPU_META__ ??= new WeakMap()).set($.f = input => {
75
+ // cropUv = (mirrorSign * screenUv.x + mirrorOffset, screenUv.y), expressed with vector ops
76
+ // so mirrorSign/mirrorOffset fold to literals and the WGSL stays branch-free.
77
+ const cropUv = add(mul(input.screenUv, d.vec2f(mirrorSign, 1)), d.vec2f(mirrorOffset, 0));
78
+ const cp = cropLayout.$.cropParams;
79
+ const sourcePixel = add(cp.cropOrigin, mul(cropUv, cp.cropSize));
80
+ const sourceUv = div(sourcePixel, d.vec2f(cp.sourceSize));
81
+ const cameraUv = add(mul(cp.uvTransform, sub(sourceUv, d.vec2f(0.5))), d.vec2f(0.5));
82
+ return sampleCamera(cameraUv);
83
+ }, {
84
+ v: 1,
85
+ name: undefined,
86
+ ast: {
87
+ params: [{
88
+ type: "i",
89
+ name: "input"
90
+ }],
91
+ body: [0, [[13, "cropUv", [6, "add", [[6, "mul", [[7, "input", "screenUv"], [6, [7, "d", "vec2f"], ["mirrorSign", [5, "1"]]]]], [6, [7, "d", "vec2f"], ["mirrorOffset", [5, "0"]]]]]], [13, "cp", [7, [7, "cropLayout", "$"], "cropParams"]], [13, "sourcePixel", [6, "add", [[7, "cp", "cropOrigin"], [6, "mul", ["cropUv", [7, "cp", "cropSize"]]]]]], [13, "sourceUv", [6, "div", ["sourcePixel", [6, [7, "d", "vec2f"], [[7, "cp", "sourceSize"]]]]]], [13, "cameraUv", [6, "add", [[6, "mul", [[7, "cp", "uvTransform"], [6, "sub", ["sourceUv", [6, [7, "d", "vec2f"], [[5, "0.5"]]]]]]], [6, [7, "d", "vec2f"], [[5, "0.5"]]]]]], [10, [6, "sampleCamera", ["cameraUv"]]]]],
92
+ externalNames: ["add", "mul", "d", "mirrorSign", "mirrorOffset", "cropLayout", "div", "sub", "sampleCamera"]
93
+ },
94
+ externals: () => {
95
+ return {
96
+ add,
97
+ mul,
98
+ d,
99
+ mirrorSign,
100
+ mirrorOffset,
101
+ cropLayout,
102
+ div,
103
+ sub,
104
+ sampleCamera
105
+ };
106
+ }
107
+ }) && $.f)({})).$name('fragmentMain');
108
+ }
109
+
110
+ /** Options for {@link createCameraPassthroughPipeline}. */
111
+
112
+ /**
113
+ * A ready-made full-screen camera→target render pipeline. Build once at setup with
114
+ * {@link createCameraPassthroughPipeline}; every field is safe to capture into the frame worklet.
115
+ *
116
+ * @group WebGPU
117
+ */
118
+
119
+ /**
120
+ * Resolves the passthrough shader to WGSL: the raw camera-texture declarations (which TypeGPU
121
+ * cannot emit) followed by the TGSL-authored vertex + fragment functions, `sampleCamera`, and the
122
+ * `FrameCropParams` struct + crop uniform.
123
+ */
124
+ function buildPassthroughShaderCode(cameraShaderBindings, mirror) {
125
+ const resolved = tgpu.resolve({
126
+ externals: {
127
+ vertexMain,
128
+ fragmentMain: makeFragmentMain(mirror)
129
+ },
130
+ names: 'strict'
131
+ });
132
+ return `${cameraShaderBindings.bindingDeclarations}\n${resolved}`;
133
+ }
134
+
135
+ /**
136
+ * Builds the full-screen camera passthrough pipeline: crop/orientation via {@link FrameCrop},
137
+ * platform-correct camera sampling, one triangle. Use it to publish the camera through the WebGPU
138
+ * tier with zero WGSL of your own, or as the base pass under your overlay passes.
139
+ *
140
+ * @group WebGPU
141
+ */
142
+ export function createCameraPassthroughPipeline(device, options = {}) {
143
+ const outputFormat = options.outputFormat ?? getOutputSurfaceFormat();
144
+ const cameraShaderBindings = createCameraShaderBindings(device, {
145
+ bindGroupIndex: CAMERA_BIND_GROUP_INDEX
146
+ });
147
+ const cropBindGroupLayout = device.createBindGroupLayout({
148
+ label: 'fishjam-camera-passthrough-crop',
149
+ entries: [{
150
+ binding: 0,
151
+ visibility: GPUShaderStage.FRAGMENT,
152
+ buffer: {
153
+ type: 'uniform'
154
+ }
155
+ }]
156
+ });
157
+ const cropParamsBuffer = /*#__PURE__*/(globalThis.__TYPEGPU_AUTONAME__ ?? (a => a))(device.createBuffer({
158
+ label: 'fishjam-camera-passthrough-crop-params',
159
+ size: FRAME_CROP_BUFFER_BYTES,
160
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
161
+ }), "cropParamsBuffer");
162
+ const cropBindGroup = device.createBindGroup({
163
+ layout: cropBindGroupLayout,
164
+ entries: [{
165
+ binding: 0,
166
+ resource: {
167
+ buffer: cropParamsBuffer
168
+ }
169
+ }]
170
+ });
171
+ const shaderModule = device.createShaderModule({
172
+ label: 'fishjam-camera-passthrough',
173
+ code: buildPassthroughShaderCode(cameraShaderBindings, options.mirror ?? false)
174
+ });
175
+ const bindGroupLayouts = [];
176
+ bindGroupLayouts[CROP_BIND_GROUP_INDEX] = cropBindGroupLayout;
177
+ bindGroupLayouts[CAMERA_BIND_GROUP_INDEX] = cameraShaderBindings.bindGroupLayout;
178
+ const pipeline = /*#__PURE__*/(globalThis.__TYPEGPU_AUTONAME__ ?? (a => a))(device.createRenderPipeline({
179
+ label: 'fishjam-camera-passthrough',
180
+ layout: device.createPipelineLayout({
181
+ bindGroupLayouts
182
+ }),
183
+ vertex: {
184
+ module: shaderModule,
185
+ entryPoint: 'vertexMain'
186
+ },
187
+ fragment: {
188
+ module: shaderModule,
189
+ entryPoint: 'fragmentMain',
190
+ targets: [{
191
+ format: outputFormat
192
+ }]
193
+ },
194
+ primitive: {
195
+ topology: 'triangle-list'
196
+ }
197
+ }), "pipeline");
198
+ return {
199
+ pipeline,
200
+ cameraShaderBindings,
201
+ cropParamsBuffer,
202
+ cropBindGroup
203
+ };
204
+ }
205
+
206
+ /**
207
+ * Encodes one full-screen pass drawing the camera into `outputView`, cropped per `crop`.
208
+ * Worklet-safe; call it inside your render callback, and encode any overlay passes after it on
209
+ * the same command encoder (with `loadOp: 'load'` so they draw on top).
210
+ *
211
+ * At most one call per pipeline instance per frame: the crop lives in a single uniform buffer
212
+ * written via `queue.writeBuffer`, which lands before the submitted command buffer executes — a
213
+ * second call in the same frame makes both draws use the second crop. Encode to multiple targets
214
+ * with different crops by building one pipeline per target.
215
+ *
216
+ * @group WebGPU
217
+ */
218
+ export function encodeCameraPassthrough(device, passthrough, cameraTexture, outputView, commandEncoder, crop) {
219
+ 'worklet';
220
+
221
+ device.queue.writeBuffer(passthrough.cropParamsBuffer, 0, packFrameCropParams(crop));
222
+ const cameraBindGroup = createCameraBindGroup(device, passthrough.cameraShaderBindings, cameraTexture);
223
+ const pass = commandEncoder.beginRenderPass({
224
+ colorAttachments: [{
225
+ view: outputView,
226
+ loadOp: 'clear',
227
+ storeOp: 'store',
228
+ clearValue: [0, 0, 0, 1]
229
+ }]
230
+ });
231
+ pass.setPipeline(passthrough.pipeline);
232
+ pass.setBindGroup(CROP_BIND_GROUP_INDEX, passthrough.cropBindGroup);
233
+ pass.setBindGroup(CAMERA_BIND_GROUP_INDEX, cameraBindGroup);
234
+ pass.draw(3);
235
+ pass.end();
236
+ }
237
+ //# sourceMappingURL=cameraPassthroughPipeline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["GPUBufferUsage","GPUShaderStage","tgpu","d","add","div","mul","sub","createCameraBindGroup","createCameraShaderBindings","sampleCamera","FrameCropParams","packFrameCropParams","getOutputSurfaceFormat","CROP_BIND_GROUP_INDEX","CAMERA_BIND_GROUP_INDEX","FRAME_CROP_BUFFER_BYTES","sizeOf","cropLayout","globalThis","__TYPEGPU_AUTONAME__","a","bindGroupLayout","cropParams","uniform","vertexMain","vertexFn","in","vertexIndex","builtin","out","position","uv","location","vec2f","$","__TYPEGPU_META__","WeakMap","set","f","input","positions","vec4f","x","y","v","name","undefined","ast","params","type","body","externalNames","externals","$name","makeFragmentMain","mirror","mirrorSign","mirrorOffset","fragmentFn","screenUv","cropUv","cp","sourcePixel","cropOrigin","cropSize","sourceUv","sourceSize","cameraUv","uvTransform","buildPassthroughShaderCode","cameraShaderBindings","resolved","resolve","fragmentMain","names","bindingDeclarations","createCameraPassthroughPipeline","device","options","outputFormat","bindGroupIndex","cropBindGroupLayout","createBindGroupLayout","label","entries","binding","visibility","FRAGMENT","buffer","cropParamsBuffer","createBuffer","size","usage","UNIFORM","COPY_DST","cropBindGroup","createBindGroup","layout","resource","shaderModule","createShaderModule","code","bindGroupLayouts","pipeline","createRenderPipeline","createPipelineLayout","vertex","module","entryPoint","fragment","targets","format","primitive","topology","encodeCameraPassthrough","passthrough","cameraTexture","outputView","commandEncoder","crop","queue","writeBuffer","cameraBindGroup","pass","beginRenderPass","colorAttachments","view","loadOp","storeOp","clearValue","setPipeline","setBindGroup","draw","end"],"sources":["cameraPassthroughPipeline.ts"],"sourcesContent":["import { GPUBufferUsage, GPUShaderStage } from 'react-native-webgpu';\nimport tgpu from 'typegpu';\nimport * as d from 'typegpu/data';\nimport { add, div, mul, sub } from 'typegpu/std';\n\nimport {\n type CameraShaderBindings,\n createCameraBindGroup,\n createCameraShaderBindings,\n sampleCamera,\n} from './cameraShaderBindings';\nimport { type FrameCrop, FrameCropParams, packFrameCropParams } from './cropUtilities';\nimport { getOutputSurfaceFormat } from './requiredFeatures';\n\n// Crop is a plain uniform, so it is authored in TGSL and TypeGPU assigns it @group(0). The camera\n// is a `texture_external`, which TypeGPU cannot resolve in a shader, so its bindings are raw WGSL\n// and go at @group(1) (see cameraShaderBindings). This is a group swap from the pre-TGSL shader\n// (which had camera at 0, crop at 1) but is otherwise identical.\nconst CROP_BIND_GROUP_INDEX = 0;\nconst CAMERA_BIND_GROUP_INDEX = 1;\n\nconst FRAME_CROP_BUFFER_BYTES = d.sizeOf(FrameCropParams);\n\n// TypeGPU bind group layout for the crop uniform — the source of truth for the WGSL declaration\n// and for the fragment's typed access to `cropParams`.\nconst cropLayout = tgpu.bindGroupLayout({ cropParams: { uniform: FrameCropParams } });\n\n// One oversized triangle covering the viewport; uv spans [0,1] top-left origin over the visible\n// area.\nconst vertexMain = tgpu\n .vertexFn({\n in: { vertexIndex: d.builtin.vertexIndex },\n out: { position: d.builtin.position, uv: d.location(0, d.vec2f) },\n })((input) => {\n const positions = [d.vec2f(-1, -1), d.vec2f(3, -1), d.vec2f(-1, 3)];\n const position = positions[input.vertexIndex];\n return {\n position: d.vec4f(position.x, position.y, 0, 1),\n uv: d.vec2f((position.x + 1) * 0.5, 1 - (position.y + 1) * 0.5),\n };\n })\n .$name('vertexMain');\n\n// The fragment applies the FrameCropParams crop + orientation transform and samples the camera via\n// sampleCamera(). Mirroring is folded into a build-time sign/offset on uv.x (mirror → 1 - x) so the\n// shader stays branch-free.\nfunction makeFragmentMain(mirror: boolean) {\n const mirrorSign = mirror ? -1 : 1;\n const mirrorOffset = mirror ? 1 : 0;\n return tgpu\n .fragmentFn({ in: { screenUv: d.location(0, d.vec2f) }, out: d.vec4f })((input) => {\n // cropUv = (mirrorSign * screenUv.x + mirrorOffset, screenUv.y), expressed with vector ops\n // so mirrorSign/mirrorOffset fold to literals and the WGSL stays branch-free.\n const cropUv = add(mul(input.screenUv, d.vec2f(mirrorSign, 1)), d.vec2f(mirrorOffset, 0));\n const cp = cropLayout.$.cropParams;\n const sourcePixel = add(cp.cropOrigin, mul(cropUv, cp.cropSize));\n const sourceUv = div(sourcePixel, d.vec2f(cp.sourceSize));\n const cameraUv = add(mul(cp.uvTransform, sub(sourceUv, d.vec2f(0.5))), d.vec2f(0.5));\n return sampleCamera(cameraUv);\n })\n .$name('fragmentMain');\n}\n\n/** Options for {@link createCameraPassthroughPipeline}. */\nexport interface CameraPassthroughPipelineOptions {\n /** Render-target format. Defaults to {@link getOutputSurfaceFormat} (the Fishjam output surface). */\n outputFormat?: GPUTextureFormat;\n /** Mirror the camera horizontally (the usual selfie self-view convention). Defaults to `false`. */\n mirror?: boolean;\n}\n\n/**\n * A ready-made full-screen camera→target render pipeline. Build once at setup with\n * {@link createCameraPassthroughPipeline}; every field is safe to capture into the frame worklet.\n *\n * @group WebGPU\n */\nexport interface CameraPassthroughPipeline {\n readonly pipeline: GPURenderPipeline;\n /** The camera shader bindings the pipeline samples through. */\n readonly cameraShaderBindings: CameraShaderBindings;\n /** Uniform buffer holding the packed FrameCropParams; written by {@link encodeCameraPassthrough}. */\n readonly cropParamsBuffer: GPUBuffer;\n /** Static bind group with the crop uniform. */\n readonly cropBindGroup: GPUBindGroup;\n}\n\n/**\n * Resolves the passthrough shader to WGSL: the raw camera-texture declarations (which TypeGPU\n * cannot emit) followed by the TGSL-authored vertex + fragment functions, `sampleCamera`, and the\n * `FrameCropParams` struct + crop uniform.\n */\nfunction buildPassthroughShaderCode(cameraShaderBindings: CameraShaderBindings, mirror: boolean): string {\n const resolved = tgpu.resolve({\n externals: { vertexMain, fragmentMain: makeFragmentMain(mirror) },\n names: 'strict',\n });\n return `${cameraShaderBindings.bindingDeclarations}\\n${resolved}`;\n}\n\n/**\n * Builds the full-screen camera passthrough pipeline: crop/orientation via {@link FrameCrop},\n * platform-correct camera sampling, one triangle. Use it to publish the camera through the WebGPU\n * tier with zero WGSL of your own, or as the base pass under your overlay passes.\n *\n * @group WebGPU\n */\nexport function createCameraPassthroughPipeline(\n device: GPUDevice,\n options: CameraPassthroughPipelineOptions = {},\n): CameraPassthroughPipeline {\n const outputFormat = options.outputFormat ?? getOutputSurfaceFormat();\n const cameraShaderBindings = createCameraShaderBindings(device, { bindGroupIndex: CAMERA_BIND_GROUP_INDEX });\n\n const cropBindGroupLayout = device.createBindGroupLayout({\n label: 'fishjam-camera-passthrough-crop',\n entries: [{ binding: 0, visibility: GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } }],\n });\n const cropParamsBuffer = device.createBuffer({\n label: 'fishjam-camera-passthrough-crop-params',\n size: FRAME_CROP_BUFFER_BYTES,\n usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,\n });\n const cropBindGroup = device.createBindGroup({\n layout: cropBindGroupLayout,\n entries: [{ binding: 0, resource: { buffer: cropParamsBuffer } }],\n });\n\n const shaderModule = device.createShaderModule({\n label: 'fishjam-camera-passthrough',\n code: buildPassthroughShaderCode(cameraShaderBindings, options.mirror ?? false),\n });\n const bindGroupLayouts: GPUBindGroupLayout[] = [];\n bindGroupLayouts[CROP_BIND_GROUP_INDEX] = cropBindGroupLayout;\n bindGroupLayouts[CAMERA_BIND_GROUP_INDEX] = cameraShaderBindings.bindGroupLayout;\n const pipeline = device.createRenderPipeline({\n label: 'fishjam-camera-passthrough',\n layout: device.createPipelineLayout({ bindGroupLayouts }),\n vertex: { module: shaderModule, entryPoint: 'vertexMain' },\n fragment: {\n module: shaderModule,\n entryPoint: 'fragmentMain',\n targets: [{ format: outputFormat }],\n },\n primitive: { topology: 'triangle-list' },\n });\n\n return { pipeline, cameraShaderBindings, cropParamsBuffer, cropBindGroup };\n}\n\n/**\n * Encodes one full-screen pass drawing the camera into `outputView`, cropped per `crop`.\n * Worklet-safe; call it inside your render callback, and encode any overlay passes after it on\n * the same command encoder (with `loadOp: 'load'` so they draw on top).\n *\n * At most one call per pipeline instance per frame: the crop lives in a single uniform buffer\n * written via `queue.writeBuffer`, which lands before the submitted command buffer executes — a\n * second call in the same frame makes both draws use the second crop. Encode to multiple targets\n * with different crops by building one pipeline per target.\n *\n * @group WebGPU\n */\nexport function encodeCameraPassthrough(\n device: GPUDevice,\n passthrough: CameraPassthroughPipeline,\n cameraTexture: GPUExternalTexture,\n outputView: GPUTextureView,\n commandEncoder: GPUCommandEncoder,\n crop: FrameCrop,\n): void {\n 'worklet';\n device.queue.writeBuffer(passthrough.cropParamsBuffer, 0, packFrameCropParams(crop));\n const cameraBindGroup = createCameraBindGroup(device, passthrough.cameraShaderBindings, cameraTexture);\n const pass = commandEncoder.beginRenderPass({\n colorAttachments: [{ view: outputView, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 1] }],\n });\n pass.setPipeline(passthrough.pipeline);\n pass.setBindGroup(CROP_BIND_GROUP_INDEX, passthrough.cropBindGroup);\n pass.setBindGroup(CAMERA_BIND_GROUP_INDEX, cameraBindGroup);\n pass.draw(3);\n pass.end();\n}\n"],"mappings":"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","ignoreList":[]}