react-native-webrtc-kaleidoscope 1.1.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 (286) hide show
  1. package/NOTICE.md +17 -6
  2. package/README.md +39 -18
  3. package/android/build.gradle +3 -4
  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 +10 -0
  27. package/android/src/main/java/com/simiancraft/kaleidoscope/gpu/ShadersGenerated.kt +15 -3
  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/dist/backgrounds/dark-office.d.ts +3 -0
  31. package/dist/backgrounds/dark-office.d.ts.map +1 -0
  32. package/dist/backgrounds/dark-office.js +5 -0
  33. package/dist/backgrounds/dark-office.js.map +1 -0
  34. package/dist/backgrounds/dark-office.web.d.ts +3 -0
  35. package/dist/backgrounds/dark-office.web.d.ts.map +1 -0
  36. package/dist/backgrounds/{office-1.web.js → dark-office.web.js} +3 -3
  37. package/dist/backgrounds/dark-office.web.js.map +1 -0
  38. package/dist/backgrounds/dark-office.webp +0 -0
  39. package/dist/backgrounds/debug-resolutions.d.ts +3 -0
  40. package/dist/backgrounds/debug-resolutions.d.ts.map +1 -0
  41. package/dist/backgrounds/debug-resolutions.js +6 -0
  42. package/dist/backgrounds/debug-resolutions.js.map +1 -0
  43. package/dist/backgrounds/debug-resolutions.web.d.ts +3 -0
  44. package/dist/backgrounds/debug-resolutions.web.d.ts.map +1 -0
  45. package/dist/backgrounds/debug-resolutions.web.js +8 -0
  46. package/dist/backgrounds/debug-resolutions.web.js.map +1 -0
  47. package/dist/backgrounds/debug-resolutions.webp +0 -0
  48. package/dist/backgrounds/home-dark.d.ts +3 -0
  49. package/dist/backgrounds/home-dark.d.ts.map +1 -0
  50. package/dist/backgrounds/{office-1.js → home-dark.js} +3 -3
  51. package/dist/backgrounds/home-dark.js.map +1 -0
  52. package/dist/backgrounds/home-dark.web.d.ts +3 -0
  53. package/dist/backgrounds/home-dark.web.d.ts.map +1 -0
  54. package/dist/backgrounds/home-dark.web.js +6 -0
  55. package/dist/backgrounds/home-dark.web.js.map +1 -0
  56. package/dist/backgrounds/home-dark.webp +0 -0
  57. package/dist/backgrounds/home-light.d.ts +3 -0
  58. package/dist/backgrounds/home-light.d.ts.map +1 -0
  59. package/dist/backgrounds/{office-2.js → home-light.js} +3 -3
  60. package/dist/backgrounds/home-light.js.map +1 -0
  61. package/dist/backgrounds/home-light.web.d.ts +3 -0
  62. package/dist/backgrounds/home-light.web.d.ts.map +1 -0
  63. package/dist/backgrounds/home-light.web.js +6 -0
  64. package/dist/backgrounds/home-light.web.js.map +1 -0
  65. package/dist/backgrounds/home-light.webp +0 -0
  66. package/dist/backgrounds/index.d.ts.map +1 -1
  67. package/dist/backgrounds/index.js +1 -1
  68. package/dist/backgrounds/index.js.map +1 -1
  69. package/dist/backgrounds/light-office.d.ts +3 -0
  70. package/dist/backgrounds/light-office.d.ts.map +1 -0
  71. package/dist/backgrounds/light-office.js +5 -0
  72. package/dist/backgrounds/light-office.js.map +1 -0
  73. package/dist/backgrounds/light-office.web.d.ts +3 -0
  74. package/dist/backgrounds/light-office.web.d.ts.map +1 -0
  75. package/dist/backgrounds/{office-2.web.js → light-office.web.js} +3 -3
  76. package/dist/backgrounds/light-office.web.js.map +1 -0
  77. package/dist/backgrounds/light-office.webp +0 -0
  78. package/dist/backgrounds/nature-dark.d.ts +3 -0
  79. package/dist/backgrounds/nature-dark.d.ts.map +1 -0
  80. package/dist/backgrounds/nature-dark.js +5 -0
  81. package/dist/backgrounds/nature-dark.js.map +1 -0
  82. package/dist/backgrounds/nature-dark.web.d.ts +3 -0
  83. package/dist/backgrounds/nature-dark.web.d.ts.map +1 -0
  84. package/dist/backgrounds/nature-dark.web.js +6 -0
  85. package/dist/backgrounds/nature-dark.web.js.map +1 -0
  86. package/dist/backgrounds/nature-dark.webp +0 -0
  87. package/dist/backgrounds/nature-light.d.ts +3 -0
  88. package/dist/backgrounds/nature-light.d.ts.map +1 -0
  89. package/dist/backgrounds/nature-light.js +5 -0
  90. package/dist/backgrounds/nature-light.js.map +1 -0
  91. package/dist/backgrounds/nature-light.web.d.ts +3 -0
  92. package/dist/backgrounds/nature-light.web.d.ts.map +1 -0
  93. package/dist/backgrounds/nature-light.web.js +6 -0
  94. package/dist/backgrounds/nature-light.web.js.map +1 -0
  95. package/dist/backgrounds/nature-light.webp +0 -0
  96. package/dist/backgrounds/presets.d.ts +1 -1
  97. package/dist/backgrounds/presets.d.ts.map +1 -1
  98. package/dist/backgrounds/presets.js +22 -9
  99. package/dist/backgrounds/presets.js.map +1 -1
  100. package/dist/backgrounds/simiancraft-dark.d.ts +3 -0
  101. package/dist/backgrounds/simiancraft-dark.d.ts.map +1 -0
  102. package/dist/backgrounds/simiancraft-dark.js +5 -0
  103. package/dist/backgrounds/simiancraft-dark.js.map +1 -0
  104. package/dist/backgrounds/simiancraft-dark.web.d.ts +3 -0
  105. package/dist/backgrounds/simiancraft-dark.web.d.ts.map +1 -0
  106. package/dist/backgrounds/simiancraft-dark.web.js +6 -0
  107. package/dist/backgrounds/simiancraft-dark.web.js.map +1 -0
  108. package/dist/backgrounds/simiancraft-dark.webp +0 -0
  109. package/dist/backgrounds/simiancraft-light.d.ts +3 -0
  110. package/dist/backgrounds/simiancraft-light.d.ts.map +1 -0
  111. package/dist/backgrounds/simiancraft-light.js +5 -0
  112. package/dist/backgrounds/simiancraft-light.js.map +1 -0
  113. package/dist/backgrounds/simiancraft-light.web.d.ts +3 -0
  114. package/dist/backgrounds/simiancraft-light.web.d.ts.map +1 -0
  115. package/dist/backgrounds/simiancraft-light.web.js +6 -0
  116. package/dist/backgrounds/simiancraft-light.web.js.map +1 -0
  117. package/dist/backgrounds/simiancraft-light.webp +0 -0
  118. package/dist/backgrounds/stylized-dark.d.ts +3 -0
  119. package/dist/backgrounds/stylized-dark.d.ts.map +1 -0
  120. package/dist/backgrounds/stylized-dark.js +5 -0
  121. package/dist/backgrounds/stylized-dark.js.map +1 -0
  122. package/dist/backgrounds/stylized-dark.web.d.ts +3 -0
  123. package/dist/backgrounds/stylized-dark.web.d.ts.map +1 -0
  124. package/dist/backgrounds/stylized-dark.web.js +6 -0
  125. package/dist/backgrounds/stylized-dark.web.js.map +1 -0
  126. package/dist/backgrounds/stylized-dark.webp +0 -0
  127. package/dist/backgrounds/stylized-light.d.ts +3 -0
  128. package/dist/backgrounds/stylized-light.d.ts.map +1 -0
  129. package/dist/backgrounds/stylized-light.js +5 -0
  130. package/dist/backgrounds/stylized-light.js.map +1 -0
  131. package/dist/backgrounds/stylized-light.web.d.ts +3 -0
  132. package/dist/backgrounds/stylized-light.web.d.ts.map +1 -0
  133. package/dist/backgrounds/stylized-light.web.js +6 -0
  134. package/dist/backgrounds/stylized-light.web.js.map +1 -0
  135. package/dist/backgrounds/stylized-light.webp +0 -0
  136. package/dist/index.d.ts +19 -5
  137. package/dist/index.d.ts.map +1 -1
  138. package/dist/index.js +54 -27
  139. package/dist/index.js.map +1 -1
  140. package/dist/index.web.d.ts +14 -2
  141. package/dist/index.web.d.ts.map +1 -1
  142. package/dist/index.web.js +25 -9
  143. package/dist/index.web.js.map +1 -1
  144. package/dist/types.d.ts +20 -13
  145. package/dist/types.d.ts.map +1 -1
  146. package/dist/types.js +0 -3
  147. package/dist/types.js.map +1 -1
  148. package/dist/web/blur-kernel.d.ts +6 -0
  149. package/dist/web/blur-kernel.d.ts.map +1 -0
  150. package/dist/web/blur-kernel.js +41 -0
  151. package/dist/web/blur-kernel.js.map +1 -0
  152. package/dist/web/effects/background-image.d.ts.map +1 -1
  153. package/dist/web/effects/background-image.js +100 -33
  154. package/dist/web/effects/background-image.js.map +1 -1
  155. package/dist/web/effects/blur.d.ts.map +1 -1
  156. package/dist/web/effects/blur.js +163 -65
  157. package/dist/web/effects/blur.js.map +1 -1
  158. package/dist/web/effects/transform.d.ts +4 -0
  159. package/dist/web/effects/transform.d.ts.map +1 -0
  160. package/dist/web/effects/transform.js +62 -0
  161. package/dist/web/effects/transform.js.map +1 -0
  162. package/dist/web/segmenter.d.ts +9 -0
  163. package/dist/web/segmenter.d.ts.map +1 -1
  164. package/dist/web/segmenter.js +32 -0
  165. package/dist/web/segmenter.js.map +1 -1
  166. package/dist/web/shaders.d.ts.map +1 -1
  167. package/dist/web/shaders.generated.d.ts +1 -1
  168. package/dist/web/shaders.generated.d.ts.map +1 -1
  169. package/dist/web/shaders.generated.js +3 -3
  170. package/dist/web/shaders.generated.js.map +1 -1
  171. package/dist/web/shaders.js +1 -1
  172. package/dist/web/shaders.js.map +1 -1
  173. package/dist/web/tuning.d.ts +4 -0
  174. package/dist/web/tuning.d.ts.map +1 -1
  175. package/dist/web/tuning.js +17 -5
  176. package/dist/web/tuning.js.map +1 -1
  177. package/ios/Kaleidoscope.podspec +13 -3
  178. package/ios/KaleidoscopeModule/EffectTuning.swift +47 -7
  179. package/ios/KaleidoscopeModule/KaleidoscopeModule.swift +12 -0
  180. package/ios/KaleidoscopeModule/Registration.swift +49 -6
  181. package/ios/KaleidoscopeModule/effects/BackgroundImageProcessor.swift +100 -33
  182. package/ios/KaleidoscopeModule/effects/BlurProcessor.swift +91 -31
  183. package/ios/KaleidoscopeModule/effects/FrameBridge.swift +11 -5
  184. package/ios/KaleidoscopeModule/effects/TransformProcessor.swift +173 -0
  185. package/ios/KaleidoscopeModule/gpu/Ingest.swift +169 -0
  186. package/ios/KaleidoscopeModule/gpu/MetalRenderer.swift +234 -56
  187. package/ios/KaleidoscopeModule/gpu/Orientation.swift +83 -0
  188. package/ios/KaleidoscopeModule/gpu/TextureBridge.swift +183 -12
  189. package/ios/KaleidoscopeModule/resources/backgrounds/dark-office.webp +0 -0
  190. package/ios/KaleidoscopeModule/resources/backgrounds/debug-resolutions.webp +0 -0
  191. package/ios/KaleidoscopeModule/resources/backgrounds/home-dark.webp +0 -0
  192. package/ios/KaleidoscopeModule/resources/backgrounds/home-light.webp +0 -0
  193. package/ios/KaleidoscopeModule/resources/backgrounds/light-office.webp +0 -0
  194. package/ios/KaleidoscopeModule/resources/backgrounds/nature-dark.webp +0 -0
  195. package/ios/KaleidoscopeModule/resources/backgrounds/nature-light.webp +0 -0
  196. package/ios/KaleidoscopeModule/resources/backgrounds/simiancraft-dark.webp +0 -0
  197. package/ios/KaleidoscopeModule/resources/backgrounds/simiancraft-light.webp +0 -0
  198. package/ios/KaleidoscopeModule/resources/backgrounds/stylized-dark.webp +0 -0
  199. package/ios/KaleidoscopeModule/resources/backgrounds/stylized-light.webp +0 -0
  200. package/ios/KaleidoscopeModule/resources/selfie_segmenter.tflite +0 -0
  201. package/ios/KaleidoscopeModule/segmentation/Segmenter.swift +379 -56
  202. package/ios/KaleidoscopeModule/shaders/SHADERS.txt +1 -0
  203. package/ios/KaleidoscopeModule/shaders/blur.metalsrc +2 -2
  204. package/ios/KaleidoscopeModule/shaders/transform.metalsrc +22 -0
  205. package/package.json +93 -19
  206. package/src/backgrounds/README.md +15 -10
  207. package/src/backgrounds/dark-office.ts +6 -0
  208. package/src/backgrounds/{office-2.web.ts → dark-office.web.ts} +2 -2
  209. package/src/backgrounds/dark-office.webp +0 -0
  210. package/src/backgrounds/debug-resolutions.ts +7 -0
  211. package/src/backgrounds/debug-resolutions.web.ts +9 -0
  212. package/src/backgrounds/debug-resolutions.webp +0 -0
  213. package/src/backgrounds/{office-1.ts → home-dark.ts} +2 -2
  214. package/src/backgrounds/home-dark.web.ts +7 -0
  215. package/src/backgrounds/home-dark.webp +0 -0
  216. package/src/backgrounds/{office-2.ts → home-light.ts} +2 -2
  217. package/src/backgrounds/home-light.web.ts +7 -0
  218. package/src/backgrounds/home-light.webp +0 -0
  219. package/src/backgrounds/index.ts +1 -1
  220. package/src/backgrounds/light-office.ts +6 -0
  221. package/src/backgrounds/{office-1.web.ts → light-office.web.ts} +2 -2
  222. package/src/backgrounds/light-office.webp +0 -0
  223. package/src/backgrounds/nature-dark.ts +6 -0
  224. package/src/backgrounds/nature-dark.web.ts +7 -0
  225. package/src/backgrounds/nature-dark.webp +0 -0
  226. package/src/backgrounds/nature-light.ts +6 -0
  227. package/src/backgrounds/nature-light.web.ts +7 -0
  228. package/src/backgrounds/nature-light.webp +0 -0
  229. package/src/backgrounds/presets.ts +22 -9
  230. package/src/backgrounds/simiancraft-dark.ts +6 -0
  231. package/src/backgrounds/simiancraft-dark.web.ts +7 -0
  232. package/src/backgrounds/simiancraft-dark.webp +0 -0
  233. package/src/backgrounds/simiancraft-light.ts +6 -0
  234. package/src/backgrounds/simiancraft-light.web.ts +7 -0
  235. package/src/backgrounds/simiancraft-light.webp +0 -0
  236. package/src/backgrounds/stylized-dark.ts +6 -0
  237. package/src/backgrounds/stylized-dark.web.ts +7 -0
  238. package/src/backgrounds/stylized-dark.webp +0 -0
  239. package/src/backgrounds/stylized-light.ts +6 -0
  240. package/src/backgrounds/stylized-light.web.ts +7 -0
  241. package/src/backgrounds/stylized-light.webp +0 -0
  242. package/src/index.ts +64 -27
  243. package/src/index.web.ts +29 -11
  244. package/src/types.ts +23 -14
  245. package/src/web/blur-kernel.ts +44 -0
  246. package/src/web/effects/background-image.ts +121 -34
  247. package/src/web/effects/blur.ts +206 -67
  248. package/src/web/effects/transform.ts +69 -0
  249. package/src/web/segmenter.ts +34 -0
  250. package/src/web/shaders.generated.ts +3 -3
  251. package/src/web/shaders.ts +1 -1
  252. package/src/web/tuning.ts +19 -5
  253. package/android/src/main/assets/backgrounds/office-1.png +0 -0
  254. package/android/src/main/assets/backgrounds/office-2.png +0 -0
  255. package/android/src/main/java/com/simiancraft/kaleidoscope/effects/MirrorFactory.kt +0 -57
  256. package/android/src/main/java/com/simiancraft/kaleidoscope/gpu/GpuEffectFactory.kt +0 -14
  257. package/android/src/main/java/com/simiancraft/kaleidoscope/gpu/GpuEffectProcessor.kt +0 -198
  258. package/dist/backgrounds/office-1.d.ts +0 -3
  259. package/dist/backgrounds/office-1.d.ts.map +0 -1
  260. package/dist/backgrounds/office-1.js.map +0 -1
  261. package/dist/backgrounds/office-1.web.d.ts +0 -3
  262. package/dist/backgrounds/office-1.web.d.ts.map +0 -1
  263. package/dist/backgrounds/office-1.web.js.map +0 -1
  264. package/dist/backgrounds/office-1.webp +0 -0
  265. package/dist/backgrounds/office-2.d.ts +0 -3
  266. package/dist/backgrounds/office-2.d.ts.map +0 -1
  267. package/dist/backgrounds/office-2.js.map +0 -1
  268. package/dist/backgrounds/office-2.web.d.ts +0 -3
  269. package/dist/backgrounds/office-2.web.d.ts.map +0 -1
  270. package/dist/backgrounds/office-2.web.js.map +0 -1
  271. package/dist/backgrounds/office-2.webp +0 -0
  272. package/dist/web/effects/mirror.d.ts +0 -3
  273. package/dist/web/effects/mirror.d.ts.map +0 -1
  274. package/dist/web/effects/mirror.js +0 -31
  275. package/dist/web/effects/mirror.js.map +0 -1
  276. package/dist/web/effects/passthrough.d.ts +0 -3
  277. package/dist/web/effects/passthrough.d.ts.map +0 -1
  278. package/dist/web/effects/passthrough.js +0 -15
  279. package/dist/web/effects/passthrough.js.map +0 -1
  280. package/ios/KaleidoscopeModule/effects/MirrorProcessor.swift +0 -136
  281. package/ios/KaleidoscopeModule/resources/backgrounds/office-1.png +0 -0
  282. package/ios/KaleidoscopeModule/resources/backgrounds/office-2.png +0 -0
  283. package/src/backgrounds/office-1.webp +0 -0
  284. package/src/backgrounds/office-2.webp +0 -0
  285. package/src/web/effects/mirror.ts +0 -37
  286. package/src/web/effects/passthrough.ts +0 -17
