@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,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
|
+
|