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
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
//
|
|
10
10
|
// Shader source lives in src/web/shaders.ts; MediaPipe loader in
|
|
11
11
|
// src/web/segmenter.ts.
|
|
12
|
-
import { loadSegmenter } from '../segmenter';
|
|
12
|
+
import { getLatestMask, loadSegmenter, requestMaskIfIdle } from '../segmenter';
|
|
13
13
|
import { COMPOSITE_FRAG_SRC, PASSTHROUGH_VERT_SRC } from '../shaders';
|
|
14
14
|
import { maskSmoothstepRange, tuning } from '../tuning';
|
|
15
15
|
const compileShader = (gl, type, source) => {
|
|
@@ -45,6 +45,21 @@ const linkProgram = (gl, vertSrc, fragSrc) => {
|
|
|
45
45
|
}
|
|
46
46
|
return prog;
|
|
47
47
|
};
|
|
48
|
+
const linkCompositeProgram = (gl) => {
|
|
49
|
+
const prog = linkProgram(gl, PASSTHROUGH_VERT_SRC, COMPOSITE_FRAG_SRC);
|
|
50
|
+
return {
|
|
51
|
+
prog,
|
|
52
|
+
uOriginal: gl.getUniformLocation(prog, 'uOriginal'),
|
|
53
|
+
uBackground: gl.getUniformLocation(prog, 'uBackground'),
|
|
54
|
+
uMask: gl.getUniformLocation(prog, 'uMask'),
|
|
55
|
+
uBgUvScale: gl.getUniformLocation(prog, 'uBgUvScale'),
|
|
56
|
+
uBgUvOffset: gl.getUniformLocation(prog, 'uBgUvOffset'),
|
|
57
|
+
uMaskUvScale: gl.getUniformLocation(prog, 'uMaskUvScale'),
|
|
58
|
+
uMaskUvOffset: gl.getUniformLocation(prog, 'uMaskUvOffset'),
|
|
59
|
+
uMaskLo: gl.getUniformLocation(prog, 'uMaskLo'),
|
|
60
|
+
uMaskHi: gl.getUniformLocation(prog, 'uMaskHi'),
|
|
61
|
+
};
|
|
62
|
+
};
|
|
48
63
|
const createTexture = (gl, width, height) => {
|
|
49
64
|
const tex = gl.createTexture();
|
|
50
65
|
if (!tex)
|
|
@@ -64,6 +79,22 @@ const uploadTexture = (gl, tex, source, flipY) => {
|
|
|
64
79
|
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
|
|
65
80
|
};
|
|
66
81
|
const cache = new Map();
|
|
82
|
+
// SECURITY: `source` is consumer-supplied and may be an arbitrary URL or data
|
|
83
|
+
// URI (see BackgroundImageSpec.source). Two bounds keep an untrusted source
|
|
84
|
+
// from becoming a memory-pressure DoS:
|
|
85
|
+
// - MAX_BG_DIMENSION caps the RETAINED raster and the GPU upload so a
|
|
86
|
+
// "decompression bomb" (a small file that decodes to hundreds of
|
|
87
|
+
// megapixels) is downscaled rather than held full-size. A background is
|
|
88
|
+
// cover-fit anyway, so the cap costs nothing visible. NOTE: createImageBitmap
|
|
89
|
+
// decodes the full image before this downscale runs, so the transient decode
|
|
90
|
+
// peak is bounded by the browser's own createImageBitmap limits, not by this
|
|
91
|
+
// cap; the cap bounds sustained memory + GPU pressure, not the decode spike.
|
|
92
|
+
// - MAX_CACHE_ENTRIES bounds the per-source cache so a stream of distinct
|
|
93
|
+
// URLs cannot grow it without limit.
|
|
94
|
+
// Consumers wiring `source` from end-user input should still validate the URL
|
|
95
|
+
// themselves; this library does not fetch-allowlist.
|
|
96
|
+
const MAX_BG_DIMENSION = 4096;
|
|
97
|
+
const MAX_CACHE_ENTRIES = 32;
|
|
67
98
|
const loadImage = (source) => fetch(source, { mode: 'cors' })
|
|
68
99
|
.then((r) => {
|
|
69
100
|
if (!r.ok)
|
|
@@ -72,23 +103,36 @@ const loadImage = (source) => fetch(source, { mode: 'cors' })
|
|
|
72
103
|
})
|
|
73
104
|
.then((blob) => createImageBitmap(blob))
|
|
74
105
|
.then((bitmap) => {
|
|
75
|
-
const
|
|
106
|
+
const longSide = Math.max(bitmap.width, bitmap.height);
|
|
107
|
+
const scale = longSide > MAX_BG_DIMENSION ? MAX_BG_DIMENSION / longSide : 1;
|
|
108
|
+
const width = Math.max(1, Math.round(bitmap.width * scale));
|
|
109
|
+
const height = Math.max(1, Math.round(bitmap.height * scale));
|
|
110
|
+
const canvas = new OffscreenCanvas(width, height);
|
|
76
111
|
const ctx = canvas.getContext('2d');
|
|
77
112
|
if (!ctx)
|
|
78
113
|
throw new Error('kaleidoscope: bg OffscreenCanvas 2D context unavailable');
|
|
79
|
-
ctx.drawImage(bitmap, 0, 0);
|
|
80
|
-
const width = bitmap.width;
|
|
81
|
-
const height = bitmap.height;
|
|
114
|
+
ctx.drawImage(bitmap, 0, 0, width, height);
|
|
82
115
|
bitmap.close();
|
|
83
116
|
return { canvas, width, height };
|
|
84
117
|
});
|
|
85
|
-
|
|
118
|
+
// Free an entry's GL resources before dropping it from the cache.
|
|
119
|
+
const disposeEntry = (entry) => {
|
|
120
|
+
if (!entry.state)
|
|
121
|
+
return;
|
|
122
|
+
const { gl, program, textures } = entry.state;
|
|
123
|
+
gl.deleteProgram(program.prog);
|
|
124
|
+
gl.deleteTexture(textures.original);
|
|
125
|
+
gl.deleteTexture(textures.mask);
|
|
126
|
+
gl.deleteTexture(textures.background);
|
|
127
|
+
entry.state = null;
|
|
128
|
+
};
|
|
129
|
+
const ensureState = (entry, width, height, bg) => {
|
|
86
130
|
if (entry.state && entry.state.width === width && entry.state.height === height) {
|
|
87
131
|
return entry.state;
|
|
88
132
|
}
|
|
89
133
|
if (entry.state) {
|
|
90
134
|
const { gl } = entry.state;
|
|
91
|
-
gl.deleteProgram(entry.state.program);
|
|
135
|
+
gl.deleteProgram(entry.state.program.prog);
|
|
92
136
|
gl.deleteTexture(entry.state.textures.original);
|
|
93
137
|
gl.deleteTexture(entry.state.textures.mask);
|
|
94
138
|
gl.deleteTexture(entry.state.textures.background);
|
|
@@ -105,13 +149,17 @@ const ensureState = (entry, width, height) => {
|
|
|
105
149
|
canvas,
|
|
106
150
|
width,
|
|
107
151
|
height,
|
|
108
|
-
program:
|
|
152
|
+
program: linkCompositeProgram(gl),
|
|
109
153
|
textures: {
|
|
110
154
|
original: createTexture(gl, width, height),
|
|
111
155
|
mask: createTexture(gl, width, height),
|
|
112
156
|
background: createTexture(gl, width, height),
|
|
113
157
|
},
|
|
114
158
|
};
|
|
159
|
+
// Static background: upload once, here, not per frame. Re-runs only when
|
|
160
|
+
// this state is rebuilt (resolution change). flipY=true matches the
|
|
161
|
+
// original's DOM-coord → GL v=1 convention.
|
|
162
|
+
uploadTexture(gl, entry.state.textures.background, bg.canvas, true);
|
|
115
163
|
return entry.state;
|
|
116
164
|
};
|
|
117
165
|
const ensureInputCanvas = (entry, width, height) => {
|
|
@@ -130,6 +178,17 @@ const ensureInputCanvas = (entry, width, height) => {
|
|
|
130
178
|
export const makeBackgroundImage = (source) => {
|
|
131
179
|
let entry = cache.get(source);
|
|
132
180
|
if (!entry) {
|
|
181
|
+
// Bound the cache: evict the oldest entry (Map preserves insertion order)
|
|
182
|
+
// and free its GL resources before inserting a new source.
|
|
183
|
+
while (cache.size >= MAX_CACHE_ENTRIES) {
|
|
184
|
+
const oldestKey = cache.keys().next().value;
|
|
185
|
+
if (oldestKey === undefined)
|
|
186
|
+
break;
|
|
187
|
+
const oldest = cache.get(oldestKey);
|
|
188
|
+
if (oldest)
|
|
189
|
+
disposeEntry(oldest);
|
|
190
|
+
cache.delete(oldestKey);
|
|
191
|
+
}
|
|
133
192
|
entry = {
|
|
134
193
|
state: null,
|
|
135
194
|
imagePromise: loadImage(source),
|
|
@@ -140,43 +199,51 @@ export const makeBackgroundImage = (source) => {
|
|
|
140
199
|
}
|
|
141
200
|
const e = entry;
|
|
142
201
|
return async (frame) => {
|
|
143
|
-
const
|
|
202
|
+
const bg = await e.imagePromise;
|
|
203
|
+
await loadSegmenter(); // ensure loaded; cached after first call.
|
|
144
204
|
const w = frame.displayWidth;
|
|
145
205
|
const h = frame.displayHeight;
|
|
146
206
|
const inputCtx = ensureInputCanvas(e, w, h);
|
|
147
207
|
inputCtx.drawImage(frame, 0, 0, w, h);
|
|
148
208
|
const inputCanvas = e.inputCanvas2D;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
209
|
+
// Decoupled segmentation; see blur.ts for the rationale. Forward the
|
|
210
|
+
// original frame until the first mask lands.
|
|
211
|
+
requestMaskIfIdle(inputCanvas);
|
|
212
|
+
const results = getLatestMask();
|
|
213
|
+
if (!results) {
|
|
214
|
+
const passthrough = new VideoFrame(e.inputCanvas2D, {
|
|
215
|
+
timestamp: frame.timestamp,
|
|
216
|
+
...(frame.duration != null ? { duration: frame.duration } : {}),
|
|
217
|
+
});
|
|
218
|
+
frame.close();
|
|
219
|
+
return passthrough;
|
|
220
|
+
}
|
|
221
|
+
const s = ensureState(e, w, h, bg);
|
|
154
222
|
const { gl, canvas, program, textures } = s;
|
|
155
|
-
// Upload original
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
// via uMaskUvScale=(1,-1) / uMaskUvOffset=(0,1) set below.
|
|
223
|
+
// Upload original with flipY=true (DOM-coord → GL v=1 = top). The static
|
|
224
|
+
// background is uploaded once in ensureState (flipped the same way), not
|
|
225
|
+
// per frame. Mask is uploaded with flipY=false for the same reason
|
|
226
|
+
// (no-op on MediaPipe's segmentationMask source) AND to preserve the soft
|
|
227
|
+
// confidence values — canvas premultiplied-alpha math would collapse them
|
|
228
|
+
// to near-binary. The composite shader compensates via
|
|
229
|
+
// uMaskUvScale=(1,-1) / uMaskUvOffset=(0,1) set below.
|
|
163
230
|
uploadTexture(gl, textures.original, inputCanvas, true);
|
|
164
231
|
uploadTexture(gl, textures.mask, results.segmentationMask, false);
|
|
165
|
-
uploadTexture(gl, textures.background, bg.canvas, true);
|
|
166
232
|
gl.disable(gl.BLEND);
|
|
167
233
|
gl.disable(gl.DEPTH_TEST);
|
|
168
234
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
169
235
|
gl.viewport(0, 0, w, h);
|
|
170
|
-
gl.useProgram(
|
|
236
|
+
// biome-ignore lint/correctness/useHookAtTopLevel: gl.useProgram is a WebGL call, not a React hook (the early return above tripped the use* heuristic).
|
|
237
|
+
gl.useProgram(program.prog);
|
|
171
238
|
gl.activeTexture(gl.TEXTURE0);
|
|
172
239
|
gl.bindTexture(gl.TEXTURE_2D, textures.original);
|
|
173
|
-
gl.uniform1i(
|
|
240
|
+
gl.uniform1i(program.uOriginal, 0);
|
|
174
241
|
gl.activeTexture(gl.TEXTURE1);
|
|
175
242
|
gl.bindTexture(gl.TEXTURE_2D, textures.background);
|
|
176
|
-
gl.uniform1i(
|
|
243
|
+
gl.uniform1i(program.uBackground, 1);
|
|
177
244
|
gl.activeTexture(gl.TEXTURE2);
|
|
178
245
|
gl.bindTexture(gl.TEXTURE_2D, textures.mask);
|
|
179
|
-
gl.uniform1i(
|
|
246
|
+
gl.uniform1i(program.uMask, 2);
|
|
180
247
|
// Cover-fit center crop: scale the smaller axis to fit, offset to
|
|
181
248
|
// center. Mirrors android/.../effects/BackgroundImageFactory.kt.
|
|
182
249
|
const outAspect = w / h;
|
|
@@ -197,14 +264,14 @@ export const makeBackgroundImage = (source) => {
|
|
|
197
264
|
bgOffsetX = 0;
|
|
198
265
|
bgOffsetY = (1 - bgScaleY) * 0.5;
|
|
199
266
|
}
|
|
200
|
-
gl.uniform2f(
|
|
201
|
-
gl.uniform2f(
|
|
267
|
+
gl.uniform2f(program.uBgUvScale, bgScaleX, bgScaleY);
|
|
268
|
+
gl.uniform2f(program.uBgUvOffset, bgOffsetX, bgOffsetY);
|
|
202
269
|
// Mask V-flip via sampling uniforms; see comment on the upload above.
|
|
203
|
-
gl.uniform2f(
|
|
204
|
-
gl.uniform2f(
|
|
270
|
+
gl.uniform2f(program.uMaskUvScale, 1, -1);
|
|
271
|
+
gl.uniform2f(program.uMaskUvOffset, 0, 1);
|
|
205
272
|
const [maskLo, maskHi] = maskSmoothstepRange(tuning.maskHardness, tuning.maskThreshold);
|
|
206
|
-
gl.uniform1f(
|
|
207
|
-
gl.uniform1f(
|
|
273
|
+
gl.uniform1f(program.uMaskLo, maskLo);
|
|
274
|
+
gl.uniform1f(program.uMaskHi, maskHi);
|
|
208
275
|
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
|
209
276
|
const out = new VideoFrame(canvas, {
|
|
210
277
|
timestamp: frame.timestamp,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"background-image.js","sourceRoot":"","sources":["../../../src/web/effects/background-image.ts"],"names":[],"mappings":"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","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.js","sourceRoot":"","sources":["../../../src/web/effects/background-image.ts"],"names":[],"mappings":"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","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"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"blur.d.ts","sourceRoot":"","sources":["../../../src/web/effects/blur.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AA+J5D,eAAO,MAAM,IAAI,EAAE,cAoFlB,CAAC","sourcesContent":["// Web blur effect, WebGL2 pipeline.\n//\n// Same shape as the Android GLES 3.0 path: separable Gaussian blur into a\n// ping-pong FBO, then a composite pass that mixes blurred and original via\n// a segmentation mask. The mask still comes from MediaPipe Selfie\n// Segmentation loaded from CDN; there is no GPU-resident segmenter for\n// the web yet.\n//\n// Shader source lives in src/web/shaders.ts; MediaPipe loader in\n// src/web/segmenter.ts. This file owns only the per-effect GL state and\n// the per-frame transform.\n\nimport type { FrameTransform } from '../insertable-streams';\nimport { loadSegmenter, type SegmenterResults } from '../segmenter';\nimport { BLUR_FRAG_SRC, 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 programs: {\n blur: WebGLProgram;\n composite: WebGLProgram;\n };\n textures: {\n original: WebGLTexture;\n mask: WebGLTexture;\n blurA: WebGLTexture;\n blurB: WebGLTexture;\n };\n fbos: {\n blurA: WebGLFramebuffer;\n blurB: WebGLFramebuffer;\n };\n};\n\nlet state: GpuState | null = null;\n\n// MediaPipe needs a 2D canvas as input; we stage the VideoFrame there each\n// frame, then point both MediaPipe and the GL upload at it.\nlet inputCanvas2D: OffscreenCanvas | null = null;\nlet inputCtx2D: OffscreenCanvasRenderingContext2D | null = null;\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 createFbo = (gl: WebGL2RenderingContext, tex: WebGLTexture): WebGLFramebuffer => {\n const fbo = gl.createFramebuffer();\n if (!fbo) throw new Error('kaleidoscope: gl.createFramebuffer returned null');\n gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);\n gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);\n const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);\n if (status !== gl.FRAMEBUFFER_COMPLETE) {\n throw new Error(`kaleidoscope: FBO incomplete: 0x${status.toString(16)}`);\n }\n gl.bindFramebuffer(gl.FRAMEBUFFER, null);\n return fbo;\n};\n\nconst ensureState = (width: number, height: number): GpuState => {\n if (state && state.width === width && state.height === height) return state;\n\n // Tear down previous state if dimensions changed.\n if (state) {\n const { gl } = state;\n gl.deleteProgram(state.programs.blur);\n gl.deleteProgram(state.programs.composite);\n gl.deleteTexture(state.textures.original);\n gl.deleteTexture(state.textures.mask);\n gl.deleteTexture(state.textures.blurA);\n gl.deleteTexture(state.textures.blurB);\n gl.deleteFramebuffer(state.fbos.blurA);\n gl.deleteFramebuffer(state.fbos.blurB);\n }\n\n const canvas = 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('kaleidoscope: WebGL2 not available; blur requires a WebGL2-capable browser');\n }\n\n const programs = {\n blur: linkProgram(gl, PASSTHROUGH_VERT_SRC, BLUR_FRAG_SRC),\n composite: linkProgram(gl, PASSTHROUGH_VERT_SRC, COMPOSITE_FRAG_SRC),\n };\n const textures = {\n original: createTexture(gl, width, height),\n mask: createTexture(gl, width, height),\n blurA: createTexture(gl, width, height),\n blurB: createTexture(gl, width, height),\n };\n const fbos = {\n blurA: createFbo(gl, textures.blurA),\n blurB: createFbo(gl, textures.blurB),\n };\n\n state = { gl, canvas, width, height, programs, textures, fbos };\n return state;\n};\n\nconst ensureInputCanvas = (width: number, height: number): OffscreenCanvasRenderingContext2D => {\n if (!inputCanvas2D || inputCanvas2D.width !== width || inputCanvas2D.height !== height) {\n inputCanvas2D = new OffscreenCanvas(width, height);\n inputCtx2D = inputCanvas2D.getContext('2d');\n if (!inputCtx2D) throw new Error('kaleidoscope: OffscreenCanvas 2D context unavailable');\n }\n return inputCtx2D as OffscreenCanvasRenderingContext2D;\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\nexport const blur: FrameTransform = async (frame) => {\n const segmenter = await loadSegmenter();\n const w = frame.displayWidth;\n const h = frame.displayHeight;\n\n // Stage on a 2D canvas so MediaPipe can ingest it.\n const inputCtx = ensureInputCanvas(w, h);\n inputCtx.drawImage(frame, 0, 0, w, h);\n const inputCanvas = inputCanvas2D as unknown as CanvasImageSource;\n\n // Get the mask from MediaPipe.\n const results: SegmenterResults = await new Promise((resolve) => {\n segmenter.onResults((r) => resolve(r));\n void segmenter.send({ image: inputCanvas });\n });\n\n // Run the GPU pipeline.\n const s = ensureState(w, h);\n const { gl, canvas, programs, textures, fbos } = s;\n\n // Upload original with flipY=true (DOM-coord source → GL v=1 = top).\n // Upload mask with flipY=false: UNPACK_FLIP_Y_WEBGL is a no-op on\n // MediaPipe's segmentationMask (also on ImageBitmaps in general), so the\n // mask lands in its natural orientation. The composite shader compensates\n // with the uMaskUvScale=(1,-1) / uMaskUvOffset=(0,1) V-flip uniforms set\n // below; this also avoids the canvas-premultiplied-alpha problem that\n // collapses soft confidence values and makes the mask binary.\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 // Pass 1: horizontal blur of original -> blurA\n gl.bindFramebuffer(gl.FRAMEBUFFER, fbos.blurA);\n gl.viewport(0, 0, w, h);\n gl.useProgram(programs.blur);\n gl.activeTexture(gl.TEXTURE0);\n gl.bindTexture(gl.TEXTURE_2D, textures.original);\n gl.uniform1i(gl.getUniformLocation(programs.blur, 'uTex'), 0);\n gl.uniform2f(gl.getUniformLocation(programs.blur, 'uAxis'), 1 / w, 0);\n gl.uniform1f(gl.getUniformLocation(programs.blur, 'uSigma'), tuning.blurSigma);\n gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);\n\n // Pass 2: vertical blur of blurA -> blurB\n gl.bindFramebuffer(gl.FRAMEBUFFER, fbos.blurB);\n gl.viewport(0, 0, w, h);\n gl.bindTexture(gl.TEXTURE_2D, textures.blurA);\n gl.uniform2f(gl.getUniformLocation(programs.blur, 'uAxis'), 0, 1 / h);\n gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);\n\n // Pass 3: composite original + blurB + mask -> default framebuffer (canvas)\n gl.bindFramebuffer(gl.FRAMEBUFFER, null);\n gl.viewport(0, 0, w, h);\n gl.useProgram(programs.composite);\n gl.activeTexture(gl.TEXTURE0);\n gl.bindTexture(gl.TEXTURE_2D, textures.original);\n gl.uniform1i(gl.getUniformLocation(programs.composite, 'uOriginal'), 0);\n gl.activeTexture(gl.TEXTURE1);\n gl.bindTexture(gl.TEXTURE_2D, textures.blurB);\n gl.uniform1i(gl.getUniformLocation(programs.composite, 'uBackground'), 1);\n gl.activeTexture(gl.TEXTURE2);\n gl.bindTexture(gl.TEXTURE_2D, textures.mask);\n gl.uniform1i(gl.getUniformLocation(programs.composite, 'uMask'), 2);\n // Blurred background is full-size; identity UV transform.\n gl.uniform2f(gl.getUniformLocation(programs.composite, 'uBgUvScale'), 1, 1);\n gl.uniform2f(gl.getUniformLocation(programs.composite, 'uBgUvOffset'), 0, 0);\n // Mask V-flip: direct upload with flipY=false leaves mask Y-inverted\n // relative to the original. Encode the flip in the sampling uniforms so\n // the shader stays byte-identical with Android (which uses identity here).\n gl.uniform2f(gl.getUniformLocation(programs.composite, 'uMaskUvScale'), 1, -1);\n gl.uniform2f(gl.getUniformLocation(programs.composite, 'uMaskUvOffset'), 0, 1);\n const [maskLo, maskHi] = maskSmoothstepRange(tuning.maskHardness, tuning.maskThreshold);\n gl.uniform1f(gl.getUniformLocation(programs.composite, 'uMaskLo'), maskLo);\n gl.uniform1f(gl.getUniformLocation(programs.composite, 'uMaskHi'), maskHi);\n gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);\n\n // Wrap the canvas as an output VideoFrame.\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"]}
|
|
1
|
+
{"version":3,"file":"blur.d.ts","sourceRoot":"","sources":["../../../src/web/effects/blur.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AA+S5D,eAAO,MAAM,IAAI,EAAE,cAiHlB,CAAC","sourcesContent":["// Web blur effect, WebGL2 pipeline.\n//\n// Same shape as the Android GLES 3.0 path: separable Gaussian blur into a\n// ping-pong FBO, then a composite pass that mixes blurred and original via\n// a segmentation mask. The mask still comes from MediaPipe Selfie\n// Segmentation loaded from CDN; there is no GPU-resident segmenter for\n// the web yet.\n//\n// Shader source lives in src/web/shaders.ts; MediaPipe loader in\n// src/web/segmenter.ts. This file owns only the per-effect GL state and\n// the per-frame transform.\n\nimport { computeBlurKernel } from '../blur-kernel';\nimport type { FrameTransform } from '../insertable-streams';\nimport { getLatestMask, loadSegmenter, requestMaskIfIdle } from '../segmenter';\nimport { BLUR_FRAG_SRC, COMPOSITE_FRAG_SRC, PASSTHROUGH_VERT_SRC } from '../shaders';\nimport { maskSmoothstepRange, tuning } from '../tuning';\n\n// Cache the kernel by sigma; the pure math lives in ../blur-kernel. Recomputed\n// only when the blur slider (setBlurSigma) moves, not per frame.\nlet cachedKernelSigma = Number.NaN;\nlet cachedKernel: { weights: Float32Array; offsets: Float32Array } | null = null;\n\nconst blurKernel = (sigma: number): { weights: Float32Array; offsets: Float32Array } => {\n if (sigma !== cachedKernelSigma || cachedKernel === null) {\n cachedKernel = computeBlurKernel(sigma);\n cachedKernelSigma = sigma;\n }\n return cachedKernel;\n};\n\ntype GpuState = {\n gl: WebGL2RenderingContext;\n canvas: OffscreenCanvas;\n width: number;\n height: number;\n // Blur ping-pong buffers run at quarter area (half each axis, short side\n // floored at 256px); the composite's LINEAR sampler upscales for free.\n downW: number;\n downH: number;\n programs: {\n blur: BlurProgram;\n composite: CompositeProgram;\n };\n timer: PassTimer;\n textures: {\n original: WebGLTexture;\n mask: WebGLTexture;\n blurA: WebGLTexture;\n blurB: WebGLTexture;\n };\n fbos: {\n blurA: WebGLFramebuffer;\n blurB: WebGLFramebuffer;\n };\n};\n\nlet state: GpuState | null = null;\n\n// MediaPipe needs a 2D canvas as input; we stage the VideoFrame there each\n// frame, then point both MediaPipe and the GL upload at it.\nlet inputCanvas2D: OffscreenCanvas | null = null;\nlet inputCtx2D: OffscreenCanvasRenderingContext2D | null = null;\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\n// Uniform locations queried once at link, not via getUniformLocation per frame.\ntype BlurProgram = {\n prog: WebGLProgram;\n uTex: WebGLUniformLocation | null;\n uAxis: WebGLUniformLocation | null;\n uWeights: WebGLUniformLocation | null;\n uOffsets: WebGLUniformLocation | null;\n};\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\nconst linkBlurProgram = (gl: WebGL2RenderingContext): BlurProgram => {\n const prog = linkProgram(gl, PASSTHROUGH_VERT_SRC, BLUR_FRAG_SRC);\n return {\n prog,\n uTex: gl.getUniformLocation(prog, 'uTex'),\n uAxis: gl.getUniformLocation(prog, 'uAxis'),\n uWeights: gl.getUniformLocation(prog, 'uWeights'),\n uOffsets: gl.getUniformLocation(prog, 'uOffsets'),\n };\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\n// Per-pass GPU timing (dev only). Set\n// `globalThis.__KALEIDOSCOPE_DEBUG_TIMING__ = true` before the first blur frame\n// to log per-pass GPU time via EXT_disjoint_timer_query_webgl2. Results read\n// back one frame late (the query is async); a no-op when the flag/ext is absent.\nconst TIMING_ENABLED =\n (globalThis as { __KALEIDOSCOPE_DEBUG_TIMING__?: boolean }).__KALEIDOSCOPE_DEBUG_TIMING__ ===\n true;\n\ntype TimerExt = { TIME_ELAPSED_EXT: number; GPU_DISJOINT_EXT: number };\ntype PassTimer = { ext: TimerExt | null; pending: Map<string, WebGLQuery> };\n\nconst makePassTimer = (gl: WebGL2RenderingContext): PassTimer => ({\n ext: TIMING_ENABLED\n ? (gl.getExtension('EXT_disjoint_timer_query_webgl2') as TimerExt | null)\n : null,\n pending: new Map(),\n});\n\nconst timePass = (\n gl: WebGL2RenderingContext,\n timer: PassTimer,\n label: string,\n draw: () => void,\n): void => {\n const ext = timer.ext;\n if (!ext) {\n draw();\n return;\n }\n const inFlight = timer.pending.get(label);\n if (inFlight) {\n const available = gl.getQueryParameter(inFlight, gl.QUERY_RESULT_AVAILABLE) as boolean;\n const disjoint = gl.getParameter(ext.GPU_DISJOINT_EXT) as boolean;\n if (available && !disjoint) {\n const ns = gl.getQueryParameter(inFlight, gl.QUERY_RESULT) as number;\n console.debug(`[kaleidoscope] blur ${label}: ${(ns / 1e6).toFixed(3)} ms`);\n }\n if (available || disjoint) {\n gl.deleteQuery(inFlight);\n timer.pending.delete(label);\n }\n // A query is still in flight for this label; just draw, don't stack another.\n draw();\n return;\n }\n const query = gl.createQuery();\n if (!query) {\n draw();\n return;\n }\n gl.beginQuery(ext.TIME_ELAPSED_EXT, query);\n draw();\n gl.endQuery(ext.TIME_ELAPSED_EXT);\n timer.pending.set(label, query);\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 createFbo = (gl: WebGL2RenderingContext, tex: WebGLTexture): WebGLFramebuffer => {\n const fbo = gl.createFramebuffer();\n if (!fbo) throw new Error('kaleidoscope: gl.createFramebuffer returned null');\n gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);\n gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);\n const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);\n if (status !== gl.FRAMEBUFFER_COMPLETE) {\n throw new Error(`kaleidoscope: FBO incomplete: 0x${status.toString(16)}`);\n }\n gl.bindFramebuffer(gl.FRAMEBUFFER, null);\n return fbo;\n};\n\n// Quarter-area downscale for the blur ping-pong buffers: half each axis, short\n// side floored at 256px so small inputs don't degrade; never upscales. Shared\n// rule with the Android/iOS R1 commits.\nconst blurDownscale = (w: number, h: number): { downW: number; downH: number } => {\n const shortTarget = Math.max(256, Math.round(Math.min(w, h) * 0.5));\n const scale = Math.min(1, shortTarget / Math.min(w, h));\n return { downW: Math.max(1, Math.round(w * scale)), downH: Math.max(1, Math.round(h * scale)) };\n};\n\nconst ensureState = (width: number, height: number): GpuState => {\n if (state && state.width === width && state.height === height) return state;\n\n // Tear down previous state if dimensions changed.\n if (state) {\n const { gl } = state;\n gl.deleteProgram(state.programs.blur.prog);\n gl.deleteProgram(state.programs.composite.prog);\n for (const q of state.timer.pending.values()) gl.deleteQuery(q);\n gl.deleteTexture(state.textures.original);\n gl.deleteTexture(state.textures.mask);\n gl.deleteTexture(state.textures.blurA);\n gl.deleteTexture(state.textures.blurB);\n gl.deleteFramebuffer(state.fbos.blurA);\n gl.deleteFramebuffer(state.fbos.blurB);\n }\n\n const canvas = 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('kaleidoscope: WebGL2 not available; blur requires a WebGL2-capable browser');\n }\n\n const programs = {\n blur: linkBlurProgram(gl),\n composite: linkCompositeProgram(gl),\n };\n const { downW, downH } = blurDownscale(width, height);\n const textures = {\n original: createTexture(gl, width, height),\n mask: createTexture(gl, width, height),\n blurA: createTexture(gl, downW, downH),\n blurB: createTexture(gl, downW, downH),\n };\n const fbos = {\n blurA: createFbo(gl, textures.blurA),\n blurB: createFbo(gl, textures.blurB),\n };\n\n state = {\n gl,\n canvas,\n width,\n height,\n downW,\n downH,\n programs,\n timer: makePassTimer(gl),\n textures,\n fbos,\n };\n return state;\n};\n\nconst ensureInputCanvas = (width: number, height: number): OffscreenCanvasRenderingContext2D => {\n if (!inputCanvas2D || inputCanvas2D.width !== width || inputCanvas2D.height !== height) {\n inputCanvas2D = new OffscreenCanvas(width, height);\n inputCtx2D = inputCanvas2D.getContext('2d');\n if (!inputCtx2D) throw new Error('kaleidoscope: OffscreenCanvas 2D context unavailable');\n }\n return inputCtx2D as OffscreenCanvasRenderingContext2D;\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\nexport const blur: FrameTransform = async (frame) => {\n await loadSegmenter(); // ensure the segmenter is loaded; cached after first call.\n const w = frame.displayWidth;\n const h = frame.displayHeight;\n\n // Stage on a 2D canvas so MediaPipe can ingest it (and so the GL upload below\n // has a flippable source).\n const inputCtx = ensureInputCanvas(w, h);\n inputCtx.drawImage(frame, 0, 0, w, h);\n const inputCanvas = inputCanvas2D as unknown as CanvasImageSource;\n\n // Decoupled segmentation (mirrors native): never block the render on the\n // segmenter. Kick a fresh mask only when idle and draw with the most recent\n // one. drawImage is atomic w.r.t. the event loop, so MediaPipe reading the\n // canvas between tasks sees at most a ~1-frame-newer image — within the\n // staleness the mask already tolerates. Before the first mask lands, forward\n // the original frame (matches native's fall-through).\n requestMaskIfIdle(inputCanvas);\n const results = getLatestMask();\n if (!results) {\n const passthrough = new VideoFrame(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 // Run the GPU pipeline.\n const s = ensureState(w, h);\n const { gl, canvas, programs, timer, textures, fbos, downW, downH } = s;\n\n // Upload original with flipY=true (DOM-coord source → GL v=1 = top).\n // Upload mask with flipY=false: UNPACK_FLIP_Y_WEBGL is a no-op on\n // MediaPipe's segmentationMask (also on ImageBitmaps in general), so the\n // mask lands in its natural orientation. The composite shader compensates\n // with the uMaskUvScale=(1,-1) / uMaskUvOffset=(0,1) V-flip uniforms set\n // below; this also avoids the canvas-premultiplied-alpha problem that\n // collapses soft confidence values and makes the mask binary.\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 // Pass 0: downsample the full-res original into blurA. uAxis=0 collapses the\n // kernel to its center tap (weights sum to 1), so this is a plain bilinear\n // box-average into the downscaled target. Both blur passes must then run IN\n // downscaled space: sampling the full-res original directly with the\n // downscaled-spaced kernel offsets skips source columns and produces a\n // serrated, one-directional smear.\n gl.bindFramebuffer(gl.FRAMEBUFFER, fbos.blurA);\n gl.viewport(0, 0, downW, downH);\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(programs.blur.prog);\n gl.activeTexture(gl.TEXTURE0);\n gl.bindTexture(gl.TEXTURE_2D, textures.original);\n gl.uniform1i(programs.blur.uTex, 0);\n const { weights, offsets } = blurKernel(tuning.blurSigma);\n gl.uniform1fv(programs.blur.uWeights, weights);\n gl.uniform1fv(programs.blur.uOffsets, offsets);\n gl.uniform2f(programs.blur.uAxis, 0, 0);\n gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);\n\n // Pass 1: horizontal blur of the downscaled copy: blurA -> blurB.\n gl.bindFramebuffer(gl.FRAMEBUFFER, fbos.blurB);\n gl.viewport(0, 0, downW, downH);\n gl.bindTexture(gl.TEXTURE_2D, textures.blurA);\n gl.uniform2f(programs.blur.uAxis, 1 / downW, 0);\n timePass(gl, timer, 'h', () => gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4));\n\n // Pass 2: vertical blur: blurB -> blurA.\n gl.bindFramebuffer(gl.FRAMEBUFFER, fbos.blurA);\n gl.viewport(0, 0, downW, downH);\n gl.bindTexture(gl.TEXTURE_2D, textures.blurB);\n gl.uniform2f(programs.blur.uAxis, 0, 1 / downH);\n timePass(gl, timer, 'v', () => gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4));\n\n // Pass 3: composite original + blurB + mask -> default framebuffer (canvas)\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(programs.composite.prog);\n gl.activeTexture(gl.TEXTURE0);\n gl.bindTexture(gl.TEXTURE_2D, textures.original);\n gl.uniform1i(programs.composite.uOriginal, 0);\n gl.activeTexture(gl.TEXTURE1);\n // Final blurred result lives in blurA after the V pass (pass 2).\n gl.bindTexture(gl.TEXTURE_2D, textures.blurA);\n gl.uniform1i(programs.composite.uBackground, 1);\n gl.activeTexture(gl.TEXTURE2);\n gl.bindTexture(gl.TEXTURE_2D, textures.mask);\n gl.uniform1i(programs.composite.uMask, 2);\n // Blurred background is full-size; identity UV transform.\n gl.uniform2f(programs.composite.uBgUvScale, 1, 1);\n gl.uniform2f(programs.composite.uBgUvOffset, 0, 0);\n // Mask V-flip: direct upload with flipY=false leaves mask Y-inverted\n // relative to the original. Encode the flip in the sampling uniforms so\n // the shader stays byte-identical with Android (which uses identity here).\n gl.uniform2f(programs.composite.uMaskUvScale, 1, -1);\n gl.uniform2f(programs.composite.uMaskUvOffset, 0, 1);\n const [maskLo, maskHi] = maskSmoothstepRange(tuning.maskHardness, tuning.maskThreshold);\n gl.uniform1f(programs.composite.uMaskLo, maskLo);\n gl.uniform1f(programs.composite.uMaskHi, maskHi);\n timePass(gl, timer, 'composite', () => gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4));\n\n // Wrap the canvas as an output VideoFrame.\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"]}
|