@javascriptcommon/react-native-track-player 4.1.12 → 4.1.14

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.
Files changed (28) hide show
  1. package/android/src/main/java/com/doublesymmetry/trackplayer/model/Track.kt +4 -1
  2. package/android/src/main/java/com/doublesymmetry/trackplayer/model/TrackAudioItem.kt +2 -1
  3. package/android/src/main/java/com/doublesymmetry/trackplayer/module/MusicEvents.kt +1 -0
  4. package/android/src/main/java/com/doublesymmetry/trackplayer/module/MusicModule.kt +13 -0
  5. package/android/src/main/java/com/doublesymmetry/trackplayer/service/MusicService.kt +31 -0
  6. package/android/src/main/java/com/lovegaoshi/kotlinaudio/player/QueuedAudioPlayer.kt +24 -0
  7. package/ios/RNTrackPlayer/Models/Track.swift +7 -0
  8. package/ios/RNTrackPlayer/RNTrackPlayer.swift +54 -36
  9. package/ios/RNTrackPlayer/TrackPlayer.mm +39 -1
  10. package/ios/RNTrackPlayer/Utils/EventType.swift +1 -0
  11. package/ios/SwiftAudioEx/Sources/SwiftAudioEx/Event.swift +11 -0
  12. package/ios/SwiftAudioEx/Sources/SwiftAudioEx/QueuedAudioPlayer.swift +27 -1
  13. package/lib/specs/NativeTrackPlayer.d.ts +1 -0
  14. package/lib/src/constants/Event.d.ts +6 -1
  15. package/lib/src/constants/Event.js +5 -0
  16. package/lib/src/hooks/useTrackPlayerEvents.d.ts +2 -3
  17. package/lib/src/hooks/useTrackPlayerEvents.js +0 -1
  18. package/lib/src/interfaces/Track.d.ts +5 -0
  19. package/lib/src/interfaces/events/EventPayloadByEvent.d.ts +1 -0
  20. package/lib/src/trackPlayer.d.ts +11 -2
  21. package/lib/src/trackPlayer.js +11 -0
  22. package/package.json +1 -1
  23. package/specs/NativeTrackPlayer.ts +1 -0
  24. package/src/constants/Event.ts +5 -0
  25. package/src/hooks/useTrackPlayerEvents.ts +2 -3
  26. package/src/interfaces/Track.ts +5 -0
  27. package/src/interfaces/events/EventPayloadByEvent.ts +1 -0
  28. package/src/trackPlayer.ts +16 -1
@@ -24,15 +24,17 @@ class Track
24
24
  var originalItem: Bundle?
25
25
  var headers: HashMap<String, String>? = null
26
26
  val queueId: Long
27
+ var notPlayable: Boolean = false
27
28
 
28
29
  override fun setMetadata(context: Context, bundle: Bundle?, ratingType: Int) {
29
30
  super.setMetadata(context, bundle, ratingType)
30
31
  if (originalItem != null && originalItem != bundle) originalItem!!.putAll(bundle)
32
+ bundle?.getBoolean("notPlayable", false)?.let { notPlayable = it }
31
33
  }
32
34
 
33
35
  fun toAudioItem(): TrackAudioItem {
34
36
  return TrackAudioItem(this, type, uri.toString(), artist, title, album, artwork.toString(), duration,
35
- AudioItemOptions(headers, userAgent, resourceId), mediaId)
37
+ AudioItemOptions(headers, userAgent, resourceId), mediaId, notPlayable)
36
38
  }
37
39
 
38
40
  init {
@@ -60,6 +62,7 @@ class Track
60
62
  headers!![header] = httpHeaders.getString(header)!!
61
63
  }
62
64
  }
65
+ notPlayable = bundle.getBoolean("notPlayable", false)
63
66
  setMetadata(context, bundle, ratingType)
64
67
  queueId = System.currentTimeMillis()
65
68
  originalItem = bundle
@@ -14,5 +14,6 @@ data class TrackAudioItem(
14
14
  override val artwork: String? = null,
15
15
  override val duration: Long? = null,
16
16
  override val options: AudioItemOptions? = null,
17
- override val mediaId: String? = null
17
+ override val mediaId: String? = null,
18
+ val notPlayable: Boolean = false
18
19
  ): AudioItem
@@ -48,6 +48,7 @@ class MusicEvents(private val reactContext: ReactContext) : BroadcastReceiver()
48
48
  const val PLAYBACK_PROGRESS_UPDATED = "playback-progress-updated"
49
49
  const val PLAYBACK_ERROR = "playback-error"
50
50
  const val PLAYBACK_ANIMATED_VOLUME_CHANGED = "playback-animated-volume-changed"
