react-native-video-trim 4.0.0 → 5.0.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 (95) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +124 -83
  3. package/VideoTrim.podspec +4 -4
  4. package/android/build.gradle +15 -54
  5. package/android/gradle.properties +2 -0
  6. package/android/src/main/AndroidManifest.xml +1 -1
  7. package/android/src/main/java/com/videotrim/VideoTrimModule.kt +660 -0
  8. package/android/src/main/java/com/videotrim/VideoTrimPackage.kt +33 -0
  9. package/android/src/main/java/com/{margelo/nitro/videotrim → videotrim}/enums/ErrorCode.java +1 -1
  10. package/android/src/main/java/com/{margelo/nitro/videotrim → videotrim}/interfaces/IVideoTrimmerView.java +1 -1
  11. package/android/src/main/java/com/{margelo/nitro/videotrim → videotrim}/interfaces/VideoTrimListener.java +5 -4
  12. package/android/src/main/java/com/{margelo/nitro/videotrim → videotrim}/utils/MediaMetadataUtil.java +1 -1
  13. package/android/src/main/java/com/{margelo/nitro/videotrim → videotrim}/utils/StorageUtil.java +1 -1
  14. package/android/src/main/java/com/{margelo/nitro/videotrim → videotrim}/utils/VideoTrimmerUtil.java +49 -39
  15. package/android/src/main/java/com/{margelo/nitro/videotrim → videotrim}/widgets/VideoTrimmerView.java +49 -46
  16. package/ios/AssetLoader.h +19 -0
  17. package/ios/AssetLoader.mm +87 -0
  18. package/ios/ErrorCode.h +9 -0
  19. package/ios/ProgressAlertController.h +15 -0
  20. package/ios/ProgressAlertController.mm +78 -0
  21. package/ios/VideoTrim.h +31 -0
  22. package/ios/VideoTrim.mm +663 -0
  23. package/ios/VideoTrimmer.h +67 -0
  24. package/ios/VideoTrimmer.mm +863 -0
  25. package/ios/VideoTrimmerThumb.h +23 -0
  26. package/ios/VideoTrimmerThumb.mm +175 -0
  27. package/ios/VideoTrimmerViewController.h +52 -0
  28. package/ios/VideoTrimmerViewController.mm +533 -0
  29. package/lib/module/NativeVideoTrim.js +5 -0
  30. package/lib/module/NativeVideoTrim.js.map +1 -0
  31. package/lib/module/index.js +59 -34
  32. package/lib/module/index.js.map +1 -1
  33. package/lib/typescript/src/NativeVideoTrim.d.ts +107 -0
  34. package/lib/typescript/src/NativeVideoTrim.d.ts.map +1 -0
  35. package/lib/typescript/src/index.d.ts +21 -10
  36. package/lib/typescript/src/index.d.ts.map +1 -1
  37. package/package.json +15 -18
  38. package/src/NativeVideoTrim.ts +113 -0
  39. package/src/index.tsx +68 -39
  40. package/android/CMakeLists.txt +0 -24
  41. package/android/src/main/cpp/cpp-adapter.cpp +0 -6
  42. package/android/src/main/java/com/margelo/nitro/videotrim/VideoTrim.kt +0 -629
  43. package/android/src/main/java/com/margelo/nitro/videotrim/VideoTrimPackage.kt +0 -22
  44. package/ios/AssetLoader.swift +0 -99
  45. package/ios/ErrorCode.swift +0 -17
  46. package/ios/ProgressAlertController.swift +0 -100
  47. package/ios/VideoTrim.swift +0 -60
  48. package/ios/VideoTrimImpl.swift +0 -860
  49. package/ios/VideoTrimmer.swift +0 -872
  50. package/ios/VideoTrimmerThumb.swift +0 -175
  51. package/ios/VideoTrimmerViewController.swift +0 -557
  52. package/lib/module/VideoTrim.nitro.js +0 -4
  53. package/lib/module/VideoTrim.nitro.js.map +0 -1
  54. package/lib/typescript/src/VideoTrim.nitro.d.ts +0 -240
  55. package/lib/typescript/src/VideoTrim.nitro.d.ts.map +0 -1
  56. package/nitrogen/generated/android/c++/JEditorConfig.hpp +0 -229
  57. package/nitrogen/generated/android/c++/JFileValidationResult.hpp +0 -61
  58. package/nitrogen/generated/android/c++/JFunc_void.hpp +0 -74
  59. package/nitrogen/generated/android/c++/JFunc_void_std__string_std__unordered_map_std__string__std__string_.hpp +0 -89
  60. package/nitrogen/generated/android/c++/JHybridVideoTrimSpec.cpp +0 -131
  61. package/nitrogen/generated/android/c++/JHybridVideoTrimSpec.hpp +0 -67
  62. package/nitrogen/generated/android/kotlin/com/margelo/nitro/videotrim/EditorConfig.kt +0 -70
  63. package/nitrogen/generated/android/kotlin/com/margelo/nitro/videotrim/FileValidationResult.kt +0 -28
  64. package/nitrogen/generated/android/kotlin/com/margelo/nitro/videotrim/Func_void.kt +0 -80
  65. package/nitrogen/generated/android/kotlin/com/margelo/nitro/videotrim/Func_void_std__string_std__unordered_map_std__string__std__string_.kt +0 -80
  66. package/nitrogen/generated/android/kotlin/com/margelo/nitro/videotrim/HybridVideoTrimSpec.kt +0 -82
  67. package/nitrogen/generated/android/kotlin/com/margelo/nitro/videotrim/videotrimOnLoad.kt +0 -35
  68. package/nitrogen/generated/android/videotrim+autolinking.cmake +0 -78
  69. package/nitrogen/generated/android/videotrim+autolinking.gradle +0 -27
  70. package/nitrogen/generated/android/videotrimOnLoad.cpp +0 -50
  71. package/nitrogen/generated/android/videotrimOnLoad.hpp +0 -25
  72. package/nitrogen/generated/ios/VideoTrim+autolinking.rb +0 -60
  73. package/nitrogen/generated/ios/VideoTrim-Swift-Cxx-Bridge.cpp +0 -88
  74. package/nitrogen/generated/ios/VideoTrim-Swift-Cxx-Bridge.hpp +0 -331
  75. package/nitrogen/generated/ios/VideoTrim-Swift-Cxx-Umbrella.hpp +0 -53
  76. package/nitrogen/generated/ios/VideoTrimAutolinking.mm +0 -33
  77. package/nitrogen/generated/ios/VideoTrimAutolinking.swift +0 -25
  78. package/nitrogen/generated/ios/c++/HybridVideoTrimSpecSwift.cpp +0 -11
  79. package/nitrogen/generated/ios/c++/HybridVideoTrimSpecSwift.hpp +0 -116
  80. package/nitrogen/generated/ios/swift/EditorConfig.swift +0 -519
  81. package/nitrogen/generated/ios/swift/FileValidationResult.swift +0 -57
  82. package/nitrogen/generated/ios/swift/Func_void.swift +0 -46
  83. package/nitrogen/generated/ios/swift/Func_void_FileValidationResult.swift +0 -46
  84. package/nitrogen/generated/ios/swift/Func_void_bool.swift +0 -46
  85. package/nitrogen/generated/ios/swift/Func_void_double.swift +0 -46
  86. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +0 -46
  87. package/nitrogen/generated/ios/swift/Func_void_std__string_std__unordered_map_std__string__std__string_.swift +0 -54
  88. package/nitrogen/generated/ios/swift/Func_void_std__vector_std__string_.swift +0 -46
  89. package/nitrogen/generated/ios/swift/HybridVideoTrimSpec.swift +0 -53
  90. package/nitrogen/generated/ios/swift/HybridVideoTrimSpec_cxx.swift +0 -222
  91. package/nitrogen/generated/shared/c++/EditorConfig.hpp +0 -245
  92. package/nitrogen/generated/shared/c++/FileValidationResult.hpp +0 -77
  93. package/nitrogen/generated/shared/c++/HybridVideoTrimSpec.cpp +0 -26
  94. package/nitrogen/generated/shared/c++/HybridVideoTrimSpec.hpp +0 -76
  95. package/src/VideoTrim.nitro.ts +0 -244
@@ -0,0 +1,663 @@
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>
13
+
14
+ @implementation VideoTrim {
15
+ std::optional<JS::NativeVideoTrim::EditorConfig> _editorConfig;
16
+ }
17
+
18
+ RCT_EXPORT_MODULE()
19
+
20
+ - (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];
42
+ }
43
+
44
+ - (void)setEditorConfig:(JS::NativeVideoTrim::EditorConfig)config {
45
+ _editorConfig = config;
46
+ }
47
+
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
+ });
74
+ }
75
+
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) };
102
+ 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];
134
+ } 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
+
312
+ [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];
655
+ }
656
+
657
+ #pragma mark - TurboModule
658
+
659
+ - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params {
660
+ return std::make_shared<facebook::react::NativeVideoTrimSpecJSI>(params);
661
+ }
662
+
663
+ @end