@capgo/capacitor-native-audio 8.4.3
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/CapgoCapacitorNativeAudio.podspec +16 -0
- package/LICENSE +373 -0
- package/Package.swift +31 -0
- package/README.md +1229 -0
- package/android/build.gradle +89 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/ee/forgr/audio/AudioAsset.java +611 -0
- package/android/src/main/java/ee/forgr/audio/AudioCompletionListener.java +5 -0
- package/android/src/main/java/ee/forgr/audio/AudioDispatcher.java +208 -0
- package/android/src/main/java/ee/forgr/audio/Constant.java +36 -0
- package/android/src/main/java/ee/forgr/audio/HlsAvailabilityChecker.java +84 -0
- package/android/src/main/java/ee/forgr/audio/Logger.java +55 -0
- package/android/src/main/java/ee/forgr/audio/NativeAudio.java +2022 -0
- package/android/src/main/java/ee/forgr/audio/RemoteAudioAsset.java +886 -0
- package/android/src/main/java/ee/forgr/audio/StreamAudioAsset.java +708 -0
- package/android/src/main/res/values/colors.xml +3 -0
- package/android/src/main/res/values/strings.xml +3 -0
- package/android/src/main/res/values/styles.xml +3 -0
- package/dist/docs.json +1470 -0
- package/dist/esm/audio-asset.d.ts +4 -0
- package/dist/esm/audio-asset.js +6 -0
- package/dist/esm/audio-asset.js.map +1 -0
- package/dist/esm/definitions.d.ts +597 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +82 -0
- package/dist/esm/web.js +553 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +571 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +574 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/NativeAudioPlugin/AudioAsset+Fade.swift +157 -0
- package/ios/Sources/NativeAudioPlugin/AudioAsset.swift +403 -0
- package/ios/Sources/NativeAudioPlugin/Constant.swift +52 -0
- package/ios/Sources/NativeAudioPlugin/Logger.swift +43 -0
- package/ios/Sources/NativeAudioPlugin/Plugin.swift +1786 -0
- package/ios/Sources/NativeAudioPlugin/RemoteAudioAsset+Fade.swift +152 -0
- package/ios/Sources/NativeAudioPlugin/RemoteAudioAsset.swift +405 -0
- package/ios/Tests/NativeAudioPluginTests/PluginTests.swift +648 -0
- package/ios/Tests/README.md +39 -0
- package/package.json +101 -0
- package/scripts/configure-dependencies.js +251 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import AVFoundation
|
|
2
|
+
|
|
3
|
+
extension RemoteAudioAsset {
|
|
4
|
+
|
|
5
|
+
/// Pause after sampling elapsed/duration for Now Playing. Caller must be on the main queue.
|
|
6
|
+
fileprivate func performRemoteFadeOutPauseOnMain(player: AVPlayer, beforePause: ((TimeInterval, TimeInterval) -> Void)?) {
|
|
7
|
+
let elapsed = player.currentTime().seconds
|
|
8
|
+
let rawDuration = player.currentItem?.duration ?? .zero
|
|
9
|
+
let duration = rawDuration.isNumeric && rawDuration.isValid ? rawDuration.seconds : 0
|
|
10
|
+
beforePause?(elapsed, duration.isFinite ? duration : 0)
|
|
11
|
+
player.pause()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
fileprivate func scheduleRemoteFadeOutPauseOnMain(player: AVPlayer, beforePause: ((TimeInterval, TimeInterval) -> Void)?) {
|
|
15
|
+
DispatchQueue.main.async { [weak self] in
|
|
16
|
+
guard let self else { return }
|
|
17
|
+
self.performRemoteFadeOutPauseOnMain(player: player, beforePause: beforePause)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/// Pause, seek to start, then emit `complete` on the main queue after the seek finishes.
|
|
22
|
+
fileprivate func pauseSeekToStartThenDispatchComplete(on player: AVPlayer) {
|
|
23
|
+
player.pause()
|
|
24
|
+
player.seek(to: .zero) { [weak self] _ in
|
|
25
|
+
guard let self else { return }
|
|
26
|
+
DispatchQueue.main.async {
|
|
27
|
+
self.dispatchComplete()
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func fadeIn(player: AVPlayer, fadeInDuration: TimeInterval, targetVolume: Float) {
|
|
33
|
+
cancelFade()
|
|
34
|
+
let steps = Int(fadeInDuration / TimeInterval(fadeDelaySecs))
|
|
35
|
+
guard steps > 0 else { return }
|
|
36
|
+
let fadeStep = targetVolume / Float(steps)
|
|
37
|
+
var currentVolume: Float = 0
|
|
38
|
+
|
|
39
|
+
var task: DispatchWorkItem?
|
|
40
|
+
task = DispatchWorkItem { [weak self] in
|
|
41
|
+
guard let self else { return }
|
|
42
|
+
for _ in 0..<steps {
|
|
43
|
+
guard let task, !task.isCancelled, self.isPlaying(), player.timeControlStatus == .playing else { return }
|
|
44
|
+
currentVolume += fadeStep
|
|
45
|
+
DispatchQueue.main.async {
|
|
46
|
+
player.volume = min(currentVolume, targetVolume)
|
|
47
|
+
}
|
|
48
|
+
Thread.sleep(forTimeInterval: TimeInterval(self.fadeDelaySecs))
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
fadeTask = task
|
|
52
|
+
if let task {
|
|
53
|
+
fadeQueue.async(execute: task)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// - Parameter beforePause: Called on the main queue immediately before `pause()` when `toPause` is true.
|
|
58
|
+
func fadeOut(
|
|
59
|
+
player: AVPlayer,
|
|
60
|
+
fadeOutDuration: TimeInterval,
|
|
61
|
+
toPause: Bool = false,
|
|
62
|
+
beforePause: ((TimeInterval, TimeInterval) -> Void)? = nil
|
|
63
|
+
) {
|
|
64
|
+
cancelFade()
|
|
65
|
+
let steps = max(0, Int(fadeOutDuration / TimeInterval(fadeDelaySecs)))
|
|
66
|
+
guard steps > 0 else {
|
|
67
|
+
if toPause {
|
|
68
|
+
scheduleRemoteFadeOutPauseOnMain(player: player, beforePause: beforePause)
|
|
69
|
+
} else {
|
|
70
|
+
DispatchQueue.main.async { [weak self] in
|
|
71
|
+
guard let self else { return }
|
|
72
|
+
self.pauseSeekToStartThenDispatchComplete(on: player)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
let fadeStep = player.volume / Float(steps)
|
|
78
|
+
var currentVolume = player.volume
|
|
79
|
+
|
|
80
|
+
var task: DispatchWorkItem?
|
|
81
|
+
task = DispatchWorkItem { [weak self] in
|
|
82
|
+
guard let self else { return }
|
|
83
|
+
for _ in 0..<steps {
|
|
84
|
+
guard let task, !task.isCancelled else { return }
|
|
85
|
+
guard self.isPlaying(), player.timeControlStatus == .playing else { break }
|
|
86
|
+
currentVolume -= fadeStep
|
|
87
|
+
DispatchQueue.main.async {
|
|
88
|
+
player.volume = max(currentVolume, 0)
|
|
89
|
+
}
|
|
90
|
+
Thread.sleep(forTimeInterval: TimeInterval(self.fadeDelaySecs))
|
|
91
|
+
}
|
|
92
|
+
guard let task, !task.isCancelled else { return }
|
|
93
|
+
DispatchQueue.main.async { [weak self] in
|
|
94
|
+
guard let self else { return }
|
|
95
|
+
if toPause {
|
|
96
|
+
self.performRemoteFadeOutPauseOnMain(player: player, beforePause: beforePause)
|
|
97
|
+
} else {
|
|
98
|
+
self.pauseSeekToStartThenDispatchComplete(on: player)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
fadeTask = task
|
|
103
|
+
if let task {
|
|
104
|
+
fadeQueue.async(execute: task)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
func fadeTo(player: AVPlayer, fadeOutDuration: TimeInterval, targetVolume: Float) {
|
|
109
|
+
cancelFade()
|
|
110
|
+
let steps = Int(fadeOutDuration / TimeInterval(fadeDelaySecs))
|
|
111
|
+
guard steps > 0 else { return }
|
|
112
|
+
|
|
113
|
+
let minVolume = zeroVolume
|
|
114
|
+
var currentVolume: Float = max(player.volume, minVolume)
|
|
115
|
+
let safeTargetVolume: Float = max(targetVolume, minVolume)
|
|
116
|
+
let ratio = pow(safeTargetVolume / currentVolume, 1.0 / Float(steps))
|
|
117
|
+
|
|
118
|
+
var task: DispatchWorkItem?
|
|
119
|
+
task = DispatchWorkItem { [weak self] in
|
|
120
|
+
guard let self else { return }
|
|
121
|
+
for _ in 0..<steps {
|
|
122
|
+
guard let task, !task.isCancelled, self.isPlaying(), player.timeControlStatus == .playing else { return }
|
|
123
|
+
currentVolume *= ratio
|
|
124
|
+
DispatchQueue.main.async {
|
|
125
|
+
player.volume = min(max(currentVolume, minVolume), self.maxVolume)
|
|
126
|
+
}
|
|
127
|
+
Thread.sleep(forTimeInterval: TimeInterval(self.fadeDelaySecs))
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
fadeTask = task
|
|
131
|
+
if let task {
|
|
132
|
+
fadeQueue.async(execute: task)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
static func clearCache() {
|
|
137
|
+
DispatchQueue.global(qos: .background).sync {
|
|
138
|
+
let urls = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)
|
|
139
|
+
if let cachePath = urls.first {
|
|
140
|
+
do {
|
|
141
|
+
let fileURLs = try FileManager.default.contentsOfDirectory(at: cachePath, includingPropertiesForKeys: nil)
|
|
142
|
+
let audioExtensions = ["mp3", "wav", "aac", "m4a", "ogg", "mp4", "caf", "aiff"]
|
|
143
|
+
for fileURL in fileURLs where audioExtensions.contains(fileURL.pathExtension.lowercased()) {
|
|
144
|
+
try FileManager.default.removeItem(at: fileURL)
|
|
145
|
+
}
|
|
146
|
+
} catch {
|
|
147
|
+
staticLogger.error("Error clearing audio cache: %@", error.localizedDescription)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
@preconcurrency import AVFoundation
|
|
2
|
+
|
|
3
|
+
// swiftlint:disable file_length
|
|
4
|
+
// swiftlint:disable:next type_body_length
|
|
5
|
+
public class RemoteAudioAsset: AudioAsset {
|
|
6
|
+
var playerItems: [AVPlayerItem] = []
|
|
7
|
+
var players: [AVPlayer] = []
|
|
8
|
+
var playerObservers: [NSKeyValueObservation] = []
|
|
9
|
+
var notificationObservers: [NSObjectProtocol] = []
|
|
10
|
+
var duration: TimeInterval = 0
|
|
11
|
+
var asset: AVURLAsset?
|
|
12
|
+
private var logger = Logger(logTag: "RemoteAudioAsset")
|
|
13
|
+
static let staticLogger = Logger(logTag: "RemoteAudioAsset")
|
|
14
|
+
|
|
15
|
+
init(owner: NativeAudio, withAssetId assetId: String, withPath path: String, withChannels channels: Int?, withVolume volume: Float?, withHeaders headers: [String: String]?) {
|
|
16
|
+
super.init(owner: owner, withAssetId: assetId, withPath: path, withChannels: channels ?? 1, withVolume: volume ?? 1.0)
|
|
17
|
+
|
|
18
|
+
let setupBlock = { [weak self] in
|
|
19
|
+
guard let self else { return }
|
|
20
|
+
guard let url = URL(string: path) else {
|
|
21
|
+
self.logger.error("Invalid URL: %@", String(describing: path))
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
var options: [String: Any] = [AVURLAssetPreferPreciseDurationAndTimingKey: true]
|
|
26
|
+
if let headers, !headers.isEmpty {
|
|
27
|
+
options["AVURLAssetHTTPHeaderFieldsKey"] = headers
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let asset = AVURLAsset(url: url, options: options)
|
|
31
|
+
self.asset = asset
|
|
32
|
+
let channelCount = min(max(channels ?? Constant.DefaultChannels, 1), Constant.MaxChannels)
|
|
33
|
+
|
|
34
|
+
for _ in 0..<channelCount {
|
|
35
|
+
let playerItem = AVPlayerItem(asset: asset)
|
|
36
|
+
let player = AVPlayer(playerItem: playerItem)
|
|
37
|
+
player.volume = self.initialVolume
|
|
38
|
+
self.playerItems.append(playerItem)
|
|
39
|
+
self.players.append(player)
|
|
40
|
+
|
|
41
|
+
let durationObserver = playerItem.observe(\.status) { [weak self] item, _ in
|
|
42
|
+
guard let self else { return }
|
|
43
|
+
self.owner?.executeOnAudioQueue {
|
|
44
|
+
if item.status == .readyToPlay {
|
|
45
|
+
self.duration = item.duration.seconds
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
self.playerObservers.append(durationObserver)
|
|
50
|
+
|
|
51
|
+
let observer = player.observe(\.timeControlStatus) { [weak self, weak player] observedPlayer, _ in
|
|
52
|
+
guard let self, let player, player === observedPlayer else { return }
|
|
53
|
+
if player.timeControlStatus == .paused &&
|
|
54
|
+
(player.currentItem?.currentTime() == player.currentItem?.duration || player.currentItem?.duration == .zero) {
|
|
55
|
+
self.playerDidFinishPlaying(player: player)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
self.playerObservers.append(observer)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if owner.isRunningTests {
|
|
63
|
+
setupBlock()
|
|
64
|
+
} else {
|
|
65
|
+
owner.executeOnAudioQueue(setupBlock)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Backward-compatible initializer signature (delegates to primary init)
|
|
70
|
+
convenience init(owner: NativeAudio, withAssetId assetId: String, withPath path: String, withChannels channels: Int?, withVolume volume: Float?, withFadeDelay _: Float?, withHeaders headers: [String: String]?) {
|
|
71
|
+
self.init(owner: owner, withAssetId: assetId, withPath: path, withChannels: channels, withVolume: volume, withHeaders: headers)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
deinit {
|
|
75
|
+
for observer in playerObservers {
|
|
76
|
+
observer.invalidate()
|
|
77
|
+
}
|
|
78
|
+
cleanupNotificationObservers()
|
|
79
|
+
for player in players {
|
|
80
|
+
player.pause()
|
|
81
|
+
}
|
|
82
|
+
playerItems = []
|
|
83
|
+
players = []
|
|
84
|
+
playerObservers = []
|
|
85
|
+
cancelFade()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
func playerDidFinishPlaying(player: AVPlayer) {
|
|
89
|
+
owner?.executeOnAudioQueue { [weak self] in
|
|
90
|
+
guard let self else { return }
|
|
91
|
+
self.owner?.notifyListeners("complete", data: ["assetId": self.assetId])
|
|
92
|
+
self.dispatchedCompleteMap[self.assetId] = true
|
|
93
|
+
self.owner?.handlePlaybackCompletion(assetId: self.assetId, audioAsset: self)
|
|
94
|
+
self.onComplete?()
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
override func play(time: TimeInterval, volume: Float? = nil) {
|
|
99
|
+
owner?.executeOnAudioQueue { [weak self] in
|
|
100
|
+
guard let self else { return }
|
|
101
|
+
guard !players.isEmpty else { return }
|
|
102
|
+
if playIndex >= players.count {
|
|
103
|
+
playIndex = 0
|
|
104
|
+
}
|
|
105
|
+
cancelFade()
|
|
106
|
+
let player = players[playIndex]
|
|
107
|
+
player.seek(to: CMTimeMakeWithSeconds(max(time, 0), preferredTimescale: 1))
|
|
108
|
+
player.volume = volume ?? self.initialVolume
|
|
109
|
+
player.play()
|
|
110
|
+
playIndex = (playIndex + 1) % players.count
|
|
111
|
+
startCurrentTimeUpdates()
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Backward-compatible signature
|
|
116
|
+
override func play(time: TimeInterval, delay: TimeInterval) {
|
|
117
|
+
let validDelay = max(delay, 0)
|
|
118
|
+
if validDelay > 0 {
|
|
119
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + validDelay) { [weak self] in
|
|
120
|
+
self?.play(time: time, volume: nil)
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
play(time: time, volume: nil)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
override func pause() {
|
|
128
|
+
owner?.executeOnAudioQueue { [weak self] in
|
|
129
|
+
guard let self else { return }
|
|
130
|
+
guard !players.isEmpty && playIndex < players.count else { return }
|
|
131
|
+
cancelFade()
|
|
132
|
+
players[playIndex].pause()
|
|
133
|
+
stopCurrentTimeUpdates()
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// Timescale for seek targets; 600 is a common media default and avoids coarse rounding from timescale 1.
|
|
138
|
+
private static let seekPreferredTimescale: CMTimeScale = 600
|
|
139
|
+
|
|
140
|
+
override func setCurrentTime(time: TimeInterval, completion: (() -> Void)? = nil) {
|
|
141
|
+
guard let owner else {
|
|
142
|
+
completion?()
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
owner.executeOnAudioQueue { [weak self] in
|
|
146
|
+
guard let self else {
|
|
147
|
+
completion?()
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
guard !players.isEmpty && playIndex < players.count else {
|
|
151
|
+
completion?()
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
let player = players[playIndex]
|
|
155
|
+
let lowerBound = max(time, 0)
|
|
156
|
+
let validTime: TimeInterval
|
|
157
|
+
if let item = player.currentItem {
|
|
158
|
+
let itemDuration = item.duration
|
|
159
|
+
if itemDuration == .indefinite || !itemDuration.isValid {
|
|
160
|
+
validTime = lowerBound
|
|
161
|
+
} else {
|
|
162
|
+
let durationSeconds = itemDuration.seconds
|
|
163
|
+
if durationSeconds.isFinite && durationSeconds > 0 {
|
|
164
|
+
validTime = min(lowerBound, durationSeconds)
|
|
165
|
+
} else {
|
|
166
|
+
validTime = lowerBound
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
validTime = lowerBound
|
|
171
|
+
}
|
|
172
|
+
let target = CMTime(seconds: validTime, preferredTimescale: Self.seekPreferredTimescale)
|
|
173
|
+
player.seek(to: target, toleranceBefore: .zero, toleranceAfter: .zero) { finished in
|
|
174
|
+
guard finished else { return }
|
|
175
|
+
completion?()
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
override func resume() {
|
|
181
|
+
owner?.executeOnAudioQueue { [weak self] in
|
|
182
|
+
guard let self else { return }
|
|
183
|
+
guard !players.isEmpty && playIndex < players.count else { return }
|
|
184
|
+
|
|
185
|
+
let player = players[playIndex]
|
|
186
|
+
player.play()
|
|
187
|
+
cleanupNotificationObservers()
|
|
188
|
+
let observer = NotificationCenter.default.addObserver(
|
|
189
|
+
forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
|
|
190
|
+
object: player.currentItem,
|
|
191
|
+
queue: OperationQueue.main
|
|
192
|
+
) { [weak self, weak player] notification in
|
|
193
|
+
guard let self, let player else { return }
|
|
194
|
+
if let currentItem = notification.object as? AVPlayerItem, player.currentItem === currentItem {
|
|
195
|
+
self.playerDidFinishPlaying(player: player)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
notificationObservers.append(observer)
|
|
199
|
+
startCurrentTimeUpdates()
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
override func stop() {
|
|
204
|
+
owner?.executeOnAudioQueue { [weak self] in
|
|
205
|
+
guard let self else { return }
|
|
206
|
+
stopCurrentTimeUpdates()
|
|
207
|
+
cancelFade()
|
|
208
|
+
for player in players {
|
|
209
|
+
player.pause()
|
|
210
|
+
player.seek(to: .zero, completionHandler: { _ in
|
|
211
|
+
player.actionAtItemEnd = .pause
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
playIndex = 0
|
|
215
|
+
self.owner?.notifyListeners("complete", data: ["assetId": self.assetId as Any])
|
|
216
|
+
self.dispatchedCompleteMap[self.assetId] = true
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
override func loop() {
|
|
221
|
+
owner?.executeOnAudioQueue { [weak self] in
|
|
222
|
+
guard let self else { return }
|
|
223
|
+
cleanupNotificationObservers()
|
|
224
|
+
for (index, player) in players.enumerated() {
|
|
225
|
+
player.actionAtItemEnd = .none
|
|
226
|
+
guard let playerItem = player.currentItem else { continue }
|
|
227
|
+
let observer = NotificationCenter.default.addObserver(
|
|
228
|
+
forName: .AVPlayerItemDidPlayToEndTime,
|
|
229
|
+
object: playerItem,
|
|
230
|
+
queue: OperationQueue.main
|
|
231
|
+
) { [weak player] notification in
|
|
232
|
+
guard let player,
|
|
233
|
+
let item = notification.object as? AVPlayerItem,
|
|
234
|
+
player.currentItem === item else { return }
|
|
235
|
+
player.seek(to: .zero)
|
|
236
|
+
player.play()
|
|
237
|
+
}
|
|
238
|
+
notificationObservers.append(observer)
|
|
239
|
+
if index == playIndex {
|
|
240
|
+
player.seek(to: .zero)
|
|
241
|
+
player.play()
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
startCurrentTimeUpdates()
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
public func cleanupNotificationObservers() {
|
|
249
|
+
for observer in notificationObservers {
|
|
250
|
+
NotificationCenter.default.removeObserver(observer)
|
|
251
|
+
}
|
|
252
|
+
notificationObservers = []
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
override func unload() {
|
|
256
|
+
owner?.executeOnAudioQueue { [weak self] in
|
|
257
|
+
guard let self else { return }
|
|
258
|
+
cancelFade()
|
|
259
|
+
stopCurrentTimeUpdates()
|
|
260
|
+
stop()
|
|
261
|
+
cleanupNotificationObservers()
|
|
262
|
+
for observer in playerObservers {
|
|
263
|
+
observer.invalidate()
|
|
264
|
+
}
|
|
265
|
+
playerObservers = []
|
|
266
|
+
players = []
|
|
267
|
+
playerItems = []
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
override func setVolume(volume: NSNumber, fadeDuration: Double) {
|
|
272
|
+
owner?.executeOnAudioQueue { [weak self] in
|
|
273
|
+
guard let self else { return }
|
|
274
|
+
cancelFade()
|
|
275
|
+
let validVolume = min(max(volume.floatValue, Constant.MinVolume), Constant.MaxVolume)
|
|
276
|
+
for player in players {
|
|
277
|
+
if isPlaying() && fadeDuration > 0 {
|
|
278
|
+
fadeTo(player: player, fadeOutDuration: fadeDuration, targetVolume: validVolume)
|
|
279
|
+
} else {
|
|
280
|
+
player.volume = validVolume
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
override func setVolume(volume: NSNumber) {
|
|
287
|
+
setVolume(volume: volume, fadeDuration: 0)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
override func setRate(rate: NSNumber) {
|
|
291
|
+
owner?.executeOnAudioQueue { [weak self] in
|
|
292
|
+
guard let self else { return }
|
|
293
|
+
let validRate = min(max(rate.floatValue, Constant.MinRate), Constant.MaxRate)
|
|
294
|
+
for player in players {
|
|
295
|
+
player.rate = validRate
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
override func isPlaying() -> Bool {
|
|
301
|
+
var result = false
|
|
302
|
+
owner?.executeOnAudioQueue { [weak self] in
|
|
303
|
+
guard let self else { return }
|
|
304
|
+
guard !players.isEmpty && playIndex < players.count else {
|
|
305
|
+
result = false
|
|
306
|
+
return
|
|
307
|
+
}
|
|
308
|
+
result = players[playIndex].timeControlStatus == .playing
|
|
309
|
+
}
|
|
310
|
+
return result
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
override func shouldStopCurrentTimeUpdatesWhenNotPlaying() -> Bool {
|
|
314
|
+
var shouldStop = true
|
|
315
|
+
owner?.executeOnAudioQueue { [weak self] in
|
|
316
|
+
guard let self else { return }
|
|
317
|
+
guard !players.isEmpty && playIndex < players.count else {
|
|
318
|
+
shouldStop = true
|
|
319
|
+
return
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
let status = players[playIndex].timeControlStatus
|
|
323
|
+
shouldStop = status != .waitingToPlayAtSpecifiedRate
|
|
324
|
+
}
|
|
325
|
+
return shouldStop
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
override func getCurrentTime() -> TimeInterval {
|
|
329
|
+
var result: TimeInterval = 0
|
|
330
|
+
owner?.executeOnAudioQueue { [weak self] in
|
|
331
|
+
guard let self else { return }
|
|
332
|
+
guard !players.isEmpty && playIndex < players.count else { return }
|
|
333
|
+
result = players[playIndex].currentTime().seconds
|
|
334
|
+
}
|
|
335
|
+
return result
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
override func getDuration() -> TimeInterval {
|
|
339
|
+
var result: TimeInterval = 0
|
|
340
|
+
owner?.executeOnAudioQueue { [weak self] in
|
|
341
|
+
guard let self else { return }
|
|
342
|
+
guard !players.isEmpty && playIndex < players.count else { return }
|
|
343
|
+
let player = players[playIndex]
|
|
344
|
+
if player.currentItem?.duration == CMTime.indefinite {
|
|
345
|
+
result = 0
|
|
346
|
+
return
|
|
347
|
+
}
|
|
348
|
+
result = player.currentItem?.duration.seconds ?? 0
|
|
349
|
+
}
|
|
350
|
+
return result
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
override func playWithFade(time: TimeInterval, volume: Float?, fadeInDuration: TimeInterval) {
|
|
354
|
+
owner?.executeOnAudioQueue { [weak self] in
|
|
355
|
+
guard let self else { return }
|
|
356
|
+
guard !players.isEmpty && playIndex < players.count else { return }
|
|
357
|
+
let player = players[playIndex]
|
|
358
|
+
player.seek(to: CMTimeMakeWithSeconds(time, preferredTimescale: 1)) { [weak self] _ in
|
|
359
|
+
guard let self else { return }
|
|
360
|
+
DispatchQueue.main.async {
|
|
361
|
+
if player.timeControlStatus != .playing {
|
|
362
|
+
player.volume = 0
|
|
363
|
+
player.play()
|
|
364
|
+
self.fadeIn(player: player, fadeInDuration: fadeInDuration, targetVolume: volume ?? self.initialVolume)
|
|
365
|
+
self.playIndex = (self.playIndex + 1) % self.players.count
|
|
366
|
+
self.startCurrentTimeUpdates()
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
override func playWithFade(time: TimeInterval) {
|
|
374
|
+
playWithFade(time: time, volume: nil, fadeInDuration: TimeInterval(Constant.DefaultFadeDuration))
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
override func stopWithFade(fadeOutDuration: TimeInterval, toPause: Bool = false) {
|
|
378
|
+
owner?.executeOnAudioQueue { [weak self] in
|
|
379
|
+
guard let self else { return }
|
|
380
|
+
guard !players.isEmpty && playIndex < players.count else {
|
|
381
|
+
if !toPause { stop() }
|
|
382
|
+
return
|
|
383
|
+
}
|
|
384
|
+
let player = players[playIndex]
|
|
385
|
+
if player.timeControlStatus == .playing {
|
|
386
|
+
if toPause {
|
|
387
|
+
fadeOut(player: player, fadeOutDuration: fadeOutDuration, toPause: true) { [weak self] elapsed, duration in
|
|
388
|
+
guard let self, let owner = self.owner else { return }
|
|
389
|
+
owner.recordPausePositionAfterFade(assetId: self.assetId, elapsedTime: elapsed, duration: duration)
|
|
390
|
+
}
|
|
391
|
+
} else {
|
|
392
|
+
fadeOut(player: player, fadeOutDuration: fadeOutDuration, toPause: false)
|
|
393
|
+
}
|
|
394
|
+
} else if !toPause {
|
|
395
|
+
stop()
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
override func stopWithFade() {
|
|
401
|
+
stopWithFade(fadeOutDuration: TimeInterval(Constant.DefaultFadeDuration), toPause: false)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
}
|
|
405
|
+
// swiftlint:enable file_length
|