react-native-skia-webgpu-video 0.0.0-stage → 0.1.0-beta.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 (236) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +303 -2
  3. package/android/BUILD_VALIDATION.md +62 -0
  4. package/android/CMakeLists.txt +99 -0
  5. package/android/build-validation.json +262 -0
  6. package/android/build.gradle +185 -0
  7. package/android/cpp/EncoderTargets.cpp +226 -0
  8. package/android/cpp/EncoderTargets.h +70 -0
  9. package/android/cpp/JNIHelpers.cpp +15 -0
  10. package/android/cpp/JNIHelpers.h +20 -0
  11. package/android/cpp/NativeEventDispatcher.cpp +59 -0
  12. package/android/cpp/NativeEventDispatcher.h +31 -0
  13. package/android/cpp/NativeHardwareBuffer.cpp +397 -0
  14. package/android/cpp/NativeHardwareBuffer.h +34 -0
  15. package/android/cpp/NativeRgbaBuffer.cpp +45 -0
  16. package/android/cpp/NativeRgbaBuffer.h +9 -0
  17. package/android/cpp/ReturnedVideoFrames.h +37 -0
  18. package/android/cpp/RgbaFrameLease.h +53 -0
  19. package/android/cpp/VideoCapabilities.cpp +87 -0
  20. package/android/cpp/VideoCapabilities.h +44 -0
  21. package/android/cpp/VideoComposition.cpp +170 -0
  22. package/android/cpp/VideoComposition.h +36 -0
  23. package/android/cpp/VideoCompositionFramesExtractor.cpp +82 -0
  24. package/android/cpp/VideoCompositionFramesExtractor.h +48 -0
  25. package/android/cpp/VideoCompositionFramesExtractorHostObject.cpp +230 -0
  26. package/android/cpp/VideoCompositionFramesExtractorHostObject.h +42 -0
  27. package/android/cpp/VideoCompositionFramesExtractorSyncHostObject.cpp +133 -0
  28. package/android/cpp/VideoCompositionFramesExtractorSyncHostObject.h +46 -0
  29. package/android/cpp/VideoEncoderHostObject.cpp +318 -0
  30. package/android/cpp/VideoEncoderHostObject.h +76 -0
  31. package/android/cpp/VideoFrame.cpp +293 -0
  32. package/android/cpp/VideoFrame.h +27 -0
  33. package/android/cpp/VideoPlayer.cpp +96 -0
  34. package/android/cpp/VideoPlayer.h +52 -0
  35. package/android/cpp/VideoPlayerHostObject.cpp +225 -0
  36. package/android/cpp/VideoPlayerHostObject.h +43 -0
  37. package/android/cpp/cpp-adapter.cpp +316 -0
  38. package/android/gradle.properties +5 -0
  39. package/android/instrumentation-inventory.json +178 -0
  40. package/android/proguard-rules.pro +4 -0
  41. package/android/src/main/AndroidManifest.xml +2 -0
  42. package/android/src/main/java/com/azzapp/rnskv/AudioCompositionExporter.java +537 -0
  43. package/android/src/main/java/com/azzapp/rnskv/AudioCompositionPlayer.java +321 -0
  44. package/android/src/main/java/com/azzapp/rnskv/BufferLifetime.java +55 -0
  45. package/android/src/main/java/com/azzapp/rnskv/DecodeRequests.java +100 -0
  46. package/android/src/main/java/com/azzapp/rnskv/DecoderWindow.java +55 -0
  47. package/android/src/main/java/com/azzapp/rnskv/EGLResourcesHolder.java +132 -0
  48. package/android/src/main/java/com/azzapp/rnskv/EGLUtils.java +225 -0
  49. package/android/src/main/java/com/azzapp/rnskv/FrameSize.java +50 -0
  50. package/android/src/main/java/com/azzapp/rnskv/GLFrameExtractor.java +510 -0
  51. package/android/src/main/java/com/azzapp/rnskv/JNIHelpers.java +12 -0
  52. package/android/src/main/java/com/azzapp/rnskv/NativeEventDispatcher.java +25 -0
  53. package/android/src/main/java/com/azzapp/rnskv/NativeHardwareBuffer.java +62 -0
  54. package/android/src/main/java/com/azzapp/rnskv/NativeRgbaBuffer.java +35 -0
  55. package/android/src/main/java/com/azzapp/rnskv/ReactNativeSkiaVideoModule.java +45 -0
  56. package/android/src/main/java/com/azzapp/rnskv/ReactNativeSkiaVideoPackage.java +43 -0
  57. package/android/src/main/java/com/azzapp/rnskv/ReactNativeSkiaVideoSpec.java +9 -0
  58. package/android/src/main/java/com/azzapp/rnskv/RgbaBufferAllocator.java +83 -0
  59. package/android/src/main/java/com/azzapp/rnskv/RgbaLayout.java +33 -0
  60. package/android/src/main/java/com/azzapp/rnskv/SurfaceOutputPolicy.java +13 -0
  61. package/android/src/main/java/com/azzapp/rnskv/TextureRenderer.java +150 -0
  62. package/android/src/main/java/com/azzapp/rnskv/TimeHelpers.java +11 -0
  63. package/android/src/main/java/com/azzapp/rnskv/VideoCapabilities.java +354 -0
  64. package/android/src/main/java/com/azzapp/rnskv/VideoComposition.java +147 -0
  65. package/android/src/main/java/com/azzapp/rnskv/VideoCompositionDecoder.java +772 -0
  66. package/android/src/main/java/com/azzapp/rnskv/VideoCompositionFramesExtractor.java +354 -0
  67. package/android/src/main/java/com/azzapp/rnskv/VideoCompositionFramesExtractorSync.java +241 -0
  68. package/android/src/main/java/com/azzapp/rnskv/VideoCompositionItemDecoder.java +492 -0
  69. package/android/src/main/java/com/azzapp/rnskv/VideoEncoder.java +639 -0
  70. package/android/src/main/java/com/azzapp/rnskv/VideoFrame.java +74 -0
  71. package/android/src/main/java/com/azzapp/rnskv/VideoPlayer.java +361 -0
  72. package/cpp/RNSVCheckedSizes.h +54 -0
  73. package/cpp/RNSVEventEmitter.cpp +199 -0
  74. package/cpp/RNSVEventEmitter.h +31 -0
  75. package/cpp/RNSVHostObject.cpp +79 -0
  76. package/cpp/RNSVHostObject.h +67 -0
  77. package/cpp/RNSVMemoryBudget.cpp +68 -0
  78. package/cpp/RNSVMemoryBudget.h +100 -0
  79. package/cpp/RNSVRuntimeLifecycleMonitor.cpp +88 -0
  80. package/cpp/RNSVRuntimeLifecycleMonitor.h +33 -0
  81. package/docs/ANDROID_DEVICE_306.md +95 -0
  82. package/docs/ANDROID_EXPORT_CACHE_306.md +48 -0
  83. package/docs/ANDROID_MEMORY_RETENTION_306.md +67 -0
  84. package/docs/BENCHMARKS.md +368 -0
  85. package/docs/IOS_DEVICE_TESTS.md +115 -0
  86. package/docs/IOS_SIMULATOR_TESTS.md +74 -0
  87. package/docs/MIGRATION.md +82 -0
  88. package/docs/NATIVE_MEMORY_INVESTIGATION_306.md +88 -0
  89. package/docs/PERFORMANCE_AB_IPHONE_303.md +196 -0
  90. package/docs/PERFORMANCE_AB_SIMULATOR.md +135 -0
  91. package/docs/PERFORMANCE_AB_SOAK_SIMULATOR.md +84 -0
  92. package/docs/PERFORMANCE_IPHONE_306_OPTIMIZATION.md +76 -0
  93. package/docs/PERFORMANCE_SIMULATOR.md +89 -0
  94. package/docs/PERFORMANCE_SIMULATOR_INITIAL.md +96 -0
  95. package/docs/PERFORMANCE_SOAK_IPHONE_303.md +41 -0
  96. package/docs/UPSTREAM_NOTES.md +188 -0
  97. package/docs/assets/banner.svg +26 -0
  98. package/examples/webgpu-overlay.ts +147 -0
  99. package/ios/AVAssetTrackUtils.h +24 -0
  100. package/ios/AudioCompositionUtils.h +27 -0
  101. package/ios/AudioCompositionUtils.mm +113 -0
  102. package/ios/DecoderWindow.h +52 -0
  103. package/ios/NativeResourceStats.h +32 -0
  104. package/ios/RNSVColorSpace.h +50 -0
  105. package/ios/RNSVJSIUtils.h +46 -0
  106. package/ios/RNSVScopedPixelBuffer.h +17 -0
  107. package/ios/RNSVVideoPlayer.h +46 -0
  108. package/ios/RNSVVideoPlayer.mm +410 -0
  109. package/ios/ReactNativeSkiaVideo.h +8 -0
  110. package/ios/ReactNativeSkiaVideo.mm +310 -0
  111. package/ios/SeekPolicy.h +27 -0
  112. package/ios/VideoComposition.h +148 -0
  113. package/ios/VideoCompositionFramesExtractorHostObject.h +105 -0
  114. package/ios/VideoCompositionFramesExtractorHostObject.mm +686 -0
  115. package/ios/VideoCompositionFramesExtractorSyncHostObject.h +37 -0
  116. package/ios/VideoCompositionFramesExtractorSyncHostObject.mm +168 -0
  117. package/ios/VideoCompositionItemDecoder.h +60 -0
  118. package/ios/VideoCompositionItemDecoder.mm +480 -0
  119. package/ios/VideoEncoderHostObject.h +83 -0
  120. package/ios/VideoEncoderHostObject.mm +706 -0
  121. package/ios/VideoFrame.h +66 -0
  122. package/ios/VideoFrame.mm +108 -0
  123. package/ios/VideoPlayerHostObject.h +52 -0
  124. package/ios/VideoPlayerHostObject.mm +332 -0
  125. package/lib/module/NativeReactNativeSkiaVideo.js +5 -0
  126. package/lib/module/NativeReactNativeSkiaVideo.js.map +1 -0
  127. package/lib/module/RNSkiaVideoModule.js +20 -0
  128. package/lib/module/RNSkiaVideoModule.js.map +1 -0
  129. package/lib/module/canvas.js +18 -0
  130. package/lib/module/canvas.js.map +1 -0
  131. package/lib/module/exportCache.js +22 -0
  132. package/lib/module/exportCache.js.map +1 -0
  133. package/lib/module/exportFrames.js +338 -0
  134. package/lib/module/exportFrames.js.map +1 -0
  135. package/lib/module/exportQueue.js +52 -0
  136. package/lib/module/exportQueue.js.map +1 -0
  137. package/lib/module/exportTarget.js +137 -0
  138. package/lib/module/exportTarget.js.map +1 -0
  139. package/lib/module/exportVideoComposition.js +492 -0
  140. package/lib/module/exportVideoComposition.js.map +1 -0
  141. package/lib/module/frameInterop.js +532 -0
  142. package/lib/module/frameInterop.js.map +1 -0
  143. package/lib/module/gpu.js +222 -0
  144. package/lib/module/gpu.js.map +1 -0
  145. package/lib/module/gpuDevice.js +54 -0
  146. package/lib/module/gpuDevice.js.map +1 -0
  147. package/lib/module/index.js +36 -0
  148. package/lib/module/index.js.map +1 -0
  149. package/lib/module/memory.js +38 -0
  150. package/lib/module/memory.js.map +1 -0
  151. package/lib/module/package.json +1 -0
  152. package/lib/module/previewLifecycle.js +52 -0
  153. package/lib/module/previewLifecycle.js.map +1 -0
  154. package/lib/module/runtimeYield.js +10 -0
  155. package/lib/module/runtimeYield.js.map +1 -0
  156. package/lib/module/types.js +4 -0
  157. package/lib/module/types.js.map +1 -0
  158. package/lib/module/utils/useEventListener.js +18 -0
  159. package/lib/module/utils/useEventListener.js.map +1 -0
  160. package/lib/module/videoCompositionPlayer.js +348 -0
  161. package/lib/module/videoCompositionPlayer.js.map +1 -0
  162. package/lib/module/videoFrameImage.js +185 -0
  163. package/lib/module/videoFrameImage.js.map +1 -0
  164. package/lib/module/videoPlayer.js +172 -0
  165. package/lib/module/videoPlayer.js.map +1 -0
  166. package/lib/module/yuvBlit.js +299 -0
  167. package/lib/module/yuvBlit.js.map +1 -0
  168. package/lib/typescript/package.json +1 -0
  169. package/lib/typescript/src/NativeReactNativeSkiaVideo.d.ts +7 -0
  170. package/lib/typescript/src/NativeReactNativeSkiaVideo.d.ts.map +1 -0
  171. package/lib/typescript/src/RNSkiaVideoModule.d.ts +4 -0
  172. package/lib/typescript/src/RNSkiaVideoModule.d.ts.map +1 -0
  173. package/lib/typescript/src/canvas.d.ts +4 -0
  174. package/lib/typescript/src/canvas.d.ts.map +1 -0
  175. package/lib/typescript/src/exportCache.d.ts +6 -0
  176. package/lib/typescript/src/exportCache.d.ts.map +1 -0
  177. package/lib/typescript/src/exportFrames.d.ts +57 -0
  178. package/lib/typescript/src/exportFrames.d.ts.map +1 -0
  179. package/lib/typescript/src/exportQueue.d.ts +5 -0
  180. package/lib/typescript/src/exportQueue.d.ts.map +1 -0
  181. package/lib/typescript/src/exportTarget.d.ts +28 -0
  182. package/lib/typescript/src/exportTarget.d.ts.map +1 -0
  183. package/lib/typescript/src/exportVideoComposition.d.ts +68 -0
  184. package/lib/typescript/src/exportVideoComposition.d.ts.map +1 -0
  185. package/lib/typescript/src/frameInterop.d.ts +17 -0
  186. package/lib/typescript/src/frameInterop.d.ts.map +1 -0
  187. package/lib/typescript/src/gpu.d.ts +39 -0
  188. package/lib/typescript/src/gpu.d.ts.map +1 -0
  189. package/lib/typescript/src/gpuDevice.d.ts +11 -0
  190. package/lib/typescript/src/gpuDevice.d.ts.map +1 -0
  191. package/lib/typescript/src/index.d.ts +21 -0
  192. package/lib/typescript/src/index.d.ts.map +1 -0
  193. package/lib/typescript/src/memory.d.ts +25 -0
  194. package/lib/typescript/src/memory.d.ts.map +1 -0
  195. package/lib/typescript/src/previewLifecycle.d.ts +22 -0
  196. package/lib/typescript/src/previewLifecycle.d.ts.map +1 -0
  197. package/lib/typescript/src/runtimeYield.d.ts +4 -0
  198. package/lib/typescript/src/runtimeYield.d.ts.map +1 -0
  199. package/lib/typescript/src/types.d.ts +631 -0
  200. package/lib/typescript/src/types.d.ts.map +1 -0
  201. package/lib/typescript/src/utils/useEventListener.d.ts +5 -0
  202. package/lib/typescript/src/utils/useEventListener.d.ts.map +1 -0
  203. package/lib/typescript/src/videoCompositionPlayer.d.ts +85 -0
  204. package/lib/typescript/src/videoCompositionPlayer.d.ts.map +1 -0
  205. package/lib/typescript/src/videoFrameImage.d.ts +60 -0
  206. package/lib/typescript/src/videoFrameImage.d.ts.map +1 -0
  207. package/lib/typescript/src/videoPlayer.d.ts +99 -0
  208. package/lib/typescript/src/videoPlayer.d.ts.map +1 -0
  209. package/lib/typescript/src/yuvBlit.d.ts +50 -0
  210. package/lib/typescript/src/yuvBlit.d.ts.map +1 -0
  211. package/package.json +154 -4
  212. package/patches/react-native-skia+3.7.0.patch +502 -0
  213. package/react-native-skia-webgpu-video.podspec +35 -0
  214. package/scripts/apply-skia-canvas-patch.mjs +48 -0
  215. package/scripts/skia-canvas-patch.json +291 -0
  216. package/src/NativeReactNativeSkiaVideo.ts +8 -0
  217. package/src/RNSkiaVideoModule.ts +27 -0
  218. package/src/canvas.ts +22 -0
  219. package/src/exportCache.ts +27 -0
  220. package/src/exportFrames.ts +421 -0
  221. package/src/exportQueue.ts +65 -0
  222. package/src/exportTarget.ts +179 -0
  223. package/src/exportVideoComposition.ts +718 -0
  224. package/src/frameInterop.ts +684 -0
  225. package/src/gpu.ts +290 -0
  226. package/src/gpuDevice.ts +80 -0
  227. package/src/index.tsx +45 -0
  228. package/src/memory.ts +48 -0
  229. package/src/previewLifecycle.ts +60 -0
  230. package/src/runtimeYield.ts +6 -0
  231. package/src/types.ts +661 -0
  232. package/src/utils/useEventListener.ts +22 -0
  233. package/src/videoCompositionPlayer.ts +520 -0
  234. package/src/videoFrameImage.ts +205 -0
  235. package/src/videoPlayer.ts +318 -0
  236. package/src/yuvBlit.ts +331 -0
