@technotoil/image-video-editor 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ImageVideoEditor.podspec +21 -0
- package/README.md +136 -0
- package/android/build.gradle +76 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +13 -0
- package/android/src/main/java/com/technotoil/image_videoeditor/FrameGrabberModule.kt +67 -0
- package/android/src/main/java/com/technotoil/image_videoeditor/MediaEditorModule.kt +548 -0
- package/android/src/main/java/com/technotoil/image_videoeditor/MediaFileUtils.kt +29 -0
- package/android/src/main/java/com/technotoil/image_videoeditor/MediaLibraryModule.kt +305 -0
- package/android/src/main/java/com/technotoil/image_videoeditor/MediaPackage.kt +26 -0
- package/android/src/main/java/com/technotoil/image_videoeditor/MediaPickerModule.kt +111 -0
- package/android/src/main/java/com/technotoil/image_videoeditor/MediaPlayerModule.kt +34 -0
- package/android/src/main/java/com/technotoil/image_videoeditor/RNCameraViewManager.kt +761 -0
- package/android/src/main/java/com/technotoil/image_videoeditor/RNVideoPreviewManager.kt +317 -0
- package/ios/PrivacyInfo.xcprivacy +38 -0
- package/ios/RNCameraViewManager.m +420 -0
- package/ios/RNFrameGrabber.m +61 -0
- package/ios/RNMediaEditor.m +905 -0
- package/ios/RNMediaLibrary.m +389 -0
- package/ios/RNMediaPicker.m +144 -0
- package/ios/RNMediaPlayer.m +73 -0
- package/ios/RNVideoPreviewManager.m +263 -0
- package/ios/frames/film_vintage.png +0 -0
- package/ios/frames/floral_gold.png +0 -0
- package/ios/frames/minimal_double.png +0 -0
- package/ios/frames/polaroid_white.png +0 -0
- package/ios/frames/watercolor_floral.png +0 -0
- package/lib/module/assets/frames/film_vintage.png +0 -0
- package/lib/module/assets/frames/floral_gold.png +0 -0
- package/lib/module/assets/frames/minimal_double.png +0 -0
- package/lib/module/assets/frames/polaroid_white.png +0 -0
- package/lib/module/assets/frames/watercolor_floral.png +0 -0
- package/lib/module/components/VideoEditor.js +156 -0
- package/lib/module/components/VideoEditor.js.map +1 -0
- package/lib/module/index.js +4 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/native/CameraView.js +104 -0
- package/lib/module/native/CameraView.js.map +1 -0
- package/lib/module/native/FrameGrabber.js +13 -0
- package/lib/module/native/FrameGrabber.js.map +1 -0
- package/lib/module/native/MediaEditor.js +19 -0
- package/lib/module/native/MediaEditor.js.map +1 -0
- package/lib/module/native/MediaLibrary.js +37 -0
- package/lib/module/native/MediaLibrary.js.map +1 -0
- package/lib/module/native/MediaPicker.js +13 -0
- package/lib/module/native/MediaPicker.js.map +1 -0
- package/lib/module/native/MediaPlayer.js +13 -0
- package/lib/module/native/MediaPlayer.js.map +1 -0
- package/lib/module/native/VideoPreview.js +12 -0
- package/lib/module/native/VideoPreview.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/screens/CropScreen.js +1211 -0
- package/lib/module/screens/CropScreen.js.map +1 -0
- package/lib/module/screens/EditorScreen.js +5752 -0
- package/lib/module/screens/EditorScreen.js.map +1 -0
- package/lib/module/screens/ExportScreen.js +289 -0
- package/lib/module/screens/ExportScreen.js.map +1 -0
- package/lib/module/screens/GalleryScreen.js +505 -0
- package/lib/module/screens/GalleryScreen.js.map +1 -0
- package/lib/module/screens/PickScreen.js +1195 -0
- package/lib/module/screens/PickScreen.js.map +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/src/components/VideoEditor.d.ts +13 -0
- package/lib/typescript/src/index.d.ts +2 -0
- package/lib/typescript/src/native/CameraView.d.ts +23 -0
- package/lib/typescript/src/native/FrameGrabber.d.ts +2 -0
- package/lib/typescript/src/native/MediaEditor.d.ts +3 -0
- package/lib/typescript/src/native/MediaLibrary.d.ts +16 -0
- package/lib/typescript/src/native/MediaPicker.d.ts +2 -0
- package/lib/typescript/src/native/MediaPlayer.d.ts +1 -0
- package/lib/typescript/src/native/VideoPreview.d.ts +19 -0
- package/lib/typescript/src/screens/CropScreen.d.ts +9 -0
- package/lib/typescript/src/screens/EditorScreen.d.ts +10 -0
- package/lib/typescript/src/screens/ExportScreen.d.ts +9 -0
- package/lib/typescript/src/screens/GalleryScreen.d.ts +8 -0
- package/lib/typescript/src/screens/PickScreen.d.ts +13 -0
- package/lib/typescript/src/types.d.ts +58 -0
- package/package.json +101 -0
- package/src/assets/frames/film_vintage.png +0 -0
- package/src/assets/frames/floral_gold.png +0 -0
- package/src/assets/frames/minimal_double.png +0 -0
- package/src/assets/frames/polaroid_white.png +0 -0
- package/src/assets/frames/watercolor_floral.png +0 -0
- package/src/components/VideoEditor.tsx +182 -0
- package/src/index.tsx +2 -0
- package/src/native/CameraView.tsx +95 -0
- package/src/native/FrameGrabber.ts +21 -0
- package/src/native/MediaEditor.ts +33 -0
- package/src/native/MediaLibrary.ts +69 -0
- package/src/native/MediaPicker.ts +17 -0
- package/src/native/MediaPlayer.ts +16 -0
- package/src/native/VideoPreview.tsx +20 -0
- package/src/screens/CropScreen.tsx +968 -0
- package/src/screens/EditorScreen.tsx +4517 -0
- package/src/screens/ExportScreen.tsx +282 -0
- package/src/screens/GalleryScreen.tsx +412 -0
- package/src/screens/PickScreen.tsx +1094 -0
- package/src/types.ts +58 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
#import <React/RCTBridgeModule.h>
|
|
2
|
+
#import <React/RCTImageLoaderProtocol.h>
|
|
3
|
+
#import <Photos/Photos.h>
|
|
4
|
+
#import <AVFoundation/AVFoundation.h>
|
|
5
|
+
|
|
6
|
+
@interface RNMediaLibrary : NSObject <RCTBridgeModule, RCTImageURLLoader>
|
|
7
|
+
@end
|
|
8
|
+
|
|
9
|
+
@implementation RNMediaLibrary
|
|
10
|
+
|
|
11
|
+
RCT_EXPORT_MODULE(RNMediaLibrary)
|
|
12
|
+
|
|
13
|
+
// --- RCTImageURLLoader Implementation ---
|
|
14
|
+
|
|
15
|
+
- (BOOL)canLoadImageURL:(NSURL *)requestURL {
|
|
16
|
+
return [requestURL.scheme.lowercaseString isEqualToString:@"ph"];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
- (RCTImageLoaderCancellationBlock)loadImageForURL:(NSURL *)imageURL
|
|
20
|
+
size:(CGSize)size
|
|
21
|
+
scale:(CGFloat)scale
|
|
22
|
+
resizeMode:(RCTResizeMode)resizeMode
|
|
23
|
+
progressHandler:(RCTImageLoaderProgressBlock)progressHandler
|
|
24
|
+
partialLoadHandler:(RCTImageLoaderPartialLoadBlock)partialLoadHandler
|
|
25
|
+
completionHandler:(RCTImageLoaderCompletionBlock)completionHandler {
|
|
26
|
+
|
|
27
|
+
NSString *localIdentifier = [imageURL.absoluteString substringFromIndex:5]; // remove "ph://"
|
|
28
|
+
PHFetchResult<PHAsset *> *assets = [PHAsset fetchAssetsWithLocalIdentifiers:@[localIdentifier] options:nil];
|
|
29
|
+
PHAsset *asset = assets.firstObject;
|
|
30
|
+
|
|
31
|
+
if (!asset) {
|
|
32
|
+
completionHandler(nil, nil);
|
|
33
|
+
return ^{};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init];
|
|
37
|
+
options.networkAccessAllowed = YES;
|
|
38
|
+
options.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat;
|
|
39
|
+
|
|
40
|
+
// If size is 0 or very large, request original image
|
|
41
|
+
CGSize targetSize = size;
|
|
42
|
+
if (CGSizeEqualToSize(size, CGSizeZero)) {
|
|
43
|
+
targetSize = PHImageManagerMaximumSize;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
PHImageRequestID requestId = [[PHImageManager defaultManager] requestImageForAsset:asset
|
|
47
|
+
targetSize:targetSize
|
|
48
|
+
contentMode:PHImageContentModeAspectFill
|
|
49
|
+
options:options
|
|
50
|
+
resultHandler:^(UIImage *result, NSDictionary *info) {
|
|
51
|
+
completionHandler(nil, result);
|
|
52
|
+
}];
|
|
53
|
+
|
|
54
|
+
return ^{
|
|
55
|
+
[[PHImageManager defaultManager] cancelImageRequest:requestId];
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// --- End RCTImageURLLoader ---
|
|
60
|
+
|
|
61
|
+
+ (BOOL)requiresMainQueueSetup {
|
|
62
|
+
return YES;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
RCT_REMAP_METHOD(requestAccess,
|
|
66
|
+
requestAccessWithResolver:(RCTPromiseResolveBlock)resolve
|
|
67
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
68
|
+
{
|
|
69
|
+
PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatusForAccessLevel:PHAccessLevelReadWrite];
|
|
70
|
+
if (status == PHAuthorizationStatusAuthorized || status == PHAuthorizationStatusLimited) {
|
|
71
|
+
resolve(@(YES));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
[PHPhotoLibrary requestAuthorizationForAccessLevel:PHAccessLevelReadWrite handler:^(PHAuthorizationStatus newStatus) {
|
|
75
|
+
BOOL ok = (newStatus == PHAuthorizationStatusAuthorized || newStatus == PHAuthorizationStatusLimited);
|
|
76
|
+
resolve(@(ok));
|
|
77
|
+
}];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
RCT_REMAP_METHOD(listAlbums,
|
|
81
|
+
listAlbumsWithResolver:(RCTPromiseResolveBlock)resolve
|
|
82
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
83
|
+
{
|
|
84
|
+
@try {
|
|
85
|
+
NSMutableArray *results = [NSMutableArray array];
|
|
86
|
+
|
|
87
|
+
// 1. Smart Albums (Recent, Favorites, etc)
|
|
88
|
+
PHFetchResult<PHAssetCollection *> *smartAlbums = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeSmartAlbum subtype:PHAssetCollectionSubtypeAny options:nil];
|
|
89
|
+
[smartAlbums enumerateObjectsUsingBlock:^(PHAssetCollection * _Nonnull collection, NSUInteger idx, BOOL * _Nonnull stop) {
|
|
90
|
+
PHFetchResult *assets = [PHAsset fetchAssetsInAssetCollection:collection options:nil];
|
|
91
|
+
if (assets.count > 0) {
|
|
92
|
+
[results addObject:@{
|
|
93
|
+
@"id": collection.localIdentifier,
|
|
94
|
+
@"title": collection.localizedTitle ?: @"Unknown"
|
|
95
|
+
}];
|
|
96
|
+
}
|
|
97
|
+
}];
|
|
98
|
+
|
|
99
|
+
// 2. User Albums
|
|
100
|
+
PHFetchResult<PHAssetCollection *> *userAlbums = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeAlbum subtype:PHAssetCollectionSubtypeAny options:nil];
|
|
101
|
+
[userAlbums enumerateObjectsUsingBlock:^(PHAssetCollection * _Nonnull collection, NSUInteger idx, BOOL * _Nonnull stop) {
|
|
102
|
+
PHFetchResult *assets = [PHAsset fetchAssetsInAssetCollection:collection options:nil];
|
|
103
|
+
if (assets.count > 0) {
|
|
104
|
+
[results addObject:@{
|
|
105
|
+
@"id": collection.localIdentifier,
|
|
106
|
+
@"title": collection.localizedTitle ?: @"Unknown"
|
|
107
|
+
}];
|
|
108
|
+
}
|
|
109
|
+
}];
|
|
110
|
+
|
|
111
|
+
resolve(results);
|
|
112
|
+
} @catch (NSException *exception) {
|
|
113
|
+
reject(@"albums_failed", exception.reason, nil);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
RCT_REMAP_METHOD(listMedia,
|
|
118
|
+
listMediaWithOptions:(NSDictionary *)options
|
|
119
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
120
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
121
|
+
{
|
|
122
|
+
@try {
|
|
123
|
+
NSNumber *limit = options[@"limit"] ?: @200;
|
|
124
|
+
NSNumber *offset = options[@"offset"] ?: @0;
|
|
125
|
+
NSString *type = options[@"type"] ?: @"all";
|
|
126
|
+
NSString *albumId = options[@"albumId"];
|
|
127
|
+
|
|
128
|
+
PHFetchOptions *fetchOptions = [[PHFetchOptions alloc] init];
|
|
129
|
+
fetchOptions.sortDescriptors = @[ [NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:NO] ];
|
|
130
|
+
|
|
131
|
+
PHFetchResult<PHAsset *> *assets = nil;
|
|
132
|
+
|
|
133
|
+
if (albumId) {
|
|
134
|
+
PHFetchResult<PHAssetCollection *> *collections = [PHAssetCollection fetchAssetCollectionsWithLocalIdentifiers:@[albumId] options:nil];
|
|
135
|
+
PHAssetCollection *collection = collections.firstObject;
|
|
136
|
+
if (collection) {
|
|
137
|
+
if ([type isEqualToString:@"image"]) {
|
|
138
|
+
fetchOptions.predicate = [NSPredicate predicateWithFormat:@"mediaType = %d", PHAssetMediaTypeImage];
|
|
139
|
+
} else if ([type isEqualToString:@"video"]) {
|
|
140
|
+
fetchOptions.predicate = [NSPredicate predicateWithFormat:@"mediaType = %d", PHAssetMediaTypeVideo];
|
|
141
|
+
}
|
|
142
|
+
assets = [PHAsset fetchAssetsInAssetCollection:collection options:fetchOptions];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!assets) {
|
|
147
|
+
if ([type isEqualToString:@"image"]) {
|
|
148
|
+
assets = [PHAsset fetchAssetsWithMediaType:PHAssetMediaTypeImage options:fetchOptions];
|
|
149
|
+
} else if ([type isEqualToString:@"video"]) {
|
|
150
|
+
assets = [PHAsset fetchAssetsWithMediaType:PHAssetMediaTypeVideo options:fetchOptions];
|
|
151
|
+
} else {
|
|
152
|
+
fetchOptions.predicate = [NSPredicate predicateWithFormat:@"mediaType = %d OR mediaType = %d", PHAssetMediaTypeImage, PHAssetMediaTypeVideo];
|
|
153
|
+
assets = [PHAsset fetchAssetsWithOptions:fetchOptions];
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
NSMutableArray *results = [NSMutableArray array];
|
|
158
|
+
PHImageRequestOptions *thumbOptions = [[PHImageRequestOptions alloc] init];
|
|
159
|
+
thumbOptions.synchronous = YES;
|
|
160
|
+
thumbOptions.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat;
|
|
161
|
+
thumbOptions.resizeMode = PHImageRequestOptionsResizeModeExact;
|
|
162
|
+
thumbOptions.networkAccessAllowed = YES;
|
|
163
|
+
|
|
164
|
+
PHImageManager *manager = [PHImageManager defaultManager];
|
|
165
|
+
NSInteger start = offset.integerValue;
|
|
166
|
+
NSInteger end = MIN(start + limit.integerValue, assets.count);
|
|
167
|
+
|
|
168
|
+
for (NSInteger i = start; i < end; i++) {
|
|
169
|
+
PHAsset *asset = [assets objectAtIndex:i];
|
|
170
|
+
NSString *mediaType = asset.mediaType == PHAssetMediaTypeVideo ? @"video" : @"image";
|
|
171
|
+
|
|
172
|
+
__block NSString *thumbUri = nil;
|
|
173
|
+
CGSize targetSize = CGSizeMake(240, 240);
|
|
174
|
+
[manager requestImageForAsset:asset targetSize:targetSize contentMode:PHImageContentModeAspectFill options:thumbOptions resultHandler:^(UIImage * _Nullable image, NSDictionary * _Nullable info) {
|
|
175
|
+
if (image) {
|
|
176
|
+
NSData *data = UIImageJPEGRepresentation(image, 0.8);
|
|
177
|
+
NSString *fileName = [NSString stringWithFormat:@"thumb_%@.jpg", [[NSUUID UUID] UUIDString]];
|
|
178
|
+
NSString *path = [NSTemporaryDirectory() stringByAppendingPathComponent:fileName];
|
|
179
|
+
[data writeToFile:path atomically:YES];
|
|
180
|
+
thumbUri = [NSURL fileURLWithPath:path].absoluteString;
|
|
181
|
+
}
|
|
182
|
+
}];
|
|
183
|
+
|
|
184
|
+
if (!thumbUri && asset.mediaType == PHAssetMediaTypeVideo) {
|
|
185
|
+
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
|
186
|
+
PHVideoRequestOptions *videoOpts = [[PHVideoRequestOptions alloc] init];
|
|
187
|
+
videoOpts.networkAccessAllowed = YES;
|
|
188
|
+
[[PHImageManager defaultManager] requestAVAssetForVideo:asset options:videoOpts resultHandler:^(AVAsset * _Nullable avAsset, AVAudioMix * _Nullable audioMix, NSDictionary * _Nullable info) {
|
|
189
|
+
if (avAsset) {
|
|
190
|
+
AVAssetImageGenerator *gen = [[AVAssetImageGenerator alloc] initWithAsset:avAsset];
|
|
191
|
+
gen.appliesPreferredTrackTransform = YES;
|
|
192
|
+
gen.maximumSize = CGSizeMake(240, 240);
|
|
193
|
+
NSError *err = nil;
|
|
194
|
+
CGImageRef imageRef = [gen copyCGImageAtTime:CMTimeMake(0, 1) actualTime:nil error:&err];
|
|
195
|
+
if (imageRef) {
|
|
196
|
+
UIImage *image = [UIImage imageWithCGImage:imageRef];
|
|
197
|
+
CGImageRelease(imageRef);
|
|
198
|
+
NSData *data = UIImageJPEGRepresentation(image, 0.8);
|
|
199
|
+
NSString *fileName = [NSString stringWithFormat:@"thumb_%@.jpg", [[NSUUID UUID] UUIDString]];
|
|
200
|
+
NSString *path = [NSTemporaryDirectory() stringByAppendingPathComponent:fileName];
|
|
201
|
+
[data writeToFile:path atomically:YES];
|
|
202
|
+
thumbUri = [NSURL fileURLWithPath:path].absoluteString;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
dispatch_semaphore_signal(sema);
|
|
206
|
+
}];
|
|
207
|
+
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
__block NSString *fullUri = nil;
|
|
211
|
+
if (asset.mediaType == PHAssetMediaTypeVideo) {
|
|
212
|
+
// Return a ph:// URI for videos so we do not attempt to read system files directly or transcode in listMedia
|
|
213
|
+
fullUri = [NSString stringWithFormat:@"ph://%@", asset.localIdentifier];
|
|
214
|
+
} else {
|
|
215
|
+
[manager requestImageForAsset:asset targetSize:PHImageManagerMaximumSize contentMode:PHImageContentModeAspectFit options:thumbOptions resultHandler:^(UIImage * _Nullable image, NSDictionary * _Nullable info) {
|
|
216
|
+
if (image) {
|
|
217
|
+
NSData *data = UIImageJPEGRepresentation(image, 0.9);
|
|
218
|
+
NSString *fileName = [NSString stringWithFormat:@"full_%@.jpg", [[NSUUID UUID] UUIDString]];
|
|
219
|
+
NSString *path = [NSTemporaryDirectory() stringByAppendingPathComponent:fileName];
|
|
220
|
+
[data writeToFile:path atomically:YES];
|
|
221
|
+
fullUri = [NSURL fileURLWithPath:path].absoluteString;
|
|
222
|
+
}
|
|
223
|
+
}];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
NSDictionary *item = @{
|
|
227
|
+
@"id": asset.localIdentifier,
|
|
228
|
+
@"uri": fullUri ?: thumbUri ?: @"",
|
|
229
|
+
@"thumbnailUri": thumbUri ?: @"",
|
|
230
|
+
@"type": mediaType,
|
|
231
|
+
@"durationMs": @(asset.duration * 1000.0)
|
|
232
|
+
};
|
|
233
|
+
[results addObject:item];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
resolve(results);
|
|
237
|
+
} @catch (NSException *exception) {
|
|
238
|
+
reject(@"list_failed", exception.reason, nil);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
- (void)exportVideoFallback:(PHAsset *)asset
|
|
243
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
244
|
+
rejecter:(RCTPromiseRejectBlock)reject
|
|
245
|
+
{
|
|
246
|
+
PHVideoRequestOptions *opts = [[PHVideoRequestOptions alloc] init];
|
|
247
|
+
opts.networkAccessAllowed = YES;
|
|
248
|
+
|
|
249
|
+
[[PHImageManager defaultManager] requestAVAssetForVideo:asset options:opts resultHandler:^(AVAsset * _Nullable avAsset, AVAudioMix * _Nullable audioMix, NSDictionary * _Nullable info) {
|
|
250
|
+
if (!avAsset) {
|
|
251
|
+
reject(@"export_failed", @"Could not get AVAsset for video", nil);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Try exporting via AVAssetExportSession
|
|
256
|
+
AVAssetExportSession *exporter = [[AVAssetExportSession alloc] initWithAsset:avAsset presetName:AVAssetExportPresetHighestQuality];
|
|
257
|
+
if (!exporter) {
|
|
258
|
+
reject(@"export_failed", @"Could not create AVAssetExportSession", nil);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
NSString *fileName = [NSString stringWithFormat:@"export_%@.mp4", [[NSUUID UUID] UUIDString]];
|
|
263
|
+
NSString *exportPath = [NSTemporaryDirectory() stringByAppendingPathComponent:fileName];
|
|
264
|
+
NSURL *exportURL = [NSURL fileURLWithPath:exportPath];
|
|
265
|
+
|
|
266
|
+
exporter.outputURL = exportURL;
|
|
267
|
+
exporter.outputFileType = AVFileTypeMPEG4;
|
|
268
|
+
exporter.shouldOptimizeForNetworkUse = YES;
|
|
269
|
+
|
|
270
|
+
[exporter exportAsynchronouslyWithCompletionHandler:^{
|
|
271
|
+
if (exporter.status == AVAssetExportSessionStatusCompleted) {
|
|
272
|
+
resolve(exportURL.absoluteString);
|
|
273
|
+
} else {
|
|
274
|
+
// Last fallback: if exporter fails, see if it is an AVURLAsset and try copying it directly
|
|
275
|
+
if ([avAsset isKindOfClass:[AVURLAsset class]]) {
|
|
276
|
+
AVURLAsset *urlAsset = (AVURLAsset *)avAsset;
|
|
277
|
+
if (urlAsset.URL) {
|
|
278
|
+
NSString *copyFileName = [NSString stringWithFormat:@"export_%@.mov", [[NSUUID UUID] UUIDString]];
|
|
279
|
+
NSString *copyPath = [NSTemporaryDirectory() stringByAppendingPathComponent:copyFileName];
|
|
280
|
+
NSURL *copyURL = [NSURL fileURLWithPath:copyPath];
|
|
281
|
+
NSError *copyError = nil;
|
|
282
|
+
[[NSFileManager defaultManager] copyItemAtURL:urlAsset.URL toURL:copyURL error:©Error];
|
|
283
|
+
if (!copyError) {
|
|
284
|
+
resolve(copyURL.absoluteString);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
NSError *err = exporter.error;
|
|
290
|
+
NSString *errMsg = err ? err.localizedDescription : @"Unknown export error";
|
|
291
|
+
reject(@"export_failed", [NSString stringWithFormat:@"Export session failed: %@", errMsg], err);
|
|
292
|
+
}
|
|
293
|
+
}];
|
|
294
|
+
}];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
RCT_REMAP_METHOD(exportAsset,
|
|
298
|
+
exportAssetWithLocalId:(NSString *)localId
|
|
299
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
300
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
301
|
+
{
|
|
302
|
+
PHFetchResult<PHAsset *> *result = [PHAsset fetchAssetsWithLocalIdentifiers:@[localId] options:nil];
|
|
303
|
+
PHAsset *asset = result.firstObject;
|
|
304
|
+
if (!asset) {
|
|
305
|
+
reject(@"not_found", @"Asset not found", nil);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (asset.mediaType == PHAssetMediaTypeImage) {
|
|
310
|
+
PHImageRequestOptions *opts = [[PHImageRequestOptions alloc] init];
|
|
311
|
+
opts.synchronous = YES;
|
|
312
|
+
opts.networkAccessAllowed = YES;
|
|
313
|
+
|
|
314
|
+
__block NSString *outUri = nil;
|
|
315
|
+
[[PHImageManager defaultManager] requestImageDataAndOrientationForAsset:asset options:opts resultHandler:^(NSData * _Nullable imageData, NSString * _Nullable dataUTI, CGImagePropertyOrientation orientation, NSDictionary * _Nullable info) {
|
|
316
|
+
if (imageData) {
|
|
317
|
+
NSString *fileName = [NSString stringWithFormat:@"export_%@.jpg", [[NSUUID UUID] UUIDString]];
|
|
318
|
+
NSString *path = [NSTemporaryDirectory() stringByAppendingPathComponent:fileName];
|
|
319
|
+
[imageData writeToFile:path atomically:YES];
|
|
320
|
+
outUri = [NSURL fileURLWithPath:path].absoluteString;
|
|
321
|
+
}
|
|
322
|
+
}];
|
|
323
|
+
|
|
324
|
+
if (outUri) {
|
|
325
|
+
resolve(outUri);
|
|
326
|
+
} else {
|
|
327
|
+
reject(@"export_failed", @"Could not export image", nil);
|
|
328
|
+
}
|
|
329
|
+
} else if (asset.mediaType == PHAssetMediaTypeVideo) {
|
|
330
|
+
NSArray<PHAssetResource *> *resources = [PHAssetResource assetResourcesForAsset:asset];
|
|
331
|
+
PHAssetResource *videoResource = nil;
|
|
332
|
+
for (PHAssetResource *res in resources) {
|
|
333
|
+
if (res.type == PHAssetResourceTypeVideo) {
|
|
334
|
+
videoResource = res;
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (videoResource) {
|
|
340
|
+
NSString *fileName = [NSString stringWithFormat:@"export_%@.mov", [[NSUUID UUID] UUIDString]];
|
|
341
|
+
NSString *exportPath = [NSTemporaryDirectory() stringByAppendingPathComponent:fileName];
|
|
342
|
+
NSURL *exportURL = [NSURL fileURLWithPath:exportPath];
|
|
343
|
+
|
|
344
|
+
PHAssetResourceRequestOptions *options = [[PHAssetResourceRequestOptions alloc] init];
|
|
345
|
+
options.networkAccessAllowed = YES;
|
|
346
|
+
|
|
347
|
+
[[PHAssetResourceManager defaultManager] writeDataForAssetResource:videoResource toFile:exportURL options:options completionHandler:^(NSError * _Nullable error) {
|
|
348
|
+
if (!error) {
|
|
349
|
+
resolve(exportURL.absoluteString);
|
|
350
|
+
} else {
|
|
351
|
+
[self exportVideoFallback:asset resolver:resolve rejecter:reject];
|
|
352
|
+
}
|
|
353
|
+
}];
|
|
354
|
+
} else {
|
|
355
|
+
[self exportVideoFallback:asset resolver:resolve rejecter:reject];
|
|
356
|
+
}
|
|
357
|
+
} else {
|
|
358
|
+
reject(@"unsupported", @"Unsupported asset type", nil);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
RCT_REMAP_METHOD(saveToGallery,
|
|
363
|
+
saveToGallery:(NSString *)uriString
|
|
364
|
+
type:(NSString *)type
|
|
365
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
366
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
367
|
+
{
|
|
368
|
+
NSURL *url = [NSURL URLWithString:uriString];
|
|
369
|
+
if (!url) {
|
|
370
|
+
reject(@"bad_uri", @"Invalid uri", nil);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
|
|
375
|
+
if ([type isEqualToString:@"video"]) {
|
|
376
|
+
[PHAssetChangeRequest creationRequestForAssetFromVideoAtFileURL:url];
|
|
377
|
+
} else {
|
|
378
|
+
[PHAssetChangeRequest creationRequestForAssetFromImageAtFileURL:url];
|
|
379
|
+
}
|
|
380
|
+
} completionHandler:^(BOOL success, NSError * _Nullable error) {
|
|
381
|
+
if (success) {
|
|
382
|
+
resolve(@(YES));
|
|
383
|
+
} else {
|
|
384
|
+
reject(@"save_failed", error.localizedDescription, error);
|
|
385
|
+
}
|
|
386
|
+
}];
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
@end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#import <React/RCTBridgeModule.h>
|
|
2
|
+
#import <React/RCTUtils.h>
|
|
3
|
+
#import <PhotosUI/PhotosUI.h>
|
|
4
|
+
#import <AVFoundation/AVFoundation.h>
|
|
5
|
+
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
|
|
6
|
+
|
|
7
|
+
@interface RNMediaPicker : NSObject <RCTBridgeModule, PHPickerViewControllerDelegate>
|
|
8
|
+
@end
|
|
9
|
+
|
|
10
|
+
@implementation RNMediaPicker {
|
|
11
|
+
RCTPromiseResolveBlock _resolve;
|
|
12
|
+
RCTPromiseRejectBlock _reject;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
RCT_EXPORT_MODULE(RNMediaPicker)
|
|
16
|
+
|
|
17
|
+
+ (BOOL)requiresMainQueueSetup {
|
|
18
|
+
return YES;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
RCT_REMAP_METHOD(pickMedia,
|
|
22
|
+
pickMediaWithResolver:(RCTPromiseResolveBlock)resolve
|
|
23
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
24
|
+
{
|
|
25
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
26
|
+
UIViewController *presenter = RCTPresentedViewController();
|
|
27
|
+
if (!presenter) {
|
|
28
|
+
reject(@"no_view", @"No view controller to present picker", nil);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (self->_resolve != nil) {
|
|
32
|
+
reject(@"in_progress", @"Another picker request is in progress", nil);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
self->_resolve = resolve;
|
|
36
|
+
self->_reject = reject;
|
|
37
|
+
|
|
38
|
+
PHPickerConfiguration *config = [[PHPickerConfiguration alloc] initWithPhotoLibrary:PHPhotoLibrary.sharedPhotoLibrary];
|
|
39
|
+
config.selectionLimit = 0;
|
|
40
|
+
config.filter = [PHPickerFilter anyFilterMatchingSubfilters:@[PHPickerFilter.imagesFilter, PHPickerFilter.videosFilter]];
|
|
41
|
+
|
|
42
|
+
PHPickerViewController *picker = [[PHPickerViewController alloc] initWithConfiguration:config];
|
|
43
|
+
picker.delegate = self;
|
|
44
|
+
[presenter presentViewController:picker animated:YES completion:nil];
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
- (void)picker:(PHPickerViewController *)picker didFinishPicking:(NSArray<PHPickerResult *> *)results {
|
|
49
|
+
[picker dismissViewControllerAnimated:YES completion:nil];
|
|
50
|
+
|
|
51
|
+
RCTPromiseResolveBlock resolve = _resolve;
|
|
52
|
+
RCTPromiseRejectBlock reject = _reject;
|
|
53
|
+
_resolve = nil;
|
|
54
|
+
_reject = nil;
|
|
55
|
+
|
|
56
|
+
if (!resolve) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (results.count == 0) {
|
|
61
|
+
resolve(@[]);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
dispatch_group_t group = dispatch_group_create();
|
|
66
|
+
NSMutableArray *output = [NSMutableArray array];
|
|
67
|
+
|
|
68
|
+
for (PHPickerResult *result in results) {
|
|
69
|
+
NSItemProvider *provider = result.itemProvider;
|
|
70
|
+
|
|
71
|
+
if ([provider hasItemConformingToTypeIdentifier:UTTypeMovie.identifier]) {
|
|
72
|
+
dispatch_group_enter(group);
|
|
73
|
+
[provider loadFileRepresentationForTypeIdentifier:UTTypeMovie.identifier completionHandler:^(NSURL * _Nullable url, NSError * _Nullable error) {
|
|
74
|
+
if (error) {
|
|
75
|
+
if (reject) reject(@"load_failed", error.localizedDescription, error);
|
|
76
|
+
dispatch_group_leave(group);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (!url) {
|
|
80
|
+
dispatch_group_leave(group);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
NSURL *tempUrl = [self copyToTemp:url prefix:@"video_"];
|
|
84
|
+
AVAsset *asset = [AVAsset assetWithURL:tempUrl];
|
|
85
|
+
double durationMs = CMTimeGetSeconds(asset.duration) * 1000.0;
|
|
86
|
+
|
|
87
|
+
NSDictionary *item = @{
|
|
88
|
+
@"id": [NSUUID UUID].UUIDString,
|
|
89
|
+
@"uri": tempUrl.absoluteString,
|
|
90
|
+
@"type": @"video",
|
|
91
|
+
@"durationMs": @(durationMs)
|
|
92
|
+
};
|
|
93
|
+
@synchronized (output) { [output addObject:item]; }
|
|
94
|
+
dispatch_group_leave(group);
|
|
95
|
+
}];
|
|
96
|
+
} else if ([provider canLoadObjectOfClass:UIImage.class]) {
|
|
97
|
+
dispatch_group_enter(group);
|
|
98
|
+
[provider loadObjectOfClass:UIImage.class completionHandler:^(UIImage * _Nullable image, NSError * _Nullable error) {
|
|
99
|
+
if (error) {
|
|
100
|
+
if (reject) reject(@"load_failed", error.localizedDescription, error);
|
|
101
|
+
dispatch_group_leave(group);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (!image) {
|
|
105
|
+
dispatch_group_leave(group);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
NSURL *tempUrl = [self writeImageToTemp:image];
|
|
109
|
+
NSDictionary *item = @{
|
|
110
|
+
@"id": [NSUUID UUID].UUIDString,
|
|
111
|
+
@"uri": tempUrl.absoluteString,
|
|
112
|
+
@"type": @"image",
|
|
113
|
+
@"width": @(image.size.width),
|
|
114
|
+
@"height": @(image.size.height)
|
|
115
|
+
};
|
|
116
|
+
@synchronized (output) { [output addObject:item]; }
|
|
117
|
+
dispatch_group_leave(group);
|
|
118
|
+
}];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
|
|
123
|
+
resolve(output);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
- (NSURL *)copyToTemp:(NSURL *)url prefix:(NSString *)prefix {
|
|
128
|
+
NSURL *dir = NSTemporaryDirectory().length ? [NSURL fileURLWithPath:NSTemporaryDirectory()] : [NSFileManager.defaultManager temporaryDirectory];
|
|
129
|
+
NSString *ext = url.pathExtension.length ? url.pathExtension : @"mp4";
|
|
130
|
+
NSURL *dest = [dir URLByAppendingPathComponent:[NSString stringWithFormat:@"%@%@.%@", prefix, [NSUUID UUID].UUIDString, ext]];
|
|
131
|
+
[NSFileManager.defaultManager removeItemAtURL:dest error:nil];
|
|
132
|
+
[NSFileManager.defaultManager copyItemAtURL:url toURL:dest error:nil];
|
|
133
|
+
return dest;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
- (NSURL *)writeImageToTemp:(UIImage *)image {
|
|
137
|
+
NSURL *dir = NSTemporaryDirectory().length ? [NSURL fileURLWithPath:NSTemporaryDirectory()] : [NSFileManager.defaultManager temporaryDirectory];
|
|
138
|
+
NSURL *dest = [dir URLByAppendingPathComponent:[NSString stringWithFormat:@"image_%@.jpg", [NSUUID UUID].UUIDString]];
|
|
139
|
+
NSData *data = UIImageJPEGRepresentation(image, 0.92);
|
|
140
|
+
[data writeToURL:dest atomically:YES];
|
|
141
|
+
return dest;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
@end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#import <React/RCTBridgeModule.h>
|
|
2
|
+
#import <AVKit/AVKit.h>
|
|
3
|
+
#import <React/RCTUtils.h>
|
|
4
|
+
#import <Photos/Photos.h>
|
|
5
|
+
|
|
6
|
+
@interface RNMediaPlayer : NSObject <RCTBridgeModule>
|
|
7
|
+
@end
|
|
8
|
+
|
|
9
|
+
@implementation RNMediaPlayer
|
|
10
|
+
|
|
11
|
+
RCT_EXPORT_MODULE(RNMediaPlayer)
|
|
12
|
+
|
|
13
|
+
+ (BOOL)requiresMainQueueSetup {
|
|
14
|
+
return YES;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
RCT_REMAP_METHOD(playVideo,
|
|
18
|
+
playVideoWithUri:(NSString *)uriString
|
|
19
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
20
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
21
|
+
{
|
|
22
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
23
|
+
UIViewController *presenter = RCTPresentedViewController();
|
|
24
|
+
if (!presenter) {
|
|
25
|
+
reject(@"no_view", @"No view controller to present player", nil);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if ([uriString hasPrefix:@"ph://"]) {
|
|
30
|
+
NSString *localId = [uriString stringByReplacingOccurrencesOfString:@"ph://" withString:@""];
|
|
31
|
+
PHFetchResult<PHAsset *> *result = [PHAsset fetchAssetsWithLocalIdentifiers:@[localId] options:nil];
|
|
32
|
+
PHAsset *asset = result.firstObject;
|
|
33
|
+
if (!asset) {
|
|
34
|
+
reject(@"not_found", @"Asset not found", nil);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
PHVideoRequestOptions *opts = [[PHVideoRequestOptions alloc] init];
|
|
39
|
+
opts.networkAccessAllowed = YES;
|
|
40
|
+
[[PHImageManager defaultManager] requestAVAssetForVideo:asset options:opts resultHandler:^(AVAsset * _Nullable avAsset, AVAudioMix * _Nullable audioMix, NSDictionary * _Nullable info) {
|
|
41
|
+
if (!avAsset) {
|
|
42
|
+
reject(@"play_failed", @"Could not load asset", nil);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
46
|
+
AVPlayerViewController *playerVC = [[AVPlayerViewController alloc] init];
|
|
47
|
+
playerVC.player = [AVPlayer playerWithPlayerItem:[AVPlayerItem playerItemWithAsset:avAsset]];
|
|
48
|
+
[presenter presentViewController:playerVC animated:YES completion:^{
|
|
49
|
+
[playerVC.player play];
|
|
50
|
+
resolve(@(YES));
|
|
51
|
+
}];
|
|
52
|
+
});
|
|
53
|
+
}];
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
NSURL *url = [NSURL URLWithString:uriString];
|
|
58
|
+
if (!url) {
|
|
59
|
+
reject(@"bad_uri", @"Invalid video uri", nil);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
AVPlayerViewController *playerVC = [[AVPlayerViewController alloc] init];
|
|
64
|
+
playerVC.player = [AVPlayer playerWithURL:url];
|
|
65
|
+
|
|
66
|
+
[presenter presentViewController:playerVC animated:YES completion:^{
|
|
67
|
+
[playerVC.player play];
|
|
68
|
+
resolve(@(YES));
|
|
69
|
+
}];
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@end
|