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/README.md +1 -1
- package/VideoTrim.podspec +1 -1
- package/android/src/main/java/com/videotrim/VideoTrimModule.kt +4 -4
- package/ios/AssetLoader.swift +99 -0
- package/ios/ErrorCode.swift +17 -0
- package/ios/ProgressAlertController.swift +100 -0
- package/ios/VideoTrim-Bridging-Header.h +1 -0
- package/ios/VideoTrim.h +2 -28
- package/ios/VideoTrim.mm +156 -639
- package/ios/VideoTrim.swift +946 -0
- package/ios/VideoTrimProtocol.swift +10 -0
- package/ios/VideoTrimmer.swift +872 -0
- package/ios/VideoTrimmerThumb.swift +175 -0
- package/ios/VideoTrimmerViewController.swift +578 -0
- package/lib/typescript/src/NativeVideoTrim.d.ts +1 -1
- package/lib/typescript/src/NativeVideoTrim.d.ts.map +1 -1
- package/package.json +4 -1
- package/src/NativeVideoTrim.ts +1 -1
- package/ios/AssetLoader.h +0 -19
- package/ios/AssetLoader.mm +0 -87
- package/ios/ErrorCode.h +0 -9
- package/ios/ProgressAlertController.h +0 -12
- package/ios/ProgressAlertController.mm +0 -106
- package/ios/VideoTrimmer.h +0 -67
- package/ios/VideoTrimmer.mm +0 -863
- package/ios/VideoTrimmerThumb.h +0 -23
- package/ios/VideoTrimmerThumb.mm +0 -175
- package/ios/VideoTrimmerViewController.h +0 -52
- package/ios/VideoTrimmerViewController.mm +0 -533
package/ios/VideoTrim.mm
CHANGED
|
@@ -1,663 +1,180 @@
|
|
|
1
1
|
#import "VideoTrim.h"
|
|
2
|
-
#import
|
|
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
|
-
|
|
5
|
+
VideoTrimSwift * _Nullable videoTrim;
|
|
16
6
|
}
|
|
17
7
|
|
|
18
8
|
RCT_EXPORT_MODULE()
|
|
19
9
|
|
|
20
10
|
- (instancetype)init {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
[
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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:
|
|
660
|
-
|
|
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
|