From 3cc996cfe7d3022c5973e1fbfe8aa8a9e6b91f4e Mon Sep 17 00:00:00 2001 From: gandol Date: Wed, 15 Jul 2026 11:33:18 +0700 Subject: [PATCH 1/2] fix(android): reject promise instead of crashing on setDataSource failure MediaMetadataRetriever.setDataSource can throw RuntimeException (e.g. status 0x80000000 for bad/unsupported remote URLs). The previous catch only handled IOException and IllegalStateException, so the uncaught RuntimeException on the worker thread killed the entire app process. Catch Exception in create(), wrap RuntimeException in getBitmapAtTime, validate empty URLs, and always release the retriever in finally. --- .../CreateThumbnailModule.java | 84 +++++++++++-------- 1 file changed, 51 insertions(+), 33 deletions(-) diff --git a/android/src/main/java/com/createthumbnail/CreateThumbnailModule.java b/android/src/main/java/com/createthumbnail/CreateThumbnailModule.java index ecc91d9..61238ac 100644 --- a/android/src/main/java/com/createthumbnail/CreateThumbnailModule.java +++ b/android/src/main/java/com/createthumbnail/CreateThumbnailModule.java @@ -58,7 +58,7 @@ public void create(ReadableMap options, Promise promise) { handler.post(() -> { promise.resolve(data); }); - } catch (IOException | IllegalStateException e) { + } catch (Exception e) { handler.post(() -> { promise.reject("CreateThumbnail_ERROR", e); }); @@ -162,44 +162,62 @@ private static File createDirIfNotExists(String path) { } private static Bitmap getBitmapAtTime(Context context, String filePath, int time, int maxWidth, int maxHeight, boolean onlySyncedFrames, Map headers) throws IOException, IllegalStateException { + if (TextUtils.isEmpty(filePath)) { + throw new IllegalStateException("Video url is empty"); + } + MediaMetadataRetriever retriever = new MediaMetadataRetriever(); - if (URLUtil.isFileUrl(filePath)) { - String decodedPath; - try { - decodedPath = URLDecoder.decode(filePath, "UTF-8"); - } catch (UnsupportedEncodingException e) { - decodedPath = filePath; + try { + if (URLUtil.isFileUrl(filePath)) { + String decodedPath; + try { + decodedPath = URLDecoder.decode(filePath, "UTF-8"); + } catch (UnsupportedEncodingException e) { + decodedPath = filePath; + } + + retriever.setDataSource(decodedPath.replace("file://", "")); + } else if (filePath.contains("content://")) { + retriever.setDataSource(context, Uri.parse(filePath)); + } else { + if (VERSION.SDK_INT < 14) { + throw new IllegalStateException("Remote videos aren't supported on sdk_version < 14"); + } + HashMap stringHeaders = new HashMap<>(); + if (headers != null) { + for (Object key : headers.keySet()) { + Object value = headers.get(key); + if (key != null && value != null) { + stringHeaders.put(String.valueOf(key), String.valueOf(value)); + } + } + } + retriever.setDataSource(filePath, stringHeaders); } - retriever.setDataSource(decodedPath.replace("file://", "")); - } else if (filePath.contains("content://")) { - retriever.setDataSource(context, Uri.parse(filePath)); - } else { - if (VERSION.SDK_INT < 14) { - throw new IllegalStateException("Remote videos aren't supported on sdk_version < 14"); + Bitmap image; + if (Build.VERSION.SDK_INT >= 27) { + image = retriever.getScaledFrameAtTime(time * 1000, MediaMetadataRetriever.OPTION_CLOSEST_SYNC, maxWidth, maxHeight); + } else { + // If on versions lower than API 27, use other methods to get frames + image = retriever.getFrameAtTime(time * 1000, MediaMetadataRetriever.OPTION_CLOSEST_SYNC); + if (image != null) { + image = Bitmap.createScaledBitmap(image, maxWidth, maxHeight, true); + } } - retriever.setDataSource(filePath, headers); - } - - Bitmap image; - if (Build.VERSION.SDK_INT >= 27) { - image = retriever.getScaledFrameAtTime(time * 1000, MediaMetadataRetriever.OPTION_CLOSEST_SYNC, maxWidth, maxHeight); - } else { - // If on versions lower than API 27, use other methods to get frames - image = retriever.getFrameAtTime(time * 1000, MediaMetadataRetriever.OPTION_CLOSEST_SYNC); - if (image != null) { - image = Bitmap.createScaledBitmap(image, maxWidth, maxHeight, true); + if (image == null) { + throw new IllegalStateException("File doesn't exist or not supported"); + } + return image; + } catch (RuntimeException e) { + throw new IllegalStateException("Failed to create thumbnail for: " + filePath, e); + } finally { + try { + retriever.release(); + } catch (Exception e) { + e.printStackTrace(); } } - try { - retriever.release(); - } catch(IOException e) { - e.printStackTrace(); - } - if (image == null) { - throw new IllegalStateException("File doesn't exist or not supported"); - } - return image; } private static long getDirSize(File dir) { From a7cf48c15305302e004fe53ee5d8f3d2cd003096 Mon Sep 17 00:00:00 2001 From: gandol Date: Wed, 15 Jul 2026 11:33:18 +0700 Subject: [PATCH 2/2] fix: guard JS exports when native module is missing Destructuring create from CreateThumbnail throws if the native module is not linked (e.g. Expo Go, incomplete autolinking). Export safe fallbacks that reject the Promise instead of crashing at import time. --- index.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index be05450..392e4de 100644 --- a/index.js +++ b/index.js @@ -2,5 +2,8 @@ import { NativeModules } from "react-native"; const { CreateThumbnail } = NativeModules; -export const { create: createThumbnail } = CreateThumbnail; -export default CreateThumbnail; +export const createThumbnail = CreateThumbnail?.create + ? (config) => CreateThumbnail.create(config) + : () => Promise.reject(new Error("CreateThumbnail native module is not available")); + +export default CreateThumbnail ?? { create: createThumbnail };