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
@@ -1,35 +1,43 @@
1
- // Mask production: async MLKit Selfie Segmentation on a worker thread,
2
- // last-known-mask cache, mask uploaded to a 2D GL texture for the composite
3
- // shader to sample.
1
+ // Mask production: GL-side adapter around the process-wide SegmentationEngine.
2
+ //
3
+ // This class owns the per-processor GL state (downsample FBO/program, mask
4
+ // texture) and the per-stream temporal-smoothing (EMA) state. The actual
5
+ // segmentation (the worker thread + the MediaPipe ImageSegmenter) lives in the
6
+ // shared SegmentationEngine, so constructing a new Mask per effect switch does
7
+ // NOT spin up a new thread or segmenter; see SegmentationEngine for why.
4
8
  //
5
9
  // Per-frame flow on the GL thread:
6
10
  // 1. If the worker produced a new mask bitmap since the last frame,
7
11
  // upload it to the cached mask GL texture.
8
12
  // 2. If no segmentation is currently in flight, render a small downsample
9
- // snapshot of the input, post it to the worker, set isProcessing=true.
13
+ // snapshot of the input and submit it to the SegmentationEngine.
10
14
  // 3. Return the current mask GL texture handle (or -1 if no mask has
11
15
  // completed yet — caller falls through to the original frame).
12
16
  //
13
- // The worker thread is the bottleneck (~20-50 ms per MLKit call); decoupling
14
- // it from the frame thread keeps render at the camera's frame rate while
15
- // the mask updates ~10-20 Hz. One frame of latency on mask updates is
16
- // acceptable for this use case.
17
+ // The engine hands the raw foreground-confidence mask back (on its worker
18
+ // thread) via packMask(), which applies EMA smoothing, flips the mask back into
19
+ // the bottom-up orientation the upload/composite expects (the engine segmented
20
+ // an upright copy), quantizes to 8-bit, and stages the result for upload.
21
+ //
22
+ // RESIDUAL LEAK (bounded, documented): because upstream rebuilds the processor
23
+ // per effect switch with no teardown hook, the dropped processor's GL resources
24
+ // are not freed until the EGL context is destroyed (camera stop). That includes
25
+ // this Mask's texture/FBO/program AND the dropped processor's own state (the
26
+ // blur ping-pong FBOs/programs, the YuvConverter). All of it is bounded by the
27
+ // number of switches in a session and small per item; the unbounded thread/
28
+ // segmenter accumulation that would actually OOM is gone (it moved to the
29
+ // process-lived SegmentationEngine).
17
30
  //
18
31
  // All failure paths log under Kaleidoscope.Mask and return -1 (or a stale
19
32
  // mask if one was previously computed).
20
33
 
21
34
  package com.simiancraft.kaleidoscope.segmentation
22
35
 
36
+ import android.content.Context
23
37
  import android.graphics.Bitmap
24
38
  import android.opengl.GLES30
25
- import android.os.Handler
26
- import android.os.HandlerThread
27
39
  import android.util.Log
28
- import com.google.android.gms.tasks.Tasks
29
- import com.google.mlkit.vision.common.InputImage
30
- import com.google.mlkit.vision.segmentation.Segmentation
31
- import com.google.mlkit.vision.segmentation.Segmenter
32
- import com.google.mlkit.vision.segmentation.selfie.SelfieSegmenterOptions
40
+ import com.simiancraft.kaleidoscope.EffectTuning
33
41
  import com.simiancraft.kaleidoscope.gpu.Fbo
34
42
  import com.simiancraft.kaleidoscope.gpu.GlDebug
35
43
  import com.simiancraft.kaleidoscope.gpu.GlProgram
@@ -39,9 +47,7 @@ import java.nio.ByteOrder
39
47
  import java.util.concurrent.atomic.AtomicBoolean
40
48
  import java.util.concurrent.atomic.AtomicReference
41
49
 
