Skip to content

fix(🤖): copy the video frame on Android again - #4026

Open
dennytosp wants to merge 1 commit into
Shopify:mainfrom
dennytosp:fix/copy-android-video-frame
Open

fix(🤖): copy the video frame on Android again#4026
dennytosp wants to merge 1 commit into
Shopify:mainfrom
dennytosp:fix/copy-android-video-frame

Conversation

@dennytosp

@dennytosp dennytosp commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Fixes #4000.

Restores the copy in copyFrameOnAndroid, which is currently commented out:

currentFrame.value = tex; //.makeNonTextureImage();
tex.dispose();

As written it assigns the image back to itself and then disposes it, so the renderer is handed a disposed image on every frame.

Why the copy is required. The Android frame is a borrowed view of a recycled buffer, end to end:

  • RNSkVideo.java#nextImage() does imageReader.acquireLatestImage()image.getHardwareBuffer()image.close(), which returns the slot to the ImageReader's pool for the next acquireLatestImage().
  • RNSkAndroidVideo::nextImage passes that AHardwareBuffer to OpenGLContext::MakeImageFromBuffer.
  • That builds a GR_GL_TEXTURE_EXTERNAL backend texture over the buffer and wraps it with SkImages::BorrowTextureFrom — the SkImage owns no pixels of its own.

So the image is only valid until the decoder reuses that slot, which is what the surviving comment ("on android we need to copy the texture before it's invalidated") is about, and what makeNonTextureImage() does — it reads the frame back off the recycled GPU buffer. That also explains the reported logcat, HardwareBuffer.close followed by EGLConsumer is not attached to an OpenGL ES context.

Where it went. git log -L 19,29:packages/skia/src/external/reanimated/useVideo.ts puts the change in #3686, a WebGPU-canvas PR that touches nothing else about video — a leftover local edit rather than an intended behaviour change.

Field measurements from the reporter, physical Pixel 9 (Android 16) and Galaxy S23 (Android 13), arm64 release builds:

Variant Result
current main black, mean frame-to-frame diff 0.000
drop only tex.dispose() still doesn't paint
restore makeNonTextureImage() paints and animates, frame diffs 17–40/255, stable over 75 s

The e2e suite has no video coverage on either backend (Video.ts is it.skip), so this is not something CI will exercise.

copyFrameOnAndroid exists because Android invalidates the decoder's texture as
soon as the next frame is produced, so the frame has to be copied off the GPU
before it is handed to the renderer. The copy was commented out in Shopify#3686, which
left the worklet assigning the texture back to itself and then disposing it -
the renderer gets a disposed image and the canvas stays black, with logcat
repeating "EGLConsumer is not attached to an OpenGL ES context".

Restore the call. Reported on a Pixel 9 and a Galaxy S23, where the frame diff
goes from a flat 0 to 17-40/255 with the copy back in place.

Fixes Shopify#4000
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Android useVideo renders a black/frozen canvas: copyFrameOnAndroid never copies the frame, then disposes it

1 participant