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.
Files changed (301) hide show
  1. package/NOTICE.md +17 -6
  2. package/README.md +67 -19
  3. package/android/build.gradle +6 -5
  4. package/android/src/main/assets/backgrounds/dark-office.webp +0 -0
  5. package/android/src/main/assets/backgrounds/debug-resolutions.webp +0 -0
  6. package/android/src/main/assets/backgrounds/home-dark.webp +0 -0
  7. package/android/src/main/assets/backgrounds/home-light.webp +0 -0
  8. package/android/src/main/assets/backgrounds/light-office.webp +0 -0
  9. package/android/src/main/assets/backgrounds/nature-dark.webp +0 -0
  10. package/android/src/main/assets/backgrounds/nature-light.webp +0 -0
  11. package/android/src/main/assets/backgrounds/simiancraft-dark.webp +0 -0
  12. package/android/src/main/assets/backgrounds/simiancraft-light.webp +0 -0
  13. package/android/src/main/assets/backgrounds/stylized-dark.webp +0 -0
  14. package/android/src/main/assets/backgrounds/stylized-light.webp +0 -0
  15. package/android/src/main/assets/selfie_segmenter.tflite +0 -0
  16. package/android/src/main/java/com/simiancraft/kaleidoscope/EffectTuning.kt +41 -10
  17. package/android/src/main/java/com/simiancraft/kaleidoscope/KaleidoscopeModule.kt +8 -0
  18. package/android/src/main/java/com/simiancraft/kaleidoscope/Registration.kt +52 -13
  19. package/android/src/main/java/com/simiancraft/kaleidoscope/effects/BackgroundImageFactory.kt +53 -23
  20. package/android/src/main/java/com/simiancraft/kaleidoscope/effects/BlurFactory.kt +114 -50
  21. package/android/src/main/java/com/simiancraft/kaleidoscope/effects/TransformFactory.kt +277 -0
  22. package/android/src/main/java/com/simiancraft/kaleidoscope/gpu/FramePipeline.kt +158 -0
  23. package/android/src/main/java/com/simiancraft/kaleidoscope/gpu/GlProgram.kt +5 -0
  24. package/android/src/main/java/com/simiancraft/kaleidoscope/gpu/Ingest.kt +123 -0
  25. package/android/src/main/java/com/simiancraft/kaleidoscope/gpu/Orientation.kt +58 -0
  26. package/android/src/main/java/com/simiancraft/kaleidoscope/gpu/Shaders.kt +23 -51
  27. package/android/src/main/java/com/simiancraft/kaleidoscope/gpu/ShadersGenerated.kt +76 -0
  28. package/android/src/main/java/com/simiancraft/kaleidoscope/segmentation/Mask.kt +100 -89
  29. package/android/src/main/java/com/simiancraft/kaleidoscope/segmentation/SegmentationEngine.kt +148 -0
  30. package/app.plugin.js +254 -4
  31. package/dist/backgrounds/dark-office.d.ts +3 -0
  32. package/dist/backgrounds/dark-office.d.ts.map +1 -0
  33. package/dist/backgrounds/dark-office.js +5 -0
  34. package/dist/backgrounds/dark-office.js.map +1 -0
  35. package/dist/backgrounds/dark-office.web.d.ts +3 -0
  36. package/dist/backgrounds/dark-office.web.d.ts.map +1 -0
  37. package/dist/backgrounds/dark-office.web.js +8 -0
  38. package/dist/backgrounds/dark-office.web.js.map +1 -0
  39. package/dist/backgrounds/dark-office.webp +0 -0
  40. package/dist/backgrounds/debug-resolutions.d.ts +3 -0
  41. package/dist/backgrounds/debug-resolutions.d.ts.map +1 -0
  42. package/dist/backgrounds/debug-resolutions.js +6 -0
  43. package/dist/backgrounds/debug-resolutions.js.map +1 -0
  44. package/dist/backgrounds/debug-resolutions.web.d.ts +3 -0
  45. package/dist/backgrounds/debug-resolutions.web.d.ts.map +1 -0
  46. package/dist/backgrounds/debug-resolutions.web.js +8 -0
  47. package/dist/backgrounds/debug-resolutions.web.js.map +1 -0
  48. package/dist/backgrounds/debug-resolutions.webp +0 -0
  49. package/dist/backgrounds/home-dark.d.ts +3 -0
  50. package/dist/backgrounds/home-dark.d.ts.map +1 -0
  51. package/dist/backgrounds/home-dark.js +5 -0
  52. package/dist/backgrounds/home-dark.js.map +1 -0
  53. package/dist/backgrounds/home-dark.web.d.ts +3 -0
  54. package/dist/backgrounds/home-dark.web.d.ts.map +1 -0
  55. package/dist/backgrounds/home-dark.web.js +6 -0
  56. package/dist/backgrounds/home-dark.web.js.map +1 -0
  57. package/dist/backgrounds/home-dark.webp +0 -0
  58. package/dist/backgrounds/home-light.d.ts +3 -0
  59. package/dist/backgrounds/home-light.d.ts.map +1 -0
  60. package/dist/backgrounds/home-light.js +5 -0
  61. package/dist/backgrounds/home-light.js.map +1 -0
  62. package/dist/backgrounds/home-light.web.d.ts +3 -0
  63. package/dist/backgrounds/home-light.web.d.ts.map +1 -0
  64. package/dist/backgrounds/home-light.web.js +6 -0
  65. package/dist/backgrounds/home-light.web.js.map +1 -0
  66. package/dist/backgrounds/home-light.webp +0 -0
  67. package/dist/backgrounds/index.d.ts +3 -0
  68. package/dist/backgrounds/index.d.ts.map +1 -0
  69. package/dist/backgrounds/index.js +6 -0
  70. package/dist/backgrounds/index.js.map +1 -0
  71. package/dist/backgrounds/light-office.d.ts +3 -0
  72. package/dist/backgrounds/light-office.d.ts.map +1 -0
  73. package/dist/backgrounds/light-office.js +5 -0
  74. package/dist/backgrounds/light-office.js.map +1 -0
  75. package/dist/backgrounds/light-office.web.d.ts +3 -0
  76. package/dist/backgrounds/light-office.web.d.ts.map +1 -0
  77. package/dist/backgrounds/light-office.web.js +8 -0
  78. package/dist/backgrounds/light-office.web.js.map +1 -0
  79. package/dist/backgrounds/light-office.webp +0 -0
  80. package/dist/backgrounds/nature-dark.d.ts +3 -0
  81. package/dist/backgrounds/nature-dark.d.ts.map +1 -0
  82. package/dist/backgrounds/nature-dark.js +5 -0
  83. package/dist/backgrounds/nature-dark.js.map +1 -0
  84. package/dist/backgrounds/nature-dark.web.d.ts +3 -0
  85. package/dist/backgrounds/nature-dark.web.d.ts.map +1 -0
  86. package/dist/backgrounds/nature-dark.web.js +6 -0
  87. package/dist/backgrounds/nature-dark.web.js.map +1 -0
  88. package/dist/backgrounds/nature-dark.webp +0 -0
  89. package/dist/backgrounds/nature-light.d.ts +3 -0
  90. package/dist/backgrounds/nature-light.d.ts.map +1 -0
  91. package/dist/backgrounds/nature-light.js +5 -0
  92. package/dist/backgrounds/nature-light.js.map +1 -0
  93. package/dist/backgrounds/nature-light.web.d.ts +3 -0
  94. package/dist/backgrounds/nature-light.web.d.ts.map +1 -0
  95. package/dist/backgrounds/nature-light.web.js +6 -0
  96. package/dist/backgrounds/nature-light.web.js.map +1 -0
  97. package/dist/backgrounds/nature-light.webp +0 -0
  98. package/dist/backgrounds/preset-source.types.d.ts +2 -0
  99. package/dist/backgrounds/preset-source.types.d.ts.map +1 -0
  100. package/dist/backgrounds/preset-source.types.js +2 -0
  101. package/dist/backgrounds/preset-source.types.js.map +1 -0
  102. package/dist/backgrounds/presets.d.ts +3 -0
  103. package/dist/backgrounds/presets.d.ts.map +1 -0
  104. package/dist/backgrounds/presets.js +34 -0
  105. package/dist/backgrounds/presets.js.map +1 -0
  106. package/dist/backgrounds/simiancraft-dark.d.ts +3 -0
  107. package/dist/backgrounds/simiancraft-dark.d.ts.map +1 -0
  108. package/dist/backgrounds/simiancraft-dark.js +5 -0
  109. package/dist/backgrounds/simiancraft-dark.js.map +1 -0
  110. package/dist/backgrounds/simiancraft-dark.web.d.ts +3 -0
  111. package/dist/backgrounds/simiancraft-dark.web.d.ts.map +1 -0
  112. package/dist/backgrounds/simiancraft-dark.web.js +6 -0
  113. package/dist/backgrounds/simiancraft-dark.web.js.map +1 -0
  114. package/dist/backgrounds/simiancraft-dark.webp +0 -0
  115. package/dist/backgrounds/simiancraft-light.d.ts +3 -0
  116. package/dist/backgrounds/simiancraft-light.d.ts.map +1 -0
  117. package/dist/backgrounds/simiancraft-light.js +5 -0
  118. package/dist/backgrounds/simiancraft-light.js.map +1 -0
  119. package/dist/backgrounds/simiancraft-light.web.d.ts +3 -0
  120. package/dist/backgrounds/simiancraft-light.web.d.ts.map +1 -0
  121. package/dist/backgrounds/simiancraft-light.web.js +6 -0
  122. package/dist/backgrounds/simiancraft-light.web.js.map +1 -0
  123. package/dist/backgrounds/simiancraft-light.webp +0 -0
  124. package/dist/backgrounds/stylized-dark.d.ts +3 -0
  125. package/dist/backgrounds/stylized-dark.d.ts.map +1 -0
  126. package/dist/backgrounds/stylized-dark.js +5 -0
  127. package/dist/backgrounds/stylized-dark.js.map +1 -0
  128. package/dist/backgrounds/stylized-dark.web.d.ts +3 -0
  129. package/dist/backgrounds/stylized-dark.web.d.ts.map +1 -0
  130. package/dist/backgrounds/stylized-dark.web.js +6 -0
  131. package/dist/backgrounds/stylized-dark.web.js.map +1 -0
  132. package/dist/backgrounds/stylized-dark.webp +0 -0
  133. package/dist/backgrounds/stylized-light.d.ts +3 -0
  134. package/dist/backgrounds/stylized-light.d.ts.map +1 -0
  135. package/dist/backgrounds/stylized-light.js +5 -0
  136. package/dist/backgrounds/stylized-light.js.map +1 -0
  137. package/dist/backgrounds/stylized-light.web.d.ts +3 -0
  138. package/dist/backgrounds/stylized-light.web.d.ts.map +1 -0
  139. package/dist/backgrounds/stylized-light.web.js +6 -0
  140. package/dist/backgrounds/stylized-light.web.js.map +1 -0
  141. package/dist/backgrounds/stylized-light.webp +0 -0
  142. package/dist/index.d.ts +19 -5
  143. package/dist/index.d.ts.map +1 -1
  144. package/dist/index.js +71 -22
  145. package/dist/index.js.map +1 -1
  146. package/dist/index.web.d.ts +25 -3
  147. package/dist/index.web.d.ts.map +1 -1
  148. package/dist/index.web.js +55 -16
  149. package/dist/index.web.js.map +1 -1
  150. package/dist/livekit.d.ts +24 -0
  151. package/dist/livekit.d.ts.map +1 -0
  152. package/dist/livekit.js +57 -0
  153. package/dist/livekit.js.map +1 -0
  154. package/dist/types.d.ts +20 -13
  155. package/dist/types.d.ts.map +1 -1
  156. package/dist/types.js +0 -3
  157. package/dist/types.js.map +1 -1
  158. package/dist/web/blur-kernel.d.ts +6 -0
  159. package/dist/web/blur-kernel.d.ts.map +1 -0
  160. package/dist/web/blur-kernel.js +41 -0
  161. package/dist/web/blur-kernel.js.map +1 -0
  162. package/dist/web/effects/background-image.d.ts.map +1 -1
  163. package/dist/web/effects/background-image.js +100 -33
  164. package/dist/web/effects/background-image.js.map +1 -1
  165. package/dist/web/effects/blur.d.ts.map +1 -1
  166. package/dist/web/effects/blur.js +169 -38
  167. package/dist/web/effects/blur.js.map +1 -1
  168. package/dist/web/effects/transform.d.ts +4 -0
  169. package/dist/web/effects/transform.d.ts.map +1 -0
  170. package/dist/web/effects/transform.js +62 -0
  171. package/dist/web/effects/transform.js.map +1 -0
  172. package/dist/web/insertable-streams.d.ts +11 -1
  173. package/dist/web/insertable-streams.d.ts.map +1 -1
  174. package/dist/web/insertable-streams.js +22 -4
  175. package/dist/web/insertable-streams.js.map +1 -1
  176. package/dist/web/segmenter.d.ts +9 -0
  177. package/dist/web/segmenter.d.ts.map +1 -1
  178. package/dist/web/segmenter.js +32 -0
  179. package/dist/web/segmenter.js.map +1 -1
  180. package/dist/web/shaders.d.ts +1 -3
  181. package/dist/web/shaders.d.ts.map +1 -1
  182. package/dist/web/shaders.generated.d.ts +4 -0
  183. package/dist/web/shaders.generated.d.ts.map +1 -0
  184. package/dist/web/shaders.generated.js +54 -0
  185. package/dist/web/shaders.generated.js.map +1 -0
  186. package/dist/web/shaders.js +29 -103
  187. package/dist/web/shaders.js.map +1 -1
  188. package/dist/web/tuning.d.ts +4 -0
  189. package/dist/web/tuning.d.ts.map +1 -1
  190. package/dist/web/tuning.js +17 -5
  191. package/dist/web/tuning.js.map +1 -1
  192. package/ios/Kaleidoscope.podspec +87 -14
  193. package/ios/KaleidoscopeModule/EffectTuning.swift +47 -7
  194. package/ios/KaleidoscopeModule/KaleidoscopeModule.swift +12 -0
  195. package/ios/KaleidoscopeModule/Registration.swift +78 -13
  196. package/ios/KaleidoscopeModule/effects/BackgroundImageProcessor.swift +293 -0
  197. package/ios/KaleidoscopeModule/effects/BlurProcessor.swift +232 -18
  198. package/ios/KaleidoscopeModule/effects/FrameBridge.swift +46 -0
  199. package/ios/KaleidoscopeModule/effects/TransformProcessor.swift +173 -0
  200. package/ios/KaleidoscopeModule/gpu/Ingest.swift +169 -0
  201. package/ios/KaleidoscopeModule/gpu/MetalRenderer.swift +601 -0
  202. package/ios/KaleidoscopeModule/gpu/Orientation.swift +83 -0
  203. package/ios/KaleidoscopeModule/gpu/ShaderLibrary.swift +100 -0
  204. package/ios/KaleidoscopeModule/gpu/TextureBridge.swift +291 -0
  205. package/ios/KaleidoscopeModule/resources/backgrounds/dark-office.webp +0 -0
  206. package/ios/KaleidoscopeModule/resources/backgrounds/debug-resolutions.webp +0 -0
  207. package/ios/KaleidoscopeModule/resources/backgrounds/home-dark.webp +0 -0
  208. package/ios/KaleidoscopeModule/resources/backgrounds/home-light.webp +0 -0
  209. package/ios/KaleidoscopeModule/resources/backgrounds/light-office.webp +0 -0
  210. package/ios/KaleidoscopeModule/resources/backgrounds/nature-dark.webp +0 -0
  211. package/ios/KaleidoscopeModule/resources/backgrounds/nature-light.webp +0 -0
  212. package/ios/KaleidoscopeModule/resources/backgrounds/simiancraft-dark.webp +0 -0
  213. package/ios/KaleidoscopeModule/resources/backgrounds/simiancraft-light.webp +0 -0
  214. package/ios/KaleidoscopeModule/resources/backgrounds/stylized-dark.webp +0 -0
  215. package/ios/KaleidoscopeModule/resources/backgrounds/stylized-light.webp +0 -0
  216. package/ios/KaleidoscopeModule/resources/selfie_segmenter.tflite +0 -0
  217. package/ios/KaleidoscopeModule/segmentation/MaskTuning.swift +23 -0
  218. package/ios/KaleidoscopeModule/segmentation/Segmenter.swift +444 -25
  219. package/ios/KaleidoscopeModule/shaders/SHADERS.txt +6 -0
  220. package/ios/KaleidoscopeModule/shaders/blur.metalsrc +72 -0
  221. package/ios/KaleidoscopeModule/shaders/composite.metalsrc +22 -0
  222. package/ios/KaleidoscopeModule/shaders/nebula.metalsrc +72 -0
  223. package/ios/KaleidoscopeModule/shaders/passthrough.metalsrc +20 -0
  224. package/ios/KaleidoscopeModule/shaders/simianlights.metalsrc +72 -0
  225. package/ios/KaleidoscopeModule/shaders/transform.metalsrc +22 -0
  226. package/package.json +120 -11
  227. package/src/backgrounds/README.md +83 -0
  228. package/src/backgrounds/assets.d.ts +7 -0
  229. package/src/backgrounds/dark-office.ts +6 -0
  230. package/src/backgrounds/dark-office.web.ts +9 -0
  231. package/src/backgrounds/dark-office.webp +0 -0
  232. package/src/backgrounds/debug-resolutions.ts +7 -0
  233. package/src/backgrounds/debug-resolutions.web.ts +9 -0
  234. package/src/backgrounds/debug-resolutions.webp +0 -0
  235. package/src/backgrounds/home-dark.ts +6 -0
  236. package/src/backgrounds/home-dark.web.ts +7 -0
  237. package/src/backgrounds/home-dark.webp +0 -0
  238. package/src/backgrounds/home-light.ts +6 -0
  239. package/src/backgrounds/home-light.web.ts +7 -0
  240. package/src/backgrounds/home-light.webp +0 -0
  241. package/src/backgrounds/index.ts +7 -0
  242. package/src/backgrounds/light-office.ts +6 -0
  243. package/src/backgrounds/light-office.web.ts +9 -0
  244. package/src/backgrounds/light-office.webp +0 -0
  245. package/src/backgrounds/nature-dark.ts +6 -0
  246. package/src/backgrounds/nature-dark.web.ts +7 -0
  247. package/src/backgrounds/nature-dark.webp +0 -0
  248. package/src/backgrounds/nature-light.ts +6 -0
  249. package/src/backgrounds/nature-light.web.ts +7 -0
  250. package/src/backgrounds/nature-light.webp +0 -0
  251. package/src/backgrounds/preset-source.types.ts +5 -0
  252. package/src/backgrounds/presets.ts +36 -0
  253. package/src/backgrounds/simiancraft-dark.ts +6 -0
  254. package/src/backgrounds/simiancraft-dark.web.ts +7 -0
  255. package/src/backgrounds/simiancraft-dark.webp +0 -0
  256. package/src/backgrounds/simiancraft-light.ts +6 -0
  257. package/src/backgrounds/simiancraft-light.web.ts +7 -0
  258. package/src/backgrounds/simiancraft-light.webp +0 -0
  259. package/src/backgrounds/stylized-dark.ts +6 -0
  260. package/src/backgrounds/stylized-dark.web.ts +7 -0
  261. package/src/backgrounds/stylized-dark.webp +0 -0
  262. package/src/backgrounds/stylized-light.ts +6 -0
  263. package/src/backgrounds/stylized-light.web.ts +7 -0
  264. package/src/backgrounds/stylized-light.webp +0 -0
  265. package/src/index.ts +94 -29
  266. package/src/index.web.ts +69 -19
  267. package/src/livekit.ts +65 -0
  268. package/src/types.ts +23 -14
  269. package/src/web/blur-kernel.ts +44 -0
  270. package/src/web/effects/background-image.ts +121 -34
  271. package/src/web/effects/blur.ts +214 -40
  272. package/src/web/effects/transform.ts +69 -0
  273. package/src/web/insertable-streams.ts +33 -5
  274. package/src/web/segmenter.ts +34 -0
  275. package/src/web/shaders.generated.ts +56 -0
  276. package/src/web/shaders.ts +29 -106
  277. package/src/web/tuning.ts +19 -5
  278. package/android/src/main/assets/backgrounds/office-1.png +0 -0
  279. package/android/src/main/assets/backgrounds/office-2.png +0 -0
  280. package/android/src/main/java/com/simiancraft/kaleidoscope/effects/MirrorFactory.kt +0 -57
  281. package/android/src/main/java/com/simiancraft/kaleidoscope/gpu/GpuEffectFactory.kt +0 -14
  282. package/android/src/main/java/com/simiancraft/kaleidoscope/gpu/GpuEffectProcessor.kt +0 -198
  283. package/dist/backgrounds.d.ts +0 -3
  284. package/dist/backgrounds.d.ts.map +0 -1
  285. package/dist/backgrounds.js +0 -21
  286. package/dist/backgrounds.js.map +0 -1
  287. package/dist/web/effects/mirror.d.ts +0 -3
  288. package/dist/web/effects/mirror.d.ts.map +0 -1
  289. package/dist/web/effects/mirror.js +0 -31
  290. package/dist/web/effects/mirror.js.map +0 -1
  291. package/dist/web/effects/passthrough.d.ts +0 -3
  292. package/dist/web/effects/passthrough.d.ts.map +0 -1
  293. package/dist/web/effects/passthrough.js +0 -15
  294. package/dist/web/effects/passthrough.js.map +0 -1
  295. package/ios/KaleidoscopeModule/effects/MirrorProcessor.swift +0 -17
  296. package/plugin/build/withKaleidoscope.d.ts +0 -3
  297. package/plugin/build/withKaleidoscope.js +0 -14
  298. package/plugin/build/withKaleidoscope.js.map +0 -1
  299. package/src/backgrounds.ts +0 -23
  300. package/src/web/effects/mirror.ts +0 -37
  301. 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 canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
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
- const ensureState = (entry, width, height) => {
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: linkProgram(gl, PASSTHROUGH_VERT_SRC, COMPOSITE_FRAG_SRC),
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 [segmenter, bg] = await Promise.all([loadSegmenter(), e.imagePromise]);
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
- const results = await new Promise((resolve) => {
150
- segmenter.onResults((r) => resolve(r));
151
- void segmenter.send({ image: inputCanvas });
152
- });
153
- const s = ensureState(e, w, h);
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 and bg with flipY=true (DOM-coord → GL v=1 = top).
156
- // Bg is staged through OffscreenCanvas (loaded at bg-image load time)
157
- // because UNPACK_FLIP_Y_WEBGL is a no-op on raw ImageBitmaps; canvas
158
- // sources flip correctly. Mask is uploaded with flipY=false for the
159
- // same reason (no-op on MediaPipe's segmentationMask source) AND to
160
- // preserve the soft confidence values canvas premultiplied-alpha math
161
- // would collapse them to near-binary. The composite shader compensates
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(program);
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(gl.getUniformLocation(program, 'uOriginal'), 0);
240
+ gl.uniform1i(program.uOriginal, 0);
174
241
  gl.activeTexture(gl.TEXTURE1);
175
242
  gl.bindTexture(gl.TEXTURE_2D, textures.background);
176
- gl.uniform1i(gl.getUniformLocation(program, 'uBackground'), 1);
243
+ gl.uniform1i(program.uBackground, 1);
177
244
  gl.activeTexture(gl.TEXTURE2);
178
245
  gl.bindTexture(gl.TEXTURE_2D, textures.mask);
179
- gl.uniform1i(gl.getUniformLocation(program, 'uMask'), 2);
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(gl.getUniformLocation(program, 'uBgUvScale'), bgScaleX, bgScaleY);
201
- gl.uniform2f(gl.getUniformLocation(program, 'uBgUvOffset'), bgOffsetX, bgOffsetY);
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(gl.getUniformLocation(program, 'uMaskUvScale'), 1, -1);
204
- gl.uniform2f(gl.getUniformLocation(program, 'uMaskUvOffset'), 0, 1);
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(gl.getUniformLocation(program, 'uMaskLo'), maskLo);
207
- gl.uniform1f(gl.getUniformLocation(program, 'uMaskHi'), maskHi);
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"]}