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:
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
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
React Native Skia Version
2.6.9
React Native Version
0.86.0
Using New Architecture
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
https://github.com/LucaL1fe/skia-usevideo-crash-repro
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 momentc2.qti.avc.decoderdelivers its first frame into Skia'sImageReader. The identical code works perfectly on iOS.If the Java exception is caught inside
RNSkVideo(patched locally), the app survives butcurrentFramenever 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)SM-S918B), Android 16 (API 36), Snapdragon 8 Gen 2 (SM8550/kalama, Adreno 740,c2.qti.avc.decoder). iOS unaffected.Steps to reproduce
@shopify/react-native-skia@2.6.9.Minimal repro
Crash log (release build of the minimal repro)
Tombstone backtrace:
The crashing tid (
mqt_v_js) is the same thread that runs theMediaCodecList/MediaCodeccalls — i.e. a Java exception raised during frame delivery inRNSkVideois left pending, and the next fbjni call converts it into an uncaught C++ exception that aborts the VM.RNSkVideo.javahas no try/catch arounddecodeFrame()/acquireLatestImage()/getHardwareBuffer().Why no ImageReader format works here (field-tested)
RNSkVideo.javadecodes into anImageReader(ImageFormat.PRIVATE,USAGE_GPU_SAMPLED_IMAGE) and imports each frame'sHardwareBufferinto Skia. On QTI decoders each option fails differently — I patchedRNSkVideo.javalocally and tested all three on the same device:c2.qti.avc.decoderPRIVATE(stock)HAL_PIXEL_FORMAT_YCbCr_420_SP_VENUS_UBWC(0x7fa30c06) → exception on first frame → SIGABRTRGBA_88880x7fa30c06vs reader0x1) →UnsupportedOperationException→ SIGABRTYUV_420_888+ try/catch around frame acquisitionThe last row is consistent with Skia's
GrAHardwareBufferUtils/MakeImageFromBuffersupporting only RGBA-familyAHardwareBufferformats (no YUV case in the switch — vendored in this repo underandroid/cpp/rnskia-android/). Corroboration:"AHardwareBuffer has unknown format (35) - cannot convert to SkImage!"(35 = YUV_420_888).DeferredFromAHardwareBufferis RGBA-only.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
RNSkVideo.nextImage()/decodeFrame()in try/catch so a decoder quirk can never abort the whole app through JNI (crash → graceful no-video).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
Steps to Reproduce
@shopify/react-native-skia@2.6.9.Snack, Code Example, Screenshot, or Link to Repository
https://github.com/LucaL1fe/skia-usevideo-crash-repro