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
|
@@ -77,6 +77,29 @@ class VideoTrimmerViewController: UIViewController {
|
|
|
77
77
|
private var headerTextColor: Double?
|
|
78
78
|
private var headerView: UIView?
|
|
79
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
|
+
|
|
80
103
|
var isSeekInProgress: Bool = false // Marker
|
|
81
104
|
private var chaseTime = CMTime.zero
|
|
82
105
|
private var preferredFrameRate: Float = 23.98
|
|
@@ -260,31 +283,44 @@ class VideoTrimmerViewController: UIViewController {
|
|
|
260
283
|
headerView = UIView()
|
|
261
284
|
headerView!.translatesAutoresizingMaskIntoConstraints = false
|
|
262
285
|
view.addSubview(headerView!)
|
|
263
|
-
let
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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)
|
|
273
300
|
|
|
274
301
|
NSLayoutConstraint.activate([
|
|
275
|
-
// HeaderView constraints
|
|
276
302
|
headerView!.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
|
|
277
303
|
headerView!.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
278
304
|
headerView!.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
279
|
-
headerView!.heightAnchor.constraint(greaterThanOrEqualToConstant: 50),
|
|
280
305
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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),
|
|
286
316
|
])
|
|
287
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
|
+
|
|
288
324
|
view.layoutIfNeeded() // layout after activate constraints, otherwise headerView height = screen height, which leads to playerViewController is missing at runtime
|
|
289
325
|
}
|
|
290
326
|
}
|
|
@@ -407,14 +443,37 @@ class VideoTrimmerViewController: UIViewController {
|
|
|
407
443
|
}
|
|
408
444
|
|
|
409
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
|
+
|
|
410
469
|
addChild(playerController)
|
|
411
|
-
|
|
470
|
+
playerContainerView.addSubview(playerController.view)
|
|
412
471
|
playerController.view.translatesAutoresizingMaskIntoConstraints = false
|
|
413
472
|
NSLayoutConstraint.activate([
|
|
414
|
-
playerController.view.leadingAnchor.constraint(equalTo:
|
|
415
|
-
playerController.view.trailingAnchor.constraint(equalTo:
|
|
416
|
-
playerController.view.topAnchor.constraint(equalTo:
|
|
417
|
-
playerController.view.bottomAnchor.constraint(equalTo:
|
|
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)
|
|
418
477
|
])
|
|
419
478
|
|
|
420
479
|
// Add observer for the end of playback
|
|
@@ -427,6 +486,364 @@ class VideoTrimmerViewController: UIViewController {
|
|
|
427
486
|
playBtn.setImage(self.playIcon, for: .normal)
|
|
428
487
|
}
|
|
429
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
|
+
|
|
430
847
|
private func setupTimeObserver() {
|
|
431
848
|
timeObserverToken = player.addPeriodicTimeObserver(forInterval: CMTime(value: 1, timescale: 30), queue: .main) { [weak self] time in
|
|
432
849
|
guard let self = self else { return }
|
|
@@ -504,6 +921,7 @@ class VideoTrimmerViewController: UIViewController {
|
|
|
504
921
|
enableHapticFeedback = config["enableHapticFeedback"] as? Bool ?? true
|
|
505
922
|
zoomOnWaitingDuration = (config["zoomOnWaitingDuration"] as? Double ?? 5.0) / 1000.0 // convert ms to s
|
|
506
923
|
autoplay = config["autoplay"] as? Bool ?? false
|
|
924
|
+
isVideoType = (config["type"] as? String ?? "video") == "video"
|
|
507
925
|
headerText = config["headerText"] as? String
|
|
508
926
|
headerTextSize = config["headerTextSize"] as? Int ?? 16
|
|
509
927
|
headerTextColor = config["headerTextColor"] as? Double
|
|
@@ -530,6 +948,7 @@ class VideoTrimmerViewController: UIViewController {
|
|
|
530
948
|
self.playBtn.isEnabled = true
|
|
531
949
|
self.saveBtn.alpha = 1
|
|
532
950
|
self.saveBtn.isEnabled = true
|
|
951
|
+
self.transformStackView?.alpha = 1
|
|
533
952
|
})
|
|
534
953
|
|
|
535
954
|
if jumpToPositionOnLoad > 0 {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["TurboModuleRegistry","getEnforcing"],"sourceRoot":"../../src","sources":["NativeVideoTrim.ts"],"mappings":";;AACA,SAASA,mBAAmB,QAAQ,cAAc;;AAGlD;AACA;AACA;;
|
|
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":[]}
|
package/lib/module/index.js
CHANGED
package/lib/module/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["VideoTrimNewArch","VideoTrimOldArch","processColor","isFabric","global","nativeFabricUIManager","VideoTrim","createBaseOptions","overrides","saveToPhoto","type","outputExt","removeAfterSavedToPhoto","removeAfterFailedToSavePhoto","
|
|
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
|
-
/**
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NativeVideoTrim.d.ts","sourceRoot":"","sources":["../../../src/NativeVideoTrim.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2CAA2C,CAAC;AAE9E;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,qEAAqE;IACrE,WAAW,EAAE,OAAO,CAAC;IACrB,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,SAAS,EAAE,MAAM,CAAC;IAClB,sFAAsF;IACtF,uBAAuB,EAAE,OAAO,CAAC;IACjC,8EAA8E;IAC9E,4BAA4B,EAAE,OAAO,CAAC;IACtC
|
|
1
|
+
{"version":3,"file":"NativeVideoTrim.d.ts","sourceRoot":"","sources":["../../../src/NativeVideoTrim.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAEV,YAAY,EACZ,oBAAoB,EACpB,WAAW,EACX,UAAU,EACX,MAAM,mBAAmB,CAAC;AAK3B,QAAA,MAAM,SAAS,KAAiD,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAEV,YAAY,EACZ,oBAAoB,EACpB,WAAW,EACX,UAAU,EACX,MAAM,mBAAmB,CAAC;AAK3B,QAAA,MAAM,SAAS,KAAiD,CAAC;AA4EjE;;;;;;;GAOG;AACH,wBAAgB,UAAU,CACxB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,OAAO,CACb,IAAI,CAAC,YAAY,EAAE,iBAAiB,GAAG,cAAc,GAAG,iBAAiB,CAAC,CAC3E,GAAG;IACF,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,GACA,IAAI,CAeN;AAED;;;;GAIG;AACH,wBAAgB,SAAS,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAE7C;AAED;;;;GAIG;AACH,wBAAgB,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,CAE5C;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAK7D;AAED;;GAEG;AACH,wBAAgB,WAAW,IAAI,IAAI,CAElC;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAEtE;AAED;;;;;;GAMG;AACH,wBAAgB,IAAI,CAClB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,OAAO,CAAC,WAAW,CAAC,GAC5B,OAAO,CAAC,UAAU,CAAC,CAErB;AAED,cAAc,mBAAmB,CAAC;AAClC,eAAe,SAAS,CAAC"}
|
package/package.json
CHANGED
package/src/NativeVideoTrim.ts
CHANGED
|
@@ -16,10 +16,16 @@ export interface BaseOptions {
|
|
|
16
16
|
removeAfterSavedToPhoto: boolean;
|
|
17
17
|
/** Whether to remove the output file if saving to the photo library fails. */
|
|
18
18
|
removeAfterFailedToSavePhoto: boolean;
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
/**
|
|
20
|
+
* When `true`, FFmpeg re-encodes the video using the platform's hardware encoder
|
|
21
|
+
* (h264_videotoolbox on iOS, h264_mediacodec on Android) for frame-accurate trimming.
|
|
22
|
+
* When `false` (default), uses stream copy (`-c copy`) which is much faster but can
|
|
23
|
+
* only cut at keyframes — the actual start/end may drift by several seconds.
|
|
24
|
+
*
|
|
25
|
+
* Note: if the user applies any transform (flip/rotate/crop), re-encoding already
|
|
26
|
+
* happens regardless of this flag, so precise trimming comes for free in that case.
|
|
27
|
+
*/
|
|
28
|
+
enablePreciseTrimming: boolean;
|
|
23
29
|
}
|
|
24
30
|
|
|
25
31
|
/**
|
package/src/index.tsx
CHANGED
|
@@ -20,8 +20,7 @@ function createBaseOptions(overrides: Partial<BaseOptions> = {}): BaseOptions {
|
|
|
20
20
|
outputExt: 'mp4',
|
|
21
21
|
removeAfterSavedToPhoto: false,
|
|
22
22
|
removeAfterFailedToSavePhoto: false,
|
|
23
|
-
|
|
24
|
-
rotationAngle: 0,
|
|
23
|
+
enablePreciseTrimming: false,
|
|
25
24
|
...overrides,
|
|
26
25
|
};
|
|
27
26
|
}
|