51
+ const val PLAYBACK_NOT_PLAYABLE_TRACK_ACTIVE = "playback-not-playable-track-active"
51
52
  const val PLAYBACK_RESUME = "playback-resume-android"
52
53
  const val FFT_UPDATED = "fft-updated"
53
54
 
@@ -485,6 +485,19 @@ class MusicModule(reactContext: ReactApplicationContext) : NativeTrackPlayerSpec
485
485
  callback.resolve(null)
486
486
  }
487
487
 
488
+ override fun setTrackPlayable(trackIndex: Double, playable: Boolean, callback: Promise) = launchInScope {
489
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
490
+
491
+ val index = trackIndex.toInt()
492
+ if (index < 0 || index >= musicService.tracks.size) {
493
+ callback.reject("index_out_of_bounds", "The track index is out of bounds")
494
+ return@launchInScope
495
+ }
496
+
497
+ musicService.setTrackPlayable(index, playable)
498
+ callback.resolve(null)
499
+ }
500
+
488
501
  override fun removeUpcomingTracks(callback: Promise) = launchInScope {
489
502
  if (verifyServiceBoundOrReject(callback)) return@launchInScope
490
503
 
@@ -357,6 +357,16 @@ class MusicService : HeadlessJsMediaService() {
357
357
  putDoubleArray("data", v)
358
358
 
359
359
  })}
360
+ // Set up callback for notPlayable tracks
361
+ player.onNotPlayableTrackActive = { index, item ->
362
+ val bundle = Bundle().apply {
363
+ putInt("index", index)
364
+ if (item is TrackAudioItem) {
365
+ putBundle("track", item.track.originalItem)
366
+ }
367
+ }
368
+ emit(MusicEvents.PLAYBACK_NOT_PLAYABLE_TRACK_ACTIVE, bundle)
369
+ }
360
370
  fakePlayer.release()
361
371
  mediaSession.player = player.player
362
372
  observeEvents()
@@ -713,6 +723,27 @@ class MusicService : HeadlessJsMediaService() {
713
723
  updateMetadataForTrack(player.currentIndex, track)
714
724
  }
715
725
 
726
+ @MainThread
727
+ fun setTrackPlayable(index: Int, playable: Boolean) {
728
+ val track = tracks.getOrNull(index) ?: return
729
+ val wasNotPlayable = track.notPlayable
730
+ track.notPlayable = !playable
731
+ player.replaceItem(index, track.toAudioItem())
732
+
733
+ // If current track: notPlayable -> playable, load it
734
+ if (wasNotPlayable && playable && player.currentIndex == index) {
735
+ player.load(track.toAudioItem())
736
+ }
737
+ // If current track: playable -> notPlayable, stop and emit event
738
+ else if (!wasNotPlayable && !playable && player.currentIndex == index) {
739
+ player.stop()
740
+ emit(MusicEvents.PLAYBACK_NOT_PLAYABLE_TRACK_ACTIVE, Bundle().apply {
741
+ putInt("index", index)
742
+ putBundle("track", track.originalItem)
743
+ })
744
+ }
745
+ }
746
+
716
747
  @MainThread
717
748
  fun clearNotificationMetadata() {
718
749
  }
@@ -8,6 +8,7 @@ import androidx.media3.common.MediaItem
8
8
  import androidx.media3.common.Player
9
9
  import androidx.media3.common.util.UnstableApi
10
10
  import com.lovegaoshi.kotlinaudio.models.*
11
+ import com.doublesymmetry.trackplayer.model.TrackAudioItem
11
12
  import java.util.*
12
13
  import kotlin.math.max
13
14
  import kotlin.math.min
