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.
Files changed (29) hide show
  1. package/README.md +34 -9
  2. package/android/src/main/java/com/videotrim/BaseVideoTrimModule.kt +146 -119
  3. package/android/src/main/java/com/videotrim/enums/ErrorCode.kt +2 -1
  4. package/android/src/main/java/com/videotrim/utils/MediaMetadataUtil.kt +6 -2
  5. package/android/src/main/java/com/videotrim/utils/StorageUtil.kt +5 -1
  6. package/android/src/main/java/com/videotrim/utils/VideoTrimmerUtil.kt +99 -26
  7. package/android/src/main/java/com/videotrim/widgets/CropOverlayView.kt +293 -0
  8. package/android/src/main/java/com/videotrim/widgets/VideoTrimmerView.kt +556 -28
  9. package/android/src/main/res/drawable/arrow_trianglehead_left_and_right_righttriangle_left_righttriangle_right.xml +19 -0
  10. package/android/src/main/res/drawable/arrow_uturn_backward.xml +15 -0
  11. package/android/src/main/res/drawable/arrow_uturn_forward.xml +15 -0
  12. package/android/src/main/res/drawable/crop.xml +15 -0
  13. package/android/src/main/res/drawable/rotate_left.xml +19 -0
  14. package/android/src/main/res/layout/video_trimmer_view.xml +115 -37
  15. package/android/src/main/res/xml/file_paths.xml +1 -1
  16. package/ios/CropOverlayView.swift +285 -0
  17. package/ios/VideoTrim.mm +2 -4
  18. package/ios/VideoTrim.swift +198 -61
  19. package/ios/VideoTrimmer.swift +2 -4
  20. package/ios/VideoTrimmerViewController.swift +478 -56
  21. package/lib/module/NativeVideoTrim.js.map +1 -1
  22. package/lib/module/index.js +1 -2
  23. package/lib/module/index.js.map +1 -1
  24. package/lib/typescript/src/NativeVideoTrim.d.ts +10 -4
  25. package/lib/typescript/src/NativeVideoTrim.d.ts.map +1 -1
  26. package/lib/typescript/src/index.d.ts.map +1 -1
  27. package/package.json +1 -1
  28. package/src/NativeVideoTrim.ts +10 -4
  29. package/src/index.tsx +1 -2
