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:
parent
d21584ae4e
commit
ec33d328af
15
README.md
15
README.md
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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 ? @{
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue