Skip to content

Commit

Permalink
✨ Enables getting the duration of a Live Photo (#1212)
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexV525 authored Oct 30, 2024
1 parent a76f2e5 commit 18b3865
Show file tree
Hide file tree
Showing 9 changed files with 137 additions and 36 deletions.
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ To know more about breaking changes, see the [Migration Guide][].

## Unreleased

*None.*
### Features

- Allows to get the duration of a Live Photo with `AssetEntity.durationWithOptions` on iOS and macOS.

### Improvements

- Improves the options when fetching fixed number of assets on iOS and macOS.

## 3.5.2

Expand Down
12 changes: 11 additions & 1 deletion example/lib/page/image_list_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -164,11 +164,16 @@ class _GalleryContentListPageState extends State<GalleryContentListPage> {
child: const Text('Get file'),
onPressed: () => getFile(entity),
),
if (entity.type == AssetType.video || entity.isLivePhoto)
if (entity.isLivePhoto)
ElevatedButton(
child: const Text('Get MP4 file'),
onPressed: () => getFileWithMP4(entity),
),
if (entity.isLivePhoto)
ElevatedButton(
child: const Text('Get Live Photo duration'),
onPressed: () => getDurationOfLivePhoto(entity),
),
ElevatedButton(
child: const Text('Show detail page'),
onPressed: () => routeToDetailPage(entity),
Expand Down Expand Up @@ -266,6 +271,11 @@ class _GalleryContentListPageState extends State<GalleryContentListPage> {
print(file);
}

Future<void> getDurationOfLivePhoto(AssetEntity entity) async {
final duration = await entity.durationWithOptions(withSubtype: true);
print(duration);
}

Future<void> routeToDetailPage(AssetEntity entity) async {
Navigator.of(context).push<void>(
MaterialPageRoute<void>(builder: (_) => DetailPage(entity: entity)),
Expand Down
4 changes: 4 additions & 0 deletions ios/Classes/PMPlugin.m
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,10 @@ - (void)handleMethodResultHandler:(ResultHandler *)handler manager:(PMManager *)
subtype:subtype
fileType:fileType];
[handler reply:@(exists)];
} else if ([call.method isEqualToString:@"getDurationWithOptions"]) {
NSString *assetId = call.arguments[@"id"];
int subtype = [call.arguments[@"subtype"] intValue];
[manager getDurationWithOptions:assetId subtype:subtype resultHandler:handler];
} else if ([call.method isEqualToString:@"getTitleAsync"]) {
NSString *assetId = call.arguments[@"id"];
int subtype = [call.arguments[@"subtype"] intValue];
Expand Down
3 changes: 3 additions & 0 deletions ios/Classes/core/PHAsset+PM_COMMON.m
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ - (bool)isLivePhoto {
if (@available(iOS 9.1, *)) {
return (self.mediaSubtypes & PHAssetMediaSubtypePhotoLive) == PHAssetMediaSubtypePhotoLive;
}
if (@available(macOS 14.0, *)) {
return (self.mediaSubtypes & PHAssetMediaSubtypePhotoLive) == PHAssetMediaSubtypePhotoLive;
}
return NO;
}

Expand Down
10 changes: 7 additions & 3 deletions ios/Classes/core/PMManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ typedef void (^AssetBlockResult)(PMAssetEntity *, NSObject *);
subtype:(int)subtype
fileType:(AVFileType)fileType;

- (void)getDurationWithOptions:(NSString *)assetId
subtype:(int)subtype
resultHandler:(NSObject <PMResultHandler> *)handler;

- (NSString*)getTitleAsyncWithAssetId:(NSString *)assetId
subtype:(int)subtype
isOrigin:(BOOL)isOrigin
Expand All @@ -101,21 +105,21 @@ typedef void (^AssetBlockResult)(PMAssetEntity *, NSObject *);

- (NSArray<PMAssetPathEntity *> *)getSubPathWithId:(NSString *)id type:(int)type albumType:(int)albumType option:(NSObject<PMBaseFilter> *)option;

- (void)copyAssetWithId:(NSString *)id toGallery:(NSString *)gallery block:(void (^)(PMAssetEntity *entity, NSObject *msg))block;
- (void)copyAssetWithId:(NSString *)id toGallery:(NSString *)gallery block:(AssetBlockResult)block;

- (void)createFolderWithName:(NSString *)name parentId:(NSString *)id block:(void (^)(NSString *newId, NSObject *error))block;

- (void)createAlbumWithName:(NSString *)name parentId:(NSString *)id block:(void (^)(NSString *newId, NSObject *error))block;

- (void)removeInAlbumWithAssetId:(NSArray *)id albumId:(NSString *)albumId block:(void (^)(NSObject *error))block;
- (void)removeInAlbumWithAssetId:(NSArray *)ids albumId:(NSString *)albumId block:(void (^)(NSObject *error))block;

- (void)removeCollectionWithId:(NSString *)id type:(int)type block:(void (^)(NSObject *))block;

- (void)favoriteWithId:(NSString *)id favorite:(BOOL)favorite block:(void (^)(BOOL result, NSObject *))block;

- (void)clearFileCache;

- (void)requestCacheAssetsThumb:(NSArray *)identifiers option:(PMThumbLoadOption *)option;
- (void)requestCacheAssetsThumb:(NSArray *)ids option:(PMThumbLoadOption *)option;

- (void)cancelCacheRequests;

Expand Down
108 changes: 77 additions & 31 deletions ios/Classes/core/PMManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ - (PHCachingImageManager *)cachingManager {
return __cachingManager;
}

- (PHFetchOptions *)singleFetchOptions {
PHFetchOptions *options = [PHFetchOptions new];
options.fetchLimit = 1;
return options;
}

- (NSArray<PMAssetPathEntity *> *)getAssetPathList:(int)type hasAll:(BOOL)hasAll onlyAll:(BOOL)onlyAll option:(NSObject <PMBaseFilter> *)option pathFilterOption:(PMPathFilterOption *)pathFilterOption {
NSMutableArray<PMAssetPathEntity *> *array = [NSMutableArray new];
PHFetchOptions *assetOptions = [self getAssetOptions:type filterOption:option];
Expand Down Expand Up @@ -147,17 +153,16 @@ - (NSUInteger)getAssetCountWithType:(int)type option:(NSObject<PMBaseFilter> *)f
}

- (BOOL)existsWithId:(NSString *)assetId {
PHFetchResult<PHAsset *> *result =
[PHAsset fetchAssetsWithLocalIdentifiers:@[assetId] options:[PHFetchOptions new]];
return result && result.count > 0;
PHFetchResult<PHAsset *> *result = [PHAsset fetchAssetsWithLocalIdentifiers:@[assetId] options:[self singleFetchOptions]];
return result && result.count == 1;
}

- (BOOL)entityIsLocallyAvailable:(NSString *)assetId
resource:(PHAssetResource *)resource
isOrigin:(BOOL)isOrigin
subtype:(int)subtype
fileType:(AVFileType)fileType {
PHFetchResult<PHAsset *> *result = [PHAsset fetchAssetsWithLocalIdentifiers:@[assetId] options:[PHFetchOptions new]];
PHFetchResult<PHAsset *> *result = [PHAsset fetchAssetsWithLocalIdentifiers:@[assetId] options:[self singleFetchOptions]];
if (!result) {
return NO;
}
Expand Down Expand Up @@ -403,13 +408,11 @@ - (PMAssetEntity *)getAssetEntity:(NSString *)assetId withCache:(BOOL)withCache
return entity;
}
}
PHFetchResult<PHAsset *> *result =
[PHAsset fetchAssetsWithLocalIdentifiers:@[assetId] options:nil];
if (result == nil || result.count == 0) {
PHFetchResult *fetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:@[assetId] options:[self singleFetchOptions]];
PHAsset *asset = [self getFirstObjFromFetchResult:fetchResult];
if (!asset) {
return nil;
}

PHAsset *asset = result[0];
entity = [self convertPHAssetToAssetEntity:asset needTitle:NO];
[cacheContainer putAssetEntity:entity];
return entity;
Expand Down Expand Up @@ -895,7 +898,7 @@ - (NSString *)makeAssetOutputPath:(PHAsset *)asset
if (resource) {
filename = resource.originalFilename;
} else {
filename = [asset valueForKey:@"filename"];
filename = [asset title];
}
filename = [NSString stringWithFormat:@"%@_%@%@_%@",
id, modifiedDate, isOrigin ? @"_o" : @"", filename];
Expand Down Expand Up @@ -1139,9 +1142,9 @@ + (void)openSetting:(NSObject<PMResultHandler>*)result {
- (void)deleteWithIds:(NSArray<NSString *> *)ids changedBlock:(ChangeIds)block {
[[PHPhotoLibrary sharedPhotoLibrary]
performChanges:^{
PHFetchResult<PHAsset *> *result =
[PHAsset fetchAssetsWithLocalIdentifiers:ids
options:[PHFetchOptions new]];
PHFetchOptions *options = [PHFetchOptions new];
options.fetchLimit = ids.count;
PHFetchResult<PHAsset *> *result = [PHAsset fetchAssetsWithLocalIdentifiers:ids options:options];
[PHAssetChangeRequest deleteAssets:result];
}
completionHandler:^(BOOL success, NSError *error) {
Expand Down Expand Up @@ -1281,19 +1284,58 @@ - (void)saveLivePhoto:(NSString *)imagePath
}];
}

- (void)getDurationWithOptions:(NSString *)assetId
subtype:(int)subtype
resultHandler:(NSObject<PMResultHandler> *)handler {
PMAssetEntity *entity = [self getAssetEntity:assetId];
if (!entity) {
[handler replyError:@"Not exists."];
return;
}
PHAsset *asset = entity.phAsset;
if (!asset) {
[handler replyError:@"Not exists."];
return;
}

if (asset.isLivePhoto) {
PHContentEditingInputRequestOptions *options = [PHContentEditingInputRequestOptions new];
options.networkAccessAllowed = YES;
[asset requestContentEditingInputWithOptions:options completionHandler:^(PHContentEditingInput * _Nullable contentEditingInput, NSDictionary * _Nonnull info) {
if (!contentEditingInput) {
[handler replyError:@"Failed to obtain the content request."];
return;
}
PHLivePhotoEditingContext *context = [[PHLivePhotoEditingContext alloc] initWithLivePhotoEditingInput:contentEditingInput];
if (!context) {
[handler replyError:@"Failed to obtain the Live Photo's context."];
return;
}
NSTimeInterval time = CMTimeGetSeconds(context.duration);
[handler reply:@(time)];
}];
return;
}

[handler reply:@(entity.duration)];
return;
}


- (NSString *)getTitleAsyncWithAssetId:(NSString *)assetId
subtype:(int)subtype
isOrigin:(BOOL)isOrigin
fileType:(AVFileType)fileType {
PHAsset *asset = [PHAsset fetchAssetsWithLocalIdentifiers:@[assetId] options:nil].firstObject;
PHFetchResult *fetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:@[assetId] options:[self singleFetchOptions]];
PHAsset *asset = [self getFirstObjFromFetchResult:fetchResult];
if (asset) {
return [asset filenameWithOptions:subtype isOrigin:isOrigin fileType:fileType];
}
return @"";
}

- (NSString *)getMimeTypeAsyncWithAssetId:(NSString *)assetId {
PHFetchResult *fetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:@[assetId] options:nil];
PHFetchResult *fetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:@[assetId] options:[self singleFetchOptions]];
PHAsset *asset = [self getFirstObjFromFetchResult:fetchResult];
if (asset) {
return [asset mimeType];
Expand All @@ -1304,7 +1346,7 @@ - (NSString *)getMimeTypeAsyncWithAssetId:(NSString *)assetId {
- (void)getMediaUrl:(NSString *)assetId
resultHandler:(NSObject <PMResultHandler> *)handler
progressHandler:(NSObject <PMProgressHandlerProtocol> *)progressHandler {
PHAsset *asset = [PHAsset fetchAssetsWithLocalIdentifiers:@[assetId] options:nil].firstObject;
PHAsset *asset = [PHAsset fetchAssetsWithLocalIdentifiers:@[assetId] options:[self singleFetchOptions]].firstObject;

if (@available(iOS 9.1, *)) {
if ((asset.mediaSubtypes & PHAssetMediaSubtypePhotoLive) == PHAssetMediaSubtypePhotoLive) {
Expand Down Expand Up @@ -1425,20 +1467,17 @@ - (void)copyAssetWithId:(NSString *)id
return;
}

__block PHFetchResult<PHAsset *> *asset = [PHAsset fetchAssetsWithLocalIdentifiers:@[id] options:nil];
__block PHFetchResult<PHAsset *> *asset = [PHAsset fetchAssetsWithLocalIdentifiers:@[id] options:[self singleFetchOptions]];
NSError *error;

[PHPhotoLibrary.sharedPhotoLibrary performChangesAndWait:^{
PHAssetCollectionChangeRequest *request = [PHAssetCollectionChangeRequest changeRequestForAssetCollection:collection];
[request addAssets:asset];
} error:&error];

if (error) {
block(nil, error);
return;
} else {
block(assetEntity, nil);
}

block(assetEntity, nil);
}

- (void)createFolderWithName:(NSString *)name parentId:(NSString *)id block:(void (^)(NSString *newId, NSObject *error))block {
Expand Down Expand Up @@ -1541,7 +1580,7 @@ - (void)createAlbumWithName:(NSString *)name parentId:(NSString *)id block:(void
}
}

- (void)removeInAlbumWithAssetId:(NSArray *)id albumId:(NSString *)albumId block:(void (^)(NSObject *error))block {
- (void)removeInAlbumWithAssetId:(NSArray *)ids albumId:(NSString *)albumId block:(void (^)(NSObject *error))block {
PHFetchResult<PHAssetCollection *> *result = [PHAssetCollection fetchAssetCollectionsWithLocalIdentifiers:@[albumId] options:nil];
PHAssetCollection *collection;
if (result && result.count > 0) {
Expand All @@ -1556,7 +1595,9 @@ - (void)removeInAlbumWithAssetId:(NSArray *)id albumId:(NSString *)albumId block
return;
}

PHFetchResult<PHAsset *> *assetResult = [PHAsset fetchAssetsWithLocalIdentifiers:id options:nil];
PHFetchOptions *options = [PHFetchOptions new];
options.fetchLimit = ids.count;
PHFetchResult<PHAsset *> *assetResult = [PHAsset fetchAssetsWithLocalIdentifiers:ids options:options];
NSError *error;
[PHPhotoLibrary.sharedPhotoLibrary
performChangesAndWait:^{
Expand Down Expand Up @@ -1626,7 +1667,7 @@ - (void)removeCollectionWithId:(NSString *)id type:(int)type block:(void (^)(NSO
}

- (void)favoriteWithId:(NSString *)id favorite:(BOOL)favorite block:(void (^)(BOOL result, NSObject *error))block {
PHFetchResult *fetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:@[id] options:nil];
PHFetchResult *fetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:@[id] options:[self singleFetchOptions]];
PHAsset *asset = [self getFirstObjFromFetchResult:fetchResult];
if (!asset) {
block(NO, [NSString stringWithFormat:@"Asset %@ not found.", id]);
Expand Down Expand Up @@ -1688,19 +1729,24 @@ - (void)clearFileCache {

#pragma mark cache thumb

- (void)requestCacheAssetsThumb:(NSArray *)identifiers option:(PMThumbLoadOption *)option {
PHFetchResult<PHAsset *> *fetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:identifiers options:nil];
- (void)requestCacheAssetsThumb:(NSArray *)ids option:(PMThumbLoadOption *)option {
PHFetchOptions *fetchOptions = [PHFetchOptions new];
fetchOptions.fetchLimit = ids.count;
PHFetchResult<PHAsset *> *fetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:ids options:fetchOptions];
NSMutableArray *array = [NSMutableArray new];

for (id asset in fetchResult) {
[array addObject:asset];
}

PHImageRequestOptions *options = [PHImageRequestOptions new];
options.resizeMode = options.resizeMode;
options.deliveryMode = option.deliveryMode;
PHImageRequestOptions *requestOptions = [PHImageRequestOptions new];
requestOptions.resizeMode = option.resizeMode;
requestOptions.deliveryMode = option.deliveryMode;

[self.cachingManager startCachingImagesForAssets:array targetSize:[option makeSize] contentMode:option.contentMode options:options];
[self.cachingManager startCachingImagesForAssets:array
targetSize:[option makeSize]
contentMode:option.contentMode
options:requestOptions];
}

- (void)cancelCacheRequests {
Expand Down
1 change: 1 addition & 0 deletions lib/src/internal/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class PMConstants {
static const String mCancelCacheRequests = 'cancelCacheRequests';
static const String mRequestCacheAssetsThumb = 'requestCacheAssetsThumb';
static const String mIsLocallyAvailable = 'isLocallyAvailable';
static const String mGetDurationWithOptions = 'getDurationWithOptions';
static const String mCreateAlbum = 'createAlbum';
static const String mCreateFolder = 'createFolder';
static const String mRemoveInAlbum = 'removeInAlbum';
Expand Down
17 changes: 17 additions & 0 deletions lib/src/internal/plugin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,23 @@ class PhotoManagerPlugin with BasePlugin, IosPlugin, AndroidPlugin, OhosPlugin {
return ConvertUtils.convertToAssetList(result.cast());
}

Future<int> getDurationWithOptions(String id, {int? subtype}) async {
if (Platform.isIOS || Platform.isMacOS) {
if (subtype != null) {
final result = await _channel.invokeMethod(
PMConstants.mGetDurationWithOptions,
<String, dynamic>{
'id': id,
'subtype': subtype,
},
);
return result as int;
}
}
final entity = await AssetEntity.fromId(id);
return entity!.duration;
}

Future<bool> isLocallyAvailable(
String id, {
bool isOrigin = false,
Expand Down
10 changes: 10 additions & 0 deletions lib/src/types/entity.dart
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,16 @@ class AssetEntity {
/// * [videoDuration] which is a duration getter for videos.
final int duration;

/// Obtain the duration with the given options.
///
/// [withSubtype] only works on iOS/macOS.
Future<int> durationWithOptions({bool withSubtype = false}) async {
if (withSubtype) {
return plugin.getDurationWithOptions(id, subtype: subtype);
}
return duration;
}

/// The width of the asset.
///
/// This field could be 0 in cases that EXIF info is failed to parse.
Expand Down

0 comments on commit 18b3865

Please sign in to comment.