react-native-skia-webgpu-video 0.1.0-beta.2 → 0.1.0-beta.3

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 (42) hide show
  1. package/LICENSE +0 -1
  2. package/README.md +28 -7
  3. package/android/cpp/VideoComposition.cpp +12 -0
  4. package/android/cpp/VideoCompositionFramesExtractor.cpp +6 -0
  5. package/android/cpp/VideoCompositionFramesExtractor.h +1 -0
  6. package/android/cpp/VideoCompositionFramesExtractorHostObject.cpp +1 -1
  7. package/android/src/main/java/com/azzapp/rnskv/PreviewFrameRateVote.java +104 -0
  8. package/android/src/main/java/com/azzapp/rnskv/VideoComposition.java +4 -0
  9. package/android/src/main/java/com/azzapp/rnskv/VideoCompositionDecoder.java +4 -2
  10. package/android/src/main/java/com/azzapp/rnskv/VideoCompositionFramesExtractor.java +32 -0
  11. package/docs/FRAME_PIXEL_FORMAT.md +124 -0
  12. package/docs/VERIFICATION_0.1.0-beta.3.md +147 -0
  13. package/ios/NativeResourceStats.h +9 -0
  14. package/ios/RNSVColorSpace.h +35 -4
  15. package/ios/ReactNativeSkiaVideo.mm +4 -0
  16. package/ios/VideoComposition.h +20 -0
  17. package/ios/VideoCompositionFramesExtractorHostObject.h +5 -0
  18. package/ios/VideoCompositionFramesExtractorHostObject.mm +31 -2
  19. package/ios/VideoCompositionItemDecoder.mm +21 -8
  20. package/ios/VideoFrame.mm +6 -0
  21. package/lib/module/exportFrames.js +25 -18
  22. package/lib/module/exportFrames.js.map +1 -1
  23. package/lib/module/frameInterop.js +7 -1
  24. package/lib/module/frameInterop.js.map +1 -1
  25. package/lib/module/videoCompositionPlayer.js +13 -5
  26. package/lib/module/videoCompositionPlayer.js.map +1 -1
  27. package/lib/module/yuvBlit.js +70 -0
  28. package/lib/module/yuvBlit.js.map +1 -1
  29. package/lib/typescript/src/exportFrames.d.ts +8 -8
  30. package/lib/typescript/src/exportFrames.d.ts.map +1 -1
  31. package/lib/typescript/src/frameInterop.d.ts.map +1 -1
  32. package/lib/typescript/src/types.d.ts +26 -0
  33. package/lib/typescript/src/types.d.ts.map +1 -1
  34. package/lib/typescript/src/videoCompositionPlayer.d.ts.map +1 -1
  35. package/lib/typescript/src/yuvBlit.d.ts +10 -0
  36. package/lib/typescript/src/yuvBlit.d.ts.map +1 -1
  37. package/package.json +1 -1
  38. package/src/exportFrames.ts +52 -34
  39. package/src/frameInterop.ts +18 -1
  40. package/src/types.ts +27 -0
  41. package/src/videoCompositionPlayer.ts +14 -4
  42. package/src/yuvBlit.ts +66 -0
package/src/types.ts CHANGED
@@ -149,6 +149,26 @@ export type VideoPlayer = {
149
149
  on(name: "error", listener: (error: any) => void): () => void;
150
150
  };
151
151
 
152
+ /**
153
+ * The pixel format decoded frames are handed over in, before they are drawn.
154
+ *
155
+ * - `yuv`: the decoder's own YUV 4:2:0 buffers, converted to RGB on the GPU
156
+ * when a frame is drawn. A frame takes 1.5 bytes a pixel rather than 4
157
+ * (12.4 MB rather than 33.2 MB in 4K), so the decoder holds more frames
158
+ * ahead for the same memory. On Android only 8-bit SDR tracks decoded at
159
+ * their own size; other tracks stay `rgba`.
160
+ * - `rgba`: frames converted to RGB as they are decoded (iOS: by VideoToolbox,
161
+ * Android: by an OpenGL pass), which a GPU export samples in place.
162
+ * - `auto`: `yuv`, on both platforms.
163
+ *
164
+ * Camera footage (Rec.709, or HLG tone mapped to Rec.709) renders the same
165
+ * either way: both convert with the frame's YCbCr matrix and range. On iOS,
166
+ * `rgba` also converts a file tagged with other primaries or another SDR
167
+ * transfer function (Display P3, sRGB) to Rec.709; `yuv` takes its samples as
168
+ * Rec.709, as Android does in both formats.
169
+ */
170
+ export type VideoFramePixelFormat = "auto" | "yuv" | "rgba";
171
+
152
172
  /**
153
173
  * Represents a video composition.
154
174
  */
@@ -171,6 +191,13 @@ export type VideoComposition = {
171
191
  * until its decoder has opened. The export always waits for it.
172
192
  */
173
193
  lazyDecoders?: boolean;
194
+ /**
195
+ * The pixel format the video items' frames are decoded into, for the
196
+ * preview and the export, see {@link VideoFramePixelFormat}.
197
+ *
198
+ * @default 'auto'
199
+ */
200
+ framePixelFormat?: VideoFramePixelFormat;
174
201
  };
175
202
 
