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.
@@ -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
- private var enableRotation: Bool {
53
- get {
54
- return editorConfig?["enableRotation"] as! Bool
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?["rotationAngle"] as! Double
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
- if enableRotation {
347
- cmds.append(contentsOf: ["-display_rotation", "\(rotationAngle)"])
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
- cmds.append(contentsOf: [
356
- "-i",
357
- inputFile.path,
358
- "-c",
359
- "copy",
360
- "-metadata",
361
- "creation_time=\(dateTime)",
362
- outputFile.path
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
- if let enableRotation = config["enableRotation"] as? Bool, enableRotation {
519
- let rotationAngle = config["rotationAngle"] as? Double ?? 0
520
- cmds.append(contentsOf: ["-display_rotation", "\(rotationAngle)"])
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
- cmds.append(contentsOf: [
524
- "-i",
525
- destPath.path,
526
- "-c",
527
- "copy",
528
- "-metadata",
529
- "creation_time=\(dateTime)",
530
- outputFile.path
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