@@ -4,8 +4,8 @@
4
4
  // 1. Render the input OES camera texture through an OES->2D passthrough
5
5
  // shader into a cached intermediate FBO (the "original 2D" copy).
6
6
  // 2. Produce a mask via Mask.produce (which downsamples the original 2D,
7
- // reads it back to a Bitmap, runs MLKit Selfie Segmentation, uploads
8
- // the confidence map as a 2D GL texture).
7
+ // reads it back to a Bitmap, runs MediaPipe segmentation off-thread via
8
+ // SegmentationEngine, uploads the confidence map as a 2D GL texture).
9
9
  // 3. Run two separable Gaussian blur passes on the "original 2D" copy
10
10
  // using ping-pong FBOs.
11
11
  // 4. Composite original + blurred + mask into a fresh output texture via
@@ -16,10 +16,11 @@
16
16
  // Every observable failure logs to adb logcat under Kaleidoscope.Blur and
17
17
  // returns null so upstream forwards the original frame instead of crashing.
18
18
  //
19
- // Replaces the CPU implementation (manual Kotlin YUV/ARGB conversion +
20
- // MLKit + RenderScript). The CPU path was ~5-10 FPS at 720p; the GPU path
21
- // now runs MLKit on a worker thread (see Mask.kt) with a last-known-mask
22
- // cache, so the render thread is no longer gated on segmentation.
19
+ // Replaces the original CPU implementation (manual Kotlin YUV/ARGB
20
+ // conversion). The CPU path was ~5-10 FPS at 720p; the GPU path now runs
21
+ // MediaPipe segmentation on a shared worker thread (see Mask.kt and
22
+ // SegmentationEngine.kt) with a last-known-mask cache, so the render thread is
23
+ // no longer gated on segmentation.
23
24
 