@@ -19,6 +20,12 @@ class QueuedAudioPlayer(
19
20
 
20
21
  var parseEmbeddedArtwork: Boolean = false
21
22
 
23
+ /**
24
+ * Callback invoked when a track with notPlayable=true becomes current.
25
+ * The player will stay on this track but won't load or play it.
26
+ */
27
+ var onNotPlayableTrackActive: ((index: Int, item: AudioItem) -> Unit)? = null
28
+
22
29
  private val queue = LinkedList<MediaItem>()
23
30
 
24
31
  private fun parseAudioItem(audioItem: AudioItem): MediaItem {
@@ -91,11 +98,21 @@ class QueuedAudioPlayer(
91
98
  get() = items.getOrNull(currentIndex - 1)
92
99
 
93
100
  override fun load(item: AudioItem, playWhenReady: Boolean) {
101
+ // Check if item is notPlayable
102
+ if (item is TrackAudioItem && item.notPlayable) {
103
+ onNotPlayableTrackActive?.invoke(currentIndex, item)
104
+ return
105
+ }
94
106
  load(item)
95
107
  exoPlayer.playWhenReady = playWhenReady
96
108
  }
97
109
 
98
110
  override fun load(item: AudioItem) {
111
+ // Check if item is notPlayable
112
+ if (item is TrackAudioItem && item.notPlayable) {
113
+ onNotPlayableTrackActive?.invoke(currentIndex, item)
114
+ return
115
+ }
99
116
  if (queue.isEmpty()) {
100
117
  add(item)
101
118
  } else {
@@ -230,6 +247,13 @@ class QueuedAudioPlayer(
230
247
  */
231
248
  fun jumpToItem(index: Int) {
232
249
  try {
250
+ // Check if the target item is notPlayable
251
+ val item = items.getOrNull(index)
252
+ if (item is TrackAudioItem && item.notPlayable) {
253
+ exoPlayer.seekTo(index, C.TIME_UNSET)
254
+ onNotPlayableTrackActive?.invoke(index, item)
255
+ return
256
+ }
233
257
  exoPlayer.seekTo(index, C.TIME_UNSET)
234
258
  exoPlayer.prepare()
235
259
  } catch (e: IllegalSeekPositionException) {
@@ -29,6 +29,9 @@ class Track: AudioItem, TimePitching, AssetOptionsProviding {
29
29
  var album: String?
30
30
  var artwork: MPMediaItemArtwork?
31
31
 
32
+ /// When true, the track won't load or play when it becomes current.
33
+ var notPlayable: Bool = false
34
+
32
35
  private var originalObject: [String: Any] = [:]
33
36
 
34
37
  init?(dictionary: [String: Any]) {
@@ -37,6 +40,7 @@ class Track: AudioItem, TimePitching, AssetOptionsProviding {
37
40
  self.headers = dictionary["headers"] as? [String: Any]
38
41
  self.userAgent = dictionary["userAgent"] as? String
39
42
  self.pitchAlgorithm = dictionary["pitchAlgorithm"] as? String
43
+ self.notPlayable = dictionary["notPlayable"] as? Bool ?? false
40
44
 
41
45
  updateMetadata(dictionary: dictionary);
42
46
  }
@@ -58,6 +62,9 @@ class Track: AudioItem, TimePitching, AssetOptionsProviding {
58
62
  self.duration = dictionary["duration"] as? Double
59
63
  self.artworkURL = MediaURL(object: dictionary["artwork"])
60
64
  self.isLiveStream = dictionary["isLiveStream"] as? Bool
65
+ if let notPlayable = dictionary["notPlayable"] as? Bool {
66
+ self.notPlayable = notPlayable
67
+ }
61
68
 
62
69
  self.originalObject = self.originalObject.merging(dictionary) { (_, new) in new }
63
70
  }
@@ -35,7 +35,7 @@ public class RNTrackPlayer: NSObject, AudioSessionControllerDelegate {
35
35
  private var hasInitialized = false
36
36
  private let player = QueuedAudioPlayer()
37
37
  private let audioSessionController = AudioSessionController.shared
38
- private var equalizerTap: EqualizerAudioTap?
38
+ private let equalizerTap = EqualizerAudioTap() // Always created, attached at setup
39
39
  private var shouldEmitProgressEvent: Bool = false
40
40
  private var shouldResumePlaybackAfterInterruptionEnds: Bool = false
41
41
  private var forwardJumpInterval: NSNumber? = nil;
@@ -59,6 +59,7 @@ public class RNTrackPlayer: NSObject, AudioSessionControllerDelegate {
59
59
  player.event.currentItem.addListener(self, handleAudioPlayerCurrentItemChange)
60
60
  player.event.secondElapse.addListener(self, handleAudioPlayerSecondElapse)
61
61
  player.event.playWhenReadyChange.addListener(self, handlePlayWhenReadyChange)
62
+ player.event.notPlayableTrackActive.addListener(self, handleNotPlayableTrackActive)
62
63
 
63
64
  // Store global reference to the underlying AVPlayer so that other native views can reuse it.
64
65
  if let wrapper = player.wrapper as? AVPlayerWrapper {
@@ -135,11 +136,12 @@ public class RNTrackPlayer: NSObject, AudioSessionControllerDelegate {
135
136
  return
136
137
  }
137
138
 
138
- // configure the FFT audio tap
139
+ // configure the FFT audio tap if specified
139
140
  if let fftLength = config["useFFTProcessor"] as? Int {
140
141
  player.audioTap = WaveformAudioTap(mFFTLength: fftLength, mEmit: {data in
141
142
  self.emit(event:EventType.FFTUpdated, body:data)})
142
143
  }
144
+ // Note: Equalizer tap is NOT attached by default. Call setEqualizerEnabled(true) to enable.
143
145
 
144
146
  // configure buffer size
145
147
  if let bufferDuration = config["minBuffer"] as? TimeInterval {
@@ -897,23 +899,56 @@ public class RNTrackPlayer: NSObject, AudioSessionControllerDelegate {
897
899
  )
898
900
  }
899
901
 
902
+ func handleNotPlayableTrackActive(item: AudioItem?, index: Int?) {
903
+ var body: Dictionary<String, Any> = [:]
904
+ if let index = index {
905
+ body["index"] = index
906
+ }
907
+ if let track = (item as? Track)?.toObject() {
908
+ body["track"] = track
909
+ }
910
+ emit(event: EventType.PlaybackNotPlayableTrackActive, body: body)
911
+ }
912
+
913
+ // MARK: - NotPlayable Track Control
914
+
915
+ @objc(setTrackPlayable:playable:resolver:rejecter:)
916
+ public func setTrackPlayable(
917
+ trackIndex: Int,
918
+ playable: Bool,
919
+ resolve: RCTPromiseResolveBlock,
920
+ reject: RCTPromiseRejectBlock
921
+ ) {
922
+ if (rejectWhenNotInitialized(reject: reject)) { return }
923
+ if (rejectWhenTrackIndexOutOfBounds(index: trackIndex, reject: reject)) { return }
924
+
925
+ let track: Track = player.items[trackIndex] as! Track
926
+ let wasNotPlayable = track.notPlayable
927
+ track.notPlayable = !playable
928
+
929
+ // If current track: notPlayable -> playable, load it
930
+ if wasNotPlayable && playable && player.currentIndex == trackIndex {
931
+ player.load(item: track)
932
+ }
933
+ // If current track: playable -> notPlayable, unload it
934
+ else if !wasNotPlayable && !playable && player.currentIndex == trackIndex {
935
+ player.wrapper.unload()
936
+ player.wrapper.state = .idle
937
+ handleNotPlayableTrackActive(item: track, index: trackIndex)
938
+ }
939
+
940
+ resolve(NSNull())
941
+ }
942
+
900
943
  // MARK: - iOS Equalizer Methods
901
944
 
902
945
  @objc(setEqualizerEnabled:resolver:rejecter:)
903
946
  public func setEqualizerEnabled(enabled: Bool, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
904
947
  if (rejectWhenNotInitialized(reject: reject)) { return }
905
948
 
906
- if enabled {
907
- if equalizerTap == nil {
908
- equalizerTap = EqualizerAudioTap()
909
- }
910
- equalizerTap?.isEnabled = true
911
- player.audioTap = equalizerTap
912
- } else {
913
- equalizerTap?.isEnabled = false
914
- player.audioTap = nil
915
- }
916
-
949
+ // Toggle the enabled flag
950
+ // Note: For the equalizer to work, the tap must be attached to the player first
951
+ equalizerTap.isEnabled = enabled
917
952
  resolve(NSNull())
918
953
  }
919
954
 
@@ -921,20 +956,14 @@ public class RNTrackPlayer: NSObject, AudioSessionControllerDelegate {
921
956
  public func getEqualizerEnabled(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
922
957
  if (rejectWhenNotInitialized(reject: reject)) { return }
923
958
 
924
- let enabled = equalizerTap?.isEnabled ?? false
925
- resolve(enabled)
959
+ resolve(equalizerTap.isEnabled)
926
960
  }
927
961
 
928
962
  @objc(setEqualizerBand:gain:resolver:rejecter:)
929
963
  public func setEqualizerBand(band: Int, gain: Float, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
930
964
  if (rejectWhenNotInitialized(reject: reject)) { return }
931
965
 
932
- if equalizerTap == nil {
933
- equalizerTap = EqualizerAudioTap()
934
- player.audioTap = equalizerTap
935
- }
936
-
937
- equalizerTap?.setGain(band: band, gainDB: gain)
966
+ equalizerTap.setGain(band: band, gainDB: gain)
938
967
  resolve(NSNull())
939
968
  }
940
969
 
@@ -942,13 +971,8 @@ public class RNTrackPlayer: NSObject, AudioSessionControllerDelegate {
942
971
  public func setEqualizerBands(gains: [NSNumber], resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
943
972
  if (rejectWhenNotInitialized(reject: reject)) { return }
944
973
 
945
- if equalizerTap == nil {
946
- equalizerTap = EqualizerAudioTap()
947
- player.audioTap = equalizerTap
948
- }
949
-
950
974
  let floatGains = gains.map { $0.floatValue }
951
- equalizerTap?.setAllGains(floatGains)
975
+ equalizerTap.setAllGains(floatGains)
952
976
  resolve(NSNull())
953
977
  }
954
978
 
@@ -956,8 +980,7 @@ public class RNTrackPlayer: NSObject, AudioSessionControllerDelegate {
956
980
  public func getEqualizerBands(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
957
981
  if (rejectWhenNotInitialized(reject: reject)) { return }
958
982
 
959
- let gains = equalizerTap?.getAllGains() ?? Array(repeating: Float(0), count: 10)
960
- resolve(gains)
983
+ resolve(equalizerTap.getAllGains())
961
984
  }
962
985
 
963
986
  @objc(getEqualizerFrequencies:rejecter:)
@@ -975,13 +998,8 @@ public class RNTrackPlayer: NSObject, AudioSessionControllerDelegate {
975
998
  return
976
999
  }
977
1000
 
978
- if equalizerTap == nil {
979
- equalizerTap = EqualizerAudioTap()
980
- player.audioTap = equalizerTap
981
- }
982
-
983
1001
  let preset = presets[presetIndex]
984
- equalizerTap?.applyPreset(preset)
1002
+ equalizerTap.applyPreset(preset)
985
1003
  resolve(NSNull())
986
1004
  }
987
1005
 
@@ -994,7 +1012,7 @@ public class RNTrackPlayer: NSObject, AudioSessionControllerDelegate {
994
1012
  public func resetEqualizer(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
995
1013
  if (rejectWhenNotInitialized(reject: reject)) { return }
996
1014
 
997
- equalizerTap?.resetGains()
1015
+ equalizerTap.resetGains()
998
1016
  resolve(NSNull())
999
1017
  }
1000
1018
  }
@@ -243,7 +243,45 @@ RCT_EXPORT_MODULE()
243
243
  - (void)setPlaybackState:(nonnull NSString *)mediaID resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
244
244
  }
245
245
 
246
- - (void)switchExoPlayer:(double)fadeDuration fadeInterval:(double)fadeInterval fadeToVolume:(double)fadeToVolume waitUntil:(nonnull NSNumber *)waitUntil resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
246
+ - (void)switchExoPlayer:(double)fadeDuration fadeInterval:(double)fadeInterval fadeToVolume:(double)fadeToVolume waitUntil:(nonnull NSNumber *)waitUntil resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
247
+ }
248
+
249
+ // iOS Equalizer methods
250
+
251
+ - (void)setEqualizerEnabled:(BOOL)enabled resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
252
+ [trackPlayer setEqualizerEnabled:enabled resolver:resolve rejecter:reject];
253
+ }
254
+
255
+ - (void)getEqualizerEnabled:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
256
+ [trackPlayer getEqualizerEnabled:resolve rejecter:reject];
257
+ }
258
+
259
+ - (void)setEqualizerBand:(double)band gain:(double)gain resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
260
+ [trackPlayer setEqualizerBand:(NSInteger)band gain:(float)gain resolver:resolve rejecter:reject];
261
+ }
262
+
263
+ - (void)setEqualizerBands:(nonnull NSArray *)gains resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
264
+ [trackPlayer setEqualizerBands:gains resolver:resolve rejecter:reject];
265
+ }
266
+
267
+ - (void)getEqualizerBands:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
268
+ [trackPlayer getEqualizerBands:resolve rejecter:reject];
269
+ }
270
+
271
+ - (void)getEqualizerFrequencies:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
272
+ [trackPlayer getEqualizerFrequencies:resolve rejecter:reject];
273
+ }
274
+
275
+ - (void)applyEqualizerPreset:(double)presetIndex resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
276
+ [trackPlayer applyEqualizerPreset:(NSInteger)presetIndex resolver:resolve rejecter:reject];
277
+ }
278
+
279
+ - (void)getEqualizerPresetNames:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
280
+ [trackPlayer getEqualizerPresetNames:resolve rejecter:reject];
281
+ }
282
+
283
+ - (void)resetEqualizer:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
284
+ [trackPlayer resetEqualizer:resolve rejecter:reject];
247
285
  }
248
286
 
249
287
  @end
@@ -38,6 +38,7 @@ enum EventType: String, CaseIterable {
38
38
  case ControllerConnected = "android-controller-connected"
39
39
  case ControllerDisconnected = "android-controller-disconnected"
40
40
  case PlaybackAnimatedVolumeChanged = "playback-animated-volume-changed"
41
+ case PlaybackNotPlayableTrackActive = "playback-not-playable-track-active"
41
42
 
42
43
  static func allRawValues() -> [String] {
43
44
  return allCases.map { $0.rawValue }
@@ -27,6 +27,10 @@ extension AudioPlayer {
27
27
  lastIndex: Int?,
28
28
  lastPosition: Double?
29
29
  )
30
+ public typealias NotPlayableTrackActiveEventData = (
31
+ item: AudioItem?,
32
+ index: Int?
33
+ )
30
34
 
31
35
  public struct EventHolder {
32
36
 
@@ -104,6 +108,13 @@ extension AudioPlayer {
104
108
  - Note: It is only fired for instances of a QueuedAudioPlayer.
105
109
  */
106
110
  public let currentItem: AudioPlayer.Event<CurrentItemEventData> = AudioPlayer.Event()
111
+
112
+ /**
113
+ Emitted when a track with notPlayable=true becomes the active track.
114
+ The track stays current but won't load or play.
115
+ - Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
116
+ */
117
+ public let notPlayableTrackActive: AudioPlayer.Event<NotPlayableTrackActiveEventData> = AudioPlayer.Event()
107
118
  }
108
119
 
109
120
  public typealias EventClosure<EventData> = (EventData) -> Void
@@ -207,7 +207,20 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
207
207
  func onCurrentItemChanged() {
208
208
  let lastPosition = currentTime;
209
209
  if let currentItem = currentItem {
210
- super.load(item: currentItem)
210
+ // Check if the track is marked as notPlayable
211
+ if isTrackNotPlayable(currentItem) {
212
+ // Don't load the item, emit notPlayableTrackActive event
213
+ event.notPlayableTrackActive.emit(
214
+ data: (
215
+ item: currentItem,
216
+ index: currentIndex == -1 ? nil : currentIndex
217
+ )
218
+ )
219
+ // Set player to idle state without loading
220
+ wrapper.state = .idle
221
+ } else {
222
+ super.load(item: currentItem)
223
+ }
211
224
  } else {
212
225
  super.clear()
213
226
  }
@@ -224,6 +237,19 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
224
237
  lastIndex = currentIndex
225
238
  }
226
239
 
240
+ /// Check if an AudioItem is marked as notPlayable.
241
+ /// This method checks if the item has a notPlayable property set to true.
242
+ private func isTrackNotPlayable(_ item: AudioItem) -> Bool {
243
+ // Use reflection to check for notPlayable property
244
+ let mirror = Mirror(reflecting: item)
245
+ for child in mirror.children {
246
+ if child.label == "notPlayable", let value = child.value as? Bool {
247
+ return value
248
+ }
249
+ }
250
+ return false
251
+ }
252
+
227
253
  func onSkippedToSameCurrentItem() {
228
254
  if (wrapper.playbackActive) {
229
255
  replay()
@@ -28,6 +28,7 @@ export interface Spec extends TurboModule {
28
28
  skipToPrevious(initialPosition: number): Promise<void>;
29
29
  updateMetadataForTrack(trackIndex: number, metadata: UnsafeObject): Promise<void>;
30
30
  updateNowPlayingMetadata(metadata: UnsafeObject): Promise<void>;
31
+ setTrackPlayable(trackIndex: number, playable: boolean): Promise<void>;
31
32
  setQueue(tracks: UnsafeObject[]): Promise<void>;
32
33
  getQueue(): Promise<UnsafeObject[]>;
33
34
  setRepeatMode(mode: number): Promise<number>;
@@ -168,5 +168,10 @@ export declare enum Event {
168
168
  /**
169
169
  * Fired when there is an fft update
170
170
  **/
171
- fftUpdate = "fft-updated"
171
+ fftUpdate = "fft-updated",
172
+ /**
173
+ * Fired when a track with notPlayable=true becomes the active track.
174
+ * The track stays current but won't load or play.
175
+ */
176
+ PlaybackNotPlayableTrackActive = "playback-not-playable-track-active"
172
177
  }
@@ -170,4 +170,9 @@ export var Event;
170
170
  * Fired when there is an fft update
171
171
  **/
172
172
  Event["fftUpdate"] = "fft-updated";
173
+ /**
174
+ * Fired when a track with notPlayable=true becomes the active track.
175
+ * The track stays current but won't load or play.
176
+ */
177
+ Event["PlaybackNotPlayableTrackActive"] = "playback-not-playable-track-active";
173
178
  })(Event || (Event = {}));
@@ -1,8 +1,7 @@
1
- import { Event } from '../constants';
2
- import type { EventPayloadByEventWithType } from '../interfaces';
1
+ import type { EventPayloadByEvent, EventPayloadByEventWithType } from '../interfaces';
3
2
  /**
4
3
  * Attaches a handler to the given TrackPlayer events and performs cleanup on unmount
5
4
  * @param events - TrackPlayer events to subscribe to
6
5
  * @param handler - callback invoked when the event fires
7
6
  */
8
- export declare const useTrackPlayerEvents: <T extends Event[], H extends (data: EventPayloadByEventWithType[T[number]]) => void>(events: T, handler: H) => void;
7
+ export declare const useTrackPlayerEvents: <T extends (keyof EventPayloadByEvent)[], H extends (data: EventPayloadByEventWithType[T[number]]) => void>(events: T, handler: H) => void;
@@ -1,6 +1,5 @@
1
1
  import { useEffect, useRef } from 'react';
2
2
  import { addEventListener } from '../trackPlayer';
3
- import { Event } from '../constants';
4
3
  /**
5
4
  * Attaches a handler to the given TrackPlayer events and performs cleanup on unmount
6
5
  * @param events - TrackPlayer events to subscribe to
@@ -13,6 +13,11 @@ export interface Track extends TrackMetadataBase {
13
13
  headers?: {
14
14
  [key: string]: any;
15
15
  };
16
+ /**
17
+ * When true, the track won't load or play when it becomes current.
18
+ * The track stays in the queue but playback is blocked.
19
+ */
20
+ notPlayable?: boolean;
16
21
  [key: string]: any;
17
22
  }
18
23
  export type AddTrack = Track & {
@@ -63,6 +63,7 @@ export type EventPayloadByEvent = {
63
63
  [Event.connectorConnected]: ControllerConnectedEvent;
64
64
  [Event.connectorDisconnected]: ControllerDisconnectedEvent;
65
65
  [Event.fftUpdate]: FFTUpdateEvent;
66
+ [Event.PlaybackNotPlayableTrackActive]: never;
66
67
  };
67
68
  type Simplify<T> = {
68
69
  [KeyType in keyof T]: T[KeyType];
@@ -1,4 +1,4 @@
1
- import { Event, RepeatMode, AndroidAutoContentStyle } from './constants';
1
+ import { RepeatMode, AndroidAutoContentStyle } from './constants';
2
2
  import type { AddTrack, EventPayloadByEvent, NowPlayingMetadata, PlaybackState, PlayerOptions, Progress, ServiceHandler, Track, TrackMetadataBase, UpdateOptions, AndroidAutoBrowseTree, MediaItem } from './interfaces';
3
3
  /**
4
4
  * Initializes the player with the specified options.
@@ -16,7 +16,7 @@ export declare function setupPlayer(options?: PlayerOptions, background?: boolea
16
16
  * Register the playback service. The service will run as long as the player runs.
17
17
  */
18
18
  export declare function registerPlaybackService(factory: () => ServiceHandler): void;
19
- export declare function addEventListener<T extends Event>(event: T, listener: EventPayloadByEvent[T] extends never ? () => void : (event: EventPayloadByEvent[T]) => void): import("react-native").EmitterSubscription;
19
+ export declare function addEventListener<T extends keyof EventPayloadByEvent>(event: T, listener: EventPayloadByEvent[T] extends never ? () => void : (event: EventPayloadByEvent[T]) => void): import("react-native").EmitterSubscription;
20
20
  /**
21
21
  * Adds one or more tracks to the queue.
22
22
  *
@@ -110,6 +110,15 @@ export declare function updateMetadataForTrack(trackIndex: number, metadata: Tra
110
110
  * without affecting the data stored for the current track.
111
111
  */
112
112
  export declare function updateNowPlayingMetadata(metadata: NowPlayingMetadata): Promise<void>;
113
+ /**
114
+ * Updates the playable status of a track in the queue.
115
+ * When playable is false (notPlayable is true), the track won't load or play when current.
116
+ * When playable is true (notPlayable is false), normal playback behavior resumes.
117
+ *
118
+ * @param trackIndex The index of the track to update.
119
+ * @param playable Whether the track should be playable.
120
+ */
121
+ export declare function setTrackPlayable(trackIndex: number, playable: boolean): Promise<void>;
113
122
  /**
114
123
  * Resets the player stopping the current track and clearing the queue.
115
124
  */
@@ -163,6 +163,17 @@ export function updateNowPlayingMetadata(metadata) {
163
163
  });
164
164
  }
165
165
  // MARK: - Player API
166
+ /**
167
+ * Updates the playable status of a track in the queue.
168
+ * When playable is false (notPlayable is true), the track won't load or play when current.
169
+ * When playable is true (notPlayable is false), normal playback behavior resumes.
170
+ *
171
+ * @param trackIndex The index of the track to update.
172
+ * @param playable Whether the track should be playable.
173
+ */
174
+ export async function setTrackPlayable(trackIndex, playable) {
175
+ return TrackPlayer.setTrackPlayable(trackIndex, playable);
176
+ }
166
177
  /**
167
178
  * Resets the player stopping the current track and clearing the queue.
168
179
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@javascriptcommon/react-native-track-player",
3
- "version": "4.1.12",
3
+ "version": "4.1.14",
4
4
  "description": "A fully fledged audio module created for music apps",
5
5
  "main": "lib/src/index.js",
6
6
  "types": "lib/src/index.d.ts",
@@ -41,6 +41,7 @@ export interface Spec extends TurboModule {
41
41
  metadata: UnsafeObject,
42
42
  ): Promise<void>;
43
43
  updateNowPlayingMetadata(metadata: UnsafeObject): Promise<void>;
44
+ setTrackPlayable(trackIndex: number, playable: boolean): Promise<void>;
44
45
  setQueue(tracks: UnsafeObject[]): Promise<void>;
45
46
  getQueue(): Promise<UnsafeObject[]>;
46
47
  setRepeatMode(mode: number): Promise<number>;
@@ -170,4 +170,9 @@ export enum Event {
170
170
  * Fired when there is an fft update
171
171
  **/
172
172
  fftUpdate = 'fft-updated',
173
+ /**
174
+ * Fired when a track with notPlayable=true becomes the active track.
175
+ * The track stays current but won't load or play.
176
+ */
177
+ PlaybackNotPlayableTrackActive = 'playback-not-playable-track-active',
173
178
  }
@@ -1,8 +1,7 @@
1
1
  import { useEffect, useRef } from 'react';
2
2
 
3
3
  import { addEventListener } from '../trackPlayer';
4
- import { Event } from '../constants';
5
- import type { EventPayloadByEventWithType } from '../interfaces';
4
+ import type { EventPayloadByEvent, EventPayloadByEventWithType } from '../interfaces';
6
5
 
7
6
  /**
8
7
  * Attaches a handler to the given TrackPlayer events and performs cleanup on unmount
@@ -10,7 +9,7 @@ import type { EventPayloadByEventWithType } from '../interfaces';
10
9
  * @param handler - callback invoked when the event fires
11
10
  */
12
11
  export const useTrackPlayerEvents = <
13
- T extends Event[],
12
+ T extends (keyof EventPayloadByEvent)[],
14
13
  H extends (data: EventPayloadByEventWithType[T[number]]) => void
15
14
  >(
16
15
  events: T,
@@ -13,6 +13,11 @@ export interface Track extends TrackMetadataBase {
13
13
  pitchAlgorithm?: PitchAlgorithm;
14
14
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
15
15
  headers?: { [key: string]: any };
16
+ /**
17
+ * When true, the track won't load or play when it becomes current.
18
+ * The track stays in the queue but playback is blocked.
19
+ */
20
+ notPlayable?: boolean;
16
21
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
22
  [key: string]: any;
18
23
  }
@@ -68,6 +68,7 @@ export type EventPayloadByEvent = {
68
68
  [Event.connectorConnected]: ControllerConnectedEvent;
69
69
  [Event.connectorDisconnected]: ControllerDisconnectedEvent;
70
70
  [Event.fftUpdate]: FFTUpdateEvent;
71
+ [Event.PlaybackNotPlayableTrackActive]: never;
71
72
  };
72
73
 
73
74
  // eslint-disable-next-line
@@ -82,7 +82,7 @@ export function registerPlaybackService(factory: () => ServiceHandler) {
82
82
  }
83
83
  }
84
84
 
85
- export function addEventListener<T extends Event>(
85
+ export function addEventListener<T extends keyof EventPayloadByEvent>(
86
86
  event: T,
87
87
  listener: EventPayloadByEvent[T] extends never
88
88
  ? () => void
@@ -275,6 +275,21 @@ export function updateNowPlayingMetadata(
275
275
 
276
276
  // MARK: - Player API
277
277
 
278
+ /**
279
+ * Updates the playable status of a track in the queue.
280
+ * When playable is false (notPlayable is true), the track won't load or play when current.
281
+ * When playable is true (notPlayable is false), normal playback behavior resumes.
282
+ *
283
+ * @param trackIndex The index of the track to update.
284
+ * @param playable Whether the track should be playable.
285
+ */
286
+ export async function setTrackPlayable(
287
+ trackIndex: number,
288
+ playable: boolean,
289
+ ): Promise<void> {
290
+ return TrackPlayer.setTrackPlayable(trackIndex, playable);
291
+ }
292
+
278
293
  /**
279
294
  * Resets the player stopping the current track and clearing the queue.
280
295
  */