react-native-skia-webgpu-video 0.1.0-beta.2 → 0.1.0-beta.4
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/LICENSE +0 -1
- package/README.md +62 -34
- 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/VideoComposition.cpp +12 -0
- package/android/cpp/VideoCompositionFramesExtractor.cpp +6 -0
- package/android/cpp/VideoCompositionFramesExtractor.h +1 -0
- package/android/cpp/VideoCompositionFramesExtractorHostObject.cpp +1 -1
- 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/PreviewFrameRateVote.java +104 -0
- package/android/src/main/java/com/azzapp/rnskv/VideoComposition.java +4 -0
- package/android/src/main/java/com/azzapp/rnskv/VideoCompositionDecoder.java +8 -9
- package/android/src/main/java/com/azzapp/rnskv/VideoCompositionFramesExtractor.java +32 -0
- package/android/src/main/java/com/azzapp/rnskv/VideoEncoder.java +68 -28
- package/docs/FRAME_PIXEL_FORMAT.md +124 -0
- package/docs/MIGRATION.md +1 -1
- package/docs/UPSTREAM_NOTES.md +15 -2
- package/docs/VERIFICATION_0.1.0-beta.3.md +147 -0
- package/ios/NativeResourceStats.h +9 -0
- package/ios/RNSVColorSpace.h +35 -4
- package/ios/ReactNativeSkiaVideo.mm +4 -0
- package/ios/VideoComposition.h +20 -0
- package/ios/VideoCompositionFramesExtractorHostObject.h +5 -0
- package/ios/VideoCompositionFramesExtractorHostObject.mm +31 -2
- package/ios/VideoCompositionItemDecoder.mm +21 -8
- package/ios/VideoFrame.mm +6 -0
- package/lib/module/canvas.js +3 -5
- package/lib/module/canvas.js.map +1 -1
- package/lib/module/exportFrames.js +25 -20
- 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 +53 -12
- package/lib/module/exportVideoComposition.js.map +1 -1
- package/lib/module/frameInterop.js +15 -12
- package/lib/module/frameInterop.js.map +1 -1
- package/lib/module/gpu.js +0 -2
- 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/previewLifecycle.js +0 -1
- package/lib/module/previewLifecycle.js.map +1 -1
- package/lib/module/videoCompositionPlayer.js +13 -5
- package/lib/module/videoCompositionPlayer.js.map +1 -1
- package/lib/module/yuvBlit.js +98 -21
- 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 +11 -10
- 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.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/previewLifecycle.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +36 -5
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/lib/typescript/src/videoCompositionPlayer.d.ts.map +1 -1
- package/lib/typescript/src/yuvBlit.d.ts +26 -14
- package/lib/typescript/src/yuvBlit.d.ts.map +1 -1
- package/package.json +6 -6
- package/patches/react-native-skia+3.7.4.patch +146 -0
- package/scripts/skia-canvas-patch.json +22 -258
- package/src/canvas.ts +3 -7
- package/src/exportFrames.ts +53 -36
- package/src/exportTarget.ts +217 -56
- package/src/exportVideoComposition.ts +87 -26
- package/src/frameInterop.ts +26 -11
- package/src/gpu.ts +0 -2
- package/src/gpuDevice.ts +26 -1
- package/src/previewLifecycle.ts +0 -1
- package/src/types.ts +37 -5
- package/src/videoCompositionPlayer.ts +14 -4
- package/src/yuvBlit.ts +108 -27
- 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/canvas.ts
CHANGED
|
@@ -12,11 +12,7 @@ export const getVideoCanvas = (surface: SkSurface): SkCanvas => {
|
|
|
12
12
|
);
|
|
13
13
|
}
|
|
14
14
|
checked.enableCheckedSubmissions();
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"Skia canvas lifecycle patch is missing from the native binary. Apply the patch and rebuild the native app.",
|
|
19
|
-
);
|
|
20
|
-
}
|
|
21
|
-
return canvas;
|
|
15
|
+
// Owned by the surface: its dispose() detaches the canvas, and drawing on
|
|
16
|
+
// it throws from then on.
|
|
17
|
+
return surface.getCanvas();
|
|
22
18
|
};
|
package/src/exportFrames.ts
CHANGED
|
@@ -18,13 +18,18 @@ import {
|
|
|
18
18
|
reserveVideoMemory,
|
|
19
19
|
} from "./memory";
|
|
20
20
|
import type { VideoFrame } from "./types";
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
blitNv12Frame,
|
|
23
|
+
blitYuvFrame,
|
|
24
|
+
releaseYuvImports,
|
|
25
|
+
yuvFrameRegion,
|
|
26
|
+
} from "./yuvBlit";
|
|
22
27
|
import type { YuvImport } from "./yuvBlit";
|
|
23
28
|
|
|
24
|
-
/** A decoded frame sampled in place, without an owned snapshot:
|
|
25
|
-
* IOSurface
|
|
26
|
-
*
|
|
27
|
-
* image is in use. */
|
|
29
|
+
/** A decoded frame sampled in place, without an owned snapshot: a BGRA
|
|
30
|
+
* frame's IOSurface on iOS, or for a YUV frame (Android, iOS NV12) its
|
|
31
|
+
* producer's RGBA texture, which the decoder buffer was just converted into.
|
|
32
|
+
* The decoder lease stays open while the image is in use. */
|
|
28
33
|
export type ExportFrameImport = {
|
|
29
34
|
id: number;
|
|
30
35
|
frame: VideoFrame;
|
|
@@ -37,7 +42,7 @@ export type ExportFrameImport = {
|
|
|
37
42
|
aliases?: WeakSet<VideoFrame>;
|
|
38
43
|
};
|
|
39
44
|
|
|
40
|
-
/**
|
|
45
|
+
/** The texture a producer's YUV frames are converted into. */
|
|
41
46
|
type ProducerTexture = { texture: GPUTexture; reservation: number };
|
|
42
47
|
|
|
43
48
|
/** Per export session, keyed by producer. `enabled` turns off after a first
|
|
@@ -48,7 +53,7 @@ export type ExportFrameImports = {
|
|
|
48
53
|
producers: Set<number>;
|
|
49
54
|
enabled: boolean;
|
|
50
55
|
imported: boolean;
|
|
51
|
-
/**
|
|
56
|
+
/** One RGBA texture per producer of YUV frames, rewritten by each frame. */
|
|
52
57
|
textures: Map<number, ProducerTexture>;
|
|
53
58
|
/** Android: decoder buffers imported once (see yuvBlit.ts). */
|
|
54
59
|
yuv: Map<number, YuvImport[]>;
|
|
@@ -65,6 +70,8 @@ type NativeBufferTexture = {
|
|
|
65
70
|
nativeBuffer: bigint;
|
|
66
71
|
region?: { x: number; y: number; width: number; height: number };
|
|
67
72
|
rotation?: number;
|
|
73
|
+
/** iOS: the decoder's NV12 buffer (`framePixelFormat: 'yuv'`), or BGRA. */
|
|
74
|
+
format?: "nv12" | "bgra8";
|
|
68
75
|
};
|
|
69
76
|
|
|
70
77
|
// Captured on the RN runtime. Android samples its ImageReader YUV frames
|
|
@@ -117,8 +124,8 @@ export const releaseExportFrame = (entry: ExportFrameImport): void => {
|
|
|
117
124
|
entry.frame.dispose?.();
|
|
118
125
|
};
|
|
119
126
|
|
|
120
|
-
/**
|
|
121
|
-
*
|
|
127
|
+
/** Releases a producer's texture and decoder buffer imports. Call only once
|
|
128
|
+
* its last frame's GPU work has completed. */
|
|
122
129
|
const releaseProducerTexture = (
|
|
123
130
|
imports: ExportFrameImports,
|
|
124
131
|
producer: number,
|
|
@@ -133,10 +140,11 @@ const releaseProducerTexture = (
|
|
|
133
140
|
}
|
|
134
141
|
};
|
|
135
142
|
|
|
136
|
-
/**
|
|
137
|
-
* texture, wrapped as a Skia image. The
|
|
138
|
-
* frame's drawing, on the same queue;
|
|
139
|
-
* both before the lease or the texture
|
|
143
|
+
/** Converts a decoded YUV frame (an Android ImageReader buffer, or an iOS
|
|
144
|
+
* NV12 one) into its producer's RGBA texture, wrapped as a Skia image. The
|
|
145
|
+
* conversion is submitted ahead of the frame's drawing, on the same queue;
|
|
146
|
+
* the export's checked flush waits for both before the lease or the texture
|
|
147
|
+
* is reused. */
|
|
140
148
|
const importYuvExportFrame = (
|
|
141
149
|
imports: ExportFrameImports,
|
|
142
150
|
frame: VideoFrame,
|
|
@@ -158,12 +166,17 @@ const importYuvExportFrame = (
|
|
|
158
166
|
try {
|
|
159
167
|
entry.nativeFrame = createNativeVideoGpuFrame(source.nativeBuffer);
|
|
160
168
|
const rotation = source.rotation ?? 0;
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
frame
|
|
166
|
-
|
|
169
|
+
// iOS NV12: the whole buffer, in decoded orientation.
|
|
170
|
+
const nv12 = source.format === "nv12";
|
|
171
|
+
if (
|
|
172
|
+
nv12 &&
|
|
173
|
+
(entry.nativeFrame.width !== frame.width ||
|
|
174
|
+
entry.nativeFrame.height !== frame.height)
|
|
175
|
+
)
|
|
176
|
+
throw new Error("Native video buffer dimensions mismatch");
|
|
177
|
+
const region = nv12
|
|
178
|
+
? null
|
|
179
|
+
: yuvFrameRegion(source.region!, rotation, entry.nativeFrame);
|
|
167
180
|
const device = getVideoGpuDevice();
|
|
168
181
|
target = imports.textures.get(producer);
|
|
169
182
|
if (
|
|
@@ -199,17 +212,17 @@ const importYuvExportFrame = (
|
|
|
199
212
|
}
|
|
200
213
|
// Can throw after submitting: drain below before giving the lease back.
|
|
201
214
|
submitted = true;
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
215
|
+
if (nv12) blitNv12Frame(device, entry.nativeFrame, target.texture);
|
|
216
|
+
else
|
|
217
|
+
blitYuvFrame(
|
|
218
|
+
device,
|
|
219
|
+
imports.yuv,
|
|
220
|
+
producer,
|
|
221
|
+
entry.nativeFrame,
|
|
222
|
+
rotation,
|
|
223
|
+
region!,
|
|
224
|
+
target.texture,
|
|
225
|
+
);
|
|
213
226
|
entry.image = Skia.Image.MakeImageFromGPUTexture(target.texture);
|
|
214
227
|
if (!entry.image) throw new Error("Cannot wrap the decoded frame");
|
|
215
228
|
return entry;
|
|
@@ -233,15 +246,18 @@ const importYuvExportFrame = (
|
|
|
233
246
|
};
|
|
234
247
|
|
|
235
248
|
/** Wraps a decoded BGRA CVPixelBuffer's IOSurface as a Skia image, read-only.
|
|
236
|
-
* The decoder already finished writing it; no producer fence is needed.
|
|
237
|
-
* Android,
|
|
249
|
+
* The decoder already finished writing it; no producer fence is needed. A
|
|
250
|
+
* YUV frame (Android, iOS NV12) is converted instead (see importYuvExportFrame). */
|
|
238
251
|
export const importExportFrame = (
|
|
239
252
|
frame: VideoFrame,
|
|
240
253
|
imports?: ExportFrameImports,
|
|
241
254
|
): ExportFrameImport => {
|
|
242
255
|
"worklet";
|
|
243
|
-
if (
|
|
244
|
-
|
|
256
|
+
if (
|
|
257
|
+
convertsYuv ||
|
|
258
|
+
(frame.texture as NativeBufferTexture).format === "nv12"
|
|
259
|
+
) {
|
|
260
|
+
if (!imports) throw new Error("YUV in-place frames need their imports");
|
|
245
261
|
return importYuvExportFrame(imports, frame);
|
|
246
262
|
}
|
|
247
263
|
const entry: ExportFrameImport = {
|
|
@@ -311,8 +327,9 @@ export const releaseExportFrames = (imports: ExportFrameImports): void => {
|
|
|
311
327
|
|
|
312
328
|
/** The export counterpart of ownVideoFrames on iOS: decoded frames are drawn
|
|
313
329
|
* in place, and a producer's previous frame is released once its next one is
|
|
314
|
-
* imported, since
|
|
315
|
-
* frames, or every frame after a first failed
|
|
330
|
+
* imported, since the export waits for the GPU to complete a frame before it
|
|
331
|
+
* imports the next one. Other frames, or every frame after a first failed
|
|
332
|
+
* import, use `fallback`. */
|
|
316
333
|
export const ownExportFrames = (
|
|
317
334
|
imports: ExportFrameImports,
|
|
318
335
|
decoded: Record<string, VideoFrame>,
|
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
|
+
};
|