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
package/src/yuvBlit.ts ADDED
@@ -0,0 +1,331 @@
1
+ /// <reference types="@webgpu/types" />
2
+ import type {
3
+ GPUNativeBufferExternalTexture,
4
+ NativeVideoFrame,
5
+ } from "react-native-webgpu";
6
+ import {
7
+ createNativeVideoGpuFrame,
8
+ videoShaderStages,
9
+ videoUniformBufferUsage,
10
+ } from "./gpuDevice";
11
+
12
+ /** One Android decoder buffer, imported once as an external texture. An
13
+ * ImageReader cycles through a few buffers: each one is imported the first
14
+ * time it shows up, then only its access is opened and closed per frame. */
15
+ export type YuvImport = {
16
+ handle: bigint;
17
+ rotation: number;
18
+ /** The import's own reference on the buffer, released with it. */
19
+ source: NativeVideoFrame;
20
+ wrapper: GPUNativeBufferExternalTexture;
21
+ uniforms: GPUBuffer;
22
+ bindGroup: GPUBindGroup;
23
+ /** uv scale and offset last written to `uniforms`. */
24
+ mapping: number[];
25
+ };
26
+
27
+ type YuvBlit = {
28
+ layout: GPUBindGroupLayout;
29
+ pipelineLayout: GPUPipelineLayout;
30
+ sampler: GPUSampler;
31
+ module: GPUShaderModule;
32
+ pipelines: Map<GPUTextureFormat, GPURenderPipeline>;
33
+ };
34
+
35
+ // An ImageReader holds four buffers (GLFrameExtractor.READER_IMAGES); the
36
+ // slack covers a reader replaced while the clip keeps its producer id.
37
+ const maxImportsPerProducer = 6;
38
+
39
+ // The copyExternalImageToTexture blit of RN WebGPU (VideoFrameBlit.h), opaque
40
+ // and without its sRGB destination variant: uv = position01 * scale + offset,
41
+ // in the upright frame.
42
+ const shader = `
43
+ struct Uniforms { uvScale: vec2f, uvOffset: vec2f };
44
+ @group(0) @binding(0) var frame: texture_external;
45
+ @group(0) @binding(1) var frameSampler: sampler;
46
+ @group(0) @binding(2) var<uniform> u: Uniforms;
47
+ struct VsOut { @builtin(position) position: vec4f, @location(0) uv: vec2f };
48
+ @vertex fn vs(@builtin(vertex_index) index: u32) -> VsOut {
49
+ var corners = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));
50
+ let corner = corners[index];
51
+ var out: VsOut;
52
+ out.position = vec4f(corner, 0.0, 1.0);
53
+ let position01 = vec2f(corner.x * 0.5 + 0.5, 0.5 - corner.y * 0.5);
54
+ out.uv = position01 * u.uvScale + u.uvOffset;
55
+ return out;
56
+ }
57
+ @fragment fn fs(in: VsOut) -> @location(0) vec4f {
58
+ let sampled = textureSampleBaseClampToEdge(frame, frameSampler, in.uv);
59
+ return vec4f(clamp(sampled.rgb, vec3f(0.0), vec3f(1.0)), 1.0);
60
+ }
61
+ `;
62
+
63
+ // The samples' encoding is kept as is, as the EGL path and iOS decoding do:
64
+ // only the YCbCr matrix and range apply (the driver's suggestion for the
65
+ // buffer). Without this, Dawn also converts the video's transfer function and
66
+ // primaries to sRGB, and every export brightens the midtones again.
67
+ const sourceColorSpace = { primaries: "srgb", transfer: "srgb" } as const;
68
+
69
+ /** The corner of a decoder buffer's crop rectangle once the buffer is turned
70
+ * clockwise for display: the blit takes its source origin in that turned
71
+ * space. Throws unless the rectangle lies in the buffer and turns into the
72
+ * frame's display size. */
73
+ export const yuvFrameOrigin = (
74
+ region: { x: number; y: number; width: number; height: number },
75
+ rotation: number,
76
+ buffer: { width: number; height: number },
77
+ frame: { width: number; height: number },
78
+ ): { x: number; y: number } => {
79
+ "worklet";
80
+ const turned = rotation === 90 || rotation === 270;
81
+ if (
82
+ (rotation !== 0 && !turned && rotation !== 180) ||
83
+ region.x < 0 ||
84
+ region.y < 0 ||
85
+ region.x + region.width > buffer.width ||
86
+ region.y + region.height > buffer.height ||
87
+ (turned ? region.height : region.width) !== frame.width ||
88
+ (turned ? region.width : region.height) !== frame.height
89
+ ) {
90
+ throw new Error("Native video buffer region or YUV layout mismatch");
91
+ }
92
+ const { width, height } = buffer;
93
+ if (rotation === 90)
94
+ return { x: height - region.y - region.height, y: region.x };
95
+ if (rotation === 180)
96
+ return {
97
+ x: width - region.x - region.width,
98
+ y: height - region.y - region.height,
99
+ };
100
+ if (rotation === 270)
101
+ return { x: region.y, y: width - region.x - region.width };
102
+ return { x: region.x, y: region.y };
103
+ };
104
+
105
+ const getYuvBlit = (device: GPUDevice): YuvBlit => {
106
+ "worklet";
107
+ const runtime = globalThis as typeof globalThis & {
108
+ __rnskwgpuYuvBlit?: YuvBlit;
109
+ };
110
+ if (!runtime.__rnskwgpuYuvBlit) {
111
+ const layout = device.createBindGroupLayout({
112
+ label: "video YUV blit",
113
+ entries: [
114
+ {
115
+ binding: 0,
116
+ visibility: videoShaderStages.fragment,
117
+ externalTexture: {},
118
+ },
119
+ {
120
+ binding: 1,
121
+ visibility: videoShaderStages.fragment,
122
+ sampler: { type: "filtering" },
123
+ },
124
+ {
125
+ binding: 2,
126
+ visibility: videoShaderStages.vertex,
127
+ buffer: { type: "uniform" },
128
+ },
129
+ ],
130
+ });
131
+ runtime.__rnskwgpuYuvBlit = {
132
+ layout,
133
+ pipelineLayout: device.createPipelineLayout({
134
+ label: "video YUV blit",
135
+ bindGroupLayouts: [layout],
136
+ }),
137
+ sampler: device.createSampler({
138
+ label: "video YUV blit",
139
+ magFilter: "linear",
140
+ minFilter: "linear",
141
+ }),
142
+ module: device.createShaderModule({
143
+ label: "video YUV blit",
144
+ code: shader,
145
+ }),
146
+ pipelines: new Map(),
147
+ };
148
+ }
149
+ return runtime.__rnskwgpuYuvBlit;
150
+ };
151
+
152
+ const getPipeline = (
153
+ device: GPUDevice,
154
+ blit: YuvBlit,
155
+ format: GPUTextureFormat,
156
+ ): GPURenderPipeline => {
157
+ "worklet";
158
+ let pipeline = blit.pipelines.get(format);
159
+ if (!pipeline) {
160
+ pipeline = device.createRenderPipeline({
161
+ label: "video YUV blit",
162
+ layout: blit.pipelineLayout,
163
+ vertex: { module: blit.module, entryPoint: "vs" },
164
+ fragment: {
165
+ module: blit.module,
166
+ entryPoint: "fs",
167
+ targets: [{ format }],
168
+ },
169
+ primitive: { topology: "triangle-list" },
170
+ });
171
+ blit.pipelines.set(format, pipeline);
172
+ }
173
+ return pipeline;
174
+ };
175
+
176
+ /** Call only once the GPU work sampling the buffer has completed. */
177
+ export const releaseYuvImport = (entry: YuvImport): void => {
178
+ "worklet";
179
+ entry.wrapper.destroy();
180
+ entry.uniforms.destroy();
181
+ entry.source.release();
182
+ };
183
+
184
+ /** Releases a producer's imports, or every producer's. Call only once the GPU
185
+ * work sampling them has completed (a checked flush). */
186
+ export const releaseYuvImports = (
187
+ imports: Map<number, YuvImport[]>,
188
+ producer?: number,
189
+ ): void => {
190
+ "worklet";
191
+ for (const [key, entries] of imports) {
192
+ if (producer !== undefined && key !== producer) continue;
193
+ while (entries.length) {
194
+ releaseYuvImport(entries[0]!);
195
+ entries.shift();
196
+ }
197
+ imports.delete(key);
198
+ }
199
+ };
200
+
201
+ const importYuvBuffer = (
202
+ device: GPUDevice,
203
+ blit: YuvBlit,
204
+ handle: bigint,
205
+ rotation: number,
206
+ ): YuvImport => {
207
+ "worklet";
208
+ // A reference of its own: the caller's frame keeps its usual lifetime.
209
+ const source = createNativeVideoGpuFrame(handle);
210
+ let wrapper: GPUNativeBufferExternalTexture | null = null;
211
+ let uniforms: GPUBuffer | null = null;
212
+ try {
213
+ wrapper = device.createNativeBufferExternalTexture(source, {
214
+ label: "video decoder buffer",
215
+ rotation: rotation as 0 | 90 | 180 | 270,
216
+ mirrored: false,
217
+ sourceColorSpace,
218
+ });
219
+ uniforms = device.createBuffer({
220
+ label: "video YUV blit uniforms",
221
+ size: 16,
222
+ usage: videoUniformBufferUsage,
223
+ });
224
+ const bindGroup = device.createBindGroup({
225
+ label: "video YUV blit",
226
+ layout: blit.layout,
227
+ entries: [
228
+ { binding: 0, resource: wrapper.externalTexture },
229
+ { binding: 1, resource: blit.sampler },
230
+ { binding: 2, resource: { buffer: uniforms } },
231
+ ],
232
+ });
233
+ return {
234
+ handle,
235
+ rotation,
236
+ source,
237
+ wrapper,
238
+ uniforms,
239
+ bindGroup,
240
+ mapping: [],
241
+ };
242
+ } catch (error) {
243
+ // Nothing was submitted with this import yet.
244
+ uniforms?.destroy();
245
+ wrapper?.destroy();
246
+ source.release();
247
+ throw error;
248
+ }
249
+ };
250
+
251
+ /**
252
+ * Draws the upright `width`×`height` region at `origin` of a decoder buffer
253
+ * into `target`, converting YUV to RGB; the buffer is imported on its first
254
+ * frame. Submits the draw: the caller waits for it before releasing the
255
+ * frame's lease or `source`, as after copyExternalImageToTexture.
256
+ */
257
+ export const blitYuvFrame = (
258
+ device: GPUDevice,
259
+ imports: Map<number, YuvImport[]>,
260
+ producer: number,
261
+ source: NativeVideoFrame,
262
+ rotation: number,
263
+ origin: { x: number; y: number },
264
+ width: number,
265
+ height: number,
266
+ target: GPUTexture,
267
+ ): void => {
268
+ "worklet";
269
+ const blit = getYuvBlit(device);
270
+ const pipeline = getPipeline(device, blit, target.format);
271
+ let entries = imports.get(producer);
272
+ if (!entries) {
273
+ entries = [];
274
+ imports.set(producer, entries);
275
+ }
276
+ let index = entries.findIndex(
277
+ (entry) => entry.handle === source.handle && entry.rotation === rotation,
278
+ );
279
+ if (index < 0) {
280
+ // The least recently drawn buffer goes first. Its last frame completed:
281
+ // every import ends with a checked flush before the next one starts.
282
+ if (entries.length >= maxImportsPerProducer) {
283
+ releaseYuvImport(entries[0]!);
284
+ entries.shift();
285
+ }
286
+ entries.push(importYuvBuffer(device, blit, source.handle, rotation));
287
+ index = entries.length - 1;
288
+ } else if (index !== entries.length - 1) {
289
+ entries.push(entries.splice(index, 1)[0]!);
290
+ index = entries.length - 1;
291
+ }
292
+ const entry = entries[index]!;
293
+ // Dawn swaps the apparent size of a frame turned by 90 or 270 degrees.
294
+ const turned = rotation === 90 || rotation === 270;
295
+ const frameWidth = turned ? source.height : source.width;
296
+ const frameHeight = turned ? source.width : source.height;
297
+ const mapping = [
298
+ width / frameWidth,
299
+ height / frameHeight,
300
+ origin.x / frameWidth,
301
+ origin.y / frameHeight,
302
+ ];
303
+ if (mapping.some((value, i) => value !== entry.mapping[i])) {
304
+ device.queue.writeBuffer(entry.uniforms, 0, new Float32Array(mapping));
305
+ entry.mapping = mapping;
306
+ }
307
+ entry.wrapper.beginAccess();
308
+ try {
309
+ const encoder = device.createCommandEncoder({ label: "video YUV blit" });
310
+ const pass = encoder.beginRenderPass({
311
+ label: "video YUV blit",
312
+ colorAttachments: [
313
+ {
314
+ view: target.createView(),
315
+ loadOp: "clear",
316
+ clearValue: { r: 0, g: 0, b: 0, a: 1 },
317
+ storeOp: "store",
318
+ },
319
+ ],
320
+ });
321
+ pass.setPipeline(pipeline);
322
+ pass.setBindGroup(0, entry.bindGroup);
323
+ pass.draw(3);
324
+ pass.end();
325
+ device.queue.submit([encoder.finish()]);
326
+ } finally {
327
+ // The caller's checked flush covers the draw; the release fence is not
328
+ // needed to hand the buffer back.
329
+ entry.wrapper.endAccess()?.fence.destroy();
330
+ }
331
+ };