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
|
@@ -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
|
-
|
|
201
|
-
|
|
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
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
402
|
-
|
|
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
|
-
|
|
470
|
+
playerContainerView.addSubview(playerController.view)
|
|
407
471
|
playerController.view.translatesAutoresizingMaskIntoConstraints = false
|
|
408
472
|
NSLayoutConstraint.activate([
|
|
409
|
-
playerController.view.leadingAnchor.constraint(equalTo:
|
|
410
|
-
playerController.view.trailingAnchor.constraint(equalTo:
|
|
411
|
-
playerController.view.topAnchor.constraint(equalTo:
|
|
412
|
-
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)
|
|
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
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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;;
|
|
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.
|