Skip to content

Commit

Permalink
feat: Added imageSize and playableDuration to include param, delete…
Browse files Browse the repository at this point in the history
…s isStored (#187)

BREAKING CHANGE: imageSize and playableDuration are no longer included by default to improve performance
  • Loading branch information
hsource committed Jun 23, 2020
1 parent d21584a commit ec33d32
Show file tree
Hide file tree
Showing 6 changed files with 185 additions and 86 deletions.
15 changes: 8 additions & 7 deletions README.md
Expand Up @@ -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:

Expand All @@ -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}
Expand Down
Expand Up @@ -43,6 +43,7 @@

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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -506,13 +509,16 @@ private static void putEdges(
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);
Expand Down Expand Up @@ -540,6 +546,10 @@ private static void putBasicNodeInfo(
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,
Expand All @@ -550,97 +560,177 @@ private static boolean putImageInfo(
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")) {
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));
String strFileName = file.getName();
image.putString("filename", strFileName);
} else {
image.putNull("filename");
}

if (includeFileSize) {
image.putDouble("fileSize", media.getLong(sizeIndex));
} else {
image.putNull("fileSize");
}

node.putMap("image", image);
return putImageSizeSuccess && putPlayableDurationSuccess;
}

/**
* @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 {
AssetFileDescriptor photoDescriptor = resolver.openAssetFileDescriptor(photoUri, "r");
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
retriever.setDataSource(photoDescriptor.getFileDescriptor());
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();
}

try {
if (width <= 0 || height <= 0) {
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);
}
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();
} 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;
}
} 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;
// Do nothing. We can't handle this, and this is usually a system problem
}
}
image.putDouble("width", width);
image.putDouble("height", height);

if (includeFilename) {
File file = new File(media.getString(dataIndex));
String strFileName = file.getName();
image.putString("filename", strFileName);
} else {
image.putNull("filename");
}

if (includeFileSize) {
image.putDouble("fileSize", media.getLong(sizeIndex));
} else {
image.putNull("fileSize");
}

node.putMap("image", image);

return true;
image.putInt("width", width);
image.putInt("height", height);
return success;
}

private static void putLocationInfo(
Cursor media,
WritableMap node,
int dataIndex,
boolean includeLocation) {
node.putNull("location");

if (!includeLocation) {
node.putNull("location");
return;
}

Expand Down
5 changes: 2 additions & 3 deletions example/js/GetPhotosPerformanceExample.tsx
Expand Up @@ -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',
];

/**
Expand All @@ -45,7 +45,6 @@ export default class GetPhotosPerformanceExample extends React.PureComponent<
output: null,
include: [],
firstStr: '1000',
after: '',
};

first = () => {
Expand Down
18 changes: 7 additions & 11 deletions ios/RNCCameraRollManager.m
Expand Up @@ -260,6 +260,8 @@ static void RCTResolvePromise(RCTPromiseResolveBlock resolve,
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.
Expand Down Expand Up @@ -366,25 +368,19 @@ static void RCTResolvePromise(RCTPromiseResolveBlock resolve,
: @"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?
@"group_name": currentCollectionName,
@"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 ? @{
Expand Down
8 changes: 6 additions & 2 deletions js/CameraRoll.js
Expand Up @@ -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.
Expand Down Expand Up @@ -97,7 +102,6 @@ export type PhotoIdentifier = {
height: number,
width: number,
fileSize: number | null,
isStored?: boolean,
playableDuration: number,
},
timestamp: number,
Expand Down

0 comments on commit ec33d32

Please sign in to comment.