React Native WebGPU

Video Player

Decode a video into native frames the GPU samples without any copy.

createVideoPlayer() decodes a video into native frames: IOSurface-backed CVPixelBuffers on Apple platforms, AHardwareBuffers on Android. The frames are sampled by WebGPU without any copy through the CPU, with importExternalTexture or importSharedTextureMemory, copied into a regular texture with copyExternalImageToTexture, and drawn by React Native Skia.

const  = ();
.();

const  = () => {
  // The most recently decoded frame, or null when there is no new frame
  const  = .();
  if () {
    const  = .({
      : ,
      : .,
    });
    // ...sample the texture in a render pass and submit
    .();
    .();
  }
  ();
};
();

createVideoPlayer

createVideoPlayer(source: string, options?: CreateVideoPlayerOptions): VideoPlayer

source is a URL (http, https, file) or a file path. The player starts paused: call play() to start decoding.

Prop

Type

VideoPlayer

Playback is controlled like an HTMLMediaElement; the difference is that frames are pulled with copyLatestFrame() instead of being displayed.

MemberDescription
copyLatestFrame()The most recently decoded frame as a NativeVideoFrame, or null when no new frame was decoded since the last call. Poll it on every render tick.
play(), pause(), pausedStart and stop playback.
currentTimeThe playback position in seconds. Assign it to seek, also while paused: the next copyLatestFrame() then returns the frame at the new position.
durationThe length in seconds, 0 until the metadata has loaded.
volume0 to 1.
loopRestart from the beginning at the end of the stream. Unlike on the web, it defaults to true.
videoWidth, videoHeightThe coded size of the frames in pixels, 0 until the metadata has loaded.
rotationThe clockwise rotation (0, 90, 180 or 270) to apply when displaying the frames: a video recorded in portrait stores landscape frames with a rotation. Pass it as the rotation of importExternalTexture().
frameRateThe nominal frame rate of the video track, 0 if unknown.
release()Stops playback and releases the decoder. Frames already copied stay valid until they are released.

The metadata of a remote video loads asynchronously, so duration, videoWidth, videoHeight, rotation and frameRate read 0 until it has. Frames carry their own width and height, which is what a render loop should use.

NativeVideoFrame

A frame holds a native buffer and keeps it alive until release() is called (or until the object is garbage collected). A player can only keep a few frames in flight: release each frame once the GPU work that samples it has been submitted, or copyLatestFrame() stops returning new frames.

Prop

Type

handle is the raw pointer of the buffer (an IOSurfaceRef or an AHardwareBuffer*) as a BigInt, which importSharedTextureMemory({ handle }) takes.

Copying a frame into a texture

queue.copyExternalImageToTexture({ source: frame, rotation: player.rotation }, { texture }, size) renders a frame into a regular texture, on both platforms and whatever the frame's layout, with the rotation applied. See the native frame copy.

Pixel formats

PlatformFrames
iOS, macOSBGRA ("bgra8", the default), or biplanar YUV with { pixelFormat: "nv12" }. Both import as external textures; BGRA frames also import as shared texture memory and as Skia images.
AndroidThe native YUV layout of the decoder, reported as "nv12". Sample them with importExternalTexture() or copy them with copyExternalImageToTexture(); the pixelFormat option is ignored.

Other sources

createVideoFrameFromNativeBuffer(pointer) wraps a buffer produced by another library (a CVPixelBufferRef on Apple platforms, an AHardwareBuffer* on Android, for instance VisionCamera's frame.getNativeBuffer().pointer) into a NativeVideoFrame that keeps it alive until released.

Examples