Shopify / Shopify/react-native-skia

useVideo crashes with SIGABRT (uncaught JniException) on Qualcomm/Samsung when the decoder delivers its first frame; with the exception caught, no frames ever arrive (black canvas)

Open
#3,936 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
Dominant language
TypeScript
Stars
8.6k
Forks
647
Avg merge
1d 17h
Merged PRs (30d)
35

Description

Description
Summary

On Qualcomm Android devices, useVideo() aborts the app with SIGABRT (terminating due to uncaught exception of type facebook::jni::JniException) roughly 100 ms after playback starts — the moment c2.qti.avc.decoder delivers its first frame into Skia's ImageReader. The identical code works perfectly on iOS.

If the Java exception is caught inside RNSkVideo (patched locally), the app survives but currentFrame never becomes a drawable image — the canvas renders empty/black instead. Same root cause, two symptoms.

Reproduced with a fresh minimal app (stock Skia 2.6.9, no patches, no custom shaders, plain opaque H.264 baseline MP4 generated with ffmpeg testsrc2).

Standalone repro repo: https://github.com/LucaL1fe/skia-usevideo-crash-repro

Environment
  • @shopify/react-native-skia: 2.6.9 (latest)
  • react-native 0.86.0, Expo 57.0.4, new architecture
  • Device: Samsung Galaxy S23 Ultra (SM-S918B), Android 16 (API 36), Snapdragon 8 Gen 2 (SM8550/kalama, Adreno 740, c2.qti.avc.decoder). iOS unaffected.
Steps to reproduce
  1. Fresh Expo app, install @shopify/react-native-skia@2.6.9.
  2. Render the component below with any plain H.264 MP4 (bundled asset or URL).
  3. Build and run on a physical Qualcomm device (S23 Ultra here). Emulators/software decoders do not reproduce it.
  4. App crashes ~100 ms after the video starts. On iOS the same code plays fine.
Minimal repro
import { Canvas, Image as SkiaImage, useVideo } from '@shopify/react-native-skia';
import { Asset } from 'expo-asset';
import { useEffect, useState } from 'react';
import { StyleSheet, Text, View } from 'react-native';

export default function App() {
  const [uri, setUri] = useState<string | null>(null);

  useEffect(() => {
    Asset.fromModule(require('./assets/test-video.mp4')) // 640x360 H.264 baseline yuv420p
      .downloadAsync()
      .then((a) => setUri(a.localUri ?? a.uri))
      .catch((e) => console.error('[repro] asset resolve failed', e));
  }, []);

  const { currentFrame } = useVideo(uri, { looping: true });

  return (
    <View style={styles.root}>
      <Text style={styles.label}>useVideo repro · uri {uri ? 'ready' : 'loading'}</Text>
      <View style={styles.frame}>
        <Canvas style={styles.canvas}>
          <SkiaImage image={currentFrame} x={0} y={0} width={320} height={180} fit="contain" />
        </Canvas>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  root: { flex: 1, backgroundColor: '#2e7d32', alignItems: 'center', justifyContent: 'center' },
  label: { color: 'white', fontSize: 16, marginBottom: 12 },
  frame: { borderWidth: 2, borderColor: 'yellow' },
  canvas: { width: 320, height: 180 },
});
Crash log (release build of the minimal repro)
V MediaPlayerNative: New video size 640 x 360
V MediaPlayerNative: MediaPlayer::notify() prepared
D SurfaceUtils: connecting to surface 0xb4000070c0cee810, reason connectToSurface
I MediaCodec: [c2.qti.avc.decoder] setting surface generation to 28486657
D Codec2Client: setOutputSurface -- failed to set consumer usage (6/BAD_INDEX)
D Codec2Client: setOutputSurface -- generation=28486657 consumer usage=0x900
I MediaCodec: setCodecState state(0), called in 6, domain 1, 1
E libc++abi: terminating due to uncaught exception of type facebook::jni::JniException: Unable to get exception message.
F libc: Fatal signal 6 (SIGABRT), code -1 (SI_QUEUE) in tid 27876 (mqt_v_js), pid 27819 (videoblackrepro)

