react-native-skia-webgpu-video 0.1.0-beta.3 → 0.1.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 (81) hide show
  1. package/README.md +48 -43
  2. package/android/BUILD_VALIDATION.md +1 -1
  3. package/android/CMakeLists.txt +3 -2
  4. package/android/cpp/ExportHintSession.cpp +114 -0
  5. package/android/cpp/ExportHintSession.h +35 -0
  6. package/android/cpp/VideoEncoderHostObject.cpp +76 -101
  7. package/android/cpp/VideoEncoderHostObject.h +14 -9
  8. package/android/cpp/VideoFrame.cpp +3 -5
  9. package/android/cpp/cpp-adapter.cpp +7 -4
  10. package/android/src/main/java/com/azzapp/rnskv/GLFrameExtractor.java +3 -9
  11. package/android/src/main/java/com/azzapp/rnskv/VideoCompositionDecoder.java +8 -11
  12. package/android/src/main/java/com/azzapp/rnskv/VideoEncoder.java +68 -28
  13. package/docs/FRAME_PIXEL_FORMAT.md +17 -2
  14. package/docs/MIGRATION.md +1 -1
  15. package/docs/UPSTREAM_NOTES.md +42 -2
  16. package/ios/RNSVColorSpace.h +8 -3
  17. package/lib/module/canvas.js +3 -5
  18. package/lib/module/canvas.js.map +1 -1
  19. package/lib/module/exportFrames.js +8 -10
  20. package/lib/module/exportFrames.js.map +1 -1
  21. package/lib/module/exportTarget.js +183 -40
  22. package/lib/module/exportTarget.js.map +1 -1
  23. package/lib/module/exportVideoComposition.js +78 -35
  24. package/lib/module/exportVideoComposition.js.map +1 -1
  25. package/lib/module/frameInterop.js +323 -82
  26. package/lib/module/frameInterop.js.map +1 -1
  27. package/lib/module/gpu.js +1 -3
  28. package/lib/module/gpu.js.map +1 -1
  29. package/lib/module/gpuDevice.js +12 -0
  30. package/lib/module/gpuDevice.js.map +1 -1
  31. package/lib/module/memory.js +7 -0
  32. package/lib/module/memory.js.map +1 -1
  33. package/lib/module/previewLifecycle.js +2 -1
  34. package/lib/module/previewLifecycle.js.map +1 -1
  35. package/lib/module/videoCompositionPlayer.js +65 -14
  36. package/lib/module/videoCompositionPlayer.js.map +1 -1
  37. package/lib/module/videoFrameImage.js +1 -1
  38. package/lib/module/yuvBlit.js +31 -24
  39. package/lib/module/yuvBlit.js.map +1 -1
  40. package/lib/typescript/src/canvas.d.ts.map +1 -1
  41. package/lib/typescript/src/exportFrames.d.ts +3 -2
  42. package/lib/typescript/src/exportFrames.d.ts.map +1 -1
  43. package/lib/typescript/src/exportTarget.d.ts +53 -14
  44. package/lib/typescript/src/exportTarget.d.ts.map +1 -1
  45. package/lib/typescript/src/exportVideoComposition.d.ts +2 -1
  46. package/lib/typescript/src/exportVideoComposition.d.ts.map +1 -1
  47. package/lib/typescript/src/frameInterop.d.ts +16 -5
  48. package/lib/typescript/src/frameInterop.d.ts.map +1 -1
  49. package/lib/typescript/src/gpu.d.ts.map +1 -1
  50. package/lib/typescript/src/gpuDevice.d.ts +5 -1
  51. package/lib/typescript/src/gpuDevice.d.ts.map +1 -1
  52. package/lib/typescript/src/memory.d.ts +2 -0
  53. package/lib/typescript/src/memory.d.ts.map +1 -1
  54. package/lib/typescript/src/previewLifecycle.d.ts +2 -0
  55. package/lib/typescript/src/previewLifecycle.d.ts.map +1 -1
  56. package/lib/typescript/src/types.d.ts +18 -12
  57. package/lib/typescript/src/types.d.ts.map +1 -1
  58. package/lib/typescript/src/videoCompositionPlayer.d.ts +4 -5
  59. package/lib/typescript/src/videoCompositionPlayer.d.ts.map +1 -1
  60. package/lib/typescript/src/videoFrameImage.d.ts +1 -1
  61. package/lib/typescript/src/yuvBlit.d.ts +16 -14
  62. package/lib/typescript/src/yuvBlit.d.ts.map +1 -1
  63. package/package.json +6 -6
  64. package/patches/react-native-skia+3.10.0.patch +150 -0
  65. package/scripts/skia-canvas-patch.json +22 -258
  66. package/src/canvas.ts +3 -7
  67. package/src/exportFrames.ts +10 -11
  68. package/src/exportTarget.ts +217 -56
  69. package/src/exportVideoComposition.ts +130 -85
  70. package/src/frameInterop.ts +389 -96
  71. package/src/gpu.ts +1 -3
  72. package/src/gpuDevice.ts +26 -1
  73. package/src/memory.ts +6 -0
  74. package/src/previewLifecycle.ts +4 -1
  75. package/src/types.ts +18 -12
  76. package/src/videoCompositionPlayer.ts +76 -23
  77. package/src/videoFrameImage.ts +1 -1
  78. package/src/yuvBlit.ts +46 -31
  79. package/android/cpp/EncoderTargets.cpp +0 -226
  80. package/android/cpp/EncoderTargets.h +0 -70
  81. package/patches/react-native-skia+3.7.1.patch +0 -502
