@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.
Files changed (99) hide show
  1. package/ImageVideoEditor.podspec +21 -0
  2. package/README.md +136 -0
  3. package/android/build.gradle +76 -0
  4. package/android/gradle.properties +5 -0
  5. package/android/src/main/AndroidManifest.xml +13 -0
  6. package/android/src/main/java/com/technotoil/image_videoeditor/FrameGrabberModule.kt +67 -0
  7. package/android/src/main/java/com/technotoil/image_videoeditor/MediaEditorModule.kt +548 -0
  8. package/android/src/main/java/com/technotoil/image_videoeditor/MediaFileUtils.kt +29 -0
  9. package/android/src/main/java/com/technotoil/image_videoeditor/MediaLibraryModule.kt +305 -0
  10. package/android/src/main/java/com/technotoil/image_videoeditor/MediaPackage.kt +26 -0
  11. package/android/src/main/java/com/technotoil/image_videoeditor/MediaPickerModule.kt +111 -0
  12. package/android/src/main/java/com/technotoil/image_videoeditor/MediaPlayerModule.kt +34 -0
  13. package/android/src/main/java/com/technotoil/image_videoeditor/RNCameraViewManager.kt +761 -0
  14. package/android/src/main/java/com/technotoil/image_videoeditor/RNVideoPreviewManager.kt +317 -0
  15. package/ios/PrivacyInfo.xcprivacy +38 -0
  16. package/ios/RNCameraViewManager.m +420 -0
  17. package/ios/RNFrameGrabber.m +61 -0
  18. package/ios/RNMediaEditor.m +905 -0
  19. package/ios/RNMediaLibrary.m +389 -0
  20. package/ios/RNMediaPicker.m +144 -0
  21. package/ios/RNMediaPlayer.m +73 -0
  22. package/ios/RNVideoPreviewManager.m +263 -0
  23. package/ios/frames/film_vintage.png +0 -0
  24. package/ios/frames/floral_gold.png +0 -0
  25. package/ios/frames/minimal_double.png +0 -0
  26. package/ios/frames/polaroid_white.png +0 -0
  27. package/ios/frames/watercolor_floral.png +0 -0
  28. package/lib/module/assets/frames/film_vintage.png +0 -0
  29. package/lib/module/assets/frames/floral_gold.png +0 -0
  30. package/lib/module/assets/frames/minimal_double.png +0 -0
  31. package/lib/module/assets/frames/polaroid_white.png +0 -0
  32. package/lib/module/assets/frames/watercolor_floral.png +0 -0
  33. package/lib/module/components/VideoEditor.js +156 -0
  34. package/lib/module/components/VideoEditor.js.map +1 -0
  35. package/lib/module/index.js +4 -0
  36. package/lib/module/index.js.map +1 -0
  37. package/lib/module/native/CameraView.js +104 -0
  38. package/lib/module/native/CameraView.js.map +1 -0
  39. package/lib/module/native/FrameGrabber.js +13 -0
  40. package/lib/module/native/FrameGrabber.js.map +1 -0
  41. package/lib/module/native/MediaEditor.js +19 -0
  42. package/lib/module/native/MediaEditor.js.map +1 -0
  43. package/lib/module/native/MediaLibrary.js +37 -0
  44. package/lib/module/native/MediaLibrary.js.map +1 -0
  45. package/lib/module/native/MediaPicker.js +13 -0
  46. package/lib/module/native/MediaPicker.js.map +1 -0
  47. package/lib/module/native/MediaPlayer.js +13 -0
  48. package/lib/module/native/MediaPlayer.js.map +1 -0
  49. package/lib/module/native/VideoPreview.js +12 -0
  50. package/lib/module/native/VideoPreview.js.map +1 -0
  51. package/lib/module/package.json +1 -0
  52. package/lib/module/screens/CropScreen.js +1211 -0
  53. package/lib/module/screens/CropScreen.js.map +1 -0
  54. package/lib/module/screens/EditorScreen.js +5752 -0
  55. package/lib/module/screens/EditorScreen.js.map +1 -0
  56. package/lib/module/screens/ExportScreen.js +289 -0
  57. package/lib/module/screens/ExportScreen.js.map +1 -0
  58. package/lib/module/screens/GalleryScreen.js +505 -0
  59. package/lib/module/screens/GalleryScreen.js.map +1 -0
  60. package/lib/module/screens/PickScreen.js +1195 -0
  61. package/lib/module/screens/PickScreen.js.map +1 -0
  62. package/lib/module/types.js +2 -0
  63. package/lib/module/types.js.map +1 -0
  64. package/lib/typescript/src/components/VideoEditor.d.ts +13 -0
  65. package/lib/typescript/src/index.d.ts +2 -0
  66. package/lib/typescript/src/native/CameraView.d.ts +23 -0
  67. package/lib/typescript/src/native/FrameGrabber.d.ts +2 -0
  68. package/lib/typescript/src/native/MediaEditor.d.ts +3 -0
  69. package/lib/typescript/src/native/MediaLibrary.d.ts +16 -0
  70. package/lib/typescript/src/native/MediaPicker.d.ts +2 -0
  71. package/lib/typescript/src/native/MediaPlayer.d.ts +1 -0
  72. package/lib/typescript/src/native/VideoPreview.d.ts +19 -0
  73. package/lib/typescript/src/screens/CropScreen.d.ts +9 -0
  74. package/lib/typescript/src/screens/EditorScreen.d.ts +10 -0
  75. package/lib/typescript/src/screens/ExportScreen.d.ts +9 -0
  76. package/lib/typescript/src/screens/GalleryScreen.d.ts +8 -0
  77. package/lib/typescript/src/screens/PickScreen.d.ts +13 -0
  78. package/lib/typescript/src/types.d.ts +58 -0
  79. package/package.json +101 -0
  80. package/src/assets/frames/film_vintage.png +0 -0
  81. package/src/assets/frames/floral_gold.png +0 -0
  82. package/src/assets/frames/minimal_double.png +0 -0
  83. package/src/assets/frames/polaroid_white.png +0 -0
  84. package/src/assets/frames/watercolor_floral.png +0 -0
  85. package/src/components/VideoEditor.tsx +182 -0
  86. package/src/index.tsx +2 -0
  87. package/src/native/CameraView.tsx +95 -0
  88. package/src/native/FrameGrabber.ts +21 -0
  89. package/src/native/MediaEditor.ts +33 -0
  90. package/src/native/MediaLibrary.ts +69 -0
  91. package/src/native/MediaPicker.ts +17 -0
  92. package/src/native/MediaPlayer.ts +16 -0
  93. package/src/native/VideoPreview.tsx +20 -0
  94. package/src/screens/CropScreen.tsx +968 -0
  95. package/src/screens/EditorScreen.tsx +4517 -0
  96. package/src/screens/ExportScreen.tsx +282 -0
  97. package/src/screens/GalleryScreen.tsx +412 -0
  98. package/src/screens/PickScreen.tsx +1094 -0
  99. 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:&copyError];
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