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.
- package/README.md +48 -43
- package/android/BUILD_VALIDATION.md +1 -1
- package/android/CMakeLists.txt +3 -2
- package/android/cpp/ExportHintSession.cpp +114 -0
- package/android/cpp/ExportHintSession.h +35 -0
- package/android/cpp/VideoEncoderHostObject.cpp +76 -101
- package/android/cpp/VideoEncoderHostObject.h +14 -9
- package/android/cpp/VideoFrame.cpp +3 -5
- package/android/cpp/cpp-adapter.cpp +7 -4
- package/android/src/main/java/com/azzapp/rnskv/GLFrameExtractor.java +3 -9
- package/android/src/main/java/com/azzapp/rnskv/VideoCompositionDecoder.java +8 -11
- package/android/src/main/java/com/azzapp/rnskv/VideoEncoder.java +68 -28
- package/docs/FRAME_PIXEL_FORMAT.md +17 -2
- package/docs/MIGRATION.md +1 -1
- package/docs/UPSTREAM_NOTES.md +42 -2
- package/ios/RNSVColorSpace.h +8 -3
- package/lib/module/canvas.js +3 -5
- package/lib/module/canvas.js.map +1 -1
- package/lib/module/exportFrames.js +8 -10
- package/lib/module/exportFrames.js.map +1 -1
- package/lib/module/exportTarget.js +183 -40
- package/lib/module/exportTarget.js.map +1 -1
- package/lib/module/exportVideoComposition.js +78 -35
- package/lib/module/exportVideoComposition.js.map +1 -1
- package/lib/module/frameInterop.js +323 -82
- package/lib/module/frameInterop.js.map +1 -1
- package/lib/module/gpu.js +1 -3
- package/lib/module/gpu.js.map +1 -1
- package/lib/module/gpuDevice.js +12 -0
- package/lib/module/gpuDevice.js.map +1 -1
- package/lib/module/memory.js +7 -0
- package/lib/module/memory.js.map +1 -1
- package/lib/module/previewLifecycle.js +2 -1
- package/lib/module/previewLifecycle.js.map +1 -1
- package/lib/module/videoCompositionPlayer.js +65 -14
- package/lib/module/videoCompositionPlayer.js.map +1 -1
- package/lib/module/videoFrameImage.js +1 -1
- package/lib/module/yuvBlit.js +31 -24
- package/lib/module/yuvBlit.js.map +1 -1
- package/lib/typescript/src/canvas.d.ts.map +1 -1
- package/lib/typescript/src/exportFrames.d.ts +3 -2
- package/lib/typescript/src/exportFrames.d.ts.map +1 -1
- package/lib/typescript/src/exportTarget.d.ts +53 -14
- package/lib/typescript/src/exportTarget.d.ts.map +1 -1
- package/lib/typescript/src/exportVideoComposition.d.ts +2 -1
- package/lib/typescript/src/exportVideoComposition.d.ts.map +1 -1
- package/lib/typescript/src/frameInterop.d.ts +16 -5
- package/lib/typescript/src/frameInterop.d.ts.map +1 -1
- package/lib/typescript/src/gpu.d.ts.map +1 -1
- package/lib/typescript/src/gpuDevice.d.ts +5 -1
- package/lib/typescript/src/gpuDevice.d.ts.map +1 -1
- package/lib/typescript/src/memory.d.ts +2 -0
- package/lib/typescript/src/memory.d.ts.map +1 -1
- package/lib/typescript/src/previewLifecycle.d.ts +2 -0
- package/lib/typescript/src/previewLifecycle.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +18 -12
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/lib/typescript/src/videoCompositionPlayer.d.ts +4 -5
- package/lib/typescript/src/videoCompositionPlayer.d.ts.map +1 -1
- package/lib/typescript/src/videoFrameImage.d.ts +1 -1
- package/lib/typescript/src/yuvBlit.d.ts +16 -14
- package/lib/typescript/src/yuvBlit.d.ts.map +1 -1
- package/package.json +6 -6
- package/patches/react-native-skia+3.10.0.patch +150 -0
- package/scripts/skia-canvas-patch.json +22 -258
- package/src/canvas.ts +3 -7
- package/src/exportFrames.ts +10 -11
- package/src/exportTarget.ts +217 -56
- package/src/exportVideoComposition.ts +130 -85
- package/src/frameInterop.ts +389 -96
- package/src/gpu.ts +1 -3
- package/src/gpuDevice.ts +26 -1
- package/src/memory.ts +6 -0
- package/src/previewLifecycle.ts +4 -1
- package/src/types.ts +18 -12
- package/src/videoCompositionPlayer.ts +76 -23
- package/src/videoFrameImage.ts +1 -1
- package/src/yuvBlit.ts +46 -31
- package/android/cpp/EncoderTargets.cpp +0 -226
- package/android/cpp/EncoderTargets.h +0 -70
- package/patches/react-native-skia+3.7.1.patch +0 -502
package/src/exportTarget.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
/**
|
|
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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
/**
|
|
47
|
-
* release. Call only after a checked flush(true) of its surface.
|
|
48
|
-
|
|
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
|
-
|
|
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
|
|
64
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
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
|
-
/**
|
|
155
|
-
* failure, or null when this device can export through encoder
|
|
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
|
+
};
|