react-native-skia-webgpu-video 0.1.0-beta.4 → 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 +21 -23
- package/android/BUILD_VALIDATION.md +1 -1
- package/docs/FRAME_PIXEL_FORMAT.md +16 -1
- package/docs/UPSTREAM_NOTES.md +28 -1
- package/ios/RNSVColorSpace.h +8 -3
- package/lib/module/exportFrames.js +2 -2
- package/lib/module/exportFrames.js.map +1 -1
- package/lib/module/exportVideoComposition.js +25 -23
- package/lib/module/exportVideoComposition.js.map +1 -1
- package/lib/module/frameInterop.js +314 -70
- package/lib/module/frameInterop.js.map +1 -1
- package/lib/module/gpu.js +1 -1
- package/lib/module/gpu.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 -0
- 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 +3 -3
- 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/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 +8 -7
- 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/package.json +6 -6
- package/patches/{react-native-skia+3.7.4.patch → react-native-skia+3.10.0.patch} +7 -3
- package/scripts/skia-canvas-patch.json +6 -6
- package/src/exportFrames.ts +2 -2
- package/src/exportVideoComposition.ts +43 -59
- package/src/frameInterop.ts +380 -85
- package/src/gpu.ts +1 -1
- package/src/memory.ts +6 -0
- package/src/previewLifecycle.ts +4 -0
- package/src/types.ts +8 -7
- package/src/videoCompositionPlayer.ts +76 -23
- package/src/videoFrameImage.ts +1 -1
- package/src/yuvBlit.ts +3 -3
|
@@ -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
|
@@ -291,7 +291,7 @@ export const blitYuvFrame = (
|
|
|
291
291
|
);
|
|
292
292
|
if (index < 0) {
|
|
293
293
|
// The least recently drawn buffer goes first. Its last frame completed:
|
|
294
|
-
//
|
|
294
|
+
// fewer imports than this are ever on the GPU (see frameInterop.ts).
|
|
295
295
|
if (entries.length >= maxImportsPerProducer) {
|
|
296
296
|
releaseYuvImport(entries[0]!);
|
|
297
297
|
entries.shift();
|
|
@@ -337,8 +337,8 @@ export const blitYuvFrame = (
|
|
|
337
337
|
pass.end();
|
|
338
338
|
device.queue.submit([encoder.finish()]);
|
|
339
339
|
} finally {
|
|
340
|
-
// The caller
|
|
341
|
-
//
|
|
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.
|
|
342
342
|
entry.wrapper.endAccess()?.fence.destroy();
|
|
343
343
|
}
|
|
344
344
|
};
|