react-native-video-trim 6.2.3 → 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 +38 -13
- package/android/src/main/java/com/videotrim/utils/VideoTrimmerUtil.kt +77 -13
- package/android/src/main/java/com/videotrim/widgets/CropOverlayView.kt +293 -0
- package/android/src/main/java/com/videotrim/widgets/VideoTrimmerView.kt +540 -21
- 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/ios/CropOverlayView.swift +285 -0
- package/ios/VideoTrim.mm +2 -4
- package/ios/VideoTrim.swift +160 -31
- package/ios/VideoTrimmerViewController.swift +441 -22
- 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
|
|
|
@@ -343,8 +342,74 @@ 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
415
|
guard let outputFile = outputFile else {
|
|
@@ -352,15 +417,51 @@ public class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDe
|
|
|
352
417
|
return
|
|
353
418
|
}
|
|
354
419
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
"
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
+
}
|
|
364
465
|
|
|
365
466
|
print("Command: ", cmds.joined(separator: " "))
|
|
366
467
|
|
|
@@ -515,20 +616,48 @@ public class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDe
|
|
|
515
616
|
"\(endTime)ms",
|
|
516
617
|
]
|
|
517
618
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
}
|
|
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
|
|
522
622
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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
|
+
}
|
|
532
661
|
|
|
533
662
|
print("Command: ", cmds.joined(separator: " "))
|
|
534
663
|
|