|
51 | 51 |
|
52 | 52 | import wseemann.media.FFmpegMediaMetadataRetriever; |
53 | 53 |
|
| 54 | +import java.util.UUID; |
| 55 | +import java.io.FileOutputStream; |
| 56 | +import java.util.Arrays; |
| 57 | +import java.util.ArrayList; |
| 58 | + |
| 59 | +import com.github.hiteshsondhi88.libffmpeg.FFmpeg; |
| 60 | +import com.github.hiteshsondhi88.libffmpeg.FFmpegExecuteResponseHandler; |
| 61 | +import com.github.hiteshsondhi88.libffmpeg.FFmpegLoadBinaryResponseHandler; |
| 62 | + |
| 63 | + |
54 | 64 | public class Trimmer { |
55 | 65 |
|
56 | 66 | private static final String LOG_TAG = "RNTrimmerManager"; |
57 | 67 |
|
| 68 | + private static boolean ffmpegLoaded; |
| 69 | + |
58 | 70 | public static void getPreviewImages(String path, Promise promise, ReactApplicationContext ctx) { |
59 | 71 | FFmpegMediaMetadataRetriever retriever = new FFmpegMediaMetadataRetriever(); |
60 | 72 | if (VideoEdit.shouldUseURI(path)) { |
@@ -202,19 +214,222 @@ public void cancelAction() { |
202 | 214 | } |
203 | 215 | } |
204 | 216 |
|
205 | | - static void getPreviewAtPosition(String source, double sec, final Promise promise) { |
| 217 | + static File createTempFile(String extension, final Promise promise, ReactApplicationContext ctx) { |
| 218 | + UUID uuid = UUID.randomUUID(); |
| 219 | + String imageName = uuid.toString() + "-screenshot"; |
| 220 | + |
| 221 | + File cacheDir = ctx.getCacheDir(); |
| 222 | + File tempFile = null; |
| 223 | + try { |
| 224 | + tempFile = File.createTempFile(imageName, "." + extension, cacheDir); |
| 225 | + } catch( IOException e ) { |
| 226 | + promise.reject("Failed to create temp file", e.toString()); |
| 227 | + return null; |
| 228 | + } |
| 229 | + |
| 230 | + if (tempFile.exists()) { |
| 231 | + tempFile.delete(); |
| 232 | + } |
| 233 | + |
| 234 | + return tempFile; |
| 235 | + } |
| 236 | + |
| 237 | + static void getPreviewImageAtPosition(String source, double sec, String format, final Promise promise, ReactApplicationContext ctx) { |
206 | 238 | FFmpegMediaMetadataRetriever metadataRetriever = new FFmpegMediaMetadataRetriever(); |
| 239 | + FFmpegMediaMetadataRetriever.IN_PREFERRED_CONFIG = Bitmap.Config.ARGB_8888; |
207 | 240 | metadataRetriever.setDataSource(source); |
208 | 241 |
|
209 | 242 | Bitmap bmp = metadataRetriever.getFrameAtTime((long) (sec * 1000000)); |
| 243 | + |
| 244 | + // NOTE: FIX ROTATED BITMAP |
| 245 | + int orientation = Integer.parseInt( metadataRetriever.extractMetadata(FFmpegMediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION) ); |
| 246 | + metadataRetriever.release(); |
| 247 | + |
| 248 | + if ( orientation != 0 ) { |
| 249 | + Matrix matrix = new Matrix(); |
| 250 | + matrix.postRotate(orientation); |
| 251 | + bmp = Bitmap.createBitmap(bmp, 0, 0, bmp.getWidth(), bmp.getHeight(), matrix, true); |
| 252 | + } |
| 253 | + |
210 | 254 | ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); |
211 | | - bmp.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream); |
212 | | - byte[] byteArray = byteArrayOutputStream .toByteArray(); |
213 | | - String encoded = Base64.encodeToString(byteArray, Base64.DEFAULT); |
214 | 255 |
|
215 | 256 | WritableMap event = Arguments.createMap(); |
216 | | - event.putString("image", encoded); |
| 257 | + |
| 258 | + if ( format.equals(null) || format.equals("base64") ) { |
| 259 | + bmp.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream); |
| 260 | + byte[] byteArray = byteArrayOutputStream .toByteArray(); |
| 261 | + String encoded = Base64.encodeToString(byteArray, Base64.DEFAULT); |
| 262 | + |
| 263 | + event.putString("image", encoded); |
| 264 | + } else if ( format.equals("JPEG") ) { |
| 265 | + bmp.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream); |
| 266 | + byte[] byteArray = byteArrayOutputStream.toByteArray(); |
| 267 | + |
| 268 | + File tempFile = createTempFile("jpeg", promise, ctx); |
| 269 | + |
| 270 | + try { |
| 271 | + FileOutputStream fos = new FileOutputStream( tempFile.getPath() ); |
| 272 | + |
| 273 | + fos.write( byteArray ); |
| 274 | + fos.close(); |
| 275 | + } catch (java.io.IOException e) { |
| 276 | + promise.reject("Failed to save image", e.toString()); |
| 277 | + return; |
| 278 | + } |
| 279 | + |
| 280 | + WritableMap imageMap = Arguments.createMap(); |
| 281 | + imageMap.putString("uri", "file://" + tempFile.getPath()); |
| 282 | + |
| 283 | + event.putMap("image", imageMap); |
| 284 | + } else { |
| 285 | + promise.reject("Wrong format error", "Wrong 'format'. Expected one of 'base64' or 'JPEG'."); |
| 286 | + return; |
| 287 | + } |
217 | 288 |
|
218 | 289 | promise.resolve(event); |
219 | 290 | } |
| 291 | + |
| 292 | + static void crop(String source, ReadableMap options, final Promise promise, ReactApplicationContext ctx) { |
| 293 | + int cropWidth = (int)( options.getDouble("cropWidth") ); |
| 294 | + int cropHeight = (int)( options.getDouble("cropHeight") ); |
| 295 | + int cropOffsetX = (int)( options.getDouble("cropOffsetX") ); |
| 296 | + int cropOffsetY = (int)( options.getDouble("cropOffsetY") ); |
| 297 | + |
| 298 | + FFmpegMediaMetadataRetriever retriever = new FFmpegMediaMetadataRetriever(); |
| 299 | + if (VideoEdit.shouldUseURI(source)) { |
| 300 | + retriever.setDataSource(ctx, Uri.parse(source)); |
| 301 | + } else { |
| 302 | + retriever.setDataSource(source); |
| 303 | + } |
| 304 | + |
| 305 | + int videoWidth = Integer.parseInt(retriever.extractMetadata(FFmpegMediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)); |
| 306 | + int videoHeight = Integer.parseInt(retriever.extractMetadata(FFmpegMediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)); |
| 307 | + retriever.release(); |
| 308 | + |
| 309 | + // NOTE: FFMpeg CROP NEED TO BE DEVIDED BY 2. OR YOU WILL SEE BLANK WHITE LINES FROM LEFT/RIGHT |
| 310 | + while( cropWidth % 2 > 0 && cropWidth < videoWidth ) { |
| 311 | + cropWidth += 1; |
| 312 | + } |
| 313 | + while( cropWidth % 2 > 0 && cropWidth > 0 ) { |
| 314 | + cropWidth -= 1; |
| 315 | + } |
| 316 | + while( cropHeight % 2 > 0 && cropHeight < videoHeight ) { |
| 317 | + cropHeight += 1; |
| 318 | + } |
| 319 | + while( cropHeight % 2 > 0 && cropHeight > 0 ) { |
| 320 | + cropHeight -= 1; |
| 321 | + } |
| 322 | + |
| 323 | + // TODO: 1) ADD METHOD TO CHECK "IS FFMPEG LOADED". |
| 324 | + // 2) CHECK IT HERE |
| 325 | + // 3) EXPORT THAT METHOD TO "JS" |
| 326 | + |
| 327 | + final File tempFile = createTempFile("mp4", promise, ctx); |
| 328 | + |
| 329 | + ArrayList<String> cmd = new ArrayList<String>(); |
| 330 | + cmd.add("-y"); // NOTE: OVERWRITE OUTPUT FILE |
| 331 | + |
| 332 | + String startTime = options.getString("startTime"); |
| 333 | + if ( !startTime.equals(null) && !startTime.equals("") ) { |
| 334 | + cmd.add("-ss"); |
| 335 | + cmd.add(startTime); |
| 336 | + } |
| 337 | + |
| 338 | + // NOTE: INPUT FILE |
| 339 | + cmd.add("-i"); |
| 340 | + cmd.add(source); |
| 341 | + |
| 342 | + String endTime = options.getString("endTime"); |
| 343 | + if ( !endTime.equals(null) && !endTime.equals("") ) { |
| 344 | + cmd.add("-to"); |
| 345 | + cmd.add(endTime); |
| 346 | + } |
| 347 | + |
| 348 | + cmd.add("-vf"); |
| 349 | + cmd.add("crop=" + Integer.toString(cropWidth) + ":" + Integer.toString(cropHeight) + ":" + Integer.toString(cropOffsetX) + ":" + Integer.toString(cropOffsetY)); |
| 350 | + |
| 351 | + cmd.add("-preset"); |
| 352 | + cmd.add("ultrafast"); |
| 353 | + // NOTE: DO NOT CONVERT AUDIO TO SAVE TIME |
| 354 | + cmd.add("-c:a"); |
| 355 | + cmd.add("copy"); |
| 356 | + // NOTE: FLAG TO CONVER "AAC" AUDIO CODEC |
| 357 | + cmd.add("-strict"); |
| 358 | + cmd.add("-2"); |
| 359 | + // NOTE: OUTPUT FILE |
| 360 | + cmd.add(tempFile.getPath()); |
| 361 | + |
| 362 | + final String[] cmdToExec = cmd.toArray( new String[0] ); |
| 363 | + |
| 364 | + Log.d(LOG_TAG, Arrays.toString(cmdToExec)); |
| 365 | + |
| 366 | + try { |
| 367 | + FFmpeg.getInstance(ctx).execute(cmdToExec, new FFmpegExecuteResponseHandler() { |
| 368 | + |
| 369 | + @Override |
| 370 | + public void onStart() { |
| 371 | + Log.d(LOG_TAG, "crop: onStart"); |
| 372 | + } |
| 373 | + |
| 374 | + @Override |
| 375 | + public void onProgress(String message) { |
| 376 | + Log.d(LOG_TAG, "crop: onProgress"); |
| 377 | + } |
| 378 | + |
| 379 | + @Override |
| 380 | + public void onFailure(String message) { |
| 381 | + Log.d(LOG_TAG, "crop: onFailure"); |
| 382 | + promise.reject("Crop error: failed.", message); |
| 383 | + } |
| 384 | + |
| 385 | + @Override |
| 386 | + public void onSuccess(String message) { |
| 387 | + Log.d(LOG_TAG, "crop: onSuccess"); |
| 388 | + Log.d(LOG_TAG, message); |
| 389 | + |
| 390 | + WritableMap event = Arguments.createMap(); |
| 391 | + event.putString("source", "file://" + tempFile.getPath()); |
| 392 | + promise.resolve(event); |
| 393 | + } |
| 394 | + |
| 395 | + @Override |
| 396 | + public void onFinish() { |
| 397 | + Log.d(LOG_TAG, "crop: onFinish"); |
| 398 | + } |
| 399 | + }); |
| 400 | + } catch (Exception e) { |
| 401 | + promise.reject("Crop error. Command already running", e.toString()); |
| 402 | + } |
| 403 | + } |
| 404 | + |
| 405 | + public static void loadFfmpeg(ReactApplicationContext ctx){ |
| 406 | + try { |
| 407 | + FFmpeg.getInstance(ctx).loadBinary(new FFmpegLoadBinaryResponseHandler() { |
| 408 | + @Override |
| 409 | + public void onStart() { |
| 410 | + Log.d(LOG_TAG, "load FFMPEG: onStart"); |
| 411 | + } |
| 412 | + |
| 413 | + @Override |
| 414 | + public void onSuccess() { |
| 415 | + Log.d(LOG_TAG, "load FFMPEG: onSuccess"); |
| 416 | + ffmpegLoaded = true; |
| 417 | + } |
| 418 | + |
| 419 | + @Override |
| 420 | + public void onFailure() { |
| 421 | + ffmpegLoaded = false; |
| 422 | + Log.d(LOG_TAG, "load FFMPEG: Failed to load ffmpeg"); |
| 423 | + } |
| 424 | + |
| 425 | + @Override |
| 426 | + public void onFinish() { |
| 427 | + Log.d(LOG_TAG, "load FFMPEG: onFinish"); |
| 428 | + } |
| 429 | + }); |
| 430 | + } catch (Exception e){ |
| 431 | + ffmpegLoaded = false; |
| 432 | + Log.d("Failed to load ffmpeg", e.toString()); |
| 433 | + } |
| 434 | + } |
220 | 435 | } |
0 commit comments