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.
Files changed (81) hide show
  1. package/README.md +48 -43
  2. package/android/BUILD_VALIDATION.md +1 -1
  3. package/android/CMakeLists.txt +3 -2
  4. package/android/cpp/ExportHintSession.cpp +114 -0
  5. package/android/cpp/ExportHintSession.h +35 -0
  6. package/android/cpp/VideoEncoderHostObject.cpp +76 -101
  7. package/android/cpp/VideoEncoderHostObject.h +14 -9
  8. package/android/cpp/VideoFrame.cpp +3 -5
  9. package/android/cpp/cpp-adapter.cpp +7 -4
  10. package/android/src/main/java/com/azzapp/rnskv/GLFrameExtractor.java +3 -9
  11. package/android/src/main/java/com/azzapp/rnskv/VideoCompositionDecoder.java +8 -11
  12. package/android/src/main/java/com/azzapp/rnskv/VideoEncoder.java +68 -28
  13. package/docs/FRAME_PIXEL_FORMAT.md +17 -2
  14. package/docs/MIGRATION.md +1 -1
  15. package/docs/UPSTREAM_NOTES.md +42 -2
  16. package/ios/RNSVColorSpace.h +8 -3
  17. package/lib/module/canvas.js +3 -5
  18. package/lib/module/canvas.js.map +1 -1
  19. package/lib/module/exportFrames.js +8 -10
  20. package/lib/module/exportFrames.js.map +1 -1
  21. package/lib/module/exportTarget.js +183 -40
  22. package/lib/module/exportTarget.js.map +1 -1
  23. package/lib/module/exportVideoComposition.js +78 -35
  24. package/lib/module/exportVideoComposition.js.map +1 -1
  25. package/lib/module/frameInterop.js +323 -82
  26. package/lib/module/frameInterop.js.map +1 -1
  27. package/lib/module/gpu.js +1 -3
  28. package/lib/module/gpu.js.map +1 -1
  29. package/lib/module/gpuDevice.js +12 -0
  30. package/lib/module/gpuDevice.js.map +1 -1
  31. package/lib/module/memory.js +7 -0
  32. package/lib/module/memory.js.map +1 -1
  33. package/lib/module/previewLifecycle.js +2 -1
  34. package/lib/module/previewLifecycle.js.map +1 -1
  35. package/lib/module/videoCompositionPlayer.js +65 -14
  36. package/lib/module/videoCompositionPlayer.js.map +1 -1
  37. package/lib/module/videoFrameImage.js +1 -1
  38. package/lib/module/yuvBlit.js +31 -24
  39. package/lib/module/yuvBlit.js.map +1 -1
  40. package/lib/typescript/src/canvas.d.ts.map +1 -1
  41. package/lib/typescript/src/exportFrames.d.ts +3 -2
  42. package/lib/typescript/src/exportFrames.d.ts.map +1 -1
  43. package/lib/typescript/src/exportTarget.d.ts +53 -14
  44. package/lib/typescript/src/exportTarget.d.ts.map +1 -1
  45. package/lib/typescript/src/exportVideoComposition.d.ts +2 -1
  46. package/lib/typescript/src/exportVideoComposition.d.ts.map +1 -1
  47. package/lib/typescript/src/frameInterop.d.ts +16 -5
  48. package/lib/typescript/src/frameInterop.d.ts.map +1 -1
  49. package/lib/typescript/src/gpu.d.ts.map +1 -1
  50. package/lib/typescript/src/gpuDevice.d.ts +5 -1
  51. package/lib/typescript/src/gpuDevice.d.ts.map +1 -1
  52. package/lib/typescript/src/memory.d.ts +2 -0
  53. package/lib/typescript/src/memory.d.ts.map +1 -1
  54. package/lib/typescript/src/previewLifecycle.d.ts +2 -0
  55. package/lib/typescript/src/previewLifecycle.d.ts.map +1 -1
  56. package/lib/typescript/src/types.d.ts +18 -12
  57. package/lib/typescript/src/types.d.ts.map +1 -1
  58. package/lib/typescript/src/videoCompositionPlayer.d.ts +4 -5
  59. package/lib/typescript/src/videoCompositionPlayer.d.ts.map +1 -1
  60. package/lib/typescript/src/videoFrameImage.d.ts +1 -1
  61. package/lib/typescript/src/yuvBlit.d.ts +16 -14
  62. package/lib/typescript/src/yuvBlit.d.ts.map +1 -1
  63. package/package.json +6 -6
  64. package/patches/react-native-skia+3.10.0.patch +150 -0
  65. package/scripts/skia-canvas-patch.json +22 -258
  66. package/src/canvas.ts +3 -7
  67. package/src/exportFrames.ts +10 -11
  68. package/src/exportTarget.ts +217 -56
  69. package/src/exportVideoComposition.ts +130 -85
  70. package/src/frameInterop.ts +389 -96
  71. package/src/gpu.ts +1 -3
  72. package/src/gpuDevice.ts +26 -1
  73. package/src/memory.ts +6 -0
  74. package/src/previewLifecycle.ts +4 -1
  75. package/src/types.ts +18 -12
  76. package/src/videoCompositionPlayer.ts +76 -23
  77. package/src/videoFrameImage.ts +1 -1
  78. package/src/yuvBlit.ts +46 -31
  79. package/android/cpp/EncoderTargets.cpp +0 -226
  80. package/android/cpp/EncoderTargets.h +0 -70
  81. 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 { ImageInfo, SkCanvas, SkSurface } from "react-native-skia";
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
- if (cleanup.safeToRelease && clean(() => state.canvas?.dispose()))
345
- state.canvas = null;
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 = acquireEncoderTarget(
431
- state.encoder!,
432
- options.width,
433
- options.height,
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
- surface.flush(true);
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
- closeEncoderTarget(target, true);
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
- const readbackCanvas = canvas as unknown as ReadbackCanvas;
473
- const pixels = readbackCanvas.readPixels(
474
- 0,
475
- 0,
476
- {
477
- width: options.width,
478
- height: options.height,
479
- colorType: bgra ? ColorType.BGRA_8888 : ColorType.RGBA_8888,
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 reusable readback and transient raster",
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; the probe
670
- // keeps devices without IOSurface interop on the readback path.
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
- const failure = supportsEncoderTargets(state.encoder)
673
- ? probeEncoderTarget(
674
- state.encoder,
675
- options.width,
676
- options.height,
677
- )
678
- : new Error("Encoder cannot lend frame buffers");
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.