24
25
  package com.simiancraft.kaleidoscope.effects
25
26
 
@@ -32,7 +33,9 @@ import com.oney.WebRTCModule.videoEffects.VideoFrameProcessorFactoryInterface
32
33
  import com.simiancraft.kaleidoscope.EffectTuning
33
34
  import com.simiancraft.kaleidoscope.gpu.Egl
34
35
  import com.simiancraft.kaleidoscope.gpu.Fbo
36
+ import com.simiancraft.kaleidoscope.gpu.FramePipeline
35
37
  import com.simiancraft.kaleidoscope.gpu.GlDebug
38
+ import com.simiancraft.kaleidoscope.gpu.Ingest
36
39
  import com.simiancraft.kaleidoscope.gpu.GlProgram
37
40
  import com.simiancraft.kaleidoscope.gpu.Shaders
38
41
  import com.simiancraft.kaleidoscope.segmentation.Mask
@@ -54,10 +57,14 @@ import org.webrtc.YuvConverter
54
57
  class BlurFactory(
55
58
  private val context: Context,
56
59
  ) : VideoFrameProcessorFactoryInterface {
57
- override fun build(): VideoFrameProcessor = BlurProcessor()
60
+ override fun build(): VideoFrameProcessor = BlurProcessor(context)
58
61
  }