176
203
  type VideoCompositionItemBase = {
@@ -319,6 +319,8 @@ export const useVideoComposition = <T = undefined>({
319
319
  const lastImportedVersion = useSharedValue(-1);
320
320
  // Whether the previous vsync imported and left its draw to this one.
321
321
  const drawDeferred = useSharedValue(false);
322
+ // Whether the previous vsync brought new frames.
323
+ const importedLastVsync = useSharedValue(false);
322
324
  // A new drawFrame can draw something else from the same time and frames.
323
325
  useEffect(() => {
324
326
  lastDrawnTime.value = -1;
@@ -384,14 +386,22 @@ export const useVideoComposition = <T = undefined>({
384
386
  sourcesGeneration.value = generation;
385
387
  const currentTime = framesExtractor.currentTime;
386
388
  const framesVersion = framesExtractor.framesVersion;
387
- if (splitImportAndDraw && framesVersion !== lastImportedVersion.value) {
389
+ const imported =
390
+ splitImportAndDraw && framesVersion !== lastImportedVersion.value;
391
+ const importedBefore = importedLastVsync.value;
392
+ // Version 0: no frame decoded yet.
393
+ importedLastVsync.value = imported && framesVersion > 0;
394
+ if (imported) {
388
395
  // ownVideoFrames above has just imported the new frames: leave the
389
- // draw to the next vsync, which finds them in the import cache. Never
390
- // twice running: a frame that lands on the vsync owed a draw would
391
- // push it again, and the frame before it would never be shown.
396
+ // draw to the next vsync, which finds them in the import cache. Not
397
+ // when the previous vsync brought new frames too: frames arriving at
398
+ // every vsync (60 fps, or a display gone down to 30 Hz) leave no vsync
399
+ // free for the draw, and the next frame would replace this one before
400
+ // it is shown. Never twice running either, for the same reason.
392
401
  lastImportedVersion.value = framesVersion;
393
402
  if (
394
403
  !drawDeferred.value &&
404
+ !importedBefore &&
395
405
  currentFrame.value !== null &&
396
406
  frameGeneration.value === generation
397
407
  ) {
package/src/yuvBlit.ts CHANGED
@@ -30,6 +30,8 @@ type YuvBlit = {
30
30
  sampler: GPUSampler;
31
31
  module: GPUShaderModule;
32
32
  pipelines: Map<GPUTextureFormat, GPURenderPipeline>;
33
+ /** The whole frame, as is: the uniforms of blitNv12Frame, written once. */
34
+ wholeFrame?: GPUBuffer;
33
35
  };
34
36
 
35
37
  // An ImageReader holds four buffers (GLFrameExtractor.READER_IMAGES); the
@@ -329,3 +331,67 @@ export const blitYuvFrame = (
329
331
  entry.wrapper.endAccess()?.fence.destroy();
330
332
  }
331
333
  };
334
+
335
+ /**
336
+ * iOS: draws a whole NV12 decoder buffer into `target`, converting YUV to RGB
337
+ * with the buffer's YCbCr matrix and range, in its decoded orientation (the
338
+ * frame's rotation applies when it is drawn, as for BGRA frames). Submits the
339
+ * draw. AVAssetReader hands out buffers from a pool rather than a fixed few,
340
+ * so each frame is imported for this draw alone, unlike Android's: the import
341
+ * ends right after the submit (Dawn keeps the surface for the in-flight
342
+ * draw), while the caller holds the frame's lease until the draw completed.
343
+ */
344
+ export const blitNv12Frame = (
345
+ device: GPUDevice,
346
+ source: NativeVideoFrame,
347
+ target: GPUTexture,
348
+ ): void => {
349
+ "worklet";
350
+ const blit = getYuvBlit(device);
351
+ const pipeline = getPipeline(device, blit, target.format);
352
+ if (!blit.wholeFrame) {
353
+ blit.wholeFrame = device.createBuffer({
354
+ label: "video NV12 blit uniforms",
355
+ size: 16,
356
+ usage: videoUniformBufferUsage,
357
+ });
358
+ device.queue.writeBuffer(blit.wholeFrame, 0, new Float32Array([1, 1, 0, 0]));
359
+ }
360
+ const external = device.importExternalTexture({
361
+ label: "video decoder buffer",
362
+ source,
363
+ sourceColorSpace,
364
+ });
365
+ try {
366
+ const encoder = device.createCommandEncoder({ label: "video NV12 blit" });
367
+ const pass = encoder.beginRenderPass({
368
+ label: "video NV12 blit",
369
+ colorAttachments: [
370
+ {
371
+ view: target.createView(),
372
+ loadOp: "clear",
373
+ clearValue: { r: 0, g: 0, b: 0, a: 1 },
374
+ storeOp: "store",
375
+ },
376
+ ],
377
+ });
378
+ pass.setPipeline(pipeline);
379
+ pass.setBindGroup(
380
+ 0,
381
+ device.createBindGroup({
382
+ label: "video NV12 blit",
383
+ layout: blit.layout,
384
+ entries: [
385
+ { binding: 0, resource: external },
386
+ { binding: 1, resource: blit.sampler },
387
+ { binding: 2, resource: { buffer: blit.wholeFrame } },
388
+ ],
389
+ }),
390
+ );
391
+ pass.draw(3);
392
+ pass.end();
393
+ device.queue.submit([encoder.finish()]);
394
+ } finally {
395
+ (external as GPUExternalTexture & { destroy?: () => void }).destroy?.();
396
+ }
397
+ };