@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,420 @@
1
+ #import <React/RCTViewManager.h>
2
+ #import <React/RCTUIManager.h>
3
+ #import <React/RCTConvert.h>
4
+ #import <AVFoundation/AVFoundation.h>
5
+
6
+ @interface RNCameraView : UIView <AVCapturePhotoCaptureDelegate, AVCaptureFileOutputRecordingDelegate>
7
+
8
+ @property (nonatomic, copy) NSString *facing;
9
+ @property (nonatomic, strong) AVCaptureSession *session;
10
+ @property (nonatomic, strong) AVCaptureVideoPreviewLayer *previewLayer;
11
+ @property (nonatomic, strong) AVCapturePhotoOutput *photoOutput;
12
+ @property (nonatomic, strong) AVCaptureMovieFileOutput *movieOutput;
13
+ @property (nonatomic, strong) AVCaptureDeviceInput *videoInput;
14
+ @property (nonatomic, strong) AVCaptureDeviceInput *audioInput;
15
+ @property (nonatomic, strong) dispatch_queue_t sessionQueue;
16
+
17
+ @property (nonatomic, copy) RCTPromiseResolveBlock photoResolve;
18
+ @property (nonatomic, copy) RCTPromiseRejectBlock photoReject;
19
+ @property (nonatomic, copy) RCTPromiseResolveBlock recordResolve;
20
+ @property (nonatomic, copy) RCTPromiseRejectBlock recordReject;
21
+
22
+ @property (nonatomic, copy) NSString *photoTrigger;
23
+ @property (nonatomic, copy) NSString *recordTrigger;
24
+ @property (nonatomic, copy) RCTDirectEventBlock onPhotoCaptured;
25
+ @property (nonatomic, copy) RCTDirectEventBlock onRecordStarted;
26
+ @property (nonatomic, copy) RCTDirectEventBlock onRecordStopped;
27
+
28
+ @end
29
+
30
+ @implementation RNCameraView
31
+
32
+ - (instancetype)initWithFrame:(CGRect)frame {
33
+ if (self = [super initWithFrame:frame]) {
34
+ _facing = @"front"; // default to front
35
+ self.sessionQueue = dispatch_queue_create("com.videoeditor.sessionQueue", DISPATCH_QUEUE_SERIAL);
36
+ self.session = [[AVCaptureSession alloc] init];
37
+ self.session.sessionPreset = AVCaptureSessionPresetHigh;
38
+
39
+ self.previewLayer = [AVCaptureVideoPreviewLayer layerWithSession:self.session];
40
+ self.previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
41
+ [self.layer addSublayer:self.previewLayer];
42
+
43
+ self.photoOutput = [[AVCapturePhotoOutput alloc] init];
44
+ self.movieOutput = [[AVCaptureMovieFileOutput alloc] init];
45
+
46
+ dispatch_async(self.sessionQueue, ^{
47
+ [self.session beginConfiguration];
48
+ if ([self.session canAddOutput:self.photoOutput]) {
49
+ [self.session addOutput:self.photoOutput];
50
+ }
51
+ if ([self.session canAddOutput:self.movieOutput]) {
52
+ [self.session addOutput:self.movieOutput];
53
+ }
54
+ [self.session commitConfiguration];
55
+ });
56
+
57
+ [self configureInputs];
58
+ }
59
+ return self;
60
+ }
61
+
62
+ - (void)layoutSubviews {
63
+ [super layoutSubviews];
64
+ self.previewLayer.frame = self.bounds;
65
+
66
+ // Update preview orientation
67
+ AVCaptureConnection *connection = self.previewLayer.connection;
68
+ if (connection && connection.supportsVideoOrientation) {
69
+ UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation];
70
+ AVCaptureVideoOrientation avOrientation;
71
+ switch (orientation) {
72
+ case UIInterfaceOrientationPortraitUpsideDown:
73
+ avOrientation = AVCaptureVideoOrientationPortraitUpsideDown;
74
+ break;
75
+ case UIInterfaceOrientationLandscapeLeft:
76
+ avOrientation = AVCaptureVideoOrientationLandscapeLeft;
77
+ break;
78
+ case UIInterfaceOrientationLandscapeRight:
79
+ avOrientation = AVCaptureVideoOrientationLandscapeRight;
80
+ break;
81
+ default:
82
+ avOrientation = AVCaptureVideoOrientationPortrait;
83
+ break;
84
+ }
85
+ connection.videoOrientation = avOrientation;
86
+ }
87
+ }
88
+
89
+ - (void)didMoveToWindow {
90
+ [super didMoveToWindow];
91
+ if (self.window) {
92
+ NSLog(@"[RNCameraView] View moved to window, requesting permissions");
93
+ [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL cameraGranted) {
94
+ [AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio completionHandler:^(BOOL audioGranted) {
95
+ dispatch_async(self.sessionQueue, ^{
96
+ NSLog(@"[RNCameraView] Starting session running");
97
+ [self.session startRunning];
98
+ });
99
+ }];
100
+ }];
101
+ } else {
102
+ dispatch_async(self.sessionQueue, ^{
103
+ NSLog(@"[RNCameraView] Stopping session running");
104
+ [self.session stopRunning];
105
+ });
106
+ }
107
+ }
108
+
109
+ - (void)setFacing:(NSString *)facing {
110
+ if ([_facing isEqualToString:facing]) return;
111
+ _facing = [facing copy];
112
+ [self configureInputs];
113
+ }
114
+
115
+ - (void)setPhotoTrigger:(NSString *)photoTrigger {
116
+ if (!photoTrigger || [photoTrigger length] == 0 || [_photoTrigger isEqualToString:photoTrigger]) return;
117
+ _photoTrigger = [photoTrigger copy];
118
+ [self capturePhotoWithResolver:^(id result) {
119
+ if (self.onPhotoCaptured) {
120
+ self.onPhotoCaptured(result);
121
+ }
122
+ } rejecter:^(NSString *code, NSString *message, NSError *error) {
123
+ if (self.onPhotoCaptured) {
124
+ self.onPhotoCaptured(@{@"error": message ?: @"Capture failed"});
125
+ }
126
+ }];
127
+ }
128
+
129
+ - (void)setRecordTrigger:(NSString *)recordTrigger {
130
+ if ([_recordTrigger isEqualToString:recordTrigger]) return;
131
+ _recordTrigger = [recordTrigger copy];
132
+
133
+ if ([recordTrigger isEqualToString:@"start"]) {
134
+ [self startRecordingWithResolver:^(id result) {
135
+ if (self.onRecordStarted) {
136
+ self.onRecordStarted(@{});
137
+ }
138
+ } rejecter:^(NSString *code, NSString *message, NSError *error) {
139
+ if (self.onRecordStarted) {
140
+ self.onRecordStarted(@{@"error": message ?: @"Start recording failed"});
141
+ }
142
+ }];
143
+ } else if ([recordTrigger isEqualToString:@"stop"]) {
144
+ [self stopRecordingWithResolver:^(id result) {
145
+ if (self.onRecordStopped) {
146
+ self.onRecordStopped(result);
147
+ }
148
+ } rejecter:^(NSString *code, NSString *message, NSError *error) {
149
+ if (self.onRecordStopped) {
150
+ self.onRecordStopped(@{@"error": message ?: @"Stop recording failed"});
151
+ }
152
+ }];
153
+ }
154
+ }
155
+
156
+ - (void)configureInputs {
157
+ dispatch_async(self.sessionQueue, ^{
158
+ NSLog(@"[RNCameraView] Configuring inputs for facing: %@", self.facing);
159
+ [self.session beginConfiguration];
160
+
161
+ // Remove existing inputs
162
+ if (self.videoInput) {
163
+ [self.session removeInput:self.videoInput];
164
+ self.videoInput = nil;
165
+ }
166
+ if (self.audioInput) {
167
+ [self.session removeInput:self.audioInput];
168
+ self.audioInput = nil;
169
+ }
170
+
171
+ // Setup Video Device
172
+ AVCaptureDevicePosition position = [self.facing isEqualToString:@"back"] ? AVCaptureDevicePositionBack : AVCaptureDevicePositionFront;
173
+ AVCaptureDevice *videoDevice = nil;
174
+
175
+ // Search for device
176
+ AVCaptureDeviceDiscoverySession *discoverySession = [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:@[AVCaptureDeviceTypeBuiltInWideAngleCamera] mediaType:AVMediaTypeVideo position:position];
177
+ for (AVCaptureDevice *device in discoverySession.devices) {
178
+ if (device.position == position) {
179
+ videoDevice = device;
180
+ break;
181
+ }
182
+ }
183
+ if (!videoDevice) {
184
+ videoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
185
+ }
186
+
187
+ if (videoDevice) {
188
+ NSError *error = nil;
189
+ self.videoInput = [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:&error];
190
+ if (error) {
191
+ NSLog(@"[RNCameraView] Error creating video input: %@", error.localizedDescription);
192
+ }
193
+ if (self.videoInput && [self.session canAddInput:self.videoInput]) {
194
+ [self.session addInput:self.videoInput];
195
+ NSLog(@"[RNCameraView] Video input added successfully");
196
+ } else {
197
+ NSLog(@"[RNCameraView] Cannot add video input");
198
+ }
199
+ } else {
200
+ NSLog(@"[RNCameraView] No video device found");
201
+ }
202
+
203
+ // Setup Audio Device
204
+ AVCaptureDevice *audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
205
+ if (audioDevice) {
206
+ NSError *error = nil;
207
+ self.audioInput = [AVCaptureDeviceInput deviceInputWithDevice:audioDevice error:&error];
208
+ if (error) {
209
+ NSLog(@"[RNCameraView] Error creating audio input: %@", error.localizedDescription);
210
+ }
211
+ if (self.audioInput && [self.session canAddInput:self.audioInput]) {
212
+ [self.session addInput:self.audioInput];
213
+ NSLog(@"[RNCameraView] Audio input added successfully");
214
+ } else {
215
+ NSLog(@"[RNCameraView] Cannot add audio input");
216
+ }
217
+ } else {
218
+ NSLog(@"[RNCameraView] No audio device found");
219
+ }
220
+
221
+ [self.session commitConfiguration];
222
+ });
223
+ }
224
+
225
+ // Photo capture method
226
+ - (void)capturePhotoWithResolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject {
227
+ self.photoResolve = resolve;
228
+ self.photoReject = reject;
229
+
230
+ dispatch_async(self.sessionQueue, ^{
231
+ AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings];
232
+ [self.photoOutput capturePhotoWithSettings:settings delegate:self];
233
+ });
234
+ }
235
+
236
+ // Photo delegate
237
+ - (void)captureOutput:(AVCapturePhotoOutput *)output didFinishProcessingPhoto:(AVCapturePhoto *)photo error:(NSError *)error {
238
+ if (error) {
239
+ if (self.photoReject) self.photoReject(@"capture_error", error.localizedDescription, error);
240
+ return;
241
+ }
242
+ NSData *data = [photo fileDataRepresentation];
243
+ UIImage *image = [UIImage imageWithData:data];
244
+
245
+ // Save to temp folder
246
+ NSString *fileName = [NSString stringWithFormat:@"photo_%f.jpg", [[NSDate date] timeIntervalSince1970]];
247
+ NSString *path = [NSTemporaryDirectory() stringByAppendingPathComponent:fileName];
248
+ [data writeToFile:path atomically:YES];
249
+
250
+ if (self.photoResolve) {
251
+ self.photoResolve(@{
252
+ @"uri": [NSURL fileURLWithPath:path].absoluteString,
253
+ @"width": @(image.size.width),
254
+ @"height": @(image.size.height)
255
+ });
256
+ }
257
+ }
258
+
259
+ // Video recording methods
260
+ - (void)startRecordingWithResolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject {
261
+ if (self.movieOutput.isRecording) {
262
+ reject(@"already_recording", @"Camera is already recording video.", nil);
263
+ return;
264
+ }
265
+
266
+ self.recordResolve = resolve;
267
+ self.recordReject = reject;
268
+
269
+ NSString *fileName = [NSString stringWithFormat:@"video_%f.mp4", [[NSDate date] timeIntervalSince1970]];
270
+ NSString *path = [NSTemporaryDirectory() stringByAppendingPathComponent:fileName];
271
+ NSURL *fileURL = [NSURL fileURLWithPath:path];
272
+
273
+ dispatch_async(self.sessionQueue, ^{
274
+ // Check orientation connection
275
+ AVCaptureConnection *connection = [self.movieOutput connectionWithMediaType:AVMediaTypeVideo];
276
+ if (connection && connection.supportsVideoOrientation) {
277
+ dispatch_async(dispatch_get_main_queue(), ^{
278
+ UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation];
279
+ AVCaptureVideoOrientation avOrientation;
280
+ switch (orientation) {
281
+ case UIInterfaceOrientationPortraitUpsideDown:
282
+ avOrientation = AVCaptureVideoOrientationPortraitUpsideDown;
283
+ break;
284
+ case UIInterfaceOrientationLandscapeLeft:
285
+ avOrientation = AVCaptureVideoOrientationLandscapeLeft;
286
+ break;
287
+ case UIInterfaceOrientationLandscapeRight:
288
+ avOrientation = AVCaptureVideoOrientationLandscapeRight;
289
+ break;
290
+ default:
291
+ avOrientation = AVCaptureVideoOrientationPortrait;
292
+ break;
293
+ }
294
+ dispatch_async(self.sessionQueue, ^{
295
+ connection.videoOrientation = avOrientation;
296
+ [self.movieOutput startRecordingToOutputFileURL:fileURL recordingDelegate:self];
297
+ });
298
+ });
299
+ } else {
300
+ [self.movieOutput startRecordingToOutputFileURL:fileURL recordingDelegate:self];
301
+ }
302
+ });
303
+ }
304
+
305
+ - (void)stopRecordingWithResolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject {
306
+ if (!self.movieOutput.isRecording) {
307
+ reject(@"not_recording", @"Camera is not recording.", nil);
308
+ return;
309
+ }
310
+
311
+ self.recordResolve = resolve;
312
+ self.recordReject = reject;
313
+
314
+ dispatch_async(self.sessionQueue, ^{
315
+ [self.movieOutput stopRecording];
316
+ });
317
+ }
318
+
319
+ // Recording delegate
320
+ - (void)captureOutput:(AVCaptureFileOutput *)output didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL fromConnections:(NSArray<AVCaptureConnection *> *)connections error:(NSError *)error {
321
+ if (error && error.code != NSURLErrorUnknown) {
322
+ if (self.recordReject) self.recordReject(@"recording_error", error.localizedDescription, error);
323
+ return;
324
+ }
325
+
326
+ AVURLAsset *asset = [AVURLAsset URLAssetWithURL:outputFileURL options:nil];
327
+ CMTime duration = asset.duration;
328
+ float durationMs = CMTimeGetSeconds(duration) * 1000.0;
329
+
330
+ AVAssetTrack *track = [[asset tracksWithMediaType:AVMediaTypeVideo] firstObject];
331
+ CGSize size = CGSizeMake(1280, 720); // fallback
332
+ if (track) {
333
+ CGSize natSize = track.naturalSize;
334
+ CGAffineTransform t = track.preferredTransform;
335
+ BOOL isPortrait = (t.a == 0 && t.d == 0 && (t.b == 1 || t.b == -1) && (t.c == 1 || t.c == -1));
336
+ size = isPortrait ? CGSizeMake(natSize.height, natSize.width) : natSize;
337
+ }
338
+
339
+ if (self.recordResolve) {
340
+ self.recordResolve(@{
341
+ @"uri": outputFileURL.absoluteString,
342
+ @"durationMs": @(durationMs),
343
+ @"width": @(size.width),
344
+ @"height": @(size.height)
345
+ });
346
+ }
347
+ }
348
+
349
+ @end
350
+
351
+ // Companion Bridge Module
352
+ @interface RNCameraModule : NSObject <RCTBridgeModule>
353
+ @end
354
+
355
+ @implementation RNCameraModule
356
+ RCT_EXPORT_MODULE(RNCameraModule)
357
+
358
+ @synthesize bridge = _bridge;
359
+
360
+ RCT_EXPORT_METHOD(capturePhoto:(nonnull NSNumber *)reactTag
361
+ resolver:(RCTPromiseResolveBlock)resolve
362
+ rejecter:(RCTPromiseRejectBlock)reject) {
363
+ [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *,UIView *> *viewRegistry) {
364
+ RNCameraView *view = (RNCameraView *)viewRegistry[reactTag];
365
+ if (!view || ![view isKindOfClass:[RNCameraView class]]) {
366
+ reject(@"error", @"Camera view not found", nil);
367
+ } else {
368
+ [view capturePhotoWithResolver:resolve rejecter:reject];
369
+ }
370
+ }];
371
+ }
372
+
373
+ RCT_EXPORT_METHOD(startRecording:(nonnull NSNumber *)reactTag
374
+ resolver:(RCTPromiseResolveBlock)resolve
375
+ rejecter:(RCTPromiseRejectBlock)reject) {
376
+ [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *,UIView *> *viewRegistry) {
377
+ RNCameraView *view = (RNCameraView *)viewRegistry[reactTag];
378
+ if (!view || ![view isKindOfClass:[RNCameraView class]]) {
379
+ reject(@"error", @"Camera view not found", nil);
380
+ } else {
381
+ [view startRecordingWithResolver:resolve rejecter:reject];
382
+ }
383
+ }];
384
+ }
385
+
386
+ RCT_EXPORT_METHOD(stopRecording:(nonnull NSNumber *)reactTag
387
+ resolver:(RCTPromiseResolveBlock)resolve
388
+ rejecter:(RCTPromiseRejectBlock)reject) {
389
+ [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *,UIView *> *viewRegistry) {
390
+ RNCameraView *view = (RNCameraView *)viewRegistry[reactTag];
391
+ if (!view || ![view isKindOfClass:[RNCameraView class]]) {
392
+ reject(@"error", @"Camera view not found", nil);
393
+ } else {
394
+ [view stopRecordingWithResolver:resolve rejecter:reject];
395
+ }
396
+ }];
397
+ }
398
+
399
+ @end
400
+
401
+ // View Manager
402
+ @interface RNCameraViewManager : RCTViewManager
403
+ @end
404
+
405
+ @implementation RNCameraViewManager
406
+
407
+ RCT_EXPORT_MODULE(RNCameraView)
408
+
409
+ - (UIView *)view {
410
+ return [[RNCameraView alloc] initWithFrame:CGRectZero];
411
+ }
412
+
413
+ RCT_EXPORT_VIEW_PROPERTY(facing, NSString)
414
+ RCT_EXPORT_VIEW_PROPERTY(photoTrigger, NSString)
415
+ RCT_EXPORT_VIEW_PROPERTY(recordTrigger, NSString)
416
+ RCT_EXPORT_VIEW_PROPERTY(onPhotoCaptured, RCTDirectEventBlock)
417
+ RCT_EXPORT_VIEW_PROPERTY(onRecordStarted, RCTDirectEventBlock)
418
+ RCT_EXPORT_VIEW_PROPERTY(onRecordStopped, RCTDirectEventBlock)
419
+
420
+ @end
@@ -0,0 +1,61 @@
1
+ #import <React/RCTBridgeModule.h>
2
+ #import <AVFoundation/AVFoundation.h>
3
+
4
+ @interface RNFrameGrabber : NSObject <RCTBridgeModule>
5
+ @end
6
+
7
+ @implementation RNFrameGrabber
8
+
9
+ RCT_EXPORT_MODULE(RNFrameGrabber)
10
+
11
+ + (BOOL)requiresMainQueueSetup { return NO; }
12
+
13
+ - (NSURL *)cleanURL:(NSString *)uriString {
14
+ NSURL *url = [NSURL URLWithString:uriString];
15
+ if ([url.scheme isEqualToString:@"file"]) {
16
+ return [NSURL fileURLWithPath:url.path];
17
+ }
18
+ return url;
19
+ }
20
+
21
+ RCT_REMAP_METHOD(captureFrame,
22
+ captureFrameWithUri:(NSString *)uriString
23
+ options:(NSDictionary *)options
24
+ resolver:(RCTPromiseResolveBlock)resolve
25
+ rejecter:(RCTPromiseRejectBlock)reject)
26
+ {
27
+ NSURL *url = [self cleanURL:uriString];
28
+ if (!url) {
29
+ reject(@"bad_uri", @"Invalid video uri", nil);
30
+ return;
31
+ }
32
+
33
+ AVAsset *asset = [AVAsset assetWithURL:url];
34
+ NSNumber *timeMs = options[@"timeMs"] ?: @0;
35
+ CMTime time = CMTimeMakeWithSeconds(timeMs.doubleValue / 1000.0, 600);
36
+
37
+ AVAssetImageGenerator *generator = [[AVAssetImageGenerator alloc] initWithAsset:asset];
38
+ generator.appliesPreferredTrackTransform = YES;
39
+
40
+ NSError *error = nil;
41
+ CGImageRef imageRef = [generator copyCGImageAtTime:time actualTime:NULL error:&error];
42
+ if (!imageRef) {
43
+ reject(@"frame_failed", error.localizedDescription ?: @"Could not capture frame", error);
44
+ return;
45
+ }
46
+
47
+ UIImage *image = [UIImage imageWithCGImage:imageRef];
48
+ CGImageRelease(imageRef);
49
+
50
+ NSURL *outUrl = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:[NSString stringWithFormat:@"frame_%@.jpg", [NSUUID UUID].UUIDString]]];
51
+ NSData *outData = UIImageJPEGRepresentation(image, 0.9);
52
+ if (![outData writeToURL:outUrl atomically:YES]) {
53
+ reject(@"write_failed", @"Failed to write frame", nil);
54
+ return;
55
+ }
56
+
57
+ resolve(outUrl.absoluteString);
58
+ }
59
+
60
+ @end
61
+