59
62
 
60
- private class BlurProcessor : VideoFrameProcessor {
63
+ private class BlurProcessor(private val context: Context) : VideoFrameProcessor {
64
+ // process() is only ever invoked on the single SurfaceTextureHelper capture
65
+ // thread (VideoEffectProcessor.onFrameCaptured), so this never actually
66
+ // contends. Retained as cheap uncontended insurance and as an explicit marker
67
+ // that the GL state below is single-threaded; it is NOT a cross-thread guard.
61
68
  private val lock = Any()
62
69
 
63
70
  private var oesToTwoD: GlProgram? = null
@@ -71,31 +78,46 @@ private class BlurProcessor : VideoFrameProcessor {
71
78
  private var blurBFbo: Fbo? = null
72
79
  private var cachedWidth = 0
73
80
  private var cachedHeight = 0
81
+ // R1: the blur ping-pong runs at a downscaled resolution; these hold its
82
+ // dimensions so the blur passes set uAxis in the downscaled texel space.
83
+ private var blurW = 0
84
+ private var blurH = 0
74
85
 
75
- private val mask = Mask()
86
+ private val mask = Mask(context)
76
87
  private var yuvConverter: YuvConverter? = null
77
88
 
78
- // 9-tap separable Gaussian kernel. tapSpacing is fixed; sigma comes from
79
- // EffectTuning at frame time and the kernel is rebuilt on the CPU only
80
- // when sigma changes. At sigma=8 the kernel covers ~+/-16 pixels with a
81
- // smooth falloff.
89
+ // R3: one-frame GPU pipeline. process() hands each rendered texture here and
90
+ // gets the previous frame's GPU-complete texture back, so the capture thread
91
+ // never blocks on the current frame's GPU work (the old per-frame glFinish).
92
+ private val pipeline = FramePipeline()
93
+
94
+ // Linear-sampled separable Gaussian: 5 entries (center + 4 bilinear pairs).
95
+ // sigma comes from EffectTuning at frame time; the kernel is rebuilt on the
96
+ // CPU only when sigma changes. See src/web/blur-kernel.ts for the derivation.
82
97
  private val blurWeights = FloatArray(KERNEL_TAPS)
83
98
  private val blurOffsets = FloatArray(KERNEL_TAPS)
84
99
  private var cachedKernelSigma: Float = Float.NaN
85
100
 
86
101
  private fun ensureKernel(sigma: Float) {
87
102
  if (sigma == cachedKernelSigma) return
88
- val tapSpacing = 2.0
89
- val sigmaD = sigma.toDouble()
90
- for (i in 0 until KERNEL_TAPS) {
91
- blurOffsets[i] = (i * tapSpacing).toFloat()
92
- val x = blurOffsets[i].toDouble()
93
- blurWeights[i] = Math.exp(-(x * x) / (2.0 * sigmaD * sigmaD)).toFloat()
94
- }
95
- // Normalize: center contributes once, each side tap contributes twice
96
- // because the shader samples vUv +/- offset and adds each.
103
+ val s = sigma.toDouble()
104
+ fun g(t: Double) = Math.exp(-(t * t) / (2.0 * s * s))
105
+ // Linear-sampled: center + 4 bilinear pairs of dense texels (1,2)(3,4)
106
+ // (5,6)(7,8); each pair is one fractional-offset fetch. Normalize so
107
+ // center + 2*sum(pairs) == 1 (the shader samples vUv +/- offset, adds each).
108
+ blurOffsets[0] = 0f
109
+ blurWeights[0] = g(0.0).toFloat()
97
110
  var sum = blurWeights[0]
98
- for (i in 1 until KERNEL_TAPS) sum += 2f * blurWeights[i]
111
+ for (p in 1 until KERNEL_TAPS) {
112
+ val a = (2 * p - 1).toDouble()
113
+ val b = (2 * p).toDouble()
114
+ val wa = g(a)
115
+ val wb = g(b)
116
+ val w = wa + wb
117
+ blurOffsets[p] = ((a * wa + b * wb) / w).toFloat()
118
+ blurWeights[p] = w.toFloat()
119
+ sum += 2f * blurWeights[p]
120
+ }
99
121
  for (i in 0 until KERNEL_TAPS) blurWeights[i] = blurWeights[i] / sum
100
122
  cachedKernelSigma = sigma
101
123
  }
@@ -136,13 +158,19 @@ private class BlurProcessor : VideoFrameProcessor {
136
158
  Log.w(TAG, "TextureBuffer type is ${inputBuffer.type}; expected OES. Forwarding original.")
137
159
  return null
138
160
  }
139
- val width = inputBuffer.width
140
- val height = inputBuffer.height
141
- if (width <= 0 || height <= 0) {
142
- Log.w(TAG, "Degenerate dims ${width}x${height}; forwarding.")
161
+ val bufW = inputBuffer.width
162
+ val bufH = inputBuffer.height
163
+ if (bufW <= 0 || bufH <= 0) {
164
+ Log.w(TAG, "Degenerate dims ${bufW}x${bufH}; forwarding.")
143
165
  return null
144
166
  }
145
167
 
168
+ // Ingest normalization: the OES->2D pass below lands a DISPLAY-UPRIGHT frame
169
+ // (Ingest folds frame.rotation into the texture matrix), so every cached FBO
170
+ // and the output are sized in DISPLAY dims, and the frame goes out rotation 0.
171
+ val width = Ingest.displayWidth(bufW, bufH, frame.rotation)
172
+ val height = Ingest.displayHeight(bufW, bufH, frame.rotation)
173
+
146
174
  GlDebug.check("blur entry")
147
175
  val saved = Egl.save()
148
176
  var outputTextureId = 0
@@ -166,7 +194,8 @@ private class BlurProcessor : VideoFrameProcessor {
166
194
  origFbo.bind()
167
195
  oes.use()
168
196
  oes.setInt("uTex", 0)
169
- val texMatrix = Egl.matrixToGl(inputBuffer.transformMatrix)
197
+ // Compose transformMatrix with the display rotation so the FBO lands upright.
198
+ val texMatrix = Ingest.composedTexMatrix(inputBuffer.transformMatrix, frame.rotation)
170
199
  GLES30.glUniformMatrix4fv(oes.uniformLocation("uTexMatrix"), 1, false, texMatrix, 0)
171
200
  GLES30.glDisable(GLES30.GL_DEPTH_TEST)
172
201
  GLES30.glDisable(GLES30.GL_BLEND)
@@ -180,7 +209,11 @@ private class BlurProcessor : VideoFrameProcessor {
180
209
  return null
181
210
  }
182
211
 
183
- // ===== Pass 2: horizontal blur =====
212
+ // ===== Pass 1b: downsample original -> blurA (box-average) =====
213
+ // uAxis=0 collapses the kernel to its center tap (weights sum to 1), so
214
+ // this is a plain bilinear box-average into the downscaled target. Both
215
+ // blur passes then run in downscaled space; sampling the full-res
216
+ // original with downscaled-spaced offsets serrates the blur (see web fix).
184
217
  GLES30.glActiveTexture(GLES30.GL_TEXTURE0)
185
218
  GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, origFbo.texture)
186
219
  blurA.bind()
@@ -188,19 +221,27 @@ private class BlurProcessor : VideoFrameProcessor {
188
221
  blur.setInt("uTex", 0)
189
222
  GLES30.glUniform1fv(blur.uniformLocation("uWeights"), KERNEL_TAPS, blurWeights, 0)
190
223
  GLES30.glUniform1fv(blur.uniformLocation("uOffsets"), KERNEL_TAPS, blurOffsets, 0)
191
- GLES30.glUniform2f(blur.uniformLocation("uAxis"), 1.0f / width, 0.0f)
224
+ GLES30.glUniform2f(blur.uniformLocation("uAxis"), 0.0f, 0.0f)
192
225
  GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, 4)
193
- GlDebug.check("blur horizontal pass")
226
+ GlDebug.check("blur downsample pass")
194
227
 
195
- // ===== Pass 3: vertical blur =====
228
+ // ===== Pass 2: horizontal blur blurA -> blurB (downscaled) =====
196
229
  GLES30.glActiveTexture(GLES30.GL_TEXTURE0)
197
230
  GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, blurA.texture)
198
231
  blurB.bind()
199
232
  blur.use()
200
233
  blur.setInt("uTex", 0)
201
- GLES30.glUniform1fv(blur.uniformLocation("uWeights"), KERNEL_TAPS, blurWeights, 0)
202
- GLES30.glUniform1fv(blur.uniformLocation("uOffsets"), KERNEL_TAPS, blurOffsets, 0)
203
- GLES30.glUniform2f(blur.uniformLocation("uAxis"), 0.0f, 1.0f / height)
234
+ GLES30.glUniform2f(blur.uniformLocation("uAxis"), 1.0f / blurW, 0.0f)
235
+ GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, 4)
236
+ GlDebug.check("blur horizontal pass")
237
+
238
+ // ===== Pass 3: vertical blur blurB -> blurA (downscaled) =====
239
+ GLES30.glActiveTexture(GLES30.GL_TEXTURE0)
240
+ GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, blurB.texture)
241
+ blurA.bind()
242
+ blur.use()
243
+ blur.setInt("uTex", 0)
244
+ GLES30.glUniform2f(blur.uniformLocation("uAxis"), 0.0f, 1.0f / blurH)
204
245
  GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, 4)
