feat: Added imageSize and playableDuration to `include` param, deletes isStored (#187)

BREAKING CHANGE: imageSize and playableDuration are no longer included by default to improve performance
This commit is contained in:
Harry Yu 2020-06-22 22:45:20 -07:00 committed by GitHub
parent d21584ae4e
commit ec33d328af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 187 additions and 88 deletions

View File

@ -206,6 +206,8 @@ Returns a Promise with photo identifier objects from the local camera roll of th
* `filename` : Ensures `image.filename` is available in each node. This has a large performance impact on iOS.
* `fileSize` : Ensures `image.fileSize` is available in each node. This has a large performance impact on iOS.
* `location`: Ensures `location` is available in each node. This has a large performance impact on Android.
* `imageSize` : Ensures `image.width` and `image.height` are available in each node. This has a small performance impact on Android.
* `playableDuration` : Ensures `image.playableDuration` is available in each node. This has a medium peformance impact on Android.
Returns a Promise which when resolved will be of the following shape:
@ -215,13 +217,12 @@ Returns a Promise which when resolved will be of the following shape:
* `group_name`: {string}
* `image`: {object} : An object with the following shape:
* `uri`: {string}
* `filename`: {string | null} : Only set if the `include` parameter contains `filename`.
* `height`: {number}
* `width`: {number}
* `fileSize`: {number | null} : Only set if the `include` parameter contains `fileSize`.
* `isStored`: {boolean}
* `playableDuration`: {number}
* `timestamp`: {number} : Timestamp in seconds.
* `filename`: {string | null} : Only set if the `include` parameter contains `filename`
* `height`: {number | null} : Only set if the `include` parameter contains `imageSize`
* `width`: {number | null} : Only set if the `include` parameter contains `imageSize`
* `fileSize`: {number | null} : Only set if the `include` parameter contains `fileSize`
* `playableDuration`: {number | null} : Only set for videos if the `include` parameter contains `playableDuration`. Will be null for images.
* `timestamp`: {number}
* `location`: {object | null} : Only set if the `include` parameter contains `location`. An object with the following shape:
* `latitude`: {number}
* `longitude`: {number}

View File

@ -43,6 +43,7 @@ import com.facebook.react.module.annotations.ReactModule;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
@ -77,6 +78,8 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
private static final String INCLUDE_FILENAME = "filename";
private static final String INCLUDE_FILE_SIZE = "fileSize";
private static final String INCLUDE_LOCATION = "location";
private static final String INCLUDE_IMAGE_SIZE = "imageSize";
private static final String INCLUDE_PLAYABLE_DURATION = "playableDuration";
private static final String[] PROJECTION = {
Images.Media._ID,
@ -506,13 +509,16 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
boolean includeLocation = include.contains(INCLUDE_LOCATION);
boolean includeFilename = include.contains(INCLUDE_FILENAME);
boolean includeFileSize = include.contains(INCLUDE_FILE_SIZE);
boolean includeImageSize = include.contains(INCLUDE_IMAGE_SIZE);
boolean includePlayableDuration = include.contains(INCLUDE_PLAYABLE_DURATION);
for (int i = 0; i < limit && !media.isAfterLast(); i++) {
WritableMap edge = new WritableNativeMap();
WritableMap node = new WritableNativeMap();
boolean imageInfoSuccess =
putImageInfo(resolver, media, node, widthIndex, heightIndex, sizeIndex, dataIndex,
mimeTypeIndex, includeFilename, includeFileSize);
mimeTypeIndex, includeFilename, includeFileSize, includeImageSize,
includePlayableDuration);
if (imageInfoSuccess) {
putBasicNodeInfo(media, node, mimeTypeIndex, groupNameIndex, dateTakenIndex);
putLocationInfo(media, node, dataIndex, includeLocation);
@ -540,6 +546,10 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
node.putDouble("timestamp", media.getLong(dateTakenIndex) / 1000d);
}
/**
* @return Whether we successfully fetched all the information about the image that we were asked
* to include
*/
private static boolean putImageInfo(
ContentResolver resolver,
Cursor media,
@ -550,70 +560,19 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
int dataIndex,
int mimeTypeIndex,
boolean includeFilename,
boolean includeFileSize) {
boolean includeFileSize,
boolean includeImageSize,
boolean includePlayableDuration) {
WritableMap image = new WritableNativeMap();
Uri photoUri = Uri.parse("file://" + media.getString(dataIndex));
image.putString("uri", photoUri.toString());
float width = media.getInt(widthIndex);
float height = media.getInt(heightIndex);
String mimeType = media.getString(mimeTypeIndex);
if (mimeType != null
&& mimeType.startsWith("video")) {
try {
AssetFileDescriptor photoDescriptor = resolver.openAssetFileDescriptor(photoUri, "r");
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
retriever.setDataSource(photoDescriptor.getFileDescriptor());
try {
if (width <= 0 || height <= 0) {
width =
Integer.parseInt(
retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH));
height =
Integer.parseInt(
retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT));
}
int timeInMillisec =
Integer.parseInt(
retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION));
int playableDuration = timeInMillisec / 1000;
image.putInt("playableDuration", playableDuration);
} catch (NumberFormatException e) {
FLog.e(
ReactConstants.TAG,
"Number format exception occurred while trying to fetch video metadata for "
+ photoUri.toString(),
e);
return false;
} finally {
retriever.release();
photoDescriptor.close();
}
} catch (Exception e) {
FLog.e(ReactConstants.TAG, "Could not get video metadata for " + photoUri.toString(), e);
return false;
}
}
if (width <= 0 || height <= 0) {
try {
AssetFileDescriptor photoDescriptor = resolver.openAssetFileDescriptor(photoUri, "r");
BitmapFactory.Options options = new BitmapFactory.Options();
// Set inJustDecodeBounds to true so we don't actually load the Bitmap, but only get its
// dimensions instead.
options.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(photoDescriptor.getFileDescriptor(), null, options);
width = options.outWidth;
height = options.outHeight;
photoDescriptor.close();
} catch (IOException e) {
FLog.e(ReactConstants.TAG, "Could not get width/height for " + photoUri.toString(), e);
return false;
}
}
image.putDouble("width", width);
image.putDouble("height", height);
boolean isVideo = mimeType != null && mimeType.startsWith("video");
boolean putImageSizeSuccess = putImageSize(resolver, media, image, widthIndex, heightIndex,
photoUri, isVideo, includeImageSize);
boolean putPlayableDurationSuccess = putPlayableDuration(resolver, image, photoUri, isVideo,
includePlayableDuration);
if (includeFilename) {
File file = new File(media.getString(dataIndex));
@ -630,8 +589,138 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
}
node.putMap("image", image);
return putImageSizeSuccess && putPlayableDurationSuccess;
}
return true;
/**
* @return Whether we succeeded in fetching and putting the playableDuration
*/
private static boolean putPlayableDuration(
ContentResolver resolver,
WritableMap image,
Uri photoUri,
boolean isVideo,
boolean includePlayableDuration) {
image.putNull("playableDuration");
if (!includePlayableDuration || !isVideo) {
return true;
}
boolean success = true;
@Nullable Integer playableDuration = null;
@Nullable AssetFileDescriptor photoDescriptor = null;
try {
photoDescriptor = resolver.openAssetFileDescriptor(photoUri, "r");
} catch (FileNotFoundException e) {
success = false;
FLog.e(ReactConstants.TAG, "Could not open asset file " + photoUri.toString(), e);
}
if (photoDescriptor != null) {
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
retriever.setDataSource(photoDescriptor.getFileDescriptor());
try {
int timeInMillisec =
Integer.parseInt(
retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION));
playableDuration = timeInMillisec / 1000;
} catch (NumberFormatException e) {
success = false;
FLog.e(
ReactConstants.TAG,
"Number format exception occurred while trying to fetch video metadata for "
+ photoUri.toString(),
e);
}
retriever.release();
}
if (photoDescriptor != null) {
try {
photoDescriptor.close();
} catch (IOException e) {
// Do nothing. We can't handle this, and this is usually a system problem
}
}
if (playableDuration != null) {
image.putInt("playableDuration", playableDuration);
}
return success;
}
private static boolean putImageSize(
ContentResolver resolver,
Cursor media,
WritableMap image,
int widthIndex,
int heightIndex,
Uri photoUri,
boolean isVideo,
boolean includeImageSize) {
image.putNull("width");
image.putNull("height");
if (!includeImageSize) {
return true;
}
boolean success = true;
int width = media.getInt(widthIndex);
int height = media.getInt(heightIndex);
if (width <= 0 || height <= 0) {
@Nullable AssetFileDescriptor photoDescriptor = null;
try {
photoDescriptor = resolver.openAssetFileDescriptor(photoUri, "r");
} catch (FileNotFoundException e) {
success = false;
FLog.e(ReactConstants.TAG, "Could not open asset file " + photoUri.toString(), e);
}
if (photoDescriptor != null) {
if (isVideo) {
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
retriever.setDataSource(photoDescriptor.getFileDescriptor());
try {
width =
Integer.parseInt(
retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH));
height =
Integer.parseInt(
retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT));
} catch (NumberFormatException e) {
success = false;
FLog.e(
ReactConstants.TAG,
"Number format exception occurred while trying to fetch video metadata for "
+ photoUri.toString(),
e);
}
retriever.release();
} else {
BitmapFactory.Options options = new BitmapFactory.Options();
// Set inJustDecodeBounds to true so we don't actually load the Bitmap, but only get its
// dimensions instead.
options.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(photoDescriptor.getFileDescriptor(), null, options);
width = options.outWidth;
height = options.outHeight;
}
}
try {
photoDescriptor.close();
} catch (IOException e) {
// Do nothing. We can't handle this, and this is usually a system problem
}
}
image.putInt("width", width);
image.putInt("height", height);
return success;
}
private static void putLocationInfo(
@ -639,8 +728,9 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
WritableMap node,
int dataIndex,
boolean includeLocation) {
node.putNull("location");
if (!includeLocation) {
node.putNull("location");
return;
}

View File

@ -21,14 +21,14 @@ interface State {
* with `this.first()` before using.
*/
firstStr: string;
/** `after` passed into `getPhotos`. Not passed if empty */
after: string;
}
const includeValues: CameraRoll.Include[] = [
'filename',
'fileSize',
'location',
'imageSize',
'playableDuration',
];
/**
@ -45,7 +45,6 @@ export default class GetPhotosPerformanceExample extends React.PureComponent<
output: null,
include: [],
firstStr: '1000',
after: '',
};
first = () => {

View File

@ -260,6 +260,8 @@ RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params
BOOL __block includeFilename = [include indexOfObject:@"filename"] != NSNotFound;
BOOL __block includeFileSize = [include indexOfObject:@"fileSize"] != NSNotFound;
BOOL __block includeLocation = [include indexOfObject:@"location"] != NSNotFound;
BOOL __block includeImageSize = [include indexOfObject:@"imageSize"] != NSNotFound;
BOOL __block includePlayableDuration = [include indexOfObject:@"playableDuration"] != NSNotFound;
// If groupTypes is "all", we want to fetch the SmartAlbum "all photos". Otherwise, all
// other groupTypes values require the "album" collection type.
@ -366,13 +368,6 @@ RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params
: @"unknown")));
CLLocation *const loc = asset.location;
// A note on isStored: in the previous code that used ALAssets, isStored
// was always set to YES, probably because iCloud-synced images were never returned (?).
// To get the "isStored" information and filename, we would need to actually request the
// image data from the image manager. Those operations could get really expensive and
// would definitely utilize the disk too much.
// Thus, this field is actually not reliable.
// Note that Android also does not return the `isStored` field at all.
[assets addObject:@{
@"node": @{
@"type": assetMediaTypeLabel, // TODO: switch to mimeType?
@ -380,11 +375,12 @@ RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params
@"image": @{
@"uri": uri,
@"filename": (includeFilename && originalFilename ? originalFilename : [NSNull null]),
@"height": @([asset pixelHeight]),
@"width": @([asset pixelWidth]),
@"height": (includeImageSize ? @([asset pixelHeight]) : [NSNull null]),
@"width": (includeImageSize ? @([asset pixelWidth]) : [NSNull null]),
@"fileSize": (includeFileSize ? fileSize : [NSNull null]),
@"isStored": @YES, // this field doesn't seem to exist on android
@"playableDuration": @([asset duration]) // fractional seconds
@"playableDuration": (includePlayableDuration && asset.mediaType != PHAssetMediaTypeImage
? @([asset duration]) // fractional seconds
: [NSNull null])
},
@"timestamp": @(asset.creationDate.timeIntervalSince1970),
@"location": (includeLocation && loc ? @{

View File

@ -31,7 +31,12 @@ const ASSET_TYPE_OPTIONS = {
export type GroupTypes = $Keys<typeof GROUP_TYPES_OPTIONS>;
export type Include = 'filename' | 'fileSize' | 'location';
export type Include =
| 'filename'
| 'fileSize'
| 'location'
| 'imageSize'
| 'playableDuration';
/**
* Shape of the param arg for the `getPhotos` function.
@ -97,7 +102,6 @@ export type PhotoIdentifier = {
height: number,
width: number,
fileSize: number | null,
isStored?: boolean,
playableDuration: number,
},
timestamp: number,

View File

@ -23,7 +23,11 @@ declare namespace CameraRoll {
/** Ensures the fileSize is included. Has a large performance hit on iOS */
| 'fileSize'
/** Ensures the location is included. Has a medium performance hit on Android */
| 'location';
| 'location'
/** Ensures the image width and height are included. Has a small performance hit on Android */
| 'imageSize'
/** Ensures the image playableDuration is included. Has a medium performance hit on Android */
| 'playableDuration';
/**
* Shape of the param arg for the `getPhotosFast` function.
@ -92,12 +96,17 @@ declare namespace CameraRoll {
/** Only set if the `include` parameter contains `filename`. */
filename: string | null;
uri: string;
/** Only set if the `include` parameter contains `imageSize`. */
height: number;
/** Only set if the `include` parameter contains `imageSize`. */
width: number;
/** Only set if the `include` parameter contains `fileSize`. */
fileSize: number | null;
isStored?: boolean;
playableDuration: number;
/**
* Only set if the `include` parameter contains `playableDuration`.
* Will be null for images.
*/
playableDuration: number | null;
};
/** Timestamp in seconds. */
timestamp: number;