@javascriptcommon/react-native-track-player 4.1.26 → 4.1.28
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/android/src/main/java/com/lovegaoshi/kotlinaudio/player/AudioPlayer.kt +4 -0
- package/ios/Example/SwiftAudio/AudioController.swift +2 -2
- package/ios/Example/SwiftAudio/PlayerView.swift +18 -1
- package/ios/RNTrackPlayer/RNTrackPlayer.swift +17 -1
- package/ios/RNTrackPlayer/TrackPlayer.mm +2 -0
- package/ios/SwiftAudioEx/Sources/SwiftAudioEx/AudioPlayer.swift +32 -4
- package/ios/SwiftAudioEx/Sources/SwiftAudioEx/QueueManager.swift +14 -2
- package/ios/SwiftAudioEx/Sources/SwiftAudioEx/QueuedAudioPlayer.swift +112 -2
- package/lib/src/trackPlayer.js +0 -4
- package/package.json +1 -1
- package/src/trackPlayer.ts +0 -2
- /package/ios/SwiftAudioEx/Sources/SwiftAudioEx/{AudioTap.swift → Processors/AudioTap.swift} +0 -0
- /package/ios/SwiftAudioEx/Sources/SwiftAudioEx/{WaveformAudioTap.swift → Processors/WaveformAudioTap.swift} +0 -0
|
@@ -234,6 +234,10 @@ abstract class AudioPlayer internal constructor(
|
|
|
234
234
|
.setContentType(options.audioContentType)
|
|
235
235
|
.build()
|
|
236
236
|
mPlayer.setAudioAttributes(audioAttributes, options.handleAudioFocus)
|
|
237
|
+
// Prevent ExoPlayer from auto-advancing to the next queue item when a track ends.
|
|
238
|
+
// All track transitions are handled by the JS side via playNext() -> load().
|
|
239
|
+
// This avoids ExoPlayer trying to load queue items that don't have a URL yet.
|
|
240
|
+
mPlayer.pauseAtEndOfMediaItems = true
|
|
237
241
|
nameHolder[0] = mPlayer.toString()
|
|
238
242
|
// https://github.com/androidx/media/issues/2319
|
|
239
243
|
mPlayer.addAnalyticsListener(AudioFxInitListener())
|
|
@@ -26,7 +26,7 @@ class AudioController {
|
|
|
26
26
|
|
|
27
27
|
init() {
|
|
28
28
|
let controller = RemoteCommandController()
|
|
29
|
-
player = QueuedAudioPlayer(remoteCommandController: controller)
|
|
29
|
+
player = QueuedAudioPlayer(remoteCommandController: controller, crossfade: true)
|
|
30
30
|
player.remoteCommands = [
|
|
31
31
|
.stop,
|
|
32
32
|
.play,
|
|
@@ -36,7 +36,7 @@ class AudioController {
|
|
|
36
36
|
.previous,
|
|
37
37
|
.changePlaybackPosition
|
|
38
38
|
]
|
|
39
|
-
player.audioTap = WaveformAudioTap()
|
|
39
|
+
// player.audioTap = WaveformAudioTap()
|
|
40
40
|
player.repeatMode = .queue
|
|
41
41
|
DispatchQueue.main.async {
|
|
42
42
|
self.player.add(items: self.sources)
|
|
@@ -106,8 +106,25 @@ struct PlayerView: View {
|
|
|
106
106
|
})
|
|
107
107
|
.frame(maxWidth: .infinity)
|
|
108
108
|
}
|
|
109
|
-
.padding(.top,
|
|
109
|
+
.padding(.top, 20)
|
|
110
110
|
|
|
111
|
+
HStack {
|
|
112
|
+
Button(action: { controller.player.crossfadePrepare() }, label: {
|
|
113
|
+
Text("Crossfade Prepare")
|
|
114
|
+
.font(.system(size: 14))
|
|
115
|
+
})
|
|
116
|
+
.frame(maxWidth: .infinity)
|
|
117
|
+
|
|
118
|
+
.frame(maxWidth: .infinity)
|
|
119
|
+
Button(action: { controller.player.switchExoPlayer() }, label: {
|
|
120
|
+
Text("Crossfade Switch")
|
|
121
|
+
.font(.system(size: 14))
|
|
122
|
+
})
|
|
123
|
+
.frame(maxWidth: .infinity)
|
|
124
|
+
}
|
|
125
|
+
.padding(.top, 20)
|
|
126
|
+
|
|
127
|
+
|
|
111
128
|
VStack {
|
|
112
129
|
if viewModel.playbackState == .failed {
|
|
113
130
|
Text("Playback failed.")
|
|
@@ -33,7 +33,7 @@ public class RNTrackPlayer: NSObject, AudioSessionControllerDelegate {
|
|
|
33
33
|
@objc public weak var delegate: RNTPDelegate? = nil
|
|
34
34
|
// MARK: - Attributes
|
|
35
35
|
private var hasInitialized = false
|
|
36
|
-
private let player = QueuedAudioPlayer()
|
|
36
|
+
private let player = QueuedAudioPlayer(crossfade: true)
|
|
37
37
|
private let audioSessionController = AudioSessionController.shared
|
|
38
38
|
private let equalizerTap = EqualizerAudioTap() // Always created, attached at setup
|
|
39
39
|
private var shouldEmitProgressEvent: Bool = false
|
|
@@ -1067,6 +1067,22 @@ public class RNTrackPlayer: NSObject, AudioSessionControllerDelegate {
|
|
|
1067
1067
|
equalizerTap.balance = max(-1, min(1, balance))
|
|
1068
1068
|
resolve(NSNull())
|
|
1069
1069
|
}
|
|
1070
|
+
|
|
1071
|
+
// MARK: - Crossfade
|
|
1072
|
+
|
|
1073
|
+
@objc(crossFadePrepare:seekTo:resolver:rejecter:)
|
|
1074
|
+
public func crossFadePrepare(previous: Bool, seekTo: NSNumber, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
|
|
1075
|
+
if (rejectWhenNotInitialized(reject: reject)) { return }
|
|
1076
|
+
player.crossfadePrepare(previous: previous)
|
|
1077
|
+
resolve(NSNull())
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
@objc(switchExoPlayer:fadeInterval:fadeToVolume:waitUntil:resolver:rejecter:)
|
|
1081
|
+
public func switchExoPlayer(fadeDuration: Double, fadeInterval: Double, fadeToVolume: Double, waitUntil: NSNumber, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
|
|
1082
|
+
if (rejectWhenNotInitialized(reject: reject)) { return }
|
|
1083
|
+
player.switchExoPlayer(fadeDuration: Int(fadeDuration), fadeInterval: Int(fadeInterval), fadeToVolume: Float(fadeToVolume))
|
|
1084
|
+
resolve(NSNull())
|
|
1085
|
+
}
|
|
1070
1086
|
}
|
|
1071
1087
|
|
|
1072
1088
|
extension RNTrackPlayer {
|
|
@@ -205,6 +205,7 @@ RCT_EXPORT_MODULE()
|
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
- (void)crossFadePrepare:(BOOL)previous seekTo:(nonnull NSNumber *)seekTo resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
|
|
208
|
+
[trackPlayer crossFadePrepare:previous seekTo:seekTo resolver:resolve rejecter:reject];
|
|
208
209
|
}
|
|
209
210
|
|
|
210
211
|
- (void)fadeOutJump:(double)index duration:(double)duration interval:(double)interval toVolume:(double)toVolume resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
|
|
@@ -244,6 +245,7 @@ RCT_EXPORT_MODULE()
|
|
|
244
245
|
}
|
|
245
246
|
|
|
246
247
|
- (void)switchExoPlayer:(double)fadeDuration fadeInterval:(double)fadeInterval fadeToVolume:(double)fadeToVolume waitUntil:(nonnull NSNumber *)waitUntil resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
|
|
248
|
+
[trackPlayer switchExoPlayer:fadeDuration fadeInterval:fadeInterval fadeToVolume:fadeToVolume waitUntil:waitUntil resolver:resolve rejecter:reject];
|
|
247
249
|
}
|
|
248
250
|
|
|
249
251
|
// iOS Equalizer methods
|
|
@@ -12,8 +12,24 @@ public typealias AudioPlayerState = AVPlayerWrapperState
|
|
|
12
12
|
|
|
13
13
|
public class AudioPlayer: AVPlayerWrapperDelegate {
|
|
14
14
|
/// The wrapper around the underlying AVPlayer
|
|
15
|
-
let
|
|
16
|
-
|
|
15
|
+
let wrapper1: AVPlayerWrapperProtocol = AVPlayerWrapper()
|
|
16
|
+
var wrapper2: AVPlayerWrapperProtocol? = nil
|
|
17
|
+
// this is the current wrapper
|
|
18
|
+
var wrapper: AVPlayerWrapperProtocol
|
|
19
|
+
// this is the secondary/crossfade wrapper
|
|
20
|
+
var crossfadeWrapper: AVPlayerWrapperProtocol
|
|
21
|
+
// when true, the current wrapper is wrapper1; else, wrapper2
|
|
22
|
+
// for crossfade tracking
|
|
23
|
+
var currentAVPlayer = true
|
|
24
|
+
// indicates if crossfade is enabled/wrapper2 is initialized
|
|
25
|
+
var crossfade: Bool = false
|
|
26
|
+
|
|
27
|
+
func players () -> [AVPlayerWrapperProtocol] {
|
|
28
|
+
if (self.crossfade) {
|
|
29
|
+
return [wrapper1, wrapper2!]
|
|
30
|
+
}
|
|
31
|
+
return [wrapper1]
|
|
32
|
+
}
|
|
17
33
|
|
|
18
34
|
/**
|
|
19
35
|
Set an instance of AudioTap, to receive frame information and audio buffer access during playback.
|
|
@@ -29,6 +45,9 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
|
|
29
45
|
public let event = EventHolder()
|
|
30
46
|
|
|
31
47
|
private(set) var currentItem: AudioItem?
|
|
48
|
+
// as audioplayer does NOT hold queue information, the crossfading audioItem needs to be
|
|
49
|
+
// stored as such - whereas exoplayer has a built in queue and can just skipToIndex
|
|
50
|
+
var crossfadeItem: AudioItem?
|
|
32
51
|
|
|
33
52
|
/**
|
|
34
53
|
Set this to false to disable automatic updating of now playing info for control center and lock screen.
|
|
@@ -190,10 +209,19 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
|
|
190
209
|
- parameter infoCenter: The InfoCenter to update. Default is `MPNowPlayingInfoCenter.default()`.
|
|
191
210
|
*/
|
|
192
211
|
public init(nowPlayingInfoController: NowPlayingInfoControllerProtocol = NowPlayingInfoController(),
|
|
193
|
-
remoteCommandController: RemoteCommandController = RemoteCommandController()
|
|
212
|
+
remoteCommandController: RemoteCommandController = RemoteCommandController(),
|
|
213
|
+
crossfade: Bool = false
|
|
214
|
+
) {
|
|
194
215
|
self.nowPlayingInfoController = nowPlayingInfoController
|
|
195
216
|
self.remoteCommandController = remoteCommandController
|
|
196
|
-
|
|
217
|
+
self.wrapper = self.wrapper1
|
|
218
|
+
self.crossfade = crossfade
|
|
219
|
+
if (crossfade) {
|
|
220
|
+
self.wrapper2 = AVPlayerWrapper()
|
|
221
|
+
self.crossfadeWrapper = self.wrapper2!
|
|
222
|
+
} else {
|
|
223
|
+
self.crossfadeWrapper = self.wrapper
|
|
224
|
+
}
|
|
197
225
|
wrapper.delegate = self
|
|
198
226
|
self.remoteCommandController.audioPlayer = self
|
|
199
227
|
}
|
|
@@ -37,7 +37,7 @@ class QueueManager<Element> {
|
|
|
37
37
|
/**
|
|
38
38
|
The index of the current item. `-1` when there is no current item
|
|
39
39
|
*/
|
|
40
|
-
|
|
40
|
+
var currentIndex: Int {
|
|
41
41
|
get {
|
|
42
42
|
return synchronize {
|
|
43
43
|
return _currentIndex
|
|
@@ -352,5 +352,17 @@ class QueueManager<Element> {
|
|
|
352
352
|
delegate?.onCurrentItemChanged()
|
|
353
353
|
}
|
|
354
354
|
}
|
|
355
|
-
|
|
355
|
+
|
|
356
|
+
// peek for the next item. does not change currentIndex
|
|
357
|
+
public func peek(direction: Int = 1) -> Int {
|
|
358
|
+
if (items.isEmpty) {
|
|
359
|
+
return -1
|
|
360
|
+
}
|
|
361
|
+
var finalIndex = (currentIndex + direction) % items.count
|
|
362
|
+
if (finalIndex < 0) {
|
|
363
|
+
finalIndex += items.count
|
|
364
|
+
}
|
|
365
|
+
return finalIndex
|
|
366
|
+
}
|
|
367
|
+
|
|
356
368
|
}
|
|
@@ -15,9 +15,107 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
|
|
|
15
15
|
let queue: QueueManager = QueueManager<AudioItem>()
|
|
16
16
|
fileprivate var lastIndex: Int = -1
|
|
17
17
|
fileprivate var lastItem: AudioItem? = nil
|
|
18
|
+
|
|
19
|
+
func findOrInsert(item: AudioItem) -> Int {
|
|
20
|
+
var itemIndex = queue.items.firstIndex(where: {$0.getSourceUrl() == item.getSourceUrl()})
|
|
21
|
+
if (itemIndex == nil) {
|
|
22
|
+
add(item: item)
|
|
23
|
+
itemIndex = queue.items.count - 1
|
|
24
|
+
}
|
|
25
|
+
queue.currentIndex = itemIndex!
|
|
26
|
+
return itemIndex!
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public func crossfadePrepare(item: AudioItem) {
|
|
30
|
+
if (!self.crossfade) {
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
self.crossfadeWrapper.load(
|
|
34
|
+
from: item.getSourceUrl(),
|
|
35
|
+
type: item.getSourceType(),
|
|
36
|
+
playWhenReady: false,
|
|
37
|
+
initialTime: (item as? InitialTiming)?.getInitialTime(),
|
|
38
|
+
options:(item as? AssetOptionsProviding)?.getAssetOptions()
|
|
39
|
+
)
|
|
40
|
+
self.crossfadeItem = item
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public func crossfadePrepare(previous: Bool = false) {
|
|
44
|
+
let nextIndex = queue.peek(direction: previous ? -1 : 1)
|
|
45
|
+
if (nextIndex < 0) {
|
|
46
|
+
// TODO: should throw error instead
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
self.crossfadePrepare(item: queue.items[nextIndex])
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public func switchExoPlayer(
|
|
53
|
+
fadeDuration: Int = 2500,
|
|
54
|
+
fadeInterval: Int = 20,
|
|
55
|
+
fadeToVolume: Float = 1
|
|
56
|
+
) {
|
|
57
|
+
if (!self.crossfade || self.crossfadeItem == nil) {
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
if (self.currentAVPlayer) {
|
|
61
|
+
self.crossfadeWrapper = self.wrapper1
|
|
62
|
+
self.wrapper = self.wrapper2!
|
|
63
|
+
} else {
|
|
64
|
+
self.crossfadeWrapper = self.wrapper2!
|
|
65
|
+
self.wrapper = self.wrapper1
|
|
66
|
+
}
|
|
18
67
|
|
|
19
|
-
|
|
20
|
-
|
|
68
|
+
// switch the event emittting delegate
|
|
69
|
+
self.wrapper.delegate = self
|
|
70
|
+
self.crossfadeWrapper.delegate = nil
|
|
71
|
+
|
|
72
|
+
// broadcast nowplaying to system
|
|
73
|
+
self.findOrInsert(item: self.crossfadeItem!)
|
|
74
|
+
loadNowPlayingMetaValues()
|
|
75
|
+
emitCurrentItemEvent()
|
|
76
|
+
|
|
77
|
+
self.crossfadeItem = nil
|
|
78
|
+
self.currentAVPlayer = !self.currentAVPlayer
|
|
79
|
+
|
|
80
|
+
// fade volume
|
|
81
|
+
Task {
|
|
82
|
+
var fadeOutDuration = fadeDuration
|
|
83
|
+
let startFadeOutTime = DispatchTime.now()
|
|
84
|
+
let fadeFromVolume = self.crossfadeWrapper.volume
|
|
85
|
+
while (fadeOutDuration > 0) {
|
|
86
|
+
fadeOutDuration -= fadeInterval
|
|
87
|
+
let timeDiff = DispatchTime.now().uptimeNanoseconds - startFadeOutTime.uptimeNanoseconds
|
|
88
|
+
let timeElapsed = Float(min(Int(timeDiff) / 1_000_000, fadeDuration))
|
|
89
|
+
self.crossfadeWrapper.volume = fadeFromVolume * (1 - timeElapsed / Float(fadeDuration))
|
|
90
|
+
print("crossfade fading out...\(self.crossfadeWrapper.volume)")
|
|
91
|
+
try await Task.sleep(nanoseconds: UInt64(fadeInterval * 1000000))
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
Task {
|
|
96
|
+
self.wrapper.volume = 0
|
|
97
|
+
if (fadeToVolume > 0) {
|
|
98
|
+
self.wrapper.play()
|
|
99
|
+
var fadeInDuration = fadeDuration
|
|
100
|
+
let startTime = DispatchTime.now()
|
|
101
|
+
while (fadeInDuration > 0) {
|
|
102
|
+
fadeInDuration -= fadeInterval
|
|
103
|
+
let timeDiff = DispatchTime.now().uptimeNanoseconds - startTime.uptimeNanoseconds
|
|
104
|
+
let timeElapsed = Float(min(Int(timeDiff) / 1_000_000, fadeDuration))
|
|
105
|
+
self.wrapper.volume = fadeToVolume * timeElapsed / Float(fadeDuration)
|
|
106
|
+
print("crossfade fading in...\(self.wrapper.volume)")
|
|
107
|
+
try await Task.sleep(nanoseconds: UInt64(fadeInterval * 1000000))
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
public override init(
|
|
114
|
+
nowPlayingInfoController: NowPlayingInfoControllerProtocol = NowPlayingInfoController(),
|
|
115
|
+
remoteCommandController: RemoteCommandController = RemoteCommandController(),
|
|
116
|
+
crossfade: Bool = false
|
|
117
|
+
) {
|
|
118
|
+
super.init(nowPlayingInfoController: nowPlayingInfoController, remoteCommandController: remoteCommandController, crossfade: crossfade)
|
|
21
119
|
queue.delegate = self
|
|
22
120
|
}
|
|
23
121
|
|
|
@@ -224,6 +322,8 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
|
|
|
224
322
|
} else {
|
|
225
323
|
super.clear()
|
|
226
324
|
}
|
|
325
|
+
func emitCurrentItemEvent(lastPosition: Double = 0) {
|
|
326
|
+
let currentItem = currentItem
|
|
227
327
|
event.currentItem.emit(
|
|
228
328
|
data: (
|
|
229
329
|
item: currentItem,
|
|
@@ -248,6 +348,16 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
|
|
|
248
348
|
}
|
|
249
349
|
}
|
|
250
350
|
return false
|
|
351
|
+
// MARK: - QueueManagerDelegate
|
|
352
|
+
|
|
353
|
+
func onCurrentItemChanged() {
|
|
354
|
+
let lastPosition = currentTime;
|
|
355
|
+
if let currentItem = currentItem {
|
|
356
|
+
super.load(item: currentItem)
|
|
357
|
+
} else {
|
|
358
|
+
super.clear()
|
|
359
|
+
}
|
|
360
|
+
emitCurrentItemEvent(lastPosition: lastPosition)
|
|
251
361
|
}
|
|
252
362
|
|
|
253
363
|
func onSkippedToSameCurrentItem() {
|
package/lib/src/trackPlayer.js
CHANGED
|
@@ -573,8 +573,6 @@ export async function abandonWakeLock() {
|
|
|
573
573
|
* crossfade so the resource can be prepared.
|
|
574
574
|
*/
|
|
575
575
|
export async function crossFadePrepare(previous = false, seekTo = 0) {
|
|
576
|
-
if (!isAndroid)
|
|
577
|
-
return;
|
|
578
576
|
TrackPlayer.crossFadePrepare(previous, seekTo);
|
|
579
577
|
}
|
|
580
578
|
/**
|
|
@@ -587,8 +585,6 @@ export async function crossFadePrepare(previous = false, seekTo = 0) {
|
|
|
587
585
|
* waitUntil is in ms.
|
|
588
586
|
*/
|
|
589
587
|
export async function crossFade(fadeDuration = 2000, fadeInterval = 20, fadeToVolume = 1, waitUntil = 0) {
|
|
590
|
-
if (!isAndroid)
|
|
591
|
-
return;
|
|
592
588
|
TrackPlayer.switchExoPlayer(fadeDuration, fadeInterval, fadeToVolume, waitUntil);
|
|
593
589
|
}
|
|
594
590
|
/**
|
package/package.json
CHANGED
package/src/trackPlayer.ts
CHANGED
|
@@ -747,7 +747,6 @@ export async function abandonWakeLock() {
|
|
|
747
747
|
* crossfade so the resource can be prepared.
|
|
748
748
|
*/
|
|
749
749
|
export async function crossFadePrepare(previous = false, seekTo = 0) {
|
|
750
|
-
if (!isAndroid) return;
|
|
751
750
|
TrackPlayer.crossFadePrepare(previous, seekTo);
|
|
752
751
|
}
|
|
753
752
|
|
|
@@ -766,7 +765,6 @@ export async function crossFade(
|
|
|
766
765
|
fadeToVolume = 1,
|
|
767
766
|
waitUntil = 0,
|
|
768
767
|
) {
|
|
769
|
-
if (!isAndroid) return;
|
|
770
768
|
TrackPlayer.switchExoPlayer(
|
|
771
769
|
fadeDuration,
|
|
772
770
|
fadeInterval,
|
|
File without changes
|
|
File without changes
|