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/gpuDevice.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
importDevice,
|
|
9
9
|
installWebGPU,
|
|
10
10
|
} from "react-native-webgpu";
|
|
11
|
-
import type { NativeVideoFrame } from "react-native-webgpu";
|
|
11
|
+
import type { NativeVideoFrame, RNCanvasContext } from "react-native-webgpu";
|
|
12
12
|
import RNSkiaVideoModule from "./RNSkiaVideoModule";
|
|
13
13
|
|
|
14
14
|
// Evaluate on the RN runtime. NativeObject serializers carry these objects to
|
|
@@ -61,11 +61,36 @@ export const createNativeVideoGpuFrame = (
|
|
|
61
61
|
return nativeWebGPU.createVideoFrameFromNativeBuffer(pointer);
|
|
62
62
|
};
|
|
63
63
|
|
|
64
|
+
/** Android: a canvas context presenting to a native window that is not a
|
|
65
|
+
* view, such as an encoder's input Surface. */
|
|
66
|
+
export const createNativeWindowGpuContext = (
|
|
67
|
+
window: bigint,
|
|
68
|
+
width: number,
|
|
69
|
+
height: number,
|
|
70
|
+
): RNCanvasContext => {
|
|
71
|
+
"worklet";
|
|
72
|
+
if (
|
|
73
|
+
typeof nativeWebGPU?.MakeWebGPUCanvasContextFromNativeWindow !== "function"
|
|
74
|
+
)
|
|
75
|
+
throw new Error(
|
|
76
|
+
"React Native WebGPU cannot present to a native window; 0.19.0 or later is required",
|
|
77
|
+
);
|
|
78
|
+
return nativeWebGPU.MakeWebGPUCanvasContextFromNativeWindow(
|
|
79
|
+
window,
|
|
80
|
+
width,
|
|
81
|
+
height,
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
|
|
64
85
|
export const nativeVideoTextureUsage =
|
|
65
86
|
GPUTextureUsage.RENDER_ATTACHMENT |
|
|
66
87
|
GPUTextureUsage.TEXTURE_BINDING |
|
|
67
88
|
GPUTextureUsage.COPY_SRC;
|
|
68
89
|
|
|
90
|
+
// An encoder's input window only receives copies of the frames Skia drew.
|
|
91
|
+
export const encoderWindowTextureUsage =
|
|
92
|
+
GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_DST;
|
|
93
|
+
|
|
69
94
|
// CPU RGBA uploads additionally need COPY_DST. Keep the native-buffer blit's
|
|
70
95
|
// descriptor unchanged: that path renders into its destination attachment.
|
|
71
96
|
export const rgbaVideoTextureUsage =
|
package/src/memory.ts
CHANGED
|
@@ -37,6 +37,12 @@ export const frameByteSize = (width: number, height: number): number => {
|
|
|
37
37
|
return bytes;
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
+
/** The budget's counters, from any runtime; null without them. */
|
|
41
|
+
export const videoMemoryBudget = (): VideoMemoryStats | null => {
|
|
42
|
+
"worklet";
|
|
43
|
+
return RNSkiaVideoModule.getMemoryBudgetStats?.() ?? null;
|
|
44
|
+
};
|
|
45
|
+
|
|
40
46
|
export const reserveVideoMemory = (bytes: number, label: string): number => {
|
|
41
47
|
"worklet";
|
|
42
48
|
return RNSkiaVideoModule.reserveMemory?.(bytes, label) ?? 0;
|
package/src/previewLifecycle.ts
CHANGED
|
@@ -5,6 +5,8 @@ import type { VideoFrame } from "./types";
|
|
|
5
5
|
|
|
6
6
|
type PreviewResources = {
|
|
7
7
|
surface?: SkSurface | null;
|
|
8
|
+
/** The preview's second buffer (see useVideoCompositionPlayer). */
|
|
9
|
+
spareSurface?: SkSurface | null;
|
|
8
10
|
canvas?: SkCanvas | null;
|
|
9
11
|
images?: Array<SkImage | null>;
|
|
10
12
|
frames?: VideoFrame[];
|
|
@@ -33,10 +35,11 @@ export const disposePreviewResources = (resources: PreviewResources) => {
|
|
|
33
35
|
frame.dispose?.();
|
|
34
36
|
resources.frames.shift();
|
|
35
37
|
}
|
|
36
|
-
resources.canvas?.dispose();
|
|
37
38
|
resources.canvas = null;
|
|
38
39
|
resources.surface?.dispose();
|
|
39
40
|
resources.surface = null;
|
|
41
|
+
resources.spareSurface?.dispose();
|
|
42
|
+
resources.spareSurface = null;
|
|
40
43
|
releaseVideoMemory(resources.reservation ?? 0);
|
|
41
44
|
resources.reservation = 0;
|
|
42
45
|
} catch (error) {
|
package/src/types.ts
CHANGED
|
@@ -157,15 +157,16 @@ export type VideoPlayer = {
|
|
|
157
157
|
* (12.4 MB rather than 33.2 MB in 4K), so the decoder holds more frames
|
|
158
158
|
* ahead for the same memory. On Android only 8-bit SDR tracks decoded at
|
|
159
159
|
* their own size; other tracks stay `rgba`.
|
|
160
|
-
* - `rgba
|
|
161
|
-
* Android: by an OpenGL pass)
|
|
160
|
+
* - `rgba` (deprecated): frames converted to RGB as they are decoded (iOS: by
|
|
161
|
+
* VideoToolbox, Android: by an OpenGL pass). It no longer has an advantage:
|
|
162
|
+
* exports run as fast in `yuv`, which renders the same colors, and a 4K60
|
|
163
|
+
* preview keeps fewer frames on screen in `rgba`, whose frames take 4 bytes
|
|
164
|
+
* a pixel. It still works; use `auto`.
|
|
162
165
|
* - `auto`: `yuv`, on both platforms.
|
|
163
166
|
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
* transfer function (Display P3, sRGB) to Rec.709; `yuv` takes its samples as
|
|
168
|
-
* Rec.709, as Android does in both formats.
|
|
167
|
+
* Both convert with the frame's YCbCr matrix and range. On iOS, both convert
|
|
168
|
+
* HDR (HLG, PQ), BT.2020, Display P3 and sRGB files to Rec.709 through
|
|
169
|
+
* AVFoundation. Android takes SDR samples as Rec.709 in both formats.
|
|
169
170
|
*/
|
|
170
171
|
export type VideoFramePixelFormat = "auto" | "yuv" | "rgba";
|
|
171
172
|
|
|
@@ -421,14 +422,19 @@ export type VideoEncoder = {
|
|
|
421
422
|
*/
|
|
422
423
|
encodeFrame(texture: unknown, time: number): void;
|
|
423
424
|
/**
|
|
424
|
-
*
|
|
425
|
-
*
|
|
426
|
-
* frame (on Android with the sync-fds ending its GPU access, in `fences`),
|
|
427
|
-
* or release it. One at a time.
|
|
425
|
+
* iOS: lends the next pool buffer (a CVPixelBuffer) to draw into on the
|
|
426
|
+
* GPU. Pass it back as a native-buffer frame, or release it. One at a time.
|
|
428
427
|
*/
|
|
429
428
|
acquireFrameBuffer?(): bigint;
|
|
430
|
-
/**
|
|
429
|
+
/** iOS: returns a lent buffer that will not be encoded. */
|
|
431
430
|
releaseFrameBuffer?(): void;
|
|
431
|
+
/**
|
|
432
|
+
* Android: the codec's input Surface, as an ANativeWindow pointer for a
|
|
433
|
+
* WebGPU canvas context to present to. Each frame is then encoded as
|
|
434
|
+
* `{ kind: "window", present }`: the encoder sets the frame's time on the
|
|
435
|
+
* window and calls `present`, which presents the context.
|
|
436
|
+
*/
|
|
437
|
+
acquireInputWindow?(): bigint;
|
|
432
438
|
/*
|
|
433
439
|
* Finish writing the video to the output file.
|
|
434
440
|
*/
|
|
@@ -19,10 +19,18 @@ import RNSkiaVideoModule from "./RNSkiaVideoModule";
|
|
|
19
19
|
import useEventListener from "./utils/useEventListener";
|
|
20
20
|
import { PixelRatio } from "react-native";
|
|
21
21
|
import { scheduleOnRN } from "react-native-worklets";
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
ownVideoFrames,
|
|
24
|
+
releaseVideoFrameImage,
|
|
25
|
+
videoFrameImportWaits,
|
|
26
|
+
} from "./frameInterop";
|
|
23
27
|
import { frameByteSize, reserveVideoMemory } from "./memory";
|
|
24
28
|
import { disposePreviewResources } from "./previewLifecycle";
|
|
25
29
|
|
|
30
|
+
// Draws in a row the preview submits without waiting for the GPU: one frame
|
|
31
|
+
// of a 24 fps video spans five vsyncs at 120 Hz, redrawn as time goes by.
|
|
32
|
+
const maxUncheckedDraws = 6;
|
|
33
|
+
|
|
26
34
|
type UseVideoCompositionPlayerOptions<T = undefined> = {
|
|
27
35
|
/**
|
|
28
36
|
* The video composition to play.
|
|
@@ -87,11 +95,10 @@ type UseVideoCompositionPlayerOptions<T = undefined> = {
|
|
|
87
95
|
/**
|
|
88
96
|
* Import a newly decoded frame in one vsync and draw it in the next, instead
|
|
89
97
|
* of both in the same one. On Android the import (YUV blit and its checked
|
|
90
|
-
* GPU wait) and the draw
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* as it is imported.
|
|
98
|
+
* GPU wait) and the draw cost about as much as each other, and together they
|
|
99
|
+
* overrun a 90–120 Hz frame budget on the UI thread. The option applies on
|
|
100
|
+
* iOS too. The picture then trails the clock by one vsync. The first frame
|
|
101
|
+
* of a session is still drawn as soon as it is imported.
|
|
95
102
|
* @default false
|
|
96
103
|
*/
|
|
97
104
|
splitImportAndDraw?: boolean;
|
|
@@ -276,6 +283,15 @@ export const useVideoComposition = <T = undefined>({
|
|
|
276
283
|
|
|
277
284
|
const surfaceSharedValue = useSharedValue<SkSurface | null>(null);
|
|
278
285
|
const canvasSharedValue = useSharedValue<SkCanvas | null>(null);
|
|
286
|
+
// The preview draws into two surfaces in turn, and shows each through an
|
|
287
|
+
// image sharing its texture (asImage): no copy per draw, as a snapshot
|
|
288
|
+
// made. The picture on screen stays whole while the next is drawn into the
|
|
289
|
+
// other surface.
|
|
290
|
+
const spareSurfaceSharedValue = useSharedValue<SkSurface | null>(null);
|
|
291
|
+
const spareCanvasSharedValue = useSharedValue<SkCanvas | null>(null);
|
|
292
|
+
const bufferImages = useSharedValue<Array<SkImage | null>>([]);
|
|
293
|
+
// The buffer on screen: 0, 1 (the spare), or -1 before the first draw.
|
|
294
|
+
const frontBuffer = useSharedValue(-1);
|
|
279
295
|
const surfaceReservation = useSharedValue(0);
|
|
280
296
|
const surfaceWidth = useSharedValue(0);
|
|
281
297
|
const surfaceHeight = useSharedValue(0);
|
|
@@ -283,8 +299,10 @@ export const useVideoComposition = <T = undefined>({
|
|
|
283
299
|
"worklet";
|
|
284
300
|
const resources = {
|
|
285
301
|
surface: surfaceSharedValue.value,
|
|
302
|
+
spareSurface: spareSurfaceSharedValue.value,
|
|
286
303
|
canvas: canvasSharedValue.value,
|
|
287
|
-
|
|
304
|
+
// currentFrame is one of the buffers' images.
|
|
305
|
+
images: [...bufferImages.value, pendingSnapshot.value],
|
|
288
306
|
frames: Object.values(previousSources.value),
|
|
289
307
|
reservation: surfaceReservation.value,
|
|
290
308
|
afterDraw: pendingAfterDraw.value?.run,
|
|
@@ -300,6 +318,10 @@ export const useVideoComposition = <T = undefined>({
|
|
|
300
318
|
sourcesGeneration.value = 0;
|
|
301
319
|
surfaceSharedValue.value = null;
|
|
302
320
|
canvasSharedValue.value = null;
|
|
321
|
+
spareSurfaceSharedValue.value = null;
|
|
322
|
+
spareCanvasSharedValue.value = null;
|
|
323
|
+
bufferImages.value = [];
|
|
324
|
+
frontBuffer.value = -1;
|
|
303
325
|
surfaceReservation.value = 0;
|
|
304
326
|
surfaceWidth.value = 0;
|
|
305
327
|
surfaceHeight.value = 0;
|
|
@@ -321,6 +343,10 @@ export const useVideoComposition = <T = undefined>({
|
|
|
321
343
|
const drawDeferred = useSharedValue(false);
|
|
322
344
|
// Whether the previous vsync brought new frames.
|
|
323
345
|
const importedLastVsync = useSharedValue(false);
|
|
346
|
+
// Draws submitted without waiting for the GPU since the last wait, the
|
|
347
|
+
// checked wait that ends every import of a new frame included.
|
|
348
|
+
const uncheckedDraws = useSharedValue(0);
|
|
349
|
+
const importWaits = useSharedValue(0);
|
|
324
350
|
// A new drawFrame can draw something else from the same time and frames.
|
|
325
351
|
useEffect(() => {
|
|
326
352
|
lastDrawnTime.value = -1;
|
|
@@ -386,6 +412,13 @@ export const useVideoComposition = <T = undefined>({
|
|
|
386
412
|
sourcesGeneration.value = generation;
|
|
387
413
|
const currentTime = framesExtractor.currentTime;
|
|
388
414
|
const framesVersion = framesExtractor.framesVersion;
|
|
415
|
+
// An import (above, or of another player on this runtime) ends with a
|
|
416
|
+
// checked wait for all the GPU work submitted before it.
|
|
417
|
+
const waits = videoFrameImportWaits();
|
|
418
|
+
if (waits !== importWaits.value) {
|
|
419
|
+
importWaits.value = waits;
|
|
420
|
+
uncheckedDraws.value = 0;
|
|
421
|
+
}
|
|
389
422
|
const imported =
|
|
390
423
|
splitImportAndDraw && framesVersion !== lastImportedVersion.value;
|
|
391
424
|
const importedBefore = importedLastVsync.value;
|
|
@@ -460,8 +493,9 @@ export const useVideoComposition = <T = undefined>({
|
|
|
460
493
|
// picture by the ratio between them, so the surface is re-keyed on its
|
|
461
494
|
// dimensions the way exportVideoComposition already re-keys its own.
|
|
462
495
|
const retired = {
|
|
463
|
-
images: [
|
|
496
|
+
images: [...bufferImages.value, pendingSnapshot.value],
|
|
464
497
|
surface,
|
|
498
|
+
spareSurface: spareSurfaceSharedValue.value,
|
|
465
499
|
canvas: canvasSharedValue.value,
|
|
466
500
|
reservation: surfaceReservation.value,
|
|
467
501
|
afterDraw: pendingAfterDraw.value?.run,
|
|
@@ -471,10 +505,14 @@ export const useVideoComposition = <T = undefined>({
|
|
|
471
505
|
pendingAfterDraw.value = null;
|
|
472
506
|
canvasSharedValue.value = null;
|
|
473
507
|
surfaceSharedValue.value = null;
|
|
508
|
+
spareSurfaceSharedValue.value = null;
|
|
509
|
+
spareCanvasSharedValue.value = null;
|
|
510
|
+
bufferImages.value = [];
|
|
511
|
+
frontBuffer.value = -1;
|
|
474
512
|
surfaceReservation.value = 0;
|
|
475
513
|
const retiredFailure = disposePreviewResources(retired);
|
|
476
514
|
if (retiredFailure.failed) throw retiredFailure.error;
|
|
477
|
-
//
|
|
515
|
+
// The two surfaces the preview draws into in turn.
|
|
478
516
|
const reservation = reserveVideoMemory(
|
|
479
517
|
frameByteSize(pixelWidth, pixelHeight) * 2,
|
|
480
518
|
"composition preview",
|
|
@@ -485,6 +523,13 @@ export const useVideoComposition = <T = undefined>({
|
|
|
485
523
|
surfaceSharedValue.value = surface;
|
|
486
524
|
if (!surface) throw new Error("Cannot allocate preview surface");
|
|
487
525
|
canvasSharedValue.value = getVideoCanvas(surface);
|
|
526
|
+
const spare = Skia.Surface.MakeOffscreen(pixelWidth, pixelHeight);
|
|
527
|
+
spareSurfaceSharedValue.value = spare;
|
|
528
|
+
if (!spare) throw new Error("Cannot allocate preview surface");
|
|
529
|
+
spareCanvasSharedValue.value = getVideoCanvas(spare);
|
|
530
|
+
const first = surface.asImage();
|
|
531
|
+
bufferImages.value = [first];
|
|
532
|
+
bufferImages.value = [first, spare.asImage()];
|
|
488
533
|
surfaceWidth.value = pixelWidth;
|
|
489
534
|
surfaceHeight.value = pixelHeight;
|
|
490
535
|
} catch (error) {
|
|
@@ -497,8 +542,13 @@ export const useVideoComposition = <T = undefined>({
|
|
|
497
542
|
console.warn("Failed to create surface");
|
|
498
543
|
return;
|
|
499
544
|
}
|
|
545
|
+
// Draw into the buffer that is not on screen.
|
|
546
|
+
const back = frontBuffer.value === 0 ? 1 : 0;
|
|
547
|
+
if (back === 1) surface = spareSurfaceSharedValue.value!;
|
|
548
|
+
const backSurface = surface;
|
|
500
549
|
|
|
501
|
-
const canvas =
|
|
550
|
+
const canvas =
|
|
551
|
+
back === 1 ? spareCanvasSharedValue.value! : canvasSharedValue.value!;
|
|
502
552
|
// Cleared as the export clears it: a drawFrame that leaves part of the
|
|
503
553
|
// canvas alone showed the previous frames there, and not in the export.
|
|
504
554
|
canvas.drawColor(Skia.Color("#00000000"), BlendMode.Clear);
|
|
@@ -523,21 +573,24 @@ export const useVideoComposition = <T = undefined>({
|
|
|
523
573
|
width: pixelWidth,
|
|
524
574
|
height: pixelHeight,
|
|
525
575
|
});
|
|
526
|
-
const
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
576
|
+
const next = bufferImages.value[back]!;
|
|
577
|
+
// Waiting for the GPU here held the UI thread for the work of every
|
|
578
|
+
// Skia view on the shared context: a vsync in three with 4K frames on
|
|
579
|
+
// iOS with Skia 3.9.1, every other one before. The draw samples only
|
|
580
|
+
// images Skia owns, which the GPU work keeps alive, so it waits only
|
|
581
|
+
// when afterDrawFrame releases a context of the caller's, and after a
|
|
582
|
+
// few draws without an import, so that the GPU queue stays bounded.
|
|
583
|
+
const wait =
|
|
584
|
+
afterDrawFrame !== undefined ||
|
|
585
|
+
uncheckedDraws.value >= maxUncheckedDraws;
|
|
586
|
+
backSurface.flush(wait);
|
|
587
|
+
uncheckedDraws.value = wait ? 0 : uncheckedDraws.value + 1;
|
|
533
588
|
drawn.submitted = true;
|
|
534
589
|
frameGeneration.value = generation;
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
}
|
|
540
|
-
pendingSnapshot.value = null;
|
|
590
|
+
// The image shares the surface's texture: it shows this draw once
|
|
591
|
+
// the flush above has submitted it.
|
|
592
|
+
currentFrame.value = next;
|
|
593
|
+
frontBuffer.value = back;
|
|
541
594
|
} catch (error) {
|
|
542
595
|
drawn.failed = true;
|
|
543
596
|
drawn.error = error;
|
package/src/videoFrameImage.ts
CHANGED
|
@@ -41,7 +41,7 @@ export type VideoFrameFit = "fill" | "cover" | "contain";
|
|
|
41
41
|
*
|
|
42
42
|
* @param frame the frame, drawn in the tick it was handed out in
|
|
43
43
|
* @param output an image returned for a previous frame, recycled rather than
|
|
44
|
-
* allocating a new wrapper. Accepted for compatibility;
|
|
44
|
+
* allocating a new wrapper. Accepted for compatibility; the image wraps a texture the library owns
|
|
45
45
|
* @returns the image and its picture, or null when the frame has no texture
|
|
46
46
|
*/
|
|
47
47
|
export const makeVideoFrameImage = (
|
package/src/yuvBlit.ts
CHANGED
|
@@ -68,40 +68,52 @@ struct VsOut { @builtin(position) position: vec4f, @location(0) uv: vec2f };
|
|
|
68
68
|
// primaries to sRGB, and every export brightens the midtones again.
|
|
69
69
|
const sourceColorSpace = { primaries: "srgb", transfer: "srgb" } as const;
|
|
70
70
|
|
|
71
|
-
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
export const
|
|
71
|
+
/** A decoder buffer's crop rectangle once the buffer is turned clockwise for
|
|
72
|
+
* display: the blit samples that upright region, and draws it at the size of
|
|
73
|
+
* its target, which a capped or explicit frame size makes smaller. Throws
|
|
74
|
+
* unless the rectangle lies in the buffer. */
|
|
75
|
+
export const yuvFrameRegion = (
|
|
76
76
|
region: { x: number; y: number; width: number; height: number },
|
|
77
77
|
rotation: number,
|
|
78
78
|
buffer: { width: number; height: number },
|
|
79
|
-
|
|
80
|
-
): { x: number; y: number } => {
|
|
79
|
+
): { x: number; y: number; width: number; height: number } => {
|
|
81
80
|
"worklet";
|
|
82
81
|
const turned = rotation === 90 || rotation === 270;
|
|
83
82
|
if (
|
|
84
83
|
(rotation !== 0 && !turned && rotation !== 180) ||
|
|
85
84
|
region.x < 0 ||
|
|
86
85
|
region.y < 0 ||
|
|
86
|
+
region.width <= 0 ||
|
|
87
|
+
region.height <= 0 ||
|
|
87
88
|
region.x + region.width > buffer.width ||
|
|
88
|
-
region.y + region.height > buffer.height
|
|
89
|
-
(turned ? region.height : region.width) !== frame.width ||
|
|
90
|
-
(turned ? region.width : region.height) !== frame.height
|
|
89
|
+
region.y + region.height > buffer.height
|
|
91
90
|
) {
|
|
92
91
|
throw new Error("Native video buffer region or YUV layout mismatch");
|
|
93
92
|
}
|
|
94
|
-
const
|
|
93
|
+
const width = turned ? region.height : region.width;
|
|
94
|
+
const height = turned ? region.width : region.height;
|
|
95
95
|
if (rotation === 90)
|
|
96
|
-
return {
|
|
96
|
+
return {
|
|
97
|
+
x: buffer.height - region.y - region.height,
|
|
98
|
+
y: region.x,
|
|
99
|
+
width,
|
|
100
|
+
height,
|
|
101
|
+
};
|
|
97
102
|
if (rotation === 180)
|
|
98
103
|
return {
|
|
99
|
-
x: width - region.x - region.width,
|
|
100
|
-
y: height - region.y - region.height,
|
|
104
|
+
x: buffer.width - region.x - region.width,
|
|
105
|
+
y: buffer.height - region.y - region.height,
|
|
106
|
+
width,
|
|
107
|
+
height,
|
|
101
108
|
};
|
|
102
109
|
if (rotation === 270)
|
|
103
|
-
return {
|
|
104
|
-
|
|
110
|
+
return {
|
|
111
|
+
x: region.y,
|
|
112
|
+
y: buffer.width - region.x - region.width,
|
|
113
|
+
width,
|
|
114
|
+
height,
|
|
115
|
+
};
|
|
116
|
+
return { x: region.x, y: region.y, width, height };
|
|
105
117
|
};
|
|
106
118
|
|
|
107
119
|
const getYuvBlit = (device: GPUDevice): YuvBlit => {
|
|
@@ -251,10 +263,11 @@ const importYuvBuffer = (
|
|
|
251
263
|
};
|
|
252
264
|
|
|
253
265
|
/**
|
|
254
|
-
* Draws the upright `
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
* frame's lease or `source`, as
|
|
266
|
+
* Draws the upright `region` of a decoder buffer (see yuvFrameRegion) into
|
|
267
|
+
* the whole of `target`, converting YUV to RGB and scaling it to the target's
|
|
268
|
+
* size; the buffer is imported on its first frame. Submits the draw: the
|
|
269
|
+
* caller waits for it before releasing the frame's lease or `source`, as
|
|
270
|
+
* after copyExternalImageToTexture.
|
|
258
271
|
*/
|
|
259
272
|
export const blitYuvFrame = (
|
|
260
273
|
device: GPUDevice,
|
|
@@ -262,9 +275,7 @@ export const blitYuvFrame = (
|
|
|
262
275
|
producer: number,
|
|
263
276
|
source: NativeVideoFrame,
|
|
264
277
|
rotation: number,
|
|
265
|
-
|
|
266
|
-
width: number,
|
|
267
|
-
height: number,
|
|
278
|
+
region: { x: number; y: number; width: number; height: number },
|
|
268
279
|
target: GPUTexture,
|
|
269
280
|
): void => {
|
|
270
281
|
"worklet";
|
|
@@ -280,7 +291,7 @@ export const blitYuvFrame = (
|
|
|
280
291
|
);
|
|
281
292
|
if (index < 0) {
|
|
282
293
|
// The least recently drawn buffer goes first. Its last frame completed:
|
|
283
|
-
//
|
|
294
|
+
// fewer imports than this are ever on the GPU (see frameInterop.ts).
|
|
284
295
|
if (entries.length >= maxImportsPerProducer) {
|
|
285
296
|
releaseYuvImport(entries[0]!);
|
|
286
297
|
entries.shift();
|
|
@@ -297,10 +308,10 @@ export const blitYuvFrame = (
|
|
|
297
308
|
const frameWidth = turned ? source.height : source.width;
|
|
298
309
|
const frameHeight = turned ? source.width : source.height;
|
|
299
310
|
const mapping = [
|
|
300
|
-
width / frameWidth,
|
|
301
|
-
height / frameHeight,
|
|
302
|
-
|
|
303
|
-
|
|
311
|
+
region.width / frameWidth,
|
|
312
|
+
region.height / frameHeight,
|
|
313
|
+
region.x / frameWidth,
|
|
314
|
+
region.y / frameHeight,
|
|
304
315
|
];
|
|
305
316
|
if (mapping.some((value, i) => value !== entry.mapping[i])) {
|
|
306
317
|
device.queue.writeBuffer(entry.uniforms, 0, new Float32Array(mapping));
|
|
@@ -326,8 +337,8 @@ export const blitYuvFrame = (
|
|
|
326
337
|
pass.end();
|
|
327
338
|
device.queue.submit([encoder.finish()]);
|
|
328
339
|
} finally {
|
|
329
|
-
// The caller
|
|
330
|
-
//
|
|
340
|
+
// The caller hands the decoder buffer back once the GPU is done with the
|
|
341
|
+
// draw; the release fence is not needed for that.
|
|
331
342
|
entry.wrapper.endAccess()?.fence.destroy();
|
|
332
343
|
}
|
|
333
344
|
};
|
|
@@ -355,7 +366,11 @@ export const blitNv12Frame = (
|
|
|
355
366
|
size: 16,
|
|
356
367
|
usage: videoUniformBufferUsage,
|
|
357
368
|
});
|
|
358
|
-
device.queue.writeBuffer(
|
|
369
|
+
device.queue.writeBuffer(
|
|
370
|
+
blit.wholeFrame,
|
|
371
|
+
0,
|
|
372
|
+
new Float32Array([1, 1, 0, 0]),
|
|
373
|
+
);
|
|
359
374
|
}
|
|
360
375
|
const external = device.importExternalTexture({
|
|
361
376
|
label: "video decoder buffer",
|