react-native-skia-webgpu-video 0.0.0-stage → 0.1.0-beta.1

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 (237) 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/DisplayVsyncPeriod.java +72 -0
  48. package/android/src/main/java/com/azzapp/rnskv/EGLResourcesHolder.java +132 -0
  49. package/android/src/main/java/com/azzapp/rnskv/EGLUtils.java +225 -0
  50. package/android/src/main/java/com/azzapp/rnskv/FrameSize.java +50 -0
  51. package/android/src/main/java/com/azzapp/rnskv/GLFrameExtractor.java +510 -0
  52. package/android/src/main/java/com/azzapp/rnskv/JNIHelpers.java +12 -0
  53. package/android/src/main/java/com/azzapp/rnskv/NativeEventDispatcher.java +25 -0
  54. package/android/src/main/java/com/azzapp/rnskv/NativeHardwareBuffer.java +62 -0
  55. package/android/src/main/java/com/azzapp/rnskv/NativeRgbaBuffer.java +43 -0
  56. package/android/src/main/java/com/azzapp/rnskv/ReactNativeSkiaVideoModule.java +45 -0
  57. package/android/src/main/java/com/azzapp/rnskv/ReactNativeSkiaVideoPackage.java +43 -0
  58. package/android/src/main/java/com/azzapp/rnskv/ReactNativeSkiaVideoSpec.java +9 -0
  59. package/android/src/main/java/com/azzapp/rnskv/RgbaBufferAllocator.java +114 -0
  60. package/android/src/main/java/com/azzapp/rnskv/RgbaLayout.java +33 -0
  61. package/android/src/main/java/com/azzapp/rnskv/SurfaceOutputPolicy.java +13 -0
  62. package/android/src/main/java/com/azzapp/rnskv/TextureRenderer.java +150 -0
  63. package/android/src/main/java/com/azzapp/rnskv/TimeHelpers.java +11 -0
  64. package/android/src/main/java/com/azzapp/rnskv/VideoCapabilities.java +354 -0
  65. package/android/src/main/java/com/azzapp/rnskv/VideoComposition.java +147 -0
  66. package/android/src/main/java/com/azzapp/rnskv/VideoCompositionDecoder.java +779 -0
  67. package/android/src/main/java/com/azzapp/rnskv/VideoCompositionFramesExtractor.java +463 -0
  68. package/android/src/main/java/com/azzapp/rnskv/VideoCompositionFramesExtractorSync.java +241 -0
  69. package/android/src/main/java/com/azzapp/rnskv/VideoCompositionItemDecoder.java +492 -0
  70. package/android/src/main/java/com/azzapp/rnskv/VideoEncoder.java +639 -0
  71. package/android/src/main/java/com/azzapp/rnskv/VideoFrame.java +74 -0
  72. package/android/src/main/java/com/azzapp/rnskv/VideoPlayer.java +361 -0
  73. package/cpp/RNSVCheckedSizes.h +54 -0
  74. package/cpp/RNSVEventEmitter.cpp +199 -0
  75. package/cpp/RNSVEventEmitter.h +31 -0
  76. package/cpp/RNSVHostObject.cpp +79 -0
  77. package/cpp/RNSVHostObject.h +67 -0
  78. package/cpp/RNSVMemoryBudget.cpp +68 -0
  79. package/cpp/RNSVMemoryBudget.h +100 -0
  80. package/cpp/RNSVRuntimeLifecycleMonitor.cpp +88 -0
  81. package/cpp/RNSVRuntimeLifecycleMonitor.h +33 -0
  82. package/docs/ANDROID_DEVICE_306.md +95 -0
  83. package/docs/ANDROID_EXPORT_CACHE_306.md +48 -0
  84. package/docs/ANDROID_MEMORY_RETENTION_306.md +67 -0
  85. package/docs/BENCHMARKS.md +368 -0
  86. package/docs/IOS_DEVICE_TESTS.md +115 -0
  87. package/docs/IOS_SIMULATOR_TESTS.md +74 -0
  88. package/docs/MIGRATION.md +82 -0
  89. package/docs/NATIVE_MEMORY_INVESTIGATION_306.md +88 -0
  90. package/docs/PERFORMANCE_AB_IPHONE_303.md +196 -0
  91. package/docs/PERFORMANCE_AB_SIMULATOR.md +135 -0
  92. package/docs/PERFORMANCE_AB_SOAK_SIMULATOR.md +84 -0
  93. package/docs/PERFORMANCE_IPHONE_306_OPTIMIZATION.md +76 -0
  94. package/docs/PERFORMANCE_SIMULATOR.md +89 -0
  95. package/docs/PERFORMANCE_SIMULATOR_INITIAL.md +96 -0
  96. package/docs/PERFORMANCE_SOAK_IPHONE_303.md +41 -0
  97. package/docs/UPSTREAM_NOTES.md +188 -0
  98. package/docs/assets/banner.svg +26 -0
  99. package/examples/webgpu-overlay.ts +147 -0
  100. package/ios/AVAssetTrackUtils.h +24 -0
  101. package/ios/AudioCompositionUtils.h +27 -0
  102. package/ios/AudioCompositionUtils.mm +113 -0
  103. package/ios/DecoderWindow.h +52 -0
  104. package/ios/NativeResourceStats.h +32 -0
  105. package/ios/RNSVColorSpace.h +50 -0
  106. package/ios/RNSVJSIUtils.h +46 -0
  107. package/ios/RNSVScopedPixelBuffer.h +17 -0
  108. package/ios/RNSVVideoPlayer.h +46 -0
  109. package/ios/RNSVVideoPlayer.mm +410 -0
  110. package/ios/ReactNativeSkiaVideo.h +8 -0
  111. package/ios/ReactNativeSkiaVideo.mm +310 -0
  112. package/ios/SeekPolicy.h +27 -0
  113. package/ios/VideoComposition.h +148 -0
  114. package/ios/VideoCompositionFramesExtractorHostObject.h +105 -0
  115. package/ios/VideoCompositionFramesExtractorHostObject.mm +686 -0
  116. package/ios/VideoCompositionFramesExtractorSyncHostObject.h +37 -0
  117. package/ios/VideoCompositionFramesExtractorSyncHostObject.mm +168 -0
  118. package/ios/VideoCompositionItemDecoder.h +60 -0
  119. package/ios/VideoCompositionItemDecoder.mm +480 -0
  120. package/ios/VideoEncoderHostObject.h +83 -0
  121. package/ios/VideoEncoderHostObject.mm +706 -0
  122. package/ios/VideoFrame.h +66 -0
  123. package/ios/VideoFrame.mm +108 -0
  124. package/ios/VideoPlayerHostObject.h +52 -0
  125. package/ios/VideoPlayerHostObject.mm +332 -0
  126. package/lib/module/NativeReactNativeSkiaVideo.js +5 -0
  127. package/lib/module/NativeReactNativeSkiaVideo.js.map +1 -0
  128. package/lib/module/RNSkiaVideoModule.js +20 -0
  129. package/lib/module/RNSkiaVideoModule.js.map +1 -0
  130. package/lib/module/canvas.js +18 -0
  131. package/lib/module/canvas.js.map +1 -0
  132. package/lib/module/exportCache.js +22 -0
  133. package/lib/module/exportCache.js.map +1 -0
  134. package/lib/module/exportFrames.js +338 -0
  135. package/lib/module/exportFrames.js.map +1 -0
  136. package/lib/module/exportQueue.js +52 -0
  137. package/lib/module/exportQueue.js.map +1 -0
  138. package/lib/module/exportTarget.js +137 -0
  139. package/lib/module/exportTarget.js.map +1 -0
  140. package/lib/module/exportVideoComposition.js +492 -0
  141. package/lib/module/exportVideoComposition.js.map +1 -0
  142. package/lib/module/frameInterop.js +532 -0
  143. package/lib/module/frameInterop.js.map +1 -0
  144. package/lib/module/gpu.js +222 -0
  145. package/lib/module/gpu.js.map +1 -0
  146. package/lib/module/gpuDevice.js +54 -0
  147. package/lib/module/gpuDevice.js.map +1 -0
  148. package/lib/module/index.js +36 -0
  149. package/lib/module/index.js.map +1 -0
  150. package/lib/module/memory.js +38 -0
  151. package/lib/module/memory.js.map +1 -0
  152. package/lib/module/package.json +1 -0
  153. package/lib/module/previewLifecycle.js +52 -0
  154. package/lib/module/previewLifecycle.js.map +1 -0
  155. package/lib/module/runtimeYield.js +10 -0
  156. package/lib/module/runtimeYield.js.map +1 -0
  157. package/lib/module/types.js +4 -0
  158. package/lib/module/types.js.map +1 -0
  159. package/lib/module/utils/useEventListener.js +18 -0
  160. package/lib/module/utils/useEventListener.js.map +1 -0
  161. package/lib/module/videoCompositionPlayer.js +368 -0
  162. package/lib/module/videoCompositionPlayer.js.map +1 -0
  163. package/lib/module/videoFrameImage.js +185 -0
  164. package/lib/module/videoFrameImage.js.map +1 -0
  165. package/lib/module/videoPlayer.js +172 -0
  166. package/lib/module/videoPlayer.js.map +1 -0
  167. package/lib/module/yuvBlit.js +299 -0
  168. package/lib/module/yuvBlit.js.map +1 -0
  169. package/lib/typescript/package.json +1 -0
  170. package/lib/typescript/src/NativeReactNativeSkiaVideo.d.ts +7 -0
  171. package/lib/typescript/src/NativeReactNativeSkiaVideo.d.ts.map +1 -0
  172. package/lib/typescript/src/RNSkiaVideoModule.d.ts +4 -0
  173. package/lib/typescript/src/RNSkiaVideoModule.d.ts.map +1 -0
  174. package/lib/typescript/src/canvas.d.ts +4 -0
  175. package/lib/typescript/src/canvas.d.ts.map +1 -0
  176. package/lib/typescript/src/exportCache.d.ts +6 -0
  177. package/lib/typescript/src/exportCache.d.ts.map +1 -0
  178. package/lib/typescript/src/exportFrames.d.ts +57 -0
  179. package/lib/typescript/src/exportFrames.d.ts.map +1 -0
  180. package/lib/typescript/src/exportQueue.d.ts +5 -0
  181. package/lib/typescript/src/exportQueue.d.ts.map +1 -0
  182. package/lib/typescript/src/exportTarget.d.ts +28 -0
  183. package/lib/typescript/src/exportTarget.d.ts.map +1 -0
  184. package/lib/typescript/src/exportVideoComposition.d.ts +68 -0
  185. package/lib/typescript/src/exportVideoComposition.d.ts.map +1 -0
  186. package/lib/typescript/src/frameInterop.d.ts +17 -0
  187. package/lib/typescript/src/frameInterop.d.ts.map +1 -0
  188. package/lib/typescript/src/gpu.d.ts +39 -0
  189. package/lib/typescript/src/gpu.d.ts.map +1 -0
  190. package/lib/typescript/src/gpuDevice.d.ts +11 -0
  191. package/lib/typescript/src/gpuDevice.d.ts.map +1 -0
  192. package/lib/typescript/src/index.d.ts +21 -0
  193. package/lib/typescript/src/index.d.ts.map +1 -0
  194. package/lib/typescript/src/memory.d.ts +25 -0
  195. package/lib/typescript/src/memory.d.ts.map +1 -0
  196. package/lib/typescript/src/previewLifecycle.d.ts +22 -0
  197. package/lib/typescript/src/previewLifecycle.d.ts.map +1 -0
  198. package/lib/typescript/src/runtimeYield.d.ts +4 -0
  199. package/lib/typescript/src/runtimeYield.d.ts.map +1 -0
  200. package/lib/typescript/src/types.d.ts +631 -0
  201. package/lib/typescript/src/types.d.ts.map +1 -0
  202. package/lib/typescript/src/utils/useEventListener.d.ts +5 -0
  203. package/lib/typescript/src/utils/useEventListener.d.ts.map +1 -0
  204. package/lib/typescript/src/videoCompositionPlayer.d.ts +105 -0
  205. package/lib/typescript/src/videoCompositionPlayer.d.ts.map +1 -0
  206. package/lib/typescript/src/videoFrameImage.d.ts +60 -0
  207. package/lib/typescript/src/videoFrameImage.d.ts.map +1 -0
  208. package/lib/typescript/src/videoPlayer.d.ts +99 -0
  209. package/lib/typescript/src/videoPlayer.d.ts.map +1 -0
  210. package/lib/typescript/src/yuvBlit.d.ts +50 -0
  211. package/lib/typescript/src/yuvBlit.d.ts.map +1 -0
  212. package/package.json +155 -4
  213. package/patches/react-native-skia+3.7.0.patch +502 -0
  214. package/react-native-skia-webgpu-video.podspec +35 -0
  215. package/scripts/apply-skia-canvas-patch.mjs +48 -0
  216. package/scripts/skia-canvas-patch.json +291 -0
  217. package/src/NativeReactNativeSkiaVideo.ts +8 -0
  218. package/src/RNSkiaVideoModule.ts +27 -0
  219. package/src/canvas.ts +22 -0
  220. package/src/exportCache.ts +27 -0
  221. package/src/exportFrames.ts +421 -0
  222. package/src/exportQueue.ts +65 -0
  223. package/src/exportTarget.ts +179 -0
  224. package/src/exportVideoComposition.ts +718 -0
  225. package/src/frameInterop.ts +684 -0
  226. package/src/gpu.ts +290 -0
  227. package/src/gpuDevice.ts +80 -0
  228. package/src/index.tsx +45 -0
  229. package/src/memory.ts +48 -0
  230. package/src/previewLifecycle.ts +60 -0
  231. package/src/runtimeYield.ts +6 -0
  232. package/src/types.ts +661 -0
  233. package/src/utils/useEventListener.ts +22 -0
  234. package/src/videoCompositionPlayer.ts +568 -0
  235. package/src/videoFrameImage.ts +205 -0
  236. package/src/videoPlayer.ts +318 -0
  237. package/src/yuvBlit.ts +331 -0
