@javascriptcommon/react-native-track-player 4.1.13 → 4.1.15

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 (29) 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/doublesymmetry/trackplayer/utils/BundleUtils.kt +2 -2
  7. package/android/src/main/java/com/lovegaoshi/kotlinaudio/models/AudioItem.kt +11 -2
  8. package/android/src/main/java/com/lovegaoshi/kotlinaudio/player/QueuedAudioPlayer.kt +24 -0
  9. package/ios/RNTrackPlayer/Models/Track.swift +7 -0
  10. package/ios/RNTrackPlayer/RNTrackPlayer.swift +42 -0
  11. package/ios/RNTrackPlayer/Utils/EventType.swift +1 -0
  12. package/ios/SwiftAudioEx/Sources/SwiftAudioEx/Event.swift +11 -0
  13. package/ios/SwiftAudioEx/Sources/SwiftAudioEx/QueuedAudioPlayer.swift +27 -1
  14. package/lib/specs/NativeTrackPlayer.d.ts +1 -0
  15. package/lib/src/constants/Event.d.ts +6 -1
  16. package/lib/src/constants/Event.js +5 -0
  17. package/lib/src/hooks/useTrackPlayerEvents.d.ts +2 -3
  18. package/lib/src/hooks/useTrackPlayerEvents.js +0 -1
  19. package/lib/src/interfaces/Track.d.ts +5 -0
  20. package/lib/src/interfaces/events/EventPayloadByEvent.d.ts +1 -0
  21. package/lib/src/trackPlayer.d.ts +11 -2
  22. package/lib/src/trackPlayer.js +11 -0
  23. package/package.json +1 -1
  24. package/specs/NativeTrackPlayer.ts +1 -0
  25. package/src/constants/Event.ts +5 -0
  26. package/src/hooks/useTrackPlayerEvents.ts +2 -3
  27. package/src/interfaces/Track.ts +5 -0
  28. package/src/interfaces/events/EventPayloadByEvent.ts +1 -0
  29. 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
  }
@@ -21,8 +21,8 @@ object BundleUtils {
21
21
  if (!data!!.containsKey(key)) return null
22
22
  val obj = data[key]
23
23
  if (obj is String) {
24
- // Remote or Local Uri
25
- if (obj.trim { it <= ' ' }.isEmpty()) throw RuntimeException("$key: The URL cannot be empty")
24
+ // Remote or Local Uri - allow empty URLs for placeholder tracks
25
+ if (obj.trim { it <= ' ' }.isEmpty()) return Uri.EMPTY
26
26
  return Uri.parse(obj as String?)
27
27
  } else if (obj is Bundle) {
28
28
  // require/import
@@ -77,15 +77,23 @@ enum class MediaType(val value: String) {
77
77
 
78
78
 
79
79
  fun audioItem2MediaItem(audioItem: AudioItem, context: Context? = null): MediaItem {
80
+ // Check if this is a placeholder track (notPlayable flag from TrackAudioItem)
81
+ val isNotPlayable = (audioItem as? com.doublesymmetry.trackplayer.model.TrackAudioItem)?.notPlayable ?: false
82
+ val hasValidUrl = audioItem.audioUrl.isNotBlank()
83
+
80
84
  return MediaItem.Builder()
81
85
  .setMediaId(audioItem.mediaId ?: UUID.randomUUID().toString())
86
+ // Always set URI (even empty string) so ExoPlayer adds item to timeline
82
87
  .setUri(audioItem.audioUrl)
83
88
  .setMediaMetadata(
84
89
  MediaMetadata.Builder()
85
90
  .setTitle(audioItem.title)
86
91
  .setArtist(audioItem.artist)
92
+ // Mark placeholder tracks as not playable so Android Auto shows them correctly in queue
93
+ .setIsPlayable(true)
94
+ .setIsBrowsable(false)
87
95
  .setArtworkUri((
88
- if (context != null && audioItem.audioUrl.startsWith("file://")) {
96
+ if (context != null && hasValidUrl && audioItem.audioUrl.startsWith("file://")) {
89
97
  saveMediaCoverToPng(
90
98
  audioItem.audioUrl,
91
99
  context.contentResolver,
@@ -94,7 +102,7 @@ fun audioItem2MediaItem(audioItem: AudioItem, context: Context? = null): MediaIt
94
102
  ?: audioItem.artwork
95
103
  }
96
104
  else audioItem.artwork)?.toUri())
97
- .setArtworkData(if (audioItem.audioUrl.startsWith("file://")) getEmbeddedBitmapArray(
105
+ .setArtworkData(if (hasValidUrl && audioItem.audioUrl.startsWith("file://")) getEmbeddedBitmapArray(
98
106
  audioItem.audioUrl.substring(7)) else null, MediaMetadata.PICTURE_TYPE_MEDIA)
99
107
  .setExtras(Bundle().apply {
100
108
  audioItem.options?.headers?.let {
@@ -108,6 +116,7 @@ fun audioItem2MediaItem(audioItem: AudioItem, context: Context? = null): MediaIt
108
116
  }
109
117
  putString("type", audioItem.type.toString())
110
118
  putString("uri", audioItem.audioUrl)
119
+ putBoolean("notPlayable", isNotPlayable)
111
120
  }).build())
112
121
  .setTag(audioItem)
113
122
  .build()
@@ -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
  }
@@ -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 {
@@ -898,6 +899,47 @@ public class RNTrackPlayer: NSObject, AudioSessionControllerDelegate {
898
899
  )
899
900
  }
900
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
+
901
943
  // MARK: - iOS Equalizer Methods
902
944
 
903
945
  @objc(setEqualizerEnabled:resolver:rejecter:)
@@ -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.13",
3
+ "version": "4.1.15",
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
  */