@@ -69,6 +69,7 @@ class VideoTrimmerViewController: UIViewController {
69
69
  private let audioBannerView = UIImage(systemName: "airpodsmax")
70
70
  private var player: AVPlayer! { playerController.player }
71
71
  private var timeObserverToken: Any?
72
+ private var statusObservation: NSKeyValueObservation?
72
73
  private var autoplay = false
73
74
  private var jumpToPositionOnLoad: Double = 0;
74
75
  private var headerText: String?
@@ -76,6 +77,29 @@ class VideoTrimmerViewController: UIViewController {
76
77
  private var headerTextColor: Double?
77
78
  private var headerView: UIView?
78
79
 
80
+ private(set) var rotationCount = 0
81
+ private(set) var isFlipped = false
82
+ private var isVideoType = true
83
+ private var playerContainerView: UIView!
84
+ private var transformStackView: UIStackView?
85
+
86
+ private var cropBtn: UIButton?
87
+ private var cropOverlayView: CropOverlayView?
88
+ private(set) var isCropActive = false
89
+
90
+ private struct TransformSnapshot: Equatable {
91
+ let rotationCount: Int
92
+ let isFlipped: Bool
93
+ let isCropActive: Bool
94
+ let cropNormalized: CGRect?
95
+ }
96
+ private var undoStack: [TransformSnapshot] = []
97
+ private var redoStack: [TransformSnapshot] = []
98
+ private var undoBtn: UIButton?
99
+ private var redoBtn: UIButton?
100
+ private var preCropSnapshot: TransformSnapshot?
101
+
102
+
79
103
  var isSeekInProgress: Bool = false // Marker
80
104
  private var chaseTime = CMTime.zero
81
105
  private var preferredFrameRate: Float = 23.98
@@ -197,8 +221,8 @@ class VideoTrimmerViewController: UIViewController {
197
221
  guard let _ = asset else { return }
198
222
  player.pause()
199
223
 
200
- // Clean up the observer
201
- player.removeObserver(self, forKeyPath: "status")
224
+ statusObservation?.invalidate()
225
+ statusObservation = nil
202
226
 
203
227
  if let token = timeObserverToken {
204
228
  player.removeTimeObserver(token)
@@ -259,31 +283,44 @@ class VideoTrimmerViewController: UIViewController {
259
283
  headerView = UIView()
260
284
  headerView!.translatesAutoresizingMaskIntoConstraints = false
261
285
  view.addSubview(headerView!)
262
- let headerTextView = UITextView()
263
- headerTextView.text = headerText
264
- headerTextView.textAlignment = .center
265
-
266
- headerTextView.textColor = RCTConvert.uiColor(headerTextColor)
267
- // UIColor.color(fromHexNumber: headerTextColor as NSNumber?, defaultColor: .white)
268
-
269
- headerTextView.font = UIFont.systemFont(ofSize: CGFloat(headerTextSize)) // Set font size here
270
- headerTextView.translatesAutoresizingMaskIntoConstraints = false
271
- headerView!.addSubview(headerTextView)
286
+ let scrollView = UIScrollView()
287
+ scrollView.showsHorizontalScrollIndicator = false
288
+ scrollView.showsVerticalScrollIndicator = false
289
+ scrollView.translatesAutoresizingMaskIntoConstraints = false
290
+ headerView!.addSubview(scrollView)
291
+
292
+ let headerLabel = UILabel()
293
+ headerLabel.text = headerText
294
+ headerLabel.textAlignment = .center
295
+ headerLabel.textColor = RCTConvert.uiColor(headerTextColor)
296
+ headerLabel.font = UIFont.systemFont(ofSize: CGFloat(headerTextSize))
297
+ headerLabel.numberOfLines = 1
298
+ headerLabel.translatesAutoresizingMaskIntoConstraints = false
299
+ scrollView.addSubview(headerLabel)
272
300
 
273
301
  NSLayoutConstraint.activate([
274
- // HeaderView constraints
275
302
  headerView!.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
276
303
  headerView!.leadingAnchor.constraint(equalTo: view.leadingAnchor),
277
304
  headerView!.trailingAnchor.constraint(equalTo: view.trailingAnchor),
278
- headerView!.heightAnchor.constraint(greaterThanOrEqualToConstant: 50),
279
305
 
280
- // HeaderText constraints
281
- headerTextView.topAnchor.constraint(equalTo: headerView!.topAnchor),
282
- headerTextView.bottomAnchor.constraint(equalTo: headerView!.bottomAnchor),
283
- headerTextView.leadingAnchor.constraint(equalTo: headerView!.leadingAnchor),
284
- headerTextView.trailingAnchor.constraint(equalTo: headerView!.trailingAnchor),
306
+ scrollView.topAnchor.constraint(equalTo: headerView!.topAnchor, constant: 6),
307
+ scrollView.bottomAnchor.constraint(equalTo: headerView!.bottomAnchor, constant: -2),
308
+ scrollView.leadingAnchor.constraint(equalTo: headerView!.leadingAnchor),
309
+ scrollView.trailingAnchor.constraint(equalTo: headerView!.trailingAnchor),
310
+ scrollView.heightAnchor.constraint(equalTo: headerLabel.heightAnchor),
311
+
312
+ headerLabel.topAnchor.constraint(equalTo: scrollView.topAnchor),
313
+ headerLabel.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
314
+ headerLabel.leadingAnchor.constraint(greaterThanOrEqualTo: scrollView.leadingAnchor, constant: 16),
315
+ headerLabel.trailingAnchor.constraint(lessThanOrEqualTo: scrollView.trailingAnchor, constant: -16),
285
316
  ])
286
317
 
318
+ let centerX = headerLabel.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor)
319
+ centerX.priority = .defaultHigh
320
+ let minWidth = headerLabel.widthAnchor.constraint(greaterThanOrEqualTo: scrollView.widthAnchor, constant: -32)
321
+ minWidth.priority = .defaultLow
322
+ NSLayoutConstraint.activate([centerX, minWidth])
323
+
287
324
  view.layoutIfNeeded() // layout after activate constraints, otherwise headerView height = screen height, which leads to playerViewController is missing at runtime
288
325
  }
289
326
  }
@@ -391,25 +428,52 @@ class VideoTrimmerViewController: UIViewController {
391
428
  }
392
429
 
393
430
  private func setupPlayerController() {
431
+ guard let asset = asset else { return }
394
432
  playerController.showsPlaybackControls = false
395
433
  if #available(iOS 16.0, *) {
396
434
  playerController.allowsVideoFrameAnalysis = false
397
435
  }
398
436
  playerController.player = AVPlayer()
399
- player.replaceCurrentItem(with: AVPlayerItem(asset: asset!))
437
+ player.replaceCurrentItem(with: AVPlayerItem(asset: asset))
400
438
 
401
- // Add observer for player status
402
- player.addObserver(self, forKeyPath: "status", options: [.new, .initial], context: nil)
439
+ statusObservation = player.observe(\.status, options: [.new, .initial]) { [weak self] player, _ in
440
+ DispatchQueue.main.async {
441
+ self?.onPlayerReady()
442
+ }
443
+ }
403
444
 
404
445
  try? AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [])
446
+
447
+ setupTransformButtons()
448
+
449
+ let topAnchor: NSLayoutYAxisAnchor
450
+ if let transformStack = transformStackView {
451
+ topAnchor = transformStack.bottomAnchor
452
+ } else if let headerView = headerView {
453
+ topAnchor = headerView.bottomAnchor
454
+ } else {
455
+ topAnchor = view.safeAreaLayoutGuide.topAnchor
456
+ }
457
+
458
+ playerContainerView = UIView()
459
+ playerContainerView.clipsToBounds = true
460
+ view.addSubview(playerContainerView)
461
+ playerContainerView.translatesAutoresizingMaskIntoConstraints = false
462
+ NSLayoutConstraint.activate([
463
+ playerContainerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
464
+ playerContainerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
465
+ playerContainerView.topAnchor.constraint(equalTo: topAnchor, constant: 4),
466
+ playerContainerView.bottomAnchor.constraint(equalTo: trimmer.topAnchor, constant: -16)
467
+ ])
468
+
405
469
  addChild(playerController)
406
- view.addSubview(playerController.view)
470
+ playerContainerView.addSubview(playerController.view)
407
471
  playerController.view.translatesAutoresizingMaskIntoConstraints = false
408
472
  NSLayoutConstraint.activate([
409
- playerController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
410
- playerController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
411
- playerController.view.topAnchor.constraint(equalTo: headerView != nil ? headerView!.bottomAnchor : view.safeAreaLayoutGuide.topAnchor),
412
- playerController.view.bottomAnchor.constraint(equalTo: trimmer.topAnchor, constant: -16)
473
+ playerController.view.leadingAnchor.constraint(equalTo: playerContainerView.leadingAnchor),
474
+ playerController.view.trailingAnchor.constraint(equalTo: playerContainerView.trailingAnchor),
475
+ playerController.view.topAnchor.constraint(equalTo: playerContainerView.topAnchor),
476
+ playerController.view.bottomAnchor.constraint(equalTo: playerContainerView.bottomAnchor)
413
477
  ])
414
478
 
415
479
  // Add observer for the end of playback
@@ -422,6 +486,364 @@ class VideoTrimmerViewController: UIViewController {
422
486
  playBtn.setImage(self.playIcon, for: .normal)
423
487
  }
424
488
 
489
+ // MARK: - Transform (Rotation/Flip/Crop) + Undo/Redo
490
+ private func setupTransformButtons() {
491
+ guard isVideoType else { return }
492
+
493
+ let symbolConfig = UIImage.SymbolConfiguration(pointSize: 14, weight: .medium)
494
+ let dimmed = UIColor.white.withAlphaComponent(0.5)
495
+
496
+ let flipBtn = UIButton(type: .system)
497
+ flipBtn.setImage(UIImage(systemName: "arrow.trianglehead.left.and.right.righttriangle.left.righttriangle.right", withConfiguration: symbolConfig), for: .normal)
498
+ flipBtn.tintColor = .white
499
+ flipBtn.addTarget(self, action: #selector(onFlipTapped), for: .touchUpInside)
500
+
501
+ let rotateBtn = UIButton(type: .system)
502
+ rotateBtn.setImage(UIImage(systemName: "rotate.left", withConfiguration: symbolConfig), for: .normal)
503
+ rotateBtn.tintColor = .white
504
+ rotateBtn.addTarget(self, action: #selector(onRotateTapped), for: .touchUpInside)
505
+
506
+ let cropButton = UIButton(type: .system)
507
+ cropButton.setImage(UIImage(systemName: "crop", withConfiguration: symbolConfig), for: .normal)
508
+ cropButton.tintColor = UIColor.white.withAlphaComponent(0.5)
509
+ cropButton.addTarget(self, action: #selector(onCropTapped), for: .touchUpInside)
510
+ self.cropBtn = cropButton
511
+
512
+ let undoButton = UIButton(type: .system)
513
+ undoButton.setImage(UIImage(systemName: "arrow.uturn.backward", withConfiguration: symbolConfig), for: .normal)
514
+ undoButton.tintColor = dimmed
515
+ undoButton.isEnabled = false
516
+ undoButton.addTarget(self, action: #selector(onUndoTapped), for: .touchUpInside)
517
+ self.undoBtn = undoButton
518
+
519
+ let redoButton = UIButton(type: .system)
520
+ redoButton.setImage(UIImage(systemName: "arrow.uturn.forward", withConfiguration: symbolConfig), for: .normal)
521
+ redoButton.tintColor = dimmed
522
+ redoButton.isEnabled = false
523
+ redoButton.addTarget(self, action: #selector(onRedoTapped), for: .touchUpInside)
524
+ self.redoBtn = redoButton
525
+
526
+ let leftStack = UIStackView(arrangedSubviews: [flipBtn, rotateBtn, cropButton])
527
+ leftStack.axis = .horizontal
528
+ leftStack.spacing = 12
529
+
530
+ let rightStack = UIStackView(arrangedSubviews: [undoButton, redoButton])
531
+ rightStack.axis = .horizontal
532
+ rightStack.spacing = 12
533
+
534
+ let spacer = UIView()
535
+ spacer.setContentHuggingPriority(.defaultLow, for: .horizontal)
536
+
537
+ let fullRow = UIStackView(arrangedSubviews: [leftStack, spacer, rightStack])
538
+ fullRow.axis = .horizontal
539
+ fullRow.translatesAutoresizingMaskIntoConstraints = false
540
+ fullRow.alpha = 0
541
+
542
+ view.addSubview(fullRow)
543
+ let btnSize: CGFloat = 28
544
+ NSLayoutConstraint.activate([
545
+ flipBtn.widthAnchor.constraint(equalToConstant: btnSize),
546
+ flipBtn.heightAnchor.constraint(equalToConstant: btnSize),
547
+ rotateBtn.widthAnchor.constraint(equalToConstant: btnSize),
548
+ rotateBtn.heightAnchor.constraint(equalToConstant: btnSize),
549
+ cropButton.widthAnchor.constraint(equalToConstant: btnSize),
550
+ cropButton.heightAnchor.constraint(equalToConstant: btnSize),
551
+ undoButton.widthAnchor.constraint(equalToConstant: btnSize),
552
+ undoButton.heightAnchor.constraint(equalToConstant: btnSize),
553
+ redoButton.widthAnchor.constraint(equalToConstant: btnSize),
554
+ redoButton.heightAnchor.constraint(equalToConstant: btnSize),
555
+ fullRow.topAnchor.constraint(equalTo: headerView != nil ? headerView!.bottomAnchor : view.safeAreaLayoutGuide.topAnchor, constant: 4),
556
+ fullRow.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16),
557
+ fullRow.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16),
558
+ ])
559
+
560
+ self.transformStackView = fullRow
561
+ }
562
+
563
+ @objc private func onFlipTapped() {
564
+ pushUndo()
565
+ isFlipped.toggle()
566
+ if enableHapticFeedback {
567
+ UIImpactFeedbackGenerator(style: .light).impactOccurred()
568
+ }
569
+ updateVideoTransform(resetCrop: true)
570
+ }
571
+
572
+ @objc private func onRotateTapped() {
573
+ pushUndo()
574
+ if isFlipped {
575
+ rotationCount = (rotationCount - 1 + 4) % 4
576
+ } else {
577
+ rotationCount = (rotationCount + 1) % 4
578
+ }
579
+ if enableHapticFeedback {
580
+ UIImpactFeedbackGenerator(style: .light).impactOccurred()
581
+ }
582
+ updateVideoTransform(resetCrop: true)
583
+ }
584
+
585
+ private func updateVideoTransform(resetCrop: Bool = false) {
586
+ let angle = -CGFloat(rotationCount) * (.pi / 2)
587
+ var transform = CGAffineTransform.identity
588
+
589
+ if isFlipped {
590
+ transform = transform.scaledBy(x: -1, y: 1)
591
+ }
592
+ transform = transform.rotated(by: angle)
593
+
594
+ if rotationCount % 2 != 0 {
595
+ let bounds = playerContainerView.bounds
596
+ if bounds.width > 0 && bounds.height > 0 {
597
+ let fitScale = min(bounds.width / bounds.height, bounds.height / bounds.width)
598
+ transform = transform.scaledBy(x: fitScale, y: fitScale)
599
+ }
600
+ }
601
+
602
+ UIView.animate(withDuration: 0.3, delay: 0, options: [.curveEaseInOut]) {
603
+ self.playerController.view.transform = transform
604
+ } completion: { _ in
605
+ if resetCrop && self.isCropActive {
606
+ self.updateCropAllowedRect()
607
+ self.cropOverlayView?.resetCrop()
608
+ }
609
+ }
610
+ }
611
+
612
+ // MARK: - Undo / Redo
613
+
614
+ private func currentSnapshot() -> TransformSnapshot {
615
+ TransformSnapshot(
616
+ rotationCount: rotationCount,
617
+ isFlipped: isFlipped,
618
+ isCropActive: isCropActive,
619
+ cropNormalized: cropNormalizedRect
620
+ )
621
+ }
622
+
623
+ private func pushUndo() {
624
+ undoStack.append(currentSnapshot())
625
+ redoStack.removeAll()
626
+ updateUndoRedoButtons()
627
+ }
628
+
629
+ @objc private func onUndoTapped() {
630
+ guard let prev = undoStack.popLast() else { return }
631
+ redoStack.append(currentSnapshot())
632
+ applySnapshot(prev)
633
+ updateUndoRedoButtons()
634
+ }
635
+
636
+ @objc private func onRedoTapped() {
637
+ guard let next = redoStack.popLast() else { return }
638
+ undoStack.append(currentSnapshot())
639
+ applySnapshot(next)
640
+ updateUndoRedoButtons()
641
+ }
642
+
643
+ private func applySnapshot(_ snap: TransformSnapshot) {
644
+ rotationCount = snap.rotationCount
645
+ isFlipped = snap.isFlipped
646
+
647
+ let angle = -CGFloat(rotationCount) * (.pi / 2)
648
+ var transform = CGAffineTransform.identity
649
+ if isFlipped { transform = transform.scaledBy(x: -1, y: 1) }
650
+ transform = transform.rotated(by: angle)
651
+ if rotationCount % 2 != 0 {
652
+ let bounds = playerContainerView.bounds
653
+ if bounds.width > 0 && bounds.height > 0 {
654
+ let fitScale = min(bounds.width / bounds.height, bounds.height / bounds.width)
655
+ transform = transform.scaledBy(x: fitScale, y: fitScale)
656
+ }
657
+ }
658
+
659
+ let wasActive = isCropActive
660
+ isCropActive = snap.isCropActive
661
+ cropBtn?.tintColor = isCropActive ? .white : UIColor.white.withAlphaComponent(0.5)
662
+
663
+ UIView.animate(withDuration: 0.3, delay: 0, options: [.curveEaseInOut]) {
664
+ self.playerController.view.transform = transform
665
+ } completion: { _ in
666
+ if self.isCropActive {
667
+ self.showCropOverlayImmediate()
668
+ self.updateCropAllowedRect()
669
+ if let norm = snap.cropNormalized {
670
+ self.setCropFromNormalized(norm)
671
+ } else {
672
+ self.cropOverlayView?.resetCrop()
673
+ }
674
+ } else if wasActive {
675
+ self.cropOverlayView?.isHidden = true
676
+ self.cropOverlayView?.alpha = 0
677
+ }
678
+ }
679
+ }
680
+
681
+ private func setCropFromNormalized(_ norm: CGRect) {
682
+ let vr = getVideoDisplayRectInContainer()
683
+ guard vr.width > 1, vr.height > 1 else { return }
684
+ cropOverlayView?.cropRect = CGRect(
685
+ x: vr.minX + norm.origin.x * vr.width,
686
+ y: vr.minY + norm.origin.y * vr.height,
687
+ width: norm.size.width * vr.width,
688
+ height: norm.size.height * vr.height
689
+ )
690
+ }
691
+
692
+ private func updateUndoRedoButtons() {
693
+ let dimmed = UIColor.white.withAlphaComponent(0.5)
694
+ undoBtn?.tintColor = undoStack.isEmpty ? dimmed : .white
695
+ undoBtn?.isEnabled = !undoStack.isEmpty
696
+ redoBtn?.tintColor = redoStack.isEmpty ? dimmed : .white
697
+ redoBtn?.isEnabled = !redoStack.isEmpty
698
+ }
699
+
700
+ // MARK: - Crop
701
+
702
+ @objc private func onCropTapped() {
703
+ isCropActive.toggle()
704
+ cropBtn?.tintColor = isCropActive ? .white : UIColor.white.withAlphaComponent(0.5)
705
+
706
+ if enableHapticFeedback {
707
+ UIImpactFeedbackGenerator(style: .light).impactOccurred()
708
+ }
709
+
710
+ if isCropActive {
711
+ showCropOverlay()
712
+ } else {
713
+ hideCropOverlay()
714
+ }
715
+ }
716
+
717
+ private func showCropOverlay() {
718
+ if let existing = cropOverlayView {
719
+ existing.isHidden = false
720
+ existing.alpha = 0
721
+ playerContainerView.layoutIfNeeded()
722
+ updateCropAllowedRect()
723
+ UIView.animate(withDuration: 0.2) { existing.alpha = 1 }
724
+ return
725
+ }
726
+
727
+ createCropOverlay()
728
+ playerContainerView.layoutIfNeeded()
729
+ updateCropAllowedRect()
730
+ UIView.animate(withDuration: 0.2) { self.cropOverlayView?.alpha = 1 }
731
+ }
732
+
733
+ private func showCropOverlayImmediate() {
734
+ if let existing = cropOverlayView {
735
+ existing.isHidden = false
736
+ existing.alpha = 1
737
+ return
738
+ }
739
+ createCropOverlay()
740
+ cropOverlayView?.alpha = 1
741
+ }
742
+
743
+ private func createCropOverlay() {
744
+ let overlay = CropOverlayView()
745
+ overlay.translatesAutoresizingMaskIntoConstraints = false
746
+ overlay.alpha = 0
747
+ playerContainerView.addSubview(overlay)
748
+ NSLayoutConstraint.activate([
749
+ overlay.leadingAnchor.constraint(equalTo: playerContainerView.leadingAnchor),
750
+ overlay.trailingAnchor.constraint(equalTo: playerContainerView.trailingAnchor),
751
+ overlay.topAnchor.constraint(equalTo: playerContainerView.topAnchor),
752
+ overlay.bottomAnchor.constraint(equalTo: playerContainerView.bottomAnchor),
753
+ ])
754
+ cropOverlayView = overlay
755
+
756
+ overlay.onCropBegan = { [weak self] in
757
+ self?.preCropSnapshot = self?.currentSnapshot()
758
+ }
759
+ overlay.onCropEnded = { [weak self] in
760
+ guard let self = self, let snap = self.preCropSnapshot else { return }
761
+ if self.currentSnapshot() != snap {
762
+ self.undoStack.append(snap)
763
+ self.redoStack.removeAll()
764
+ self.updateUndoRedoButtons()
765
+ }
766
+ self.preCropSnapshot = nil
767
+ }
768
+ }
769
+
770
+ private func hideCropOverlay() {
771
+ guard let overlay = cropOverlayView else { return }
772
+ UIView.animate(withDuration: 0.2, animations: {
773
+ overlay.alpha = 0
774
+ }) { _ in
775
+ overlay.isHidden = true
776
+ }
777
+ }
778
+
779
+ private func updateCropAllowedRect() {
780
+ guard let overlay = cropOverlayView else { return }
781
+ overlay.allowedRect = getVideoDisplayRectInContainer()
782
+ }
783
+
784
+ func getVideoDisplayRectInContainer() -> CGRect {
785
+ guard let containerView = playerContainerView,
786
+ let asset = asset,
787
+ let track = asset.tracks(withMediaType: .video).first else {
788
+ return playerContainerView?.bounds ?? .zero
789
+ }
790
+
791
+ let raw = track.naturalSize
792
+ let pt = track.preferredTransform
793
+ let angle = atan2(pt.b, pt.a)
794
+ let isSourceRotated = abs(angle - .pi / 2) < 0.1 || abs(angle + .pi / 2) < 0.1
795
+ let displayedSize = isSourceRotated
796
+ ? CGSize(width: raw.height, height: raw.width)
797
+ : raw
798
+
799
+ let pvBounds = playerController.view.bounds
800
+ guard pvBounds.width > 0, pvBounds.height > 0,
801
+ displayedSize.width > 0, displayedSize.height > 0 else {
802
+ return containerView.bounds
803
+ }
804
+
805
+ let videoAR = displayedSize.width / displayedSize.height
806
+ let viewAR = pvBounds.width / pvBounds.height
807
+
808
+ var videoRect: CGRect
809
+ if videoAR > viewAR {
810
+ let h = pvBounds.width / videoAR
811
+ videoRect = CGRect(x: 0, y: (pvBounds.height - h) / 2,
812
+ width: pvBounds.width, height: h)
813
+ } else {
814
+ let w = pvBounds.height * videoAR
815
+ videoRect = CGRect(x: (pvBounds.width - w) / 2, y: 0,
816
+ width: w, height: pvBounds.height)
817
+ }
818
+
819
+ return playerController.view.convert(videoRect, to: containerView)
820
+ }
821
+
822
+ var cropNormalizedRect: CGRect? {
823
+ guard isCropActive,
824
+ let overlay = cropOverlayView, !overlay.isHidden else { return nil }
825
+
826
+ let videoRect = getVideoDisplayRectInContainer()
827
+ guard videoRect.width > 1, videoRect.height > 1 else { return nil }
828
+
829
+ let cr = overlay.cropRect
830
+ let nx = (cr.minX - videoRect.minX) / videoRect.width
831
+ let ny = (cr.minY - videoRect.minY) / videoRect.height
832
+ let nw = cr.width / videoRect.width
833
+ let nh = cr.height / videoRect.height
834
+
835
+ if nx < 0.01 && ny < 0.01 && nw > 0.99 && nh > 0.99 {
836
+ return nil
837
+ }
838
+
839
+ return CGRect(
840
+ x: max(0, min(1, nx)),
841
+ y: max(0, min(1, ny)),
842
+ width: max(0, min(1, nw)),
843
+ height: max(0, min(1, nh))
844
+ )
845
+ }
846
+
425
847
  private func setupTimeObserver() {
426
848
  timeObserverToken = player.addPeriodicTimeObserver(forInterval: CMTime(value: 1, timescale: 30), queue: .main) { [weak self] time in
427
849
  guard let self = self else { return }
@@ -499,6 +921,7 @@ class VideoTrimmerViewController: UIViewController {
499
921
  enableHapticFeedback = config["enableHapticFeedback"] as? Bool ?? true
500
922
  zoomOnWaitingDuration = (config["zoomOnWaitingDuration"] as? Double ?? 5.0) / 1000.0 // convert ms to s
501
923
  autoplay = config["autoplay"] as? Bool ?? false
924
+ isVideoType = (config["type"] as? String ?? "video") == "video"
502
925
  headerText = config["headerText"] as? String
503
926
  headerTextSize = config["headerTextSize"] as? Int ?? 16
504
927
  headerTextColor = config["headerTextColor"] as? Double
@@ -512,35 +935,34 @@ class VideoTrimmerViewController: UIViewController {
512
935
  }
513
936
  }
514
937
 
515
- override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
516
- if keyPath == "status" {
517
- if player.status == .readyToPlay {
518
- loadingIndicator.stopAnimating()
519
- btnStackView.removeArrangedSubview(loadingIndicator)
520
- loadingIndicator.removeFromSuperview()
521
- btnStackView.insertArrangedSubview(playBtn, at: 1)
522
-
523
- UIView.animate(withDuration: 0.25, animations: {
524
- self.playBtn.alpha = 1
525
- self.playBtn.isEnabled = true
526
- self.saveBtn.alpha = 1
527
- self.saveBtn.isEnabled = true
528
- })
529
-
530
- if jumpToPositionOnLoad > 0 {
531
- let duration = (asset?.duration.seconds ?? 0) * 1000
532
- let time = jumpToPositionOnLoad > duration ? duration : jumpToPositionOnLoad
533
- let cmtime = CMTime(value: CMTimeValue(time), timescale: 1000)
534
-
535
- self.seek(to: cmtime)
536
- self.trimmer.progress = cmtime
537
- self.currentTimeLabel.text = self.trimmer.progress.displayString
538
- }
539
-
540
- if autoplay {
541
- togglePlay(sender: playBtn)
542
- }
543
- }
938
+ private func onPlayerReady() {
939
+ guard player.status == .readyToPlay else { return }
940
+
941
+ loadingIndicator.stopAnimating()
942
+ btnStackView.removeArrangedSubview(loadingIndicator)
943
+ loadingIndicator.removeFromSuperview()
944
+ btnStackView.insertArrangedSubview(playBtn, at: 1)
945
+
946
+ UIView.animate(withDuration: 0.25, animations: {
947
+ self.playBtn.alpha = 1
948
+ self.playBtn.isEnabled = true
949
+ self.saveBtn.alpha = 1
950
+ self.saveBtn.isEnabled = true
951
+ self.transformStackView?.alpha = 1
952
+ })
953
+
954
+ if jumpToPositionOnLoad > 0 {
955
+ let duration = (asset?.duration.seconds ?? 0) * 1000
956
+ let time = jumpToPositionOnLoad > duration ? duration : jumpToPositionOnLoad
957
+ let cmtime = CMTime(value: CMTimeValue(time), timescale: 1000)
958
+
959
+ self.seek(to: cmtime)
960
+ self.trimmer.progress = cmtime
961
+ self.currentTimeLabel.text = self.trimmer.progress.displayString
962
+ }
963
+
964
+ if autoplay {
965
+ togglePlay(sender: playBtn)
544
966
  }
545
967
  }
546
968
  }
@@ -1 +1 @@
1
- {"version":3,"names":["TurboModuleRegistry","getEnforcing"],"sourceRoot":"../../src","sources":["NativeVideoTrim.ts"],"mappings":";;AACA,SAASA,mBAAmB,QAAQ,cAAc;;AAGlD;AACA;AACA;;AAkBA;AACA;AACA;;AAgGA;AACA;AACA;;AAQA;AACA;AACA;;AAUA;AACA;AACA;;AAcA;AACA;AACA;;AAmEA,eAAeA,mBAAmB,CAACC,YAAY,CAAO,WAAW,CAAC","ignoreList":[]}
1
+ {"version":3,"names":["TurboModuleRegistry","getEnforcing"],"sourceRoot":"../../src","sources":["NativeVideoTrim.ts"],"mappings":";;AACA,SAASA,mBAAmB,QAAQ,cAAc;;AAGlD;AACA;AACA;;AAwBA;AACA;AACA;;AAgGA;AACA;AACA;;AAQA;AACA;AACA;;AAUA;AACA;AACA;;AAcA;AACA;AACA;;AAmEA,eAAeA,mBAAmB,CAACC,YAAY,CAAO,WAAW,CAAC","ignoreList":[]}
@@ -14,8 +14,7 @@ function createBaseOptions(overrides = {}) {
14
14
  outputExt: 'mp4',
15
15
  removeAfterSavedToPhoto: false,
16
16
  removeAfterFailedToSavePhoto: false,
17
- enableRotation: false,
18
- rotationAngle: 0,
17
+ enablePreciseTrimming: false,
19
18
  ...overrides
20
19
  };
21
20
  }
@@ -1 +1 @@
1
- {"version":3,"names":["VideoTrimNewArch","VideoTrimOldArch","processColor","isFabric","global","nativeFabricUIManager","VideoTrim","createBaseOptions","overrides","saveToPhoto","type","outputExt","removeAfterSavedToPhoto","removeAfterFailedToSavePhoto","enableRotation","rotationAngle","createEditorConfig","enableHapticFeedback","maxDuration","minDuration","openDocumentsOnFinish","openShareSheetOnFinish","removeAfterSavedToDocuments","removeAfterFailedToSaveDocuments","removeAfterShared","removeAfterFailedToShare","cancelButtonText","saveButtonText","enableCancelDialog","cancelDialogTitle","cancelDialogMessage","cancelDialogCancelText","cancelDialogConfirmText","enableSaveDialog","saveDialogTitle","saveDialogMessage","saveDialogCancelText","saveDialogConfirmText","trimmingText","fullScreenModalIOS","autoplay","jumpToPositionOnLoad","closeWhenFinish","enableCancelTrimming","cancelTrimmingButtonText","enableCancelTrimmingDialog","cancelTrimmingDialogTitle","cancelTrimmingDialogMessage","cancelTrimmingDialogCancelText","cancelTrimmingDialogConfirmText","headerText","headerTextSize","headerTextColor","trimmerColor","handleIconColor","zoomOnWaitingDuration","alertOnFailToLoad","alertOnFailTitle","alertOnFailMessage","alertOnFailCloseText","createTrimOptions","startTime","endTime","showEditor","filePath","config","_headerTextColor","_trimmerColor","_handleIconColor","listFiles","cleanFiles","deleteFile","trim","length","Error","closeEditor","isValidFile","url","options"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"names":["VideoTrimNewArch","VideoTrimOldArch","processColor","isFabric","global","nativeFabricUIManager","VideoTrim","createBaseOptions","overrides","saveToPhoto","type","outputExt","removeAfterSavedToPhoto","removeAfterFailedToSavePhoto","enablePreciseTrimming","createEditorConfig","enableHapticFeedback","maxDuration","minDuration","openDocumentsOnFinish","openShareSheetOnFinish","removeAfterSavedToDocuments","removeAfterFailedToSaveDocuments","removeAfterShared","removeAfterFailedToShare","cancelButtonText","saveButtonText","enableCancelDialog","cancelDialogTitle","cancelDialogMessage","cancelDialogCancelText","cancelDialogConfirmText","enableSaveDialog","saveDialogTitle","saveDialogMessage","saveDialogCancelText","saveDialogConfirmText","trimmingText","fullScreenModalIOS","autoplay","jumpToPositionOnLoad","closeWhenFinish","enableCancelTrimming","cancelTrimmingButtonText","enableCancelTrimmingDialog","cancelTrimmingDialogTitle","cancelTrimmingDialogMessage","cancelTrimmingDialogCancelText","cancelTrimmingDialogConfirmText","headerText","headerTextSize","headerTextColor","trimmerColor","handleIconColor","zoomOnWaitingDuration","alertOnFailToLoad","alertOnFailTitle","alertOnFailMessage","alertOnFailCloseText","createTrimOptions","startTime","endTime","showEditor","filePath","config","_headerTextColor","_trimmerColor","_handleIconColor","listFiles","cleanFiles","deleteFile","trim","length","Error","closeEditor","isValidFile","url","options"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":"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","ignoreList":[]}
@@ -14,10 +14,16 @@ export interface BaseOptions {
14
14
  removeAfterSavedToPhoto: boolean;
15
15
  /** Whether to remove the output file if saving to the photo library fails. */
16
16
  removeAfterFailedToSavePhoto: boolean;
17
- /** Whether to enable video rotation during trimming. */
18
- enableRotation: boolean;
19
- /** Rotation angle in degrees (e.g. `90`, `180`, `270`). Only used when `enableRotation` is `true`. */
20
- rotationAngle: number;
17
+ /**
18
+ * When `true`, FFmpeg re-encodes the video using the platform's hardware encoder
19
+ * (h264_videotoolbox on iOS, h264_mediacodec on Android) for frame-accurate trimming.
20
+ * When `false` (default), uses stream copy (`-c copy`) which is much faster but can
21
+ * only cut at keyframes — the actual start/end may drift by several seconds.
22
+ *
23
+ * Note: if the user applies any transform (flip/rotate/crop), re-encoding already
24
+ * happens regardless of this flag, so precise trimming comes for free in that case.
25
+ */
26
+ enablePreciseTrimming: boolean;
21
27
  }
22
28
  /**
23
29
  * Configuration for the video trimmer editor UI.