react-native-video-trim 5.0.4 → 5.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/ios/VideoTrim.mm CHANGED
@@ -1,663 +1,180 @@
1
1
  #import "VideoTrim.h"
2
- #import "ProgressAlertController.h"
3
- #import "VideoTrimmerViewController.h"
4
- #import "AssetLoader.h"
5
- #import <React/RCTBridgeModule.h>
6
- #import <React/RCTUtils.h>
7
- #import <React/RCTConvert.h>
8
- #import <AVFoundation/AVFoundation.h>
9
- #import <Photos/Photos.h>
10
- #import <UIKit/UIKit.h>
11
- #import <ffmpegkit/FFmpegKit.h>
12
- #import <ffmpegkit/FFmpegKitConfig.h>
2
+ #import <VideoTrim-Swift.h>
13
3
 
14
4
  @implementation VideoTrim {
15
- std::optional<JS::NativeVideoTrim::EditorConfig> _editorConfig;
5
+ VideoTrimSwift * _Nullable videoTrim;
16
6
  }
17
7
 
18
8
  RCT_EXPORT_MODULE()
19
9
 
20
10
  - (instancetype)init {
21
- if (self = [super init]) {
22
- self.FILE_PREFIX = @"trimmedVideo";
23
- self.BEFORE_TRIM_PREFIX = @"beforeTrim";
24
- self.isShowing = NO;
25
- self.vc = nil;
26
- self.outputFile = nil;
27
- self.isVideoType = YES;
28
-
29
- }
30
- return self;
31
- }
32
-
33
- // Add custom getter and setter
34
- - (JS::NativeVideoTrim::EditorConfig)editorConfig {
35
- if (_editorConfig.has_value()) {
36
- return _editorConfig.value();
37
- }
38
- // This shouldn't happen if properly initialized
39
- @throw [NSException exceptionWithName:@"EditorConfigNotInitialized"
40
- reason:@"EditorConfig accessed before initialization"
41
- userInfo:nil];
11
+ if (self = [super init]) {
12
+ // self.BEFORE_TRIM_PREFIX = @"beforeTrim";
13
+ }
14
+ return self;
42
15
  }
43
16
 
44
- - (void)setEditorConfig:(JS::NativeVideoTrim::EditorConfig)config {
45
- _editorConfig = config;
17
+ // MARK: swift static methods
18
+ - (void)cleanFiles:(nonnull RCTPromiseResolveBlock)resolve
19
+ reject:(nonnull RCTPromiseRejectBlock)reject {
20
+ NSInteger successCount = [VideoTrimSwift cleanFiles];
21
+ resolve(@(successCount));
46
22
  }
47
23
 
48
- - (void)cleanFiles:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
49
- NSURL *documentsDirectory = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] firstObject];
50
- NSError *error = nil;
51
- NSArray *directoryContents = [[NSFileManager defaultManager] contentsOfDirectoryAtURL:documentsDirectory includingPropertiesForKeys:nil options:0 error:&error];
52
- int successCount = 0;
53
- for (NSURL *fileURL in directoryContents) {
54
- NSString *last = [fileURL lastPathComponent];
55
- if ([last hasPrefix:self.FILE_PREFIX] || [last hasPrefix:self.BEFORE_TRIM_PREFIX]) {
56
- NSError *removeError = nil;
57
- [[NSFileManager defaultManager] removeItemAtURL:fileURL error:&removeError];
58
- if (!removeError) {
59
- successCount++;
60
- }
61
- }
62
- }
63
- resolve(@(successCount));
64
- }
65
-
66
- - (void)closeEditor {
67
- if (!self.vc) return;
68
- dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
69
- [self.vc dismissViewControllerAnimated:YES completion:^{
70
- [self emitOnHide];
71
- self.isShowing = NO;
72
- }];
73
- });
24
+ - (void)deleteFile:(nonnull NSString *)filePath
25
+ resolve:(nonnull RCTPromiseResolveBlock)resolve
26
+ reject:(nonnull RCTPromiseRejectBlock)reject {
27
+ resolve(@([VideoTrimSwift deleteFile:filePath]));
74
28
  }
75
29
 
