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
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
type WorkletRuntime,
|
|
12
12
|
} from "react-native-worklets";
|
|
13
13
|
import { AlphaType, BlendMode, ColorType, Skia } from "react-native-skia";
|
|
14
|
-
import type {
|
|
14
|
+
import type { SkCanvas, SkSurface } from "react-native-skia";
|
|
15
15
|
import { Platform } from "react-native";
|
|
16
16
|
import type {
|
|
17
17
|
ExportOptions,
|
|
@@ -35,10 +35,16 @@ import {
|
|
|
35
35
|
import { enqueueExport, exportAbortError } from "./exportQueue";
|
|
36
36
|
import {
|
|
37
37
|
acquireEncoderTarget,
|
|
38
|
+
acquireWindowTarget,
|
|
38
39
|
closeEncoderTarget,
|
|
40
|
+
closeEncoderWindow,
|
|
41
|
+
presentWindowTarget,
|
|
39
42
|
probeEncoderTarget,
|
|
43
|
+
probeEncoderWindow,
|
|
44
|
+
releaseEncoderWindow,
|
|
40
45
|
supportsEncoderTargets,
|
|
41
46
|
type EncoderTarget,
|
|
47
|
+
type EncoderWindow,
|
|
42
48
|
} from "./exportTarget";
|
|
43
49
|
import { yieldToRuntime } from "./runtimeYield";
|
|
44
50
|
import {
|
|
@@ -82,7 +88,8 @@ export type ExportProfile = {
|
|
|
82
88
|
targetMs: number;
|
|
83
89
|
/** drawFrame, before/afterDrawFrame and progress scheduling. */
|
|
84
90
|
drawMs: number;
|
|
85
|
-
/** The checked flush(true): waiting for the GPU to complete the frame
|
|
91
|
+
/** The checked flush(true): waiting for the GPU to complete the frame,
|
|
92
|
+
* which Android's window export does once it decoded the next frame. */
|
|
86
93
|
gpuMs: number;
|
|
87
94
|
/** Readback when used, submission to the encoder including its
|
|
88
95
|
* backpressure, and the final file write. */
|
|
@@ -114,33 +121,24 @@ export type VideoExportArguments<T = undefined> = ExportOptions & {
|
|
|
114
121
|
onProfile?: (profile: ExportProfile) => void;
|
|
115
122
|
};
|
|
116
123
|
|
|
117
|
-
// Native Skia 3.4.4 accepts destination/stride in readPixels; its
|
|
118
|
-
// published Canvas type currently omits these arguments. Use only a private,
|
|
119
|
-
// full-length, zero-offset Uint8Array (the native implementation ignores offsets).
|
|
120
|
-
type ReadbackCanvas = {
|
|
121
|
-
readPixels: (
|
|
122
|
-
x: number,
|
|
123
|
-
y: number,
|
|
124
|
-
info: ImageInfo,
|
|
125
|
-
destination: Uint8Array,
|
|
126
|
-
bytesPerRow: number,
|
|
127
|
-
) => Uint8Array | Float32Array | null;
|
|
128
|
-
};
|
|
129
|
-
|
|
130
124
|
type ExportState = {
|
|
131
125
|
encoder: VideoEncoder | null;
|
|
132
126
|
extractor: VideoCompositionFramesExtractorSync | null;
|
|
133
127
|
surface: SkSurface | null;
|
|
134
128
|
canvas: SkCanvas | null;
|
|
135
|
-
readback: Uint8Array | null;
|
|
136
129
|
/** Draw into a lent encoder buffer instead of reading pixels back. */
|
|
137
130
|
gpu: boolean;
|
|
138
131
|
target: EncoderTarget | null;
|
|
132
|
+
/** Android GPU export: the encoder's input window, drawn into directly. */
|
|
133
|
+
window: EncoderWindow | null;
|
|
139
134
|
/** iOS GPU export: decoded frames sampled in place, by producer. */
|
|
140
135
|
imports: ExportFrameImports | null;
|
|
141
136
|
frames: Record<string, VideoFrame>;
|
|
142
137
|
processor: VideoFrameProcessor | null;
|
|
143
138
|
pendingAfterDraw: (() => void) | null;
|
|
139
|
+
/** Android window: the last frame was submitted without its checked GPU
|
|
140
|
+
* wait, which comes before anything it sampled is released. */
|
|
141
|
+
unchecked?: boolean;
|
|
144
142
|
reservations: number[];
|
|
145
143
|
index: number;
|
|
146
144
|
setupError?: unknown;
|
|
@@ -290,6 +288,16 @@ const executeExport = <T>(args: VideoExportArguments<T>): Promise<void> =>
|
|
|
290
288
|
if (clock.last >= 0) profile[stage] += now - clock.last;
|
|
291
289
|
clock.last = now;
|
|
292
290
|
};
|
|
291
|
+
/** Android window: the checked GPU wait the last frame left for later,
|
|
292
|
+
* so that the GPU converted and drew it while the CPU presented it and
|
|
293
|
+
* decoded the next one. An empty recording: Graphite still waits for
|
|
294
|
+
* every command submitted so far. */
|
|
295
|
+
const checkWindowFrame = () => {
|
|
296
|
+
"worklet";
|
|
297
|
+
if (!state.unchecked) return;
|
|
298
|
+
state.window!.slots[state.window!.next]!.surface!.flush(true);
|
|
299
|
+
state.unchecked = false;
|
|
300
|
+
};
|
|
293
301
|
const finish = async (
|
|
294
302
|
error: unknown,
|
|
295
303
|
aborted = false,
|
|
@@ -313,6 +321,7 @@ const executeExport = <T>(args: VideoExportArguments<T>): Promise<void> =>
|
|
|
313
321
|
};
|
|
314
322
|
// Submit any recording left by a drawing exception before an
|
|
315
323
|
// adapter is allowed to destroy textures referenced by that canvas.
|
|
324
|
+
clean(checkWindowFrame);
|
|
316
325
|
clean(() => state.surface?.flush(true));
|
|
317
326
|
clean(() => state.target?.surface?.flush(true));
|
|
318
327
|
if (cleanup.safeToRelease && state.pendingAfterDraw) {
|
|
@@ -329,6 +338,10 @@ const executeExport = <T>(args: VideoExportArguments<T>): Promise<void> =>
|
|
|
329
338
|
}
|
|
330
339
|
}
|
|
331
340
|
if (clean(() => state.extractor?.dispose())) state.extractor = null;
|
|
341
|
+
// Android: the window's swapchain lets go of the codec's buffers
|
|
342
|
+
// before the encoder goes. Its own textures are freed further down.
|
|
343
|
+
const window = state.window;
|
|
344
|
+
if (window) clean(() => closeEncoderWindow(window));
|
|
332
345
|
if (clean(() => state.encoder?.dispose())) state.encoder = null;
|
|
333
346
|
if (cleanup.safeToRelease) {
|
|
334
347
|
clean(() => state.surface?.flush(true));
|
|
@@ -341,10 +354,20 @@ const executeExport = <T>(args: VideoExportArguments<T>): Promise<void> =>
|
|
|
341
354
|
state.imports = null;
|
|
342
355
|
clean(() => clearVideoFrameImages(true));
|
|
343
356
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
if (cleanup.safeToRelease && clean(() => state.surface?.dispose()))
|
|
357
|
+
// Disposing the surface detaches its canvas.
|
|
358
|
+
if (cleanup.safeToRelease && clean(() => state.surface?.dispose())) {
|
|
347
359
|
state.surface = null;
|
|
360
|
+
state.canvas = null;
|
|
361
|
+
}
|
|
362
|
+
// The target, if any, is one of the window's slots.
|
|
363
|
+
if (
|
|
364
|
+
cleanup.safeToRelease &&
|
|
365
|
+
window &&
|
|
366
|
+
clean(() => releaseEncoderWindow(window))
|
|
367
|
+
) {
|
|
368
|
+
state.window = null;
|
|
369
|
+
state.target = null;
|
|
370
|
+
}
|
|
348
371
|
const target = state.target;
|
|
349
372
|
if (cleanup.safeToRelease && target) {
|
|
350
373
|
if (clean(() => closeEncoderTarget(target))) state.target = null;
|
|
@@ -364,7 +387,6 @@ const executeExport = <T>(args: VideoExportArguments<T>): Promise<void> =>
|
|
|
364
387
|
}
|
|
365
388
|
if (cleanup.safeToRelease) {
|
|
366
389
|
state.frames = {};
|
|
367
|
-
state.readback = null;
|
|
368
390
|
delete storage.__rnskwgpuExport;
|
|
369
391
|
} else {
|
|
370
392
|
// One retained session at most; subsequent exports fail before
|
|
@@ -394,6 +416,8 @@ const executeExport = <T>(args: VideoExportArguments<T>): Promise<void> =>
|
|
|
394
416
|
const owned = runWithVideoAutoreleasePool(() => {
|
|
395
417
|
const decoded = state.extractor!.decodeCompositionFrames(time);
|
|
396
418
|
lap("decodeMs");
|
|
419
|
+
checkWindowFrame();
|
|
420
|
+
lap("gpuMs");
|
|
397
421
|
const imports = state.imports;
|
|
398
422
|
// In place, a producer's frame is released by its next import.
|
|
399
423
|
const imported = imports
|
|
@@ -424,14 +448,20 @@ const executeExport = <T>(args: VideoExportArguments<T>): Promise<void> =>
|
|
|
424
448
|
await finish(null, true);
|
|
425
449
|
return false;
|
|
426
450
|
}
|
|
451
|
+
// CPU readback path only: the frame's pixels, still on their way.
|
|
452
|
+
const readback: {
|
|
453
|
+
pixels: ReturnType<SkCanvas["readPixels"]> | null;
|
|
454
|
+
} = { pixels: null };
|
|
427
455
|
const draw = () => {
|
|
428
456
|
"worklet";
|
|
429
457
|
if (state.gpu)
|
|
430
|
-
state.target =
|
|
431
|
-
state.
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
458
|
+
state.target = state.window
|
|
459
|
+
? acquireWindowTarget(state.window)
|
|
460
|
+
: acquireEncoderTarget(
|
|
461
|
+
state.encoder!,
|
|
462
|
+
options.width,
|
|
463
|
+
options.height,
|
|
464
|
+
);
|
|
435
465
|
lap("targetMs");
|
|
436
466
|
const target = state.target;
|
|
437
467
|
const surface = target ? target.surface! : state.surface!;
|
|
@@ -454,61 +484,36 @@ const executeExport = <T>(args: VideoExportArguments<T>): Promise<void> =>
|
|
|
454
484
|
height: options.height,
|
|
455
485
|
});
|
|
456
486
|
lap("drawMs");
|
|
457
|
-
|
|
487
|
+
// Android window, no context to release: the next frame waits
|
|
488
|
+
// for this one (see checkWindowFrame).
|
|
489
|
+
const unchecked =
|
|
490
|
+
!!state.window &&
|
|
491
|
+
!!target &&
|
|
492
|
+
!afterDrawFrame &&
|
|
493
|
+
!state.processor;
|
|
494
|
+
surface.flush(!unchecked);
|
|
495
|
+
state.unchecked = unchecked;
|
|
458
496
|
drawn.drained = true;
|
|
459
497
|
lap("gpuMs");
|
|
460
|
-
if (target) {
|
|
461
|
-
|
|
498
|
+
if (target && state.window) {
|
|
499
|
+
state.target = null;
|
|
500
|
+
presentWindowTarget(state.encoder!, state.window, target, time);
|
|
501
|
+
} else if (target) {
|
|
502
|
+
closeEncoderTarget(target);
|
|
462
503
|
state.target = null;
|
|
463
504
|
state.encoder!.encodeFrame(
|
|
464
|
-
{
|
|
465
|
-
kind: "native-buffer",
|
|
466
|
-
nativeBuffer: target.pointer,
|
|
467
|
-
fences: target.fences,
|
|
468
|
-
},
|
|
505
|
+
{ kind: "native-buffer", nativeBuffer: target.pointer },
|
|
469
506
|
time,
|
|
470
507
|
);
|
|
471
508
|
} else {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
alphaType: AlphaType.Premul,
|
|
481
|
-
},
|
|
482
|
-
state.readback!,
|
|
483
|
-
options.width * 4,
|
|
484
|
-
);
|
|
485
|
-
if (
|
|
486
|
-
!(pixels instanceof Uint8Array) ||
|
|
487
|
-
pixels.byteLength !== bytes ||
|
|
488
|
-
pixels.byteOffset !== 0 ||
|
|
489
|
-
pixels.buffer !== state.readback!.buffer
|
|
490
|
-
) {
|
|
491
|
-
throw new Error("Video export pixel readback failed");
|
|
492
|
-
}
|
|
493
|
-
// Fallback: consumed synchronously by the bounded native pool.
|
|
494
|
-
const storage =
|
|
495
|
-
pixels.byteOffset === 0 &&
|
|
496
|
-
pixels.buffer.byteLength === pixels.byteLength
|
|
497
|
-
? pixels.buffer
|
|
498
|
-
: pixels.buffer.slice(
|
|
499
|
-
pixels.byteOffset,
|
|
500
|
-
pixels.byteOffset + pixels.byteLength,
|
|
501
|
-
);
|
|
502
|
-
state.encoder!.encodeFrame(
|
|
503
|
-
{
|
|
504
|
-
kind: bgra ? "bgra" : "rgba",
|
|
505
|
-
data: storage,
|
|
506
|
-
width: options.width,
|
|
507
|
-
height: options.height,
|
|
508
|
-
bytesPerRow: options.width * 4,
|
|
509
|
-
},
|
|
510
|
-
time,
|
|
511
|
-
);
|
|
509
|
+
// Skia 3.9 reads back asynchronously, into a new array: the
|
|
510
|
+
// frame is encoded once it lands, after this scope.
|
|
511
|
+
readback.pixels = canvas.readPixels(0, 0, {
|
|
512
|
+
width: options.width,
|
|
513
|
+
height: options.height,
|
|
514
|
+
colorType: bgra ? ColorType.BGRA_8888 : ColorType.RGBA_8888,
|
|
515
|
+
alphaType: AlphaType.Premul,
|
|
516
|
+
});
|
|
512
517
|
}
|
|
513
518
|
lap("encodeMs");
|
|
514
519
|
} catch (error) {
|
|
@@ -545,6 +550,35 @@ const executeExport = <T>(args: VideoExportArguments<T>): Promise<void> =>
|
|
|
545
550
|
if (drawn.failed) throw drawn.failure;
|
|
546
551
|
};
|
|
547
552
|
runWithVideoAutoreleasePool(draw);
|
|
553
|
+
if (readback.pixels) {
|
|
554
|
+
const pixels = await readback.pixels;
|
|
555
|
+
lap("gpuMs");
|
|
556
|
+
// isView, not instanceof: true of a typed array of any realm.
|
|
557
|
+
if (!ArrayBuffer.isView(pixels) || pixels.byteLength !== bytes)
|
|
558
|
+
throw new Error("Video export pixel readback failed");
|
|
559
|
+
// Fallback: consumed synchronously by the bounded native pool.
|
|
560
|
+
const storage =
|
|
561
|
+
pixels.byteOffset === 0 &&
|
|
562
|
+
pixels.buffer.byteLength === pixels.byteLength
|
|
563
|
+
? pixels.buffer
|
|
564
|
+
: pixels.buffer.slice(
|
|
565
|
+
pixels.byteOffset,
|
|
566
|
+
pixels.byteOffset + pixels.byteLength,
|
|
567
|
+
);
|
|
568
|
+
runWithVideoAutoreleasePool(() =>
|
|
569
|
+
state.encoder!.encodeFrame(
|
|
570
|
+
{
|
|
571
|
+
kind: bgra ? "bgra" : "rgba",
|
|
572
|
+
data: storage,
|
|
573
|
+
width: options.width,
|
|
574
|
+
height: options.height,
|
|
575
|
+
bytesPerRow: options.width * 4,
|
|
576
|
+
},
|
|
577
|
+
time,
|
|
578
|
+
),
|
|
579
|
+
);
|
|
580
|
+
lap("encodeMs");
|
|
581
|
+
}
|
|
548
582
|
state.index++;
|
|
549
583
|
if (onProgress)
|
|
550
584
|
scheduleOnRN(onProgress, {
|
|
@@ -612,9 +646,9 @@ const executeExport = <T>(args: VideoExportArguments<T>): Promise<void> =>
|
|
|
612
646
|
extractor: null,
|
|
613
647
|
surface: null,
|
|
614
648
|
canvas: null,
|
|
615
|
-
readback: null,
|
|
616
649
|
gpu: false,
|
|
617
650
|
target: null,
|
|
651
|
+
window: null,
|
|
618
652
|
imports: null,
|
|
619
653
|
frames: {},
|
|
620
654
|
processor: null,
|
|
@@ -641,10 +675,9 @@ const executeExport = <T>(args: VideoExportArguments<T>): Promise<void> =>
|
|
|
641
675
|
state.reservations.push(
|
|
642
676
|
reserveVideoMemory(
|
|
643
677
|
bytes * 2,
|
|
644
|
-
"export
|
|
678
|
+
"export readback and transient raster",
|
|
645
679
|
),
|
|
646
680
|
);
|
|
647
|
-
state.readback = new Uint8Array(bytes);
|
|
648
681
|
state.hasRecorder = true;
|
|
649
682
|
state.surface = Skia.Surface.MakeOffscreen(
|
|
650
683
|
options.width,
|
|
@@ -666,16 +699,28 @@ const executeExport = <T>(args: VideoExportArguments<T>): Promise<void> =>
|
|
|
666
699
|
);
|
|
667
700
|
state.encoder.prepare();
|
|
668
701
|
if (gpuExport) {
|
|
669
|
-
// Skia draws straight into the encoder's buffers
|
|
670
|
-
//
|
|
702
|
+
// Skia draws straight into the encoder's buffers: lent pool
|
|
703
|
+
// buffers on iOS, the input window on Android. The probe keeps
|
|
704
|
+
// devices where that fails on the readback path.
|
|
671
705
|
state.hasRecorder = true;
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
706
|
+
let failure: unknown;
|
|
707
|
+
if (bgra) {
|
|
708
|
+
failure = supportsEncoderTargets(state.encoder)
|
|
709
|
+
? probeEncoderTarget(
|
|
710
|
+
state.encoder,
|
|
711
|
+
options.width,
|
|
712
|
+
options.height,
|
|
713
|
+
)
|
|
714
|
+
: new Error("Encoder cannot lend frame buffers");
|
|
715
|
+
} else {
|
|
716
|
+
const probe = probeEncoderWindow(
|
|
717
|
+
state.encoder,
|
|
718
|
+
options.width,
|
|
719
|
+
options.height,
|
|
720
|
+
);
|
|
721
|
+
state.window = probe.window;
|
|
722
|
+
failure = probe.failure;
|
|
723
|
+
}
|
|
679
724
|
state.gpu = failure === null;
|
|
680
725
|
// Decoded frames are sampled in place too: on iOS through the
|
|
681
726
|
// same IOSurface import, on Android once converted from YUV.
|