Tombstone backtrace:

#00 libc.so (abort+160)
#01-#04 libc++_shared.so
#05 libc++_shared.so (__cxa_throw+128)
#06 libfbjni.so (facebook::jni::throwPendingJniExceptionAsCppException()+272)
#07 libfbjni.so (facebook::jni::ThreadScope::WithClassLoader(std::function<void()>&&)+216)
#08 libworklets.so

The crashing tid (mqt_v_js) is the same thread that runs the MediaCodecList/MediaCodec calls — i.e. a Java exception raised during frame delivery in RNSkVideo is left pending, and the next fbjni call converts it into an uncaught C++ exception that aborts the VM. RNSkVideo.java has no try/catch around decodeFrame() / acquireLatestImage() / getHardwareBuffer().

Why no ImageReader format works here (field-tested)

RNSkVideo.java decodes into an ImageReader (ImageFormat.PRIVATE, USAGE_GPU_SAMPLED_IMAGE) and imports each frame's HardwareBuffer into Skia. On QTI decoders each option fails differently — I patched RNSkVideo.java locally and tested all three on the same device:

ImageReader format Result on c2.qti.avc.decoder
PRIVATE (stock) decoder picks proprietary UBWC HAL_PIXEL_FORMAT_YCbCr_420_SP_VENUS_UBWC (0x7fa30c06) → exception on first frame → SIGABRT
RGBA_8888 decoder refuses the producer format (producer 0x7fa30c06 vs reader 0x1) → UnsupportedOperationExceptionSIGABRT
YUV_420_888 + try/catch around frame acquisition survives, decodes, but no frame is ever drawable → black canvas

The last row is consistent with Skia's GrAHardwareBufferUtils / MakeImageFromBuffer supporting only RGBA-family AHardwareBuffer formats (no YUV case in the switch — vendored in this repo under android/cpp/rnskia-android/). Corroboration:

So on this (very common) decoder family the two constraints are mutually exclusive: Skia's buffer import needs RGBA, but the QTI decoder only emits UBWC/YUV.

Suggested fix direction
  1. Short term: wrap frame acquisition in RNSkVideo.nextImage()/decodeFrame() in try/catch so a decoder quirk can never abort the whole app through JNI (crash → graceful no-video).
  2. Actual fix: decode into a Surface backed by a SurfaceTexture and sample via GL_TEXTURE_EXTERNAL_OES (samplerExternalOES) — the vendor-portable path where the driver performs UBWC-decompression/YUV→RGB at sample time — wrapping the texture into Skia via GrBackendTextures::MakeGL(id, GL_TEXTURE_EXTERNAL_OES) instead of the ImageReader+HardwareBuffer import.
Screenshot
Image Image
React Native Skia Version

2.6.9

React Native Version

0.86.0

Using New Architecture
  • Enabled
Steps to Reproduce
  1. Fresh Expo app, install @shopify/react-native-skia@2.6.9.
  2. Render the component below with any plain H.264 MP4 (bundled asset or URL).
  3. Build and run on a physical Qualcomm device (S23 Ultra here). Emulators/software decoders do not reproduce it.
  4. App crashes ~100 ms after the video starts. On iOS the same code plays fine.
Snack, Code Example, Screenshot, or Link to Repository

https://github.com/LucaL1fe/skia-usevideo-crash-repro

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reproducing the crash with the linked minimal app on a Qualcomm device, then inspect RNSkVideo.java around decodeFrame(), nextImage(), acquireLatestImage(), and getHardwareBuffer(). Review android/cpp/rnskia-android/ and the cited Skia import path. Done means frame delivery no longer aborts through JNI and useVideo produces a drawable frame on the affected decoder path.

Written by the indexing model from the issue text.

Assessment

Tech stack
android, java, react-native
Domain
computer-graphics, mobile
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.