react-native-webrtc-kaleidoscope 1.0.0 → 2.0.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.
- package/NOTICE.md +17 -6
- package/README.md +67 -19
- package/android/build.gradle +6 -5
- package/android/src/main/assets/backgrounds/dark-office.webp +0 -0
- package/android/src/main/assets/backgrounds/debug-resolutions.webp +0 -0
- package/android/src/main/assets/backgrounds/home-dark.webp +0 -0
- package/android/src/main/assets/backgrounds/home-light.webp +0 -0
- package/android/src/main/assets/backgrounds/light-office.webp +0 -0
- package/android/src/main/assets/backgrounds/nature-dark.webp +0 -0
- package/android/src/main/assets/backgrounds/nature-light.webp +0 -0
- package/android/src/main/assets/backgrounds/simiancraft-dark.webp +0 -0
- package/android/src/main/assets/backgrounds/simiancraft-light.webp +0 -0
- package/android/src/main/assets/backgrounds/stylized-dark.webp +0 -0
- package/android/src/main/assets/backgrounds/stylized-light.webp +0 -0
- package/android/src/main/assets/selfie_segmenter.tflite +0 -0
- package/android/src/main/java/com/simiancraft/kaleidoscope/EffectTuning.kt +41 -10
- package/android/src/main/java/com/simiancraft/kaleidoscope/KaleidoscopeModule.kt +8 -0
- package/android/src/main/java/com/simiancraft/kaleidoscope/Registration.kt +52 -13
- package/android/src/main/java/com/simiancraft/kaleidoscope/effects/BackgroundImageFactory.kt +53 -23
- package/android/src/main/java/com/simiancraft/kaleidoscope/effects/BlurFactory.kt +114 -50
- package/android/src/main/java/com/simiancraft/kaleidoscope/effects/TransformFactory.kt +277 -0
- package/android/src/main/java/com/simiancraft/kaleidoscope/gpu/FramePipeline.kt +158 -0
- package/android/src/main/java/com/simiancraft/kaleidoscope/gpu/GlProgram.kt +5 -0
- package/android/src/main/java/com/simiancraft/kaleidoscope/gpu/Ingest.kt +123 -0
- package/android/src/main/java/com/simiancraft/kaleidoscope/gpu/Orientation.kt +58 -0
- package/android/src/main/java/com/simiancraft/kaleidoscope/gpu/Shaders.kt +23 -51
- package/android/src/main/java/com/simiancraft/kaleidoscope/gpu/ShadersGenerated.kt +76 -0
- package/android/src/main/java/com/simiancraft/kaleidoscope/segmentation/Mask.kt +100 -89
- package/android/src/main/java/com/simiancraft/kaleidoscope/segmentation/SegmentationEngine.kt +148 -0
- package/app.plugin.js +254 -4
- package/dist/backgrounds/dark-office.d.ts +3 -0
- package/dist/backgrounds/dark-office.d.ts.map +1 -0
- package/dist/backgrounds/dark-office.js +5 -0
- package/dist/backgrounds/dark-office.js.map +1 -0
- package/dist/backgrounds/dark-office.web.d.ts +3 -0
- package/dist/backgrounds/dark-office.web.d.ts.map +1 -0
- package/dist/backgrounds/dark-office.web.js +8 -0
- package/dist/backgrounds/dark-office.web.js.map +1 -0
- package/dist/backgrounds/dark-office.webp +0 -0
- package/dist/backgrounds/debug-resolutions.d.ts +3 -0
- package/dist/backgrounds/debug-resolutions.d.ts.map +1 -0
- package/dist/backgrounds/debug-resolutions.js +6 -0
- package/dist/backgrounds/debug-resolutions.js.map +1 -0
- package/dist/backgrounds/debug-resolutions.web.d.ts +3 -0
- package/dist/backgrounds/debug-resolutions.web.d.ts.map +1 -0
- package/dist/backgrounds/debug-resolutions.web.js +8 -0
- package/dist/backgrounds/debug-resolutions.web.js.map +1 -0
- package/dist/backgrounds/debug-resolutions.webp +0 -0
- package/dist/backgrounds/home-dark.d.ts +3 -0
- package/dist/backgrounds/home-dark.d.ts.map +1 -0
- package/dist/backgrounds/home-dark.js +5 -0
- package/dist/backgrounds/home-dark.js.map +1 -0
- package/dist/backgrounds/home-dark.web.d.ts +3 -0
- package/dist/backgrounds/home-dark.web.d.ts.map +1 -0
- package/dist/backgrounds/home-dark.web.js +6 -0
- package/dist/backgrounds/home-dark.web.js.map +1 -0
- package/dist/backgrounds/home-dark.webp +0 -0
- package/dist/backgrounds/home-light.d.ts +3 -0
- package/dist/backgrounds/home-light.d.ts.map +1 -0
- package/dist/backgrounds/home-light.js +5 -0
- package/dist/backgrounds/home-light.js.map +1 -0
- package/dist/backgrounds/home-light.web.d.ts +3 -0
- package/dist/backgrounds/home-light.web.d.ts.map +1 -0
- package/dist/backgrounds/home-light.web.js +6 -0
- package/dist/backgrounds/home-light.web.js.map +1 -0
- package/dist/backgrounds/home-light.webp +0 -0
- package/dist/backgrounds/index.d.ts +3 -0
- package/dist/backgrounds/index.d.ts.map +1 -0
- package/dist/backgrounds/index.js +6 -0
- package/dist/backgrounds/index.js.map +1 -0
- package/dist/backgrounds/light-office.d.ts +3 -0
- package/dist/backgrounds/light-office.d.ts.map +1 -0
- package/dist/backgrounds/light-office.js +5 -0
- package/dist/backgrounds/light-office.js.map +1 -0
- package/dist/backgrounds/light-office.web.d.ts +3 -0
- package/dist/backgrounds/light-office.web.d.ts.map +1 -0
- package/dist/backgrounds/light-office.web.js +8 -0
- package/dist/backgrounds/light-office.web.js.map +1 -0
- package/dist/backgrounds/light-office.webp +0 -0
- package/dist/backgrounds/nature-dark.d.ts +3 -0
- package/dist/backgrounds/nature-dark.d.ts.map +1 -0
- package/dist/backgrounds/nature-dark.js +5 -0
- package/dist/backgrounds/nature-dark.js.map +1 -0
- package/dist/backgrounds/nature-dark.web.d.ts +3 -0
- package/dist/backgrounds/nature-dark.web.d.ts.map +1 -0
- package/dist/backgrounds/nature-dark.web.js +6 -0
- package/dist/backgrounds/nature-dark.web.js.map +1 -0
- package/dist/backgrounds/nature-dark.webp +0 -0
- package/dist/backgrounds/nature-light.d.ts +3 -0
- package/dist/backgrounds/nature-light.d.ts.map +1 -0
- package/dist/backgrounds/nature-light.js +5 -0
- package/dist/backgrounds/nature-light.js.map +1 -0
- package/dist/backgrounds/nature-light.web.d.ts +3 -0
- package/dist/backgrounds/nature-light.web.d.ts.map +1 -0
- package/dist/backgrounds/nature-light.web.js +6 -0
- package/dist/backgrounds/nature-light.web.js.map +1 -0
- package/dist/backgrounds/nature-light.webp +0 -0
- package/dist/backgrounds/preset-source.types.d.ts +2 -0
- package/dist/backgrounds/preset-source.types.d.ts.map +1 -0
- package/dist/backgrounds/preset-source.types.js +2 -0
- package/dist/backgrounds/preset-source.types.js.map +1 -0
- package/dist/backgrounds/presets.d.ts +3 -0
- package/dist/backgrounds/presets.d.ts.map +1 -0
- package/dist/backgrounds/presets.js +34 -0
- package/dist/backgrounds/presets.js.map +1 -0
- package/dist/backgrounds/simiancraft-dark.d.ts +3 -0
- package/dist/backgrounds/simiancraft-dark.d.ts.map +1 -0
- package/dist/backgrounds/simiancraft-dark.js +5 -0
- package/dist/backgrounds/simiancraft-dark.js.map +1 -0
- package/dist/backgrounds/simiancraft-dark.web.d.ts +3 -0
- package/dist/backgrounds/simiancraft-dark.web.d.ts.map +1 -0
- package/dist/backgrounds/simiancraft-dark.web.js +6 -0
- package/dist/backgrounds/simiancraft-dark.web.js.map +1 -0
- package/dist/backgrounds/simiancraft-dark.webp +0 -0
- package/dist/backgrounds/simiancraft-light.d.ts +3 -0
- package/dist/backgrounds/simiancraft-light.d.ts.map +1 -0
- package/dist/backgrounds/simiancraft-light.js +5 -0
- package/dist/backgrounds/simiancraft-light.js.map +1 -0
- package/dist/backgrounds/simiancraft-light.web.d.ts +3 -0
- package/dist/backgrounds/simiancraft-light.web.d.ts.map +1 -0
- package/dist/backgrounds/simiancraft-light.web.js +6 -0
- package/dist/backgrounds/simiancraft-light.web.js.map +1 -0
- package/dist/backgrounds/simiancraft-light.webp +0 -0
- package/dist/backgrounds/stylized-dark.d.ts +3 -0
- package/dist/backgrounds/stylized-dark.d.ts.map +1 -0
- package/dist/backgrounds/stylized-dark.js +5 -0
- package/dist/backgrounds/stylized-dark.js.map +1 -0
- package/dist/backgrounds/stylized-dark.web.d.ts +3 -0
- package/dist/backgrounds/stylized-dark.web.d.ts.map +1 -0
- package/dist/backgrounds/stylized-dark.web.js +6 -0
- package/dist/backgrounds/stylized-dark.web.js.map +1 -0
- package/dist/backgrounds/stylized-dark.webp +0 -0
- package/dist/backgrounds/stylized-light.d.ts +3 -0
- package/dist/backgrounds/stylized-light.d.ts.map +1 -0
- package/dist/backgrounds/stylized-light.js +5 -0
- package/dist/backgrounds/stylized-light.js.map +1 -0
- package/dist/backgrounds/stylized-light.web.d.ts +3 -0
- package/dist/backgrounds/stylized-light.web.d.ts.map +1 -0
- package/dist/backgrounds/stylized-light.web.js +6 -0
- package/dist/backgrounds/stylized-light.web.js.map +1 -0
- package/dist/backgrounds/stylized-light.webp +0 -0
- package/dist/index.d.ts +19 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +71 -22
- package/dist/index.js.map +1 -1
- package/dist/index.web.d.ts +25 -3
- package/dist/index.web.d.ts.map +1 -1
- package/dist/index.web.js +55 -16
- package/dist/index.web.js.map +1 -1
- package/dist/livekit.d.ts +24 -0
- package/dist/livekit.d.ts.map +1 -0
- package/dist/livekit.js +57 -0
- package/dist/livekit.js.map +1 -0
- package/dist/types.d.ts +20 -13
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +0 -3
- package/dist/types.js.map +1 -1
- package/dist/web/blur-kernel.d.ts +6 -0
- package/dist/web/blur-kernel.d.ts.map +1 -0
- package/dist/web/blur-kernel.js +41 -0
- package/dist/web/blur-kernel.js.map +1 -0
- package/dist/web/effects/background-image.d.ts.map +1 -1
- package/dist/web/effects/background-image.js +100 -33
- package/dist/web/effects/background-image.js.map +1 -1
- package/dist/web/effects/blur.d.ts.map +1 -1
- package/dist/web/effects/blur.js +169 -38
- package/dist/web/effects/blur.js.map +1 -1
- package/dist/web/effects/transform.d.ts +4 -0
- package/dist/web/effects/transform.d.ts.map +1 -0
- package/dist/web/effects/transform.js +62 -0
- package/dist/web/effects/transform.js.map +1 -0
- package/dist/web/insertable-streams.d.ts +11 -1
- package/dist/web/insertable-streams.d.ts.map +1 -1
- package/dist/web/insertable-streams.js +22 -4
- package/dist/web/insertable-streams.js.map +1 -1
- package/dist/web/segmenter.d.ts +9 -0
- package/dist/web/segmenter.d.ts.map +1 -1
- package/dist/web/segmenter.js +32 -0
- package/dist/web/segmenter.js.map +1 -1
- package/dist/web/shaders.d.ts +1 -3
- package/dist/web/shaders.d.ts.map +1 -1
- package/dist/web/shaders.generated.d.ts +4 -0
- package/dist/web/shaders.generated.d.ts.map +1 -0
- package/dist/web/shaders.generated.js +54 -0
- package/dist/web/shaders.generated.js.map +1 -0
- package/dist/web/shaders.js +29 -103
- package/dist/web/shaders.js.map +1 -1
- package/dist/web/tuning.d.ts +4 -0
- package/dist/web/tuning.d.ts.map +1 -1
- package/dist/web/tuning.js +17 -5
- package/dist/web/tuning.js.map +1 -1
- package/ios/Kaleidoscope.podspec +87 -14
- package/ios/KaleidoscopeModule/EffectTuning.swift +47 -7
- package/ios/KaleidoscopeModule/KaleidoscopeModule.swift +12 -0
- package/ios/KaleidoscopeModule/Registration.swift +78 -13
- package/ios/KaleidoscopeModule/effects/BackgroundImageProcessor.swift +293 -0
- package/ios/KaleidoscopeModule/effects/BlurProcessor.swift +232 -18
- package/ios/KaleidoscopeModule/effects/FrameBridge.swift +46 -0
- package/ios/KaleidoscopeModule/effects/TransformProcessor.swift +173 -0
- package/ios/KaleidoscopeModule/gpu/Ingest.swift +169 -0
- package/ios/KaleidoscopeModule/gpu/MetalRenderer.swift +601 -0
- package/ios/KaleidoscopeModule/gpu/Orientation.swift +83 -0
- package/ios/KaleidoscopeModule/gpu/ShaderLibrary.swift +100 -0
- package/ios/KaleidoscopeModule/gpu/TextureBridge.swift +291 -0
- package/ios/KaleidoscopeModule/resources/backgrounds/dark-office.webp +0 -0
- package/ios/KaleidoscopeModule/resources/backgrounds/debug-resolutions.webp +0 -0
- package/ios/KaleidoscopeModule/resources/backgrounds/home-dark.webp +0 -0
- package/ios/KaleidoscopeModule/resources/backgrounds/home-light.webp +0 -0
- package/ios/KaleidoscopeModule/resources/backgrounds/light-office.webp +0 -0
- package/ios/KaleidoscopeModule/resources/backgrounds/nature-dark.webp +0 -0
- package/ios/KaleidoscopeModule/resources/backgrounds/nature-light.webp +0 -0
- package/ios/KaleidoscopeModule/resources/backgrounds/simiancraft-dark.webp +0 -0
- package/ios/KaleidoscopeModule/resources/backgrounds/simiancraft-light.webp +0 -0
- package/ios/KaleidoscopeModule/resources/backgrounds/stylized-dark.webp +0 -0
- package/ios/KaleidoscopeModule/resources/backgrounds/stylized-light.webp +0 -0
- package/ios/KaleidoscopeModule/resources/selfie_segmenter.tflite +0 -0
- package/ios/KaleidoscopeModule/segmentation/MaskTuning.swift +23 -0
- package/ios/KaleidoscopeModule/segmentation/Segmenter.swift +444 -25
- package/ios/KaleidoscopeModule/shaders/SHADERS.txt +6 -0
- package/ios/KaleidoscopeModule/shaders/blur.metalsrc +72 -0
- package/ios/KaleidoscopeModule/shaders/composite.metalsrc +22 -0
- package/ios/KaleidoscopeModule/shaders/nebula.metalsrc +72 -0
- package/ios/KaleidoscopeModule/shaders/passthrough.metalsrc +20 -0
- package/ios/KaleidoscopeModule/shaders/simianlights.metalsrc +72 -0
- package/ios/KaleidoscopeModule/shaders/transform.metalsrc +22 -0
- package/package.json +120 -11
- package/src/backgrounds/README.md +83 -0
- package/src/backgrounds/assets.d.ts +7 -0
- package/src/backgrounds/dark-office.ts +6 -0
- package/src/backgrounds/dark-office.web.ts +9 -0
- package/src/backgrounds/dark-office.webp +0 -0
- package/src/backgrounds/debug-resolutions.ts +7 -0
- package/src/backgrounds/debug-resolutions.web.ts +9 -0
- package/src/backgrounds/debug-resolutions.webp +0 -0
- package/src/backgrounds/home-dark.ts +6 -0
- package/src/backgrounds/home-dark.web.ts +7 -0
- package/src/backgrounds/home-dark.webp +0 -0
- package/src/backgrounds/home-light.ts +6 -0
- package/src/backgrounds/home-light.web.ts +7 -0
- package/src/backgrounds/home-light.webp +0 -0
- package/src/backgrounds/index.ts +7 -0
- package/src/backgrounds/light-office.ts +6 -0
- package/src/backgrounds/light-office.web.ts +9 -0
- package/src/backgrounds/light-office.webp +0 -0
- package/src/backgrounds/nature-dark.ts +6 -0
- package/src/backgrounds/nature-dark.web.ts +7 -0
- package/src/backgrounds/nature-dark.webp +0 -0
- package/src/backgrounds/nature-light.ts +6 -0
- package/src/backgrounds/nature-light.web.ts +7 -0
- package/src/backgrounds/nature-light.webp +0 -0
- package/src/backgrounds/preset-source.types.ts +5 -0
- package/src/backgrounds/presets.ts +36 -0
- package/src/backgrounds/simiancraft-dark.ts +6 -0
- package/src/backgrounds/simiancraft-dark.web.ts +7 -0
- package/src/backgrounds/simiancraft-dark.webp +0 -0
- package/src/backgrounds/simiancraft-light.ts +6 -0
- package/src/backgrounds/simiancraft-light.web.ts +7 -0
- package/src/backgrounds/simiancraft-light.webp +0 -0
- package/src/backgrounds/stylized-dark.ts +6 -0
- package/src/backgrounds/stylized-dark.web.ts +7 -0
- package/src/backgrounds/stylized-dark.webp +0 -0
- package/src/backgrounds/stylized-light.ts +6 -0
- package/src/backgrounds/stylized-light.web.ts +7 -0
- package/src/backgrounds/stylized-light.webp +0 -0
- package/src/index.ts +94 -29
- package/src/index.web.ts +69 -19
- package/src/livekit.ts +65 -0
- package/src/types.ts +23 -14
- package/src/web/blur-kernel.ts +44 -0
- package/src/web/effects/background-image.ts +121 -34
- package/src/web/effects/blur.ts +214 -40
- package/src/web/effects/transform.ts +69 -0
- package/src/web/insertable-streams.ts +33 -5
- package/src/web/segmenter.ts +34 -0
- package/src/web/shaders.generated.ts +56 -0
- package/src/web/shaders.ts +29 -106
- package/src/web/tuning.ts +19 -5
- package/android/src/main/assets/backgrounds/office-1.png +0 -0
- package/android/src/main/assets/backgrounds/office-2.png +0 -0
- package/android/src/main/java/com/simiancraft/kaleidoscope/effects/MirrorFactory.kt +0 -57
- package/android/src/main/java/com/simiancraft/kaleidoscope/gpu/GpuEffectFactory.kt +0 -14
- package/android/src/main/java/com/simiancraft/kaleidoscope/gpu/GpuEffectProcessor.kt +0 -198
- package/dist/backgrounds.d.ts +0 -3
- package/dist/backgrounds.d.ts.map +0 -1
- package/dist/backgrounds.js +0 -21
- package/dist/backgrounds.js.map +0 -1
- package/dist/web/effects/mirror.d.ts +0 -3
- package/dist/web/effects/mirror.d.ts.map +0 -1
- package/dist/web/effects/mirror.js +0 -31
- package/dist/web/effects/mirror.js.map +0 -1
- package/dist/web/effects/passthrough.d.ts +0 -3
- package/dist/web/effects/passthrough.d.ts.map +0 -1
- package/dist/web/effects/passthrough.js +0 -15
- package/dist/web/effects/passthrough.js.map +0 -1
- package/ios/KaleidoscopeModule/effects/MirrorProcessor.swift +0 -17
- package/plugin/build/withKaleidoscope.d.ts +0 -3
- package/plugin/build/withKaleidoscope.js +0 -14
- package/plugin/build/withKaleidoscope.js.map +0 -1
- package/src/backgrounds.ts +0 -23
- package/src/web/effects/mirror.ts +0 -37
- package/src/web/effects/passthrough.ts +0 -17
package/dist/index.web.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { type ApplyVideoEffects } from './types';
|
|
1
|
+
import { type ApplyVideoEffects, type EffectInput } from './types';
|
|
2
|
+
import { type DisposablePipeline } from './web/insertable-streams';
|
|
2
3
|
/**
|
|
3
4
|
* Set the Gaussian sigma for the blur effect. Higher = softer blur.
|
|
4
|
-
* Clamped to [0.5,
|
|
5
|
+
* Clamped to [0.5, 7] (the useful range before the linear-sampled kernel
|
|
6
|
+
* truncates and bands). Default 5.
|
|
5
7
|
*/
|
|
6
8
|
export declare const setBlurSigma: (value: number) => void;
|
|
7
9
|
/**
|
|
@@ -17,11 +19,31 @@ export declare const setMaskHardness: (value: number) => void;
|
|
|
17
19
|
* distribution.
|
|
18
20
|
*/
|
|
19
21
|
export declare const setMaskThreshold: (value: number) => void;
|
|
22
|
+
/**
|
|
23
|
+
* Set the segmentation input short-side (px). Native-only knob (lower =
|
|
24
|
+
* cheaper segmentation on older devices); stored on web for API parity but
|
|
25
|
+
* the web MediaPipe pipeline does not consume it.
|
|
26
|
+
*/
|
|
27
|
+
export declare const setSegmentationTargetShortSide: (value: number) => void;
|
|
28
|
+
/**
|
|
29
|
+
* Toggle native per-frame timing logs (off by default). No-op effect on web
|
|
30
|
+
* beyond storing the flag.
|
|
31
|
+
*/
|
|
32
|
+
export declare const setDebugTiming: (value: boolean) => void;
|
|
20
33
|
/**
|
|
21
34
|
* Reset all effect tuning parameters to library defaults.
|
|
22
35
|
*/
|
|
23
36
|
export declare const resetEffectTuning: () => void;
|
|
24
37
|
export type { BackgroundPresetName } from './backgrounds';
|
|
25
|
-
export type { ApplyVideoEffects, BackgroundImageSpec, BlurSpec, EffectInput, EffectName, EffectSpec,
|
|
38
|
+
export type { ApplyVideoEffects, BackgroundImageSpec, BlurSpec, EffectInput, EffectName, EffectSpec, TransformName, TransformSpec, } from './types';
|
|
39
|
+
/**
|
|
40
|
+
* Like `applyVideoEffects`, but also returns a `dispose()` that tears down every
|
|
41
|
+
* Insertable-Streams stage (stops each generator and aborts each pipe). The
|
|
42
|
+
* LiveKit adapter (`react-native-webrtc-kaleidoscope/livekit`) uses this so a
|
|
43
|
+
* camera flip (restart) or unpublish (destroy) does not leak generators. The
|
|
44
|
+
* page-shared segmenter and WebGL state are module singletons reused across
|
|
45
|
+
* stages, so they are intentionally NOT torn down here.
|
|
46
|
+
*/
|
|
47
|
+
export declare const applyVideoEffectsDisposable: (track: MediaStreamTrack, effects: ReadonlyArray<EffectInput>) => DisposablePipeline;
|
|
26
48
|
export declare const applyVideoEffects: ApplyVideoEffects;
|
|
27
49
|
//# sourceMappingURL=index.web.d.ts.map
|
package/dist/index.web.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.web.d.ts","sourceRoot":"","sources":["../src/index.web.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.web.d.ts","sourceRoot":"","sources":["../src/index.web.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,KAAK,iBAAiB,EAAE,KAAK,WAAW,EAAiC,MAAM,SAAS,CAAC;AAIlG,OAAO,EAEL,KAAK,kBAAkB,EAExB,MAAM,0BAA0B,CAAC;AAGlC;;;;GAIG;AACH,eAAO,MAAM,YAAY,UAAW,MAAM,KAAG,IAE5C,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,eAAe,UAAW,MAAM,KAAG,IAE/C,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,UAAW,MAAM,KAAG,IAEhD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,8BAA8B,UAAW,MAAM,KAAG,IAE9D,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,cAAc,UAAW,OAAO,KAAG,IAE/C,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,iBAAiB,QAAO,IAEpC,CAAC;AAEF,YAAY,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAC1D,YAAY,EACV,iBAAiB,EACjB,mBAAmB,EACnB,QAAQ,EACR,WAAW,EACX,UAAU,EACV,UAAU,EACV,aAAa,EACb,aAAa,GACd,MAAM,SAAS,CAAC;AAkBjB;;;;;;;GAOG;AACH,eAAO,MAAM,2BAA2B,UAC/B,gBAAgB,WACd,aAAa,CAAC,WAAW,CAAC,KAClC,kBA0BF,CAAC;AAEF,eAAO,MAAM,iBAAiB,EAAE,iBACmB,CAAC","sourcesContent":["// Web entry point (source). This builds to `dist/index.web.js`, which web\n// bundlers resolve through the package's `browser` export condition. With an\n// `exports` map present, that condition supersedes platform-extension\n// resolution; the `.web.ts` suffix here is just our source convention, not what\n// selects this file at consume time.\n//\n// applyVideoEffects(track, effects) wires an Insertable-Streams pipeline that\n// chains each effect's transform and returns a new MediaStreamTrack carrying\n// the transformed frames. Pass the returned track to a `<video>` element or\n// to `RTCRtpSender.replaceTrack(...)`.\n\nimport { type ApplyVideoEffects, type EffectInput, type EffectSpec, toEffectSpec } from './types';\nimport { makeBackgroundImage } from './web/effects/background-image';\nimport { blur } from './web/effects/blur';\nimport { makeTransform } from './web/effects/transform';\nimport {\n applyEffectToTrack,\n type DisposablePipeline,\n type FrameTransform,\n} from './web/insertable-streams';\nimport { tuning } from './web/tuning';\n\n/**\n * Set the Gaussian sigma for the blur effect. Higher = softer blur.\n * Clamped to [0.5, 7] (the useful range before the linear-sampled kernel\n * truncates and bands). Default 5.\n */\nexport const setBlurSigma = (value: number): void => {\n tuning.setBlurSigma(value);\n};\n\n/**\n * Set the mask smoothstep hardness for blur and background-image\n * composites, in [0, 1]. 0 = soft halo, 1 = near-step edge. Default 0.5.\n */\nexport const setMaskHardness = (value: number): void => {\n tuning.setMaskHardness(value);\n};\n\n/**\n * Set the mask smoothstep threshold (center of the transition) in\n * [0.05, 0.95]. 0.5 is neutral. Higher values reject low-confidence\n * pixels; lower values are more inclusive. Optimal value is platform-\n * specific because each segmentation model has a different confidence\n * distribution.\n */\nexport const setMaskThreshold = (value: number): void => {\n tuning.setMaskThreshold(value);\n};\n\n/**\n * Set the segmentation input short-side (px). Native-only knob (lower =\n * cheaper segmentation on older devices); stored on web for API parity but\n * the web MediaPipe pipeline does not consume it.\n */\nexport const setSegmentationTargetShortSide = (value: number): void => {\n tuning.setSegmentationTargetShortSide(value);\n};\n\n/**\n * Toggle native per-frame timing logs (off by default). No-op effect on web\n * beyond storing the flag.\n */\nexport const setDebugTiming = (value: boolean): void => {\n tuning.setDebugTiming(value);\n};\n\n/**\n * Reset all effect tuning parameters to library defaults.\n */\nexport const resetEffectTuning = (): void => {\n tuning.reset();\n};\n\nexport type { BackgroundPresetName } from './backgrounds';\nexport type {\n ApplyVideoEffects,\n BackgroundImageSpec,\n BlurSpec,\n EffectInput,\n EffectName,\n EffectSpec,\n TransformName,\n TransformSpec,\n} from './types';\n\nconst specToTransform = (spec: EffectSpec): FrameTransform => {\n switch (spec.name) {\n case 'flip-x':\n case 'flip-y':\n case 'rotate-cw':\n case 'rotate-ccw':\n return makeTransform(spec.name);\n case 'blur':\n // Blur strength comes from the global tuning state (setBlurSigma), not\n // the spec; the spec carries no parameters.\n return blur;\n case 'background-image':\n return makeBackgroundImage(spec.source);\n }\n};\n\n/**\n * Like `applyVideoEffects`, but also returns a `dispose()` that tears down every\n * Insertable-Streams stage (stops each generator and aborts each pipe). The\n * LiveKit adapter (`react-native-webrtc-kaleidoscope/livekit`) uses this so a\n * camera flip (restart) or unpublish (destroy) does not leak generators. The\n * page-shared segmenter and WebGL state are module singletons reused across\n * stages, so they are intentionally NOT torn down here.\n */\nexport const applyVideoEffectsDisposable = (\n track: MediaStreamTrack,\n effects: ReadonlyArray<EffectInput>,\n): DisposablePipeline => {\n if (!track || track.kind !== 'video') {\n throw new Error('kaleidoscope: applyVideoEffects requires a video MediaStreamTrack');\n }\n if (effects.length === 0) {\n return { track, dispose: () => {} };\n }\n\n let current = track;\n const disposers: Array<() => void> = [];\n for (const input of effects) {\n const spec = toEffectSpec(input);\n const transform = specToTransform(spec);\n const stage = applyEffectToTrack(current, transform);\n current = stage.track;\n disposers.push(stage.dispose);\n }\n return {\n track: current,\n dispose: () => {\n // Tear down in reverse so downstream stages stop before their sources.\n for (const dispose of disposers.reverse()) {\n dispose();\n }\n },\n };\n};\n\nexport const applyVideoEffects: ApplyVideoEffects = (track, effects) =>\n applyVideoEffectsDisposable(track, effects).track;\n"]}
|
package/dist/index.web.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
// Web entry point
|
|
2
|
-
//
|
|
1
|
+
// Web entry point (source). This builds to `dist/index.web.js`, which web
|
|
2
|
+
// bundlers resolve through the package's `browser` export condition. With an
|
|
3
|
+
// `exports` map present, that condition supersedes platform-extension
|
|
4
|
+
// resolution; the `.web.ts` suffix here is just our source convention, not what
|
|
5
|
+
// selects this file at consume time.
|
|
3
6
|
//
|
|
4
7
|
// applyVideoEffects(track, effects) wires an Insertable-Streams pipeline that
|
|
5
8
|
// chains each effect's transform and returns a new MediaStreamTrack carrying
|
|
@@ -8,13 +11,13 @@
|
|
|
8
11
|
import { toEffectSpec } from './types';
|
|
9
12
|
import { makeBackgroundImage } from './web/effects/background-image';
|
|
10
13
|
import { blur } from './web/effects/blur';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { applyEffectToTrack } from './web/insertable-streams';
|
|
14
|
+
import { makeTransform } from './web/effects/transform';
|
|
15
|
+
import { applyEffectToTrack, } from './web/insertable-streams';
|
|
14
16
|
import { tuning } from './web/tuning';
|
|
15
17
|
/**
|
|
16
18
|
* Set the Gaussian sigma for the blur effect. Higher = softer blur.
|
|
17
|
-
* Clamped to [0.5,
|
|
19
|
+
* Clamped to [0.5, 7] (the useful range before the linear-sampled kernel
|
|
20
|
+
* truncates and bands). Default 5.
|
|
18
21
|
*/
|
|
19
22
|
export const setBlurSigma = (value) => {
|
|
20
23
|
tuning.setBlurSigma(value);
|
|
@@ -36,6 +39,21 @@ export const setMaskHardness = (value) => {
|
|
|
36
39
|
export const setMaskThreshold = (value) => {
|
|
37
40
|
tuning.setMaskThreshold(value);
|
|
38
41
|
};
|
|
42
|
+
/**
|
|
43
|
+
* Set the segmentation input short-side (px). Native-only knob (lower =
|
|
44
|
+
* cheaper segmentation on older devices); stored on web for API parity but
|
|
45
|
+
* the web MediaPipe pipeline does not consume it.
|
|
46
|
+
*/
|
|
47
|
+
export const setSegmentationTargetShortSide = (value) => {
|
|
48
|
+
tuning.setSegmentationTargetShortSide(value);
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Toggle native per-frame timing logs (off by default). No-op effect on web
|
|
52
|
+
* beyond storing the flag.
|
|
53
|
+
*/
|
|
54
|
+
export const setDebugTiming = (value) => {
|
|
55
|
+
tuning.setDebugTiming(value);
|
|
56
|
+
};
|
|
39
57
|
/**
|
|
40
58
|
* Reset all effect tuning parameters to library defaults.
|
|
41
59
|
*/
|
|
@@ -44,31 +62,52 @@ export const resetEffectTuning = () => {
|
|
|
44
62
|
};
|
|
45
63
|
const specToTransform = (spec) => {
|
|
46
64
|
switch (spec.name) {
|
|
47
|
-
case '
|
|
48
|
-
|
|
65
|
+
case 'flip-x':
|
|
66
|
+
case 'flip-y':
|
|
67
|
+
case 'rotate-cw':
|
|
68
|
+
case 'rotate-ccw':
|
|
69
|
+
return makeTransform(spec.name);
|
|
49
70
|
case 'blur':
|
|
50
|
-
// Blur
|
|
51
|
-
// the
|
|
71
|
+
// Blur strength comes from the global tuning state (setBlurSigma), not
|
|
72
|
+
// the spec; the spec carries no parameters.
|
|
52
73
|
return blur;
|
|
53
74
|
case 'background-image':
|
|
54
75
|
return makeBackgroundImage(spec.source);
|
|
55
|
-
case 'gpu-passthrough':
|
|
56
|
-
return passthrough;
|
|
57
76
|
}
|
|
58
77
|
};
|
|
59
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Like `applyVideoEffects`, but also returns a `dispose()` that tears down every
|
|
80
|
+
* Insertable-Streams stage (stops each generator and aborts each pipe). The
|
|
81
|
+
* LiveKit adapter (`react-native-webrtc-kaleidoscope/livekit`) uses this so a
|
|
82
|
+
* camera flip (restart) or unpublish (destroy) does not leak generators. The
|
|
83
|
+
* page-shared segmenter and WebGL state are module singletons reused across
|
|
84
|
+
* stages, so they are intentionally NOT torn down here.
|
|
85
|
+
*/
|
|
86
|
+
export const applyVideoEffectsDisposable = (track, effects) => {
|
|
60
87
|
if (!track || track.kind !== 'video') {
|
|
61
88
|
throw new Error('kaleidoscope: applyVideoEffects requires a video MediaStreamTrack');
|
|
62
89
|
}
|
|
63
90
|
if (effects.length === 0) {
|
|
64
|
-
return track;
|
|
91
|
+
return { track, dispose: () => { } };
|
|
65
92
|
}
|
|
66
93
|
let current = track;
|
|
94
|
+
const disposers = [];
|
|
67
95
|
for (const input of effects) {
|
|
68
96
|
const spec = toEffectSpec(input);
|
|
69
97
|
const transform = specToTransform(spec);
|
|
70
|
-
|
|
98
|
+
const stage = applyEffectToTrack(current, transform);
|
|
99
|
+
current = stage.track;
|
|
100
|
+
disposers.push(stage.dispose);
|
|
71
101
|
}
|
|
72
|
-
return
|
|
102
|
+
return {
|
|
103
|
+
track: current,
|
|
104
|
+
dispose: () => {
|
|
105
|
+
// Tear down in reverse so downstream stages stop before their sources.
|
|
106
|
+
for (const dispose of disposers.reverse()) {
|
|
107
|
+
dispose();
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
};
|
|
73
111
|
};
|
|
112
|
+
export const applyVideoEffects = (track, effects) => applyVideoEffectsDisposable(track, effects).track;
|
|
74
113
|
//# sourceMappingURL=index.web.js.map
|
package/dist/index.web.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.web.js","sourceRoot":"","sources":["../src/index.web.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"index.web.js","sourceRoot":"","sources":["../src/index.web.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,6EAA6E;AAC7E,sEAAsE;AACtE,gFAAgF;AAChF,qCAAqC;AACrC,EAAE;AACF,8EAA8E;AAC9E,6EAA6E;AAC7E,4EAA4E;AAC5E,uCAAuC;AAEvC,OAAO,EAA6D,YAAY,EAAE,MAAM,SAAS,CAAC;AAClG,OAAO,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AACrE,OAAO,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,EACL,kBAAkB,GAGnB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAEtC;;;;GAIG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,KAAa,EAAQ,EAAE;IAClD,MAAM,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;AAC7B,CAAC,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,KAAa,EAAQ,EAAE;IACrD,MAAM,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;AAChC,CAAC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,KAAa,EAAQ,EAAE;IACtD,MAAM,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC;AACjC,CAAC,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,MAAM,8BAA8B,GAAG,CAAC,KAAa,EAAQ,EAAE;IACpE,MAAM,CAAC,8BAA8B,CAAC,KAAK,CAAC,CAAC;AAC/C,CAAC,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,KAAc,EAAQ,EAAE;IACrD,MAAM,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;AAC/B,CAAC,CAAC;AAEF;;GAEG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,GAAS,EAAE;IAC1C,MAAM,CAAC,KAAK,EAAE,CAAC;AACjB,CAAC,CAAC;AAcF,MAAM,eAAe,GAAG,CAAC,IAAgB,EAAkB,EAAE;IAC3D,QAAQ,IAAI,CAAC,IAAI,EAAE,CAAC;QAClB,KAAK,QAAQ,CAAC;QACd,KAAK,QAAQ,CAAC;QACd,KAAK,WAAW,CAAC;QACjB,KAAK,YAAY;YACf,OAAO,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAClC,KAAK,MAAM;YACT,uEAAuE;YACvE,4CAA4C;YAC5C,OAAO,IAAI,CAAC;QACd,KAAK,kBAAkB;YACrB,OAAO,mBAAmB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC5C,CAAC;AACH,CAAC,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,CACzC,KAAuB,EACvB,OAAmC,EACf,EAAE;IACtB,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;QACrC,MAAM,IAAI,KAAK,CAAC,mEAAmE,CAAC,CAAC;IACvF,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,GAAE,CAAC,EAAE,CAAC;IACtC,CAAC;IAED,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,MAAM,SAAS,GAAsB,EAAE,CAAC;IACxC,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,MAAM,IAAI,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;QACjC,MAAM,SAAS,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;QACxC,MAAM,KAAK,GAAG,kBAAkB,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;QACrD,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC;QACtB,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAChC,CAAC;IACD,OAAO;QACL,KAAK,EAAE,OAAO;QACd,OAAO,EAAE,GAAG,EAAE;YACZ,uEAAuE;YACvE,KAAK,MAAM,OAAO,IAAI,SAAS,CAAC,OAAO,EAAE,EAAE,CAAC;gBAC1C,OAAO,EAAE,CAAC;YACZ,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,iBAAiB,GAAsB,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,CACrE,2BAA2B,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC","sourcesContent":["// Web entry point (source). This builds to `dist/index.web.js`, which web\n// bundlers resolve through the package's `browser` export condition. With an\n// `exports` map present, that condition supersedes platform-extension\n// resolution; the `.web.ts` suffix here is just our source convention, not what\n// selects this file at consume time.\n//\n// applyVideoEffects(track, effects) wires an Insertable-Streams pipeline that\n// chains each effect's transform and returns a new MediaStreamTrack carrying\n// the transformed frames. Pass the returned track to a `<video>` element or\n// to `RTCRtpSender.replaceTrack(...)`.\n\nimport { type ApplyVideoEffects, type EffectInput, type EffectSpec, toEffectSpec } from './types';\nimport { makeBackgroundImage } from './web/effects/background-image';\nimport { blur } from './web/effects/blur';\nimport { makeTransform } from './web/effects/transform';\nimport {\n applyEffectToTrack,\n type DisposablePipeline,\n type FrameTransform,\n} from './web/insertable-streams';\nimport { tuning } from './web/tuning';\n\n/**\n * Set the Gaussian sigma for the blur effect. Higher = softer blur.\n * Clamped to [0.5, 7] (the useful range before the linear-sampled kernel\n * truncates and bands). Default 5.\n */\nexport const setBlurSigma = (value: number): void => {\n tuning.setBlurSigma(value);\n};\n\n/**\n * Set the mask smoothstep hardness for blur and background-image\n * composites, in [0, 1]. 0 = soft halo, 1 = near-step edge. Default 0.5.\n */\nexport const setMaskHardness = (value: number): void => {\n tuning.setMaskHardness(value);\n};\n\n/**\n * Set the mask smoothstep threshold (center of the transition) in\n * [0.05, 0.95]. 0.5 is neutral. Higher values reject low-confidence\n * pixels; lower values are more inclusive. Optimal value is platform-\n * specific because each segmentation model has a different confidence\n * distribution.\n */\nexport const setMaskThreshold = (value: number): void => {\n tuning.setMaskThreshold(value);\n};\n\n/**\n * Set the segmentation input short-side (px). Native-only knob (lower =\n * cheaper segmentation on older devices); stored on web for API parity but\n * the web MediaPipe pipeline does not consume it.\n */\nexport const setSegmentationTargetShortSide = (value: number): void => {\n tuning.setSegmentationTargetShortSide(value);\n};\n\n/**\n * Toggle native per-frame timing logs (off by default). No-op effect on web\n * beyond storing the flag.\n */\nexport const setDebugTiming = (value: boolean): void => {\n tuning.setDebugTiming(value);\n};\n\n/**\n * Reset all effect tuning parameters to library defaults.\n */\nexport const resetEffectTuning = (): void => {\n tuning.reset();\n};\n\nexport type { BackgroundPresetName } from './backgrounds';\nexport type {\n ApplyVideoEffects,\n BackgroundImageSpec,\n BlurSpec,\n EffectInput,\n EffectName,\n EffectSpec,\n TransformName,\n TransformSpec,\n} from './types';\n\nconst specToTransform = (spec: EffectSpec): FrameTransform => {\n switch (spec.name) {\n case 'flip-x':\n case 'flip-y':\n case 'rotate-cw':\n case 'rotate-ccw':\n return makeTransform(spec.name);\n case 'blur':\n // Blur strength comes from the global tuning state (setBlurSigma), not\n // the spec; the spec carries no parameters.\n return blur;\n case 'background-image':\n return makeBackgroundImage(spec.source);\n }\n};\n\n/**\n * Like `applyVideoEffects`, but also returns a `dispose()` that tears down every\n * Insertable-Streams stage (stops each generator and aborts each pipe). The\n * LiveKit adapter (`react-native-webrtc-kaleidoscope/livekit`) uses this so a\n * camera flip (restart) or unpublish (destroy) does not leak generators. The\n * page-shared segmenter and WebGL state are module singletons reused across\n * stages, so they are intentionally NOT torn down here.\n */\nexport const applyVideoEffectsDisposable = (\n track: MediaStreamTrack,\n effects: ReadonlyArray<EffectInput>,\n): DisposablePipeline => {\n if (!track || track.kind !== 'video') {\n throw new Error('kaleidoscope: applyVideoEffects requires a video MediaStreamTrack');\n }\n if (effects.length === 0) {\n return { track, dispose: () => {} };\n }\n\n let current = track;\n const disposers: Array<() => void> = [];\n for (const input of effects) {\n const spec = toEffectSpec(input);\n const transform = specToTransform(spec);\n const stage = applyEffectToTrack(current, transform);\n current = stage.track;\n disposers.push(stage.dispose);\n }\n return {\n track: current,\n dispose: () => {\n // Tear down in reverse so downstream stages stop before their sources.\n for (const dispose of disposers.reverse()) {\n dispose();\n }\n },\n };\n};\n\nexport const applyVideoEffects: ApplyVideoEffects = (track, effects) =>\n applyVideoEffectsDisposable(track, effects).track;\n"]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ProcessorOptions, Track, TrackProcessor } from 'livekit-client';
|
|
2
|
+
import type { EffectInput } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* A LiveKit `TrackProcessor` that applies Kaleidoscope video effects to a local
|
|
5
|
+
* camera track. Construct it with the same effect inputs `applyVideoEffects`
|
|
6
|
+
* accepts (bare names like `'blur'` or full `EffectSpec` objects), then pass it
|
|
7
|
+
* to `localTrack.setProcessor(processor)`.
|
|
8
|
+
*
|
|
9
|
+
* `restart` (camera flip / source change) and `destroy` (unpublish) tear down
|
|
10
|
+
* the prior Insertable-Streams pipeline, so repeated flips do not leak
|
|
11
|
+
* generators. The page-shared segmenter and WebGL state are module singletons
|
|
12
|
+
* reused across pipelines, so they are intentionally retained.
|
|
13
|
+
*/
|
|
14
|
+
export declare class KaleidoscopeProcessor implements TrackProcessor<Track.Kind.Video> {
|
|
15
|
+
readonly name = "kaleidoscope";
|
|
16
|
+
processedTrack?: MediaStreamTrack;
|
|
17
|
+
private readonly effects;
|
|
18
|
+
private disposePipeline;
|
|
19
|
+
constructor(effects: ReadonlyArray<EffectInput>);
|
|
20
|
+
init(opts: ProcessorOptions<Track.Kind.Video>): Promise<void>;
|
|
21
|
+
restart(opts: ProcessorOptions<Track.Kind.Video>): Promise<void>;
|
|
22
|
+
destroy(): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=livekit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"livekit.d.ts","sourceRoot":"","sources":["../src/livekit.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAE9E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C;;;;;;;;;;GAUG;AACH,qBAAa,qBAAsB,YAAW,cAAc,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC;IAC5E,QAAQ,CAAC,IAAI,kBAAkB;IAC/B,cAAc,CAAC,EAAE,gBAAgB,CAAC;IAElC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA6B;IACrD,OAAO,CAAC,eAAe,CAA6B;IAEpD,YAAY,OAAO,EAAE,aAAa,CAAC,WAAW,CAAC,EAE9C;IAEK,IAAI,CAAC,IAAI,EAAE,gBAAgB,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAIlE;IAEK,OAAO,CAAC,IAAI,EAAE,gBAAgB,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAGrE;IAEK,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAI7B;CACF","sourcesContent":["// Opt-in LiveKit adapter, exported from the `react-native-webrtc-kaleidoscope/livekit`\n// subpath. It wraps the web Insertable-Streams pipeline in LiveKit's\n// TrackProcessor interface so consumers do not have to hand-write the glue.\n//\n// Why this exists: the core web API is `applyVideoEffects(track) -> newTrack`,\n// which assumes the caller owns the RTCRtpSender and calls replaceTrack itself.\n// LiveKit owns the sender, so consumers go through `track.setProcessor(...)`\n// instead. This adapter is that bridge:\n//\n// import { KaleidoscopeProcessor } from 'react-native-webrtc-kaleidoscope/livekit';\n// await localVideoTrack.setProcessor(new KaleidoscopeProcessor(['blur']));\n//\n// `livekit-client` is an OPTIONAL peer dependency: the import below is type-only\n// (erased at build), so the published JS has no runtime dependency on LiveKit\n// and the agnostic core stays decoupled. Only consumers that import this subpath\n// need livekit-client installed (they already have it).\n//\n// This adapter is web-only: it builds on MediaStreamTrackProcessor /\n// MediaStreamTrackGenerator (Insertable Streams), which exist in Chromium-based\n// browsers. On unsupported environments the underlying pipeline throws a typed\n// error at init().\n\nimport type { ProcessorOptions, Track, TrackProcessor } from 'livekit-client';\nimport { applyVideoEffectsDisposable } from './index.web';\nimport type { EffectInput } from './types';\n\n/**\n * A LiveKit `TrackProcessor` that applies Kaleidoscope video effects to a local\n * camera track. Construct it with the same effect inputs `applyVideoEffects`\n * accepts (bare names like `'blur'` or full `EffectSpec` objects), then pass it\n * to `localTrack.setProcessor(processor)`.\n *\n * `restart` (camera flip / source change) and `destroy` (unpublish) tear down\n * the prior Insertable-Streams pipeline, so repeated flips do not leak\n * generators. The page-shared segmenter and WebGL state are module singletons\n * reused across pipelines, so they are intentionally retained.\n */\nexport class KaleidoscopeProcessor implements TrackProcessor<Track.Kind.Video> {\n readonly name = 'kaleidoscope';\n processedTrack?: MediaStreamTrack;\n\n private readonly effects: ReadonlyArray<EffectInput>;\n private disposePipeline: (() => void) | null = null;\n\n constructor(effects: ReadonlyArray<EffectInput>) {\n this.effects = effects;\n }\n\n async init(opts: ProcessorOptions<Track.Kind.Video>): Promise<void> {\n const { track, dispose } = applyVideoEffectsDisposable(opts.track, this.effects);\n this.processedTrack = track;\n this.disposePipeline = dispose;\n }\n\n async restart(opts: ProcessorOptions<Track.Kind.Video>): Promise<void> {\n await this.destroy();\n await this.init(opts);\n }\n\n async destroy(): Promise<void> {\n this.disposePipeline?.();\n this.disposePipeline = null;\n this.processedTrack = undefined;\n }\n}\n"]}
|
package/dist/livekit.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Opt-in LiveKit adapter, exported from the `react-native-webrtc-kaleidoscope/livekit`
|
|
2
|
+
// subpath. It wraps the web Insertable-Streams pipeline in LiveKit's
|
|
3
|
+
// TrackProcessor interface so consumers do not have to hand-write the glue.
|
|
4
|
+
//
|
|
5
|
+
// Why this exists: the core web API is `applyVideoEffects(track) -> newTrack`,
|
|
6
|
+
// which assumes the caller owns the RTCRtpSender and calls replaceTrack itself.
|
|
7
|
+
// LiveKit owns the sender, so consumers go through `track.setProcessor(...)`
|
|
8
|
+
// instead. This adapter is that bridge:
|
|
9
|
+
//
|
|
10
|
+
// import { KaleidoscopeProcessor } from 'react-native-webrtc-kaleidoscope/livekit';
|
|
11
|
+
// await localVideoTrack.setProcessor(new KaleidoscopeProcessor(['blur']));
|
|
12
|
+
//
|
|
13
|
+
// `livekit-client` is an OPTIONAL peer dependency: the import below is type-only
|
|
14
|
+
// (erased at build), so the published JS has no runtime dependency on LiveKit
|
|
15
|
+
// and the agnostic core stays decoupled. Only consumers that import this subpath
|
|
16
|
+
// need livekit-client installed (they already have it).
|
|
17
|
+
//
|
|
18
|
+
// This adapter is web-only: it builds on MediaStreamTrackProcessor /
|
|
19
|
+
// MediaStreamTrackGenerator (Insertable Streams), which exist in Chromium-based
|
|
20
|
+
// browsers. On unsupported environments the underlying pipeline throws a typed
|
|
21
|
+
// error at init().
|
|
22
|
+
import { applyVideoEffectsDisposable } from './index.web';
|
|
23
|
+
/**
|
|
24
|
+
* A LiveKit `TrackProcessor` that applies Kaleidoscope video effects to a local
|
|
25
|
+
* camera track. Construct it with the same effect inputs `applyVideoEffects`
|
|
26
|
+
* accepts (bare names like `'blur'` or full `EffectSpec` objects), then pass it
|
|
27
|
+
* to `localTrack.setProcessor(processor)`.
|
|
28
|
+
*
|
|
29
|
+
* `restart` (camera flip / source change) and `destroy` (unpublish) tear down
|
|
30
|
+
* the prior Insertable-Streams pipeline, so repeated flips do not leak
|
|
31
|
+
* generators. The page-shared segmenter and WebGL state are module singletons
|
|
32
|
+
* reused across pipelines, so they are intentionally retained.
|
|
33
|
+
*/
|
|
34
|
+
export class KaleidoscopeProcessor {
|
|
35
|
+
name = 'kaleidoscope';
|
|
36
|
+
processedTrack;
|
|
37
|
+
effects;
|
|
38
|
+
disposePipeline = null;
|
|
39
|
+
constructor(effects) {
|
|
40
|
+
this.effects = effects;
|
|
41
|
+
}
|
|
42
|
+
async init(opts) {
|
|
43
|
+
const { track, dispose } = applyVideoEffectsDisposable(opts.track, this.effects);
|
|
44
|
+
this.processedTrack = track;
|
|
45
|
+
this.disposePipeline = dispose;
|
|
46
|
+
}
|
|
47
|
+
async restart(opts) {
|
|
48
|
+
await this.destroy();
|
|
49
|
+
await this.init(opts);
|
|
50
|
+
}
|
|
51
|
+
async destroy() {
|
|
52
|
+
this.disposePipeline?.();
|
|
53
|
+
this.disposePipeline = null;
|
|
54
|
+
this.processedTrack = undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=livekit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"livekit.js","sourceRoot":"","sources":["../src/livekit.ts"],"names":[],"mappings":"AAAA,uFAAuF;AACvF,qEAAqE;AACrE,4EAA4E;AAC5E,EAAE;AACF,+EAA+E;AAC/E,gFAAgF;AAChF,6EAA6E;AAC7E,wCAAwC;AACxC,EAAE;AACF,sFAAsF;AACtF,6EAA6E;AAC7E,EAAE;AACF,iFAAiF;AACjF,8EAA8E;AAC9E,iFAAiF;AACjF,wDAAwD;AACxD,EAAE;AACF,qEAAqE;AACrE,gFAAgF;AAChF,+EAA+E;AAC/E,mBAAmB;AAGnB,OAAO,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAC;AAG1D;;;;;;;;;;GAUG;AACH,MAAM,OAAO,qBAAqB;IACvB,IAAI,GAAG,cAAc,CAAC;IAC/B,cAAc,CAAoB;IAEjB,OAAO,CAA6B;IAC7C,eAAe,GAAwB,IAAI,CAAC;IAEpD,YAAY,OAAmC;QAC7C,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,IAAwC;QACjD,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,2BAA2B,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;QACjF,IAAI,CAAC,cAAc,GAAG,KAAK,CAAC;QAC5B,IAAI,CAAC,eAAe,GAAG,OAAO,CAAC;IACjC,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,IAAwC;QACpD,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;QACrB,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACxB,CAAC;IAED,KAAK,CAAC,OAAO;QACX,IAAI,CAAC,eAAe,EAAE,EAAE,CAAC;QACzB,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC;QAC5B,IAAI,CAAC,cAAc,GAAG,SAAS,CAAC;IAClC,CAAC;CACF","sourcesContent":["// Opt-in LiveKit adapter, exported from the `react-native-webrtc-kaleidoscope/livekit`\n// subpath. It wraps the web Insertable-Streams pipeline in LiveKit's\n// TrackProcessor interface so consumers do not have to hand-write the glue.\n//\n// Why this exists: the core web API is `applyVideoEffects(track) -> newTrack`,\n// which assumes the caller owns the RTCRtpSender and calls replaceTrack itself.\n// LiveKit owns the sender, so consumers go through `track.setProcessor(...)`\n// instead. This adapter is that bridge:\n//\n// import { KaleidoscopeProcessor } from 'react-native-webrtc-kaleidoscope/livekit';\n// await localVideoTrack.setProcessor(new KaleidoscopeProcessor(['blur']));\n//\n// `livekit-client` is an OPTIONAL peer dependency: the import below is type-only\n// (erased at build), so the published JS has no runtime dependency on LiveKit\n// and the agnostic core stays decoupled. Only consumers that import this subpath\n// need livekit-client installed (they already have it).\n//\n// This adapter is web-only: it builds on MediaStreamTrackProcessor /\n// MediaStreamTrackGenerator (Insertable Streams), which exist in Chromium-based\n// browsers. On unsupported environments the underlying pipeline throws a typed\n// error at init().\n\nimport type { ProcessorOptions, Track, TrackProcessor } from 'livekit-client';\nimport { applyVideoEffectsDisposable } from './index.web';\nimport type { EffectInput } from './types';\n\n/**\n * A LiveKit `TrackProcessor` that applies Kaleidoscope video effects to a local\n * camera track. Construct it with the same effect inputs `applyVideoEffects`\n * accepts (bare names like `'blur'` or full `EffectSpec` objects), then pass it\n * to `localTrack.setProcessor(processor)`.\n *\n * `restart` (camera flip / source change) and `destroy` (unpublish) tear down\n * the prior Insertable-Streams pipeline, so repeated flips do not leak\n * generators. The page-shared segmenter and WebGL state are module singletons\n * reused across pipelines, so they are intentionally retained.\n */\nexport class KaleidoscopeProcessor implements TrackProcessor<Track.Kind.Video> {\n readonly name = 'kaleidoscope';\n processedTrack?: MediaStreamTrack;\n\n private readonly effects: ReadonlyArray<EffectInput>;\n private disposePipeline: (() => void) | null = null;\n\n constructor(effects: ReadonlyArray<EffectInput>) {\n this.effects = effects;\n }\n\n async init(opts: ProcessorOptions<Track.Kind.Video>): Promise<void> {\n const { track, dispose } = applyVideoEffectsDisposable(opts.track, this.effects);\n this.processedTrack = track;\n this.disposePipeline = dispose;\n }\n\n async restart(opts: ProcessorOptions<Track.Kind.Video>): Promise<void> {\n await this.destroy();\n await this.init(opts);\n }\n\n async destroy(): Promise<void> {\n this.disposePipeline?.();\n this.disposePipeline = null;\n this.processedTrack = undefined;\n }\n}\n"]}
|
package/dist/types.d.ts
CHANGED
|
@@ -2,18 +2,21 @@
|
|
|
2
2
|
* Effect specs are parameterized objects. Each effect's shape carries the
|
|
3
3
|
* uniforms its shader needs; the library exposes one shader per effect family
|
|
4
4
|
* (blur, background-image, etc.) and consumers pick uniform values per call.
|
|
5
|
-
*
|
|
6
|
-
* `gpu-passthrough` is a temporary architecture-proof hook and gets
|
|
7
|
-
* removed in the v0.1 cleanup pass.
|
|
8
5
|
*/
|
|
9
6
|
import type { BackgroundPresetName } from './backgrounds';
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Geometric reorientation of the frame: an axis flip or a 90-degree rotation.
|
|
9
|
+
* The operation IS the effect name (parameterless), which keeps it compatible
|
|
10
|
+
* with the bare-string input form and the flat-string native registry. On web
|
|
11
|
+
* these run in display space (the reference behavior); native pipelines correct
|
|
12
|
+
* for the camera buffer's rotation so the on-screen result matches everywhere.
|
|
13
|
+
*/
|
|
14
|
+
export type TransformName = 'flip-x' | 'flip-y' | 'rotate-cw' | 'rotate-ccw';
|
|
15
|
+
export type TransformSpec = {
|
|
16
|
+
readonly name: TransformName;
|
|
12
17
|
};
|
|
13
18
|
export type BlurSpec = {
|
|
14
19
|
readonly name: 'blur';
|
|
15
|
-
/** Gaussian σ. Higher = softer blur. Default 8.0. */
|
|
16
|
-
readonly sigma?: number;
|
|
17
20
|
};
|
|
18
21
|
export type BackgroundImageSpec = {
|
|
19
22
|
readonly name: 'background-image';
|
|
@@ -25,13 +28,17 @@ export type BackgroundImageSpec = {
|
|
|
25
28
|
*
|
|
26
29
|
* The `string & {}` trick preserves preset-name autocomplete while still
|
|
27
30
|
* permitting arbitrary URL inputs without a separate union branch.
|
|
31
|
+
*
|
|
32
|
+
* SECURITY (web): a URL/data-URI `source` is fetched in the page origin and
|
|
33
|
+
* decoded to a texture. If you wire this from end-user input, validate the
|
|
34
|
+
* URL yourself; the library does not allowlist fetch targets. Decoded
|
|
35
|
+
* dimensions are capped and the per-source cache is bounded, but an
|
|
36
|
+
* unvalidated `source` can still issue a same-origin request you did not
|
|
37
|
+
* intend.
|
|
28
38
|
*/
|
|
29
39
|
readonly source: BackgroundPresetName | (string & {});
|
|
30
40
|
};
|
|
31
|
-
export type
|
|
32
|
-
readonly name: 'gpu-passthrough';
|
|
33
|
-
};
|
|
34
|
-
export type EffectSpec = MirrorSpec | BlurSpec | BackgroundImageSpec | PassthroughSpec;
|
|
41
|
+
export type EffectSpec = TransformSpec | BlurSpec | BackgroundImageSpec;
|
|
35
42
|
/**
|
|
36
43
|
* Legacy alias for the discriminant. Useful for typed switch statements and
|
|
37
44
|
* the bare-string call shape (`applyVideoEffects(track, ['blur'])`).
|
|
@@ -47,8 +54,8 @@ export type EffectInput = EffectSpec | EffectName;
|
|
|
47
54
|
*
|
|
48
55
|
* - Native: thin facade over `track._setVideoEffects(names)` from
|
|
49
56
|
* `react-native-webrtc`. Returns the same track reference; mutation is in place.
|
|
50
|
-
*
|
|
51
|
-
*
|
|
57
|
+
* Effects are tuned globally via the tuning setters (setBlurSigma, etc.),
|
|
58
|
+
* not via per-call spec parameters.
|
|
52
59
|
* - Web: builds an Insertable-Streams pipeline with `MediaStreamTrackProcessor`
|
|
53
60
|
* and `MediaStreamTrackGenerator` and returns a NEW track carrying the
|
|
54
61
|
* transformed frames. Replace the upstream sender's track with the return value
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAE1D;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,QAAQ,GAAG,WAAW,GAAG,YAAY,CAAC;AAE7E,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CAGvB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC;IAClC;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,MAAM,EAAE,oBAAoB,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;CACvD,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG,aAAa,GAAG,QAAQ,GAAG,mBAAmB,CAAC;AAExE;;;GAGG;AACH,MAAM,MAAM,UAAU,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC;AAE5C;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,UAAU,CAAC;AAElD;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,iBAAiB,GAAG,CAC9B,KAAK,EAAE,gBAAgB,EACvB,OAAO,EAAE,aAAa,CAAC,WAAW,CAAC,KAChC,gBAAgB,CAAC;AAEtB;;GAEG;AACH,eAAO,MAAM,YAAY,UAAW,WAAW,KAAG,UACmB,CAAC","sourcesContent":["/**\n * Effect specs are parameterized objects. Each effect's shape carries the\n * uniforms its shader needs; the library exposes one shader per effect family\n * (blur, background-image, etc.) and consumers pick uniform values per call.\n */\n\nimport type { BackgroundPresetName } from './backgrounds';\n\n/**\n * Geometric reorientation of the frame: an axis flip or a 90-degree rotation.\n * The operation IS the effect name (parameterless), which keeps it compatible\n * with the bare-string input form and the flat-string native registry. On web\n * these run in display space (the reference behavior); native pipelines correct\n * for the camera buffer's rotation so the on-screen result matches everywhere.\n */\nexport type TransformName = 'flip-x' | 'flip-y' | 'rotate-cw' | 'rotate-ccw';\n\nexport type TransformSpec = {\n readonly name: TransformName;\n};\n\nexport type BlurSpec = {\n readonly name: 'blur';\n // Blur strength is tuned globally via the tuning setters (setBlurSigma),\n // not per-call; there is no per-spec parameter.\n};\n\nexport type BackgroundImageSpec = {\n readonly name: 'background-image';\n /**\n * Either a bundled preset name (autocompleted; see src/backgrounds.ts) or\n * a free-form URL / data URI. Web accepts both; native only resolves\n * bundled preset names because the upstream rn-webrtc registry takes flat\n * strings, not URIs.\n *\n * The `string & {}` trick preserves preset-name autocomplete while still\n * permitting arbitrary URL inputs without a separate union branch.\n *\n * SECURITY (web): a URL/data-URI `source` is fetched in the page origin and\n * decoded to a texture. If you wire this from end-user input, validate the\n * URL yourself; the library does not allowlist fetch targets. Decoded\n * dimensions are capped and the per-source cache is bounded, but an\n * unvalidated `source` can still issue a same-origin request you did not\n * intend.\n */\n readonly source: BackgroundPresetName | (string & {});\n};\n\nexport type EffectSpec = TransformSpec | BlurSpec | BackgroundImageSpec;\n\n/**\n * Legacy alias for the discriminant. Useful for typed switch statements and\n * the bare-string call shape (`applyVideoEffects(track, ['blur'])`).\n */\nexport type EffectName = EffectSpec['name'];\n\n/**\n * applyVideoEffects accepts either a bare effect name (no params; library\n * picks sensible defaults) or a full EffectSpec object with parameters.\n */\nexport type EffectInput = EffectSpec | EffectName;\n\n/**\n * Apply zero or more effects to a local `MediaStreamTrack`.\n *\n * - Native: thin facade over `track._setVideoEffects(names)` from\n * `react-native-webrtc`. Returns the same track reference; mutation is in place.\n * Effects are tuned globally via the tuning setters (setBlurSigma, etc.),\n * not via per-call spec parameters.\n * - Web: builds an Insertable-Streams pipeline with `MediaStreamTrackProcessor`\n * and `MediaStreamTrackGenerator` and returns a NEW track carrying the\n * transformed frames. Replace the upstream sender's track with the return value\n * (`sender.replaceTrack(returnedTrack)`) to apply effects to a peer connection,\n * or attach it to a `<video>` element for local preview.\n *\n * Throws on remote tracks, unknown effect names, missing platform capabilities\n * (web: Insertable Streams; native: peer-dep `_setVideoEffects`), or non-video\n * tracks.\n */\nexport type ApplyVideoEffects = (\n track: MediaStreamTrack,\n effects: ReadonlyArray<EffectInput>,\n) => MediaStreamTrack;\n\n/**\n * Normalize an EffectInput (bare name or spec) into an EffectSpec.\n */\nexport const toEffectSpec = (input: EffectInput): EffectSpec =>\n typeof input === 'string' ? ({ name: input } as EffectSpec) : input;\n"]}
|
package/dist/types.js
CHANGED
|
@@ -2,9 +2,6 @@
|
|
|
2
2
|
* Effect specs are parameterized objects. Each effect's shape carries the
|
|
3
3
|
* uniforms its shader needs; the library exposes one shader per effect family
|
|
4
4
|
* (blur, background-image, etc.) and consumers pick uniform values per call.
|
|
5
|
-
*
|
|
6
|
-
* `gpu-passthrough` is a temporary architecture-proof hook and gets
|
|
7
|
-
* removed in the v0.1 cleanup pass.
|
|
8
5
|
*/
|
|
9
6
|
/**
|
|
10
7
|
* Normalize an EffectInput (bare name or spec) into an EffectSpec.
|
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAgFH;;GAEG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,KAAkB,EAAc,EAAE,CAC7D,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAE,EAAE,IAAI,EAAE,KAAK,EAAiB,CAAC,CAAC,CAAC,KAAK,CAAC","sourcesContent":["/**\n * Effect specs are parameterized objects. Each effect's shape carries the\n * uniforms its shader needs; the library exposes one shader per effect family\n * (blur, background-image, etc.) and consumers pick uniform values per call.\n */\n\nimport type { BackgroundPresetName } from './backgrounds';\n\n/**\n * Geometric reorientation of the frame: an axis flip or a 90-degree rotation.\n * The operation IS the effect name (parameterless), which keeps it compatible\n * with the bare-string input form and the flat-string native registry. On web\n * these run in display space (the reference behavior); native pipelines correct\n * for the camera buffer's rotation so the on-screen result matches everywhere.\n */\nexport type TransformName = 'flip-x' | 'flip-y' | 'rotate-cw' | 'rotate-ccw';\n\nexport type TransformSpec = {\n readonly name: TransformName;\n};\n\nexport type BlurSpec = {\n readonly name: 'blur';\n // Blur strength is tuned globally via the tuning setters (setBlurSigma),\n // not per-call; there is no per-spec parameter.\n};\n\nexport type BackgroundImageSpec = {\n readonly name: 'background-image';\n /**\n * Either a bundled preset name (autocompleted; see src/backgrounds.ts) or\n * a free-form URL / data URI. Web accepts both; native only resolves\n * bundled preset names because the upstream rn-webrtc registry takes flat\n * strings, not URIs.\n *\n * The `string & {}` trick preserves preset-name autocomplete while still\n * permitting arbitrary URL inputs without a separate union branch.\n *\n * SECURITY (web): a URL/data-URI `source` is fetched in the page origin and\n * decoded to a texture. If you wire this from end-user input, validate the\n * URL yourself; the library does not allowlist fetch targets. Decoded\n * dimensions are capped and the per-source cache is bounded, but an\n * unvalidated `source` can still issue a same-origin request you did not\n * intend.\n */\n readonly source: BackgroundPresetName | (string & {});\n};\n\nexport type EffectSpec = TransformSpec | BlurSpec | BackgroundImageSpec;\n\n/**\n * Legacy alias for the discriminant. Useful for typed switch statements and\n * the bare-string call shape (`applyVideoEffects(track, ['blur'])`).\n */\nexport type EffectName = EffectSpec['name'];\n\n/**\n * applyVideoEffects accepts either a bare effect name (no params; library\n * picks sensible defaults) or a full EffectSpec object with parameters.\n */\nexport type EffectInput = EffectSpec | EffectName;\n\n/**\n * Apply zero or more effects to a local `MediaStreamTrack`.\n *\n * - Native: thin facade over `track._setVideoEffects(names)` from\n * `react-native-webrtc`. Returns the same track reference; mutation is in place.\n * Effects are tuned globally via the tuning setters (setBlurSigma, etc.),\n * not via per-call spec parameters.\n * - Web: builds an Insertable-Streams pipeline with `MediaStreamTrackProcessor`\n * and `MediaStreamTrackGenerator` and returns a NEW track carrying the\n * transformed frames. Replace the upstream sender's track with the return value\n * (`sender.replaceTrack(returnedTrack)`) to apply effects to a peer connection,\n * or attach it to a `<video>` element for local preview.\n *\n * Throws on remote tracks, unknown effect names, missing platform capabilities\n * (web: Insertable Streams; native: peer-dep `_setVideoEffects`), or non-video\n * tracks.\n */\nexport type ApplyVideoEffects = (\n track: MediaStreamTrack,\n effects: ReadonlyArray<EffectInput>,\n) => MediaStreamTrack;\n\n/**\n * Normalize an EffectInput (bare name or spec) into an EffectSpec.\n */\nexport const toEffectSpec = (input: EffectInput): EffectSpec =>\n typeof input === 'string' ? ({ name: input } as EffectSpec) : input;\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blur-kernel.d.ts","sourceRoot":"","sources":["../../src/web/blur-kernel.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,WAAW,IAAI,CAAC;AAU7B,eAAO,MAAM,iBAAiB,UACrB,MAAM,KACZ;IAAE,OAAO,EAAE,YAAY,CAAC;IAAC,OAAO,EAAE,YAAY,CAAA;CAsBhD,CAAC","sourcesContent":["// Pure 9-tap separable Gaussian kernel math. Extracted so it is unit-testable\n// in isolation (no WebGL/DOM imports) and shared by the web blur effect.\n//\n// Mirrors Android's BlurFactory.ensureKernel exactly so web, Android, and iOS\n// run the identical blur.frag: tap spacing 2.0, weight = exp(-x^2 / 2σ^2),\n// normalized by (w0 + 2·Σ w[1..]) because the shader samples vUv ± offset and\n// adds each side tap twice. Given a sigma it returns fresh arrays; the caller\n// caches by sigma (the kernel only changes when the blur slider moves).\n\nexport const KERNEL_TAPS = 5;\n\n// Linear-sampled separable Gaussian. A dense Gaussian over texels 0..8 is\n// collapsed into 5 entries: the center plus 4 bilinear pairs of adjacent\n// texels (1,2), (3,4), (5,6), (7,8). Each pair becomes one fractional-offset\n// fetch whose bilinear blend reproduces the two texels' weighted sum, so the\n// shader issues 9 fetches per pass instead of 17. offsets[0] is the center\n// (0). Normalized so center + 2*Σ(pairs) == 1 (the shader samples vUv ±\n// offset and adds each side). Mirrors Android BlurFactory.ensureKernel and iOS\n// BlurKernel.ensure exactly.\nexport const computeBlurKernel = (\n sigma: number,\n): { weights: Float32Array; offsets: Float32Array } => {\n const g = (t: number): number => Math.exp(-(t * t) / (2 * sigma * sigma));\n const offsets = new Float32Array(KERNEL_TAPS);\n const raw = new Array<number>(KERNEL_TAPS);\n offsets[0] = 0;\n raw[0] = g(0);\n let sum = raw[0];\n for (let p = 1; p < KERNEL_TAPS; p++) {\n const a = 2 * p - 1;\n const b = 2 * p;\n const wa = g(a);\n const wb = g(b);\n const w = wa + wb;\n offsets[p] = (a * wa + b * wb) / w;\n raw[p] = w;\n sum += 2 * w;\n }\n const weights = new Float32Array(KERNEL_TAPS);\n for (let i = 0; i < KERNEL_TAPS; i++) {\n weights[i] = (raw[i] ?? 0) / sum;\n }\n return { weights, offsets };\n};\n"]}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Pure 9-tap separable Gaussian kernel math. Extracted so it is unit-testable
|
|
2
|
+
// in isolation (no WebGL/DOM imports) and shared by the web blur effect.
|
|
3
|
+
//
|
|
4
|
+
// Mirrors Android's BlurFactory.ensureKernel exactly so web, Android, and iOS
|
|
5
|
+
// run the identical blur.frag: tap spacing 2.0, weight = exp(-x^2 / 2σ^2),
|
|
6
|
+
// normalized by (w0 + 2·Σ w[1..]) because the shader samples vUv ± offset and
|
|
7
|
+
// adds each side tap twice. Given a sigma it returns fresh arrays; the caller
|
|
8
|
+
// caches by sigma (the kernel only changes when the blur slider moves).
|
|
9
|
+
export const KERNEL_TAPS = 5;
|
|
10
|
+
// Linear-sampled separable Gaussian. A dense Gaussian over texels 0..8 is
|
|
11
|
+
// collapsed into 5 entries: the center plus 4 bilinear pairs of adjacent
|
|
12
|
+
// texels (1,2), (3,4), (5,6), (7,8). Each pair becomes one fractional-offset
|
|
13
|
+
// fetch whose bilinear blend reproduces the two texels' weighted sum, so the
|
|
14
|
+
// shader issues 9 fetches per pass instead of 17. offsets[0] is the center
|
|
15
|
+
// (0). Normalized so center + 2*Σ(pairs) == 1 (the shader samples vUv ±
|
|
16
|
+
// offset and adds each side). Mirrors Android BlurFactory.ensureKernel and iOS
|
|
17
|
+
// BlurKernel.ensure exactly.
|
|
18
|
+
export const computeBlurKernel = (sigma) => {
|
|
19
|
+
const g = (t) => Math.exp(-(t * t) / (2 * sigma * sigma));
|
|
20
|
+
const offsets = new Float32Array(KERNEL_TAPS);
|
|
21
|
+
const raw = new Array(KERNEL_TAPS);
|
|
22
|
+
offsets[0] = 0;
|
|
23
|
+
raw[0] = g(0);
|
|
24
|
+
let sum = raw[0];
|
|
25
|
+
for (let p = 1; p < KERNEL_TAPS; p++) {
|
|
26
|
+
const a = 2 * p - 1;
|
|
27
|
+
const b = 2 * p;
|
|
28
|
+
const wa = g(a);
|
|
29
|
+
const wb = g(b);
|
|
30
|
+
const w = wa + wb;
|
|
31
|
+
offsets[p] = (a * wa + b * wb) / w;
|
|
32
|
+
raw[p] = w;
|
|
33
|
+
sum += 2 * w;
|
|
34
|
+
}
|
|
35
|
+
const weights = new Float32Array(KERNEL_TAPS);
|
|
36
|
+
for (let i = 0; i < KERNEL_TAPS; i++) {
|
|
37
|
+
weights[i] = (raw[i] ?? 0) / sum;
|
|
38
|
+
}
|
|
39
|
+
return { weights, offsets };
|
|
40
|
+
};
|
|
41
|
+
//# sourceMappingURL=blur-kernel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blur-kernel.js","sourceRoot":"","sources":["../../src/web/blur-kernel.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,yEAAyE;AACzE,EAAE;AACF,8EAA8E;AAC9E,2EAA2E;AAC3E,8EAA8E;AAC9E,8EAA8E;AAC9E,wEAAwE;AAExE,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC;AAE7B,0EAA0E;AAC1E,yEAAyE;AACzE,6EAA6E;AAC7E,6EAA6E;AAC7E,2EAA2E;AAC3E,wEAAwE;AACxE,+EAA+E;AAC/E,6BAA6B;AAC7B,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAC/B,KAAa,EACqC,EAAE;IACpD,MAAM,CAAC,GAAG,CAAC,CAAS,EAAU,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC;IAC1E,MAAM,OAAO,GAAG,IAAI,YAAY,CAAC,WAAW,CAAC,CAAC;IAC9C,MAAM,GAAG,GAAG,IAAI,KAAK,CAAS,WAAW,CAAC,CAAC;IAC3C,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACf,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IACd,IAAI,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;IACjB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,WAAW,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACpB,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAChB,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QAChB,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QAChB,MAAM,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC;QAClB,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC;QACnC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QACX,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;IACf,CAAC;IACD,MAAM,OAAO,GAAG,IAAI,YAAY,CAAC,WAAW,CAAC,CAAC;IAC9C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,WAAW,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,GAAG,CAAC;IACnC,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;AAC9B,CAAC,CAAC","sourcesContent":["// Pure 9-tap separable Gaussian kernel math. Extracted so it is unit-testable\n// in isolation (no WebGL/DOM imports) and shared by the web blur effect.\n//\n// Mirrors Android's BlurFactory.ensureKernel exactly so web, Android, and iOS\n// run the identical blur.frag: tap spacing 2.0, weight = exp(-x^2 / 2σ^2),\n// normalized by (w0 + 2·Σ w[1..]) because the shader samples vUv ± offset and\n// adds each side tap twice. Given a sigma it returns fresh arrays; the caller\n// caches by sigma (the kernel only changes when the blur slider moves).\n\nexport const KERNEL_TAPS = 5;\n\n// Linear-sampled separable Gaussian. A dense Gaussian over texels 0..8 is\n// collapsed into 5 entries: the center plus 4 bilinear pairs of adjacent\n// texels (1,2), (3,4), (5,6), (7,8). Each pair becomes one fractional-offset\n// fetch whose bilinear blend reproduces the two texels' weighted sum, so the\n// shader issues 9 fetches per pass instead of 17. offsets[0] is the center\n// (0). Normalized so center + 2*Σ(pairs) == 1 (the shader samples vUv ±\n// offset and adds each side). Mirrors Android BlurFactory.ensureKernel and iOS\n// BlurKernel.ensure exactly.\nexport const computeBlurKernel = (\n sigma: number,\n): { weights: Float32Array; offsets: Float32Array } => {\n const g = (t: number): number => Math.exp(-(t * t) / (2 * sigma * sigma));\n const offsets = new Float32Array(KERNEL_TAPS);\n const raw = new Array<number>(KERNEL_TAPS);\n offsets[0] = 0;\n raw[0] = g(0);\n let sum = raw[0];\n for (let p = 1; p < KERNEL_TAPS; p++) {\n const a = 2 * p - 1;\n const b = 2 * p;\n const wa = g(a);\n const wb = g(b);\n const w = wa + wb;\n offsets[p] = (a * wa + b * wb) / w;\n raw[p] = w;\n sum += 2 * w;\n }\n const weights = new Float32Array(KERNEL_TAPS);\n for (let i = 0; i < KERNEL_TAPS; i++) {\n weights[i] = (raw[i] ?? 0) / sum;\n }\n return { weights, offsets };\n};\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"background-image.d.ts","sourceRoot":"","sources":["../../../src/web/effects/background-image.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AA2K5D,eAAO,MAAM,mBAAmB,WAAY,MAAM,KAAG,cA6FpD,CAAC","sourcesContent":["// Web background-image effect. Same composite pattern as the blur effect:\n// MediaPipe Selfie Segmentation produces a person mask, then a WebGL2\n// composite shader mixes the original camera frame and the background image\n// based on the mask.\n//\n// Difference from blur: instead of generating the background from two\n// Gaussian passes on the camera, the background is a still image loaded\n// once from the URL the consumer passes in. Cached per-source URL.\n//\n// Shader source lives in src/web/shaders.ts; MediaPipe loader in\n// src/web/segmenter.ts.\n\nimport type { FrameTransform } from '../insertable-streams';\nimport { loadSegmenter, type SegmenterResults } from '../segmenter';\nimport { COMPOSITE_FRAG_SRC, PASSTHROUGH_VERT_SRC } from '../shaders';\nimport { maskSmoothstepRange, tuning } from '../tuning';\n\ntype GpuState = {\n gl: WebGL2RenderingContext;\n canvas: OffscreenCanvas;\n width: number;\n height: number;\n program: WebGLProgram;\n textures: {\n original: WebGLTexture;\n mask: WebGLTexture;\n background: WebGLTexture;\n };\n};\n\nconst compileShader = (gl: WebGL2RenderingContext, type: number, source: string): WebGLShader => {\n const shader = gl.createShader(type);\n if (!shader) throw new Error('kaleidoscope: gl.createShader returned null');\n gl.shaderSource(shader, source);\n gl.compileShader(shader);\n if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n const log = gl.getShaderInfoLog(shader) ?? '(no info log)';\n gl.deleteShader(shader);\n throw new Error(`kaleidoscope: shader compile failed: ${log}\\n---\\n${source}`);\n }\n return shader;\n};\n\nconst linkProgram = (\n gl: WebGL2RenderingContext,\n vertSrc: string,\n fragSrc: string,\n): WebGLProgram => {\n const vs = compileShader(gl, gl.VERTEX_SHADER, vertSrc);\n const fs = compileShader(gl, gl.FRAGMENT_SHADER, fragSrc);\n const prog = gl.createProgram();\n if (!prog) throw new Error('kaleidoscope: gl.createProgram returned null');\n gl.attachShader(prog, vs);\n gl.attachShader(prog, fs);\n gl.linkProgram(prog);\n gl.detachShader(prog, vs);\n gl.detachShader(prog, fs);\n gl.deleteShader(vs);\n gl.deleteShader(fs);\n if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {\n const log = gl.getProgramInfoLog(prog) ?? '(no info log)';\n gl.deleteProgram(prog);\n throw new Error(`kaleidoscope: program link failed: ${log}`);\n }\n return prog;\n};\n\nconst createTexture = (gl: WebGL2RenderingContext, width: number, height: number): WebGLTexture => {\n const tex = gl.createTexture();\n if (!tex) throw new Error('kaleidoscope: gl.createTexture returned null');\n gl.bindTexture(gl.TEXTURE_2D, tex);\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n return tex;\n};\n\nconst uploadTexture = (\n gl: WebGL2RenderingContext,\n tex: WebGLTexture,\n source: TexImageSource,\n flipY: boolean,\n): void => {\n gl.bindTexture(gl.TEXTURE_2D, tex);\n gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, flipY);\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source);\n gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);\n};\n\n// --- Per-source caching ----------------------------------------------------\n\ntype CacheEntry = {\n state: GpuState | null;\n // The bg image is staged onto an OffscreenCanvas at load time because\n // UNPACK_FLIP_Y_WEBGL is silently a no-op on ImageBitmaps; we want\n // flipY to actually flip on upload, and it works on canvas sources.\n imagePromise: Promise<{ canvas: OffscreenCanvas; width: number; height: number }>;\n inputCanvas2D: OffscreenCanvas | null;\n inputCtx2D: OffscreenCanvasRenderingContext2D | null;\n};\n\nconst cache = new Map<string, CacheEntry>();\n\nconst loadImage = (\n source: string,\n): Promise<{ canvas: OffscreenCanvas; width: number; height: number }> =>\n fetch(source, { mode: 'cors' })\n .then((r) => {\n if (!r.ok) throw new Error(`kaleidoscope: failed to fetch background image: ${r.status}`);\n return r.blob();\n })\n .then((blob) => createImageBitmap(blob))\n .then((bitmap) => {\n const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);\n const ctx = canvas.getContext('2d');\n if (!ctx) throw new Error('kaleidoscope: bg OffscreenCanvas 2D context unavailable');\n ctx.drawImage(bitmap, 0, 0);\n const width = bitmap.width;\n const height = bitmap.height;\n bitmap.close();\n return { canvas, width, height };\n });\n\nconst ensureState = (entry: CacheEntry, width: number, height: number): GpuState => {\n if (entry.state && entry.state.width === width && entry.state.height === height) {\n return entry.state;\n }\n if (entry.state) {\n const { gl } = entry.state;\n gl.deleteProgram(entry.state.program);\n gl.deleteTexture(entry.state.textures.original);\n gl.deleteTexture(entry.state.textures.mask);\n gl.deleteTexture(entry.state.textures.background);\n }\n\n const canvas = entry.state?.canvas ?? new OffscreenCanvas(width, height);\n canvas.width = width;\n canvas.height = height;\n const gl = canvas.getContext('webgl2');\n if (!gl) {\n throw new Error(\n 'kaleidoscope: WebGL2 not available; background-image requires a WebGL2-capable browser',\n );\n }\n\n entry.state = {\n gl,\n canvas,\n width,\n height,\n program: linkProgram(gl, PASSTHROUGH_VERT_SRC, COMPOSITE_FRAG_SRC),\n textures: {\n original: createTexture(gl, width, height),\n mask: createTexture(gl, width, height),\n background: createTexture(gl, width, height),\n },\n };\n return entry.state;\n};\n\nconst ensureInputCanvas = (\n entry: CacheEntry,\n width: number,\n height: number,\n): OffscreenCanvasRenderingContext2D => {\n if (\n !entry.inputCanvas2D ||\n entry.inputCanvas2D.width !== width ||\n entry.inputCanvas2D.height !== height\n ) {\n entry.inputCanvas2D = new OffscreenCanvas(width, height);\n entry.inputCtx2D = entry.inputCanvas2D.getContext('2d');\n if (!entry.inputCtx2D) {\n throw new Error('kaleidoscope: OffscreenCanvas 2D context unavailable');\n }\n }\n return entry.inputCtx2D as OffscreenCanvasRenderingContext2D;\n};\n\n// --- The effect factory ----------------------------------------------------\n\nexport const makeBackgroundImage = (source: string): FrameTransform => {\n let entry = cache.get(source);\n if (!entry) {\n entry = {\n state: null,\n imagePromise: loadImage(source),\n inputCanvas2D: null,\n inputCtx2D: null,\n };\n cache.set(source, entry);\n }\n const e = entry;\n\n return async (frame) => {\n const [segmenter, bg] = await Promise.all([loadSegmenter(), e.imagePromise]);\n const w = frame.displayWidth;\n const h = frame.displayHeight;\n\n const inputCtx = ensureInputCanvas(e, w, h);\n inputCtx.drawImage(frame, 0, 0, w, h);\n const inputCanvas = e.inputCanvas2D as unknown as CanvasImageSource;\n\n const results: SegmenterResults = await new Promise((resolve) => {\n segmenter.onResults((r) => resolve(r));\n void segmenter.send({ image: inputCanvas });\n });\n\n const s = ensureState(e, w, h);\n const { gl, canvas, program, textures } = s;\n\n // Upload original and bg with flipY=true (DOM-coord → GL v=1 = top).\n // Bg is staged through OffscreenCanvas (loaded at bg-image load time)\n // because UNPACK_FLIP_Y_WEBGL is a no-op on raw ImageBitmaps; canvas\n // sources flip correctly. Mask is uploaded with flipY=false for the\n // same reason (no-op on MediaPipe's segmentationMask source) AND to\n // preserve the soft confidence values — canvas premultiplied-alpha math\n // would collapse them to near-binary. The composite shader compensates\n // via uMaskUvScale=(1,-1) / uMaskUvOffset=(0,1) set below.\n uploadTexture(gl, textures.original, inputCanvas as unknown as TexImageSource, true);\n uploadTexture(gl, textures.mask, results.segmentationMask as unknown as TexImageSource, false);\n uploadTexture(gl, textures.background, bg.canvas as unknown as TexImageSource, true);\n\n gl.disable(gl.BLEND);\n gl.disable(gl.DEPTH_TEST);\n\n gl.bindFramebuffer(gl.FRAMEBUFFER, null);\n gl.viewport(0, 0, w, h);\n gl.useProgram(program);\n gl.activeTexture(gl.TEXTURE0);\n gl.bindTexture(gl.TEXTURE_2D, textures.original);\n gl.uniform1i(gl.getUniformLocation(program, 'uOriginal'), 0);\n gl.activeTexture(gl.TEXTURE1);\n gl.bindTexture(gl.TEXTURE_2D, textures.background);\n gl.uniform1i(gl.getUniformLocation(program, 'uBackground'), 1);\n gl.activeTexture(gl.TEXTURE2);\n gl.bindTexture(gl.TEXTURE_2D, textures.mask);\n gl.uniform1i(gl.getUniformLocation(program, 'uMask'), 2);\n // Cover-fit center crop: scale the smaller axis to fit, offset to\n // center. Mirrors android/.../effects/BackgroundImageFactory.kt.\n const outAspect = w / h;\n const bgAspect = bg.width / bg.height;\n let bgScaleX: number;\n let bgScaleY: number;\n let bgOffsetX: number;\n let bgOffsetY: number;\n if (bgAspect > outAspect) {\n bgScaleX = outAspect / bgAspect;\n bgScaleY = 1.0;\n bgOffsetX = (1 - bgScaleX) * 0.5;\n bgOffsetY = 0;\n } else {\n bgScaleX = 1.0;\n bgScaleY = bgAspect / outAspect;\n bgOffsetX = 0;\n bgOffsetY = (1 - bgScaleY) * 0.5;\n }\n gl.uniform2f(gl.getUniformLocation(program, 'uBgUvScale'), bgScaleX, bgScaleY);\n gl.uniform2f(gl.getUniformLocation(program, 'uBgUvOffset'), bgOffsetX, bgOffsetY);\n // Mask V-flip via sampling uniforms; see comment on the upload above.\n gl.uniform2f(gl.getUniformLocation(program, 'uMaskUvScale'), 1, -1);\n gl.uniform2f(gl.getUniformLocation(program, 'uMaskUvOffset'), 0, 1);\n const [maskLo, maskHi] = maskSmoothstepRange(tuning.maskHardness, tuning.maskThreshold);\n gl.uniform1f(gl.getUniformLocation(program, 'uMaskLo'), maskLo);\n gl.uniform1f(gl.getUniformLocation(program, 'uMaskHi'), maskHi);\n gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);\n\n const out = new VideoFrame(canvas as unknown as CanvasImageSource, {\n timestamp: frame.timestamp,\n ...(frame.duration != null ? { duration: frame.duration } : {}),\n });\n frame.close();\n return out;\n };\n};\n"]}
|
|
1
|
+
{"version":3,"file":"background-image.d.ts","sourceRoot":"","sources":["../../../src/web/effects/background-image.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAiP5D,eAAO,MAAM,mBAAmB,WAAY,MAAM,KAAG,cA8GpD,CAAC","sourcesContent":["// Web background-image effect. Same composite pattern as the blur effect:\n// MediaPipe Selfie Segmentation produces a person mask, then a WebGL2\n// composite shader mixes the original camera frame and the background image\n// based on the mask.\n//\n// Difference from blur: instead of generating the background from two\n// Gaussian passes on the camera, the background is a still image loaded\n// once from the URL the consumer passes in. Cached per-source URL.\n//\n// Shader source lives in src/web/shaders.ts; MediaPipe loader in\n// src/web/segmenter.ts.\n\nimport type { FrameTransform } from '../insertable-streams';\nimport { getLatestMask, loadSegmenter, requestMaskIfIdle } from '../segmenter';\nimport { COMPOSITE_FRAG_SRC, PASSTHROUGH_VERT_SRC } from '../shaders';\nimport { maskSmoothstepRange, tuning } from '../tuning';\n\n// Uniform locations queried once at link, not via getUniformLocation per frame\n// (mirrors src/web/effects/blur.ts; the composite shader is identical).\ntype CompositeProgram = {\n prog: WebGLProgram;\n uOriginal: WebGLUniformLocation | null;\n uBackground: WebGLUniformLocation | null;\n uMask: WebGLUniformLocation | null;\n uBgUvScale: WebGLUniformLocation | null;\n uBgUvOffset: WebGLUniformLocation | null;\n uMaskUvScale: WebGLUniformLocation | null;\n uMaskUvOffset: WebGLUniformLocation | null;\n uMaskLo: WebGLUniformLocation | null;\n uMaskHi: WebGLUniformLocation | null;\n};\n\ntype GpuState = {\n gl: WebGL2RenderingContext;\n canvas: OffscreenCanvas;\n width: number;\n height: number;\n program: CompositeProgram;\n textures: {\n original: WebGLTexture;\n mask: WebGLTexture;\n background: WebGLTexture;\n };\n};\n\nconst compileShader = (gl: WebGL2RenderingContext, type: number, source: string): WebGLShader => {\n const shader = gl.createShader(type);\n if (!shader) throw new Error('kaleidoscope: gl.createShader returned null');\n gl.shaderSource(shader, source);\n gl.compileShader(shader);\n if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n const log = gl.getShaderInfoLog(shader) ?? '(no info log)';\n gl.deleteShader(shader);\n throw new Error(`kaleidoscope: shader compile failed: ${log}\\n---\\n${source}`);\n }\n return shader;\n};\n\nconst linkProgram = (\n gl: WebGL2RenderingContext,\n vertSrc: string,\n fragSrc: string,\n): WebGLProgram => {\n const vs = compileShader(gl, gl.VERTEX_SHADER, vertSrc);\n const fs = compileShader(gl, gl.FRAGMENT_SHADER, fragSrc);\n const prog = gl.createProgram();\n if (!prog) throw new Error('kaleidoscope: gl.createProgram returned null');\n gl.attachShader(prog, vs);\n gl.attachShader(prog, fs);\n gl.linkProgram(prog);\n gl.detachShader(prog, vs);\n gl.detachShader(prog, fs);\n gl.deleteShader(vs);\n gl.deleteShader(fs);\n if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {\n const log = gl.getProgramInfoLog(prog) ?? '(no info log)';\n gl.deleteProgram(prog);\n throw new Error(`kaleidoscope: program link failed: ${log}`);\n }\n return prog;\n};\n\nconst linkCompositeProgram = (gl: WebGL2RenderingContext): CompositeProgram => {\n const prog = linkProgram(gl, PASSTHROUGH_VERT_SRC, COMPOSITE_FRAG_SRC);\n return {\n prog,\n uOriginal: gl.getUniformLocation(prog, 'uOriginal'),\n uBackground: gl.getUniformLocation(prog, 'uBackground'),\n uMask: gl.getUniformLocation(prog, 'uMask'),\n uBgUvScale: gl.getUniformLocation(prog, 'uBgUvScale'),\n uBgUvOffset: gl.getUniformLocation(prog, 'uBgUvOffset'),\n uMaskUvScale: gl.getUniformLocation(prog, 'uMaskUvScale'),\n uMaskUvOffset: gl.getUniformLocation(prog, 'uMaskUvOffset'),\n uMaskLo: gl.getUniformLocation(prog, 'uMaskLo'),\n uMaskHi: gl.getUniformLocation(prog, 'uMaskHi'),\n };\n};\n\nconst createTexture = (gl: WebGL2RenderingContext, width: number, height: number): WebGLTexture => {\n const tex = gl.createTexture();\n if (!tex) throw new Error('kaleidoscope: gl.createTexture returned null');\n gl.bindTexture(gl.TEXTURE_2D, tex);\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n return tex;\n};\n\nconst uploadTexture = (\n gl: WebGL2RenderingContext,\n tex: WebGLTexture,\n source: TexImageSource,\n flipY: boolean,\n): void => {\n gl.bindTexture(gl.TEXTURE_2D, tex);\n gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, flipY);\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source);\n gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);\n};\n\n// --- Per-source caching ----------------------------------------------------\n\ntype CacheEntry = {\n state: GpuState | null;\n // The bg image is staged onto an OffscreenCanvas at load time because\n // UNPACK_FLIP_Y_WEBGL is silently a no-op on ImageBitmaps; we want\n // flipY to actually flip on upload, and it works on canvas sources.\n imagePromise: Promise<{ canvas: OffscreenCanvas; width: number; height: number }>;\n inputCanvas2D: OffscreenCanvas | null;\n inputCtx2D: OffscreenCanvasRenderingContext2D | null;\n};\n\nconst cache = new Map<string, CacheEntry>();\n\n// SECURITY: `source` is consumer-supplied and may be an arbitrary URL or data\n// URI (see BackgroundImageSpec.source). Two bounds keep an untrusted source\n// from becoming a memory-pressure DoS:\n// - MAX_BG_DIMENSION caps the RETAINED raster and the GPU upload so a\n// \"decompression bomb\" (a small file that decodes to hundreds of\n// megapixels) is downscaled rather than held full-size. A background is\n// cover-fit anyway, so the cap costs nothing visible. NOTE: createImageBitmap\n// decodes the full image before this downscale runs, so the transient decode\n// peak is bounded by the browser's own createImageBitmap limits, not by this\n// cap; the cap bounds sustained memory + GPU pressure, not the decode spike.\n// - MAX_CACHE_ENTRIES bounds the per-source cache so a stream of distinct\n// URLs cannot grow it without limit.\n// Consumers wiring `source` from end-user input should still validate the URL\n// themselves; this library does not fetch-allowlist.\nconst MAX_BG_DIMENSION = 4096;\nconst MAX_CACHE_ENTRIES = 32;\n\nconst loadImage = (\n source: string,\n): Promise<{ canvas: OffscreenCanvas; width: number; height: number }> =>\n fetch(source, { mode: 'cors' })\n .then((r) => {\n if (!r.ok) throw new Error(`kaleidoscope: failed to fetch background image: ${r.status}`);\n return r.blob();\n })\n .then((blob) => createImageBitmap(blob))\n .then((bitmap) => {\n const longSide = Math.max(bitmap.width, bitmap.height);\n const scale = longSide > MAX_BG_DIMENSION ? MAX_BG_DIMENSION / longSide : 1;\n const width = Math.max(1, Math.round(bitmap.width * scale));\n const height = Math.max(1, Math.round(bitmap.height * scale));\n const canvas = new OffscreenCanvas(width, height);\n const ctx = canvas.getContext('2d');\n if (!ctx) throw new Error('kaleidoscope: bg OffscreenCanvas 2D context unavailable');\n ctx.drawImage(bitmap, 0, 0, width, height);\n bitmap.close();\n return { canvas, width, height };\n });\n\n// Free an entry's GL resources before dropping it from the cache.\nconst disposeEntry = (entry: CacheEntry): void => {\n if (!entry.state) return;\n const { gl, program, textures } = entry.state;\n gl.deleteProgram(program.prog);\n gl.deleteTexture(textures.original);\n gl.deleteTexture(textures.mask);\n gl.deleteTexture(textures.background);\n entry.state = null;\n};\n\nconst ensureState = (\n entry: CacheEntry,\n width: number,\n height: number,\n bg: { canvas: OffscreenCanvas; width: number; height: number },\n): GpuState => {\n if (entry.state && entry.state.width === width && entry.state.height === height) {\n return entry.state;\n }\n if (entry.state) {\n const { gl } = entry.state;\n gl.deleteProgram(entry.state.program.prog);\n gl.deleteTexture(entry.state.textures.original);\n gl.deleteTexture(entry.state.textures.mask);\n gl.deleteTexture(entry.state.textures.background);\n }\n\n const canvas = entry.state?.canvas ?? new OffscreenCanvas(width, height);\n canvas.width = width;\n canvas.height = height;\n const gl = canvas.getContext('webgl2');\n if (!gl) {\n throw new Error(\n 'kaleidoscope: WebGL2 not available; background-image requires a WebGL2-capable browser',\n );\n }\n\n entry.state = {\n gl,\n canvas,\n width,\n height,\n program: linkCompositeProgram(gl),\n textures: {\n original: createTexture(gl, width, height),\n mask: createTexture(gl, width, height),\n background: createTexture(gl, width, height),\n },\n };\n // Static background: upload once, here, not per frame. Re-runs only when\n // this state is rebuilt (resolution change). flipY=true matches the\n // original's DOM-coord → GL v=1 convention.\n uploadTexture(gl, entry.state.textures.background, bg.canvas as unknown as TexImageSource, true);\n return entry.state;\n};\n\nconst ensureInputCanvas = (\n entry: CacheEntry,\n width: number,\n height: number,\n): OffscreenCanvasRenderingContext2D => {\n if (\n !entry.inputCanvas2D ||\n entry.inputCanvas2D.width !== width ||\n entry.inputCanvas2D.height !== height\n ) {\n entry.inputCanvas2D = new OffscreenCanvas(width, height);\n entry.inputCtx2D = entry.inputCanvas2D.getContext('2d');\n if (!entry.inputCtx2D) {\n throw new Error('kaleidoscope: OffscreenCanvas 2D context unavailable');\n }\n }\n return entry.inputCtx2D as OffscreenCanvasRenderingContext2D;\n};\n\n// --- The effect factory ----------------------------------------------------\n\nexport const makeBackgroundImage = (source: string): FrameTransform => {\n let entry = cache.get(source);\n if (!entry) {\n // Bound the cache: evict the oldest entry (Map preserves insertion order)\n // and free its GL resources before inserting a new source.\n while (cache.size >= MAX_CACHE_ENTRIES) {\n const oldestKey = cache.keys().next().value;\n if (oldestKey === undefined) break;\n const oldest = cache.get(oldestKey);\n if (oldest) disposeEntry(oldest);\n cache.delete(oldestKey);\n }\n entry = {\n state: null,\n imagePromise: loadImage(source),\n inputCanvas2D: null,\n inputCtx2D: null,\n };\n cache.set(source, entry);\n }\n const e = entry;\n\n return async (frame) => {\n const bg = await e.imagePromise;\n await loadSegmenter(); // ensure loaded; cached after first call.\n const w = frame.displayWidth;\n const h = frame.displayHeight;\n\n const inputCtx = ensureInputCanvas(e, w, h);\n inputCtx.drawImage(frame, 0, 0, w, h);\n const inputCanvas = e.inputCanvas2D as unknown as CanvasImageSource;\n\n // Decoupled segmentation; see blur.ts for the rationale. Forward the\n // original frame until the first mask lands.\n requestMaskIfIdle(inputCanvas);\n const results = getLatestMask();\n if (!results) {\n const passthrough = new VideoFrame(e.inputCanvas2D as unknown as CanvasImageSource, {\n timestamp: frame.timestamp,\n ...(frame.duration != null ? { duration: frame.duration } : {}),\n });\n frame.close();\n return passthrough;\n }\n\n const s = ensureState(e, w, h, bg);\n const { gl, canvas, program, textures } = s;\n\n // Upload original with flipY=true (DOM-coord → GL v=1 = top). The static\n // background is uploaded once in ensureState (flipped the same way), not\n // per frame. Mask is uploaded with flipY=false for the same reason\n // (no-op on MediaPipe's segmentationMask source) AND to preserve the soft\n // confidence values — canvas premultiplied-alpha math would collapse them\n // to near-binary. The composite shader compensates via\n // uMaskUvScale=(1,-1) / uMaskUvOffset=(0,1) set below.\n uploadTexture(gl, textures.original, inputCanvas as unknown as TexImageSource, true);\n uploadTexture(gl, textures.mask, results.segmentationMask as unknown as TexImageSource, false);\n\n gl.disable(gl.BLEND);\n gl.disable(gl.DEPTH_TEST);\n\n gl.bindFramebuffer(gl.FRAMEBUFFER, null);\n gl.viewport(0, 0, w, h);\n // biome-ignore lint/correctness/useHookAtTopLevel: gl.useProgram is a WebGL call, not a React hook (the early return above tripped the use* heuristic).\n gl.useProgram(program.prog);\n gl.activeTexture(gl.TEXTURE0);\n gl.bindTexture(gl.TEXTURE_2D, textures.original);\n gl.uniform1i(program.uOriginal, 0);\n gl.activeTexture(gl.TEXTURE1);\n gl.bindTexture(gl.TEXTURE_2D, textures.background);\n gl.uniform1i(program.uBackground, 1);\n gl.activeTexture(gl.TEXTURE2);\n gl.bindTexture(gl.TEXTURE_2D, textures.mask);\n gl.uniform1i(program.uMask, 2);\n // Cover-fit center crop: scale the smaller axis to fit, offset to\n // center. Mirrors android/.../effects/BackgroundImageFactory.kt.\n const outAspect = w / h;\n const bgAspect = bg.width / bg.height;\n let bgScaleX: number;\n let bgScaleY: number;\n let bgOffsetX: number;\n let bgOffsetY: number;\n if (bgAspect > outAspect) {\n bgScaleX = outAspect / bgAspect;\n bgScaleY = 1.0;\n bgOffsetX = (1 - bgScaleX) * 0.5;\n bgOffsetY = 0;\n } else {\n bgScaleX = 1.0;\n bgScaleY = bgAspect / outAspect;\n bgOffsetX = 0;\n bgOffsetY = (1 - bgScaleY) * 0.5;\n }\n gl.uniform2f(program.uBgUvScale, bgScaleX, bgScaleY);\n gl.uniform2f(program.uBgUvOffset, bgOffsetX, bgOffsetY);\n // Mask V-flip via sampling uniforms; see comment on the upload above.\n gl.uniform2f(program.uMaskUvScale, 1, -1);\n gl.uniform2f(program.uMaskUvOffset, 0, 1);\n const [maskLo, maskHi] = maskSmoothstepRange(tuning.maskHardness, tuning.maskThreshold);\n gl.uniform1f(program.uMaskLo, maskLo);\n gl.uniform1f(program.uMaskHi, maskHi);\n gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);\n\n const out = new VideoFrame(canvas as unknown as CanvasImageSource, {\n timestamp: frame.timestamp,\n ...(frame.duration != null ? { duration: frame.duration } : {}),\n });\n frame.close();\n return out;\n };\n};\n"]}
|