react-native-video-trim 1.0.21 → 1.0.23
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/ios/VideoTrimmer.swift +387 -330
- package/ios/VideoTrimmerThumb.swift +139 -90
- package/ios/VideoTrimmerViewController.swift +231 -162
- package/package.json +1 -1
|
@@ -1,41 +1,23 @@
|
|
|
1
|
-
//
|
|
2
|
-
// VideoTrimmerViewController.swift
|
|
3
|
-
// react-native-video-trim
|
|
4
|
-
//
|
|
5
|
-
// Created by Duc Trung Mai on 17/1/24.
|
|
6
|
-
//
|
|
7
|
-
|
|
8
1
|
import UIKit
|
|
9
2
|
import AVKit
|
|
10
3
|
|
|
11
4
|
extension CMTime {
|
|
12
5
|
var displayString: String {
|
|
13
6
|
let offset = TimeInterval(seconds)
|
|
14
|
-
let numberOfNanosecondsFloat = (offset - TimeInterval(Int(offset))) *
|
|
7
|
+
let numberOfNanosecondsFloat = (offset - TimeInterval(Int(offset))) * 100.0
|
|
15
8
|
let nanoseconds = Int(numberOfNanosecondsFloat)
|
|
9
|
+
|
|
10
|
+
let formatter = CMTime.dateFormatter
|
|
11
|
+
return String(format: "%@.%02d", formatter.string(from: offset) ?? "00:00", nanoseconds)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
private static var dateFormatter: DateComponentsFormatter = {
|
|
16
15
|
let formatter = DateComponentsFormatter()
|
|
17
16
|
formatter.unitsStyle = .positional
|
|
18
17
|
formatter.zeroFormattingBehavior = .pad
|
|
19
18
|
formatter.allowedUnits = [.minute, .second]
|
|
20
|
-
return
|
|
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
|
-
}
|
|
19
|
+
return formatter
|
|
20
|
+
}()
|
|
39
21
|
}
|
|
40
22
|
|
|
41
23
|
@available(iOS 13.0, *)
|
|
@@ -48,69 +30,60 @@ class VideoTrimmerViewController: UIViewController {
|
|
|
48
30
|
var cancelBtnClicked: (() -> Void)?
|
|
49
31
|
var saveBtnClicked: ((CMTimeRange) -> Void)?
|
|
50
32
|
|
|
51
|
-
let playerController = AVPlayerViewController()
|
|
52
|
-
var trimmer: VideoTrimmer!
|
|
53
|
-
var timingStackView: UIStackView!
|
|
54
|
-
var leadingTrimLabel: UILabel!
|
|
55
|
-
var currentTimeLabel: UILabel!
|
|
56
|
-
var trailingTrimLabel: UILabel!
|
|
57
|
-
|
|
33
|
+
private let playerController = AVPlayerViewController()
|
|
34
|
+
private var trimmer: VideoTrimmer!
|
|
35
|
+
private var timingStackView: UIStackView!
|
|
36
|
+
private var leadingTrimLabel: UILabel!
|
|
37
|
+
private var currentTimeLabel: UILabel!
|
|
38
|
+
private var trailingTrimLabel: UILabel!
|
|
58
39
|
private var btnStackView: UIStackView!
|
|
59
40
|
private var cancelBtn: UIButton!
|
|
60
41
|
private var playBtn: UIButton!
|
|
61
42
|
private var saveBtn: UIButton!
|
|
62
43
|
private let playIcon = UIImage(systemName: "play.fill")
|
|
63
44
|
private let pauseIcon = UIImage(systemName: "pause.fill")
|
|
64
|
-
|
|
65
|
-
private var wasPlaying = false
|
|
66
|
-
private var player: AVPlayer! {playerController.player}
|
|
45
|
+
private var player: AVPlayer! { playerController.player }
|
|
67
46
|
private var timeObserverToken: Any?
|
|
68
47
|
|
|
48
|
+
var isSeekInProgress: Bool = false // Marker
|
|
49
|
+
private var chaseTime = CMTime.zero
|
|
50
|
+
private var preferredFrameRate: Float = 23.98
|
|
69
51
|
|
|
70
52
|
// MARK: - Input
|
|
71
|
-
@objc private func
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
wasPlaying = (player.timeControlStatus != .paused)
|
|
75
|
-
player.pause()
|
|
76
|
-
|
|
77
|
-
updatePlayerAsset()
|
|
53
|
+
@objc private func didBeginTrimmingFromStart(_ sender: VideoTrimmer) {
|
|
54
|
+
handleBeforeProgressChange()
|
|
78
55
|
}
|
|
79
56
|
|
|
80
|
-
@objc private func
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if wasPlaying == true {
|
|
84
|
-
player.play()
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
updatePlayerAsset()
|
|
57
|
+
@objc private func leadingGrabberChanged(_ sender: VideoTrimmer) {
|
|
58
|
+
handleProgressChanged(time: trimmer.selectedRange.start)
|
|
88
59
|
}
|
|
89
60
|
|
|
90
|
-
@objc private func
|
|
91
|
-
|
|
61
|
+
@objc private func didEndTrimmingFromStart(_ sender: VideoTrimmer) {
|
|
62
|
+
handleTrimmingEnd(true)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@objc private func didBeginTrimmingFromEnd(_ sender: VideoTrimmer) {
|
|
66
|
+
handleBeforeProgressChange()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@objc private func trailingGrabberChanged(_ sender: VideoTrimmer) {
|
|
70
|
+
handleProgressChanged(time: trimmer.selectedRange.end)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@objc private func didEndTrimmingFromEnd(_ sender: VideoTrimmer) {
|
|
74
|
+
handleTrimmingEnd(false)
|
|
92
75
|
}
|
|
93
76
|
|
|
94
77
|
@objc private func didBeginScrubbing(_ sender: VideoTrimmer) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
wasPlaying = (player.timeControlStatus != .paused)
|
|
98
|
-
player.pause()
|
|
78
|
+
handleBeforeProgressChange()
|
|
99
79
|
}
|
|
100
80
|
|
|
101
81
|
@objc private func didEndScrubbing(_ sender: VideoTrimmer) {
|
|
102
82
|
updateLabels()
|
|
103
|
-
|
|
104
|
-
if wasPlaying == true {
|
|
105
|
-
player.play()
|
|
106
|
-
}
|
|
107
83
|
}
|
|
108
84
|
|
|
109
85
|
@objc private func progressDidChanged(_ sender: VideoTrimmer) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
let time = CMTimeSubtract(trimmer.progress, trimmer.selectedRange.start)
|
|
113
|
-
player.seek(to: time, toleranceBefore: .zero, toleranceAfter: .zero)
|
|
86
|
+
handleProgressChanged(time: trimmer.progress)
|
|
114
87
|
}
|
|
115
88
|
|
|
116
89
|
// MARK: - Private
|
|
@@ -120,38 +93,84 @@ class VideoTrimmerViewController: UIViewController {
|
|
|
120
93
|
trailingTrimLabel.text = trimmer.selectedRange.end.displayString
|
|
121
94
|
}
|
|
122
95
|
|
|
123
|
-
private func
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
96
|
+
private func handleBeforeProgressChange() {
|
|
97
|
+
updateLabels()
|
|
98
|
+
player.pause()
|
|
99
|
+
setPlayBtnIcon()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private func handleProgressChanged(time: CMTime) {
|
|
103
|
+
updateLabels()
|
|
104
|
+
seek(to: time)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private func handleTrimmingEnd(_ start: Bool) {
|
|
108
|
+
self.trimmer.progress = start ? trimmer.selectedRange.start : trimmer.selectedRange.end
|
|
109
|
+
updateLabels()
|
|
110
|
+
player.seek(to: trimmer.progress, toleranceBefore: .zero, toleranceAfter: .zero)
|
|
129
111
|
}
|
|
130
112
|
|
|
131
113
|
// MARK: - UIViewController
|
|
132
114
|
override func viewDidLoad() {
|
|
133
115
|
super.viewDidLoad()
|
|
134
116
|
|
|
135
|
-
|
|
117
|
+
setupView()
|
|
118
|
+
setupButtons()
|
|
119
|
+
setupTimeLabels()
|
|
120
|
+
setupVideoTrimmer()
|
|
121
|
+
setupPlayerController()
|
|
122
|
+
setupTimeObserver()
|
|
136
123
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
cancelBtn.addTarget(self, action: #selector(onCancelBtnClicked), for: .touchUpInside)
|
|
124
|
+
updateLabels()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
override func viewWillDisappear(_ animated: Bool) {
|
|
128
|
+
super.viewWillDisappear(animated)
|
|
143
129
|
|
|
130
|
+
player.pause()
|
|
131
|
+
if let token = timeObserverToken {
|
|
132
|
+
player.removeTimeObserver(token)
|
|
133
|
+
timeObserverToken = nil
|
|
134
|
+
}
|
|
135
|
+
// Remove observer
|
|
136
|
+
NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: player.currentItem)
|
|
144
137
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
138
|
+
playerController.player = nil
|
|
139
|
+
playerController.dismiss(animated: false, completion: nil)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@objc private func togglePlay(sender: UIButton) {
|
|
143
|
+
if player.timeControlStatus == .playing {
|
|
144
|
+
player.pause()
|
|
145
|
+
} else {
|
|
146
|
+
if CMTimeCompare(trimmer.progress, trimmer.selectedRange.end) != -1 {
|
|
147
|
+
trimmer.progress = trimmer.selectedRange.start
|
|
148
|
+
player.seek(to: trimmer.progress, toleranceBefore: .zero, toleranceAfter: .zero)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
player.play()
|
|
152
|
+
}
|
|
149
153
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
154
|
+
setPlayBtnIcon()
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
@objc private func onSaveBtnClicked() {
|
|
158
|
+
saveBtnClicked?(trimmer.selectedRange)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
@objc private func onCancelBtnClicked() {
|
|
162
|
+
cancelBtnClicked?()
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// MARK: - Setup Methods
|
|
166
|
+
private func setupView() {
|
|
167
|
+
view.backgroundColor = .black
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private func setupButtons() {
|
|
171
|
+
cancelBtn = UIButton.createButton(title: cancelBtnText, font: .systemFont(ofSize: 18), titleColor: .white, target: self, action: #selector(onCancelBtnClicked))
|
|
172
|
+
playBtn = UIButton.createButton(image: playIcon, tintColor: .systemBlue, target: self, action: #selector(togglePlay(sender:)))
|
|
173
|
+
saveBtn = UIButton.createButton(title: saveButtonText, font: .systemFont(ofSize: 18), titleColor: .systemBlue, target: self, action: #selector(onSaveBtnClicked))
|
|
155
174
|
|
|
156
175
|
btnStackView = UIStackView(arrangedSubviews: [cancelBtn, playBtn, saveBtn])
|
|
157
176
|
btnStackView.axis = .horizontal
|
|
@@ -163,24 +182,14 @@ class VideoTrimmerViewController: UIViewController {
|
|
|
163
182
|
NSLayoutConstraint.activate([
|
|
164
183
|
btnStackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16),
|
|
165
184
|
btnStackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16),
|
|
166
|
-
btnStackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16)
|
|
185
|
+
btnStackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16)
|
|
167
186
|
])
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
leadingTrimLabel
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
currentTimeLabel = UILabel()
|
|
176
|
-
currentTimeLabel.font = UIFont.preferredFont(forTextStyle: .caption1)
|
|
177
|
-
currentTimeLabel.textAlignment = .center
|
|
178
|
-
currentTimeLabel.textColor = .white
|
|
179
|
-
|
|
180
|
-
trailingTrimLabel = UILabel()
|
|
181
|
-
trailingTrimLabel.font = UIFont.preferredFont(forTextStyle: .caption1)
|
|
182
|
-
trailingTrimLabel.textAlignment = .right
|
|
183
|
-
trailingTrimLabel.textColor = .white
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private func setupTimeLabels() {
|
|
190
|
+
leadingTrimLabel = UILabel.createLabel(textAlignment: .left, textColor: .white)
|
|
191
|
+
currentTimeLabel = UILabel.createLabel(textAlignment: .center, textColor: .white)
|
|
192
|
+
trailingTrimLabel = UILabel.createLabel(textAlignment: .right, textColor: .white)
|
|
184
193
|
|
|
185
194
|
timingStackView = UIStackView(arrangedSubviews: [leadingTrimLabel, currentTimeLabel, trailingTrimLabel])
|
|
186
195
|
timingStackView.axis = .horizontal
|
|
@@ -192,50 +201,57 @@ class VideoTrimmerViewController: UIViewController {
|
|
|
192
201
|
NSLayoutConstraint.activate([
|
|
193
202
|
timingStackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16),
|
|
194
203
|
timingStackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16),
|
|
195
|
-
timingStackView.bottomAnchor.constraint(equalTo: btnStackView.topAnchor, constant: -8)
|
|
204
|
+
timingStackView.bottomAnchor.constraint(equalTo: btnStackView.topAnchor, constant: -8)
|
|
196
205
|
])
|
|
197
|
-
|
|
198
|
-
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private func setupVideoTrimmer() {
|
|
199
209
|
trimmer = VideoTrimmer()
|
|
200
|
-
trimmer.asset = asset
|
|
210
|
+
trimmer.asset = asset
|
|
201
211
|
trimmer.minimumDuration = CMTime(seconds: 1, preferredTimescale: 600)
|
|
202
212
|
|
|
203
|
-
if
|
|
204
|
-
trimmer.maximumDuration = CMTime(seconds: max(1, Double(
|
|
205
|
-
|
|
206
|
-
// guard check to make sure max duration can only <= asset.duration
|
|
207
|
-
if CMTimeCompare(trimmer.maximumDuration, asset.duration) == 1 {
|
|
213
|
+
if let maxDuration = maximumDuration {
|
|
214
|
+
trimmer.maximumDuration = CMTime(seconds: max(1, Double(maxDuration)), preferredTimescale: 600)
|
|
215
|
+
if trimmer.maximumDuration > asset.duration {
|
|
208
216
|
trimmer.maximumDuration = asset.duration
|
|
209
217
|
}
|
|
210
|
-
|
|
211
218
|
trimmer.selectedRange = CMTimeRange(start: .zero, end: trimmer.maximumDuration)
|
|
212
219
|
}
|
|
213
|
-
|
|
214
|
-
if
|
|
215
|
-
trimmer.minimumDuration = CMTime(seconds: max(1, Double(
|
|
220
|
+
|
|
221
|
+
if let minDuration = minimumDuration {
|
|
222
|
+
trimmer.minimumDuration = CMTime(seconds: max(1, Double(minDuration)), preferredTimescale: 600)
|
|
216
223
|
}
|
|
217
224
|
|
|
218
|
-
trimmer.addTarget(self, action: #selector(didBeginTrimming(_:)), for: VideoTrimmer.didBeginTrimming)
|
|
219
|
-
trimmer.addTarget(self, action: #selector(didEndTrimming(_:)), for: VideoTrimmer.didEndTrimming)
|
|
220
|
-
trimmer.addTarget(self, action: #selector(selectedRangeDidChanged(_:)), for: VideoTrimmer.selectedRangeChanged)
|
|
221
225
|
trimmer.addTarget(self, action: #selector(didBeginScrubbing(_:)), for: VideoTrimmer.didBeginScrubbing)
|
|
222
226
|
trimmer.addTarget(self, action: #selector(didEndScrubbing(_:)), for: VideoTrimmer.didEndScrubbing)
|
|
223
227
|
trimmer.addTarget(self, action: #selector(progressDidChanged(_:)), for: VideoTrimmer.progressChanged)
|
|
228
|
+
|
|
229
|
+
trimmer.addTarget(self, action: #selector(didBeginTrimmingFromStart(_:)), for: VideoTrimmer.didBeginTrimmingFromStart)
|
|
230
|
+
trimmer.addTarget(self, action: #selector(leadingGrabberChanged(_:)), for: VideoTrimmer.leadingGrabberChanged)
|
|
231
|
+
trimmer.addTarget(self, action: #selector(didEndTrimmingFromStart(_:)), for: VideoTrimmer.didEndTrimmingFromStart)
|
|
232
|
+
|
|
233
|
+
trimmer.addTarget(self, action: #selector(didBeginTrimmingFromEnd(_:)), for: VideoTrimmer.didBeginTrimmingFromEnd)
|
|
234
|
+
trimmer.addTarget(self, action: #selector(trailingGrabberChanged(_:)), for: VideoTrimmer.trailingGrabberChanged)
|
|
235
|
+
trimmer.addTarget(self, action: #selector(didEndTrimmingFromEnd(_:)), for: VideoTrimmer.didEndTrimmingFromEnd)
|
|
224
236
|
view.addSubview(trimmer)
|
|
225
237
|
trimmer.translatesAutoresizingMaskIntoConstraints = false
|
|
226
238
|
NSLayoutConstraint.activate([
|
|
227
239
|
trimmer.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
|
228
240
|
trimmer.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
|
229
241
|
trimmer.bottomAnchor.constraint(equalTo: timingStackView.topAnchor, constant: -16),
|
|
230
|
-
trimmer.heightAnchor.constraint(equalToConstant: 50)
|
|
242
|
+
trimmer.heightAnchor.constraint(equalToConstant: 50)
|
|
231
243
|
])
|
|
232
|
-
|
|
233
|
-
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private func setupPlayerController() {
|
|
247
|
+
playerController.showsPlaybackControls = false
|
|
234
248
|
if #available(iOS 16.0, *) {
|
|
235
|
-
playerController.allowsVideoFrameAnalysis = false
|
|
249
|
+
playerController.allowsVideoFrameAnalysis = false
|
|
236
250
|
}
|
|
237
251
|
playerController.player = AVPlayer()
|
|
238
|
-
|
|
252
|
+
player.replaceCurrentItem(with: AVPlayerItem(asset: asset))
|
|
253
|
+
|
|
254
|
+
try? AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [])
|
|
239
255
|
addChild(playerController)
|
|
240
256
|
view.addSubview(playerController.view)
|
|
241
257
|
playerController.view.translatesAutoresizingMaskIntoConstraints = false
|
|
@@ -246,55 +262,108 @@ class VideoTrimmerViewController: UIViewController {
|
|
|
246
262
|
playerController.view.bottomAnchor.constraint(equalTo: trimmer.topAnchor, constant: -16)
|
|
247
263
|
])
|
|
248
264
|
|
|
249
|
-
|
|
250
|
-
|
|
265
|
+
// Add observer for the end of playback
|
|
266
|
+
NotificationCenter.default.addObserver(self, selector: #selector(playerDidFinishPlaying), name: .AVPlayerItemDidPlayToEndTime, object: player.currentItem)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
@objc private func playerDidFinishPlaying(note: NSNotification) {
|
|
270
|
+
// Directly set the play icon
|
|
271
|
+
// the reason in at this time player.timeControlStatus == .playing still returns true
|
|
272
|
+
playBtn.setImage(self.playIcon, for: .normal)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private func setupTimeObserver() {
|
|
251
276
|
timeObserverToken = player.addPeriodicTimeObserver(forInterval: CMTime(value: 1, timescale: 30), queue: .main) { [weak self] time in
|
|
252
|
-
guard let self = self else {return}
|
|
253
|
-
|
|
254
|
-
currentTimeLabel.text = player.currentTime().displayString
|
|
277
|
+
guard let self = self else { return }
|
|
255
278
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
let finalTime = self.trimmer.trimmingState == .none ? CMTimeAdd(time, self.trimmer.selectedRange.start) : time
|
|
260
|
-
self.trimmer.progress = finalTime
|
|
279
|
+
if self.player.timeControlStatus != .playing {
|
|
280
|
+
return
|
|
281
|
+
}
|
|
261
282
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
283
|
+
self.trimmer.progress = time
|
|
284
|
+
|
|
285
|
+
// pause if reach end of selected range
|
|
286
|
+
if CMTimeCompare(self.trimmer.progress, trimmer.selectedRange.end) == 1 {
|
|
287
|
+
player.pause()
|
|
288
|
+
self.trimmer.progress = trimmer.selectedRange.end
|
|
289
|
+
player.seek(to: trimmer.selectedRange.end, toleranceBefore: .zero, toleranceAfter: .zero)
|
|
266
290
|
}
|
|
291
|
+
|
|
292
|
+
currentTimeLabel.text = trimmer.progress.displayString
|
|
293
|
+
|
|
294
|
+
self.setPlayBtnIcon()
|
|
267
295
|
}
|
|
268
|
-
|
|
269
|
-
updateLabels()
|
|
270
296
|
}
|
|
271
297
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
player.pause()
|
|
276
|
-
if timeObserverToken != nil {
|
|
277
|
-
player.removeTimeObserver(timeObserverToken as Any)
|
|
278
|
-
timeObserverToken = nil
|
|
279
|
-
}
|
|
280
|
-
playerController.player = nil
|
|
281
|
-
playerController.dismiss(animated: false, completion: nil)
|
|
298
|
+
private func setPlayBtnIcon() {
|
|
299
|
+
self.playBtn.setImage(self.player.timeControlStatus == .playing ? self.pauseIcon : self.playIcon, for: .normal)
|
|
282
300
|
}
|
|
283
301
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
302
|
+
// ====Smoother seek
|
|
303
|
+
public func seek(to time: CMTime) {
|
|
304
|
+
seekSmoothlyToTime(newChaseTime: time)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private func seekSmoothlyToTime(newChaseTime: CMTime) {
|
|
308
|
+
if CMTimeCompare(newChaseTime, chaseTime) != 0 {
|
|
309
|
+
chaseTime = newChaseTime
|
|
310
|
+
|
|
311
|
+
if !isSeekInProgress {
|
|
312
|
+
trySeekToChaseTime()
|
|
313
|
+
}
|
|
289
314
|
}
|
|
290
|
-
|
|
291
315
|
}
|
|
292
316
|
|
|
293
|
-
|
|
294
|
-
|
|
317
|
+
private func trySeekToChaseTime() {
|
|
318
|
+
guard player?.status == .readyToPlay else { return }
|
|
319
|
+
actuallySeekToTime()
|
|
295
320
|
}
|
|
296
321
|
|
|
297
|
-
|
|
298
|
-
|
|
322
|
+
private func actuallySeekToTime() {
|
|
323
|
+
isSeekInProgress = true
|
|
324
|
+
let seekTimeInProgress = chaseTime
|
|
325
|
+
|
|
326
|
+
player?.seek(to: seekTimeInProgress, toleranceBefore: .zero, toleranceAfter: .zero) { [weak self] _ in
|
|
327
|
+
guard let `self` = self else { return }
|
|
328
|
+
|
|
329
|
+
if CMTimeCompare(seekTimeInProgress, self.chaseTime) == 0 {
|
|
330
|
+
self.isSeekInProgress = false
|
|
331
|
+
} else {
|
|
332
|
+
self.trySeekToChaseTime()
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private extension UIButton {
|
|
339
|
+
static func createButton(title: String? = nil, image: UIImage? = nil, font: UIFont? = nil, titleColor: UIColor? = nil, tintColor: UIColor? = nil, target: Any?, action: Selector) -> UIButton {
|
|
340
|
+
let button = UIButton(type: .system)
|
|
341
|
+
if let title = title {
|
|
342
|
+
button.setTitle(title, for: .normal)
|
|
343
|
+
}
|
|
344
|
+
if let image = image {
|
|
345
|
+
button.setImage(image, for: .normal)
|
|
346
|
+
}
|
|
347
|
+
if let font = font {
|
|
348
|
+
button.titleLabel?.font = font
|
|
349
|
+
}
|
|
350
|
+
if let titleColor = titleColor {
|
|
351
|
+
button.setTitleColor(titleColor, for: .normal)
|
|
352
|
+
}
|
|
353
|
+
if let tintColor = tintColor {
|
|
354
|
+
button.tintColor = tintColor
|
|
355
|
+
}
|
|
356
|
+
button.addTarget(target, action: action, for: .touchUpInside)
|
|
357
|
+
return button
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private extension UILabel {
|
|
362
|
+
static func createLabel(textAlignment: NSTextAlignment, textColor: UIColor) -> UILabel {
|
|
363
|
+
let label = UILabel()
|
|
364
|
+
label.font = UIFont.preferredFont(forTextStyle: .caption1)
|
|
365
|
+
label.textAlignment = textAlignment
|
|
366
|
+
label.textColor = textColor
|
|
367
|
+
return label
|
|
299
368
|
}
|
|
300
369
|
}
|