42
- internal class Mask {
43
- private var segmenter: Segmenter? = null
44
-
50
+ internal class Mask(private val context: Context) {
45
51
  // Cached small-FBO state for the downsample pass.
46
52
  private var downsampleFbo: Fbo? = null
47
53
  private var downsampleProgram: GlProgram? = null
@@ -51,14 +57,8 @@ internal class Mask {
51
57
  private var maskTexWidth: Int = 0
52
58
  private var maskTexHeight: Int = 0
53
59
 
54
- // Worker thread for blocking MLKit calls. Started lazily on first frame.
55
- private val workerThread: HandlerThread = HandlerThread("Kaleidoscope.MaskWorker").apply {
56
- start()
57
- }
58
- private val workerHandler: Handler = Handler(workerThread.looper)
59
-
60
60
  // Throttle to a single in-flight segmentation at a time. Set true on
61
- // kickoff (GL thread), reset false when the worker finishes.
61
+ // kickoff (GL thread), reset false when the engine reports done.
62
62
  private val isProcessing = AtomicBoolean(false)
63
63
 
64
64
  // Worker -> GL thread handoff: a Bitmap ready to upload as the mask
@@ -70,17 +70,24 @@ internal class Mask {
70
70
  // Pre-allocated readback buffer (resized only on input-dim change).
71
71
  private var pixelByteBuffer: ByteBuffer? = null
72
72
 
73
+ // Temporal-smoothing (EMA) state: the previous smoothed confidence buffer and
74
+ // its dims. Touched only on the SegmentationEngine worker thread (packMask),
75
+ // which is single-threaded, so no locking. (If a future change ever reads
76
+ // these off that thread, e.g. on the GL thread, they would need @Volatile.)
77
+ private var smoothedMask: FloatArray? = null
78
+ private var smoothedMaskW: Int = 0
79
+ private var smoothedMaskH: Int = 0
80
+
73
81
  /**
74
- * Per-frame mask production. Always returns immediately (no MLKit blocking
75
- * on the GL thread). Returns the GL texture handle of the latest available
76
- * mask, or -1 if no segmentation has completed yet. Callers must treat -1
77
- * as "no mask this frame" and fall through to the original frame.
82
+ * Per-frame mask production. Always returns immediately (no segmentation
83
+ * blocking on the GL thread). Returns the GL texture handle of the latest
84
+ * available mask, or -1 if no segmentation has completed yet. Callers must
85
+ * treat -1 as "no mask this frame" and fall through to the original frame.
78
86
  */
79
87
  fun produce(
80
88
  source2D: Int,
81
89
  sourceWidth: Int,
82
90
  sourceHeight: Int,
83
- downsampleSize: Int = 256,
84
91
  ): Int {
85
92
  // Step 1: drain any pending mask the worker has produced. getAndSet
86
93
  // claims the bitmap atomically; the GL thread is now its sole owner.
@@ -98,9 +105,15 @@ internal class Mask {
98
105
  // Step 2: kick off a new segmentation if the worker is idle.
99
106
  if (isProcessing.compareAndSet(false, true)) {
100
107
  try {
101
- val downsampleBmp = renderAndReadback(source2D, sourceWidth, sourceHeight, downsampleSize)
108
+ val downsampleBmp =
109
+ renderAndReadback(source2D, sourceWidth, sourceHeight, EffectTuning.targetShortSide)
102
110
  if (downsampleBmp != null) {
103
- workerHandler.post { runSegmentation(downsampleBmp) }
111
+ SegmentationEngine.submit(
112
+ downsampleBmp,
113
+ context,
114
+ onMask = { raw, w, h -> packMask(raw, w, h) },
115
+ onDone = { isProcessing.set(false) },
116
+ )
104
117
  } else {
105
118
  isProcessing.set(false)
106
119
  }
@@ -114,13 +127,14 @@ internal class Mask {
114
127
  }
115
128
 
116
129
  /**
117
- * Release MLKit + worker thread + GL resources. Call from the GL thread.
118
- * Not currently invoked by any caller because VideoFrameProcessor has no
119
- * explicit teardown hook; worker thread leaks for the app's lifetime.
130
+ * Release this Mask's GL resources. Call from the GL thread. Does NOT touch
131
+ * the SegmentationEngine's worker thread or segmenter (those are process-
132
+ * lived and shared). Not currently invoked by any caller because
133
+ * VideoFrameProcessor has no explicit teardown hook; see the file header on
134
+ * the bounded GL leak this implies.
120
135
  */
121
136
  fun release() {
122
137
  try {
123
- workerThread.quitSafely()
124
138
  if (maskTextureId != 0) {
125
139
  GLES30.glDeleteTextures(1, intArrayOf(maskTextureId), 0)
126
140
  maskTextureId = 0
@@ -129,14 +143,57 @@ internal class Mask {
129
143
  downsampleFbo = null
130
144
  downsampleProgram?.delete()
131
145
  downsampleProgram = null
132
- segmenter?.close()
133
- segmenter = null
134
146
  pendingMaskBitmap.getAndSet(null)?.recycle()
147
+ smoothedMask = null
135
148
  } catch (t: Throwable) {
136
149
  Log.w(TAG, "Mask.release encountered an error; resources may leak", t)
137
150
  }
138
151
  }
139
152
 
153
+ // --- Worker thread (invoked by SegmentationEngine) -----------------------
154
+
155
+ /**
156
+ * Apply EMA smoothing to the raw upright confidence mask, flip it back into
157
+ * the bottom-up orientation the upload/composite expects, quantize to 8-bit
158
+ * RGBA, and stage it for the GL thread to upload. Runs on the engine's single
159
+ * worker thread, so the EMA state below needs no locking.
160
+ */
161
+ private fun packMask(raw: FloatArray, maskW: Int, maskH: Int) {
162
+ val pixelCount = maskW * maskH
163
+
164
+ // Temporal smoothing (exponential moving average) across mask updates, to
165
+ // damp shoulder-popping / edge shimmer. History resets on dim change.
166
+ val prevSmoothed = smoothedMask
167
+ val blend = prevSmoothed != null && smoothedMaskW == maskW && smoothedMaskH == maskH
168
+ val smoothed = if (blend) prevSmoothed!! else FloatArray(pixelCount)
169
+
170
+ val outPixels = IntArray(pixelCount)
171
+ for (i in 0 until pixelCount) {
172
+ val r = raw[i].coerceIn(0f, 1f)
173
+ val s = if (blend) MASK_EMA_ALPHA * r + (1f - MASK_EMA_ALPHA) * smoothed[i] else r
174
+ smoothed[i] = s
175
+ val c = (s * 255f + 0.5f).toInt() and 0xFF
176
+ // Flip vertically back into the orientation the upload/composite expects
177
+ // (the engine segmented an upright copy). smoothedMask stays in upright
178
+ // space for frame-to-frame EMA consistency; only the output is flipped.
179
+ val row = i / maskW
180
+ val col = i - row * maskW
181
+ outPixels[(maskH - 1 - row) * maskW + col] = (0xFF shl 24) or (c shl 16) or (c shl 8) or c
182
+ }
183
+ smoothedMask = smoothed
184
+ smoothedMaskW = maskW
185
+ smoothedMaskH = maskH
186
+
187
+ val outBmp = Bitmap.createBitmap(maskW, maskH, Bitmap.Config.ARGB_8888)
188
+ outBmp.setPixels(outPixels, 0, maskW, 0, 0, maskW, maskH)
189
+
190
+ // Hand off to GL thread. getAndSet atomically claims any previously
191
+ // unconsumed bitmap as `prev` so we own the recycle; the GL thread
192
+ // can never observe the same reference we are about to free.
193
+ val prev = pendingMaskBitmap.getAndSet(outBmp)
194
+ prev?.recycle()
195
+ }
196
+
140
197
  // --- GL thread -----------------------------------------------------------
141
198
 
142
199
  private fun renderAndReadback(
@@ -184,59 +241,8 @@ internal class Mask {
184
241
  GlDebug.check("mask upload texImage2D")
185
242
  }
186
243
 
187
- // --- Worker thread -------------------------------------------------------
188
-
189
- private fun runSegmentation(inputBmp: Bitmap) {
190
- try {
191
- val seg = ensureSegmenter()
192
- val inputImage = InputImage.fromBitmap(inputBmp, 0)
193
- val rawMask = Tasks.await(seg.process(inputImage))
194
-
195
- val maskBuffer = rawMask.buffer.order(ByteOrder.nativeOrder()).asFloatBuffer()
196
- val maskW = rawMask.width
197
- val maskH = rawMask.height
198
-
199
- val outPixels = IntArray(maskW * maskH)
200
- maskBuffer.rewind()
201
- for (i in 0 until maskW * maskH) {
202
- val c = (maskBuffer.get().coerceIn(0f, 1f) * 255f + 0.5f).toInt() and 0xFF
203
- outPixels[i] = (0xFF shl 24) or (c shl 16) or (c shl 8) or c
204
- }
205
-
206
- val outBmp = Bitmap.createBitmap(maskW, maskH, Bitmap.Config.ARGB_8888)
207
- outBmp.setPixels(outPixels, 0, maskW, 0, 0, maskW, maskH)
208
-
209
- // Hand off to GL thread. getAndSet atomically claims any previously
210
- // unconsumed bitmap as `prev` so we own the recycle; the GL thread
211
- // can never observe the same reference we are about to free.
212
- val prev = pendingMaskBitmap.getAndSet(outBmp)
213
- prev?.recycle()
214
- } catch (t: Throwable) {
215
- Log.e(TAG, "runSegmentation failed on worker", t)
216
- } finally {
217
- inputBmp.recycle()
218
- isProcessing.set(false)
219
- }
220
- }
221
-
222
244
  // --- Lazy init helpers ---------------------------------------------------
223
245
 
224
- private fun ensureSegmenter(): Segmenter {
225
- val existing = segmenter
226
- if (existing != null) return existing
227
- val seg = Segmentation.getClient(
228
- SelfieSegmenterOptions.Builder()
229
- .setDetectorMode(SelfieSegmenterOptions.STREAM_MODE)
230
- // Raw model resolution; MLKit returns the mask at the segmenter's
231
- // native size instead of upsampling internally. Faster per call;
232
- // the composite shader's smoothstep softens the coarser edge.
233
- .enableRawSizeMask()
234
- .build(),
235
- )
236
- segmenter = seg
237
- return seg
238
- }
239
-
240
246
  private fun ensureDownsampleFbo(w: Int, h: Int): Fbo {
241
247
  val existing = downsampleFbo
242
248
  if (existing != null && existing.width == w && existing.height == h) return existing
@@ -285,6 +291,11 @@ internal class Mask {
285
291
  companion object {
286
292
  private const val TAG = "Kaleidoscope.Mask"
287
293
 
294
+ // EMA weight for the new mask vs history. Higher = more responsive, lower =
295
+ // smoother (more lag). 0.5 is ~a 1-2 update time constant at the ~10-20 Hz
296
+ // mask rate: damps flicker without obvious lag.
297
+ private const val MASK_EMA_ALPHA = 0.5f
298
+
288
299
  private const val TWO_D_PASSTHROUGH_FRAG = """#version 300 es
289
300
  precision mediump float;
290
301
  uniform sampler2D uTex;
@@ -0,0 +1,148 @@
1
+ // Process-wide segmentation worker. Owns the ONE MediaPipe Tasks
2
+ // ImageSegmenter and the ONE worker thread for the whole process; every Mask
3
+ // instance (one per active VideoFrameProcessor) submits through here instead of
4
+ // spinning up its own thread + segmenter.
5
+ //
6
+ // WHY THIS IS A SINGLETON: react-native-webrtc calls VideoFrameProcessorFactory
7
+ // .build() on EVERY effect switch from JS, constructing a fresh processor (and
8
+ // thus a fresh Mask) each time, and upstream has no teardown hook to dispose
9
+ // the old one. If each Mask owned a HandlerThread + ImageSegmenter, toggling
10
+ // effects would leak a thread and a native segmenter handle per switch and
11
+ // eventually OOM on a constrained device. Hoisting the thread + segmenter here
12
+ // bounds them to one-per-process regardless of how many Masks come and go.
13
+ //
14
+ // The segmenter is CPU (BitmapImageBuilder input), so sharing it across Masks
15
+ // is safe; VIDEO running mode only needs monotonically increasing timestamps,
16
+ // which a single shared counter guarantees. Per-stream temporal state (the EMA
17
+ // smoothing) stays on each Mask, so two simultaneous tracks do not blend masks.
18
+ //
19
+ // The thread and segmenter are created lazily on first submit and live for the
20
+ // process lifetime (the intended steady state: one thread, one segmenter). They
21
+ // are intentionally never torn down.
22
+
23
+ package com.simiancraft.kaleidoscope.segmentation
24
+
25
+ import android.content.Context
26
+ import android.graphics.Bitmap
27
+ import android.graphics.Matrix
28
+ import android.os.Handler
29
+ import android.os.HandlerThread
30
+ import android.util.Log
31
+ import com.google.mediapipe.framework.image.BitmapImageBuilder
32
+ import com.google.mediapipe.framework.image.ByteBufferExtractor
33
+ import com.google.mediapipe.framework.image.MPImage
34
+ import com.google.mediapipe.tasks.core.BaseOptions
35
+ import com.google.mediapipe.tasks.vision.core.RunningMode
36
+ import com.google.mediapipe.tasks.vision.imagesegmenter.ImageSegmenter
37
+ import java.nio.ByteBuffer
38
+ import java.nio.ByteOrder
39
+
40
+ internal object SegmentationEngine {
41
+ private const val TAG = "Kaleidoscope.SegEngine"
42
+
43
+ // Lazy, process-lived. Created on first submit, never torn down.
44
+ private val workerThread: HandlerThread by lazy {
45
+ HandlerThread("Kaleidoscope.MaskWorker").also { it.start() }
46
+ }
47
+ private val workerHandler: Handler by lazy { Handler(workerThread.looper) }
48
+
49
+ // Worker-thread-confined state.
50
+ private var segmenter: ImageSegmenter? = null
51
+ private var videoTimestamp: Long = 0
52
+
53
+ /**
54
+ * Run segmentation for [inputBmp] off the GL thread. [inputBmp] is the
55
+ * downsampled, bottom-up GL readback; this uprights it (the selfie model is
56
+ * trained on upright people and does its worst on an inverted one), segments
57
+ * it, and hands the raw foreground-confidence mask back via [onMask] in the
58
+ * SAME upright orientation the caller's EMA/flip-back expects.
59
+ *
60
+ * [onMask] is invoked on the worker thread only on success. [onDone] is
61
+ * invoked on the worker thread exactly once, success or failure, so the
62
+ * caller can reset its in-flight throttle. [inputBmp] is recycled here.
63
+ */
64
+ fun submit(
65
+ inputBmp: Bitmap,
66
+ context: Context,
67
+ onMask: (rawUpright: FloatArray, width: Int, height: Int) -> Unit,
68
+ onDone: () -> Unit,
69
+ ) {
70
+ workerHandler.post {
71
+ var upright: Bitmap? = null
72
+ var mpImage: MPImage? = null
73
+ try {
74
+ val seg = ensureSegmenter(context)
75
+ // The GL readback (glReadPixels reads bottom-to-top) is vertically
76
+ // flipped (head-down). Feed the segmenter an upright copy; the caller
77
+ // flips the mask back so the upload/composite alignment is untouched.
78
+ // Vertical flip only, not a 180 rotate (columns are already correct).
79
+ upright = flipVertical(inputBmp)
80
+ mpImage = BitmapImageBuilder(upright).build()
81
+ val result = seg.segmentForVideo(mpImage, videoTimestamp++)
82
+
83
+ val confidenceMasks = result.confidenceMasks()
84
+ if (!confidenceMasks.isPresent || confidenceMasks.get().isEmpty()) {
85
+ Log.w(TAG, "segmentation produced no confidence mask")
86
+ return@post
87
+ }
88
+ // General selfie segmenter: a single foreground-confidence mask
89
+ // (float [0,1], higher = person). Copy it out of MediaPipe's buffer
90
+ // into a plain array the caller owns.
91
+ val maskImage = confidenceMasks.get()[0]
92
+ val maskW = maskImage.width
93
+ val maskH = maskImage.height
94
+ val maskBuffer = ByteBufferExtractor.extract(maskImage)
95
+ .order(ByteOrder.nativeOrder())
96
+ .asFloatBuffer()
97
+ val raw = FloatArray(maskW * maskH)
98
+ maskBuffer.rewind()
99
+ maskBuffer.get(raw)
100
+ onMask(raw, maskW, maskH)
101
+ } catch (t: Throwable) {
102
+ Log.e(TAG, "segmentation failed on worker", t)
103
+ } finally {
104
+ // close() in finally: if segmentForVideo throws, the MPImage native
105
+ // handle would otherwise leak (unbounded if the error recurs per frame).
106
+ mpImage?.close()
107
+ upright?.recycle()
108
+ inputBmp.recycle()
109
+ onDone()
110
+ }
111
+ }
112
+ }
113
+
114
+ private fun ensureSegmenter(context: Context): ImageSegmenter {
115
+ val existing = segmenter
116
+ if (existing != null) return existing
117
+ // Load the model as a direct ByteBuffer (setModelAssetBuffer) rather than
118
+ // setModelAssetPath: the path variant memory-maps the asset and requires it
119
+ // to be stored uncompressed (aaptOptions noCompress), which we cannot
120
+ // guarantee in the CONSUMING app's build. Reading the asset into a direct
121
+ // buffer ourselves works regardless of apk compression.
122
+ val appContext = context.applicationContext
123
+ val modelBytes = appContext.assets.open("selfie_segmenter.tflite").use { it.readBytes() }
124
+ val modelBuffer = ByteBuffer.allocateDirect(modelBytes.size).order(ByteOrder.nativeOrder())
125
+ modelBuffer.put(modelBytes)
126
+ modelBuffer.rewind()
127
+
128
+ val baseOptions = BaseOptions.builder()
129
+ .setModelAssetBuffer(modelBuffer)
130
+ .build()
131
+ val options = ImageSegmenter.ImageSegmenterOptions.builder()
132
+ .setBaseOptions(baseOptions)
133
+ .setRunningMode(RunningMode.VIDEO)
134
+ .setOutputConfidenceMasks(true)
135
+ .setOutputCategoryMask(false)
136
+ .build()
137
+ val seg = ImageSegmenter.createFromOptions(appContext, options)
138
+ segmenter = seg
139
+ return seg
140
+ }
141
+
142
+ /** Vertical mirror (flip across the horizontal axis). Uprights the
143
+ * bottom-to-top glReadPixels frame before segmentation. */
144
+ private fun flipVertical(src: Bitmap): Bitmap {
145
+ val m = Matrix().apply { postScale(1f, -1f) }
146
+ return Bitmap.createBitmap(src, 0, 0, src.width, src.height, m, true)
147
+ }
148
+ }