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)
Nobody has claimed this yet.
- 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
- Fresh Expo app, install
@shopify/react-native-skia@2.6.9. - Render the component below with any plain H.264 MP4 (bundled asset or URL).
- Build and run on a physical Qualcomm device (S23 Ultra here). Emulators/software decoders do not reproduce it.
- 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) → UnsupportedOperationException → SIGABRT |
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:
- https://github.com/mrousavy/react-native-vision-camera/issues/2752 — same importer path:
"AHardwareBuffer has unknown format (35) - cannot convert to SkImage!"(35 = YUV_420_888). - https://groups.google.com/g/skia-discuss/c/J4dH2PJr2-E — Skia upstream:
DeferredFromAHardwareBufferis RGBA-only. - Possibly the same family as #2964 ("Failed to convert NativeBuffer to SkImage" / black, Android).
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
- 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). - Actual fix: decode into a
Surfacebacked by aSurfaceTextureand sample viaGL_TEXTURE_EXTERNAL_OES(samplerExternalOES) — the vendor-portable path where the driver performs UBWC-decompression/YUV→RGB at sample time — wrapping the texture into Skia viaGrBackendTextures::MakeGL(id, GL_TEXTURE_EXTERNAL_OES)instead of theImageReader+HardwareBufferimport.
Screenshot
React Native Skia Version
2.6.9
React Native Version
0.86.0
Using New Architecture
- Enabled
Steps to Reproduce
- Fresh Expo app, install
@shopify/react-native-skia@2.6.9. - Render the component below with any plain H.264 MP4 (bundled asset or URL).
- Build and run on a physical Qualcomm device (S23 Ultra here). Emulators/software decoders do not reproduce it.
- App crashes ~100 ms after the video starts. On iOS the same code plays fine.
Snack, Code Example, Screenshot, or Link to Repository
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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