1 package abi47_0_0.expo.modules.videothumbnails; 2 3 import android.content.Context; 4 import android.graphics.Bitmap; 5 import android.media.MediaMetadataRetriever; 6 import android.net.Uri; 7 import android.os.AsyncTask; 8 import android.os.Bundle; 9 import android.webkit.URLUtil; 10 11 import abi47_0_0.expo.modules.core.ExportedModule; 12 import abi47_0_0.expo.modules.core.ModuleRegistry; 13 import abi47_0_0.expo.modules.core.Promise; 14 import abi47_0_0.expo.modules.core.arguments.ReadableArguments; 15 import abi47_0_0.expo.modules.core.interfaces.ExpoMethod; 16 import abi47_0_0.expo.modules.core.utilities.FileUtilities; 17 18 import java.io.File; 19 import java.io.FileDescriptor; 20 import java.io.FileInputStream; 21 import java.io.FileOutputStream; 22 import java.io.IOException; 23 import java.io.OutputStream; 24 import java.util.HashMap; 25 26 import abi47_0_0.expo.modules.interfaces.filesystem.FilePermissionModuleInterface; 27 import abi47_0_0.expo.modules.interfaces.filesystem.Permission; 28 29 public class VideoThumbnailsModule extends ExportedModule { 30 private static final String TAG = "ExpoVideoThumbnails"; 31 private static final String ERROR_TAG = "E_VIDEO_THUMBNAILS"; 32 private static String ERR_COULD_NOT_GET_THUMBNAIL = "ERR_COULD_NOT_GET_THUMBNAIL"; 33 34 private static final String KEY_QUALITY = "quality"; 35 private static final String KEY_TIME = "time"; 36 private static final String KEY_HEADERS = "headers"; 37 38 private ModuleRegistry mModuleRegistry; 39 VideoThumbnailsModule(Context context)40 public VideoThumbnailsModule(Context context) { 41 super(context); 42 } 43 44 @Override getName()45 public String getName() { 46 return TAG; 47 } 48 49 @Override onCreate(ModuleRegistry moduleRegistry)50 public void onCreate(ModuleRegistry moduleRegistry) { 51 mModuleRegistry = moduleRegistry; 52 } 53 54 private static class GetThumbnailAsyncTask extends AsyncTask<Void, Void, Bitmap> { 55 private String mSourceFilename; 56 private ReadableArguments mVideoOptions; 57 private Context mContext; 58 Exception mError; 59 GetThumbnailAsyncTask(String sourceFilename, ReadableArguments videoOptions, Context context)60 GetThumbnailAsyncTask(String sourceFilename, ReadableArguments videoOptions, Context context) { 61 mSourceFilename = sourceFilename; 62 mVideoOptions = videoOptions; 63 mContext = context; 64 } 65 66 @Override doInBackground(Void... voids)67 protected final Bitmap doInBackground(Void... voids) { 68 long time = mVideoOptions.getInt(KEY_TIME, 0) * 1000; 69 MediaMetadataRetriever retriever = new MediaMetadataRetriever(); 70 try { 71 if (URLUtil.isFileUrl(mSourceFilename)) { 72 retriever.setDataSource(Uri.decode(mSourceFilename).replace("file://", "")); 73 } else if (URLUtil.isContentUrl(mSourceFilename)) { 74 Uri fileUri = Uri.parse(mSourceFilename); 75 FileDescriptor fileDescriptor = mContext.getContentResolver().openFileDescriptor(fileUri, "r").getFileDescriptor(); 76 FileInputStream inputStream = new FileInputStream(fileDescriptor); 77 retriever.setDataSource(inputStream.getFD()); 78 } else { 79 retriever.setDataSource(mSourceFilename, mVideoOptions.getMap(KEY_HEADERS, new HashMap<String, String>())); 80 } 81 } catch (IOException | RuntimeException e) { 82 mError = e; 83 return null; 84 } 85 86 return retriever.getFrameAtTime(time, MediaMetadataRetriever.OPTION_CLOSEST_SYNC); 87 } 88 } 89 isAllowedToRead(String url)90 private boolean isAllowedToRead(String url) { 91 if (mModuleRegistry != null) { 92 FilePermissionModuleInterface permissionModuleInterface = mModuleRegistry.getModule(FilePermissionModuleInterface.class); 93 if (permissionModuleInterface != null) { 94 return permissionModuleInterface.getPathPermissions(getContext(), url).contains(Permission.READ); 95 } 96 } 97 return true; 98 } 99 100 @ExpoMethod getThumbnail(String sourceFilename, final ReadableArguments videoOptions, final Promise promise)101 public void getThumbnail(String sourceFilename, final ReadableArguments videoOptions, final Promise promise) { 102 if (URLUtil.isFileUrl(sourceFilename) && !isAllowedToRead(Uri.decode(sourceFilename).replace("file://", ""))) { 103 promise.reject(ERROR_TAG, "Can't read file"); 104 return; 105 } 106 107 GetThumbnailAsyncTask getThumbnailAsyncTask = new GetThumbnailAsyncTask(sourceFilename, videoOptions, getContext()) { 108 @Override 109 protected void onPostExecute(Bitmap thumbnail) { 110 if (thumbnail == null || mError != null) { 111 String errorMessage = "Could not generate thumbnail."; 112 if (mError != null) { 113 errorMessage = String.format("%s %s", errorMessage, mError.getMessage()); 114 } 115 promise.reject(ERR_COULD_NOT_GET_THUMBNAIL, errorMessage, mError); 116 return; 117 } 118 try { 119 String path = FileUtilities.generateOutputPath(getContext().getCacheDir(), "VideoThumbnails", "jpg"); 120 OutputStream outputStream = new FileOutputStream(path); 121 thumbnail.compress(Bitmap.CompressFormat.JPEG, (int) (videoOptions.getDouble(KEY_QUALITY, 1) * 100), outputStream); 122 outputStream.flush(); 123 outputStream.close(); 124 Bundle response = new Bundle(); 125 response.putString("uri", Uri.fromFile(new File(path)).toString()); 126 response.putInt("width", thumbnail.getWidth()); 127 response.putInt("height", thumbnail.getHeight()); 128 promise.resolve(response); 129 } catch (IOException ex) { 130 promise.reject(ERROR_TAG, ex); 131 } 132 } 133 }; 134 getThumbnailAsyncTask.execute(); 135 } 136 } 137