@@ -1,21 +1,33 @@
1
1
  /// <reference types="@webgpu/types" />
2
- import { Platform } from "react-native";
3
2
  import { BlendMode, Skia } from "react-native-skia";
4
3
  import type { SkCanvas, SkSurface } from "react-native-skia";
5
4
  import type {
6
5
  GPUSharedTextureMemory,
7
6
  GPUSharedTextureMemoryDescriptor,
8
7
  NativeVideoFrame,
8
+ RNCanvasContext,
9
9
  } from "react-native-webgpu";
10
10
  import { getVideoCanvas } from "./canvas";
11
- import { createNativeVideoGpuFrame, getVideoGpuDevice } from "./gpuDevice";
11
+ import {
12
+ createNativeVideoGpuFrame,
13
+ createNativeWindowGpuContext,
14
+ encoderWindowTextureUsage,
15
+ getVideoGpuDevice,
16
+ nativeVideoTextureUsage,
17
+ } from "./gpuDevice";
18
+ import {
19
+ frameByteSize,
20
+ releaseVideoMemory,
21
+ reserveVideoMemory,
22
+ } from "./memory";
12
23
  import type { VideoEncoder } from "./types";
13
24
 
14
- /** An encoder pool buffer that Skia draws into through WebGPU. */
25
+ /** A texture Skia draws an exported frame into through WebGPU: a lent
26
+ * encoder pool buffer on iOS, one of the encoder window's own textures on
27
+ * Android. */
15
28
  export type EncoderTarget = {
16
- pointer: bigint;
17
- /** Android: sync-fds ending the GPU access, handed over to the encoder. */
18
- fences: bigint[];
29
+ /** iOS: the lent CVPixelBuffer. */
30
+ pointer: bigint | null;
19
31
  frame: NativeVideoFrame | null;
20
32
  memory: GPUSharedTextureMemory | null;
21
33
  texture: GPUTexture | null;
@@ -24,9 +36,25 @@ export type EncoderTarget = {
24
36
  canvas: SkCanvas | null;
25
37
  };
26
38
 
27
- // Captured on the RN runtime. iOS lends CVPixelBuffers, imported through
28
- // their IOSurface; Android lends the AHardwareBuffer itself.
29
- const lendsPixelBuffers = Platform.OS === "ios";
39
+ /** Android: a canvas context presenting to the codec's input Surface. Skia
40
+ * draws each frame into one of two textures of our own, which a GPU copy then
41
+ * puts into the window's texture. The window's textures are the codec's
42
+ * input buffers: the copy waits on the GPU for the codec to hand one back,
43
+ * while the export goes on with the next frame, rather than the frame's
44
+ * checked wait. */
45
+ export type EncoderWindow = {
46
+ context: RNCanvasContext;
47
+ device: GPUDevice;
48
+ configured: boolean;
49
+ /** Drawn into in turn, each copied into the window after its frame. */
50
+ slots: EncoderTarget[];
51
+ next: number;
52
+ reservations: number[];
53
+ /** The window texture the probe acquired, the first frame's copy target. */
54
+ pending: GPUTexture | null;
55
+ };
56
+
57
+ const windowSlots = 2;
30
58
 
31
59
  type SharedMemoryDevice = GPUDevice & {
32
60
  importSharedTextureMemory?: (
@@ -43,38 +71,19 @@ export const supportsEncoderTargets = (encoder: VideoEncoder): boolean => {
43
71
  };
44
72
 
45
73
  // Declared before its callers: worklets capture their closure on creation.
46
- /** Ends GPU access; the lent buffer stays with the caller, to encode or
47
- * release. Call only after a checked flush(true) of its surface. With
48
- * `encoding`, Android keeps the access's sync-fds in `target.fences` for the
49
- * encoder, which owns and closes them: ending an AHB access submits a
50
- * release that flush(true) has not waited for. */
51
- export const closeEncoderTarget = (
52
- target: EncoderTarget,
53
- encoding = false,
54
- ): void => {
74
+ /** iOS: ends GPU access; the lent buffer stays with the caller, to encode or
75
+ * release. Call only after a checked flush(true) of its surface. */
76
+ export const closeEncoderTarget = (target: EncoderTarget): void => {
55
77
  "worklet";
56
- target.canvas?.dispose();
78
+ // Disposing the surface detaches its canvas.
57
79
  target.canvas = null;
58
80
  target.surface?.dispose();
59
81
  target.surface = null;
60
82
  if (target.accessing && target.memory && target.texture) {
61
83
  const ended = target.memory.endAccess(target.texture);
62
84
  target.accessing = false;
63
- for (const state of ended?.fences ?? []) {
64
- try {
65
- if (encoding && !lendsPixelBuffers) {
66
- const exported = state.fence.export();
67
- // Unreachable when negotiated: AHB interop requires the device's
68
- // only shared-fence feature to be sync-fd (see gpuDevice.ts).
69
- if (exported.type !== "sync-fd")
70
- throw new Error("GPU export needs sync-fd fences");
71
- target.fences.push(exported.handle);
72
- }
73
- } finally {
74
- // iOS: already waited for. The wrapper is released either way.
75
- state.fence.destroy();
76
- }
77
- }
85
+ // Already waited for. The wrappers are released either way.
86
+ for (const state of ended?.fences ?? []) state.fence.destroy();
78
87
  }
79
88
  target.accessing = false;
80
89
  target.texture?.destroy();
@@ -88,7 +97,7 @@ export const closeEncoderTarget = (
88
97
  target.frame = null;
89
98
  };
90
99
 
91
- /** Lends the next pool buffer and wraps it as a Skia surface. */
100
+ /** iOS: lends the next pool buffer and wraps it as a Skia surface. */
92
101
  export const acquireEncoderTarget = (
93
102
  encoder: VideoEncoder,
94
103
  width: number,
@@ -97,7 +106,6 @@ export const acquireEncoderTarget = (
97
106
  "worklet";
98
107
  const target: EncoderTarget = {
99
108
  pointer: encoder.acquireFrameBuffer!(),
100
- fences: [],
101
109
  frame: null,
102
110
  memory: null,
103
111
  texture: null,
@@ -106,33 +114,23 @@ export const acquireEncoderTarget = (
106
114
  canvas: null,
107
115
  };
108
116
  try {
109
- if (lendsPixelBuffers) {
110
- target.frame = createNativeVideoGpuFrame(target.pointer);
111
- if (
112
- target.frame.width !== width ||
113
- target.frame.height !== height ||
114
- target.frame.pixelFormat !== "bgra8"
115
- ) {
116
- throw new Error("Encoder buffer dimensions or BGRA layout mismatch");
117
- }
117
+ target.frame = createNativeVideoGpuFrame(target.pointer!);
118
+ if (
119
+ target.frame.width !== width ||
120
+ target.frame.height !== height ||
121
+ target.frame.pixelFormat !== "bgra8"
122
+ ) {
123
+ throw new Error("Encoder buffer dimensions or BGRA layout mismatch");
118
124
  }
119
125
  const device = getVideoGpuDevice() as SharedMemoryDevice;
120
126
  if (typeof device.importSharedTextureMemory !== "function") {
121
127
  throw new Error("WebGPU shared texture memory is unavailable");
122
128
  }
123
129
  target.memory = device.importSharedTextureMemory({
124
- handle: target.frame ? target.frame.handle : target.pointer,
130
+ handle: target.frame.handle,
125
131
  label: "video export frame",
126
132
  });
127
133
  target.texture = target.memory.createTexture();
128
- if (
129
- !lendsPixelBuffers &&
130
- (target.texture.width !== width ||
131
- target.texture.height !== height ||
132
- target.texture.format !== "rgba8unorm")
133
- ) {
134
- throw new Error("Encoder buffer dimensions or RGBA layout mismatch");
135
- }
136
134
  // Uninitialized: every frame starts with a full clear.
137
135
  target.memory.beginAccess(target.texture, false);
138
136
  target.accessing = true;
@@ -151,8 +149,9 @@ export const acquireEncoderTarget = (
151
149
  }
152
150
  };
153
151
 
154
- /** Clears one lent buffer on the GPU, then returns it unencoded. Returns the
155
- * failure, or null when this device can export through encoder targets. */
152
+ /** iOS: clears one lent buffer on the GPU, then returns it unencoded.
153
+ * Returns the failure, or null when this device can export through encoder
154
+ * targets. */
156
155
  export const probeEncoderTarget = (
157
156
  encoder: VideoEncoder,
158
157
  width: number,
@@ -177,3 +176,165 @@ export const probeEncoderTarget = (
177
176
  encoder.releaseFrameBuffer!();
178
177
  return failure;
179
178
  };
179
+
180
+ /** Android: the window's next texture, checked against the export size. */
181
+ const windowTexture = (
182
+ window: EncoderWindow,
183
+ width: number,
184
+ height: number,
185
+ ): GPUTexture => {
186
+ "worklet";
187
+ const texture = window.pending ?? window.context.getCurrentTexture();
188
+ window.pending = null;
189
+ if (
190
+ texture.width !== width ||
191
+ texture.height !== height ||
192
+ texture.format !== "rgba8unorm"
193
+ ) {
194
+ throw new Error("Encoder window dimensions or RGBA layout mismatch");
195
+ }
196
+ return texture;
197
+ };
198
+
199
+ /** Android: the slot the next frame draws into. Its previous copy completed:
200
+ * the checked wait the export makes before each frame's import covers the
201
+ * queue before it. */
202
+ export const acquireWindowTarget = (window: EncoderWindow): EncoderTarget => {
203
+ "worklet";
204
+ return window.slots[window.next]!;
205
+ };
206
+
207
+ /** Android: copies the slot Skia drew into to the window's next texture and
208
+ * hands it to the encoder, with its time. Call once the slot's surface was
209
+ * flushed: the copy follows the drawing on the same queue. */
210
+ export const presentWindowTarget = (
211
+ encoder: VideoEncoder,
212
+ window: EncoderWindow,
213
+ target: EncoderTarget,
214
+ time: number,
215
+ ): void => {
216
+ "worklet";
217
+ const source = target.texture!;
218
+ const destination = windowTexture(window, source.width, source.height);
219
+ const commands = window.device.createCommandEncoder({
220
+ label: "video export window copy",
221
+ });
222
+ commands.copyTextureToTexture(
223
+ { texture: source },
224
+ { texture: destination },
225
+ { width: source.width, height: source.height },
226
+ );
227
+ window.device.queue.submit([commands.finish()]);
228
+ window.next = (window.next + 1) % window.slots.length;
229
+ // The encoder sets the frame's time on the window, then presents.
230
+ encoder.encodeFrame(
231
+ { kind: "window", present: () => window.context.present() },
232
+ time,
233
+ );
234
+ };
235
+
236
+ /** Android: the swapchain gives the codec its buffers back at once, rather
237
+ * than once the context is collected. Call before disposing the encoder. */
238
+ export const closeEncoderWindow = (window: EncoderWindow): void => {
239
+ "worklet";
240
+ window.pending = null;
241
+ if (!window.configured) return;
242
+ window.context.unconfigure();
243
+ window.configured = false;
244
+ };
245
+
246
+ /** Android: frees the window's own textures. Call once their last recording
247
+ * was submitted: a copy still in flight keeps its source alive. */
248
+ export const releaseEncoderWindow = (window: EncoderWindow): void => {
249
+ "worklet";
250
+ while (window.slots.length) {
251
+ const slot = window.slots[window.slots.length - 1]!;
252
+ // Disposing the surface detaches its canvas.
253
+ slot.canvas = null;
254
+ slot.surface?.dispose();
255
+ slot.surface = null;
256
+ slot.texture?.destroy();
257
+ slot.texture = null;
258
+ window.slots.pop();
259
+ }
260
+ while (window.reservations.length) {
261
+ releaseVideoMemory(window.reservations[window.reservations.length - 1]!);
262
+ window.reservations.pop();
263
+ }
264
+ };
265
+
266
+ /** Android: opens a canvas context on the encoder's input window with its
267
+ * slots, acquires the window's first texture and draws a clear into the
268
+ * first slot: nothing reaches the encoder. Returns the window, or the
269
+ * failure, after which the window is closed and the export can fall back to
270
+ * readback. */
271
+ export const probeEncoderWindow = (
272
+ encoder: VideoEncoder,
273
+ width: number,
274
+ height: number,
275
+ ): { window: EncoderWindow | null; failure: unknown } => {
276
+ "worklet";
277
+ let window: EncoderWindow | null = null;
278
+ let failure: unknown = null;
279
+ try {
280
+ if (typeof encoder.acquireInputWindow !== "function")
281
+ throw new Error("Encoder cannot present through its input window");
282
+ const device = getVideoGpuDevice();
283
+ window = {
284
+ context: createNativeWindowGpuContext(
285
+ encoder.acquireInputWindow(),
286
+ width,
287
+ height,
288
+ ),
289
+ device,
290
+ configured: false,
291
+ slots: [],
292
+ next: 0,
293
+ reservations: [],
294
+ pending: null,
295
+ };
296
+ // Presented in FIFO order, opaque: every frame reaches the codec.
297
+ window.context.configure({
298
+ device,
299
+ format: "rgba8unorm",
300
+ usage: encoderWindowTextureUsage,
301
+ });
302
+ window.configured = true;
303
+ for (let i = 0; i < windowSlots; i++) {
304
+ window.reservations.push(
305
+ reserveVideoMemory(frameByteSize(width, height), "export window slot"),
306
+ );
307
+ const slot: EncoderTarget = {
308
+ pointer: null,
309
+ frame: null,
310
+ memory: null,
311
+ texture: device.createTexture({
312
+ label: "video export window slot",
313
+ size: { width, height },
314
+ format: "rgba8unorm",
315
+ usage: nativeVideoTextureUsage,
316
+ }),
317
+ accessing: false,
318
+ surface: null,
319
+ canvas: null,
320
+ };
321
+ window.slots.push(slot);
322
+ slot.surface = Skia.Surface.MakeFromGPUTexture(slot.texture!);
323
+ if (!slot.surface) throw new Error("Cannot wrap the export frame");
324
+ slot.canvas = getVideoCanvas(slot.surface);
325
+ }
326
+ window.pending = windowTexture(window, width, height);
327
+ const target = acquireWindowTarget(window);
328
+ target.canvas!.drawColor(Skia.Color("#00000000"), BlendMode.Clear);
329
+ target.surface!.flush(true);
330
+ } catch (error) {
331
+ failure = error ?? new Error("GPU export probe failed");
332
+ }
333
+ if (failure === null) return { window, failure };
334
+ if (window) {
335
+ // Not caught: a window still drawn by WebGPU cannot fall back to EGL.
336
+ closeEncoderWindow(window);
337
+ releaseEncoderWindow(window);
338
+ }
339
+ return { window: null, failure };
340
+ };