From a2f353459853763b5e267f601c81f2259264a784 Mon Sep 17 00:00:00 2001 From: 1vedantshinde <2vedsh@gmail.com> Date: Fri, 17 Apr 2026 12:18:06 +0530 Subject: [PATCH 1/3] fixed frame extraction rate and multithreading --- .../com/splats/app/VideoFrameExtractor.java | 164 +++++++++++------- 1 file changed, 101 insertions(+), 63 deletions(-) diff --git a/crates/brush-app/app/src/main/java/com/splats/app/VideoFrameExtractor.java b/crates/brush-app/app/src/main/java/com/splats/app/VideoFrameExtractor.java index c42ac6e4..345fbdbf 100644 --- a/crates/brush-app/app/src/main/java/com/splats/app/VideoFrameExtractor.java +++ b/crates/brush-app/app/src/main/java/com/splats/app/VideoFrameExtractor.java @@ -93,12 +93,21 @@ public static void extractFrames(Context context, Uri videoUri, Params params, E final int dim = Math.max(96, Math.min(extractionParams.maxDecodeDimension, 4096)); new Thread(() -> { + int numThreads = Math.min(8, Runtime.getRuntime().availableProcessors()); + java.util.concurrent.ExecutorService executor = java.util.concurrent.Executors.newFixedThreadPool(numThreads); + java.util.concurrent.ArrayBlockingQueue retrievers = new java.util.concurrent.ArrayBlockingQueue<>(numThreads); + try { - MediaMetadataRetriever retriever = new MediaMetadataRetriever(); - retriever.setDataSource(context, videoUri); - Log.i(TAG, "Starting frame extraction for " + videoUri + " dim=" + dim + " steps=" + totalSteps); + for(int i = 0; i < numThreads; i++) { + MediaMetadataRetriever r = new MediaMetadataRetriever(); + r.setDataSource(context, videoUri); + retrievers.add(r); + } - String durationStr = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); + MediaMetadataRetriever infoRetriever = retrievers.peek(); + Log.i(TAG, "Starting frame extraction for " + videoUri + " dim=" + dim + " steps=" + totalSteps + " threads=" + numThreads); + + String durationStr = infoRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); long durationMs = 0; try { durationMs = Long.parseLong(durationStr); @@ -109,10 +118,11 @@ public static void extractFrames(Context context, Uri videoUri, Params params, E File outputDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES); if (outputDir != null && !outputDir.exists()) outputDir.mkdirs(); - int writtenFrames = 0; + + java.util.concurrent.atomic.AtomicInteger writtenFrames = new java.util.concurrent.atomic.AtomicInteger(0); if (durationMs <= 0 && (extractionParams.timesUsRelative == null || extractionParams.timesUsRelative.length == 0)) { - Bitmap single = retriever.getFrameAtTime(0); + Bitmap single = infoRetriever.getFrameAtTime(0); if (single != null) { single = scaleDownIfNeeded(single, dim); File f = new File(outputDir, "frame_000.jpg"); @@ -120,19 +130,21 @@ public static void extractFrames(Context context, Uri videoUri, Params params, E single.compress(Bitmap.CompressFormat.JPEG, JPEG_QUALITY, fos); } single.recycle(); - writtenFrames = 1; + writtenFrames.set(1); + } + for (MediaMetadataRetriever r : retrievers) { + try { r.release(); } catch (Exception ignore) {} } - retriever.release(); + executor.shutdown(); finishExtraction(context, dialogHolder[0], callback, "Extraction complete (1 frame)"); return; } Log.i(TAG, "Starting extraction loop. Steps: " + totalSteps); - long lastTimeUs = -2000L; + java.util.List> futures = new java.util.ArrayList<>(); + + long[] timeUsList = new long[totalSteps]; for (int i = 0; i < totalSteps; i++) { - if (i % 10 == 0) { - Log.i(TAG, "Extraction step " + i + " of " + totalSteps); - } long timeUs; if (extractionParams.timesUsRelative != null && extractionParams.timesUsRelative.length > 0) { timeUs = extractionParams.timesUsRelative[Math.min(i, extractionParams.timesUsRelative.length - 1)]; @@ -140,79 +152,105 @@ public static void extractFrames(Context context, Uri videoUri, Params params, E timeUs = Math.min(timeUs, durationUs - 1); } } else { - // Evenly space samples across [0, duration]: first frame at start, last near end. if (totalSteps <= 1) { timeUs = 0; } else { timeUs = (durationUs * (long) i) / (long) (totalSteps - 1); } } + timeUsList[i] = timeUs; + } + + java.util.concurrent.atomic.AtomicInteger progressCounter = new java.util.concurrent.atomic.AtomicInteger(0); + for (int i = 0; i < totalSteps; i++) { + final int stepIndex = i; + final long timeUs = timeUsList[i]; + // Skip redundant seeks if the gap is < 1ms - if (i > 0 && Math.abs(timeUs - lastTimeUs) < 1000L) { - float progress = (float)(i + 1) / totalSteps; + if (stepIndex > 0 && Math.abs(timeUs - timeUsList[stepIndex - 1]) < 1000L) { + int c = progressCounter.incrementAndGet(); + float progress = (float)c / totalSteps; if (callback != null) callback.onProgress(progress); - updateProgress(context, progressBarHolder[0], statusTextHolder[0], i + 1, totalSteps); + updateProgress(context, progressBarHolder[0], statusTextHolder[0], c, totalSteps); continue; } - lastTimeUs = timeUs; - Bitmap bitmap = null; - try { - int opt = MediaMetadataRetriever.OPTION_CLOSEST_SYNC; - if (Build.VERSION.SDK_INT >= 27) { + futures.add(executor.submit(() -> { + MediaMetadataRetriever retriever = retrievers.take(); + try { + Bitmap bitmap = null; try { - bitmap = retriever.getScaledFrameAtTime(timeUs, opt, dim, dim); - } catch (Throwable t) { - bitmap = retriever.getFrameAtTime(timeUs, opt); - } - } else { - bitmap = retriever.getFrameAtTime(timeUs, opt); - } - - if (bitmap == null) { - opt = MediaMetadataRetriever.OPTION_CLOSEST; - if (Build.VERSION.SDK_INT >= 27) { - try { - bitmap = retriever.getScaledFrameAtTime(timeUs, opt, dim, dim); - } catch (Throwable t) { + int opt = MediaMetadataRetriever.OPTION_CLOSEST_SYNC; + if (Build.VERSION.SDK_INT >= 27) { + try { + bitmap = retriever.getScaledFrameAtTime(timeUs, opt, dim, dim); + } catch (Throwable t) { + bitmap = retriever.getFrameAtTime(timeUs, opt); + } + } else { bitmap = retriever.getFrameAtTime(timeUs, opt); } - } else { - bitmap = retriever.getFrameAtTime(timeUs, opt); - } - } - - if (bitmap == null) { - updateProgress(context, progressBarHolder[0], statusTextHolder[0], i + 1, totalSteps); - continue; - } - bitmap = scaleDownIfNeeded(bitmap, dim); + if (bitmap == null) { + opt = MediaMetadataRetriever.OPTION_CLOSEST; + if (Build.VERSION.SDK_INT >= 27) { + try { + bitmap = retriever.getScaledFrameAtTime(timeUs, opt, dim, dim); + } catch (Throwable t) { + bitmap = retriever.getFrameAtTime(timeUs, opt); + } + } else { + bitmap = retriever.getFrameAtTime(timeUs, opt); + } + } - File outFile = new File(outputDir, String.format("frame_%03d.jpg", i)); - try (FileOutputStream fos = new FileOutputStream(outFile)) { - bitmap.compress(Bitmap.CompressFormat.JPEG, JPEG_QUALITY, fos); - fos.flush(); - writtenFrames++; - } catch (Exception e) { - Log.e(TAG, "Failed to write frame file " + outFile, e); + if (bitmap != null) { + bitmap = scaleDownIfNeeded(bitmap, dim); + File outFile = new File(outputDir, String.format("frame_%03d.jpg", stepIndex)); + try (FileOutputStream fos = new FileOutputStream(outFile)) { + bitmap.compress(Bitmap.CompressFormat.JPEG, JPEG_QUALITY, fos); + fos.flush(); + writtenFrames.incrementAndGet(); + } catch (Exception e) { + Log.e(TAG, "Failed to write frame file " + outFile, e); + } finally { + bitmap.recycle(); + } + } + } catch (OutOfMemoryError oom) { + Log.e(TAG, "OOM extracting frame " + stepIndex, oom); + } catch (Throwable t) { + Log.e(TAG, "Error extracting frame " + stepIndex, t); + } } finally { - bitmap.recycle(); + retrievers.put(retriever); + int c = progressCounter.incrementAndGet(); + if (c % 10 == 0) { + Log.i(TAG, "Extraction progress: " + c + " of " + totalSteps); + } + float progress = (float)c / totalSteps; + if (callback != null) callback.onProgress(progress); + updateProgress(context, progressBarHolder[0], statusTextHolder[0], c, totalSteps); } - } catch (OutOfMemoryError oom) { - Log.e(TAG, "OOM extracting frame " + i, oom); - } catch (Throwable t) { - Log.e(TAG, "Error extracting frame " + i, t); - } + return null; + })); + } - float progress = (float)(i + 1) / totalSteps; - if (callback != null) callback.onProgress(progress); - updateProgress(context, progressBarHolder[0], statusTextHolder[0], i + 1, totalSteps); + for (java.util.concurrent.Future f : futures) { + try { + f.get(); + } catch (Exception e) { + Log.e(TAG, "Error waiting for frame extraction thread", e); + } + } + + executor.shutdown(); + for (MediaMetadataRetriever r : retrievers) { + try { r.release(); } catch (Exception ignore) {} } - retriever.release(); - Log.i(TAG, "Extraction finished: wrote " + writtenFrames + "/" + totalSteps + Log.i(TAG, "Extraction finished: wrote " + writtenFrames.get() + "/" + totalSteps + ", outputDir=" + (outputDir != null ? outputDir.getAbsolutePath() : "")); finishExtraction(context, dialogHolder[0], callback, "Extraction finished"); From 794a29bbdf10ea23d5442bd50c4454fa0a87b03e Mon Sep 17 00:00:00 2001 From: 1vedantshinde <2vedsh@gmail.com> Date: Fri, 17 Apr 2026 12:53:30 +0530 Subject: [PATCH 2/3] Fix MediaMetadataRetriever leaks, OOM risk, and UI thread safety in frame extraction --- .../com/splats/app/VideoFrameExtractor.java | 180 ++++++++++-------- 1 file changed, 104 insertions(+), 76 deletions(-) diff --git a/crates/brush-app/app/src/main/java/com/splats/app/VideoFrameExtractor.java b/crates/brush-app/app/src/main/java/com/splats/app/VideoFrameExtractor.java index 345fbdbf..777edab9 100644 --- a/crates/brush-app/app/src/main/java/com/splats/app/VideoFrameExtractor.java +++ b/crates/brush-app/app/src/main/java/com/splats/app/VideoFrameExtractor.java @@ -92,52 +92,68 @@ public static void extractFrames(Context context, Uri videoUri, Params params, E final int dim = Math.max(96, Math.min(extractionParams.maxDecodeDimension, 4096)); + final Context appContext = context.getApplicationContext(); + new Thread(() -> { - int numThreads = Math.min(8, Runtime.getRuntime().availableProcessors()); - java.util.concurrent.ExecutorService executor = java.util.concurrent.Executors.newFixedThreadPool(numThreads); + int numThreads = Math.max(1, Math.min(4, Runtime.getRuntime().availableProcessors())); + java.util.concurrent.ExecutorService executor = new java.util.concurrent.ThreadPoolExecutor( + numThreads, numThreads, + 0L, java.util.concurrent.TimeUnit.MILLISECONDS, + new java.util.concurrent.ArrayBlockingQueue<>(numThreads * 2), + new java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy()); + java.util.concurrent.ArrayBlockingQueue retrievers = new java.util.concurrent.ArrayBlockingQueue<>(numThreads); try { - for(int i = 0; i < numThreads; i++) { - MediaMetadataRetriever r = new MediaMetadataRetriever(); - r.setDataSource(context, videoUri); - retrievers.add(r); - } - - MediaMetadataRetriever infoRetriever = retrievers.peek(); - Log.i(TAG, "Starting frame extraction for " + videoUri + " dim=" + dim + " steps=" + totalSteps + " threads=" + numThreads); - - String durationStr = infoRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); - long durationMs = 0; - try { - durationMs = Long.parseLong(durationStr); - } catch (Exception e) { - Log.w(TAG, "Invalid duration metadata, defaulting to 0", e); - } - long durationUs = durationMs * 1000L; + // Determine duration using a single temporary retriever + long durationUs = 0; + File outputDir = new File(appContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES), "extracted_frames"); + if (!outputDir.exists()) outputDir.mkdirs(); - File outputDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES); - if (outputDir != null && !outputDir.exists()) outputDir.mkdirs(); - java.util.concurrent.atomic.AtomicInteger writtenFrames = new java.util.concurrent.atomic.AtomicInteger(0); - if (durationMs <= 0 && (extractionParams.timesUsRelative == null || extractionParams.timesUsRelative.length == 0)) { - Bitmap single = infoRetriever.getFrameAtTime(0); - if (single != null) { - single = scaleDownIfNeeded(single, dim); - File f = new File(outputDir, "frame_000.jpg"); - try (FileOutputStream fos = new FileOutputStream(f)) { - single.compress(Bitmap.CompressFormat.JPEG, JPEG_QUALITY, fos); + try (MediaMetadataRetriever infoRetriever = new MediaMetadataRetriever()) { + infoRetriever.setDataSource(appContext, videoUri); + String durationStr = infoRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); + if (durationStr != null) { + try { + durationUs = Long.parseLong(durationStr) * 1000L; + } catch (NumberFormatException e) { + Log.w(TAG, "Invalid duration metadata", e); } - single.recycle(); - writtenFrames.set(1); } - for (MediaMetadataRetriever r : retrievers) { - try { r.release(); } catch (Exception ignore) {} + + if (durationUs <= 0 && (extractionParams.timesUsRelative == null || extractionParams.timesUsRelative.length == 0)) { + Bitmap single = infoRetriever.getFrameAtTime(0); + if (single != null) { + single = scaleDownIfNeeded(single, dim); + File f = new File(outputDir, "frame_000.jpg"); + try (FileOutputStream fos = new FileOutputStream(f)) { + single.compress(Bitmap.CompressFormat.JPEG, JPEG_QUALITY, fos); + } + single.recycle(); + writtenFrames.set(1); + } + executor.shutdown(); + finishExtraction(appContext, dialogHolder[0], callback, "Extraction complete (1 frame)"); + return; } - executor.shutdown(); - finishExtraction(context, dialogHolder[0], callback, "Extraction complete (1 frame)"); - return; + } + + // Initialize retriever pool safely + for (int i = 0; i < numThreads; i++) { + MediaMetadataRetriever r = new MediaMetadataRetriever(); + try { + r.setDataSource(appContext, videoUri); + retrievers.add(r); + } catch (Exception e) { + r.release(); + Log.e(TAG, "Failed to initialize MediaMetadataRetriever " + i, e); + } + } + + if (retrievers.isEmpty()) { + throw new IllegalStateException("Failed to initialize any MediaMetadataRetrievers"); } Log.i(TAG, "Starting extraction loop. Steps: " + totalSteps); @@ -172,20 +188,22 @@ public static void extractFrames(Context context, Uri videoUri, Params params, E int c = progressCounter.incrementAndGet(); float progress = (float)c / totalSteps; if (callback != null) callback.onProgress(progress); - updateProgress(context, progressBarHolder[0], statusTextHolder[0], c, totalSteps); + updateProgress(appContext, progressBarHolder[0], statusTextHolder[0], c, totalSteps); continue; } futures.add(executor.submit(() -> { - MediaMetadataRetriever retriever = retrievers.take(); + MediaMetadataRetriever retriever = null; try { + retriever = retrievers.take(); + Bitmap bitmap = null; try { int opt = MediaMetadataRetriever.OPTION_CLOSEST_SYNC; if (Build.VERSION.SDK_INT >= 27) { try { bitmap = retriever.getScaledFrameAtTime(timeUs, opt, dim, dim); - } catch (Throwable t) { + } catch (Exception t) { bitmap = retriever.getFrameAtTime(timeUs, opt); } } else { @@ -197,7 +215,7 @@ public static void extractFrames(Context context, Uri videoUri, Params params, E if (Build.VERSION.SDK_INT >= 27) { try { bitmap = retriever.getScaledFrameAtTime(timeUs, opt, dim, dim); - } catch (Throwable t) { + } catch (Exception t) { bitmap = retriever.getFrameAtTime(timeUs, opt); } } else { @@ -206,32 +224,47 @@ public static void extractFrames(Context context, Uri videoUri, Params params, E } if (bitmap != null) { - bitmap = scaleDownIfNeeded(bitmap, dim); + Bitmap scaled = scaleDownIfNeeded(bitmap, dim); + if (scaled != bitmap) { + bitmap.recycle(); + bitmap = scaled; + } + File outFile = new File(outputDir, String.format("frame_%03d.jpg", stepIndex)); try (FileOutputStream fos = new FileOutputStream(outFile)) { bitmap.compress(Bitmap.CompressFormat.JPEG, JPEG_QUALITY, fos); fos.flush(); writtenFrames.incrementAndGet(); - } catch (Exception e) { + } catch (java.io.IOException e) { Log.e(TAG, "Failed to write frame file " + outFile, e); - } finally { - bitmap.recycle(); } } } catch (OutOfMemoryError oom) { Log.e(TAG, "OOM extracting frame " + stepIndex, oom); - } catch (Throwable t) { + } catch (Exception t) { Log.e(TAG, "Error extracting frame " + stepIndex, t); + } finally { + if (bitmap != null) { + bitmap.recycle(); + } } + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); } finally { - retrievers.put(retriever); + if (retriever != null) { + try { + retrievers.put(retriever); + } catch (InterruptedException ignore) { + Thread.currentThread().interrupt(); + } + } int c = progressCounter.incrementAndGet(); if (c % 10 == 0) { Log.i(TAG, "Extraction progress: " + c + " of " + totalSteps); } float progress = (float)c / totalSteps; if (callback != null) callback.onProgress(progress); - updateProgress(context, progressBarHolder[0], statusTextHolder[0], c, totalSteps); + updateProgress(appContext, progressBarHolder[0], statusTextHolder[0], c, totalSteps); } return null; })); @@ -246,17 +279,20 @@ public static void extractFrames(Context context, Uri videoUri, Params params, E } executor.shutdown(); - for (MediaMetadataRetriever r : retrievers) { - try { r.release(); } catch (Exception ignore) {} - } Log.i(TAG, "Extraction finished: wrote " + writtenFrames.get() + "/" + totalSteps + ", outputDir=" + (outputDir != null ? outputDir.getAbsolutePath() : "")); - finishExtraction(context, dialogHolder[0], callback, "Extraction finished"); + finishExtraction(appContext, dialogHolder[0], callback, "Extraction finished"); } catch (Exception e) { Log.e(TAG, "Extraction failed", e); - failExtraction(context, dialogHolder[0], callback, e); + failExtraction(appContext, dialogHolder[0], callback, e); + } finally { + for (MediaMetadataRetriever r : retrievers) { + try { + r.release(); + } catch (Exception ignore) {} + } } }).start(); } @@ -271,42 +307,34 @@ private static Bitmap scaleDownIfNeeded(Bitmap bitmap, int maxEdge) { float scale = (float) maxEdge / (float) maxDim; int newW = Math.max(1, Math.round(w * scale)); int newH = Math.max(1, Math.round(h * scale)); - Bitmap scaled = Bitmap.createScaledBitmap(bitmap, newW, newH, true); - bitmap.recycle(); - return scaled; + return Bitmap.createScaledBitmap(bitmap, newW, newH, true); } private static void updateProgress(Context context, ProgressBar bar, TextView text, int progress, int total) { - if (context instanceof android.app.Activity) { - ((android.app.Activity) context).runOnUiThread(() -> { - if (bar != null) bar.setProgress(progress); - if (text != null) text.setText(progress + " / " + total); - }); - } + new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> { + if (bar != null) bar.setProgress(progress); + if (text != null) text.setText(progress + " / " + total); + }); } private static void finishExtraction(Context context, AlertDialog dialog, ExtractionCallback callback, String msg) { - if (context instanceof android.app.Activity) { - ((android.app.Activity) context).runOnUiThread(() -> { - if (dialog != null) dialog.dismiss(); - Toast.makeText(context, msg, Toast.LENGTH_SHORT).show(); - if (callback != null) callback.onFinished(); - }); - } + new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> { + if (dialog != null) dialog.dismiss(); + Toast.makeText(context, msg, Toast.LENGTH_SHORT).show(); + if (callback != null) callback.onFinished(); + }); } private static void failExtraction(Context context, AlertDialog dialog, ExtractionCallback callback, Exception e) { - if (context instanceof android.app.Activity) { - ((android.app.Activity) context).runOnUiThread(() -> { - if (dialog != null) dialog.dismiss(); - Toast.makeText(context, "Extraction failed: " + e.getMessage(), Toast.LENGTH_LONG).show(); - if (callback != null) callback.onFailure(e); - }); - } + new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> { + if (dialog != null) dialog.dismiss(); + Toast.makeText(context, "Extraction failed: " + e.getMessage(), Toast.LENGTH_LONG).show(); + if (callback != null) callback.onFailure(e); + }); } public static void cleanupExtractedFrames(Context context) { - File outputDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES); + File outputDir = new File(context.getApplicationContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES), "extracted_frames"); deleteFilesMatching(outputDir, name -> name.startsWith("frame_") && name.endsWith(".jpg")); } From 867e6180834b14ac1d697b20fbe8bc0eb1fe94a9 Mon Sep 17 00:00:00 2001 From: 1vedantshinde <2vedsh@gmail.com> Date: Fri, 17 Apr 2026 13:00:04 +0530 Subject: [PATCH 3/3] Fix Further Architectural Errors --- .../com/splats/app/VideoFrameExtractor.java | 271 +++++++++--------- 1 file changed, 139 insertions(+), 132 deletions(-) diff --git a/crates/brush-app/app/src/main/java/com/splats/app/VideoFrameExtractor.java b/crates/brush-app/app/src/main/java/com/splats/app/VideoFrameExtractor.java index 777edab9..c17ae5a5 100644 --- a/crates/brush-app/app/src/main/java/com/splats/app/VideoFrameExtractor.java +++ b/crates/brush-app/app/src/main/java/com/splats/app/VideoFrameExtractor.java @@ -90,57 +90,14 @@ public static void extractFrames(Context context, Uri videoUri, Params params, E }); } - final int dim = Math.max(96, Math.min(extractionParams.maxDecodeDimension, 4096)); - final Context appContext = context.getApplicationContext(); new Thread(() -> { int numThreads = Math.max(1, Math.min(4, Runtime.getRuntime().availableProcessors())); - java.util.concurrent.ExecutorService executor = new java.util.concurrent.ThreadPoolExecutor( - numThreads, numThreads, - 0L, java.util.concurrent.TimeUnit.MILLISECONDS, - new java.util.concurrent.ArrayBlockingQueue<>(numThreads * 2), - new java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy()); - java.util.concurrent.ArrayBlockingQueue retrievers = new java.util.concurrent.ArrayBlockingQueue<>(numThreads); try { - // Determine duration using a single temporary retriever - long durationUs = 0; - File outputDir = new File(appContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES), "extracted_frames"); - if (!outputDir.exists()) outputDir.mkdirs(); - - java.util.concurrent.atomic.AtomicInteger writtenFrames = new java.util.concurrent.atomic.AtomicInteger(0); - - try (MediaMetadataRetriever infoRetriever = new MediaMetadataRetriever()) { - infoRetriever.setDataSource(appContext, videoUri); - String durationStr = infoRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); - if (durationStr != null) { - try { - durationUs = Long.parseLong(durationStr) * 1000L; - } catch (NumberFormatException e) { - Log.w(TAG, "Invalid duration metadata", e); - } - } - - if (durationUs <= 0 && (extractionParams.timesUsRelative == null || extractionParams.timesUsRelative.length == 0)) { - Bitmap single = infoRetriever.getFrameAtTime(0); - if (single != null) { - single = scaleDownIfNeeded(single, dim); - File f = new File(outputDir, "frame_000.jpg"); - try (FileOutputStream fos = new FileOutputStream(f)) { - single.compress(Bitmap.CompressFormat.JPEG, JPEG_QUALITY, fos); - } - single.recycle(); - writtenFrames.set(1); - } - executor.shutdown(); - finishExtraction(appContext, dialogHolder[0], callback, "Extraction complete (1 frame)"); - return; - } - } - - // Initialize retriever pool safely + // Determine duration and initialize pool simultaneously (no redundant setup) for (int i = 0; i < numThreads; i++) { MediaMetadataRetriever r = new MediaMetadataRetriever(); try { @@ -156,6 +113,39 @@ public static void extractFrames(Context context, Uri videoUri, Params params, E throw new IllegalStateException("Failed to initialize any MediaMetadataRetrievers"); } + MediaMetadataRetriever infoRetriever = retrievers.peek(); + long durationUs = 0; + String durationStr = infoRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); + if (durationStr != null) { + try { + durationUs = Long.parseLong(durationStr) * 1000L; + } catch (NumberFormatException e) { + Log.w(TAG, "Invalid duration metadata", e); + } + } + + File outputDir = new File(appContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES), "extracted_frames"); + if (!outputDir.exists()) outputDir.mkdirs(); + + java.util.concurrent.atomic.AtomicInteger writtenFrames = new java.util.concurrent.atomic.AtomicInteger(0); + + if (durationUs <= 0 && (extractionParams.timesUsRelative == null || extractionParams.timesUsRelative.length == 0)) { + Bitmap single = infoRetriever.getFrameAtTime(0); + if (single != null) { + single = scaleDownIfNeeded(single, dim); + File f = new File(outputDir, "frame_000.jpg"); + try (FileOutputStream fos = new FileOutputStream(f)) { + single.compress(Bitmap.CompressFormat.JPEG, JPEG_QUALITY, fos); + } + single.recycle(); + writtenFrames.set(1); + } + finishExtraction(context, dialogHolder[0], callback, "Extraction complete (1 frame)"); + return; + } + + java.util.concurrent.ExecutorService executor = java.util.concurrent.Executors.newFixedThreadPool(numThreads); + Log.i(TAG, "Starting extraction loop. Steps: " + totalSteps); java.util.List> futures = new java.util.ArrayList<>(); @@ -179,114 +169,115 @@ public static void extractFrames(Context context, Uri videoUri, Params params, E java.util.concurrent.atomic.AtomicInteger progressCounter = new java.util.concurrent.atomic.AtomicInteger(0); - for (int i = 0; i < totalSteps; i++) { - final int stepIndex = i; - final long timeUs = timeUsList[i]; - - // Skip redundant seeks if the gap is < 1ms - if (stepIndex > 0 && Math.abs(timeUs - timeUsList[stepIndex - 1]) < 1000L) { - int c = progressCounter.incrementAndGet(); - float progress = (float)c / totalSteps; - if (callback != null) callback.onProgress(progress); - updateProgress(appContext, progressBarHolder[0], statusTextHolder[0], c, totalSteps); - continue; - } + try { + for (int i = 0; i < totalSteps; i++) { + final int stepIndex = i; + final long timeUs = timeUsList[i]; + + // Skip redundant seeks if the gap is < 1ms + if (stepIndex > 0 && Math.abs(timeUs - timeUsList[stepIndex - 1]) < 1000L) { + int c = progressCounter.incrementAndGet(); + float progress = (float)c / totalSteps; + if (callback != null) callback.onProgress(progress); + updateProgress(context, progressBarHolder[0], statusTextHolder[0], c, totalSteps); + continue; + } - futures.add(executor.submit(() -> { - MediaMetadataRetriever retriever = null; - try { - retriever = retrievers.take(); - - Bitmap bitmap = null; + futures.add(executor.submit(() -> { + MediaMetadataRetriever retriever = null; try { - int opt = MediaMetadataRetriever.OPTION_CLOSEST_SYNC; - if (Build.VERSION.SDK_INT >= 27) { - try { - bitmap = retriever.getScaledFrameAtTime(timeUs, opt, dim, dim); - } catch (Exception t) { - bitmap = retriever.getFrameAtTime(timeUs, opt); - } - } else { - bitmap = retriever.getFrameAtTime(timeUs, opt); - } - - if (bitmap == null) { - opt = MediaMetadataRetriever.OPTION_CLOSEST; + retriever = retrievers.take(); + + Bitmap bitmap = null; + try { + int opt = MediaMetadataRetriever.OPTION_CLOSEST_SYNC; + if (Build.VERSION.SDK_INT >= 27) { try { bitmap = retriever.getScaledFrameAtTime(timeUs, opt, dim, dim); - } catch (Exception t) { - bitmap = retriever.getFrameAtTime(timeUs, opt); + } catch (IllegalArgumentException | IllegalStateException e) { + // Fallback } - } else { + } + if (bitmap == null) { bitmap = retriever.getFrameAtTime(timeUs, opt); } - } - if (bitmap != null) { - Bitmap scaled = scaleDownIfNeeded(bitmap, dim); - if (scaled != bitmap) { - bitmap.recycle(); - bitmap = scaled; + if (bitmap == null) { + opt = MediaMetadataRetriever.OPTION_CLOSEST; + if (Build.VERSION.SDK_INT >= 27) { + try { + bitmap = retriever.getScaledFrameAtTime(timeUs, opt, dim, dim); + } catch (IllegalArgumentException | IllegalStateException e) { + // Fallback + } + } + if (bitmap == null) { + bitmap = retriever.getFrameAtTime(timeUs, opt); + } } - - File outFile = new File(outputDir, String.format("frame_%03d.jpg", stepIndex)); - try (FileOutputStream fos = new FileOutputStream(outFile)) { - bitmap.compress(Bitmap.CompressFormat.JPEG, JPEG_QUALITY, fos); - fos.flush(); - writtenFrames.incrementAndGet(); - } catch (java.io.IOException e) { - Log.e(TAG, "Failed to write frame file " + outFile, e); + + if (bitmap != null) { + bitmap = scaleDownIfNeeded(bitmap, dim); + + File outFile = new File(outputDir, String.format("frame_%03d.jpg", stepIndex)); + try (FileOutputStream fos = new FileOutputStream(outFile)) { + bitmap.compress(Bitmap.CompressFormat.JPEG, JPEG_QUALITY, fos); + fos.flush(); + writtenFrames.incrementAndGet(); + } catch (java.io.IOException e) { + Log.e(TAG, "Failed to write frame file " + outFile, e); + } + } + } catch (OutOfMemoryError oom) { + Log.e(TAG, "OOM extracting frame " + stepIndex, oom); + } catch (Exception t) { + Log.e(TAG, "Error extracting frame " + stepIndex, t); + } finally { + if (bitmap != null) { + bitmap.recycle(); } } - } catch (OutOfMemoryError oom) { - Log.e(TAG, "OOM extracting frame " + stepIndex, oom); - } catch (Exception t) { - Log.e(TAG, "Error extracting frame " + stepIndex, t); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); } finally { - if (bitmap != null) { - bitmap.recycle(); + if (retriever != null) { + try { + retrievers.put(retriever); + } catch (InterruptedException ignore) { + Thread.currentThread().interrupt(); + } } - } - } catch (InterruptedException ie) { - Thread.currentThread().interrupt(); - } finally { - if (retriever != null) { - try { - retrievers.put(retriever); - } catch (InterruptedException ignore) { - Thread.currentThread().interrupt(); + int c = progressCounter.incrementAndGet(); + if (c % 10 == 0) { + Log.i(TAG, "Extraction progress: " + c + " of " + totalSteps); } + float progress = (float)c / totalSteps; + if (callback != null) callback.onProgress(progress); + updateProgress(context, progressBarHolder[0], statusTextHolder[0], c, totalSteps); } - int c = progressCounter.incrementAndGet(); - if (c % 10 == 0) { - Log.i(TAG, "Extraction progress: " + c + " of " + totalSteps); - } - float progress = (float)c / totalSteps; - if (callback != null) callback.onProgress(progress); - updateProgress(appContext, progressBarHolder[0], statusTextHolder[0], c, totalSteps); - } - return null; - })); - } + return null; + })); + } - for (java.util.concurrent.Future f : futures) { - try { - f.get(); - } catch (Exception e) { - Log.e(TAG, "Error waiting for frame extraction thread", e); + for (java.util.concurrent.Future f : futures) { + try { + f.get(); + } catch (Exception e) { + Log.e(TAG, "Error waiting for frame extraction thread", e); + } } + } finally { + executor.shutdownNow(); } - - executor.shutdown(); Log.i(TAG, "Extraction finished: wrote " + writtenFrames.get() + "/" + totalSteps + ", outputDir=" + (outputDir != null ? outputDir.getAbsolutePath() : "")); - finishExtraction(appContext, dialogHolder[0], callback, "Extraction finished"); + finishExtraction(context, dialogHolder[0], callback, "Extraction finished"); } catch (Exception e) { Log.e(TAG, "Extraction failed", e); - failExtraction(appContext, dialogHolder[0], callback, e); + failExtraction(context, dialogHolder[0], callback, e); } finally { for (MediaMetadataRetriever r : retrievers) { try { @@ -307,11 +298,19 @@ private static Bitmap scaleDownIfNeeded(Bitmap bitmap, int maxEdge) { float scale = (float) maxEdge / (float) maxDim; int newW = Math.max(1, Math.round(w * scale)); int newH = Math.max(1, Math.round(h * scale)); - return Bitmap.createScaledBitmap(bitmap, newW, newH, true); + + Bitmap scaled = Bitmap.createScaledBitmap(bitmap, newW, newH, true); + if (scaled != bitmap && bitmap != null) { + bitmap.recycle(); + } + return scaled; } private static void updateProgress(Context context, ProgressBar bar, TextView text, int progress, int total) { new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> { + if (context instanceof android.app.Activity && (((android.app.Activity)context).isFinishing() || ((android.app.Activity)context).isDestroyed())) { + return; + } if (bar != null) bar.setProgress(progress); if (text != null) text.setText(progress + " / " + total); }); @@ -319,16 +318,24 @@ private static void updateProgress(Context context, ProgressBar bar, TextView te private static void finishExtraction(Context context, AlertDialog dialog, ExtractionCallback callback, String msg) { new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> { - if (dialog != null) dialog.dismiss(); - Toast.makeText(context, msg, Toast.LENGTH_SHORT).show(); + if (context instanceof android.app.Activity && !((android.app.Activity)context).isFinishing() && !((android.app.Activity)context).isDestroyed()) { + if (dialog != null && dialog.isShowing()) { + try { dialog.dismiss(); } catch (Exception ignore) {} + } + } + Toast.makeText(context.getApplicationContext(), msg, Toast.LENGTH_SHORT).show(); if (callback != null) callback.onFinished(); }); } private static void failExtraction(Context context, AlertDialog dialog, ExtractionCallback callback, Exception e) { new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> { - if (dialog != null) dialog.dismiss(); - Toast.makeText(context, "Extraction failed: " + e.getMessage(), Toast.LENGTH_LONG).show(); + if (context instanceof android.app.Activity && !((android.app.Activity)context).isFinishing() && !((android.app.Activity)context).isDestroyed()) { + if (dialog != null && dialog.isShowing()) { + try { dialog.dismiss(); } catch (Exception ignore) {} + } + } + Toast.makeText(context.getApplicationContext(), "Extraction failed: " + e.getMessage(), Toast.LENGTH_LONG).show(); if (callback != null) callback.onFailure(e); }); }