package/src/gpu.ts ADDED
@@ -0,0 +1,290 @@
1
+ /// <reference types="@webgpu/types" />
2
+ import { Skia } from "react-native-skia";
3
+ import type { SkCanvas, SkImage, SkSurface } from "react-native-skia";
4
+ import { GPUTextureUsage } from "react-native-webgpu";
5
+ import { getVideoCanvas } from "./canvas";
6
+ import { releaseVideoMemory, reserveVideoMemory } from "./memory";
7
+ import { getVideoGpuDevice } from "./gpuDevice";
8
+ export { getVideoGpuDevice } from "./gpuDevice";
9
+
10
+ export type VideoGpuTextureFormat =
11
+ | "rgba8unorm"
12
+ | "bgra8unorm"
13
+ | "rgba16float"
14
+ | "r8unorm"
15
+ | "depth24plus"
16
+ | "depth32float";
17
+ export type VideoGpuTarget = {
18
+ readonly texture: GPUTexture;
19
+ readonly image: SkImage | null;
20
+ readonly surface: SkSurface | null;
21
+ readonly canvas: SkCanvas | null;
22
+ };
23
+ export type VideoGpuTextureOptions = {
24
+ width: number;
25
+ height: number;
26
+ format?: VideoGpuTextureFormat;
27
+ usage?: GPUTextureUsageFlags;
28
+ /** Images sample the texture; surfaces let Skia draw into it. Both are
29
+ * restricted to the color formats supported by Skia's native interop. */
30
+ skia?: "image" | "surface" | "none";
31
+ label?: string;
32
+ };
33
+ export type VideoGpuScope = {
34
+ readonly device: GPUDevice;
35
+ createTexture: (options: VideoGpuTextureOptions) => VideoGpuTarget;
36
+ createBuffer: (descriptor: GPUBufferDescriptor) => GPUBuffer;
37
+ /** Register pending CPU/ML work which reads this scope's resources. */
38
+ track: <T>(start: () => Promise<T>) => Promise<T>;
39
+ /** Submit owned Skia recordings, await tracked jobs and the shared GPU queue. */
40
+ drain: () => Promise<void>;
41
+ /** First detach every external image/surface consumer. Repeated calls return
42
+ * the same promise; failed GPU drain retains resources and reservations. */
43
+ dispose: () => Promise<void>;
44
+ };
45
+
46
+ type Resource = {
47
+ reservation: number;
48
+ texture?: GPUTexture;
49
+ buffer?: GPUBuffer;
50
+ image?: SkImage | null;
51
+ surface?: SkSurface | null;
52
+ canvas?: SkCanvas | null;
53
+ };
54
+
55
+ /** Create once on the consuming runtime, reuse targets across frames, then
56
+ * dispose after removing consumers. Byte accounting is an explicit allocation
57
+ * estimate, not driver RSS: codecs, shaders and driver padding are separate. */
58
+ export const createVideoGpuScope = (options?: {
59
+ maxResources?: number;
60
+ maxPendingJobs?: number;
61
+ }): VideoGpuScope => {
62
+ "worklet";
63
+ const device = getVideoGpuDevice();
64
+ const maxResources = options?.maxResources ?? 32;
65
+ const maxPendingJobs = options?.maxPendingJobs ?? 32;
66
+ if (
67
+ !Number.isSafeInteger(maxResources) ||
68
+ maxResources <= 0 ||
69
+ maxResources > 256 ||
70
+ !Number.isSafeInteger(maxPendingJobs) ||
71
+ maxPendingJobs <= 0 ||
72
+ maxPendingJobs > 256
73
+ ) {
74
+ throw new Error("Invalid GPU scope bounds");
75
+ }
76
+ const resources: Resource[] = [];
77
+ const pending = new Set<Promise<void>>();
78
+ // Worklets materializes a separate lexical closure for each nested
79
+ // function. Mutable primitives would be captured as stale copies.
80
+ const state = {
81
+ pendingCount: 0,
82
+ jobGeneration: 0,
83
+ jobFailure: undefined as unknown,
84
+ hasJobFailure: false,
85
+ closing: false,
86
+ disposal: null as Promise<void> | null,
87
+ };
88
+ const ensureCapacity = () => {
89
+ "worklet";
90
+ if (state.closing) throw new Error("GPU scope is closing");
91
+ if (resources.length >= maxResources)
92
+ throw new Error("GPU scope resource limit exceeded");
93
+ };
94
+ const drainGpu = async () => {
95
+ "worklet";
96
+ // Wait even for rejected ML promises. Those failures do not imply that
97
+ // other concurrent reads have finished.
98
+ for (;;) {
99
+ while (state.pendingCount) await Promise.all([...pending]);
100
+ const generation = state.jobGeneration;
101
+ for (const resource of resources) resource.surface?.flush(true);
102
+ await device.queue.onSubmittedWorkDone();
103
+ // drain() leaves the scope open. A job registered while awaiting the GPU
104
+ // may submit further work after this fence; await that job and a new fence.
105
+ if (!state.pendingCount && state.jobGeneration === generation) return;
106
+ }
107
+ };
108
+ const drain = async () => {
109
+ "worklet";
110
+ await drainGpu();
111
+ if (state.hasJobFailure) throw state.jobFailure;
112
+ };
113
+ const createTexture = (opts: VideoGpuTextureOptions): VideoGpuTarget => {
114
+ "worklet";
115
+ ensureCapacity();
116
+ const format = opts.format ?? "rgba8unorm";
117
+ const skia = opts.skia ?? "image";
118
+ const bytesPerPixel =
119
+ format === "rgba16float" ? 8 : format === "r8unorm" ? 1 : 4;
120
+ const bytes = opts.width * opts.height * bytesPerPixel;
121
+ if (
122
+ !Number.isSafeInteger(opts.width) ||
123
+ opts.width <= 0 ||
124
+ !Number.isSafeInteger(opts.height) ||
125
+ opts.height <= 0 ||
126
+ !Number.isSafeInteger(bytes) ||
127
+ ![
128
+ "rgba8unorm",
129
+ "bgra8unorm",
130
+ "rgba16float",
131
+ "r8unorm",
132
+ "depth24plus",
133
+ "depth32float",
134
+ ].includes(format) ||
135
+ !["image", "surface", "none"].includes(skia) ||
136
+ (["r8unorm", "depth24plus", "depth32float"].includes(format) &&
137
+ skia !== "none")
138
+ ) {
139
+ throw new Error("Invalid GPU texture dimensions, format or Skia view");
140
+ }
141
+ const required =
142
+ skia === "surface"
143
+ ? GPUTextureUsage.RENDER_ATTACHMENT
144
+ : skia === "image"
145
+ ? GPUTextureUsage.TEXTURE_BINDING
146
+ : 0;
147
+ const usage =
148
+ (opts.usage ??
149
+ GPUTextureUsage.RENDER_ATTACHMENT |
150
+ GPUTextureUsage.TEXTURE_BINDING |
151
+ GPUTextureUsage.COPY_SRC |
152
+ GPUTextureUsage.COPY_DST) | required;
153
+ const reservation = reserveVideoMemory(
154
+ bytes,
155
+ opts.label ?? "video GPU texture",
156
+ );
157
+ const resource: Resource = { reservation };
158
+ try {
159
+ resource.texture = device.createTexture({
160
+ size: { width: opts.width, height: opts.height },
161
+ format,
162
+ usage,
163
+ label: opts.label,
164
+ });
165
+ if (skia === "surface") {
166
+ resource.surface = Skia.Surface.MakeFromGPUTexture(resource.texture);
167
+ resource.canvas = getVideoCanvas(resource.surface);
168
+ } else if (skia === "image") {
169
+ resource.image = Skia.Image.MakeImageFromGPUTexture(resource.texture);
170
+ }
171
+ resources.push(resource);
172
+ return {
173
+ texture: resource.texture,
174
+ image: resource.image ?? null,
175
+ surface: resource.surface ?? null,
176
+ canvas: resource.canvas ?? null,
177
+ };
178
+ } catch (error) {
179
+ // No texture has been exposed or submitted to any consumer yet.
180
+ try {
181
+ resource.canvas?.dispose();
182
+ resource.canvas = null;
183
+ resource.surface?.dispose();
184
+ resource.surface = null;
185
+ resource.image?.dispose();
186
+ resource.image = null;
187
+ resource.texture?.destroy();
188
+ resource.texture = undefined;
189
+ releaseVideoMemory(reservation);
190
+ } catch {
191
+ // Retain/account partial cleanup failures for the caller's state.disposal.
192
+ resources.push(resource);
193
+ }
194
+ throw error;
195
+ }
196
+ };
197
+ const createBuffer = (descriptor: GPUBufferDescriptor): GPUBuffer => {
198
+ "worklet";
199
+ ensureCapacity();
200
+ if (!Number.isSafeInteger(descriptor.size) || descriptor.size <= 0)
201
+ throw new Error("Invalid GPU buffer size");
202
+ const reservation = reserveVideoMemory(
203
+ descriptor.size,
204
+ descriptor.label ?? "video GPU buffer",
205
+ );
206
+ try {
207
+ const buffer = device.createBuffer(descriptor);
208
+ resources.push({ buffer, reservation });
209
+ return buffer;
210
+ } catch (error) {
211
+ try {
212
+ releaseVideoMemory(reservation);
213
+ } catch {
214
+ // Keep a failed rollback reservation reachable for scope disposal.
215
+ resources.push({ reservation });
216
+ }
217
+ throw error;
218
+ }
219
+ };
220
+ const track = <T>(start: () => Promise<T>): Promise<T> => {
221
+ "worklet";
222
+ if (state.closing) throw new Error("GPU scope is closing");
223
+ if (state.pendingCount >= maxPendingJobs)
224
+ throw new Error("GPU scope pending job limit exceeded");
225
+ state.pendingCount++;
226
+ state.jobGeneration++;
227
+ let job: Promise<T>;
228
+ try {
229
+ job = Promise.resolve(start());
230
+ } catch (error) {
231
+ state.pendingCount--;
232
+ throw error;
233
+ }
234
+ let tracked: Promise<void>;
235
+ tracked = job.then(
236
+ () => {
237
+ pending.delete(tracked);
238
+ state.pendingCount--;
239
+ },
240
+ (error: unknown) => {
241
+ if (!state.hasJobFailure) {
242
+ state.hasJobFailure = true;
243
+ state.jobFailure = error;
244
+ }
245
+ pending.delete(tracked);
246
+ state.pendingCount--;
247
+ },
248
+ );
249
+ pending.add(tracked);
250
+ return job;
251
+ };
252
+ const dispose = (): Promise<void> => {
253
+ "worklet";
254
+ if (state.disposal) return state.disposal;
255
+ state.closing = true;
256
+ state.disposal = (async () => {
257
+ "worklet";
258
+ // If this fence fails, preserve all resources rather than destroy any
259
+ // texture whose pending native use can no longer be established.
260
+ await drainGpu();
261
+ let failure: unknown;
262
+ let hasFailure = false;
263
+ for (let i = resources.length - 1; i >= 0; i--) {
264
+ const resource = resources[i]!;
265
+ try {
266
+ resource.image?.dispose();
267
+ resource.image = null;
268
+ resource.canvas?.dispose();
269
+ resource.canvas = null;
270
+ resource.surface?.dispose();
271
+ resource.surface = null;
272
+ resource.texture?.destroy();
273
+ resource.texture = undefined;
274
+ resource.buffer?.destroy();
275
+ resource.buffer = undefined;
276
+ releaseVideoMemory(resource.reservation);
277
+ resources.splice(i, 1);
278
+ } catch (error) {
279
+ if (!hasFailure) {
280
+ hasFailure = true;
281
+ failure = error;
282
+ }
283
+ }
284
+ }
285
+ if (hasFailure) throw failure;
286
+ })();
287
+ return state.disposal;
288
+ };
289
+ return { device, createTexture, createBuffer, track, drain, dispose };
290
+ };
@@ -0,0 +1,80 @@
1
+ /// <reference types="@webgpu/types" />
2
+ import { Platform } from "react-native";
3
+ import { Skia } from "react-native-skia";
4
+ import {
5
+ GPUBufferUsage,
6
+ GPUShaderStage,
7
+ GPUTextureUsage,
8
+ importDevice,
9
+ installWebGPU,
10
+ } from "react-native-webgpu";
11
+ import type { NativeVideoFrame } from "react-native-webgpu";
12
+ import RNSkiaVideoModule from "./RNSkiaVideoModule";
13
+
14
+ // Evaluate on the RN runtime. NativeObject serializers carry these objects to
15
+ // each consuming worklet; installWebGPU does not install RNWebGPU there.
16
+ // Never destroy the shared Graphite device or call methods across runtimes on
17
+ // a command encoder. Each caller owns its own per-runtime import resources.
18
+ const sharedDevice = importDevice(Skia.getNativeDevice());
19
+ const nativeWebGPU = globalThis.RNWebGPU;
20
+
21
+ // Negotiate before any Android decoder is prepared. The native side starts in
22
+ // CPU mode and probes EGL/AHB setup only when this shared Dawn device can
23
+ // import and end access to AHBs. Missing capability is a clean CPU fallback;
24
+ // budget failures, OOM and unrelated setup errors must remain visible.
25
+ if (
26
+ Platform.OS === "android" &&
27
+ RNSkiaVideoModule.configureNativeBufferInterop
28
+ ) {
29
+ const features = sharedDevice.features as ReadonlySet<string> | undefined;
30
+ const supported = Boolean(
31
+ typeof nativeWebGPU?.createVideoFrameFromNativeBuffer === "function" &&
32
+ typeof features?.has === "function" &&
33
+ features.has("shared-texture-memory-ahardware-buffer") &&
34
+ features.has("shared-fence-sync-fd"),
35
+ );
36
+ try {
37
+ RNSkiaVideoModule.configureNativeBufferInterop(supported);
38
+ } catch (error) {
39
+ if (
40
+ !supported ||
41
+ !(error instanceof Error) ||
42
+ !error.message.includes("Android native buffer interop unavailable:")
43
+ )
44
+ throw error;
45
+ RNSkiaVideoModule.configureNativeBufferInterop(false);
46
+ }
47
+ }
48
+
49
+ export const getVideoGpuDevice = (): GPUDevice => {
50
+ "worklet";
51
+ installWebGPU();
52
+ return sharedDevice;
53
+ };
54
+
55
+ export const createNativeVideoGpuFrame = (
56
+ pointer: bigint,
57
+ ): NativeVideoFrame => {
58
+ "worklet";
59
+ if (!nativeWebGPU)
60
+ throw new Error("React Native WebGPU is missing from the native app");
61
+ return nativeWebGPU.createVideoFrameFromNativeBuffer(pointer);
62
+ };
63
+
64
+ export const nativeVideoTextureUsage =
65
+ GPUTextureUsage.RENDER_ATTACHMENT |
66
+ GPUTextureUsage.TEXTURE_BINDING |
67
+ GPUTextureUsage.COPY_SRC;
68
+
69
+ // CPU RGBA uploads additionally need COPY_DST. Keep the native-buffer blit's
70
+ // descriptor unchanged: that path renders into its destination attachment.
71
+ export const rgbaVideoTextureUsage =
72
+ nativeVideoTextureUsage | GPUTextureUsage.COPY_DST;
73
+
74
+ // The flag globals exist only on the RN runtime: worklets capture these.
75
+ export const videoShaderStages = {
76
+ vertex: GPUShaderStage.VERTEX,
77
+ fragment: GPUShaderStage.FRAGMENT,
78
+ };
79
+ export const videoUniformBufferUsage =
80
+ GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST;
package/src/index.tsx ADDED
@@ -0,0 +1,45 @@
1
+ import RNSkiaVideoModule from './RNSkiaVideoModule';
2
+ import { Platform } from 'react-native';
3
+
4
+ export { RNSkiaVideoModule as __RNSkiaVideoPrivateAPI };
5
+
6
+ export * from './types';
7
+ export * from './videoPlayer';
8
+ export * from './videoCompositionPlayer';
9
+ export * from './exportVideoComposition';
10
+ export * from './videoFrameImage';
11
+ export { clearVideoFrameImages, releaseVideoFrameImage } from './frameInterop';
12
+ export { configureVideoMemory, getVideoResourceStats } from './memory';
13
+ export type { VideoMemoryStats } from './memory';
14
+
15
+ export const getValidEncoderConfigurations: typeof RNSkiaVideoModule.getValidEncoderConfigurations =
16
+ (...args) => {
17
+ if (Platform.OS === 'android') {
18
+ return RNSkiaVideoModule.getValidEncoderConfigurations(...args);
19
+ } else {
20
+ throw new Error(
21
+ 'getValidEncoderConfigurations is only available on Android'
22
+ );
23
+ }
24
+ };
25
+
26
+ export const getDecodingCapabilitiesFor: typeof RNSkiaVideoModule.getDecodingCapabilitiesFor =
27
+ (...args) => {
28
+ if (Platform.OS === 'android') {
29
+ return RNSkiaVideoModule.getDecodingCapabilitiesFor(...args);
30
+ } else {
31
+ throw new Error(
32
+ 'getDecodingCapabilitiesFor is only available on Android'
33
+ );
34
+ }
35
+ };
36
+
37
+ /**
38
+ * Whether the device can encode with the given codec.
39
+ *
40
+ * Unlike the two above this is answered on both platforms. Use it to decide
41
+ * whether to offer a codec at all; exports fall back to H.264 by themselves
42
+ * when the answer is false.
43
+ */
44
+ export const isEncodingSupported: typeof RNSkiaVideoModule.isEncodingSupported =
45
+ (...args) => RNSkiaVideoModule.isEncodingSupported(...args);
package/src/memory.ts ADDED
@@ -0,0 +1,48 @@
1
+ import RNSkiaVideoModule from "./RNSkiaVideoModule";
2
+
3
+ export type VideoMemoryStats = {
4
+ currentBytes: number;
5
+ peakBytes: number;
6
+ allocations: number;
7
+ maxBytes: number;
8
+ };
9
+
10
+ /** Limits library-owned reservations across preview and export runtimes.
11
+ * Driver/codec caches must additionally be measured on the device. */
12
+ export const configureVideoMemory = ({ maxBytes }: { maxBytes: number }) => {
13
+ if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) {
14
+ throw new Error("maxBytes must be a positive safe integer");
15
+ }
16
+ RNSkiaVideoModule.configureMemoryBudget!(maxBytes);
17
+ };
18
+
19
+ export const getVideoResourceStats = () => ({
20
+ budget: RNSkiaVideoModule.getMemoryBudgetStats?.() ?? null,
21
+ native: RNSkiaVideoModule.getResourceStats?.() ?? null,
22
+ backend: RNSkiaVideoModule.getBackendInfo?.() ?? null,
23
+ });
24
+
25
+ export const frameByteSize = (width: number, height: number): number => {
26
+ "worklet";
27
+ const bytes = width * height * 4;
28
+ if (
29
+ !Number.isSafeInteger(width) ||
30
+ !Number.isSafeInteger(height) ||
31
+ width <= 0 ||
32
+ height <= 0 ||
33
+ !Number.isSafeInteger(bytes)
34
+ ) {
35
+ throw new Error("Invalid video frame dimensions");
36
+ }
37
+ return bytes;
38
+ };
39
+
40
+ export const reserveVideoMemory = (bytes: number, label: string): number => {
41
+ "worklet";
42
+ return RNSkiaVideoModule.reserveMemory?.(bytes, label) ?? 0;
43
+ };
44
+
45
+ export const releaseVideoMemory = (reservation: number): void => {
46
+ "worklet";
47
+ if (reservation) RNSkiaVideoModule.releaseMemory?.(reservation);
48
+ };
@@ -0,0 +1,60 @@
1
+ import type { SkCanvas, SkImage, SkSurface } from "react-native-skia";
2
+ import { releaseVideoFrameImage } from "./frameInterop";
3
+ import { releaseVideoMemory } from "./memory";
4
+ import type { VideoFrame } from "./types";
5
+
6
+ type PreviewResources = {
7
+ surface?: SkSurface | null;
8
+ canvas?: SkCanvas | null;
9
+ images?: Array<SkImage | null>;
10
+ frames?: VideoFrame[];
11
+ reservation?: number;
12
+ afterDraw?: (() => void) | null;
13
+ native?: { dispose: () => void } | null;
14
+ };
15
+
16
+ /** Private UI-runtime quarantine. Detach consumers before calling this helper.
17
+ * A failed drain keeps every potentially referenced resource and its accounting
18
+ * alive until runtime teardown; native codecs can still close independently. */
19
+ export const disposePreviewResources = (resources: PreviewResources) => {
20
+ "worklet";
21
+ const failure = { failed: false, error: undefined as unknown };
22
+ try {
23
+ resources.surface?.flush(true);
24
+ resources.afterDraw?.();
25
+ resources.afterDraw = null;
26
+ while (resources.images?.length) {
27
+ resources.images[0]?.dispose();
28
+ resources.images.shift();
29
+ }
30
+ while (resources.frames?.length) {
31
+ const frame = resources.frames[0]!;
32
+ releaseVideoFrameImage(frame);
33
+ frame.dispose?.();
34
+ resources.frames.shift();
35
+ }
36
+ resources.canvas?.dispose();
37
+ resources.canvas = null;
38
+ resources.surface?.dispose();
39
+ resources.surface = null;
40
+ releaseVideoMemory(resources.reservation ?? 0);
41
+ resources.reservation = 0;
42
+ } catch (error) {
43
+ failure.failed = true;
44
+ failure.error = error;
45
+ }
46
+ try {
47
+ resources.native?.dispose();
48
+ resources.native = null;
49
+ } catch (error) {
50
+ if (!failure.failed) failure.error = error;
51
+ failure.failed = true;
52
+ }
53
+ if (failure.failed) {
54
+ const runtime = globalThis as typeof globalThis & {
55
+ __rnskwgpuPreviewQuarantine?: PreviewResources[];
56
+ };
57
+ (runtime.__rnskwgpuPreviewQuarantine ??= []).push(resources);
58
+ }
59
+ return failure;
60
+ };
@@ -0,0 +1,6 @@
1
+ /** Continues on the calling worklet runtime's own event loop, so promise and
2
+ * native callbacks run between frames without a hop through the RN thread. */
3
+ export const yieldToRuntime = (fn: () => void): void => {
4
+ "worklet";
5
+ setTimeout(fn, 0);
6
+ };