react-native-video-trim 6.2.2 → 7.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.
- package/README.md +34 -9
- package/android/src/main/java/com/videotrim/BaseVideoTrimModule.kt +146 -119
- package/android/src/main/java/com/videotrim/enums/ErrorCode.kt +2 -1
- package/android/src/main/java/com/videotrim/utils/MediaMetadataUtil.kt +6 -2
- package/android/src/main/java/com/videotrim/utils/StorageUtil.kt +5 -1
- package/android/src/main/java/com/videotrim/utils/VideoTrimmerUtil.kt +99 -26
- package/android/src/main/java/com/videotrim/widgets/CropOverlayView.kt +293 -0
- package/android/src/main/java/com/videotrim/widgets/VideoTrimmerView.kt +556 -28
- package/android/src/main/res/drawable/arrow_trianglehead_left_and_right_righttriangle_left_righttriangle_right.xml +19 -0
- package/android/src/main/res/drawable/arrow_uturn_backward.xml +15 -0
- package/android/src/main/res/drawable/arrow_uturn_forward.xml +15 -0
- package/android/src/main/res/drawable/crop.xml +15 -0
- package/android/src/main/res/drawable/rotate_left.xml +19 -0
- package/android/src/main/res/layout/video_trimmer_view.xml +115 -37
- package/android/src/main/res/xml/file_paths.xml +1 -1
- package/ios/CropOverlayView.swift +285 -0
- package/ios/VideoTrim.mm +2 -4
- package/ios/VideoTrim.swift +198 -61
- package/ios/VideoTrimmer.swift +2 -4
- package/ios/VideoTrimmerViewController.swift +478 -56
- package/lib/module/NativeVideoTrim.js.map +1 -1
- package/lib/module/index.js +1 -2
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/NativeVideoTrim.d.ts +10 -4
- package/lib/typescript/src/NativeVideoTrim.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/NativeVideoTrim.ts +10 -4
- package/src/index.tsx +1 -2
package/ios/VideoTrim.swift
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React
|
|
2
2
|
import Photos
|
|
3
|
+
import AVFoundation
|
|
3
4
|
import ffmpegkit
|
|
4
5
|
|
|
5
6
|
let FILE_PREFIX = "trimmedVideo"
|
|
@@ -49,14 +50,12 @@ public class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDe
|
|
|
49
50
|
return editorConfig?["removeAfterFailedToShare"] as! Bool
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
private var rotationAngle: Double {
|
|
53
|
+
|
|
54
|
+
/// When true, forces re-encoding even when no transforms are applied,
|
|
55
|
+
/// giving frame-accurate trim points instead of keyframe-aligned cuts.
|
|
56
|
+
private var enablePreciseTrimming: Bool {
|
|
58
57
|
get {
|
|
59
|
-
return editorConfig?["
|
|
58
|
+
return editorConfig?["enablePreciseTrimming"] as? Bool ?? false
|
|
60
59
|
}
|
|
61
60
|
}
|
|
62
61
|
|
|
@@ -293,7 +292,7 @@ public class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDe
|
|
|
293
292
|
dialogMessage.overrideUserInterfaceStyle = .dark
|
|
294
293
|
|
|
295
294
|
// Create OK button with action handler
|
|
296
|
-
let ok = UIAlertAction(title: self.
|
|
295
|
+
let ok = UIAlertAction(title: self.cancelTrimmingDialogConfirmText, style: .destructive, handler: { (action) -> Void in
|
|
297
296
|
|
|
298
297
|
if let ffmpegSession = ffmpegSession {
|
|
299
298
|
ffmpegSession.cancel()
|
|
@@ -307,7 +306,7 @@ public class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDe
|
|
|
307
306
|
})
|
|
308
307
|
|
|
309
308
|
// Create Cancel button with action handlder
|
|
310
|
-
let cancel = UIAlertAction(title: self.
|
|
309
|
+
let cancel = UIAlertAction(title: self.cancelTrimmingDialogCancelText, style: .cancel)
|
|
311
310
|
|
|
312
311
|
//Add OK and Cancel button to an Alert object
|
|
313
312
|
dialogMessage.addAction(ok)
|
|
@@ -343,19 +342,126 @@ public class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDe
|
|
|
343
342
|
"\(endTime * 1000)ms",
|
|
344
343
|
]
|
|
345
344
|
|
|
346
|
-
|
|
347
|
-
|
|
345
|
+
var videoFilters: [String] = []
|
|
346
|
+
let hasUserTransform = vc != nil && (vc!.rotationCount != 0 || vc!.isFlipped)
|
|
347
|
+
let cropNorm = vc?.cropNormalizedRect
|
|
348
|
+
// Re-encode is required when: (1) user applied flip/rotate, (2) user cropped, or
|
|
349
|
+
// (3) enablePreciseTrimming is on. In all three cases, -c copy won't work because
|
|
350
|
+
// either we need video filters or we need frame-accurate cut points.
|
|
351
|
+
let needsReEncode = hasUserTransform || cropNorm != nil || enablePreciseTrimming
|
|
352
|
+
|
|
353
|
+
if needsReEncode, let vc = vc {
|
|
354
|
+
// -noautorotate disables FFmpeg's automatic rotation, so we must manually
|
|
355
|
+
// compensate for the source video's rotation metadata via transpose filters.
|
|
356
|
+
if let asset = vc.asset,
|
|
357
|
+
let videoTrack = asset.tracks(withMediaType: .video).first {
|
|
358
|
+
let t = videoTrack.preferredTransform
|
|
359
|
+
let sourceAngle = atan2(t.b, t.a)
|
|
360
|
+
if abs(sourceAngle - .pi / 2) < 0.1 {
|
|
361
|
+
videoFilters.append("transpose=1")
|
|
362
|
+
} else if abs(sourceAngle + .pi / 2) < 0.1 {
|
|
363
|
+
videoFilters.append("transpose=2")
|
|
364
|
+
} else if abs(abs(sourceAngle) - .pi) < 0.1 {
|
|
365
|
+
videoFilters.append("transpose=1")
|
|
366
|
+
videoFilters.append("transpose=1")
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
switch vc.rotationCount {
|
|
371
|
+
case 1: videoFilters.append("transpose=2")
|
|
372
|
+
case 2:
|
|
373
|
+
videoFilters.append("transpose=2")
|
|
374
|
+
videoFilters.append("transpose=2")
|
|
375
|
+
case 3: videoFilters.append("transpose=1")
|
|
376
|
+
default: break
|
|
377
|
+
}
|
|
378
|
+
if vc.isFlipped {
|
|
379
|
+
videoFilters.append("hflip")
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if let cn = cropNorm, let asset = vc.asset,
|
|
383
|
+
let track = asset.tracks(withMediaType: .video).first {
|
|
384
|
+
let raw = track.naturalSize
|
|
385
|
+
let pt = track.preferredTransform
|
|
386
|
+
let angle = atan2(pt.b, pt.a)
|
|
387
|
+
let isSrcRotated = abs(angle - .pi / 2) < 0.1 || abs(angle + .pi / 2) < 0.1
|
|
388
|
+
let corrected = isSrcRotated
|
|
389
|
+
? CGSize(width: raw.height, height: raw.width)
|
|
390
|
+
: raw
|
|
391
|
+
|
|
392
|
+
let postW: CGFloat
|
|
393
|
+
let postH: CGFloat
|
|
394
|
+
if vc.rotationCount % 2 != 0 {
|
|
395
|
+
postW = corrected.height
|
|
396
|
+
postH = corrected.width
|
|
397
|
+
} else {
|
|
398
|
+
postW = corrected.width
|
|
399
|
+
postH = corrected.height
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
let cx = Int(round(cn.origin.x * postW))
|
|
403
|
+
let cy = Int(round(cn.origin.y * postH))
|
|
404
|
+
var cw = Int(round(cn.size.width * postW))
|
|
405
|
+
var ch = Int(round(cn.size.height * postH))
|
|
406
|
+
// H.264 requires even dimensions; round down to nearest even number.
|
|
407
|
+
cw = cw & ~1
|
|
408
|
+
ch = ch & ~1
|
|
409
|
+
if cw > 0 && ch > 0 {
|
|
410
|
+
videoFilters.append("crop=\(cw):\(ch):\(cx):\(cy)")
|
|
411
|
+
}
|
|
412
|
+
}
|
|
348
413
|
}
|
|
349
414
|
|
|
350
|
-
|
|
351
|
-
"
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
415
|
+
guard let outputFile = outputFile else {
|
|
416
|
+
self.onError(message: "Output file path is nil", code: .trimmingFailed)
|
|
417
|
+
return
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if needsReEncode {
|
|
421
|
+
// Preserve source quality by matching the original bitrate. Falls back to 10 Mbps
|
|
422
|
+
// if the track's estimated data rate is unavailable.
|
|
423
|
+
var bitrateStr = "10M"
|
|
424
|
+
if let asset = vc?.asset,
|
|
425
|
+
let videoTrack = asset.tracks(withMediaType: .video).first {
|
|
426
|
+
let bitrate = Int(videoTrack.estimatedDataRate)
|
|
427
|
+
if bitrate > 0 {
|
|
428
|
+
bitrateStr = "\(bitrate)"
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// -noautorotate: we handle rotation via explicit transpose filters above,
|
|
433
|
+
// so FFmpeg must not auto-rotate or the output will be double-rotated.
|
|
434
|
+
cmds.append("-noautorotate")
|
|
435
|
+
cmds.append(contentsOf: ["-i", inputFile.path])
|
|
436
|
+
// When enablePreciseTrimming is the only reason for re-encode (no transforms),
|
|
437
|
+
// videoFilters is empty — skip -vf entirely to avoid FFmpeg error on empty filter.
|
|
438
|
+
if !videoFilters.isEmpty {
|
|
439
|
+
cmds.append(contentsOf: ["-vf", videoFilters.joined(separator: ",")])
|
|
440
|
+
}
|
|
441
|
+
// h264_videotoolbox: Apple's hardware H.264 encoder — fast and energy-efficient.
|
|
442
|
+
cmds.append(contentsOf: [
|
|
443
|
+
"-c:v",
|
|
444
|
+
"h264_videotoolbox",
|
|
445
|
+
"-b:v",
|
|
446
|
+
bitrateStr,
|
|
447
|
+
"-c:a",
|
|
448
|
+
"copy",
|
|
449
|
+
"-metadata",
|
|
450
|
+
"creation_time=\(dateTime)",
|
|
451
|
+
outputFile.path
|
|
452
|
+
])
|
|
453
|
+
} else {
|
|
454
|
+
// Stream copy: no re-encoding, extremely fast but only cuts at keyframes.
|
|
455
|
+
cmds.append(contentsOf: [
|
|
456
|
+
"-i",
|
|
457
|
+
inputFile.path,
|
|
458
|
+
"-c",
|
|
459
|
+
"copy",
|
|
460
|
+
"-metadata",
|
|
461
|
+
"creation_time=\(dateTime)",
|
|
462
|
+
outputFile.path
|
|
463
|
+
])
|
|
464
|
+
}
|
|
359
465
|
|
|
360
466
|
print("Command: ", cmds.joined(separator: " "))
|
|
361
467
|
|
|
@@ -372,7 +478,7 @@ public class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDe
|
|
|
372
478
|
var shouldCloseEditor = false
|
|
373
479
|
|
|
374
480
|
if ReturnCode.isSuccess(returnCode) {
|
|
375
|
-
let eventPayload: [String: Any] = ["outputPath":
|
|
481
|
+
let eventPayload: [String: Any] = ["outputPath": outputFile.absoluteString, "startTime": (startTime * 1000).rounded(), "endTime": (endTime * 1000).rounded(), "duration": (videoDuration * 1000).rounded()]
|
|
376
482
|
self.emitEventToJS("onFinishTrimming", eventData: eventPayload)
|
|
377
483
|
|
|
378
484
|
if (self.saveToPhoto && isVideoType) {
|
|
@@ -383,19 +489,19 @@ public class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDe
|
|
|
383
489
|
}
|
|
384
490
|
|
|
385
491
|
PHPhotoLibrary.shared().performChanges({
|
|
386
|
-
let request = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL:
|
|
492
|
+
let request = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: outputFile)
|
|
387
493
|
request?.creationDate = Date()
|
|
388
494
|
}) { success, error in
|
|
389
495
|
if success {
|
|
390
496
|
print("Edited video saved to Photo Library successfully.")
|
|
391
497
|
|
|
392
498
|
if self.removeAfterSavedToPhoto {
|
|
393
|
-
let _ = VideoTrim.deleteFile(url:
|
|
499
|
+
let _ = VideoTrim.deleteFile(url: outputFile)
|
|
394
500
|
}
|
|
395
501
|
} else {
|
|
396
502
|
self.onError(message: "Failed to save edited video to Photo Library: \(error?.localizedDescription ?? "Unknown error")", code: .failToSaveToPhoto)
|
|
397
503
|
if self.removeAfterFailedToSavePhoto {
|
|
398
|
-
let _ = VideoTrim.deleteFile(url:
|
|
504
|
+
let _ = VideoTrim.deleteFile(url: outputFile)
|
|
399
505
|
}
|
|
400
506
|
}
|
|
401
507
|
}
|
|
@@ -404,7 +510,7 @@ public class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDe
|
|
|
404
510
|
DispatchQueue.main.async {
|
|
405
511
|
progressAlert.dismiss(animated: true) {
|
|
406
512
|
self.isTrimming = false
|
|
407
|
-
self.saveFileToFilesApp(fileURL:
|
|
513
|
+
self.saveFileToFilesApp(fileURL: outputFile)
|
|
408
514
|
}
|
|
409
515
|
}
|
|
410
516
|
return
|
|
@@ -412,7 +518,7 @@ public class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDe
|
|
|
412
518
|
DispatchQueue.main.async {
|
|
413
519
|
progressAlert.dismiss(animated: true) {
|
|
414
520
|
self.isTrimming = false
|
|
415
|
-
self.shareFile(fileURL:
|
|
521
|
+
self.shareFile(fileURL: outputFile)
|
|
416
522
|
}
|
|
417
523
|
}
|
|
418
524
|
return
|
|
@@ -479,9 +585,7 @@ public class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDe
|
|
|
479
585
|
// New Arch
|
|
480
586
|
@objc(trim:url:config:)
|
|
481
587
|
public func _trim(inputFile: String, config: NSDictionary, completion: @escaping ([String: Any]) -> Void) {
|
|
482
|
-
let destPath = URL(string: inputFile)
|
|
483
|
-
|
|
484
|
-
guard let destPath = destPath else {
|
|
588
|
+
guard let destPath = URL(string: inputFile) ?? URL(fileURLWithPath: inputFile) as URL? else {
|
|
485
589
|
let result = [
|
|
486
590
|
"success": false,
|
|
487
591
|
"message": "Invalid input file path",
|
|
@@ -512,20 +616,48 @@ public class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDe
|
|
|
512
616
|
"\(endTime)ms",
|
|
513
617
|
]
|
|
514
618
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
}
|
|
619
|
+
// Headless trim: no editor UI, so no transforms (flip/rotate/crop) are possible.
|
|
620
|
+
// The only reason to re-encode here is enablePreciseTrimming for frame-accurate cuts.
|
|
621
|
+
let enablePrecise = config["enablePreciseTrimming"] as? Bool ?? false
|
|
519
622
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
623
|
+
if enablePrecise {
|
|
624
|
+
// Match source bitrate to preserve quality; fall back to 10 Mbps.
|
|
625
|
+
var bitrateStr = "10M"
|
|
626
|
+
let asset = AVURLAsset(url: destPath)
|
|
627
|
+
if let videoTrack = asset.tracks(withMediaType: .video).first {
|
|
628
|
+
let bitrate = Int(videoTrack.estimatedDataRate)
|
|
629
|
+
if bitrate > 0 {
|
|
630
|
+
bitrateStr = "\(bitrate)"
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// No -noautorotate here: headless trim has no manual rotation filters,
|
|
635
|
+
// so FFmpeg's auto-rotation produces the correct output orientation.
|
|
636
|
+
cmds.append(contentsOf: [
|
|
637
|
+
"-i",
|
|
638
|
+
destPath.path,
|
|
639
|
+
"-c:v",
|
|
640
|
+
"h264_videotoolbox",
|
|
641
|
+
"-b:v",
|
|
642
|
+
bitrateStr,
|
|
643
|
+
"-c:a",
|
|
644
|
+
"copy",
|
|
645
|
+
"-metadata",
|
|
646
|
+
"creation_time=\(dateTime)",
|
|
647
|
+
outputFile.path
|
|
648
|
+
])
|
|
649
|
+
} else {
|
|
650
|
+
// Stream copy: no re-encoding, extremely fast but only cuts at keyframes.
|
|
651
|
+
cmds.append(contentsOf: [
|
|
652
|
+
"-i",
|
|
653
|
+
destPath.path,
|
|
654
|
+
"-c",
|
|
655
|
+
"copy",
|
|
656
|
+
"-metadata",
|
|
657
|
+
"creation_time=\(dateTime)",
|
|
658
|
+
outputFile.path
|
|
659
|
+
])
|
|
660
|
+
}
|
|
529
661
|
|
|
530
662
|
print("Command: ", cmds.joined(separator: " "))
|
|
531
663
|
|
|
@@ -677,21 +809,21 @@ public class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDe
|
|
|
677
809
|
print(message)
|
|
678
810
|
self.onError(message: message, code: .failToShare)
|
|
679
811
|
|
|
680
|
-
if self.removeAfterFailedToShare {
|
|
681
|
-
let _ = VideoTrim.deleteFile(url:
|
|
812
|
+
if self.removeAfterFailedToShare, let outputFile = self.outputFile {
|
|
813
|
+
let _ = VideoTrim.deleteFile(url: outputFile)
|
|
682
814
|
}
|
|
683
815
|
return
|
|
684
816
|
}
|
|
685
817
|
|
|
686
818
|
if completed {
|
|
687
819
|
print("User completed the sharing activity")
|
|
688
|
-
if self.removeAfterShared {
|
|
689
|
-
let _ = VideoTrim.deleteFile(url:
|
|
820
|
+
if self.removeAfterShared, let outputFile = self.outputFile {
|
|
821
|
+
let _ = VideoTrim.deleteFile(url: outputFile)
|
|
690
822
|
}
|
|
691
823
|
} else {
|
|
692
824
|
print("User cancelled or failed to complete the sharing activity")
|
|
693
|
-
if self.removeAfterFailedToShare {
|
|
694
|
-
let _ = VideoTrim.deleteFile(url:
|
|
825
|
+
if self.removeAfterFailedToShare, let outputFile = self.outputFile {
|
|
826
|
+
let _ = VideoTrim.deleteFile(url: outputFile)
|
|
695
827
|
}
|
|
696
828
|
}
|
|
697
829
|
|
|
@@ -727,10 +859,8 @@ extension VideoTrim {
|
|
|
727
859
|
editorConfig = config
|
|
728
860
|
print("Show editor called with URI: \(uri)")
|
|
729
861
|
|
|
730
|
-
let destPath = URL(string: uri)
|
|
731
|
-
print("Destination Path: \(destPath
|
|
732
|
-
|
|
733
|
-
guard let destPath = destPath else { return }
|
|
862
|
+
guard let destPath = URL(string: uri) ?? URL(fileURLWithPath: uri) as URL? else { return }
|
|
863
|
+
print("Destination Path: \(destPath.absoluteString), path: \(destPath.path)")
|
|
734
864
|
|
|
735
865
|
DispatchQueue.main.async {
|
|
736
866
|
self.vc = VideoTrimmerViewController()
|
|
@@ -773,8 +903,9 @@ extension VideoTrim {
|
|
|
773
903
|
let isVideoType = (config["type"] as? String ?? "video") == "video"
|
|
774
904
|
|
|
775
905
|
vc.saveBtnClicked = {(selectedRange: CMTimeRange) in
|
|
906
|
+
guard let asset = vc.asset else { return }
|
|
776
907
|
if !self.enableSaveDialog {
|
|
777
|
-
self.trim(viewController: vc,inputFile: destPath, videoDuration:
|
|
908
|
+
self.trim(viewController: vc,inputFile: destPath, videoDuration: asset.duration.seconds, startTime: selectedRange.start.seconds, endTime: selectedRange.end.seconds, isVideoType: isVideoType)
|
|
778
909
|
return
|
|
779
910
|
}
|
|
780
911
|
|
|
@@ -784,7 +915,7 @@ extension VideoTrim {
|
|
|
784
915
|
|
|
785
916
|
// Create OK button with action handler
|
|
786
917
|
let ok = UIAlertAction(title: self.saveDialogConfirmText, style: .default, handler: { (action) -> Void in
|
|
787
|
-
self.trim(viewController: vc,inputFile: destPath, videoDuration:
|
|
918
|
+
self.trim(viewController: vc,inputFile: destPath, videoDuration: asset.duration.seconds, startTime: selectedRange.start.seconds, endTime: selectedRange.end.seconds, isVideoType: isVideoType)
|
|
788
919
|
})
|
|
789
920
|
|
|
790
921
|
// Create Cancel button with action handlder
|
|
@@ -829,6 +960,7 @@ extension VideoTrim {
|
|
|
829
960
|
vc.dismiss(animated: true, completion: {
|
|
830
961
|
self.emitEventToJS("onHide", eventData: nil)
|
|
831
962
|
self.isShowing = false
|
|
963
|
+
self.vc = nil
|
|
832
964
|
})
|
|
833
965
|
}
|
|
834
966
|
|
|
@@ -885,7 +1017,8 @@ extension VideoTrim {
|
|
|
885
1017
|
// New Arch
|
|
886
1018
|
@objc(deleteFile:)
|
|
887
1019
|
public static func deleteFile(uri: String) -> Bool {
|
|
888
|
-
let
|
|
1020
|
+
guard let url = URL(string: uri) else { return false }
|
|
1021
|
+
let state = deleteFile(url: url)
|
|
889
1022
|
return state == 0
|
|
890
1023
|
}
|
|
891
1024
|
|
|
@@ -918,7 +1051,10 @@ extension VideoTrim {
|
|
|
918
1051
|
// New Arch
|
|
919
1052
|
@objc(isValidFile:url:)
|
|
920
1053
|
public static func isValidFile(url: String, completion: @escaping ([String: Any]) -> Void) -> Void {
|
|
921
|
-
let fileURL = URL(string: url)
|
|
1054
|
+
guard let fileURL = URL(string: url) ?? URL(fileURLWithPath: url) as URL? else {
|
|
1055
|
+
completion(["isValid": false, "fileType": "unknown", "duration": -1])
|
|
1056
|
+
return
|
|
1057
|
+
}
|
|
922
1058
|
checkFileValidity(url: fileURL) { isValid, fileType, duration in
|
|
923
1059
|
if isValid {
|
|
924
1060
|
print("Valid \(fileType) file with duration: \(duration) milliseconds")
|
|
@@ -1018,8 +1154,9 @@ extension VideoTrim {
|
|
|
1018
1154
|
|
|
1019
1155
|
vc?.asset = loader.asset
|
|
1020
1156
|
|
|
1157
|
+
let duration = loader.asset?.duration.seconds ?? 0
|
|
1021
1158
|
let eventPayload: [String: Any] = [
|
|
1022
|
-
"duration":
|
|
1159
|
+
"duration": duration * 1000,
|
|
1023
1160
|
]
|
|
1024
1161
|
self.emitEventToJS("onLoad", eventData: eventPayload)
|
|
1025
1162
|
}
|
|
@@ -1029,15 +1166,15 @@ extension VideoTrim {
|
|
|
1029
1166
|
// MARK: DocumentPicker delegate
|
|
1030
1167
|
extension VideoTrim {
|
|
1031
1168
|
public func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
|
1032
|
-
if removeAfterSavedToDocuments {
|
|
1033
|
-
let _ = VideoTrim.deleteFile(url: outputFile
|
|
1169
|
+
if removeAfterSavedToDocuments, let outputFile = self.outputFile {
|
|
1170
|
+
let _ = VideoTrim.deleteFile(url: outputFile)
|
|
1034
1171
|
}
|
|
1035
1172
|
closeEditor()
|
|
1036
1173
|
}
|
|
1037
1174
|
|
|
1038
1175
|
public func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
|
|
1039
|
-
if removeAfterFailedToSaveDocuments {
|
|
1040
|
-
let _ = VideoTrim.deleteFile(url: outputFile
|
|
1176
|
+
if removeAfterFailedToSaveDocuments, let outputFile = self.outputFile {
|
|
1177
|
+
let _ = VideoTrim.deleteFile(url: outputFile)
|
|
1041
1178
|
}
|
|
1042
1179
|
closeEditor()
|
|
1043
1180
|
}
|
package/ios/VideoTrimmer.swift
CHANGED
|
@@ -446,17 +446,15 @@ import AVFoundation
|
|
|
446
446
|
self.thumbnails.removeAll(where: {uuidsToRemove.contains($0.uuid)})
|
|
447
447
|
})
|
|
448
448
|
|
|
449
|
-
var seenIndex = 0
|
|
450
449
|
generator.requestedTimeToleranceBefore = .zero
|
|
451
450
|
generator.requestedTimeToleranceAfter = .zero
|
|
452
451
|
generator.generateCGImagesAsynchronously(forTimes: times) { requestedTime, cgImage, actualTime, result, error in
|
|
453
452
|
DispatchQueue.main.async {
|
|
454
|
-
seenIndex += 1
|
|
455
|
-
|
|
456
453
|
guard let cgImage = cgImage else {return}
|
|
454
|
+
guard let index = newThumbnails.firstIndex(where: { CMTimeCompare($0.time, requestedTime) == 0 }) else {return}
|
|
457
455
|
let image = UIImage(cgImage: cgImage)
|
|
458
456
|
|
|
459
|
-
let imageView = newThumbnails[
|
|
457
|
+
let imageView = newThumbnails[index].imageView
|
|
460
458
|
UIView.transition(with: imageView, duration: 0.25, options: [.transitionCrossDissolve], animations: {
|
|
461
459
|
imageView.image = image
|
|
462
460
|
})
|