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