Skip to content

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) #3936

Description

@LucaL1fe

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions