react-native-video-trim 1.0.9 → 1.0.11
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 +38 -5
- package/android/src/main/java/com/videotrim/VideoTrimModule.java +96 -12
- package/android/src/main/java/com/videotrim/interfaces/VideoTrimListener.java +1 -0
- package/android/src/main/java/com/videotrim/utils/StorageUtil.java +14 -143
- package/android/src/main/java/com/videotrim/utils/VideoTrimmerUtil.java +2 -10
- package/android/src/main/java/com/videotrim/widgets/VideoTrimmerView.java +5 -32
- package/android/src/main/res/values/strings.xml +1 -1
- package/ios/VideoTrim.mm +6 -1
- package/ios/VideoTrim.swift +279 -115
- package/ios/VideoTrimmer.swift +808 -0
- package/ios/VideoTrimmerThumb.swift +119 -0
- package/ios/VideoTrimmerViewController.swift +292 -0
- package/lib/commonjs/index.js +64 -4
- package/lib/commonjs/index.js.map +1 -1
- package/lib/module/index.js +61 -4
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/index.d.ts +46 -3
- package/lib/typescript/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/react-native-video-trim.podspec +1 -0
- package/src/index.tsx +75 -5
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
//
|
|
2
|
+
// VideoTrimmerThumb.swift
|
|
3
|
+
// react-native-video-trim
|
|
4
|
+
//
|
|
5
|
+
// Created by Duc Trung Mai on 17/1/24.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import UIKit
|
|
9
|
+
|
|
10
|
+
@available(iOS 13.0, *)
|
|
11
|
+
class VideoTrimmerThumb: UIView {
|
|
12
|
+
var isActive = false
|
|
13
|
+
|
|
14
|
+
var leadingChevronImageView = UIImageView(image: UIImage(systemName: "chevron.compact.left"))
|
|
15
|
+
var trailingChevronView = UIImageView(image: UIImage(systemName: "chevron.compact.right"))
|
|
16
|
+
|
|
17
|
+
var wrapperView = UIView()
|
|
18
|
+
var leadingView = UIView()
|
|
19
|
+
var trailingView = UIView()
|
|
20
|
+
var topView = UIView()
|
|
21
|
+
var bottomView = UIView()
|
|
22
|
+
|
|
23
|
+
let leadingGrabber = UIControl()
|
|
24
|
+
let trailingGrabber = UIControl()
|
|
25
|
+
|
|
26
|
+
let chevronWidth = CGFloat(16)
|
|
27
|
+
let edgeHeight = CGFloat(4)
|
|
28
|
+
|
|
29
|
+
// MARK: - Input
|
|
30
|
+
@objc private func x(_ sender: Any) {
|
|
31
|
+
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// MARK: - Private
|
|
35
|
+
private func updateColor() {
|
|
36
|
+
let color = UIColor.systemYellow
|
|
37
|
+
leadingView.backgroundColor = color
|
|
38
|
+
trailingView.backgroundColor = color
|
|
39
|
+
topView.backgroundColor = color
|
|
40
|
+
bottomView.backgroundColor = color
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private func setup() {
|
|
44
|
+
|
|
45
|
+
leadingChevronImageView.contentMode = .scaleAspectFill
|
|
46
|
+
trailingChevronView.contentMode = .scaleAspectFill
|
|
47
|
+
|
|
48
|
+
leadingChevronImageView.tintColor = .white
|
|
49
|
+
trailingChevronView.tintColor = .white
|
|
50
|
+
|
|
51
|
+
leadingChevronImageView.tintAdjustmentMode = .normal
|
|
52
|
+
trailingChevronView.tintAdjustmentMode = .normal
|
|
53
|
+
|
|
54
|
+
leadingView.layer.cornerRadius = 6
|
|
55
|
+
leadingView.layer.cornerCurve = .continuous
|
|
56
|
+
leadingView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMinXMinYCorner]
|
|
57
|
+
|
|
58
|
+
trailingView.layer.cornerRadius = 6
|
|
59
|
+
trailingView.layer.cornerCurve = .continuous
|
|
60
|
+
trailingView.layer.maskedCorners = [.layerMaxXMaxYCorner, .layerMaxXMinYCorner]
|
|
61
|
+
|
|
62
|
+
leadingView.addSubview(leadingChevronImageView)
|
|
63
|
+
trailingView.addSubview(trailingChevronView)
|
|
64
|
+
|
|
65
|
+
// wrapperView.layer.shadowColor = UIColor.black.cgColor
|
|
66
|
+
// wrapperView.layer.shadowOffset = .zero
|
|
67
|
+
// wrapperView.layer.shadowRadius = 2
|
|
68
|
+
// wrapperView.layer.shadowOpacity = 0.25
|
|
69
|
+
|
|
70
|
+
wrapperView.addSubview(leadingView)
|
|
71
|
+
wrapperView.addSubview(trailingView)
|
|
72
|
+
wrapperView.addSubview(topView)
|
|
73
|
+
wrapperView.addSubview(bottomView)
|
|
74
|
+
addSubview(wrapperView)
|
|
75
|
+
|
|
76
|
+
wrapperView.addSubview(leadingGrabber)
|
|
77
|
+
wrapperView.addSubview(trailingGrabber)
|
|
78
|
+
|
|
79
|
+
updateColor()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
// MARK: - UIView
|
|
85
|
+
|
|
86
|
+
override func layoutSubviews() {
|
|
87
|
+
super.layoutSubviews()
|
|
88
|
+
|
|
89
|
+
let size = bounds.size
|
|
90
|
+
|
|
91
|
+
wrapperView.frame = CGRect(origin: .zero, size: size)
|
|
92
|
+
|
|
93
|
+
leadingView.frame = CGRect(x: 0, y: 0, width: chevronWidth, height: bounds.height)
|
|
94
|
+
trailingView.frame = CGRect(x: bounds.width - chevronWidth, y: 0, width: chevronWidth, height: bounds.height)
|
|
95
|
+
topView.frame = CGRect(x: chevronWidth, y: 0, width: bounds.width - chevronWidth * 2, height: edgeHeight)
|
|
96
|
+
bottomView.frame = CGRect(x: chevronWidth, y: bounds.height - edgeHeight, width: bounds.width - chevronWidth * 2, height: edgeHeight)
|
|
97
|
+
|
|
98
|
+
let chevronHorizontalInset = CGFloat(2)
|
|
99
|
+
let chevronVerticalInset = CGFloat(8)
|
|
100
|
+
let chevronFrame = CGRect(x: chevronHorizontalInset, y: chevronVerticalInset, width: chevronWidth - chevronHorizontalInset * 2, height: size.height - chevronVerticalInset * 2)
|
|
101
|
+
|
|
102
|
+
leadingChevronImageView.frame = chevronFrame
|
|
103
|
+
trailingChevronView.frame = chevronFrame
|
|
104
|
+
|
|
105
|
+
leadingGrabber.frame = leadingView.frame
|
|
106
|
+
trailingGrabber.frame = trailingView.frame
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
override init(frame: CGRect) {
|
|
110
|
+
super.init(frame: frame)
|
|
111
|
+
setup()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
required init?(coder: NSCoder) {
|
|
115
|
+
super.init(coder: coder)
|
|
116
|
+
setup()
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
//
|
|
2
|
+
// VideoTrimmerViewController.swift
|
|
3
|
+
// react-native-video-trim
|
|
4
|
+
//
|
|
5
|
+
// Created by Duc Trung Mai on 17/1/24.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import UIKit
|
|
9
|
+
import AVKit
|
|
10
|
+
|
|
11
|
+
extension CMTime {
|
|
12
|
+
var displayString: String {
|
|
13
|
+
let offset = TimeInterval(seconds)
|
|
14
|
+
let numberOfNanosecondsFloat = (offset - TimeInterval(Int(offset))) * 1000.0
|
|
15
|
+
let nanoseconds = Int(numberOfNanosecondsFloat)
|
|
16
|
+
let formatter = DateComponentsFormatter()
|
|
17
|
+
formatter.unitsStyle = .positional
|
|
18
|
+
formatter.zeroFormattingBehavior = .pad
|
|
19
|
+
formatter.allowedUnits = [.minute, .second]
|
|
20
|
+
return String(format: "%@.%03d", formatter.string(from: offset) ?? "00:00", nanoseconds)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
extension AVAsset {
|
|
25
|
+
var fullRange: CMTimeRange {
|
|
26
|
+
return CMTimeRange(start: .zero, duration: duration)
|
|
27
|
+
}
|
|
28
|
+
func trimmedComposition(_ range: CMTimeRange) -> AVAsset {
|
|
29
|
+
guard CMTimeRangeEqual(fullRange, range) == false else {return self}
|
|
30
|
+
|
|
31
|
+
let composition = AVMutableComposition()
|
|
32
|
+
try? composition.insertTimeRange(range, of: self, at: .zero)
|
|
33
|
+
|
|
34
|
+
if let videoTrack = tracks(withMediaType: .video).first {
|
|
35
|
+
composition.tracks.forEach {$0.preferredTransform = videoTrack.preferredTransform}
|
|
36
|
+
}
|
|
37
|
+
return composition
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@available(iOS 13.0, *)
|
|
42
|
+
class VideoTrimmerViewController: UIViewController {
|
|
43
|
+
var asset: AVAsset!
|
|
44
|
+
var maximumDuration: Int?
|
|
45
|
+
var cancelBtnText = "Cancel"
|
|
46
|
+
var saveButtonText = "Save"
|
|
47
|
+
var cancelBtnClicked: (() -> Void)?
|
|
48
|
+
var saveBtnClicked: ((CMTimeRange) -> Void)?
|
|
49
|
+
|
|
50
|
+
let playerController = AVPlayerViewController()
|
|
51
|
+
var trimmer: VideoTrimmer!
|
|
52
|
+
var timingStackView: UIStackView!
|
|
53
|
+
var leadingTrimLabel: UILabel!
|
|
54
|
+
var currentTimeLabel: UILabel!
|
|
55
|
+
var trailingTrimLabel: UILabel!
|
|
56
|
+
|
|
57
|
+
private var btnStackView: UIStackView!
|
|
58
|
+
private var cancelBtn: UIButton!
|
|
59
|
+
private var playBtn: UIButton!
|
|
60
|
+
private var saveBtn: UIButton!
|
|
61
|
+
private let playIcon = UIImage(systemName: "play.fill")
|
|
62
|
+
private let pauseIcon = UIImage(systemName: "pause.fill")
|
|
63
|
+
|
|
64
|
+
private var wasPlaying = false
|
|
65
|
+
private var player: AVPlayer! {playerController.player}
|
|
66
|
+
private var timeObserverToken: Any?
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
// MARK: - Input
|
|
70
|
+
@objc private func didBeginTrimming(_ sender: VideoTrimmer) {
|
|
71
|
+
updateLabels()
|
|
72
|
+
|
|
73
|
+
wasPlaying = (player.timeControlStatus != .paused)
|
|
74
|
+
player.pause()
|
|
75
|
+
|
|
76
|
+
updatePlayerAsset()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@objc private func didEndTrimming(_ sender: VideoTrimmer) {
|
|
80
|
+
updateLabels()
|
|
81
|
+
|
|
82
|
+
if wasPlaying == true {
|
|
83
|
+
player.play()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
updatePlayerAsset()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@objc private func selectedRangeDidChanged(_ sender: VideoTrimmer) {
|
|
90
|
+
updateLabels()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@objc private func didBeginScrubbing(_ sender: VideoTrimmer) {
|
|
94
|
+
updateLabels()
|
|
95
|
+
|
|
96
|
+
wasPlaying = (player.timeControlStatus != .paused)
|
|
97
|
+
player.pause()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@objc private func didEndScrubbing(_ sender: VideoTrimmer) {
|
|
101
|
+
updateLabels()
|
|
102
|
+
|
|
103
|
+
if wasPlaying == true {
|
|
104
|
+
player.play()
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@objc private func progressDidChanged(_ sender: VideoTrimmer) {
|
|
109
|
+
updateLabels()
|
|
110
|
+
|
|
111
|
+
let time = CMTimeSubtract(trimmer.progress, trimmer.selectedRange.start)
|
|
112
|
+
player.seek(to: time, toleranceBefore: .zero, toleranceAfter: .zero)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// MARK: - Private
|
|
116
|
+
private func updateLabels() {
|
|
117
|
+
leadingTrimLabel.text = trimmer.selectedRange.start.displayString
|
|
118
|
+
currentTimeLabel.text = trimmer.progress.displayString
|
|
119
|
+
trailingTrimLabel.text = trimmer.selectedRange.end.displayString
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private func updatePlayerAsset() {
|
|
123
|
+
let outputRange = trimmer.trimmingState == .none ? trimmer.selectedRange : asset.fullRange
|
|
124
|
+
let trimmedAsset = asset.trimmedComposition(outputRange)
|
|
125
|
+
if trimmedAsset != player.currentItem?.asset {
|
|
126
|
+
player.replaceCurrentItem(with: AVPlayerItem(asset: trimmedAsset))
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// MARK: - UIViewController
|
|
131
|
+
override func viewDidLoad() {
|
|
132
|
+
super.viewDidLoad()
|
|
133
|
+
|
|
134
|
+
view.backgroundColor = .black
|
|
135
|
+
|
|
136
|
+
// bottom action buttons
|
|
137
|
+
cancelBtn = UIButton(type: .system)
|
|
138
|
+
cancelBtn.setTitle(cancelBtnText, for: .normal)
|
|
139
|
+
cancelBtn.titleLabel?.font = .systemFont(ofSize: 18)
|
|
140
|
+
cancelBtn.setTitleColor(.white, for: .normal)
|
|
141
|
+
cancelBtn.addTarget(self, action: #selector(onCancelBtnClicked), for: .touchUpInside)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
playBtn = UIButton(type: .system)
|
|
145
|
+
playBtn.setImage(playIcon, for: .normal)
|
|
146
|
+
playBtn.tintColor = .systemBlue
|
|
147
|
+
playBtn.addTarget(self, action: #selector(togglePlay(sender:)), for: .touchUpInside)
|
|
148
|
+
|
|
149
|
+
saveBtn = UIButton(type: .system)
|
|
150
|
+
saveBtn.setTitle(saveButtonText, for: .normal)
|
|
151
|
+
saveBtn.titleLabel?.font = .systemFont(ofSize: 18)
|
|
152
|
+
saveBtn.setTitleColor(.systemBlue, for: .normal)
|
|
153
|
+
saveBtn.addTarget(self, action: #selector(onSaveBtnClicked), for: .touchUpInside)
|
|
154
|
+
|
|
155
|
+
btnStackView = UIStackView(arrangedSubviews: [cancelBtn, playBtn, saveBtn])
|
|
156
|
+
btnStackView.axis = .horizontal
|
|
157
|
+
btnStackView.alignment = .fill
|
|
158
|
+
btnStackView.distribution = .fillEqually
|
|
159
|
+
btnStackView.spacing = UIStackView.spacingUseSystem
|
|
160
|
+
view.addSubview(btnStackView)
|
|
161
|
+
btnStackView.translatesAutoresizingMaskIntoConstraints = false
|
|
162
|
+
NSLayoutConstraint.activate([
|
|
163
|
+
btnStackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16),
|
|
164
|
+
btnStackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16),
|
|
165
|
+
btnStackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16),
|
|
166
|
+
])
|
|
167
|
+
|
|
168
|
+
// time labels
|
|
169
|
+
leadingTrimLabel = UILabel()
|
|
170
|
+
leadingTrimLabel.font = UIFont.preferredFont(forTextStyle: .caption1)
|
|
171
|
+
leadingTrimLabel.textAlignment = .left
|
|
172
|
+
leadingTrimLabel.textColor = .white
|
|
173
|
+
|
|
174
|
+
currentTimeLabel = UILabel()
|
|
175
|
+
currentTimeLabel.font = UIFont.preferredFont(forTextStyle: .caption1)
|
|
176
|
+
currentTimeLabel.textAlignment = .center
|
|
177
|
+
currentTimeLabel.textColor = .white
|
|
178
|
+
|
|
179
|
+
trailingTrimLabel = UILabel()
|
|
180
|
+
trailingTrimLabel.font = UIFont.preferredFont(forTextStyle: .caption1)
|
|
181
|
+
trailingTrimLabel.textAlignment = .right
|
|
182
|
+
trailingTrimLabel.textColor = .white
|
|
183
|
+
|
|
184
|
+
timingStackView = UIStackView(arrangedSubviews: [leadingTrimLabel, currentTimeLabel, trailingTrimLabel])
|
|
185
|
+
timingStackView.axis = .horizontal
|
|
186
|
+
timingStackView.alignment = .fill
|
|
187
|
+
timingStackView.distribution = .fillEqually
|
|
188
|
+
timingStackView.spacing = UIStackView.spacingUseSystem
|
|
189
|
+
view.addSubview(timingStackView)
|
|
190
|
+
timingStackView.translatesAutoresizingMaskIntoConstraints = false
|
|
191
|
+
NSLayoutConstraint.activate([
|
|
192
|
+
timingStackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16),
|
|
193
|
+
timingStackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16),
|
|
194
|
+
timingStackView.bottomAnchor.constraint(equalTo: btnStackView.topAnchor, constant: -8),
|
|
195
|
+
])
|
|
196
|
+
|
|
197
|
+
// THIS IS WHERE WE SETUP THE VIDEOTRIMMER:
|
|
198
|
+
trimmer = VideoTrimmer()
|
|
199
|
+
trimmer.asset = asset // this should happen before trimmer.selectedRange below otherwise its didSet will override
|
|
200
|
+
trimmer.minimumDuration = CMTime(seconds: 1, preferredTimescale: 600)
|
|
201
|
+
|
|
202
|
+
if maximumDuration != nil {
|
|
203
|
+
trimmer.maximumDuration = CMTime(seconds: max(1, Double(maximumDuration!)), preferredTimescale: 600) // minimum 1 second
|
|
204
|
+
|
|
205
|
+
// guard check to make sure max duration can only <= asset.duration
|
|
206
|
+
if CMTimeCompare(trimmer.maximumDuration, asset.duration) == 1 {
|
|
207
|
+
trimmer.maximumDuration = asset.duration
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
trimmer.selectedRange = CMTimeRange(start: .zero, end: trimmer.maximumDuration)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
trimmer.addTarget(self, action: #selector(didBeginTrimming(_:)), for: VideoTrimmer.didBeginTrimming)
|
|
214
|
+
trimmer.addTarget(self, action: #selector(didEndTrimming(_:)), for: VideoTrimmer.didEndTrimming)
|
|
215
|
+
trimmer.addTarget(self, action: #selector(selectedRangeDidChanged(_:)), for: VideoTrimmer.selectedRangeChanged)
|
|
216
|
+
trimmer.addTarget(self, action: #selector(didBeginScrubbing(_:)), for: VideoTrimmer.didBeginScrubbing)
|
|
217
|
+
trimmer.addTarget(self, action: #selector(didEndScrubbing(_:)), for: VideoTrimmer.didEndScrubbing)
|
|
218
|
+
trimmer.addTarget(self, action: #selector(progressDidChanged(_:)), for: VideoTrimmer.progressChanged)
|
|
219
|
+
view.addSubview(trimmer)
|
|
220
|
+
trimmer.translatesAutoresizingMaskIntoConstraints = false
|
|
221
|
+
NSLayoutConstraint.activate([
|
|
222
|
+
trimmer.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
|
223
|
+
trimmer.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
|
224
|
+
trimmer.bottomAnchor.constraint(equalTo: timingStackView.topAnchor, constant: -16),
|
|
225
|
+
trimmer.heightAnchor.constraint(equalToConstant: 50),
|
|
226
|
+
])
|
|
227
|
+
|
|
228
|
+
playerController.showsPlaybackControls = false // hide control buttons
|
|
229
|
+
if #available(iOS 16.0, *) {
|
|
230
|
+
playerController.allowsVideoFrameAnalysis = false // hide live text
|
|
231
|
+
}
|
|
232
|
+
playerController.player = AVPlayer()
|
|
233
|
+
try? AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: []) // this is to play audio even when device is in silent mode
|
|
234
|
+
addChild(playerController)
|
|
235
|
+
view.addSubview(playerController.view)
|
|
236
|
+
playerController.view.translatesAutoresizingMaskIntoConstraints = false
|
|
237
|
+
NSLayoutConstraint.activate([
|
|
238
|
+
playerController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
239
|
+
playerController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
240
|
+
playerController.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
|
|
241
|
+
playerController.view.bottomAnchor.constraint(equalTo: trimmer.topAnchor, constant: -16)
|
|
242
|
+
])
|
|
243
|
+
|
|
244
|
+
updatePlayerAsset()
|
|
245
|
+
|
|
246
|
+
timeObserverToken = player.addPeriodicTimeObserver(forInterval: CMTime(value: 1, timescale: 30), queue: .main) { [weak self] time in
|
|
247
|
+
guard let self = self else {return}
|
|
248
|
+
// when we're not trimming, the players starting point is actual later than the trimmer,
|
|
249
|
+
// (because the vidoe has been trimmed), so we need to account for that.
|
|
250
|
+
// When we're trimming, we always show the full video
|
|
251
|
+
let finalTime = self.trimmer.trimmingState == .none ? CMTimeAdd(time, self.trimmer.selectedRange.start) : time
|
|
252
|
+
self.trimmer.progress = finalTime
|
|
253
|
+
|
|
254
|
+
if player.timeControlStatus == .playing {
|
|
255
|
+
playBtn.setImage(pauseIcon, for: .normal)
|
|
256
|
+
} else {
|
|
257
|
+
playBtn.setImage(playIcon, for: .normal)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
updateLabels()
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
override func viewWillDisappear(_ animated: Bool) {
|
|
265
|
+
super.viewWillDisappear(animated)
|
|
266
|
+
|
|
267
|
+
player.pause()
|
|
268
|
+
if timeObserverToken != nil {
|
|
269
|
+
player.removeTimeObserver(timeObserverToken as Any)
|
|
270
|
+
timeObserverToken = nil
|
|
271
|
+
}
|
|
272
|
+
playerController.player = nil
|
|
273
|
+
playerController.dismiss(animated: false, completion: nil)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
@objc private func togglePlay(sender: UIButton) {
|
|
277
|
+
if player.timeControlStatus == .playing {
|
|
278
|
+
player.pause()
|
|
279
|
+
} else {
|
|
280
|
+
player.play()
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
@objc func onSaveBtnClicked() {
|
|
286
|
+
saveBtnClicked?(trimmer.selectedRange)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
@objc func onCancelBtnClicked() {
|
|
290
|
+
cancelBtnClicked?()
|
|
291
|
+
}
|
|
292
|
+
}
|
package/lib/commonjs/index.js
CHANGED
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
+
exports.cleanFiles = cleanFiles;
|
|
7
|
+
exports.deleteFile = deleteFile;
|
|
6
8
|
exports.isValidVideo = isValidVideo;
|
|
9
|
+
exports.listFiles = listFiles;
|
|
7
10
|
exports.showEditor = showEditor;
|
|
8
11
|
var _reactNative = require("react-native");
|
|
9
12
|
const LINKING_ERROR = `The package 'react-native-video-trim' doesn't seem to be linked. Make sure: \n\n` + _reactNative.Platform.select({
|
|
@@ -15,18 +18,31 @@ const VideoTrim = _reactNative.NativeModules.VideoTrim ? _reactNative.NativeModu
|
|
|
15
18
|
throw new Error(LINKING_ERROR);
|
|
16
19
|
}
|
|
17
20
|
});
|
|
18
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Delete a file
|
|
23
|
+
*
|
|
24
|
+
* @param {string} videoPath: absolute non-empty file path to edit
|
|
25
|
+
* @param {EditorConfig} config: editor configuration
|
|
26
|
+
* @returns {void} A **Promise** which resolves `void`
|
|
27
|
+
*/
|
|
28
|
+
async function showEditor(filePath) {
|
|
19
29
|
let config = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
30
|
+
if (!(filePath !== null && filePath !== void 0 && filePath.trim().length)) {
|
|
31
|
+
throw new Error('File path cannot be empty!');
|
|
32
|
+
}
|
|
20
33
|
const {
|
|
21
34
|
saveToPhoto = true
|
|
22
35
|
} = config;
|
|
23
|
-
const outputPath = await VideoTrim.showEditor(
|
|
36
|
+
const outputPath = await VideoTrim.showEditor(filePath, config);
|
|
24
37
|
if (_reactNative.Platform.OS === 'android') {
|
|
25
38
|
if (saveToPhoto) {
|
|
26
39
|
try {
|
|
27
40
|
if (_reactNative.Platform.Version >= 33) {
|
|
28
41
|
// since android 13 it's not needed to request permission for write storage: https://github.com/facebook/react-native/issues/36714#issuecomment-1491338276
|
|
29
42
|
await VideoTrim.saveVideo(outputPath);
|
|
43
|
+
if (config.removeAfterSavedToPhoto) {
|
|
44
|
+
deleteFile(outputPath);
|
|
45
|
+
}
|
|
30
46
|
} else {
|
|
31
47
|
const granted = await _reactNative.PermissionsAndroid.request(_reactNative.PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, {
|
|
32
48
|
title: 'Video Trimmer Photos Access Required',
|
|
@@ -37,6 +53,9 @@ async function showEditor(videoPath) {
|
|
|
37
53
|
});
|
|
38
54
|
if (granted === _reactNative.PermissionsAndroid.RESULTS.GRANTED) {
|
|
39
55
|
await VideoTrim.saveVideo(outputPath);
|
|
56
|
+
if (config.removeAfterSavedToPhoto) {
|
|
57
|
+
deleteFile(outputPath);
|
|
58
|
+
}
|
|
40
59
|
} else {
|
|
41
60
|
throw new Error('Photos Library permission denied');
|
|
42
61
|
}
|
|
@@ -51,7 +70,48 @@ async function showEditor(videoPath) {
|
|
|
51
70
|
}
|
|
52
71
|
}
|
|
53
72
|
}
|
|
54
|
-
|
|
55
|
-
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Delete a file
|
|
76
|
+
*
|
|
77
|
+
* @param {string} filePath: absolute non-empty file path to check if editable
|
|
78
|
+
* @returns {Promise} A **Promise** which resolves `true` if editable
|
|
79
|
+
*/
|
|
80
|
+
function isValidVideo(filePath) {
|
|
81
|
+
if (!(filePath !== null && filePath !== void 0 && filePath.trim().length)) {
|
|
82
|
+
throw new Error('File path cannot be empty!');
|
|
83
|
+
}
|
|
84
|
+
return VideoTrim.isValidVideo(filePath);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Clean output files generated at all time
|
|
89
|
+
*
|
|
90
|
+
* @returns {Promise<string[]>} A **Promise** which resolves to array of files
|
|
91
|
+
*/
|
|
92
|
+
function listFiles() {
|
|
93
|
+
return VideoTrim.listFiles();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Clean output files generated at all time
|
|
98
|
+
*
|
|
99
|
+
* @returns {Promise} A **Promise** which resolves to number of deleted files
|
|
100
|
+
*/
|
|
101
|
+
function cleanFiles() {
|
|
102
|
+
return VideoTrim.cleanFiles();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Delete a file
|
|
107
|
+
*
|
|
108
|
+
* @param {string} filePath: absolute non-empty file path to delete
|
|
109
|
+
* @returns {Promise} A **Promise** which resolves `true` if successful
|
|
110
|
+
*/
|
|
111
|
+
function deleteFile(filePath) {
|
|
112
|
+
if (!(filePath !== null && filePath !== void 0 && filePath.trim().length)) {
|
|
113
|
+
throw new Error('File path cannot be empty!');
|
|
114
|
+
}
|
|
115
|
+
return VideoTrim.deleteFile(filePath);
|
|
56
116
|
}
|
|
57
117
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["_reactNative","require","LINKING_ERROR","Platform","select","ios","default","VideoTrim","NativeModules","Proxy","get","Error","showEditor","
|
|
1
|
+
{"version":3,"names":["_reactNative","require","LINKING_ERROR","Platform","select","ios","default","VideoTrim","NativeModules","Proxy","get","Error","showEditor","filePath","config","arguments","length","undefined","trim","saveToPhoto","outputPath","OS","Version","saveVideo","removeAfterSavedToPhoto","deleteFile","granted","PermissionsAndroid","request","PERMISSIONS","WRITE_EXTERNAL_STORAGE","title","message","buttonNeutral","buttonNegative","buttonPositive","RESULTS","GRANTED","err","hideDialog","isValidVideo","listFiles","cleanFiles"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":"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"}
|
package/lib/module/index.js
CHANGED
|
@@ -8,18 +8,31 @@ const VideoTrim = NativeModules.VideoTrim ? NativeModules.VideoTrim : new Proxy(
|
|
|
8
8
|
throw new Error(LINKING_ERROR);
|
|
9
9
|
}
|
|
10
10
|
});
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Delete a file
|
|
13
|
+
*
|
|
14
|
+
* @param {string} videoPath: absolute non-empty file path to edit
|
|
15
|
+
* @param {EditorConfig} config: editor configuration
|
|
16
|
+
* @returns {void} A **Promise** which resolves `void`
|
|
17
|
+
*/
|
|
18
|
+
export async function showEditor(filePath) {
|
|
12
19
|
let config = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
20
|
+
if (!(filePath !== null && filePath !== void 0 && filePath.trim().length)) {
|
|
21
|
+
throw new Error('File path cannot be empty!');
|
|
22
|
+
}
|
|
13
23
|
const {
|
|
14
24
|
saveToPhoto = true
|
|
15
25
|
} = config;
|
|
16
|
-
const outputPath = await VideoTrim.showEditor(
|
|
26
|
+
const outputPath = await VideoTrim.showEditor(filePath, config);
|
|
17
27
|
if (Platform.OS === 'android') {
|
|
18
28
|
if (saveToPhoto) {
|
|
19
29
|
try {
|
|
20
30
|
if (Platform.Version >= 33) {
|
|
21
31
|
// since android 13 it's not needed to request permission for write storage: https://github.com/facebook/react-native/issues/36714#issuecomment-1491338276
|
|
22
32
|
await VideoTrim.saveVideo(outputPath);
|
|
33
|
+
if (config.removeAfterSavedToPhoto) {
|
|
34
|
+
deleteFile(outputPath);
|
|
35
|
+
}
|
|
23
36
|
} else {
|
|
24
37
|
const granted = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, {
|
|
25
38
|
title: 'Video Trimmer Photos Access Required',
|
|
@@ -30,6 +43,9 @@ export async function showEditor(videoPath) {
|
|
|
30
43
|
});
|
|
31
44
|
if (granted === PermissionsAndroid.RESULTS.GRANTED) {
|
|
32
45
|
await VideoTrim.saveVideo(outputPath);
|
|
46
|
+
if (config.removeAfterSavedToPhoto) {
|
|
47
|
+
deleteFile(outputPath);
|
|
48
|
+
}
|
|
33
49
|
} else {
|
|
34
50
|
throw new Error('Photos Library permission denied');
|
|
35
51
|
}
|
|
@@ -44,7 +60,48 @@ export async function showEditor(videoPath) {
|
|
|
44
60
|
}
|
|
45
61
|
}
|
|
46
62
|
}
|
|
47
|
-
|
|
48
|
-
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Delete a file
|
|
66
|
+
*
|
|
67
|
+
* @param {string} filePath: absolute non-empty file path to check if editable
|
|
68
|
+
* @returns {Promise} A **Promise** which resolves `true` if editable
|
|
69
|
+
*/
|
|
70
|
+
export function isValidVideo(filePath) {
|
|
71
|
+
if (!(filePath !== null && filePath !== void 0 && filePath.trim().length)) {
|
|
72
|
+
throw new Error('File path cannot be empty!');
|
|
73
|
+
}
|
|
74
|
+
return VideoTrim.isValidVideo(filePath);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Clean output files generated at all time
|
|
79
|
+
*
|
|
80
|
+
* @returns {Promise<string[]>} A **Promise** which resolves to array of files
|
|
81
|
+
*/
|
|
82
|
+
export function listFiles() {
|
|
83
|
+
return VideoTrim.listFiles();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Clean output files generated at all time
|
|
88
|
+
*
|
|
89
|
+
* @returns {Promise} A **Promise** which resolves to number of deleted files
|
|
90
|
+
*/
|
|
91
|
+
export function cleanFiles() {
|
|
92
|
+
return VideoTrim.cleanFiles();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Delete a file
|
|
97
|
+
*
|
|
98
|
+
* @param {string} filePath: absolute non-empty file path to delete
|
|
99
|
+
* @returns {Promise} A **Promise** which resolves `true` if successful
|
|
100
|
+
*/
|
|
101
|
+
export function deleteFile(filePath) {
|
|
102
|
+
if (!(filePath !== null && filePath !== void 0 && filePath.trim().length)) {
|
|
103
|
+
throw new Error('File path cannot be empty!');
|
|
104
|
+
}
|
|
105
|
+
return VideoTrim.deleteFile(filePath);
|
|
49
106
|
}
|
|
50
107
|
//# sourceMappingURL=index.js.map
|
package/lib/module/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["NativeModules","PermissionsAndroid","Platform","LINKING_ERROR","select","ios","default","VideoTrim","Proxy","get","Error","showEditor","
|
|
1
|
+
{"version":3,"names":["NativeModules","PermissionsAndroid","Platform","LINKING_ERROR","select","ios","default","VideoTrim","Proxy","get","Error","showEditor","filePath","config","arguments","length","undefined","trim","saveToPhoto","outputPath","OS","Version","saveVideo","removeAfterSavedToPhoto","deleteFile","granted","request","PERMISSIONS","WRITE_EXTERNAL_STORAGE","title","message","buttonNeutral","buttonNegative","buttonPositive","RESULTS","GRANTED","err","hideDialog","isValidVideo","listFiles","cleanFiles"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":"AAAA,SAASA,aAAa,EAAEC,kBAAkB,EAAEC,QAAQ,QAAQ,cAAc;AAE1E,MAAMC,aAAa,GAChB,kFAAiF,GAClFD,QAAQ,CAACE,MAAM,CAAC;EAAEC,GAAG,EAAE,gCAAgC;EAAEC,OAAO,EAAE;AAAG,CAAC,CAAC,GACvE,sDAAsD,GACtD,+BAA+B;AAEjC,MAAMC,SAAS,GAAGP,aAAa,CAACO,SAAS,GACrCP,aAAa,CAACO,SAAS,GACvB,IAAIC,KAAK,CACP,CAAC,CAAC,EACF;EACEC,GAAGA,CAAA,EAAG;IACJ,MAAM,IAAIC,KAAK,CAACP,aAAa,CAAC;EAChC;AACF,CACF,CAAC;AAqBL;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,eAAeQ,UAAUA,CAC9BC,QAAgB,EAED;EAAA,IADfC,MAAoB,GAAAC,SAAA,CAAAC,MAAA,QAAAD,SAAA,QAAAE,SAAA,GAAAF,SAAA,MAAG,CAAC,CAAC;EAEzB,IAAI,EAACF,QAAQ,aAARA,QAAQ,eAARA,QAAQ,CAAEK,IAAI,CAAC,CAAC,CAACF,MAAM,GAAE;IAC5B,MAAM,IAAIL,KAAK,CAAC,4BAA4B,CAAC;EAC/C;EAEA,MAAM;IAAEQ,WAAW,GAAG;EAAK,CAAC,GAAGL,MAAM;EACrC,MAAMM,UAAU,GAAG,MAAMZ,SAAS,CAACI,UAAU,CAACC,QAAQ,EAAEC,MAAM,CAAC;EAE/D,IAAIX,QAAQ,CAACkB,EAAE,KAAK,SAAS,EAAE;IAC7B,IAAIF,WAAW,EAAE;MACf,IAAI;QACF,IAAIhB,QAAQ,CAACmB,OAAO,IAAI,EAAE,EAAE;UAC1B;UACA,MAAMd,SAAS,CAACe,SAAS,CAACH,UAAU,CAAC;UAErC,IAAIN,MAAM,CAACU,uBAAuB,EAAE;YAClCC,UAAU,CAACL,UAAU,CAAC;UACxB;QACF,CAAC,MAAM;UACL,MAAMM,OAAO,GAAG,MAAMxB,kBAAkB,CAACyB,OAAO,CAC9CzB,kBAAkB,CAAC0B,WAAW,CAACC,sBAAsB,EACrD;YACEC,KAAK,EAAE,sCAAsC;YAC7CC,OAAO,EAAE,mDAAmD;YAC5DC,aAAa,EAAE,cAAc;YAC7BC,cAAc,EAAE,QAAQ;YACxBC,cAAc,EAAE;UAClB,CACF,CAAC;UACD,IAAIR,OAAO,KAAKxB,kBAAkB,CAACiC,OAAO,CAACC,OAAO,EAAE;YAClD,MAAM5B,SAAS,CAACe,SAAS,CAACH,UAAU,CAAC;YAErC,IAAIN,MAAM,CAACU,uBAAuB,EAAE;cAClCC,UAAU,CAACL,UAAU,CAAC;YACxB;UACF,CAAC,MAAM;YACL,MAAM,IAAIT,KAAK,CAAC,kCAAkC,CAAC;UACrD;QACF;MACF,CAAC,CAAC,OAAO0B,GAAG,EAAE;QACZ,MAAMA,GAAG;MACX,CAAC,SAAS;QACR7B,SAAS,CAAC8B,UAAU,CAAC,CAAC;MACxB;IACF,CAAC,MAAM;MACL9B,SAAS,CAAC8B,UAAU,CAAC,CAAC;IACxB;EACF;AACF;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,YAAYA,CAAC1B,QAAgB,EAAoB;EAC/D,IAAI,EAACA,QAAQ,aAARA,QAAQ,eAARA,QAAQ,CAAEK,IAAI,CAAC,CAAC,CAACF,MAAM,GAAE;IAC5B,MAAM,IAAIL,KAAK,CAAC,4BAA4B,CAAC;EAC/C;EACA,OAAOH,SAAS,CAAC+B,YAAY,CAAC1B,QAAQ,CAAC;AACzC;;AAEA;AACA;AACA;AACA;AACA;AACA,OAAO,SAAS2B,SAASA,CAAA,EAAsB;EAC7C,OAAOhC,SAAS,CAACgC,SAAS,CAAC,CAAC;AAC9B;;AAEA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,UAAUA,CAAA,EAAoB;EAC5C,OAAOjC,SAAS,CAACiC,UAAU,CAAC,CAAC;AAC/B;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAAShB,UAAUA,CAACZ,QAAgB,EAAoB;EAC7D,IAAI,EAACA,QAAQ,aAARA,QAAQ,eAARA,QAAQ,CAAEK,IAAI,CAAC,CAAC,CAACF,MAAM,GAAE;IAC5B,MAAM,IAAIL,KAAK,CAAC,4BAA4B,CAAC;EAC/C;EACA,OAAOH,SAAS,CAACiB,UAAU,CAACZ,QAAQ,CAAC;AACvC"}
|