205
246
  GlDebug.check("blur vertical pass")
206
247
 
@@ -217,7 +258,8 @@ private class BlurProcessor : VideoFrameProcessor {
217
258
  composite.setInt("uOriginal", 0)
218
259
 
219
260
  GLES30.glActiveTexture(GLES30.GL_TEXTURE1)
220
- GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, blurB.texture)
261
+ // Final blurred result is in blurA after the vertical pass.
262
+ GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, blurA.texture)
221
263
  composite.setInt("uBackground", 1)
222
264
 
223
265
  GLES30.glActiveTexture(GLES30.GL_TEXTURE2)
@@ -239,10 +281,6 @@ private class BlurProcessor : VideoFrameProcessor {
239
281
  GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, 4)
240
282
  GlDebug.check("blur composite pass")
241
283
 
242
- // Synchronize before handing the output texture to the renderer's
243
- // EGL context.
244
- GLES30.glFinish()
245
-
246
284
  // Detach the texture from the FBO and free the FBO. The texture lives
247
285
  // with the VideoFrame and gets deleted in the release callback.
248
286
  GLES30.glFramebufferTexture2D(
@@ -257,28 +295,45 @@ private class BlurProcessor : VideoFrameProcessor {
257
295
  outputFboHandle = 0
258
296
  GlDebug.check("blur output cleanup")
259
297
 
298
+ // R3: fence this frame's GPU work and hand the PREVIOUS (GPU-complete)
299
+ // frame's texture downstream instead of glFinish-ing on this one. The
300
+ // pipeline now owns outputTextureId, so zero our local handle to keep the
301
+ // orphan-cleanup catch from double-freeing it.
302
+ val ready = pipeline.enqueue(
303
+ outputTextureId,
304
+ width,
305
+ height,
306
+ // Pixels are already display-upright after ingest; emit rotation 0.
307
+ 0,
308
+ frame.timestampNs,
309
+ EffectTuning.debugTiming,
310
+ TAG,
311
+ )
312
+ outputTextureId = 0
313
+ // First frame: nothing to hand off yet. Forward the original frame once.
314
+ ready ?: return null
315
+
260
316
  val yc = yuvConverter ?: run {
261
317
  val c = YuvConverter()
262
318
  yuvConverter = c
263
319
  c
264
320
  }
265
321
 
266
- val capturedTextureId = outputTextureId
322
+ val readyTextureId = ready.textureId
267
323
  val outputBuffer = TextureBufferImpl(
268
- width,
269
- height,
324
+ ready.width,
325
+ ready.height,
270
326
  VideoFrame.TextureBuffer.Type.RGB,
271
- capturedTextureId,
327
+ readyTextureId,
272
328
  Matrix(),
273
329
  textureHelper.handler,
274
330
  yc,
275
331
  Runnable {
276
- GLES30.glDeleteTextures(1, intArrayOf(capturedTextureId), 0)
332
+ GLES30.glDeleteTextures(1, intArrayOf(readyTextureId), 0)
277
333
  },
278
334
  )
279
- outputTextureId = 0
280
335
 
281
- VideoFrame(outputBuffer, frame.rotation, frame.timestampNs)
336
+ VideoFrame(outputBuffer, ready.rotation, ready.timestampNs)
282
337
  } catch (t: Throwable) {
283
338
  if (outputTextureId != 0) {
284
339
  try {
@@ -320,9 +375,18 @@ private class BlurProcessor : VideoFrameProcessor {
320
375
  originalFbo?.delete()
321
376
  blurAFbo?.delete()
322
377
  blurBFbo?.delete()
378
+ // R1: blur at quarter area (half each axis), floored so the short side
379
+ // stays >= 256px. The original stays full-res (foreground source + blur
380
+ // input); the composite upscales the downscaled blurred bg with GL_LINEAR
381
+ // for free, and the blur discards the detail the extra resolution carries.
382
+ val shortSide = minOf(width, height)
383
+ val target = maxOf(256, Math.round(shortSide * 0.5f))
384
+ val scale = target.toFloat() / shortSide
385
+ blurW = Math.round(width * scale).coerceAtLeast(2).let { if (it % 2 == 0) it else it - 1 }
386
+ blurH = Math.round(height * scale).coerceAtLeast(2).let { if (it % 2 == 0) it else it - 1 }
323
387
  originalFbo = Fbo(width, height)
324
- blurAFbo = Fbo(width, height)
325
- blurBFbo = Fbo(width, height)
388
+ blurAFbo = Fbo(blurW, blurH)
389
+ blurBFbo = Fbo(blurW, blurH)
326
390
  cachedWidth = width
327
391
  cachedHeight = height
328
392
  GlDebug.check("blur intermediates allocated")
@@ -331,6 +395,6 @@ private class BlurProcessor : VideoFrameProcessor {
331
395
  companion object {
332
396
  private const val TAG = "Kaleidoscope.Blur"
333
397
  private const val GL_TEXTURE_EXTERNAL_OES = 0x8D65
334
- private const val KERNEL_TAPS = 9
398
+ private const val KERNEL_TAPS = 5
335
399
  }
336
400
  }
@@ -0,0 +1,277 @@
1
+ // Android transform effects — GPU pipeline. One factory class serves all four
2
+ // geometric reorientation ops (flip-x, flip-y, rotate-cw, rotate-ccw); each
3
+ // registration passes a different Orientation.Op.
4
+ //
5
+ // flip-x = screen-horizontal mirror (left<->right, head stays up); keeps w x h.
6
+ // flip-y = screen-vertical flip (upside down, left/right unchanged); keeps w x h.
7
+ // rotate-cw = whole frame rotated 90 degrees clockwise; output dims swap to h x w.
8
+ // rotate-ccw = 90 degrees counter-clockwise; output dims swap to h x w.
9
+ //
10
+ // flip-x replaces the old CPU "mirror" effect (the corrected screen-horizontal
11
+ // mirror); MirrorFactory and its "mirror" registration are removed.
12
+ //
13
+ // Per frame:
14
+ // 1. Render the input OES camera texture through the OES->2D passthrough into
15
+ // a cached DISPLAY-UPRIGHT "original 2D" FBO, exactly like BlurFactory /
16
+ // BackgroundImageFactory. Ingest folds the display rotation into the
17
+ // texture matrix, so the FBO is sized in display dims and already upright.
18
+ // 2. Single TRANSFORM_FRAG pass: sample that upright 2D copy through the
19
+ // uUvTransform mat2 from Orientation.mat2For(op) into a fresh output
20
+ // texture sized display w x h (flips) or h x w (rotations).
21
+ // 3. Wrap the fresh output texture in a TextureBufferImpl and return a
22
+ // VideoFrame at rotation 0 (pixels are already upright).
23
+ //
24
+ // Camera orientation lives ONLY in Ingest; the op here is pure screen space and
25
+ // never consults frame.rotation. All failure paths log under
26
+ // Kaleidoscope.Transform and return null so upstream forwards the original frame.
27
+
28
+ package com.simiancraft.kaleidoscope.effects
29
+
30
+ import android.graphics.Matrix
31
+ import android.opengl.GLES30
32
+ import android.util.Log
33
+ import com.oney.WebRTCModule.videoEffects.VideoFrameProcessor
34
+ import com.oney.WebRTCModule.videoEffects.VideoFrameProcessorFactoryInterface
35
+ import com.simiancraft.kaleidoscope.EffectTuning
36
+ import com.simiancraft.kaleidoscope.gpu.Egl
37
+ import com.simiancraft.kaleidoscope.gpu.Fbo
38
+ import com.simiancraft.kaleidoscope.gpu.FramePipeline
39
+ import com.simiancraft.kaleidoscope.gpu.GlDebug
40
+ import com.simiancraft.kaleidoscope.gpu.GlProgram
41
+ import com.simiancraft.kaleidoscope.gpu.Ingest
42
+ import com.simiancraft.kaleidoscope.gpu.Orientation
43
+ import com.simiancraft.kaleidoscope.gpu.Shaders
44
+ import org.webrtc.SurfaceTextureHelper
45
+ import org.webrtc.TextureBufferImpl
46
+ import org.webrtc.VideoFrame
47
+ import org.webrtc.YuvConverter
48
+
49
+ /**
50
+ * @param op Which screen-space reorientation this factory's processor applies.
51
+ */
52
+ class TransformFactory(
53
+ private val op: Orientation.Op,
54
+ ) : VideoFrameProcessorFactoryInterface {
55
+ override fun build(): VideoFrameProcessor = TransformProcessor(op)
56
+ }
57
+
58
+ private class TransformProcessor(
59
+ private val op: Orientation.Op,
60
+ ) : VideoFrameProcessor {
61
+ // process() is only ever invoked on the single SurfaceTextureHelper capture
62
+ // thread (VideoEffectProcessor.onFrameCaptured), so this never actually
63
+ // contends. Retained as cheap uncontended insurance and as an explicit marker
64
+ // that the GL state below is single-threaded; it is NOT a cross-thread guard.
65
+ private val lock = Any()
66
+
67
+ private var oesToTwoD: GlProgram? = null
68
+ private var transformProgram: GlProgram? = null
69
+
70
+ // Cached display-oriented "original 2D" copy at full input resolution.
71
+ private var originalFbo: Fbo? = null
72
+ private var cachedWidth = 0
73
+ private var cachedHeight = 0
74
+
75
+ private var yuvConverter: YuvConverter? = null
76
+
77
+ // R3: one-frame GPU pipeline (see FramePipeline). Replaces the per-frame
78
+ // glFinish; only changes WHEN the frame is returned, not its orientation.
79
+ private val pipeline = FramePipeline()
80
+
81
+ override fun process(frame: VideoFrame, textureHelper: SurfaceTextureHelper?): VideoFrame? {
82
+ return synchronized(lock) { processOuter(frame, textureHelper) }
83
+ }
84
+
85
+ private fun processOuter(frame: VideoFrame, textureHelper: SurfaceTextureHelper?): VideoFrame? {
86
+ return try {
87
+ processInner(frame, textureHelper)
88
+ } catch (t: Throwable) {
89
+ Log.e(
90
+ TAG,
91
+ "process() threw; falling through to original frame. op=$op " +
92
+ "frame=${frame.buffer.width}x${frame.buffer.height} " +
93
+ "rotation=${frame.rotation} bufferClass=${frame.buffer.javaClass.simpleName}",
94
+ t,
95
+ )
96
+ null
97
+ }
98
+ }
99
+
100
+ private fun processInner(
101
+ frame: VideoFrame,
102
+ textureHelper: SurfaceTextureHelper?,
103
+ ): VideoFrame? {
104
+ if (textureHelper == null) {
105
+ Log.w(TAG, "textureHelper is null; falling through.")
106
+ return null
107
+ }
108
+ val inputBuffer = frame.buffer
109
+ if (inputBuffer !is VideoFrame.TextureBuffer) {
110
+ // Chained after a CPU effect emitting I420 — silently forward.
111
+ return null
112
+ }
113
+ if (inputBuffer.type != VideoFrame.TextureBuffer.Type.OES) {
114
+ Log.w(TAG, "TextureBuffer type is ${inputBuffer.type}; expected OES. Forwarding original.")
115
+ return null
116
+ }
117
+ val bufW = inputBuffer.width
118
+ val bufH = inputBuffer.height
119
+ if (bufW <= 0 || bufH <= 0) {
120
+ Log.w(TAG, "Degenerate dims ${bufW}x${bufH}; forwarding.")
121
+ return null
122
+ }
123
+
124
+ // Ingest normalization: the OES->2D pass lands a DISPLAY-UPRIGHT frame, so
125
+ // the original FBO is sized in DISPLAY dims and the op runs in pure screen
126
+ // space (Orientation no longer consults frame.rotation).
127
+ val width = Ingest.displayWidth(bufW, bufH, frame.rotation)
128
+ val height = Ingest.displayHeight(bufW, bufH, frame.rotation)
129
+
130
+ // Rotations swap the (display) output dimensions; flips keep them.
131
+ val swap = Orientation.swapsDimensions(op)
132
+ val outWidth = if (swap) height else width
133
+ val outHeight = if (swap) width else height
134
+
135
+ GlDebug.check("transform entry")
136
+ val saved = Egl.save()
137
+ var outputTextureId = 0
138
+ var outputFboHandle = 0
139
+ return try {
140
+ ensurePrograms()
141
+ ensureIntermediates(width, height)
142
+ val origFbo = originalFbo ?: error("originalFbo null after ensure")
143
+ val oes = oesToTwoD ?: error("oesToTwoD program null after ensure")
144
+ val transform = transformProgram ?: error("transformProgram null after ensure")
145
+
146
+ // ===== Pass 1: OES camera -> display-oriented "original 2D" =====
147
+ GLES30.glActiveTexture(GLES30.GL_TEXTURE0)
148
+ GLES30.glBindTexture(GL_TEXTURE_EXTERNAL_OES, inputBuffer.textureId)
149
+ origFbo.bind()
150
+ oes.use()
151
+ oes.setInt("uTex", 0)
152
+ // Compose transformMatrix with the display rotation so the FBO lands upright.
153
+ val texMatrix = Ingest.composedTexMatrix(inputBuffer.transformMatrix, frame.rotation)
154
+ GLES30.glUniformMatrix4fv(oes.uniformLocation("uTexMatrix"), 1, false, texMatrix, 0)
155
+ GLES30.glDisable(GLES30.GL_DEPTH_TEST)
156
+ GLES30.glDisable(GLES30.GL_BLEND)
157
+ GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, 4)
158
+ GlDebug.check("transform OES->2D")
159
+
160
+ // ===== Pass 2: geometric reorientation into a fresh output texture =====
161
+ // Output FBO is sized for the (possibly swapped) output dims. The
162
+ // uUvTransform mat2 reads the square [0,1] original UV back through the
163
+ // op; rotations map the unit square onto itself, so swapping the FBO's
164
+ // physical dims yields the rotated image with no UV clamping.
165
+ val outputFbo = Fbo(outWidth, outHeight)
166
+ outputTextureId = outputFbo.texture
167
+ outputFboHandle = outputFbo.framebuffer
168
+
169
+ outputFbo.bind()
170
+ transform.use()
171
+ GLES30.glActiveTexture(GLES30.GL_TEXTURE0)
172
+ GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, origFbo.texture)
173
+ transform.setInt("uTex", 0)
174
+ // Input is already display-upright (ingest), so the op is pure screen
175
+ // space; frame.rotation is no longer consulted here.
176
+ transform.setMat2("uUvTransform", Orientation.mat2For(op))
177
+ GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, 4)
178
+ GlDebug.check("transform reorient pass")
179
+
180
+ // Detach the texture from the FBO and free the FBO; the texture lives
181
+ // with the VideoFrame and is deleted in the release callback.
182
+ GLES30.glFramebufferTexture2D(
183
+ GLES30.GL_FRAMEBUFFER,
184
+ GLES30.GL_COLOR_ATTACHMENT0,
185
+ GLES30.GL_TEXTURE_2D,
186
+ 0,
187
+ 0,
188
+ )
189
+ GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, 0)
190
+ GLES30.glDeleteFramebuffers(1, intArrayOf(outputFboHandle), 0)
191
+ outputFboHandle = 0
192
+ GlDebug.check("transform output cleanup")
193
+
194
+ // R3: fence this frame and hand the previous GPU-complete frame off. The
195
+ // (possibly swapped) output dims travel with the frame, so the previous
196
+ // frame's own dims/rotation are wrapped unchanged; orientation behavior
197
+ // is untouched, only the return is deferred one frame.
198
+ val ready = pipeline.enqueue(
199
+ outputTextureId,
200
+ outWidth,
201
+ outHeight,
202
+ // Pixels are already display-upright after ingest; emit rotation 0.
203
+ 0,
204
+ frame.timestampNs,
205
+ EffectTuning.debugTiming,
206
+ TAG,
207
+ )
208
+ outputTextureId = 0
209
+ ready ?: return null
210
+
211
+ val yc = yuvConverter ?: run {
212
+ val c = YuvConverter()
213
+ yuvConverter = c
214
+ c
215
+ }
216
+
217
+ val readyTextureId = ready.textureId
218
+ val outputBuffer = TextureBufferImpl(
219
+ ready.width,
220
+ ready.height,
221
+ VideoFrame.TextureBuffer.Type.RGB,
222
+ readyTextureId,
223
+ Matrix(),
224
+ textureHelper.handler,
225
+ yc,
226
+ Runnable {
227
+ GLES30.glDeleteTextures(1, intArrayOf(readyTextureId), 0)
228
+ },
229
+ )
230
+
231
+ VideoFrame(outputBuffer, ready.rotation, ready.timestampNs)
232
+ } catch (t: Throwable) {
233
+ if (outputTextureId != 0) {
234
+ try {
235
+ GLES30.glDeleteTextures(1, intArrayOf(outputTextureId), 0)
236
+ } catch (delErr: Throwable) {
237
+ Log.w(TAG, "failed to free orphan texture $outputTextureId", delErr)
238
+ }
239
+ }
240
+ if (outputFboHandle != 0) {
241
+ try {
242
+ GLES30.glDeleteFramebuffers(1, intArrayOf(outputFboHandle), 0)
243
+ } catch (delErr: Throwable) {
244
+ Log.w(TAG, "failed to free orphan FBO $outputFboHandle", delErr)
245
+ }
246
+ }
247
+ throw t
248
+ } finally {
249
+ Egl.restore(saved)
250
+ }
251
+ }
252
+
253
+ private fun ensurePrograms() {
254
+ if (oesToTwoD == null) {
255
+ oesToTwoD = GlProgram(Shaders.PASSTHROUGH_VERT, Shaders.OES_PASSTHROUGH_FRAG)
256
+ GlDebug.check("oesToTwoD program compile/link")
257
+ }
258
+ if (transformProgram == null) {
259
+ transformProgram = GlProgram(Shaders.PASSTHROUGH_VERT, Shaders.TRANSFORM_FRAG)
260
+ GlDebug.check("transform program compile/link")
261
+ }
262
+ }
263
+
264
+ private fun ensureIntermediates(width: Int, height: Int) {
265
+ if (cachedWidth == width && cachedHeight == height && originalFbo != null) return
266
+ originalFbo?.delete()
267
+ originalFbo = Fbo(width, height)
268
+ cachedWidth = width
269
+ cachedHeight = height
270
+ GlDebug.check("transform intermediates allocated")
271
+ }
272
+
273
+ companion object {
274
+ private const val TAG = "Kaleidoscope.Transform"
275
+ private const val GL_TEXTURE_EXTERNAL_OES = 0x8D65
276
+ }
277
+ }