react-native-video-trim 2.0.0 → 2.2.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 +168 -33
- package/android/src/main/AndroidManifest.xml +13 -0
- package/android/src/main/java/com/videotrim/VideoTrimModule.java +282 -75
- package/android/src/main/java/com/videotrim/enums/ErrorCode.java +10 -0
- package/android/src/main/java/com/videotrim/interfaces/VideoTrimListener.java +4 -1
- package/android/src/main/java/com/videotrim/utils/MediaMetadataUtil.java +75 -0
- package/android/src/main/java/com/videotrim/utils/StorageUtil.java +2 -2
- package/android/src/main/java/com/videotrim/utils/VideoTrimmerUtil.java +26 -16
- package/android/src/main/java/com/videotrim/widgets/VideoTrimmerView.java +310 -81
- package/android/src/main/res/drawable/airpodsmax.xml +19 -0
- package/android/src/main/res/drawable/exclamationmark_triangle_fill.xml +15 -0
- package/android/src/main/res/drawable/thumb_container_bg.xml +8 -0
- package/android/src/main/res/layout/video_trimmer_view.xml +71 -4
- package/android/src/main/res/xml/file_paths.xml +5 -0
- package/ios/AssetLoader.swift +99 -0
- package/ios/ErrorCode.swift +16 -0
- package/ios/ProgressAlertController.swift +100 -0
- package/ios/VideoTrim.mm +4 -2
- package/ios/VideoTrim.swift +472 -177
- package/ios/VideoTrimmer.swift +16 -10
- package/ios/VideoTrimmerViewController.swift +191 -22
- package/lib/commonjs/index.js +25 -55
- package/lib/commonjs/index.js.map +1 -1
- package/lib/module/index.js +24 -55
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/index.d.ts +215 -9
- package/lib/typescript/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/index.tsx +229 -66
- package/android/src/main/java/iknow/android/utils/BuildConfig.java +0 -18
- package/android/src/main/java/iknow/android/utils/DateUtil.java +0 -64
package/ios/VideoTrimmer.swift
CHANGED
|
@@ -36,7 +36,7 @@ import AVFoundation
|
|
|
36
36
|
// TODO: migrate all to AutoLayout
|
|
37
37
|
private let thumbView: VideoTrimmerThumb = {
|
|
38
38
|
let view = VideoTrimmerThumb()
|
|
39
|
-
view.accessibilityIdentifier = "
|
|
39
|
+
view.accessibilityIdentifier = "thumbView"
|
|
40
40
|
return view
|
|
41
41
|
}()
|
|
42
42
|
|
|
@@ -59,7 +59,7 @@ import AVFoundation
|
|
|
59
59
|
let view = UIView()
|
|
60
60
|
view.accessibilityIdentifier = "thumbnailClipView"
|
|
61
61
|
view.translatesAutoresizingMaskIntoConstraints = false
|
|
62
|
-
|
|
62
|
+
|
|
63
63
|
return view
|
|
64
64
|
}()
|
|
65
65
|
|
|
@@ -152,6 +152,7 @@ import AVFoundation
|
|
|
152
152
|
// a clip cannot be trimmed shorter than this duration
|
|
153
153
|
var minimumDuration: CMTime = CMTime(seconds: 1, preferredTimescale: 600)
|
|
154
154
|
var maximumDuration: CMTime = .positiveInfinity
|
|
155
|
+
var enableHapticFeedback = true
|
|
155
156
|
|
|
156
157
|
// the available range of the asset.
|
|
157
158
|
// Will be set to the full duration of the asset when assigning a new asset
|
|
@@ -517,7 +518,9 @@ import AVFoundation
|
|
|
517
518
|
isZoomedIn = true
|
|
518
519
|
animateChanges()
|
|
519
520
|
|
|
520
|
-
|
|
521
|
+
if enableHapticFeedback {
|
|
522
|
+
UISelectionFeedbackGenerator().selectionChanged()
|
|
523
|
+
}
|
|
521
524
|
}
|
|
522
525
|
|
|
523
526
|
private func animateChanges() {
|
|
@@ -530,12 +533,13 @@ import AVFoundation
|
|
|
530
533
|
}
|
|
531
534
|
|
|
532
535
|
private func startPanning() {
|
|
533
|
-
UISelectionFeedbackGenerator().selectionChanged()
|
|
534
|
-
|
|
535
536
|
didClampWhilePanning = false
|
|
536
537
|
|
|
537
|
-
|
|
538
|
-
|
|
538
|
+
if enableHapticFeedback {
|
|
539
|
+
UISelectionFeedbackGenerator().selectionChanged()
|
|
540
|
+
impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .heavy)
|
|
541
|
+
impactFeedbackGenerator?.prepare()
|
|
542
|
+
}
|
|
539
543
|
|
|
540
544
|
UIView.animate(withDuration: 0.25, delay: 0, options: [.beginFromCurrentState, .allowUserInteraction], animations: {
|
|
541
545
|
self.updateProgressIndicator()
|
|
@@ -612,10 +616,12 @@ import AVFoundation
|
|
|
612
616
|
}
|
|
613
617
|
switch sender.state {
|
|
614
618
|
case .began:
|
|
619
|
+
if enableHapticFeedback {
|
|
620
|
+
UISelectionFeedbackGenerator().selectionChanged()
|
|
621
|
+
impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .heavy)
|
|
622
|
+
impactFeedbackGenerator?.prepare()
|
|
623
|
+
}
|
|
615
624
|
|
|
616
|
-
UISelectionFeedbackGenerator().selectionChanged()
|
|
617
|
-
impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .heavy)
|
|
618
|
-
impactFeedbackGenerator?.prepare()
|
|
619
625
|
didClampWhilePanning = false
|
|
620
626
|
|
|
621
627
|
isScrubbing = true
|
|
@@ -22,13 +22,23 @@ extension CMTime {
|
|
|
22
22
|
|
|
23
23
|
@available(iOS 13.0, *)
|
|
24
24
|
class VideoTrimmerViewController: UIViewController {
|
|
25
|
-
var asset: AVAsset
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
var asset: AVAsset? {
|
|
26
|
+
didSet {
|
|
27
|
+
if let _ = asset {
|
|
28
|
+
setupVideoTrimmer()
|
|
29
|
+
setupPlayerController()
|
|
30
|
+
setupTimeObserver()
|
|
31
|
+
updateLabels()
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
private var maximumDuration: Int?
|
|
36
|
+
private var minimumDuration: Int?
|
|
37
|
+
private var cancelButtonText = "Cancel"
|
|
38
|
+
private var saveButtonText = "Save"
|
|
30
39
|
var cancelBtnClicked: (() -> Void)?
|
|
31
40
|
var saveBtnClicked: ((CMTimeRange) -> Void)?
|
|
41
|
+
private var enableHapticFeedback = true
|
|
32
42
|
|
|
33
43
|
private let playerController = AVPlayerViewController()
|
|
34
44
|
private var trimmer: VideoTrimmer!
|
|
@@ -39,16 +49,50 @@ class VideoTrimmerViewController: UIViewController {
|
|
|
39
49
|
private var btnStackView: UIStackView!
|
|
40
50
|
private var cancelBtn: UIButton!
|
|
41
51
|
private var playBtn: UIButton!
|
|
52
|
+
private let loadingIndicator = UIActivityIndicatorView()
|
|
42
53
|
private var saveBtn: UIButton!
|
|
43
54
|
private let playIcon = UIImage(systemName: "play.fill")
|
|
44
55
|
private let pauseIcon = UIImage(systemName: "pause.fill")
|
|
56
|
+
private let audioBannerView = UIImage(systemName: "airpodsmax")
|
|
45
57
|
private var player: AVPlayer! { playerController.player }
|
|
46
58
|
private var timeObserverToken: Any?
|
|
59
|
+
private var autoplay = false
|
|
60
|
+
private var jumpToPositionOnLoad: Double = 0;
|
|
61
|
+
private var headerText: String?
|
|
62
|
+
private var headerTextSize = 16
|
|
63
|
+
private var headerTextColor: NSNumber?
|
|
64
|
+
private var headerView: UIView?
|
|
47
65
|
|
|
48
66
|
var isSeekInProgress: Bool = false // Marker
|
|
49
67
|
private var chaseTime = CMTime.zero
|
|
50
68
|
private var preferredFrameRate: Float = 23.98
|
|
51
69
|
|
|
70
|
+
public func onAssetFailToLoad() {
|
|
71
|
+
loadingIndicator.stopAnimating()
|
|
72
|
+
btnStackView.removeArrangedSubview(loadingIndicator)
|
|
73
|
+
loadingIndicator.removeFromSuperview()
|
|
74
|
+
|
|
75
|
+
let imageViewContainer = UIView()
|
|
76
|
+
let imageView = UIImageView(image: UIImage(systemName: "exclamationmark.triangle.fill"))
|
|
77
|
+
imageView.tintColor = .systemYellow
|
|
78
|
+
imageView.translatesAutoresizingMaskIntoConstraints = false
|
|
79
|
+
|
|
80
|
+
imageViewContainer.addSubview(imageView)
|
|
81
|
+
NSLayoutConstraint.activate([
|
|
82
|
+
imageView.widthAnchor.constraint(equalToConstant: 36),
|
|
83
|
+
imageView.heightAnchor.constraint(equalToConstant: 36),
|
|
84
|
+
imageView.centerXAnchor.constraint(equalTo: imageViewContainer.centerXAnchor),
|
|
85
|
+
imageView.centerYAnchor.constraint(equalTo: imageViewContainer.centerYAnchor)
|
|
86
|
+
])
|
|
87
|
+
imageViewContainer.alpha = 0
|
|
88
|
+
|
|
89
|
+
btnStackView.insertArrangedSubview(imageViewContainer, at: 1)
|
|
90
|
+
|
|
91
|
+
UIView.animate(withDuration: 0.25, animations: {
|
|
92
|
+
imageViewContainer.alpha = 1
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
52
96
|
// MARK: - Input
|
|
53
97
|
@objc private func didBeginTrimmingFromStart(_ sender: VideoTrimmer) {
|
|
54
98
|
handleBeforeProgressChange()
|
|
@@ -107,7 +151,7 @@ class VideoTrimmerViewController: UIViewController {
|
|
|
107
151
|
private func handleTrimmingEnd(_ start: Bool) {
|
|
108
152
|
self.trimmer.progress = start ? trimmer.selectedRange.start : trimmer.selectedRange.end
|
|
109
153
|
updateLabels()
|
|
110
|
-
|
|
154
|
+
seek(to: trimmer.progress)
|
|
111
155
|
}
|
|
112
156
|
|
|
113
157
|
// MARK: - UIViewController
|
|
@@ -117,17 +161,18 @@ class VideoTrimmerViewController: UIViewController {
|
|
|
117
161
|
setupView()
|
|
118
162
|
setupButtons()
|
|
119
163
|
setupTimeLabels()
|
|
120
|
-
setupVideoTrimmer()
|
|
121
|
-
setupPlayerController()
|
|
122
|
-
setupTimeObserver()
|
|
123
|
-
|
|
124
|
-
updateLabels()
|
|
125
164
|
}
|
|
126
165
|
|
|
127
166
|
override func viewWillDisappear(_ animated: Bool) {
|
|
128
167
|
super.viewWillDisappear(animated)
|
|
129
168
|
|
|
169
|
+
// if asset has been initialized
|
|
170
|
+
guard let _ = asset else { return }
|
|
130
171
|
player.pause()
|
|
172
|
+
|
|
173
|
+
// Clean up the observer
|
|
174
|
+
player.removeObserver(self, forKeyPath: "status")
|
|
175
|
+
|
|
131
176
|
if let token = timeObserverToken {
|
|
132
177
|
player.removeTimeObserver(token)
|
|
133
178
|
timeObserverToken = nil
|
|
@@ -139,13 +184,18 @@ class VideoTrimmerViewController: UIViewController {
|
|
|
139
184
|
playerController.dismiss(animated: false, completion: nil)
|
|
140
185
|
}
|
|
141
186
|
|
|
187
|
+
public func pausePlayer() {
|
|
188
|
+
player.pause()
|
|
189
|
+
setPlayBtnIcon()
|
|
190
|
+
}
|
|
191
|
+
|
|
142
192
|
@objc private func togglePlay(sender: UIButton) {
|
|
143
193
|
if player.timeControlStatus == .playing {
|
|
144
194
|
player.pause()
|
|
145
195
|
} else {
|
|
146
196
|
if CMTimeCompare(trimmer.progress, trimmer.selectedRange.end) != -1 {
|
|
147
197
|
trimmer.progress = trimmer.selectedRange.start
|
|
148
|
-
|
|
198
|
+
self.seek(to: trimmer.progress)
|
|
149
199
|
}
|
|
150
200
|
|
|
151
201
|
player.play()
|
|
@@ -164,32 +214,74 @@ class VideoTrimmerViewController: UIViewController {
|
|
|
164
214
|
|
|
165
215
|
// MARK: - Setup Methods
|
|
166
216
|
private func setupView() {
|
|
167
|
-
|
|
217
|
+
self.overrideUserInterfaceStyle = .dark
|
|
218
|
+
view.backgroundColor = .black // need to have this otherwise during animation the background of this VC is still white in white theme
|
|
219
|
+
|
|
220
|
+
if let headerText = headerText {
|
|
221
|
+
headerView = UIView()
|
|
222
|
+
headerView!.translatesAutoresizingMaskIntoConstraints = false
|
|
223
|
+
view.addSubview(headerView!)
|
|
224
|
+
let headerTextView = UITextView()
|
|
225
|
+
headerTextView.text = headerText
|
|
226
|
+
headerTextView.textAlignment = .center
|
|
227
|
+
headerTextView.textColor = headerTextColor != nil ? RCTConvert.uiColor(headerTextColor) : .white
|
|
228
|
+
headerTextView.font = UIFont.systemFont(ofSize: CGFloat(headerTextSize)) // Set font size here
|
|
229
|
+
headerTextView.translatesAutoresizingMaskIntoConstraints = false
|
|
230
|
+
headerView!.addSubview(headerTextView)
|
|
231
|
+
|
|
232
|
+
NSLayoutConstraint.activate([
|
|
233
|
+
// HeaderView constraints
|
|
234
|
+
headerView!.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
|
|
235
|
+
headerView!.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
236
|
+
headerView!.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
237
|
+
headerView!.heightAnchor.constraint(greaterThanOrEqualToConstant: 50),
|
|
238
|
+
|
|
239
|
+
// HeaderText constraints
|
|
240
|
+
headerTextView.topAnchor.constraint(equalTo: headerView!.topAnchor),
|
|
241
|
+
headerTextView.bottomAnchor.constraint(equalTo: headerView!.bottomAnchor),
|
|
242
|
+
headerTextView.leadingAnchor.constraint(equalTo: headerView!.leadingAnchor),
|
|
243
|
+
headerTextView.trailingAnchor.constraint(equalTo: headerView!.trailingAnchor),
|
|
244
|
+
])
|
|
245
|
+
|
|
246
|
+
view.layoutIfNeeded() // layout after activate constraints, otherwise headerView height = screen height, which leads to playerViewController is missing at runtime
|
|
247
|
+
}
|
|
168
248
|
}
|
|
169
249
|
|
|
170
250
|
private func setupButtons() {
|
|
171
|
-
cancelBtn = UIButton.createButton(title:
|
|
251
|
+
cancelBtn = UIButton.createButton(title: cancelButtonText, font: .systemFont(ofSize: 18), titleColor: .white, target: self, action: #selector(onCancelBtnClicked))
|
|
172
252
|
playBtn = UIButton.createButton(image: playIcon, tintColor: .white, target: self, action: #selector(togglePlay(sender:)))
|
|
253
|
+
playBtn.alpha = 0
|
|
254
|
+
playBtn.isEnabled = false
|
|
255
|
+
|
|
173
256
|
saveBtn = UIButton.createButton(title: saveButtonText, font: .systemFont(ofSize: 18), titleColor: .systemBlue, target: self, action: #selector(onSaveBtnClicked))
|
|
257
|
+
saveBtn.alpha = 0
|
|
258
|
+
saveBtn.isEnabled = false
|
|
174
259
|
|
|
175
|
-
btnStackView = UIStackView(arrangedSubviews: [cancelBtn,
|
|
260
|
+
btnStackView = UIStackView(arrangedSubviews: [cancelBtn, loadingIndicator, saveBtn])
|
|
176
261
|
btnStackView.axis = .horizontal
|
|
177
|
-
btnStackView.alignment = .
|
|
262
|
+
btnStackView.alignment = .center
|
|
178
263
|
btnStackView.distribution = .fillEqually
|
|
179
264
|
btnStackView.spacing = UIStackView.spacingUseSystem
|
|
180
|
-
view.addSubview(btnStackView)
|
|
181
265
|
btnStackView.translatesAutoresizingMaskIntoConstraints = false
|
|
266
|
+
|
|
267
|
+
view.addSubview(btnStackView)
|
|
268
|
+
|
|
182
269
|
NSLayoutConstraint.activate([
|
|
183
270
|
btnStackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16),
|
|
184
271
|
btnStackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16),
|
|
185
272
|
btnStackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16)
|
|
186
273
|
])
|
|
274
|
+
|
|
275
|
+
loadingIndicator.startAnimating()
|
|
187
276
|
}
|
|
188
277
|
|
|
189
278
|
private func setupTimeLabels() {
|
|
190
279
|
leadingTrimLabel = UILabel.createLabel(textAlignment: .left, textColor: .white)
|
|
280
|
+
leadingTrimLabel.text = "00:00.000"
|
|
191
281
|
currentTimeLabel = UILabel.createLabel(textAlignment: .center, textColor: .white)
|
|
282
|
+
currentTimeLabel.text = "00:00.000"
|
|
192
283
|
trailingTrimLabel = UILabel.createLabel(textAlignment: .right, textColor: .white)
|
|
284
|
+
trailingTrimLabel.text = "00:00.000"
|
|
193
285
|
|
|
194
286
|
timingStackView = UIStackView(arrangedSubviews: [leadingTrimLabel, currentTimeLabel, trailingTrimLabel])
|
|
195
287
|
timingStackView.axis = .horizontal
|
|
@@ -209,11 +301,12 @@ class VideoTrimmerViewController: UIViewController {
|
|
|
209
301
|
trimmer = VideoTrimmer()
|
|
210
302
|
trimmer.asset = asset
|
|
211
303
|
trimmer.minimumDuration = CMTime(seconds: 1, preferredTimescale: 600)
|
|
304
|
+
trimmer.enableHapticFeedback = enableHapticFeedback
|
|
212
305
|
|
|
213
306
|
if let maxDuration = maximumDuration {
|
|
214
307
|
trimmer.maximumDuration = CMTime(seconds: max(1, Double(maxDuration)), preferredTimescale: 600)
|
|
215
|
-
if trimmer.maximumDuration > asset
|
|
216
|
-
trimmer.maximumDuration = asset
|
|
308
|
+
if trimmer.maximumDuration > asset!.duration {
|
|
309
|
+
trimmer.maximumDuration = asset!.duration
|
|
217
310
|
}
|
|
218
311
|
trimmer.selectedRange = CMTimeRange(start: .zero, end: trimmer.maximumDuration)
|
|
219
312
|
}
|
|
@@ -233,6 +326,7 @@ class VideoTrimmerViewController: UIViewController {
|
|
|
233
326
|
trimmer.addTarget(self, action: #selector(didBeginTrimmingFromEnd(_:)), for: VideoTrimmer.didBeginTrimmingFromEnd)
|
|
234
327
|
trimmer.addTarget(self, action: #selector(trailingGrabberChanged(_:)), for: VideoTrimmer.trailingGrabberChanged)
|
|
235
328
|
trimmer.addTarget(self, action: #selector(didEndTrimmingFromEnd(_:)), for: VideoTrimmer.didEndTrimmingFromEnd)
|
|
329
|
+
trimmer.alpha = 0
|
|
236
330
|
view.addSubview(trimmer)
|
|
237
331
|
trimmer.translatesAutoresizingMaskIntoConstraints = false
|
|
238
332
|
NSLayoutConstraint.activate([
|
|
@@ -241,6 +335,10 @@ class VideoTrimmerViewController: UIViewController {
|
|
|
241
335
|
trimmer.bottomAnchor.constraint(equalTo: timingStackView.topAnchor, constant: -16),
|
|
242
336
|
trimmer.heightAnchor.constraint(equalToConstant: 50)
|
|
243
337
|
])
|
|
338
|
+
|
|
339
|
+
UIView.animate(withDuration: 0.25, animations: {
|
|
340
|
+
self.trimmer.alpha = 1
|
|
341
|
+
})
|
|
244
342
|
}
|
|
245
343
|
|
|
246
344
|
private func setupPlayerController() {
|
|
@@ -249,7 +347,10 @@ class VideoTrimmerViewController: UIViewController {
|
|
|
249
347
|
playerController.allowsVideoFrameAnalysis = false
|
|
250
348
|
}
|
|
251
349
|
playerController.player = AVPlayer()
|
|
252
|
-
player.replaceCurrentItem(with: AVPlayerItem(asset: asset))
|
|
350
|
+
player.replaceCurrentItem(with: AVPlayerItem(asset: asset!))
|
|
351
|
+
|
|
352
|
+
// Add observer for player status
|
|
353
|
+
player.addObserver(self, forKeyPath: "status", options: [.new, .initial], context: nil)
|
|
253
354
|
|
|
254
355
|
try? AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [])
|
|
255
356
|
addChild(playerController)
|
|
@@ -258,7 +359,7 @@ class VideoTrimmerViewController: UIViewController {
|
|
|
258
359
|
NSLayoutConstraint.activate([
|
|
259
360
|
playerController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
260
361
|
playerController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
261
|
-
playerController.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
|
|
362
|
+
playerController.view.topAnchor.constraint(equalTo: headerView != nil ? headerView!.bottomAnchor : view.safeAreaLayoutGuide.topAnchor),
|
|
262
363
|
playerController.view.bottomAnchor.constraint(equalTo: trimmer.topAnchor, constant: -16)
|
|
263
364
|
])
|
|
264
365
|
|
|
@@ -286,7 +387,7 @@ class VideoTrimmerViewController: UIViewController {
|
|
|
286
387
|
if CMTimeCompare(self.trimmer.progress, trimmer.selectedRange.end) == 1 {
|
|
287
388
|
player.pause()
|
|
288
389
|
self.trimmer.progress = trimmer.selectedRange.end
|
|
289
|
-
|
|
390
|
+
self.seek(to: trimmer.selectedRange.end)
|
|
290
391
|
}
|
|
291
392
|
|
|
292
393
|
currentTimeLabel.text = trimmer.progress.displayString
|
|
@@ -333,6 +434,74 @@ class VideoTrimmerViewController: UIViewController {
|
|
|
333
434
|
}
|
|
334
435
|
}
|
|
335
436
|
}
|
|
437
|
+
|
|
438
|
+
public func configure(config: NSDictionary) {
|
|
439
|
+
if let maxDuration = config["maxDuration"] as? Int {
|
|
440
|
+
maximumDuration = maxDuration
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if let minDuration = config["minDuration"] as? Int {
|
|
444
|
+
minimumDuration = minDuration
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if let cancelButtonText = config["cancelButtonText"] as? String, !cancelButtonText.isEmpty {
|
|
448
|
+
self.cancelButtonText = cancelButtonText
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if let saveButtonText = config["saveButtonText"] as? String, !saveButtonText.isEmpty {
|
|
452
|
+
self.saveButtonText = saveButtonText
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if let jumpToPositionOnLoad = config["jumpToPositionOnLoad"] as? Int {
|
|
456
|
+
self.jumpToPositionOnLoad = Double(jumpToPositionOnLoad)
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
enableHapticFeedback = config["enableHapticFeedback"] as? Bool ?? true
|
|
460
|
+
autoplay = config["autoplay"] as? Bool ?? false
|
|
461
|
+
|
|
462
|
+
if let jumpToPositionOnLoad = config["jumpToPositionOnLoad"] as? Int {
|
|
463
|
+
self.jumpToPositionOnLoad = Double(jumpToPositionOnLoad)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if let headerText = config["headerText"] as? String, !headerText.isEmpty {
|
|
467
|
+
self.headerText = headerText
|
|
468
|
+
|
|
469
|
+
headerTextSize = config["headerTextSize"] as? Int ?? 16
|
|
470
|
+
headerTextColor = config["headerTextColor"] as? NSNumber
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
|
475
|
+
if keyPath == "status" {
|
|
476
|
+
if player.status == .readyToPlay {
|
|
477
|
+
loadingIndicator.stopAnimating()
|
|
478
|
+
btnStackView.removeArrangedSubview(loadingIndicator)
|
|
479
|
+
loadingIndicator.removeFromSuperview()
|
|
480
|
+
btnStackView.insertArrangedSubview(playBtn, at: 1)
|
|
481
|
+
|
|
482
|
+
UIView.animate(withDuration: 0.25, animations: {
|
|
483
|
+
self.playBtn.alpha = 1
|
|
484
|
+
self.playBtn.isEnabled = true
|
|
485
|
+
self.saveBtn.alpha = 1
|
|
486
|
+
self.saveBtn.isEnabled = true
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
if jumpToPositionOnLoad > 0 {
|
|
490
|
+
let duration = (asset?.duration.seconds ?? 0) * 1000
|
|
491
|
+
let time = jumpToPositionOnLoad > duration ? duration : jumpToPositionOnLoad
|
|
492
|
+
let cmtime = CMTime(value: CMTimeValue(time), timescale: 1000)
|
|
493
|
+
|
|
494
|
+
self.seek(to: cmtime)
|
|
495
|
+
self.trimmer.progress = cmtime
|
|
496
|
+
self.currentTimeLabel.text = self.trimmer.progress.displayString
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if autoplay {
|
|
500
|
+
togglePlay(sender: playBtn)
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
336
505
|
}
|
|
337
506
|
|
|
338
507
|
private extension UIButton {
|
package/lib/commonjs/index.js
CHANGED
|
@@ -4,8 +4,9 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.cleanFiles = cleanFiles;
|
|
7
|
+
exports.closeEditor = closeEditor;
|
|
7
8
|
exports.deleteFile = deleteFile;
|
|
8
|
-
exports.
|
|
9
|
+
exports.isValidFile = isValidFile;
|
|
9
10
|
exports.listFiles = listFiles;
|
|
10
11
|
exports.showEditor = showEditor;
|
|
11
12
|
var _reactNative = require("react-native");
|
|
@@ -25,63 +26,16 @@ const VideoTrim = _reactNative.NativeModules.VideoTrim ? _reactNative.NativeModu
|
|
|
25
26
|
* @param {EditorConfig} config: editor configuration
|
|
26
27
|
* @returns {void} A **Promise** which resolves `void`
|
|
27
28
|
*/
|
|
28
|
-
|
|
29
|
+
function showEditor(filePath) {
|
|
29
30
|
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
|
-
}
|
|
33
31
|
const {
|
|
34
|
-
|
|
32
|
+
headerTextColor
|
|
35
33
|
} = config;
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
// since android 13 it's not needed to request permission for write storage: https://github.com/facebook/react-native/issues/36714#issuecomment-1491338276
|
|
42
|
-
await VideoTrim.saveVideo(outputPath);
|
|
43
|
-
if (config.removeAfterSavedToPhoto) {
|
|
44
|
-
deleteFile(outputPath);
|
|
45
|
-
}
|
|
46
|
-
} else {
|
|
47
|
-
const granted = await _reactNative.PermissionsAndroid.request(_reactNative.PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, {
|
|
48
|
-
title: 'Video Trimmer Photos Access Required',
|
|
49
|
-
message: 'Grant access to your Photos to write output Video',
|
|
50
|
-
buttonNeutral: 'Ask Me Later',
|
|
51
|
-
buttonNegative: 'Cancel',
|
|
52
|
-
buttonPositive: 'OK'
|
|
53
|
-
});
|
|
54
|
-
if (granted === _reactNative.PermissionsAndroid.RESULTS.GRANTED) {
|
|
55
|
-
await VideoTrim.saveVideo(outputPath);
|
|
56
|
-
if (config.removeAfterSavedToPhoto) {
|
|
57
|
-
deleteFile(outputPath);
|
|
58
|
-
}
|
|
59
|
-
} else {
|
|
60
|
-
throw new Error('Photos Library permission denied');
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
} catch (err) {
|
|
64
|
-
throw err;
|
|
65
|
-
} finally {
|
|
66
|
-
VideoTrim.hideDialog();
|
|
67
|
-
}
|
|
68
|
-
} else {
|
|
69
|
-
VideoTrim.hideDialog();
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
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);
|
|
34
|
+
const color = (0, _reactNative.processColor)(headerTextColor);
|
|
35
|
+
VideoTrim.showEditor(filePath, {
|
|
36
|
+
...config,
|
|
37
|
+
headerTextColor: color
|
|
38
|
+
});
|
|
85
39
|
}
|
|
86
40
|
|
|
87
41
|
/**
|
|
@@ -114,4 +68,20 @@ function deleteFile(filePath) {
|
|
|
114
68
|
}
|
|
115
69
|
return VideoTrim.deleteFile(filePath);
|
|
116
70
|
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Close editor
|
|
74
|
+
*/
|
|
75
|
+
function closeEditor() {
|
|
76
|
+
return VideoTrim.closeEditor();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Check if a file is valid audio or video file
|
|
81
|
+
*
|
|
82
|
+
* @returns {Promise} A **Promise** which resolves file info if successful
|
|
83
|
+
*/
|
|
84
|
+
function isValidFile(url) {
|
|
85
|
+
return VideoTrim.isValidFile(url);
|
|
86
|
+
}
|
|
117
87
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["_reactNative","require","LINKING_ERROR","Platform","select","ios","default","VideoTrim","NativeModules","Proxy","get","Error","showEditor","filePath","config","arguments","length","undefined","
|
|
1
|
+
{"version":3,"names":["_reactNative","require","LINKING_ERROR","Platform","select","ios","default","VideoTrim","NativeModules","Proxy","get","Error","showEditor","filePath","config","arguments","length","undefined","headerTextColor","color","processColor","listFiles","cleanFiles","deleteFile","trim","closeEditor","isValidFile","url"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;;;;;;;;;;AAAA,IAAAA,YAAA,GAAAC,OAAA;AAOA,MAAMC,aAAa,GAChB,kFAAiF,GAClFC,qBAAQ,CAACC,MAAM,CAAC;EAAEC,GAAG,EAAE,gCAAgC;EAAEC,OAAO,EAAE;AAAG,CAAC,CAAC,GACvE,sDAAsD,GACtD,+BAA+B;AAEjC,MAAMC,SAAS,GAAGC,0BAAa,CAACD,SAAS,GACrCC,0BAAa,CAACD,SAAS,GACvB,IAAIE,KAAK,CACP,CAAC,CAAC,EACF;EACEC,GAAGA,CAAA,EAAG;IACJ,MAAM,IAAIC,KAAK,CAACT,aAAa,CAAC;EAChC;AACF,CACF,CAAC;AAiOL;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASU,UAAUA,CAACC,QAAgB,EAAmC;EAAA,IAAjCC,MAAoB,GAAAC,SAAA,CAAAC,MAAA,QAAAD,SAAA,QAAAE,SAAA,GAAAF,SAAA,MAAG,CAAC,CAAC;EACpE,MAAM;IAAEG;EAAgB,CAAC,GAAGJ,MAAM;EAClC,MAAMK,KAAK,GAAG,IAAAC,yBAAY,EAACF,eAAe,CAAC;EAC3CX,SAAS,CAACK,UAAU,CAACC,QAAQ,EAAE;IAAE,GAAGC,MAAM;IAAEI,eAAe,EAAEC;EAAM,CAAC,CAAC;AACvE;;AAEA;AACA;AACA;AACA;AACA;AACO,SAASE,SAASA,CAAA,EAAsB;EAC7C,OAAOd,SAAS,CAACc,SAAS,CAAC,CAAC;AAC9B;;AAEA;AACA;AACA;AACA;AACA;AACO,SAASC,UAAUA,CAAA,EAAoB;EAC5C,OAAOf,SAAS,CAACe,UAAU,CAAC,CAAC;AAC/B;;AAEA;AACA;AACA;AACA;AACA;AACA;AACO,SAASC,UAAUA,CAACV,QAAgB,EAAoB;EAC7D,IAAI,EAACA,QAAQ,aAARA,QAAQ,eAARA,QAAQ,CAAEW,IAAI,CAAC,CAAC,CAACR,MAAM,GAAE;IAC5B,MAAM,IAAIL,KAAK,CAAC,4BAA4B,CAAC;EAC/C;EACA,OAAOJ,SAAS,CAACgB,UAAU,CAACV,QAAQ,CAAC;AACvC;;AAEA;AACA;AACA;AACO,SAASY,WAAWA,CAAA,EAAS;EAClC,OAAOlB,SAAS,CAACkB,WAAW,CAAC,CAAC;AAChC;;AAEA;AACA;AACA;AACA;AACA;AACO,SAASC,WAAWA,CAACC,GAAW,EAAoB;EACzD,OAAOpB,SAAS,CAACmB,WAAW,CAACC,GAAG,CAAC;AACnC"}
|
package/lib/module/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { NativeModules,
|
|
1
|
+
import { NativeModules, Platform, processColor } from 'react-native';
|
|
2
2
|
const LINKING_ERROR = `The package 'react-native-video-trim' doesn't seem to be linked. Make sure: \n\n` + Platform.select({
|
|
3
3
|
ios: "- You have run 'pod install'\n",
|
|
4
4
|
default: ''
|
|
@@ -15,63 +15,16 @@ const VideoTrim = NativeModules.VideoTrim ? NativeModules.VideoTrim : new Proxy(
|
|
|
15
15
|
* @param {EditorConfig} config: editor configuration
|
|
16
16
|
* @returns {void} A **Promise** which resolves `void`
|
|
17
17
|
*/
|
|
18
|
-
export
|
|
18
|
+
export function showEditor(filePath) {
|
|
19
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
|
-
}
|
|
23
20
|
const {
|
|
24
|
-
|
|
21
|
+
headerTextColor
|
|
25
22
|
} = config;
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// since android 13 it's not needed to request permission for write storage: https://github.com/facebook/react-native/issues/36714#issuecomment-1491338276
|
|
32
|
-
await VideoTrim.saveVideo(outputPath);
|
|
33
|
-
if (config.removeAfterSavedToPhoto) {
|
|
34
|
-
deleteFile(outputPath);
|
|
35
|
-
}
|
|
36
|
-
} else {
|
|
37
|
-
const granted = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, {
|
|
38
|
-
title: 'Video Trimmer Photos Access Required',
|
|
39
|
-
message: 'Grant access to your Photos to write output Video',
|
|
40
|
-
buttonNeutral: 'Ask Me Later',
|
|
41
|
-
buttonNegative: 'Cancel',
|
|
42
|
-
buttonPositive: 'OK'
|
|
43
|
-
});
|
|
44
|
-
if (granted === PermissionsAndroid.RESULTS.GRANTED) {
|
|
45
|
-
await VideoTrim.saveVideo(outputPath);
|
|
46
|
-
if (config.removeAfterSavedToPhoto) {
|
|
47
|
-
deleteFile(outputPath);
|
|
48
|
-
}
|
|
49
|
-
} else {
|
|
50
|
-
throw new Error('Photos Library permission denied');
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
} catch (err) {
|
|
54
|
-
throw err;
|
|
55
|
-
} finally {
|
|
56
|
-
VideoTrim.hideDialog();
|
|
57
|
-
}
|
|
58
|
-
} else {
|
|
59
|
-
VideoTrim.hideDialog();
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
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);
|
|
23
|
+
const color = processColor(headerTextColor);
|
|
24
|
+
VideoTrim.showEditor(filePath, {
|
|
25
|
+
...config,
|
|
26
|
+
headerTextColor: color
|
|
27
|
+
});
|
|
75
28
|
}
|
|
76
29
|
|
|
77
30
|
/**
|
|
@@ -104,4 +57,20 @@ export function deleteFile(filePath) {
|
|
|
104
57
|
}
|
|
105
58
|
return VideoTrim.deleteFile(filePath);
|
|
106
59
|
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Close editor
|
|
63
|
+
*/
|
|
64
|
+
export function closeEditor() {
|
|
65
|
+
return VideoTrim.closeEditor();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Check if a file is valid audio or video file
|
|
70
|
+
*
|
|
71
|
+
* @returns {Promise} A **Promise** which resolves file info if successful
|
|
72
|
+
*/
|
|
73
|
+
export function isValidFile(url) {
|
|
74
|
+
return VideoTrim.isValidFile(url);
|
|
75
|
+
}
|
|
107
76
|
//# sourceMappingURL=index.js.map
|
package/lib/module/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["NativeModules","
|
|
1
|
+
{"version":3,"names":["NativeModules","Platform","processColor","LINKING_ERROR","select","ios","default","VideoTrim","Proxy","get","Error","showEditor","filePath","config","arguments","length","undefined","headerTextColor","color","listFiles","cleanFiles","deleteFile","trim","closeEditor","isValidFile","url"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":"AAAA,SACEA,aAAa,EACbC,QAAQ,EACRC,YAAY,QAEP,cAAc;AAErB,MAAMC,aAAa,GAChB,kFAAiF,GAClFF,QAAQ,CAACG,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;AAiOL;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASQ,UAAUA,CAACC,QAAgB,EAAmC;EAAA,IAAjCC,MAAoB,GAAAC,SAAA,CAAAC,MAAA,QAAAD,SAAA,QAAAE,SAAA,GAAAF,SAAA,MAAG,CAAC,CAAC;EACpE,MAAM;IAAEG;EAAgB,CAAC,GAAGJ,MAAM;EAClC,MAAMK,KAAK,GAAGhB,YAAY,CAACe,eAAe,CAAC;EAC3CV,SAAS,CAACI,UAAU,CAACC,QAAQ,EAAE;IAAE,GAAGC,MAAM;IAAEI,eAAe,EAAEC;EAAM,CAAC,CAAC;AACvE;;AAEA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,SAASA,CAAA,EAAsB;EAC7C,OAAOZ,SAAS,CAACY,SAAS,CAAC,CAAC;AAC9B;;AAEA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,UAAUA,CAAA,EAAoB;EAC5C,OAAOb,SAAS,CAACa,UAAU,CAAC,CAAC;AAC/B;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,UAAUA,CAACT,QAAgB,EAAoB;EAC7D,IAAI,EAACA,QAAQ,aAARA,QAAQ,eAARA,QAAQ,CAAEU,IAAI,CAAC,CAAC,CAACP,MAAM,GAAE;IAC5B,MAAM,IAAIL,KAAK,CAAC,4BAA4B,CAAC;EAC/C;EACA,OAAOH,SAAS,CAACc,UAAU,CAACT,QAAQ,CAAC;AACvC;;AAEA;AACA;AACA;AACA,OAAO,SAASW,WAAWA,CAAA,EAAS;EAClC,OAAOhB,SAAS,CAACgB,WAAW,CAAC,CAAC;AAChC;;AAEA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,WAAWA,CAACC,GAAW,EAAoB;EACzD,OAAOlB,SAAS,CAACiB,WAAW,CAACC,GAAG,CAAC;AACnC"}
|