@@ -0,0 +1,188 @@
1
+ # Points amont à revoir lors d'une mise à jour Skia
2
+
3
+ **Migration du 19 octobre 2026 :** le code et les dépendances déclarées ciblent désormais **`react-native-skia@3.7.0` / `react-native-webgpu@0.17.0`**, sur Graphite **157.0.0** et **`react-native-webgpu-dawn@157.0.0`** (Skia `chrome/m157`). Les deux paquets doivent monter ensemble : Gradle et le podspec refusent deux Dawn différents. Le patch local garde 61 de ses 62 éditions ; seule tombe l'include `<stdexcept>`, désormais présent en amont. Le Dawn 157 demande trois images FIFO à la swapchain Android, ce qui supprime l'attente du thread principal dans `dequeueBuffer` sur Android 10 ; il garde aussi la robustesse des shaders sur Android, nos WGSL compris. L'[audit](../migration/skia-3.7.0/upstream-audit.md) détaille ces changements ; la [vérification](../migration/skia-3.7.0/verification.md) donne les mesures sur iPhone 15 Pro et Pixel 8a. Après une mise à jour, supprimer le bundle JS généré par Gradle avant de construire un APK de qualification : sa tâche ne voit pas le `node_modules` racine.
4
+
5
+ **Migration du 16 octobre 2026 :** le code et les dépendances déclarées ciblaient alors **`react-native-skia@3.6.1` / `react-native-webgpu@0.16.0`**, toujours sur Graphite **154.3.0** et **`react-native-webgpu-dawn@154.3.0`** : les binaires Skia et Dawn ne changent pas. L'[audit](../migration/skia-3.6.1/upstream-audit.md) détaille les changements amont, la régression de couleurs des frames décodées Android introduite par WebGPU 0.15 et sa correction, puis l'import en place à l'export Android. La [vérification](../migration/skia-3.6.1/verification.md) donne les mesures sur iPhone 15 Pro et Pixel 8a. **À chaque mise à jour WebGPU, relancer le contrôle `interop` du Pixel** : il compare les pixels décodés et exportés au chemin CPU, et c'est lui qui a révélé cette régression.
6
+
7
+ **Migration précédente, 14 octobre 2026 :** le code et les dépendances déclarées ciblent désormais **`react-native-skia@3.4.4` / `react-native-webgpu@0.14.2`**, Graphite **154.3.0** et un Dawn unique, **`react-native-webgpu-dawn@154.3.0`** (tag `chrome-m154a`). L'[audit de migration](../migration/skia-3.4.4/upstream-audit.md) détaille les changements amont, le patch rebasé et la passe d'export iOS ; la [vérification](../migration/skia-3.4.4/verification.md) liste ce qui a réellement été exécuté. Les mesures physiques citées ci-dessous, iPhone comme Android, restent celles du couple **3.0.6 / 0.12.1** : elles ne qualifient pas ces nouveaux binaires.
8
+
9
+ État précédent, revu le **8 octobre 2026**. Le code et les dépendances déclarées ciblaient alors **`react-native-skia@3.0.6` / `react-native-webgpu@0.12.1`**, Graphite **154.1.0** et Dawn **m154a**. Le code courant passe **196 tests JavaScript dans huit suites et le contrôle TypeScript**. La voie AHardwareBuffer Android a passé **24/24 essais physiques ciblés**, trois répétitions des huit scénarios, avec fermeture suivie à zéro ; **6/6 tests natifs AHB** passent sans skip. Le fallback scoped-copy antérieur avait terminé **8/8 opérations sans OOM**, mais **0/8 fermetures strictes**, après une première campagne CPU à **0/24 réussites strictes**. Les [diagnostics Android](ANDROID_DEVICE_306.md) conservent chaque checkpoint. La [passe iPhone initial/optimisé 3.0.6](PERFORMANCE_IPHONE_306_OPTIMIZATION.md) précède ces changements Android et ne les qualifie pas. Les [mesures iPhone 3.0.3](PERFORMANCE_AB_IPHONE_303.md), l'[endurance iPhone 3.0.3](PERFORMANCE_SOAK_IPHONE_303.md) et les chiffres simulateur conservés plus bas restent des résultats historiques du couple **3.0.3 / 0.11.0**.
10
+
11
+ La [correction du 8 octobre](NATIVE_MEMORY_INVESTIGATION_306.md) réutilise une seule fonction worklet par export, au lieu de recréer son graphe capturé à chaque image. Elle est intégrée dans `main` par la [PR #4](https://github.com/batical/react-native-skia-webgpu-video/pull/4), sans nouveau patch natif amont. Les mesures Release nominales après deux montages et 60 s au repos passent de 425,99 à 257,70 Mio d’empreinte physique sur iPhone, et de 737,83 à 577,20 Mio de PSS sur Pixel. Six montages iPhone et quatre Android passent ensuite. Lors d’une mise à jour Worklets, conserver le test d’identité de fonction et refaire les mesures : le comportement de sérialisation/rétention fait partie du diagnostic, sans supposer que toute mémoire restante appartient à Skia.
12
+
13
+ ## Interop actuelle et coût de la migration
14
+
15
+ Skia 3.0.5 a déplacé l'import natif vidéo vers WebGPU ; 3.0.6 conserve les mêmes sources d'API et livre les nouveaux binaires Graphite. Le changement vient de [#4139](https://github.com/wcandillon/react-native-skia/pull/4139), pas d'un renommage suffisant à lui seul.
16
+
17
+ | API utilisée en 3.0.3 | Contrat en 3.0.6 |
18
+ | --- | --- |
19
+ | `Image.MakeImageFromNativeTexture` | `Image.MakeImageFromGPUTexture`, avec objet GPUTexture ou pointeur WGPUTexture en BigInt |
20
+ | `Surface.MakeFromNativeTexture` | `Surface.MakeFromGPUTexture`, sur le device partagé et avec `RENDER_ATTACHMENT` |
21
+ | `Image.MakeNativeTextureFromImage` | `Image.MakeGPUTextureFromImage` ; référence native à adopter exactement une fois |
22
+ | `Image.MakeImageFromNativeBuffer` | Toujours présente pour le web, mais lève une erreur en natif ; notre adaptateur emploie désormais le wrapper NativeVideoFrame de WebGPU |
23
+ | `Skia.NativeBuffer` | Supprimée ; ne pas restaurer cette API dans notre patch |
24
+
25
+ Sur iOS, [frameInterop.ts](../src/frameInterop.ts) conserve notre CVPixelBuffer et le fait retenir aussi par un `NativeVideoFrame`. WebGPU copie les pixels dans **une texture réutilisable par runtime**, puis Skia crée directement un snapshot possédé depuis une surface enveloppant cette texture. La fin de la queue partagée précède la libération des deux leases, la fermeture des wrappers de surface/canvas et la réutilisation de l'intermédiaire. Il n'y a plus de surface de staging distincte ni de `drawImage` plein cadre entre l'import et le snapshot.
26
+
27
+ La surface Skia est **un wrapper neuf par frame**, sans nouvelle texture associée : les écritures externes WebGPU n'invalident pas son cache de snapshot. Réutiliser ce wrapper pourrait rendre l'image précédente. Le snapshot Graphite reste une copie indépendante, et `COPY_SRC` ajouté à la texture permet sa copie GPU directe. La fermeture explicite des wrappers conserve uniquement la texture réutilisable et les images publiées. Les tests modélisent notamment ce cache de snapshot, un producteur en pause, les redimensionnements, les erreurs de wrapping/drain et la rétention des leases ; ils ne prouvent pas les pixels effectivement calculés par le GPU.
28
+
29
+ Le chemin iOS optimisé comporte **deux écritures GPU plein cadre** : buffer natif → texture d'import → snapshot. Le premier build 3.0.6, archivé comme référence avant cette optimisation, en comportait trois avec un staging Skia supplémentaire. Le changement enlève donc une passe et une cible GPU : **31,64 Mio à 3840 × 2160 BGRA8 par runtime**, hors alignement et pilote. Une texture d'import de cette taille reste nécessaire, en plus des snapshots des producteurs. Cette déduction des allocations ne constitue pas à elle seule une mesure mémoire ou vitesse : la [passe iPhone dédiée](PERFORMANCE_IPHONE_306_OPTIMIZATION.md) conserve ses résultats et ses limites, notamment ses conditions thermiques non nominales. La synchronisation et la durée de vie des images n'ont pas été relâchées.
30
+
31
+ Depuis le 8 octobre, l'export iOS dessine directement dans les buffers du pool encodeur via leur IOSurface, avec repli sur le readback CPU si la sonde échoue. Depuis la migration 3.4.4, il échantillonne aussi les frames décodées en place, sans le blit ni le snapshot décrits ci-dessus, et ferme chaque import par `GPUSharedTextureMemory.destroy()` ; voir l'[audit](../migration/skia-3.4.4/upstream-audit.md#passe-export-ios-de-cette-migration). Sur Android, l'export dessine désormais dans des AHardwareBuffer prêtés par l'encodeur, échantillonnés par EGL dans la Surface du codec, si l'interop AHB est négociée ; voir l'[audit](../migration/skia-3.4.4/upstream-audit.md#passe-export-android-14-octobre-soir--compilée-non-mesurée). Depuis la migration 3.6.1, il échantillonne aussi en place les frames décodées par `ImageReader` : chaque buffer YUV, importé une fois, est converti dans une texture RGBA par producteur, sans snapshot ni attente par clip ; voir l'[audit](../migration/skia-3.6.1/upstream-audit.md#import-en-place-à-lexport-android). Le paragraphe suivant décrit le chemin de readback, désormais repli sur les deux plateformes.
32
+
33
+ L'export par readback garde son `readPixels` vers un buffer CPU réutilisé puis le pool encodeur natif. Les modes publics `copy` et `direct` restent des options de compatibilité, avec le même transport effectif dans ce backend. Android emploie AHardwareBuffer quand les capacités requises sont disponibles, et conserve un fallback CPU RGBA scoped-copy. La voie AHB évite le readback d'entrée et son upload CPU, comme détaillé ci-dessous ; elle ne transforme pas l'export composé en sortie sans copie.
34
+
35
+ ## Android : éviter le cache d'images raster du provider
36
+
37
+ Les sources **installées de Skia 3.0.6** montrent un autre cache, distinct des budgets GPU en octets : `node_modules/react-native-skia/cpp/rnskia/RNImageProvider.h:21,49–62,97` conserve jusqu'à **256 images** dans un LRU de références fortes `sk_sp<SkImage>`. Le provider convertit une image raster avec `TextureFromImage` puis conserve l'image GPU obtenue. `cpp/skia/include/gpu/graphite/ImageProvider.h:19–23` indique que Graphite consulte ce provider pour les images qui ne sont pas déjà Graphite. Le provider persiste avec chaque Recorder ; voir `cpp/rnskia/RNDawnContext.h:88–92,343–350` dans le même paquet.
38
+
39
+ Créer une nouvelle image raster pour chaque frame vidéo fournit donc de nouvelles clés au cache. Fermer nos wrappers raster ne retire pas les images GPU détenues par ce provider. Sa limite porte sur un nombre d'images, indépendamment de leur résolution ; les budgets de cache Graphite ne peuvent pas garantir l'éviction de ressources encore référencées. Le budget vidéo comptabilise nos propriétaires, pas ces références amont. Cette chaîne explique une rétention possible hors budget vidéo ; elle ne suffit pas à attribuer un relevé PSS/RSS ou à prouver une fuite.
40
+
41
+ Les deux voies Android de [frameInterop.ts](../src/frameInterop.ts) évitent ce cache : elles ne créent plus de `SkData` ni d'image raster. Le fallback testé conserve le readback EGL, puis `copyPixelsTo` copie les pixels dans **un ArrayBuffer CPU privé réutilisable par runtime, dimensions et stride**, sans matérialiser le getter natif `texture.data`. `queue.writeTexture` alimente ensuite une texture d'import réutilisable et Skia en fait un snapshot Graphite indépendant. La lecture explicite de `texture.data` reste disponible aux consommateurs ; leurs alias ont une durée de vie distincte.
42
+
43
+ La voie AHardwareBuffer remplace ce transfert CPU d'entrée par **rendu EGL du codec dans un AHardwareBuffer RGBA → blit WebGPU → texture d'import RGBA → snapshot Graphite indépendant**. [gpuDevice.ts](../src/gpuDevice.ts) négocie avant création des décodeurs : le device Skia partagé doit exposer `shared-texture-memory-ahardware-buffer` et `shared-fence-sync-fd`, et le wrapper natif doit être disponible. Un refus de capacité identifié permet le fallback ; une erreur de quota, d'allocation ou de programmation reste une erreur. Dans WebGPU 0.12.1, `AndroidPlatformContext.h:390–433` et `GPUExternalTexture.cpp:318–354` importent l'AHB RGBA mais l'exposent avec le marqueur `pixelFormat: 'bgra8'` pour une source RGB à un seul plan. Le format réel de destination Android reste RGBA. L'API d'import ne transmet pas de fence producteur : [NativeHardwareBuffer.cpp](../android/cpp/NativeHardwareBuffer.cpp) termine et vérifie les écritures EGL avant publication du pointeur.
44
+
45
+ Dans les deux voies, chaque écriture utilise un wrapper de surface neuf, une soumission vérifiée et une attente avant fermeture des leases ou réécriture de l'intermédiaire. Les transitions CPU/AHB ferment le scratch CPU inutilisé, même à dimensions identiques. Dimensions, stride et stockage sont contrôlés avant allocation ; les buffers, textures et snapshots possédés sont réservés dans le budget vidéo. Un drain échoué conserve les ressources et bloque les imports suivants. Aucun cache global ni device partagé n'est purgé ou remplacé.
46
+
47
+ Les backings Java/JSI exposés aux consommateurs gardent leur durée de vie **ART/Hermes à deux phases** : leurs vues et alias ArrayBuffer doivent mourir avant restitution de la réservation. Retirer notre référence au scratch CPU et sa réservation logique ne garantit pas un retour physique immédiat de ses pages. Aucun GC forcé n'est ajouté. Le fallback a terminé les huit opérations ciblées sans OOM, mais a échoué aux huit contrôles de fermeture après deux secondes ; ce résultat ne prouve ni fuite permanente ni stabilité mémoire.
48
+
49
+ Sur Pixel 8a, le binaire AHB a passé les **24 essais et leurs fermetures strictes**, avec **986 relevés nominal/normal/active**, un budget de **512 Mio** et les ressources suivies finales à zéro. L'oracle CPU/AHB séparé a passé **10/10 cas** : disposition de lignes 638×358, portrait, rotation 90°, H.264 et HEVC 4K, en copy/direct. Les **66 560 canaux RGBA échantillonnés** sont identiques, y compris sur les frames retenues ; ce diagnostic effectue un readback et ne mesure pas la performance. Les tests natifs 5×3 vérifient séparément pixels, stride, finish/abort, retenue après fermeture EGL et quota. Ces preuves ne qualifient pas toute l'image, le HDR, l'audio, la mémoire complète du pilote ou l'absence de fuite longue. Lors d'une mise à jour Skia, relire le provider, les formats AHB et les contrats de fences avant de changer la stratégie vidéo.
50
+
51
+ Une passe séparée a passé **4/4 cas de cycle de vie**, une répétition chacun : pause/reprise, retrait des items fermés, pause/attente suivie de seeks, et composition de huit clips 4K en lazy/direct, avec **25 cycles courts et 40 remontages**. Ce dernier cas a atteint **800,25 Mio de RSS, 1 037,91 Mio de PSS et 257,08 Mio de réservations suivies** au pic ; le RSS final reste à **785,82 Mio** malgré les réservations à zéro. Ces pics sont échantillonnés indépendamment et n'attribuent pas les pages aux caches Graphite, au pilote ou aux codecs. Le cas nommé `held-image-survives-seek` ne conserve pas de SkImage distincte et ne valide pas ses pixels : la preuve de retenue reste l'oracle raw frame séparé. Les **5 tests JNI CPU** ont aussi repassé sur le binaire AHB, sans skip. Aucune stabilité sur longue durée ni configuration à 256 Mio n'est qualifiée.
52
+
53
+ ## Historique 3.0.3 : pourquoi éviter `Skia.NativeBuffer.MakeFromImage`
54
+
55
+ En 3.0.3, le chemin Apple de `makeNativeBuffer` réalise une lecture des pixels sur CPU, construit un IOSurface puis un CVPixelBuffer. Ce n'est pas une sortie vidéo sans copie. Dans [`apple/RNSkApplePlatformContext.mm`, version 3.0.3](https://github.com/wcandillon/react-native-skia/blob/v3.0.3/packages/skia/apple/RNSkApplePlatformContext.mm#L122-L174), les objets retournés par `CFNumberCreate`, le dictionnaire créé par `CFDictionaryCreateMutable` et l'IOSurface créé par `IOSurfaceCreate` n'ont pas de libération correspondante dans ce chemin, y compris lors de certaines erreurs.
56
+
57
+ **Décision historique, conservée : ne pas utiliser cette fonction dans la boucle d'export.** L'encodeur de référence reçoit des pixels RGBA/BGRA et utilise son propre pool borné. Cela évite de dépendre de cette allocation amont par frame. Corriger seulement les références Core Foundation ne transformerait pas la fonction en export GPU direct : les copies CPU resteraient présentes.
58
+
59
+ Cette API est maintenant supprimée en 3.0.6. Si une future API de sortie de buffers natifs apparaît, reprendre les critères suivants :
60
+
61
+ 1. Lire la nouvelle fonction et les chemins d'erreur, vérifier l'équilibre de chaque Create/Retain et du release du CVPixelBuffer retourné.
62
+ 2. Vérifier le format, les strides, le coût de copie et l'ownership de l'objet exporté, indépendamment de la correction des références.
63
+ 3. Exécuter des exports longs, répétés et annulés sur appareils iOS physiques, avec des tailles alternées ; vérifier les compteurs de session et un palier des allocations natives/GPU après warmup.
64
+ 4. Comparer au chemin de référence avec les mêmes fichiers, dimensions, codecs et collecteurs. Activer le nouveau chemin explicitement seulement si ses pixels, timestamps, fermeture et résultats mémoire sont qualifiés.
65
+
66
+ Aucun correctif de cette fonction n'est inclus dans ce dépôt et aucune issue amont n'a été publiée dans le cadre de ce travail. Cette analyse historique ne décrit pas une fonction encore disponible dans le build 3.0.6.
67
+
68
+ ## Prototype futur : rendre dans le buffer de l'encodeur
69
+
70
+ **Implémenté le 8 octobre 2026** (sortie seulement, une frame à la fois, attente explicite), puis complété en 3.4.4 par la fermeture immédiate des imports et l'entrée en place. La proposition d'origine est conservée ci-dessous ; la version asynchrone par propagation de fences reste à faire.
71
+
72
+ **Proposition à discuter, sans implémentation ni validation de performances.** Le backend actuel attend le GPU puis appelle `readPixels` et transmet ces pixels au pool natif, pour les deux valeurs de `encoderMode` : [exportVideoComposition.ts](../src/exportVideoComposition.ts). Les deux valeurs de `textureMode` produisent des images possédées selon le chemin décrit ci-dessus : [frameInterop.ts](../src/frameInterop.ts). Le [contrat public](../src/types.ts) décrit cette compatibilité. Le mot `direct` dans un scénario ne prouve donc pas que le nouveau chemin évite les copies.
73
+
74
+ L'ancien moteur iOS utilisait déjà Metal. Son export direct copiait sur GPU la texture vers une vue Metal du CVPixelBuffer destiné à l'encodeur. La piste d'optimisation du candidat consiste surtout à supprimer son aller-retour CPU par image ; ajouter un autre canvas natif ne règle pas à lui seul cet intermédiaire. La comparaison de bibliothèques doit conserver cette différence de chemins dans son interprétation.
75
+
76
+ Les sources des versions ciblées contiennent plusieurs briques utiles :
77
+
78
+ - WebGPU 0.12.1 expose `importSharedTextureMemory`, puis `createTexture`, `beginAccess` et `endAccess`. Sur iOS, son `handle` est un **IOSurfaceRef**, tandis que `createVideoFrameFromNativeBuffer` reçoit un **CVPixelBufferRef**. Ne pas intervertir ces pointeurs. La fermeture explicite des ressources d'import reste à vérifier pour le prototype de sortie. [Description officielle des imports natifs](https://wcandillon.github.io/react-native-webgpu/docs/getting-started/native-api).
79
+ - Skia 3.0.6 expose `Skia.Surface.MakeFromGPUTexture` pour dessiner dans une texture du device partagé ; le wrapper exige l'usage `RenderAttachment`. [Source versionnée](https://github.com/wcandillon/react-native-skia/blob/v3.0.6/packages/skia/cpp/rnskia/RNDawnContext.h). Cette existence ne prouve pas que tous les formats ou buffers vidéo importés sont des cibles de rendu acceptées.
80
+ - Le pool de [VideoEncoderHostObject.mm](../ios/VideoEncoderHostObject.mm) crée des buffers BGRA compatibles Metal/IOSurface, refuse de dépasser trois allocations et attend leur disponibilité. Sa branche native `encodeFrame(CVPixelBufferRef, ...)` peut soumettre directement un buffer ; elle n'est pas utilisée par le chemin JavaScript actuel de lecture des pixels.
81
+
82
+ Le device principal de Skia 3.0.6 demande `SharedTextureMemoryIOSurface` et `SharedFenceMTLSharedEvent` dans [RNDawnUtils.h](https://github.com/wcandillon/react-native-skia/blob/v3.0.6/packages/skia/cpp/rnskia/RNDawnUtils.h). Le commentaire natif impose une soumission avant `EndAccess` sur ce chemin Metal ; le device secondaire possède un contrat différent et omet délibérément cette feature de fence. Le prototype doit donc utiliser le device principal partagé, vérifier ses capacités effectives et ne pas transposer le chemin caméra du device secondaire.
83
+
84
+ Le plus petit prototype iOS devrait porter sur **la sortie seulement**, à taille fixe, avec une frame préparée à la fois. Il conserverait le décodage et `drawFrame` existants :
85
+
86
+ 1. Acquérir un CVPixelBuffer du pool par une nouvelle interface native de session. La session doit conserver explicitement le buffer et fournir un identifiant de réservation, son IOSurface, ses dimensions et son format ; un pointeur nu sans propriétaire ne suffit pas.
87
+ 2. Importer cet IOSurface sur le device de Skia, vérifier les usages/format permis, ouvrir son accès et envelopper la texture dans une surface Skia. Initialiser toute l'image avant de dessiner. Ne pas créer de snapshot intermédiaire pour ce chemin.
88
+ 3. Soumettre le dessin Skia, fermer l'accès partagé et établir la fin effective des écritures GPU avant la soumission à AVAssetWriter. Le premier prototype peut attendre cette fin explicitement ; une version asynchrone ultérieure demandera une propagation correcte des fences. `endAccess` ou l'acceptation d'une commande ne doit pas être confondu avec la fin physique de son exécution.
89
+ 4. Transmettre le CVPixelBuffer à l'encodeur, détacher et fermer ses consommateurs GPU, puis relâcher la réservation de la session dans un ordre prouvé. AVAssetWriter peut conserver le buffer après son acceptation : le pool doit attendre sa restitution, sans réécrire manuellement dans un buffer encore en cours d'encodage.
90
+ 5. Traiter annulation, erreur d'import, erreur d'encodage et échec de synchronisation sans fermer une ressource encore utilisée. Garder des files et réservations bornées, comptabiliser les allocations réelles et revenir au chemin de référence si la capacité requise manque avant le démarrage. Une perte du device partagé demande l'arrêt du travail concerné, pas la création silencieuse d'un device de remplacement.
91
+
92
+ Cette architecture pourrait éviter la lecture CPU de la **sortie composée**. Elle ne supprimerait pas automatiquement la copie des frames d'entrée, les conversions du codec ni toutes les copies internes du pilote. Elle ne doit pas être présentée comme une vidéo intégralement « zero-copy ». L'équivalent Android exige une étude distincte des buffers et de l'entrée de MediaCodec.
93
+
94
+ **Décision proposée : essayer les APIs existantes avant un nouveau patch Skia.** Le travail principal appartient à l'interface entre notre pool vidéo, l'accès partagé et l'encodeur. Si une capacité manque réellement, isoler un reproducer et proposer le plus petit changement amont ; ne pas maintenir un fork général de Graphite ou Dawn. Les critères de passage seront les pixels/couleurs/timestamps, les annulations et changements de taille, puis les mêmes sessions longues sur appareils avec mesure de mémoire et durée. Le petit patch de durée de vie décrit ci-dessous reste indépendant de ce prototype.
95
+
96
+ ## Patch distinct : durée de vie et soumissions vérifiées
97
+
98
+ Ces quatre protections restent nécessaires en **3.4.4**, puis en **3.6.1**, dont les fichiers patchés sont identiques à ceux de 3.4.4 : l'amont n'a ajouté ni `dispose()` de canvas, ni contrôle d'`insertRecording`, ni garde du snapshot nul, et sa factory d'`ImageProvider` n'a pas changé. Le `dispose()` rebasé remet la surface au `Dispatcher` de son thread créateur, comme le fait désormais le destructeur amont du canvas. Le reste de cette section décrit le patch d'origine.
99
+
100
+ Dans Skia 3.0.3 et encore dans les sources 3.0.6, le wrapper `JsiSkCanvas` peut garder une référence propriétaire à la surface dont il provient. Disposer uniquement le wrapper `SkSurface` ne ferme donc pas cette référence tant que le canvas est encore détenu ; attendre le GC pour les surfaces temporaires est insuffisant pour une fermeture déterministe.
101
+
102
+ Le patch local ajoute `canvas.dispose()`, idempotent, qui annule `_canvas` et relâche `_surface`, ainsi que la déclaration TypeScript. Les accès ultérieurs via `getCanvas()` échouent explicitement.
103
+
104
+ Il vérifie aussi le résultat de `makeImageSnapshot()` dans `JsiSkSurface.h`. En cas d'échec d'allocation, une erreur est levée avant de remplacer l'image détenue par le consommateur ou de construire un wrapper autour d'une image nulle. Cette garde est nécessaire sous pression mémoire. Le patch ne restaure pas `Skia.NativeBuffer`.
105
+
106
+ La migration ajoute aussi une activation interne par surface, `enableCheckedSubmissions()`, appelée par [getVideoCanvas](../src/canvas.ts). Pour ces seules surfaces, la soumission de `flush` et celle de `makeImageSnapshot` refusent un recording nul et vérifient les résultats de `insertRecording` et `submit`. Les surfaces qui n'activent pas ce mode conservent le comportement amont. Cette méthode est une garde privée de notre adaptateur, pas une nouvelle API publique à utiliser dans une application de montage.
107
+
108
+ Cette protection répond à un cas précis : le blit WebGPU peut déjà être soumis lorsque Graphite échoue. Un retour normal de `flush(true)` ne doit pas être interprété comme une preuve de fin de queue si l'amont a ignoré l'échec d'insertion ou de soumission. Les chemins d'erreur conservent les leases et leurs réservations jusqu'à un drain réussi, et refusent de les fermer lors d'un retry du même frame encore retenu.
109
+
110
+ La [source Skia de ce build](https://chromium.googlesource.com/skia/+/2466dcf3937437e217e7f284afe0e1aae15891ce/src/gpu/graphite/Recorder.cpp) distingue un recording vide valide d'un recording nul signalant un échec. Le mode vérifié accepte donc le premier. En fonctionnement normal, [DawnQueueManager](https://chromium.googlesource.com/skia/+/2466dcf3937437e217e7f284afe0e1aae15891ce/src/gpu/graphite/dawn/DawnQueueManager.cpp) soumet sur la même queue que WebGPU puis attend `OnSubmittedWorkDone` pour le flush synchrone. Cette revue de source et les tests JavaScript ne remplacent pas la qualification du chemin optimisé sur appareil ; ils ne garantissent pas une récupération générale après perte du device.
111
+
112
+ Fichiers inspectables :
113
+
114
+ - [`patches/react-native-skia+3.6.1.patch`](../patches/react-native-skia+3.6.1.patch) : diff du correctif courant, identique à celui de 3.4.4 ; ceux de 3.4.4 et 3.0.6 sont conservés comme historique.
115
+ - [`scripts/skia-canvas-patch.json`](../scripts/skia-canvas-patch.json) : version, empreintes avant/après et substitutions attendues.
116
+ - [`scripts/apply-skia-canvas-patch.mjs`](../scripts/apply-skia-canvas-patch.mjs) : vérifie tout avant toute écriture, accepte un patch déjà appliqué, refuse un paquet différent.
117
+
118
+ Les appels de la bibliothèque doivent soumettre et attendre les enregistrements concernés avant cette fermeture, sur le runtime propriétaire. Le patch n'autorise pas la libération anticipée d'une texture encore utilisée par le GPU. Reconstruire le binaire après son application et refaire les cycles de preview/export.
119
+
120
+ L'application consommatrice doit intégrer le patch avant sa compilation native, dans son propre mécanisme d'installation/CI. Le script conservé dans le paquet est une entrée explicite ; il n'est pas appliqué silencieusement par le chargement JavaScript. Lors d'une mise à jour Skia, conserver l'échec du garde-fou jusqu'à la revue de la nouvelle durée de vie et des erreurs d'allocation. Retirer chaque partie du patch lorsque l'amont offre une protection équivalente et que les tests de ressources l'ont confirmé.
121
+
122
+ ### Readback d'export : garde amont à étudier séparément
123
+
124
+ **En 3.4.4**, `canvas.readPixels` lit le rectangle demandé directement dans la destination via `JsiSkImage::readPixelsInto`, et `DawnContext::readPixelsSync` attend hors du verrou du contexte, un thread dédié interrogeant le GPU. Le readback ne fige donc plus les autres vues Skia. Le snapshot du canvas n'est toujours pas vérifié, et le résultat nul est désormais propagé en `null` plutôt que déréférencé. Le paragraphe suivant décrit les sources 3.0.6.
125
+
126
+ Dans les sources 3.0.6, `JsiSkCanvas::readPixels` crée son propre snapshot et le soumet directement à `DawnContext`, sans passer par le mode vérifié du wrapper surface. Ce chemin ne vérifie pas son snapshot nul. `DawnContext::MakeRasterImage` ne vérifie pas non plus le résultat nul du callback asynchrone avant son utilisation et ignore le résultat de sa soumission. Les sources sont [JsiSkCanvas.h](https://github.com/wcandillon/react-native-skia/blob/v3.0.6/packages/skia/cpp/api/JsiSkCanvas.h) et [RNDawnContext.h](https://github.com/wcandillon/react-native-skia/blob/v3.0.6/packages/skia/cpp/rnskia/RNDawnContext.h).
127
+
128
+ Ce sont des chemins d'échec à reproduire sous pression mémoire ou perte du device, pas un crash attribué aux mesures actuelles. Le flush vérifié avant le readback sécurise le travail déjà enregistré ; il ne transforme pas ces opérations suivantes en soumissions vérifiées. Aucun correctif supplémentaire de ce readback n'est inclus dans l'optimisation d'entrée. Avant de supprimer une attente ou de modifier l'export, isoler ce contrat et tester les échecs et la fermeture ; conserver le petit patch existant dans son périmètre actuel.
129
+
130
+ ## Couple Skia / WebGPU et anciens modules natifs
131
+
132
+ Le partage de textures demande le même Dawn et le même device natif. Le couple actuel ne doit pas être mis à jour séparément sans revue des versions natives et des APIs d'interop. Une nouvelle version de package ou un nouveau SDK peut changer les formats, les fences et la durée de vie des buffers.
133
+
134
+ Le device possédé par Skia doit rester possédé par Skia. Un adaptateur de la bibliothèque ne doit pas le détruire à la fermeture d'une session. Les imports natifs Metal/GL historiques, les définitions Ganesh et les modules compilés contre les anciens headers demandent une migration explicite. Réappliquer le patch de destruction de surface de l'application Skia 2 à Skia 3 sans cette revue serait une hypothèse injustifiée.
135
+
136
+ La paire a été vérifiée dans les **archives npm publiées**, sans contourner le contrôle du podspec : Graphite Apple iOS 154.1.0 contient `libs/.dawn-version = dawn-chrome-m154a` ; WebGPU 0.12.1 annonce `chrome-m154a`, commit Dawn `3d786993a7ded64c4ebb4884b9b079db9ad0e580`. Le couple historique mesuré 3.0.3 / 0.11.0 utilisait le tag `dawn-chrome-m154`. Ne pas mélanger ces binaires dans une série A/B. La [documentation d'interop](https://wcandillon.github.io/react-native-webgpu/docs/integrations/react-native-skia) demande un couple compatible et une instance Dawn commune.
137
+
138
+ La [release Skia 3.0.5](https://github.com/wcandillon/react-native-skia/releases/tag/v3.0.5) annonce le déplacement des APIs NativeBuffer. La [release 3.0.6 du 7 octobre](https://github.com/wcandillon/react-native-skia/releases/tag/v3.0.6) livre Graphite 154.1.0, le build Android avec RTTI et un correctif de publication SwiftPM. La comparaison des tarballs 3.0.5 et 3.0.6 ne trouve qu'un changement de `package.json` : les sources d'interop sont identiques. Le patch de durée de vie reste nécessaire et doit porter les empreintes 3.0.6.
139
+
140
+ **Depuis Skia 3.4.x / WebGPU 0.14.x**, les deux paquets dépendent du même paquet npm `react-native-webgpu-dawn` ; le podspec et le Gradle de Skia comparent les versions résolues de ce paquet, et non plus une chaîne de tag. Le couple courant est 3.4.4 / 0.14.2 avec `react-native-webgpu-dawn@154.3.0`. Associer Skia 3.0.6 à WebGPU 0.14.x passerait l'ancien contrôle de tag tout en mélangeant deux builds Dawn : ne pas le faire.
141
+
142
+ WebGPU 0.12.0 ajoute le blit de `NativeVideoFrame` dans `copyExternalImageToTexture`, des helpers publics de vidéo et des contrôles supplémentaires de lecteur. Notre migration conserve ses propres codecs ; elle utilise le wrapper et le blit. Entre 0.12.0 et 0.12.1, les sources JS/C++ de binding sont identiques : les changements portent sur les binaires Dawn et leur packaging.
143
+
144
+ Le suffixe **m154a** désigne une fabrication corrigée de Dawn, pas une nouvelle version du standard WebGPU. Le [correctif Tint au tag exact](https://github.com/wcandillon/react-native-webgpu/blob/dawn-chrome-m154a/packages/webgpu/scripts/dawn-patches/tint-looprange-function-variable.patch) adapte les variables de boucle pour éviter un blocage GPU documenté sur Pixel 8 / Mali-G715. Il ne démontre aucun gain de vitesse sur iPhone. Les pages de documentation non versionnées doivent toujours être confrontées au tag et aux dépendances verrouillées ; le fait qu'une API y figure ne prouve pas sa présence dans un ancien build.
145
+
146
+ ## Pourquoi investir dans WebGPU pour la composition vidéo
147
+
148
+ WebGPU est l'API de rendu et de calcul GPU ; Dawn en est une implémentation native utilisant notamment Metal et Vulkan ; Graphite est le backend de rendu de Skia ; React Native Skia 3 expose ce rendu à l'application. Three.js ajoute un moteur 3D. Core ML/Vision et les modèles ML restent des composants séparés. Une annonce concernant l'un de ces niveaux ne livre pas automatiquement une capacité dans tous les autres. [Présentation officielle de Dawn](https://dawn.googlesource.com/dawn/+/refs/heads/main/README.md).
149
+
150
+ William Candillon explique que React Native WebGPU vise la 3D et le calcul GPU général, puis décrit Redraw comme une bibliothèque de nouvelles primitives vectorielles communes au web et au natif. Ce sont ses objectifs publiés, pas une hypothèse sur ses motivations personnelles. [Présentation de William](https://wcandillon.dev/).
151
+
152
+ Pour une application de montage avec widgets, la valeur immédiate à explorer est le partage de textures entre une petite scène 3D ou un effet de calcul et le canvas Skia qui porte déjà les widgets. L'[intégration Three.js](https://wcandillon.github.io/react-native-webgpu/docs/integrations/three-js) décrit une scène rendue sur le device de Skia puis composée comme une image. Ce partage peut éviter une copie entre ces deux rendus ; il ne préjuge pas du chemin d'export vidéo ni de son coût global. Le [guide de migration](MIGRATION.md) distingue les fonctionnalités disponibles des intégrations encore à construire.
153
+
154
+ La mention d'une prochaine « version WebGPU par Google » n'a pas pu être rattachée à une annonce précise dans cette étude. Ne pas décider de la migration à partir de cette formulation seule : relever le lien, la date, le composant concerné, puis sa disponibilité dans les versions natives retenues. Une sortie Chrome ou Dawn ne met pas automatiquement à jour le binaire de l'application iPhone. Aucun suivi automatique n'est créé par cette note.
155
+
156
+ ## Caches Graphite : budget indépendant des ressources vidéo
157
+
158
+ Les headers inspectés dans **Skia 3.0.3 et 3.0.6** fixent un budget GPU par défaut de **256 Mio pour chaque Recorder** et de **256 Mio pour le Context**. Références exactes dans ces paquets : `cpp/skia/include/gpu/graphite/Recorder.h:79–81` et `ContextOptions.h:123–127`. Ce sont des budgets de ressources allouées/retenues et de cache ; cela ne signifie pas que 256 Mio sont alloués en permanence, ni que le RSS leur est attribuable.
159
+
160
+ Le wrapper [RNDawnContext.h de Skia 3.0.3](https://github.com/wcandillon/react-native-skia/blob/v3.0.3/packages/skia/cpp/rnskia/RNDawnContext.h) crée les options et le Recorder en `thread_local` aux lignes 429–436 et le Context avec son budget par défaut aux lignes 480–485, sans override de ces budgets. Les sources 3.0.6 conservent cette politique par défaut. Le Recorder appartient à un thread, pas à chaque export ; plusieurs threads peuvent posséder leurs Recorders. Ces budgets ne sont pas comptés par le budget `ownedBytes` de la vidéo et ne plafonnent pas le RSS du processus.
161
+
162
+ Les APIs natives distinguent `currentBudgetedBytes`, `currentPurgeableBytes`, `maxBudgetedBytes` et `setMaxBudgetedBytes` (`Recorder.h:218–238`, `Context.h:240–260`). Une baisse du budget tente de libérer des ressources ; ce n'est pas un plafond strict de mémoire physique. `freeGpuResources` conserve les ressources en cours d'utilisation et demande de soumettre/attendre les travaux pour une fermeture complète (`Recorder.h:200–206`, `Context.h:222–228`).
163
+
164
+ **Point futur amont : obtenir une API prise en charge pour inspecter/configurer ces caches au niveau de l'application.** La bibliothèque ne purge pas globalement Graphite, ne modifie pas le budget du device partagé et ne le détruit pas. Une politique globale décidée par la vidéo pourrait perturber les scènes 3D, compute ou ML de l'application. Lors d'une mise à jour Skia, revoir ces valeurs, l'affinité des Recorders et les APIs exposées, puis qualifier toute politique de cache avec les autres consommateurs GPU. Les relevés RSS du simulateur ne permettent pas de conclure quels caches ou allocateurs expliquent leur variation.
165
+
166
+ La [comparaison du 6 octobre 2026](PERFORMANCE_AB_SOAK_SIMULATOR.md) apporte désormais une référence à rejouer après chaque changement : 300 exports par moteur, mêmes dimensions et collecteur, puis trois minutes de repos. Le RSS final du candidat progresse de 416 à 553 puis 626 Mio, contre environ 308/315/300 Mio pour l'ancien ; au repos à trois minutes, il reste à 437,38 Mio contre 133,20 Mio. Les compteurs vidéo du candidat reviennent à zéro. Ces chiffres motivent l'inspection des allocations encore résidentes, mais **n'attribuent pas la différence aux caches Graphite** : une preuve par catégorie d'allocation reste nécessaire avant de modifier une politique globale.
167
+
168
+ Une autre catégorie à instrumenter est le buffer CPU de [l'export](../src/exportVideoComposition.ts) : un `Uint8Array` est alloué par session, réutilisé pour les frames, puis sa référence est mise à null après fermeture ; le runtime d'export persiste entre sessions. La disparition de cette référence et de sa réservation comptable n'atteste pas la restitution immédiate du backing store ou des pages de l'allocateur. Distinguer ce stockage, les rasters transitoires du readback, les codecs et les ressources Graphite avant d'attribuer la hausse à l'un d'eux. Cette lecture de code ne prouve pas l'origine de la rétention observée et n'ajoute ni GC forcé ni nouveau cache partagé.
169
+
170
+ ### Mesure Android et référence orpheline du provider — 7 octobre 2026
171
+
172
+ L'[investigation sur Pixel 8a](ANDROID_MEMORY_RETENTION_306.md) a mesuré les caches du couple 3.0.6 / 0.12.1 : le binaire diagnostique observe les Recorders sur leurs threads propriétaires et compare observation, drain seul et drain avec purge. Le rapport conserve les catégories mémoire, les compteurs et les limites de ce test. Ce mécanisme n'est pas ajouté au patch de production ; une purge du Context partagé demande une politique au niveau de l'application.
173
+
174
+ Autre point concret à revoir lors de la prochaine mise à jour : `RNImageProvider.h` amont en 3.0.6 construit son provider avec `sk_ref_sp(new ImageProvider)`. Ce helper incrémente la référence initiale au lieu de l'adopter. Le patch versionné remplace maintenant cette expression par `sk_make_sp<ImageProvider>()`. Le [test de propriété](../scripts/test-skia-provider-ownership.mjs) compile le corps de la factory installée avec le véritable `SkRefCnt` et un objet dont on compte les destructions ; il vérifie aussi la survie d'un alias puis la destruction à sa fermeture. Cette correction ne suffit pas à expliquer la mémoire restante, comme le montre le [test isolé sur appareil](ANDROID_EXPORT_CACHE_306.md).
175
+
176
+ Le patch ajoute également une méthode privée `__rnskvTrimRecorderCache` : elle soumet et attend le recording courant en vérifiant les erreurs, relâche ce recording, puis appelle `freeGpuResources` sur ce seul Recorder. Elle ne purge pas le Context. La bibliothèque l'utilise uniquement sur son runtime d'export privé, après fermeture des consommateurs et avant restitution des réservations. Une erreur de drain conserve la quarantaine et interdit de réutiliser ce runtime. La preview ne l'appelle pas ; ses caches et ceux des autres Recorders restent inchangés. Recompiler le binaire natif après réapplication du patch : l'export refuse une ancienne version avant ses allocations si la méthode manque.
177
+
178
+ Lors d'une mise à jour amont, vérifier séparément la correction de la factory, l'affinité de thread du Recorder, les erreurs de soumission et une éventuelle API officielle de nettoyage. Ne pas remplacer cette politique limitée par une purge globale à chaque frame.
179
+
180
+ ## Référence fournie : `enzomanuelmangano/demos`
181
+
182
+ Le [dépôt de démos](https://github.com/enzomanuelmangano/demos) est utile pour choisir des scènes visuelles : transitions, reflets de shader et interactions pilotées par des valeurs partagées. Le [manifest consulté](https://github.com/enzomanuelmangano/demos/blob/main/package.json) fixe encore Skia 2.6.2 et WebGPU 0.5.14 ; ses patches de compatibilité ne sont donc pas une preuve de compatibilité avec le couple désormais ciblé 3.0.6/0.12.1, ni avec le couple mesuré 3.0.3/0.11.0.
183
+
184
+ Les idées retenues pour les futurs adaptateurs sont une transition à progression déterministe, un shader piloté par les mêmes paramètres en preview/export et une scène 3D calculée au temps de composition. Aucun code ou asset de ce dépôt n'est copié dans la bibliothèque. Sa [licence publiée](https://github.com/enzomanuelmangano/demos/blob/main/LICENSE.md) comporte des restrictions de redistribution et de création de bibliothèques d'animation concurrentes ; la référence sert ici à l'étude visuelle. Les implémentations de comparaison doivent être originales ou venir d'une source réutilisable dans ce paquet.
185
+
186
+ ## Procédure de mise à jour
187
+
188
+ Pour chaque nouvelle version Skia : relever le couple Dawn/WebGPU et les versions RN/Reanimated/Worklets compatibles, relire les imports/sorties de buffers, les soumissions vérifiées, la fermeture canvas/snapshot et les budgets/caches Graphite, revoir les empreintes, compiler les deux plateformes, exécuter les tests de pixels/timestamps puis les **65 workloads du catalogue `2026-10-07.1`** (59 historiques, trois lectures continues `copy` et leurs trois équivalents `direct`) et les sessions longues. La configuration iPhone déjà compilée pour la référence 3.0.3 utilise RN 0.86.2, Reanimated 4.5.3 et Worklets 0.11.3 ; ces versions sont conservées pour la migration 3.0.6. Ne pas laisser une plage ouverte sélectionner une autre version sans refaire l'installation native. Documenter la version et le transport effectif dans les résultats. La présence d'un cas dans le catalogue ne signifie pas qu'il a été exécuté sur toutes les cibles. Une correction amont ou des tests de l'hôte verts seuls ne justifient pas de supprimer un garde-fou mémoire.
@@ -0,0 +1,26 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1200" height="300" viewBox="0 0 1200 300" role="img" aria-labelledby="title desc">
2
+ <title id="title">React Native Skia WebGPU Video</title>
3
+ <desc id="desc">Play, compose, and export. Layered video frames on a dark blue background.</desc>
4
+ <defs>
5
+ <linearGradient id="bg" x2="1" y2="1"><stop stop-color="#0b1225"/><stop offset="1" stop-color="#17233b"/></linearGradient>
6
+ <linearGradient id="accent" x2="1" y2="1"><stop stop-color="#38bdf8"/><stop offset="1" stop-color="#a78bfa"/></linearGradient>
7
+ <pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse"><path d="M40 0H0V40" fill="none" stroke="#334155" stroke-opacity=".28"/></pattern>
8
+ </defs>
9
+ <rect width="1200" height="300" rx="22" fill="url(#bg)"/>
10
+ <rect width="1200" height="300" rx="22" fill="url(#grid)"/>
11
+ <rect x="54" y="54" width="5" height="192" rx="2.5" fill="url(#accent)"/>
12
+ <g font-family="-apple-system,BlinkMacSystemFont,Segoe UI,Arial,sans-serif">
13
+ <text x="82" y="87" fill="#7dd3fc" font-size="16" font-weight="600" letter-spacing="3">REACT NATIVE · SKIA 3</text>
14
+ <text x="79" y="157" fill="#f8fafc" font-size="55" font-weight="700" letter-spacing="-1.8">WebGPU Video</text>
15
+ <text x="82" y="206" fill="#cbd5e1" font-size="24">Play. Compose. Export.</text>
16
+ <text x="82" y="242" fill="#94a3b8" font-size="16">One timeline. Your Skia canvas.</text>
17
+ </g>
18
+ <g transform="translate(835 51)">
19
+ <rect x="60" y="0" width="214" height="132" rx="14" fill="#172b44" stroke="#38bdf8" stroke-opacity=".45"/>
20
+ <rect x="30" y="34" width="214" height="132" rx="14" fill="#1b2947" stroke="#818cf8" stroke-opacity=".65"/>
21
+ <rect y="68" width="214" height="132" rx="14" fill="#0f172a" stroke="url(#accent)" stroke-width="2"/>
22
+ <path d="m88 104 50 30-50 30z" fill="url(#accent)"/>
23
+ <path d="M21 179h51m10 0h30m10 0h72" stroke="#475569" stroke-width="4" stroke-linecap="round"/>
24
+ <path d="M21 179h51" stroke="#38bdf8" stroke-width="4" stroke-linecap="round"/>
25
+ </g>
26
+ </svg>
@@ -0,0 +1,147 @@
1
+ import type { VideoFrame } from "../src/types";
2
+ import type { VideoFrameProcessor } from "../src/exportVideoComposition";
3
+ import { createVideoGpuScope } from "../src/gpu";
4
+ import {
5
+ GPUBufferUsage,
6
+ GPUShaderStage,
7
+ GPUTextureUsage,
8
+ } from "react-native-webgpu";
9
+
10
+ // Original minimal 3D example: one color target, one depth target, one uniform
11
+ // buffer, reused for the whole export. Draw frames['gpu-overlay'] with
12
+ // drawVideoFrame in the existing synchronous drawFrame callback.
13
+ const cubeShader = `
14
+ struct Params { value: vec4f };
15
+ @group(0) @binding(0) var<uniform> params: Params;
16
+ struct Vertex { @builtin(position) position: vec4f, @location(0) color: vec3f };
17
+ @vertex fn vertexMain(@builtin(vertex_index) index: u32) -> Vertex {
18
+ let points = array<vec3f,8>(
19
+ vec3f(-1,-1,-1), vec3f(1,-1,-1), vec3f(1,1,-1), vec3f(-1,1,-1),
20
+ vec3f(-1,-1,1), vec3f(1,-1,1), vec3f(1,1,1), vec3f(-1,1,1));
21
+ let indices = array<u32,36>(
22
+ 0,2,1,0,3,2,4,5,6,4,6,7,0,1,5,0,5,4,
23
+ 3,7,6,3,6,2,0,4,7,0,7,3,1,2,6,1,6,5);
24
+ let p = points[indices[index]] * 0.65;
25
+ let t = params.value.x;
26
+ let q = vec3f(cos(t)*p.x+sin(t)*p.z, p.y, -sin(t)*p.x+cos(t)*p.z);
27
+ let r = vec3f(q.x, cos(t*0.7)*q.y-sin(t*0.7)*q.z, sin(t*0.7)*q.y+cos(t*0.7)*q.z);
28
+ let z = r.z+3.0;
29
+ var out: Vertex;
30
+ out.position = vec4f(r.x/params.value.y, r.y, (z-0.1)*10.0/9.9, z);
31
+ out.color = (p+vec3f(0.65))/1.3;
32
+ return out;
33
+ }
34
+ @fragment fn fragmentMain(input: Vertex) -> @location(0) vec4f {
35
+ return vec4f(input.color, 1.0);
36
+ }`;
37
+
38
+ export const createCubeOverlay = async ({
39
+ width,
40
+ height,
41
+ }: {
42
+ width: number;
43
+ height: number;
44
+ }): Promise<VideoFrameProcessor> => {
45
+ "worklet";
46
+ const scope = createVideoGpuScope({ maxResources: 3 });
47
+ try {
48
+ const color = scope.createTexture({ width, height, label: "3D color" });
49
+ const depth = scope.createTexture({
50
+ width,
51
+ height,
52
+ format: "depth24plus",
53
+ skia: "none",
54
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
55
+ label: "3D depth",
56
+ });
57
+ const uniform = scope.createBuffer({
58
+ size: 16,
59
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
60
+ label: "3D time",
61
+ });
62
+ const device = scope.device;
63
+ const module = device.createShaderModule({ code: cubeShader });
64
+ const layout = device.createBindGroupLayout({
65
+ entries: [
66
+ {
67
+ binding: 0,
68
+ visibility: GPUShaderStage.VERTEX,
69
+ buffer: { type: "uniform" },
70
+ },
71
+ ],
72
+ });
73
+ const pipeline = await device.createRenderPipelineAsync({
74
+ layout: device.createPipelineLayout({ bindGroupLayouts: [layout] }),
75
+ vertex: { module, entryPoint: "vertexMain" },
76
+ fragment: {
77
+ module,
78
+ entryPoint: "fragmentMain",
79
+ targets: [{ format: "rgba8unorm" }],
80
+ },
81
+ primitive: { topology: "triangle-list" },
82
+ depthStencil: {
83
+ format: "depth24plus",
84
+ depthWriteEnabled: true,
85
+ depthCompare: "less",
86
+ },
87
+ });
88
+ const binding = device.createBindGroup({
89
+ layout,
90
+ entries: [{ binding: 0, resource: { buffer: uniform } }],
91
+ });
92
+ // Worklets capture primitive bindings separately. Share an object so the
93
+ // cleanup observes the last map published by prepareFrame.
94
+ const output: { published: Record<string, VideoFrame> | null } = { published: null };
95
+ return {
96
+ prepareFrame: ({ frames, currentTime, isCancelled }) => {
97
+ "worklet";
98
+ if (isCancelled()) return;
99
+ device.queue.writeBuffer(
100
+ uniform,
101
+ 0,
102
+ new Float32Array([currentTime, width / height, 0, 0]),
103
+ );
104
+ const encoder = device.createCommandEncoder();
105
+ const pass = encoder.beginRenderPass({
106
+ colorAttachments: [
107
+ {
108
+ view: color.texture.createView(),
109
+ clearValue: { r: 0, g: 0, b: 0, a: 0 },
110
+ loadOp: "clear",
111
+ storeOp: "store",
112
+ },
113
+ ],
114
+ depthStencilAttachment: {
115
+ view: depth.texture.createView(),
116
+ depthClearValue: 1,
117
+ depthLoadOp: "clear",
118
+ depthStoreOp: "store",
119
+ },
120
+ });
121
+ pass.setPipeline(pipeline);
122
+ pass.setBindGroup(0, binding);
123
+ pass.draw(36);
124
+ pass.end();
125
+ device.queue.submit([encoder.finish()]);
126
+ // Ordered with Skia on the SAME queue/device. No per-frame texture or
127
+ // image allocation; export flush/readback is the synchronization point.
128
+ output.published = frames;
129
+ frames["gpu-overlay"] = {
130
+ width,
131
+ height,
132
+ rotation: 0,
133
+ texture: { kind: "skia-image", image: color.image },
134
+ };
135
+ },
136
+ dispose: () => {
137
+ "worklet";
138
+ if (output.published) delete output.published["gpu-overlay"];
139
+ output.published = null;
140
+ return scope.dispose();
141
+ },
142
+ };
143
+ } catch (error) {
144
+ await scope.dispose();
145
+ throw error;
146
+ }
147
+ };
@@ -0,0 +1,24 @@
1
+ #pragma once
2
+
3
+ #define RADIANS_TO_DEGREES(radians) ((radians) * (180.0 / M_PI))
4
+ namespace RNSkiaVideo {
5
+
6
+ class AVAssetTrackUtils {
7
+ public:
8
+ static float radiansToDegrees(CGFloat radians) {
9
+ // Input range [-pi, pi] or [-180, 180]
10
+ CGFloat degrees = RADIANS_TO_DEGREES((float)radians);
11
+ if (degrees < 0) {
12
+ // Convert -90 to 270 and -180 to 180
13
+ return degrees + 360;
14
+ }
15
+ // Output degrees in between [0, 360]
16
+ return degrees;
17
+ };
18
+
19
+ static int GetTrackRotationInDegree(AVAssetTrack* track) {
20
+ CGAffineTransform t = track.preferredTransform;
21
+ return round(radiansToDegrees(atan2(t.b, t.a)));
22
+ }
23
+ };
24
+ } // namespace RNSkiaVideo
@@ -0,0 +1,27 @@
1
+ #pragma once
2
+
3
+ #import "VideoComposition.h"
4
+ #import <AVFoundation/AVFoundation.h>
5
+
6
+ namespace RNSkiaVideo {
7
+
8
+ struct AudioComposition {
9
+ AVMutableComposition* composition;
10
+ AVMutableAudioMix* audioMix;
11
+ };
12
+
13
+ /**
14
+ * Builds an audio only AVComposition (with an audio mix for per item
15
+ * volumes) from the audio-enabled items of the given video composition.
16
+ * Returns {nil, nil} if no item provides audio.
17
+ * Throws NSError* if an audio item points to a file without audio track.
18
+ */
19
+ AudioComposition
20
+ buildAudioComposition(const std::shared_ptr<VideoComposition>& composition,
21
+ NSMutableDictionary<NSString*, AVURLAsset*>* assetCache);
22
+
23
+ AVURLAsset*
24
+ getOrCreateAsset(const std::string& path,
25
+ NSMutableDictionary<NSString*, AVURLAsset*>* assetCache);
26
+
27
+ } // namespace RNSkiaVideo
@@ -0,0 +1,113 @@
1
+ #import "AudioCompositionUtils.h"
2
+
3
+ namespace RNSkiaVideo {
4
+
5
+ NS_INLINE NSError* audioErrorWithMessage(NSString* message) {
6
+ return [NSError errorWithDomain:@"com.azzapp.rnskv"
7
+ code:0
8
+ userInfo:@{NSLocalizedDescriptionKey : message}];
9
+ }
10
+
11
+ AVURLAsset*
12
+ getOrCreateAsset(const std::string& path,
13
+ NSMutableDictionary<NSString*, AVURLAsset*>* assetCache) {
14
+ NSString* nsPath =
15
+ [NSString stringWithCString:path.c_str()
16
+ encoding:[NSString defaultCStringEncoding]];
17
+ AVURLAsset* asset = assetCache ? assetCache[nsPath] : nil;
18
+ if (!asset) {
19
+ asset = [AVURLAsset URLAssetWithURL:[NSURL fileURLWithPath:nsPath]
20
+ options:nil];
21
+ if (assetCache) {
22
+ assetCache[nsPath] = asset;
23
+ }
24
+ }
25
+ return asset;
26
+ }
27
+
28
+ AudioComposition
29
+ buildAudioComposition(const std::shared_ptr<VideoComposition>& composition,
30
+ NSMutableDictionary<NSString*, AVURLAsset*>* assetCache) {
31
+ AVMutableComposition* audioComposition = [AVMutableComposition composition];
32
+ NSMutableArray<AVAudioMixInputParameters*>* inputParameters =
33
+ [NSMutableArray array];
34
+ bool hasTracks = false;
35
+ for (const auto& item : composition->items) {
36
+ if (!item->audioEnabled) {
37
+ continue;
38
+ }
39
+ AVURLAsset* asset = getOrCreateAsset(item->path, assetCache);
40
+ AVAssetTrack* audioTrack =
41
+ [[asset tracksWithMediaType:AVMediaTypeAudio] firstObject];
42
+ if (!audioTrack) {
43
+ if (!item->isVideo) {
44
+ throw audioErrorWithMessage(
45
+ [NSString stringWithFormat:@"No audio track for path: %s",
46
+ item->path.c_str()]);
47
+ }
48
+ continue;
49
+ }
50
+
51
+ CMTime startTime = CMTimeMakeWithSeconds(item->startTime, NSEC_PER_SEC);
52
+ auto sourceRange = CMTimeRangeGetIntersection(
53
+ CMTimeRangeMake(startTime,
54
+ CMTimeMakeWithSeconds(item->duration, NSEC_PER_SEC)),
55
+ audioTrack.timeRange);
56
+ if (CMTimeCompare(sourceRange.duration, kCMTimeZero) <= 0) {
57
+ continue;
58
+ }
59
+
60
+ AVMutableCompositionTrack* compositionTrack = [audioComposition
61
+ addMutableTrackWithMediaType:AVMediaTypeAudio
62
+ preferredTrackID:kCMPersistentTrackID_Invalid];
63
+ // If the audio track starts after the requested start time, keep the
64
+ // offset so the audio stays aligned on the composition timeline.
65
+ CMTime insertTime = CMTimeAdd(
66
+ CMTimeMakeWithSeconds(item->compositionStartTime, NSEC_PER_SEC),
67
+ CMTimeSubtract(sourceRange.start, startTime));
68
+ NSError* insertError = nil;
69
+ if (![compositionTrack insertTimeRange:sourceRange
70
+ ofTrack:audioTrack
71
+ atTime:insertTime
72
+ error:&insertError]) {
73
+ throw insertError
74
+ ?: audioErrorWithMessage([NSString
75
+ stringWithFormat:@"Could not insert audio track for item: %s",
76
+ item->id.c_str()]);
77
+ }
78
+ hasTracks = true;
79
+
80
+ if (item->audioVolume != 1.0) {
81
+ AVMutableAudioMixInputParameters* parameters =
82
+ [AVMutableAudioMixInputParameters
83
+ audioMixInputParametersWithTrack:compositionTrack];
84
+ [parameters setVolume:MAX(0.0, MIN(1.0, item->audioVolume))
85
+ atTime:kCMTimeZero];
86
+ [inputParameters addObject:parameters];
87
+ }
88
+ }
89
+
90
+ if (!hasTracks) {
91
+ return {nil, nil};
92
+ }
93
+
94
+ // The playback clock is driven by the audio player: the composition is
95
+ // padded with silence so the playhead can run the whole timeline and go
96
+ // past its duration (completion detection).
97
+ CMTime paddedDuration =
98
+ CMTimeMakeWithSeconds(composition->duration + 0.5, NSEC_PER_SEC);
99
+ if (CMTimeCompare(audioComposition.duration, paddedDuration) < 0) {
100
+ [audioComposition
101
+ insertEmptyTimeRange:CMTimeRangeFromTimeToTime(
102
+ audioComposition.duration, paddedDuration)];
103
+ }
104
+
105
+ AVMutableAudioMix* audioMix = nil;
106
+ if (inputParameters.count > 0) {
107
+ audioMix = [AVMutableAudioMix audioMix];
108
+ audioMix.inputParameters = inputParameters;
109
+ }
110
+ return {audioComposition, audioMix};
111
+ }
112
+
113
+ } // namespace RNSkiaVideo
@@ -0,0 +1,52 @@
1
+ #pragma once
2
+
3
+ namespace RNSkiaVideo {
4
+
5
+ /**
6
+ * When an item's decoder is open, for a composition with lazy decoders: from a
7
+ * little before the item starts to a little after it ends. Without lazy
8
+ * decoders every decoder is open for the life of the player. Same numbers as
9
+ * android/.../DecoderWindow.java.
10
+ */
11
+ class DecoderWindow {
12
+ public:
13
+ DecoderWindow(bool lazy, bool realTime)
14
+ : lazy(lazy), lead(realTime ? kPreviewLead : kExportLead) {}
15
+
16
+ bool isLazy() const { return lazy; }
17
+
18
+ bool opens(double start, double end, double position, double duration,
19
+ bool looping) const {
20
+ return !lazy || within(start - lead, end, position, duration, looping);
21
+ }
22
+
23
+ /** Wider than opens, so an item is never closed and reopened at one edge. */
24
+ bool keeps(double start, double end, double position, double duration,
25
+ bool looping) const {
26
+ return !lazy || within(start - lead - kMargin, end + kMargin, position,
27
+ duration, looping);
28
+ }
29
+
30
+ private:
31
+ // The preview has to open a reader and decode the first frame before the
32
+ // item is on screen; the export waits for it anyway. Chosen, not measured.
33
+ static constexpr double kPreviewLead = 1.5;
34
+ static constexpr double kExportLead = 0.5;
35
+ // Kept a little past both ends, so a scrub across a cut does not reopen it.
36
+ static constexpr double kMargin = 0.5;
37
+
38
+ bool lazy;
39
+ double lead;
40
+
41
+ static bool within(double from, double to, double position, double duration,
42
+ bool looping) {
43
+ if (position >= from && position < to) {
44
+ return true;
45
+ }
46
+ // Near the end of a loop, the items at the start come next.
47
+ double wrapped = position - duration;
48
+ return looping && wrapped >= from && wrapped < to;
49
+ }
50
+ };
51
+
52
+ } // namespace RNSkiaVideo