76
- - (void)deleteFile:(nonnull NSString *)filePath resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
77
- NSURL *url = [NSURL URLWithString:filePath];
78
- NSError *error = nil;
79
- if ([[NSFileManager defaultManager] fileExistsAtPath:[url path]]) {
80
- [[NSFileManager defaultManager] removeItemAtURL:url error:&error];
81
- if (error) {
82
- NSLog(@"[deleteFile] Error: %@", error);
83
- reject(@"delete_file_error", @"Failed to delete file", error);
84
- resolve(@(NO));
85
- return;
86
- }
87
- resolve(@(YES));
88
- return;
89
- }
90
- resolve(@(NO));
91
- }
92
-
93
- - (void)isValidFile:(nonnull NSString *)url resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
94
- NSURL *fileURL = [NSURL URLWithString:url];
95
- AVAsset *asset = [AVAsset assetWithURL:fileURL];
96
- NSArray *videoTracks = [asset tracksWithMediaType:AVMediaTypeVideo];
97
- NSArray *audioTracks = [asset tracksWithMediaType:AVMediaTypeAudio];
98
- BOOL isValid = (videoTracks.count > 0 || audioTracks.count > 0);
99
- NSString *fileType = videoTracks.count > 0 ? @"video" : (audioTracks.count > 0 ? @"audio" : @"unknown");
100
- double durationMs = CMTimeGetSeconds(asset.duration) * 1000;
101
- NSDictionary *result = @{ @"isValid": @(isValid), @"fileType": fileType, @"duration": @(isValid ? durationMs : -1) };
30
+ - (void)isValidFile:(nonnull NSString *)url
31
+ resolve:(nonnull RCTPromiseResolveBlock)resolve
32
+ reject:(nonnull RCTPromiseRejectBlock)reject {
33
+ [VideoTrimSwift isValidFile:url url:^(NSDictionary<NSString *,id> * _Nonnull result) {
102
34
  resolve(result);
103
- }
104
-
105
- - (void)listFiles:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
106
- NSMutableArray *files = [NSMutableArray array];
107
- NSURL *documentsDirectory = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] firstObject];
108
- NSError *error = nil;
109
- NSArray *directoryContents = [[NSFileManager defaultManager] contentsOfDirectoryAtURL:documentsDirectory includingPropertiesForKeys:nil options:0 error:&error];
110
- if (error) {
111
- NSLog(@"[listFiles] Error: %@", error);
112
- reject(@"list_files_error", @"Failed to list files", error);
113
- return;
114
- }
115
- for (NSURL *fileURL in directoryContents) {
116
- NSString *last = [fileURL lastPathComponent];
117
- if ([last hasPrefix:self.FILE_PREFIX] || [last hasPrefix:self.BEFORE_TRIM_PREFIX]) {
118
- [files addObject:[fileURL absoluteString]];
119
- }
120
- }
121
- resolve(files);
122
- }
123
-
124
- - (void)showEditor:(nonnull NSString *)filePath config:(JS::NativeVideoTrim::EditorConfig &)config {
125
- if (self.isShowing) return;
126
-
127
- self.editorConfig = config;
128
- self.isVideoType = [config.type() isEqualToString:@"video"];
129
-
130
- NSURL *destPath = nil;
131
-
132
- if ([filePath hasPrefix:@"http://"] || [filePath hasPrefix:@"https://"]) {
133
- destPath = [NSURL URLWithString:filePath];
35
+ }];
36
+ }
37
+
38
+ - (void)listFiles:(nonnull RCTPromiseResolveBlock)resolve
39
+ reject:(nonnull RCTPromiseRejectBlock)reject {
40
+
41
+ resolve([VideoTrimSwift listFiles]);
42
+ }
43
+
44
+ - (void)trim:(nonnull NSString *)url
45
+ options:(JS::NativeVideoTrim::TrimOptions &)options
46
+ resolve:(nonnull RCTPromiseResolveBlock)resolve
47
+ reject:(nonnull RCTPromiseRejectBlock)reject {
48
+ // TODO: implement
49
+ if (!self->videoTrim) {
50
+ self->videoTrim = [[VideoTrimSwift alloc] init];
51
+ }
52
+
53
+ NSMutableDictionary *dict = [NSMutableDictionary dictionary];
54
+
55
+ dict[@"saveToPhoto"] = @(options.saveToPhoto());
56
+ dict[@"type"] = options.type();
57
+ dict[@"outputExt"] = options.outputExt();
58
+ dict[@"openDocumentsOnFinish"] = @(options.openDocumentsOnFinish());
59
+ dict[@"openShareSheetOnFinish"] = @(options.openShareSheetOnFinish());
60
+ dict[@"removeAfterSavedToPhoto"] = @(options.removeAfterSavedToPhoto());
61
+ dict[@"removeAfterFailedToSavePhoto"] = @(options.removeAfterFailedToSavePhoto());
62
+ dict[@"removeAfterSavedToDocuments"] = @(options.removeAfterSavedToDocuments());
63
+ dict[@"removeAfterFailedToSaveDocuments"] = @(options.removeAfterFailedToSaveDocuments());
64
+ dict[@"removeAfterShared"] = @(options.removeAfterShared());
65
+ dict[@"removeAfterFailedToShare"] = @(options.removeAfterFailedToShare());
66
+ dict[@"enableRotation"] = @(options.enableRotation());
67
+ dict[@"rotationAngle"] = @(options.rotationAngle());
68
+ dict[@"startTime"] = @(options.startTime());
69
+ dict[@"endTime"] = @(options.endTime());
70
+
71
+ [self->videoTrim trim:url url:dict config:^(NSDictionary<NSString *,id> * _Nullable result) {
72
+ if (!result) {
73
+ reject(@"ERR_TRIM_FAILED", @"Trim failed", nil);
134
74
  } else {
135
- NSLog(@"before rename");
136
- destPath = [self renameFileAtURL:[NSURL URLWithString:filePath] newName:self.BEFORE_TRIM_PREFIX];
137
- }
138
-
139
- if (!destPath) {
140
- [self onError:@"Fail to rename file" code:@"INVALID_FILE_PATH"];
141
- self.isShowing = NO;
142
- return;
143
- }
144
-
145
- NSLog(@"✅ destPath created: %@", destPath);
146
-
147
-
148
- dispatch_async(dispatch_get_main_queue(), ^{
149
- self.vc = [[VideoTrimmerViewController alloc] init];
150
- if (!self.vc) return;
151
-
152
- [self.vc configureWithConfig:self.editorConfig];
153
-
154
- __weak __typeof__(self) weakSelf = self;
155
-
156
- self.vc.cancelBtnClicked = ^{
157
- if (!weakSelf.editorConfig.enableCancelDialog()) {
158
- [weakSelf emitOnCancel];
159
- [weakSelf.vc dismissViewControllerAnimated:YES completion:^{
160
- [weakSelf emitOnHide];
161
- weakSelf.isShowing = NO;
162
- }];
163
- return;
164
- }
165
-
166
- UIAlertController *dialogMessage = [UIAlertController alertControllerWithTitle:weakSelf.editorConfig.cancelDialogTitle()
167
- message:weakSelf.editorConfig.cancelDialogMessage()
168
- preferredStyle:UIAlertControllerStyleAlert];
169
- dialogMessage.overrideUserInterfaceStyle = UIUserInterfaceStyleDark;
170
-
171
- UIAlertAction *ok = [UIAlertAction actionWithTitle:weakSelf.editorConfig.cancelDialogConfirmText()
172
- style:UIAlertActionStyleDestructive
173
- handler:^(UIAlertAction *action) {
174
- [weakSelf emitOnCancel];
175
- [weakSelf.vc dismissViewControllerAnimated:YES completion:^{
176
- [weakSelf emitOnHide];
177
- weakSelf.isShowing = NO;
178
- }];
179
- }];
180
-
181
- UIAlertAction *cancel = [UIAlertAction actionWithTitle:weakSelf.editorConfig.cancelDialogCancelText()
182
- style:UIAlertActionStyleCancel
183
- handler:nil];
184
-
185
- [dialogMessage addAction:ok];
186
- [dialogMessage addAction:cancel];
187
-
188
- UIViewController *root = RCTPresentedViewController();
189
- if (root) {
190
- [root presentViewController:dialogMessage animated:YES completion:nil];
191
- }
192
- };
193
-
194
- self.vc.saveBtnClicked = ^(CMTimeRange selectedRange) {
195
- if (!weakSelf.editorConfig.enableSaveDialog()) {
196
- [weakSelf trimWithViewController:weakSelf.vc
197
- inputFile:destPath
198
- videoDuration:CMTimeGetSeconds(weakSelf.vc.asset.duration)
199
- startTime:CMTimeGetSeconds(selectedRange.start)
200
- endTime:CMTimeGetSeconds(CMTimeRangeGetEnd(selectedRange))];
201
- return;
202
- }
203
-
204
- UIAlertController *dialogMessage = [UIAlertController alertControllerWithTitle:weakSelf.editorConfig.saveDialogTitle()
205
- message:weakSelf.editorConfig.saveDialogMessage()
206
- preferredStyle:UIAlertControllerStyleAlert];
207
- dialogMessage.overrideUserInterfaceStyle = UIUserInterfaceStyleDark;
208
-
209
- UIAlertAction *ok = [UIAlertAction actionWithTitle:weakSelf.editorConfig.saveDialogConfirmText()
210
- style:UIAlertActionStyleDefault
211
- handler:^(UIAlertAction *action) {
212
- [weakSelf trimWithViewController:weakSelf.vc
213
- inputFile:destPath
214
- videoDuration:CMTimeGetSeconds(weakSelf.vc.asset.duration)
215
- startTime:CMTimeGetSeconds(selectedRange.start)
216
- endTime:CMTimeGetSeconds(CMTimeRangeGetEnd(selectedRange))];
217
- }];
218
-
219
- UIAlertAction *cancel = [UIAlertAction actionWithTitle:weakSelf.editorConfig.saveDialogCancelText()
220
- style:UIAlertActionStyleCancel
221
- handler:nil];
222
-
223
- [dialogMessage addAction:ok];
224
- [dialogMessage addAction:cancel];
225
-
226
- UIViewController *root = RCTPresentedViewController();
227
- if (root) {
228
- [root presentViewController:dialogMessage animated:YES completion:nil];
229
- }
230
- };
231
-
232
- self.vc.modalInPresentation = YES;
233
-
234
- if (self.editorConfig.fullScreenModalIOS()) {
235
- self.vc.modalPresentationStyle = UIModalPresentationFullScreen;
236
- }
237
-
238
- UIViewController *root = RCTPresentedViewController();
239
- if (root) {
240
- [root presentViewController:self.vc animated:YES completion:^{
241
- [self emitOnShow];
242
- self.isShowing = YES;
243
-
244
- AssetLoader *assetLoader = [[AssetLoader alloc] init];
245
- assetLoader.delegate = self;
246
- NSLog(@"🔄 Starting to load asset from: %@", destPath);
247
- [assetLoader loadAssetWithURL:destPath isVideoType:self.isVideoType];
248
- }];
249
- }
250
- });
251
- }
252
-
253
- - (void)trim:(nonnull NSString *)url options:(JS::NativeVideoTrim::TrimOptions &)options resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
254
- NSURL *inputURL = [NSURL URLWithString:url];
255
- if (!inputURL) {
256
- reject(@"INVALID_URL", @"Invalid input URL", nil);
257
- return;
258
- }
259
-
260
- // Create output file
261
- NSTimeInterval timestamp = [[NSDate date] timeIntervalSince1970];
262
- NSString *outputName = [NSString stringWithFormat:@"%@_%d.%@", self.FILE_PREFIX, (int)timestamp, options.outputExt()];
263
- NSURL *documentsDirectory = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] firstObject];
264
- NSURL *outputURL = [documentsDirectory URLByAppendingPathComponent:outputName];
265
-
266
- // Prepare FFmpeg command
267
- NSMutableArray *cmds = [NSMutableArray arrayWithObjects:
268
- @"-ss", [NSString stringWithFormat:@"%.3f", options.startTime() / 1000.0],
269
- @"-to", [NSString stringWithFormat:@"%.3f", options.endTime() / 1000.0],
270
- @"-i", inputURL.absoluteString,
271
- @"-c", @"copy",
272
- outputURL.absoluteString, nil];
273
-
274
- [FFmpegKit executeWithArgumentsAsync:cmds
275
- withCompleteCallback:^(FFmpegSession* session) {
276
- SessionState state = [session getState];
277
- ReturnCode *returnCode = [session getReturnCode];
278
-
279
- if ([ReturnCode isSuccess:returnCode]) {
280
- NSDictionary *result = @{
281
- @"outputPath": outputURL.absoluteString,
282
- @"startTime": [NSNumber numberWithDouble:options.startTime()],
283
- @"endTime": [NSNumber numberWithDouble:options.endTime()]
284
- };
285
- resolve(result);
286
- } else {
287
- NSString *errorMessage = [NSString stringWithFormat:@"Trimming failed with state %@ and rc %@",
288
- [FFmpegKitConfig sessionStateToString:state],
289
- returnCode];
290
- reject(@"TRIMMING_FAILED", errorMessage, nil);
291
- }
292
- } withLogCallback:nil withStatisticsCallback:nil];
293
- }
294
-
295
- - (void)trimWithViewController:(VideoTrimmerViewController *)viewController
296
- inputFile:(NSURL *)inputFile
297
- videoDuration:(double)videoDuration
298
- startTime:(double)startTime
299
- endTime:(double)endTime {
300
- [self.vc pausePlayer];
301
-
302
- NSTimeInterval timestamp = [[NSDate date] timeIntervalSince1970];
303
- NSString *outputName = [NSString stringWithFormat:@"%@_%d.%@", self.FILE_PREFIX, (int)timestamp, self.editorConfig.outputExt()];
304
- NSURL *documentsDirectory = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] firstObject];
305
- self.outputFile = [documentsDirectory URLByAppendingPathComponent:outputName];
306
-
307
- NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
308
- formatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss.SSSSSSZ";
309
- formatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"];
310
- NSString *dateTime = [formatter stringFromDate:[NSDate date]];
311
-
75
+ resolve(result);
76
+ }
77
+ }];
78
+ }
79
+
80
+ // MARK: swift instance methods
81
+ - (void)showEditor:(nonnull NSString *)filePath
82
+ config:(JS::NativeVideoTrim::EditorConfig &)config {
83
+ if (!self->videoTrim) {
84
+ self->videoTrim = [[VideoTrimSwift alloc] init];
85
+ self->videoTrim.delegate = self;
86
+ }
87
+
88
+ NSMutableDictionary *dict = [NSMutableDictionary dictionary];
89
+
90
+ dict[@"saveToPhoto"] = @(config.saveToPhoto());
91
+ dict[@"type"] = config.type();
92
+ dict[@"outputExt"] = config.outputExt();
93
+ dict[@"openDocumentsOnFinish"] = @(config.openDocumentsOnFinish());
94
+ dict[@"openShareSheetOnFinish"] = @(config.openShareSheetOnFinish());
95
+ dict[@"removeAfterSavedToPhoto"] = @(config.removeAfterSavedToPhoto());
96
+ dict[@"removeAfterFailedToSavePhoto"] = @(config.removeAfterFailedToSavePhoto());
97
+ dict[@"removeAfterSavedToDocuments"] = @(config.removeAfterSavedToDocuments());
98
+ dict[@"removeAfterFailedToSaveDocuments"] = @(config.removeAfterFailedToSaveDocuments());
99
+ dict[@"removeAfterShared"] = @(config.removeAfterShared());
100
+ dict[@"removeAfterFailedToShare"] = @(config.removeAfterFailedToShare());
101
+ dict[@"enableRotation"] = @(config.enableRotation());
102
+ dict[@"rotationAngle"] = @(config.rotationAngle());
103
+ dict[@"enableHapticFeedback"] = @(config.enableHapticFeedback());
104
+ dict[@"maxDuration"] = @(config.maxDuration());
105
+ dict[@"minDuration"] = @(config.minDuration());
106
+ dict[@"cancelButtonText"] = config.cancelButtonText();
107
+ dict[@"saveButtonText"] = config.saveButtonText();
108
+ dict[@"enableCancelDialog"] = @(config.enableCancelDialog());
109
+ dict[@"cancelDialogTitle"] = config.cancelDialogTitle();
110
+ dict[@"cancelDialogMessage"] = config.cancelDialogMessage();
111
+ dict[@"cancelDialogCancelText"] = config.cancelDialogCancelText();
112
+ dict[@"cancelDialogConfirmText"] = config.cancelDialogConfirmText();
113
+ dict[@"enableSaveDialog"] = @(config.enableSaveDialog());
114
+ dict[@"saveDialogTitle"] = config.saveDialogTitle();
115
+ dict[@"saveDialogMessage"] = config.saveDialogMessage();
116
+ dict[@"saveDialogCancelText"] = config.saveDialogCancelText();
117
+ dict[@"saveDialogConfirmText"] = config.saveDialogConfirmText();
118
+ dict[@"trimmingText"] = config.trimmingText();
119
+ dict[@"fullScreenModalIOS"] = @(config.fullScreenModalIOS());
120
+ dict[@"autoplay"] = @(config.autoplay());
121
+ dict[@"jumpToPositionOnLoad"] = @(config.jumpToPositionOnLoad());
122
+ dict[@"closeWhenFinish"] = @(config.closeWhenFinish());
123
+ dict[@"enableCancelTrimming"] = @(config.enableCancelTrimming());
124
+ dict[@"cancelTrimmingButtonText"] = config.cancelTrimmingButtonText();
125
+ dict[@"enableCancelTrimmingDialog"] = @(config.enableCancelTrimmingDialog());
126
+ dict[@"cancelTrimmingDialogTitle"] = config.cancelTrimmingDialogTitle();
127
+ dict[@"cancelTrimmingDialogMessage"] = config.cancelTrimmingDialogMessage();
128
+ dict[@"cancelTrimmingDialogCancelText"] = config.cancelTrimmingDialogCancelText();
129
+ dict[@"cancelTrimmingDialogConfirmText"] = config.cancelTrimmingDialogConfirmText();
130
+ dict[@"headerText"] = config.headerText();
131
+ dict[@"headerTextSize"] = @(config.headerTextSize());
132
+ dict[@"headerTextColor"] = @(config.headerTextColor());
133
+ dict[@"alertOnFailToLoad"] = @(config.alertOnFailToLoad());
134
+ dict[@"alertOnFailTitle"] = config.alertOnFailTitle();
135
+ dict[@"alertOnFailMessage"] = config.alertOnFailMessage();
136
+ dict[@"alertOnFailCloseText"] = config.alertOnFailCloseText();
137
+
138
+ [self->videoTrim showEditor:filePath withConfig:dict];
139
+ }
140
+
141
+ - (void)closeEditor {
142
+ if (self->videoTrim) {
143
+ [self->videoTrim closeEditor];
144
+ }
145
+ }
146
+
147
+ #pragma mark - VideoTrimDelegate methods
148
+ - (void)emitEventToJSWithEventName:(NSString * _Nonnull)eventName body:(NSDictionary<NSString *,id> * _Nullable)body {
149
+
150
+ if ([eventName isEqualToString:@"onLog"]) {
151
+ [self emitOnLog:body];
152
+ } else if ([eventName isEqualToString:@"onError"]) {
153
+ [self emitOnError:body];
154
+ } else if ([eventName isEqualToString:@"onLoad"]) {
155
+ [self emitOnLoad:body];
156
+ } else if ([eventName isEqualToString:@"onStartTrimming"]) {
312
157
  [self emitOnStartTrimming];
313
-
314
- __block FFmpegSession *ffmpegSession = nil;
315
- ProgressAlertController *progressAlert = [[ProgressAlertController alloc] init];
316
- progressAlert.modalPresentationStyle = UIModalPresentationOverFullScreen;
317
- progressAlert.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
318
- [progressAlert setTitle:self.editorConfig.trimmingText()];
319
-
320
- if (self.editorConfig.enableCancelTrimming()) {
321
- [progressAlert setCancelTitle:self.editorConfig.cancelTrimmingButtonText()];
322
- [progressAlert showCancelBtn];
323
- __weak __typeof__(progressAlert) weakProgressAlert = progressAlert;
324
-
325
- progressAlert.onDismiss = ^{
326
- if (self.editorConfig.enableCancelTrimmingDialog()) {
327
- UIAlertController *dialogMessage = [UIAlertController alertControllerWithTitle:self.editorConfig.cancelTrimmingDialogTitle()
328
- message:self.editorConfig.cancelTrimmingDialogMessage()
329
- preferredStyle:UIAlertControllerStyleAlert];
330
- dialogMessage.overrideUserInterfaceStyle = UIUserInterfaceStyleDark;
331
-
332
- UIAlertAction *ok = [UIAlertAction actionWithTitle:self.editorConfig.cancelTrimmingDialogConfirmText()
333
- style:UIAlertActionStyleDestructive
334
- handler:^(UIAlertAction *action) {
335
- if (ffmpegSession) {
336
- [ffmpegSession cancel];
337
- } else {
338
- [self emitOnCancelTrimming];
339
- }
340
- [weakProgressAlert dismissViewControllerAnimated:YES completion:nil];
341
- }];
342
-
343
- UIAlertAction *cancel = [UIAlertAction actionWithTitle:self.editorConfig.cancelTrimmingDialogCancelText()
344
- style:UIAlertActionStyleCancel
345
- handler:nil];
346
-
347
- [dialogMessage addAction:ok];
348
- [dialogMessage addAction:cancel];
349
-
350
- UIViewController *root = RCTPresentedViewController();
351
- if (root) {
352
- [root presentViewController:dialogMessage animated:YES completion:nil];
353
- }
354
- } else {
355
- if (ffmpegSession) {
356
- [ffmpegSession cancel];
357
- } else {
358
- [self emitOnCancelTrimming];
359
- }
360
- [weakProgressAlert dismissViewControllerAnimated:YES completion:nil];
361
- }
362
- };
363
- }
364
-
365
- UIViewController *root = RCTPresentedViewController();
366
- if (root) {
367
- [root presentViewController:progressAlert animated:YES completion:nil];
368
- }
369
-
370
- NSMutableArray *cmds = [NSMutableArray arrayWithObjects:
371
- @"-ss", [NSString stringWithFormat:@"%.0fms", startTime * 1000],
372
- @"-to", [NSString stringWithFormat:@"%.0fms", endTime * 1000], nil];
373
-
374
- if (self.editorConfig.enableRotation()) {
375
- [cmds addObjectsFromArray:@[
376
- @"-display_rotation", [NSString stringWithFormat:@"%.0f", self.editorConfig.rotationAngle()]
377
- ]];
378
- }
379
-
380
- [cmds addObjectsFromArray:@[
381
- @"-i", inputFile.absoluteString,
382
- @"-c", @"copy",
383
- @"-metadata", [NSString stringWithFormat:@"creation_time=%@", dateTime],
384
- self.outputFile.absoluteString
385
- ]];
386
-
387
- NSLog(@"Command: %@", [cmds componentsJoinedByString:@" "]);
388
-
389
- NSDictionary *eventPayload = @{ @"command": [cmds componentsJoinedByString:@" "] };
390
-
391
- [self emitOnLog:eventPayload];
392
-
393
- ffmpegSession = [FFmpegKit executeWithArgumentsAsync:cmds
394
- withCompleteCallback:^(FFmpegSession* session) {
395
- dispatch_async(dispatch_get_main_queue(), ^{
396
- [progressAlert dismissViewControllerAnimated:YES completion:^{
397
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
398
- SessionState state = [session getState];
399
- ReturnCode *returnCode = [session getReturnCode];
400
-
401
- if ([ReturnCode isSuccess:returnCode]) {
402
- NSDictionary *eventPayload = @{
403
- @"outputPath": self.outputFile.absoluteString,
404
- @"startTime": [NSString stringWithFormat:@"%.0f", startTime * 1000],
405
- @"endTime": [NSString stringWithFormat:@"%.0f", endTime * 1000],
406
- @"duration": [NSString stringWithFormat:@"%.0f", videoDuration * 1000]
407
- };
408
- [self emitOnFinishTrimming:eventPayload];
409
-
410
- if (self.editorConfig.saveToPhoto() && self.isVideoType) {
411
- [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) {
412
- if (status != PHAuthorizationStatusAuthorized) {
413
- [self onError:@"Permission to access Photo Library is not granted" code:@"NO_PHOTO_PERMISSION"];
414
- return;
415
- }
416
-
417
- [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
418
- PHAssetChangeRequest *request = [PHAssetChangeRequest creationRequestForAssetFromVideoAtFileURL:self.outputFile];
419
- request.creationDate = [NSDate date];
420
- } completionHandler:^(BOOL success, NSError *error) {
421
- if (success) {
422
- NSLog(@"Edited video saved to Photo Library successfully.");
423
- if (self.editorConfig.removeAfterSavedToPhoto()) {
424
- [self deleteFileAtURL:self.outputFile];
425
- }
426
- } else {
427
- [self onError:[NSString stringWithFormat:@"Failed to save edited video to Photo Library: %@", error.localizedDescription ?: @"Unknown error"] code:@"FAIL_TO_SAVE_TO_PHOTO"];
428
- if (self.editorConfig.removeAfterFailedToSavePhoto()) {
429
- [self deleteFileAtURL:self.outputFile];
430
- }
431
- }
432
- }];
433
- }];
434
- } else if (self.editorConfig.openDocumentsOnFinish()) {
435
- [self saveFileToFilesApp:self.outputFile];
436
- return;
437
- } else if (self.editorConfig.openShareSheetOnFinish()) {
438
- [self shareFile:self.outputFile];
439
- return;
440
- }
441
-
442
- if (self.editorConfig.closeWhenFinish()) {
443
- [self closeEditor];
444
- }
445
-
446
- } else if ([ReturnCode isCancel:returnCode]) {
447
- [self emitOnCancelTrimming];
448
- } else {
449
- NSString *errorMessage = [NSString stringWithFormat:@"Command failed with state %@ and rc %@.%@",
450
- [FFmpegKitConfig sessionStateToString:state],
451
- returnCode,
452
- [session getFailStackTrace] ?: @""];
453
- [self onError:errorMessage code:@"TRIMMING_FAILED"];
454
- if (self.editorConfig.closeWhenFinish()) {
455
- [self closeEditor];
456
- }
457
- }
458
- });
459
- }];
460
- });
461
- } withLogCallback:^(Log* log) {
462
- NSLog(@"FFmpeg process started with log %@", [log getMessage]);
463
- NSDictionary *eventPayload = @{
464
- @"level": @([log getLevel]),
465
- @"message": [log getMessage] ?: @"",
466
- @"sessionId": @([log getSessionId])
467
- };
468
- [self emitOnLog:eventPayload];
469
- } withStatisticsCallback:^(Statistics* statistics) {
470
- int timeInMilliseconds = [statistics getTime];
471
- if (timeInMilliseconds > 0) {
472
- double completePercentage = timeInMilliseconds / (videoDuration * 1000);
473
- dispatch_async(dispatch_get_main_queue(), ^{
474
- [progressAlert setProgress:(float)completePercentage];
475
- });
476
- }
477
-
478
- NSDictionary *eventPayload = @{
479
- @"sessionId": @([statistics getSessionId]),
480
- @"videoFrameNumber": @([statistics getVideoFrameNumber]),
481
- @"videoFps": @([statistics getVideoFps]),
482
- @"videoQuality": @([statistics getVideoQuality]),
483
- @"size": @([statistics getSize]),
484
- @"time": @([statistics getTime]),
485
- @"bitrate": @([statistics getBitrate]),
486
- @"speed": @([statistics getSpeed])
487
- };
488
- [self emitOnStatistics:eventPayload];
489
- }];
490
- }
491
-
492
- - (void)saveFileToFilesApp:(NSURL *)fileURL {
493
- dispatch_async(dispatch_get_main_queue(), ^{
494
- UIDocumentPickerViewController *documentPicker = [[UIDocumentPickerViewController alloc] initWithURL:fileURL inMode:UIDocumentPickerModeExportToService];
495
- documentPicker.delegate = self;
496
- documentPicker.modalPresentationStyle = UIModalPresentationFormSheet;
497
-
498
- UIViewController *root = RCTPresentedViewController();
499
- if (root) {
500
- [root presentViewController:documentPicker animated:YES completion:nil];
501
- }
502
- });
503
- }
504
-
505
- - (void)shareFile:(NSURL *)fileURL {
506
- dispatch_async(dispatch_get_main_queue(), ^{
507
- UIActivityViewController *activityViewController = [[UIActivityViewController alloc] initWithActivityItems:@[fileURL] applicationActivities:nil];
508
-
509
- activityViewController.completionWithItemsHandler = ^(UIActivityType activityType, BOOL completed, NSArray *returnedItems, NSError *error) {
510
- if (error) {
511
- NSString *message = [NSString stringWithFormat:@"Sharing error: %@", error.localizedDescription];
512
- NSLog(@"%@", message);
513
- [self onError:message code:@"FAIL_TO_SHARE"];
514
-
515
- if (self.editorConfig.removeAfterFailedToShare()) {
516
- [self deleteFileAtURL:fileURL];
517
- }
518
- return;
519
- }
520
-
521
- if (completed) {
522
- NSLog(@"User completed the sharing activity");
523
- if (self.editorConfig.removeAfterShared()) {
524
- [self deleteFileAtURL:fileURL];
525
- }
526
- } else {
527
- NSLog(@"User cancelled or failed to complete the sharing activity");
528
- if (self.editorConfig.removeAfterFailedToShare()) {
529
- [self deleteFileAtURL:fileURL];
530
- }
531
- }
532
-
533
- [self closeEditor];
534
- };
535
-
536
- UIViewController *root = RCTPresentedViewController();
537
- if (root) {
538
- [root presentViewController:activityViewController animated:YES completion:nil];
539
- }
540
- });
541
- }
542
-
543
- - (int)deleteFileAtURL:(NSURL *)url {
544
- NSError *error = nil;
545
- if ([[NSFileManager defaultManager] fileExistsAtPath:[url path]]) {
546
- [[NSFileManager defaultManager] removeItemAtURL:url error:&error];
547
- if (error) {
548
- NSLog(@"[deleteFile] Error deleting files: %@", error);
549
- return 2;
550
- }
551
- return 0;
552
- }
553
- return 1;
554
- }
555
-
556
- - (NSURL *)renameFileAtURL:(NSURL *)url newName:(NSString *)newName {
557
- NSString *fileExtension = url.pathExtension;
558
- NSTimeInterval timestamp = [[NSDate date] timeIntervalSince1970];
559
- NSString *newFileName = [NSString stringWithFormat:@"%@_%lld.%@", newName, (long long)timestamp, fileExtension];
560
-
561
- NSURL *documentsDirectory = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory
562
- inDomains:NSUserDomainMask] firstObject];
563
- NSURL *newURL = [documentsDirectory URLByAppendingPathComponent:newFileName];
564
-
565
- NSError *error;
566
- BOOL success = [[NSFileManager defaultManager] copyItemAtURL:url toURL:newURL error:&error];
567
-
568
- if (success) {
569
- return newURL;
570
- } else {
571
- NSLog(@"Failed to rename file: %@", error.localizedDescription);
572
- return nil;
573
- }
574
- }
575
-
576
- - (void)onError:(NSString *)message code:(NSString *)code {
577
- NSDictionary *eventPayload = @{
578
- @"message": message,
579
- @"errorCode": code
580
- };
581
- [self emitOnError:eventPayload];
582
- }
583
-
584
- #pragma mark - AssetLoaderDelegate
585
-
586
- - (void)assetLoaderDidSucceed:(AssetLoader *)assetLoader {
587
- NSLog(@"assetLoaderDidSucceed");
588
- dispatch_async(dispatch_get_main_queue(), ^{
589
- if (self.vc) {
590
- self.vc.asset = assetLoader.asset;
591
-
592
- NSDictionary *eventPayload = @{
593
- @"duration": [NSNumber numberWithFloat:CMTimeGetSeconds(self.vc.asset.duration) * 1000]
594
- };
595
-
596
- [self emitOnLoad:eventPayload];
597
- }
598
- });
599
- }
600
-
601
- - (void)assetLoader:(AssetLoader *)assetLoader didFailWithError:(NSError *)error forKey:(NSString *)key {
602
- NSLog(@"❌ Asset loading failed: %@ for key: %@", error.localizedDescription, key);
603
-
604
- NSString *message = [NSString stringWithFormat:@"Failed to load %@: %@", key, error.localizedDescription];
605
-
606
- [self onError:message code:@"FAIL_TO_LOAD_MEDIA"];
607
-
608
- if (self.vc) {
609
- [self.vc onAssetFailToLoad];
610
- }
611
-
612
- if (self.editorConfig.alertOnFailToLoad()) {
613
- dispatch_async(dispatch_get_main_queue(), ^{
614
- UIAlertController *dialogMessage = [UIAlertController alertControllerWithTitle:self.editorConfig.alertOnFailTitle()
615
- message:self.editorConfig.alertOnFailMessage()
616
- preferredStyle:UIAlertControllerStyleAlert];
617
- dialogMessage.overrideUserInterfaceStyle = UIUserInterfaceStyleDark;
618
-
619
- UIAlertAction *ok = [UIAlertAction actionWithTitle:self.editorConfig.alertOnFailCloseText()
620
- style:UIAlertActionStyleDefault
621
- handler:nil];
622
-
623
- [dialogMessage addAction:ok];
624
-
625
- UIViewController *root = RCTPresentedViewController();
626
- if (root) {
627
- [root presentViewController:dialogMessage animated:YES completion:nil];
628
- }
629
- });
630
- }
631
- }
632
-
633
- #pragma mark - UIDocumentPickerDelegate
634
-
635
- - (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls {
636
- // Handle document picker results if needed
637
- NSLog(@"Document picker selected URLs: %@", urls);
638
-
639
- if (self.editorConfig.removeAfterSavedToDocuments()) {
640
- [self deleteFileAtURL:self.outputFile];
641
- }
642
-
643
- [self closeEditor];
644
- }
645
-
646
- - (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller {
647
- // Handle document picker cancellation if needed
648
- NSLog(@"Document picker was cancelled");
649
-
650
- if (self.editorConfig.removeAfterFailedToSaveDocuments()) {
651
- [self deleteFileAtURL:self.outputFile];
652
- }
653
-
654
- [self closeEditor];
158
+ } else if ([eventName isEqualToString:@"onCancelTrimming"]) {
159
+ [self emitOnCancelTrimming];
160
+ } else if ([eventName isEqualToString:@"onCancel"]) {
161
+ [self emitOnCancel];
162
+ } else if ([eventName isEqualToString:@"onHide"]) {
163
+ [self emitOnHide];
164
+ } else if ([eventName isEqualToString:@"onShow"]) {
165
+ [self emitOnShow];
166
+ } else if ([eventName isEqualToString:@"onFinishTrimming"]) {
167
+ [self emitOnFinishTrimming:body];
168
+ } else if ([eventName isEqualToString:@"onStatistics"]) {
169
+ [self emitOnStatistics:body];
170
+ }
655
171
  }
656
172
 
657
173
  #pragma mark - TurboModule
658
174
 
659
- - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params {
660
- return std::make_shared<facebook::react::NativeVideoTrimSpecJSI>(params);
175
+ - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
176
+ (const facebook::react::ObjCTurboModule::InitParams &)params {
177
+ return std::make_shared<facebook::react::NativeVideoTrimSpecJSI>(params);
661
178
  }
662
179
 
663
180
  @end