@javascriptcommon/react-native-track-player 4.1.17 → 4.1.18
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/doublesymmetry/trackplayer/module/MusicModule.kt +29 -1
- package/android/src/main/java/com/doublesymmetry/trackplayer/service/MusicService.kt +111 -18
- package/lib/specs/NativeTrackPlayer.d.ts +2 -0
- package/lib/src/constants/Capability.d.ts +2 -1
- package/lib/src/constants/Capability.js +1 -0
- package/lib/src/constants/Event.d.ts +4 -0
- package/lib/src/constants/Event.js +4 -0
- package/lib/src/interfaces/events/EventPayloadByEvent.d.ts +1 -0
- package/package.json +1 -1
- package/specs/NativeTrackPlayer.ts +1 -0
- package/src/constants/Capability.ts +1 -0
- package/src/constants/Event.ts +4 -0
- package/src/interfaces/events/EventPayloadByEvent.ts +1 -0
|
@@ -242,6 +242,7 @@ class MusicModule(reactContext: ReactApplicationContext) : NativeTrackPlayerSpec
|
|
|
242
242
|
this["CAPABILITY_LIKE"] = -1
|
|
243
243
|
this["CAPABILITY_DISLIKE"] = -1
|
|
244
244
|
this["CAPABILITY_BOOKMARK"] = -1
|
|
245
|
+
this["CAPABILITY_SHUFFLE"] = Capability.SHUFFLE.ordinal
|
|
245
246
|
|
|
246
247
|
}
|
|
247
248
|
}
|
|
@@ -420,6 +421,18 @@ class MusicModule(reactContext: ReactApplicationContext) : NativeTrackPlayerSpec
|
|
|
420
421
|
val bundle = Arguments.toBundle(data)
|
|
421
422
|
if (bundle is Bundle) {
|
|
422
423
|
musicService.load(bundleToTrack(bundle))
|
|
424
|
+
|
|
425
|
+
// Update heart state based on rating (for notification icon)
|
|
426
|
+
try {
|
|
427
|
+
val rating = bundle.get("rating")
|
|
428
|
+
val heartState = when (rating) {
|
|
429
|
+
is Number -> rating.toDouble() > 0
|
|
430
|
+
is Boolean -> rating
|
|
431
|
+
else -> false
|
|
432
|
+
}
|
|
433
|
+
musicService.setHeartState(heartState)
|
|
434
|
+
} catch (_: Exception) { }
|
|
435
|
+
|
|
423
436
|
callback.resolve(null)
|
|
424
437
|
} else {
|
|
425
438
|
callback.reject("invalid_track_object", "Track was not a dictionary type")
|
|
@@ -474,12 +487,27 @@ class MusicModule(reactContext: ReactApplicationContext) : NativeTrackPlayerSpec
|
|
|
474
487
|
override fun updateNowPlayingMetadata(map: ReadableMap?, callback: Promise) = launchInScope {
|
|
475
488
|
if (verifyServiceBoundOrReject(callback)) return@launchInScope
|
|
476
489
|
|
|
477
|
-
if (musicService.tracks.isEmpty())
|
|
490
|
+
if (musicService.tracks.isEmpty()) {
|
|
478
491
|
callback.reject("no_current_item", "There is no current item in the player")
|
|
492
|
+
return@launchInScope
|
|
493
|
+
}
|
|
479
494
|
|
|
480
495
|
Arguments.toBundle(map)?.let {
|
|
481
496
|
val track = bundleToTrack(it)
|
|
482
497
|
musicService.updateNowPlayingMetadata(track)
|
|
498
|
+
|
|
499
|
+
// Update heart state based on rating (for notification icon)
|
|
500
|
+
try {
|
|
501
|
+
val rating = it.get("rating")
|
|
502
|
+
if (rating != null) {
|
|
503
|
+
val heartState = when (rating) {
|
|
504
|
+
is Number -> rating.toDouble() > 0
|
|
505
|
+
is Boolean -> rating
|
|
506
|
+
else -> false
|
|
507
|
+
}
|
|
508
|
+
musicService.setHeartState(heartState)
|
|
509
|
+
}
|
|
510
|
+
} catch (_: Exception) { }
|
|
483
511
|
}
|
|
484
512
|
|
|
485
513
|
callback.resolve(null)
|
|
@@ -135,6 +135,9 @@ class MusicService : HeadlessJsMediaService() {
|
|
|
135
135
|
private var playerCommands: Player.Commands? = null
|
|
136
136
|
private var customLayout: List<CommandButton> = listOf()
|
|
137
137
|
private var lastWake: Long = 0
|
|
138
|
+
private var shuffleState: Boolean = false
|
|
139
|
+
private var heartState: Boolean = false
|
|
140
|
+
private var notificationCapabilities: List<Capability> = emptyList()
|
|
138
141
|
var searchResults: List<MediaItem> = listOf()
|
|
139
142
|
var searchBrowser: MediaSession.ControllerInfo? = null
|
|
140
143
|
var searchQuery: String = ""
|
|
@@ -402,7 +405,14 @@ class MusicService : HeadlessJsMediaService() {
|
|
|
402
405
|
}
|
|
403
406
|
|
|
404
407
|
player.alwaysPauseOnInterruption = androidOptions?.getBoolean(PAUSE_ON_INTERRUPTION_KEY) ?: false
|
|
405
|
-
|
|
408
|
+
val newShuffleState = androidOptions?.getBoolean(SHUFFLE_KEY) ?: false
|
|
409
|
+
player.shuffleMode = newShuffleState
|
|
410
|
+
shuffleState = newShuffleState
|
|
411
|
+
|
|
412
|
+
// Update heart state if provided
|
|
413
|
+
if (androidOptions?.containsKey("heartState") == true) {
|
|
414
|
+
heartState = androidOptions.getBoolean("heartState")
|
|
415
|
+
}
|
|
406
416
|
|
|
407
417
|
// setup progress update events if configured
|
|
408
418
|
progressUpdateJob?.cancel()
|
|
@@ -414,7 +424,7 @@ class MusicService : HeadlessJsMediaService() {
|
|
|
414
424
|
}
|
|
415
425
|
|
|
416
426
|
val capabilities = options.getIntegerArrayList("capabilities")?.map { Capability.entries[it] } ?: emptyList()
|
|
417
|
-
|
|
427
|
+
notificationCapabilities = options.getIntegerArrayList("notificationCapabilities")?.map { Capability.entries[it] } ?: emptyList()
|
|
418
428
|
compactCapabilities = options.getIntegerArrayList("compactCapabilities")?.map { Capability.entries[it] } ?: emptyList()
|
|
419
429
|
val customActions = options.getBundle(CUSTOM_ACTIONS_KEY)
|
|
420
430
|
val customActionsList = customActions?.getStringArrayList(CUSTOM_ACTIONS_LIST_KEY)
|
|
@@ -464,7 +474,7 @@ class MusicService : HeadlessJsMediaService() {
|
|
|
464
474
|
else -> { }
|
|
465
475
|
}
|
|
466
476
|
}
|
|
467
|
-
|
|
477
|
+
val customButtons = customActionsList?.map {
|
|
468
478
|
v -> CustomButton(
|
|
469
479
|
displayName = v,
|
|
470
480
|
sessionCommand = v,
|
|
@@ -475,7 +485,29 @@ class MusicService : HeadlessJsMediaService() {
|
|
|
475
485
|
TrackPlayerR.drawable.ifl_24px
|
|
476
486
|
)
|
|
477
487
|
).commandButton
|
|
478
|
-
} ?:
|
|
488
|
+
}?.toMutableList() ?: mutableListOf()
|
|
489
|
+
|
|
490
|
+
// Add heart button if SetRating capability is present
|
|
491
|
+
if (notificationCapabilities.contains(Capability.SET_RATING)) {
|
|
492
|
+
val heartIcon = if (heartState) TrackPlayerR.drawable.heart_24px else TrackPlayerR.drawable.hearte_24px
|
|
493
|
+
customButtons.add(0, CustomButton(
|
|
494
|
+
displayName = "Heart",
|
|
495
|
+
sessionCommand = "heart",
|
|
496
|
+
iconRes = heartIcon
|
|
497
|
+
).commandButton)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Add shuffle button if capability is present
|
|
501
|
+
if (notificationCapabilities.contains(Capability.SHUFFLE)) {
|
|
502
|
+
val shuffleIcon = if (shuffleState) TrackPlayerR.drawable.shuffle_on_24px else TrackPlayerR.drawable.shuffle_24px
|
|
503
|
+
customButtons.add(0, CustomButton(
|
|
504
|
+
displayName = "Shuffle",
|
|
505
|
+
sessionCommand = "shuffle",
|
|
506
|
+
iconRes = shuffleIcon
|
|
507
|
+
).commandButton)
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
customLayout = customButtons
|
|
479
511
|
|
|
480
512
|
val sessionCommandsBuilder = MediaSession.ConnectionResult.DEFAULT_SESSION_AND_LIBRARY_COMMANDS.buildUpon()
|
|
481
513
|
customLayout.forEach {
|
|
@@ -486,17 +518,15 @@ class MusicService : HeadlessJsMediaService() {
|
|
|
486
518
|
sessionCommands = sessionCommandsBuilder.build()
|
|
487
519
|
playerCommands = playerCommandsBuilder.build()
|
|
488
520
|
|
|
489
|
-
|
|
521
|
+
// Use safe call to avoid race condition
|
|
522
|
+
mediaSession.mediaNotificationControllerInfo?.let { controllerInfo ->
|
|
490
523
|
// https://github.com/androidx/media/blob/c35a9d62baec57118ea898e271ac66819399649b/demos/session_service/src/main/java/androidx/media3/demo/session/DemoMediaLibrarySessionCallback.kt#L107
|
|
491
|
-
mediaSession.setCustomLayout(
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
sessionCommands!!,
|
|
498
|
-
playerCommands!!
|
|
499
|
-
)
|
|
524
|
+
mediaSession.setCustomLayout(controllerInfo, customLayout)
|
|
525
|
+
sessionCommands?.let { sc ->
|
|
526
|
+
playerCommands?.let { pc ->
|
|
527
|
+
mediaSession.setAvailableCommands(controllerInfo, sc, pc)
|
|
528
|
+
}
|
|
529
|
+
}
|
|
500
530
|
}
|
|
501
531
|
}
|
|
502
532
|
|
|
@@ -648,6 +678,61 @@ class MusicService : HeadlessJsMediaService() {
|
|
|
648
678
|
player.repeatMode = value
|
|
649
679
|
}
|
|
650
680
|
|
|
681
|
+
@MainThread
|
|
682
|
+
fun setShuffleState(enabled: Boolean) {
|
|
683
|
+
if (shuffleState != enabled) {
|
|
684
|
+
shuffleState = enabled
|
|
685
|
+
updateCustomLayout()
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
@MainThread
|
|
690
|
+
fun setHeartState(saved: Boolean) {
|
|
691
|
+
if (heartState != saved) {
|
|
692
|
+
heartState = saved
|
|
693
|
+
updateCustomLayout()
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
@MainThread
|
|
698
|
+
private fun updateCustomLayout() {
|
|
699
|
+
// Check if mediaSession is initialized before accessing it
|
|
700
|
+
if (!::mediaSession.isInitialized) return
|
|
701
|
+
|
|
702
|
+
try {
|
|
703
|
+
val customButtons = mutableListOf<CommandButton>()
|
|
704
|
+
|
|
705
|
+
// Add heart button if SetRating capability is present
|
|
706
|
+
if (notificationCapabilities.contains(Capability.SET_RATING)) {
|
|
707
|
+
val heartIcon = if (heartState) TrackPlayerR.drawable.heart_24px else TrackPlayerR.drawable.hearte_24px
|
|
708
|
+
customButtons.add(CustomButton(
|
|
709
|
+
displayName = "Heart",
|
|
710
|
+
sessionCommand = "heart",
|
|
711
|
+
iconRes = heartIcon
|
|
712
|
+
).commandButton)
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Add shuffle button if capability is present
|
|
716
|
+
if (notificationCapabilities.contains(Capability.SHUFFLE)) {
|
|
717
|
+
val shuffleIcon = if (shuffleState) TrackPlayerR.drawable.shuffle_on_24px else TrackPlayerR.drawable.shuffle_24px
|
|
718
|
+
customButtons.add(0, CustomButton(
|
|
719
|
+
displayName = "Shuffle",
|
|
720
|
+
sessionCommand = "shuffle",
|
|
721
|
+
iconRes = shuffleIcon
|
|
722
|
+
).commandButton)
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
customLayout = customButtons
|
|
726
|
+
|
|
727
|
+
// Use safe call to avoid race condition where mediaNotificationControllerInfo becomes null
|
|
728
|
+
mediaSession.mediaNotificationControllerInfo?.let { controllerInfo ->
|
|
729
|
+
mediaSession.setCustomLayout(controllerInfo, customLayout)
|
|
730
|
+
}
|
|
731
|
+
} catch (e: Exception) {
|
|
732
|
+
// Ignore errors in custom layout update - notification is non-critical
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
651
736
|
@MainThread
|
|
652
737
|
fun getVolume(): Float = player.volume
|
|
653
738
|
|
|
@@ -869,9 +954,13 @@ class MusicService : HeadlessJsMediaService() {
|
|
|
869
954
|
}
|
|
870
955
|
|
|
871
956
|
is MediaSessionCallback.CUSTOMACTION -> {
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
emit(MusicEvents.
|
|
957
|
+
when (it.customAction) {
|
|
958
|
+
"shuffle" -> emit(MusicEvents.BUTTON_SHUFFLE)
|
|
959
|
+
"heart" -> emit(MusicEvents.BUTTON_SET_RATING, Bundle())
|
|
960
|
+
else -> Bundle().apply {
|
|
961
|
+
putString("customAction", it.customAction)
|
|
962
|
+
emit(MusicEvents.BUTTON_CUSTOM_ACTION, this)
|
|
963
|
+
}
|
|
875
964
|
}
|
|
876
965
|
}
|
|
877
966
|
}
|
|
@@ -1224,7 +1313,11 @@ class MusicService : HeadlessJsMediaService() {
|
|
|
1224
1313
|
customCommand: SessionCommand,
|
|
1225
1314
|
args: Bundle
|
|
1226
1315
|
): ListenableFuture<SessionResult> {
|
|
1227
|
-
|
|
1316
|
+
when (customCommand.customAction) {
|
|
1317
|
+
"shuffle" -> emit(MusicEvents.BUTTON_SHUFFLE)
|
|
1318
|
+
"heart" -> emit(MusicEvents.BUTTON_SET_RATING, Bundle())
|
|
1319
|
+
else -> emit(MusicEvents.BUTTON_CUSTOM_ACTION, Bundle().apply { putString("customAction", customCommand.customAction) })
|
|
1320
|
+
}
|
|
1228
1321
|
return super.onCustomCommand(session, controller, customCommand, args)
|
|
1229
1322
|
}
|
|
1230
1323
|
|
|
@@ -74,6 +74,7 @@ export interface Spec extends TurboModule {
|
|
|
74
74
|
CAPABILITY_LIKE: number;
|
|
75
75
|
CAPABILITY_DISLIKE: number;
|
|
76
76
|
CAPABILITY_BOOKMARK: number;
|
|
77
|
+
CAPABILITY_SHUFFLE: number;
|
|
77
78
|
STATE_NONE: string;
|
|
78
79
|
STATE_READY: string;
|
|
79
80
|
STATE_PLAYING: string;
|
|
@@ -121,6 +122,7 @@ export declare const Constants: {
|
|
|
121
122
|
CAPABILITY_LIKE: number;
|
|
122
123
|
CAPABILITY_DISLIKE: number;
|
|
123
124
|
CAPABILITY_BOOKMARK: number;
|
|
125
|
+
CAPABILITY_SHUFFLE: number;
|
|
124
126
|
STATE_NONE: string;
|
|
125
127
|
STATE_READY: string;
|
|
126
128
|
STATE_PLAYING: string;
|
|
@@ -16,4 +16,5 @@ export var Capability;
|
|
|
16
16
|
Capability[Capability["Like"] = TrackPlayer.CAPABILITY_LIKE] = "Like";
|
|
17
17
|
Capability[Capability["Dislike"] = TrackPlayer.CAPABILITY_DISLIKE] = "Dislike";
|
|
18
18
|
Capability[Capability["Bookmark"] = TrackPlayer.CAPABILITY_BOOKMARK] = "Bookmark";
|
|
19
|
+
Capability[Capability["Shuffle"] = TrackPlayer.CAPABILITY_SHUFFLE] = "Shuffle";
|
|
19
20
|
})(Capability || (Capability = {}));
|
|
@@ -141,6 +141,10 @@ export declare enum Event {
|
|
|
141
141
|
* See https://rntp.dev/docs/api/events#remoteCustomAction
|
|
142
142
|
**/
|
|
143
143
|
RemoteCustomAction = "remote-custom-action",
|
|
144
|
+
/**
|
|
145
|
+
* (Android only) Fired when the user presses the shuffle button.
|
|
146
|
+
**/
|
|
147
|
+
RemoteShuffle = "remote-shuffle",
|
|
144
148
|
/** (iOS only) Fired when chapter metadata is received.
|
|
145
149
|
* See https://rntp.dev/docs/api/events#chaptermetadatareceived
|
|
146
150
|
**/
|
|
@@ -142,6 +142,10 @@ export var Event;
|
|
|
142
142
|
* See https://rntp.dev/docs/api/events#remoteCustomAction
|
|
143
143
|
**/
|
|
144
144
|
Event["RemoteCustomAction"] = "remote-custom-action";
|
|
145
|
+
/**
|
|
146
|
+
* (Android only) Fired when the user presses the shuffle button.
|
|
147
|
+
**/
|
|
148
|
+
Event["RemoteShuffle"] = "remote-shuffle";
|
|
145
149
|
/** (iOS only) Fired when chapter metadata is received.
|
|
146
150
|
* See https://rntp.dev/docs/api/events#chaptermetadatareceived
|
|
147
151
|
**/
|
|
@@ -57,6 +57,7 @@ export type EventPayloadByEvent = {
|
|
|
57
57
|
[Event.RemoteBrowse]: RemoteBrowseEvent;
|
|
58
58
|
[Event.PlaybackResume]: PlaybackResumeEvent;
|
|
59
59
|
[Event.RemoteCustomAction]: RemoteCustomActionEvent;
|
|
60
|
+
[Event.RemoteShuffle]: never;
|
|
60
61
|
[Event.MetadataChapterReceived]: AudioMetadataReceivedEvent;
|
|
61
62
|
[Event.MetadataTimedReceived]: AudioMetadataReceivedEvent;
|
|
62
63
|
[Event.MetadataCommonReceived]: AudioCommonMetadataReceivedEvent;
|
package/package.json
CHANGED
package/src/constants/Event.ts
CHANGED
|
@@ -142,6 +142,10 @@ export enum Event {
|
|
|
142
142
|
* See https://rntp.dev/docs/api/events#remoteCustomAction
|
|
143
143
|
**/
|
|
144
144
|
RemoteCustomAction = 'remote-custom-action',
|
|
145
|
+
/**
|
|
146
|
+
* (Android only) Fired when the user presses the shuffle button.
|
|
147
|
+
**/
|
|
148
|
+
RemoteShuffle = 'remote-shuffle',
|
|
145
149
|
/** (iOS only) Fired when chapter metadata is received.
|
|
146
150
|
* See https://rntp.dev/docs/api/events#chaptermetadatareceived
|
|
147
151
|
**/
|
|
@@ -62,6 +62,7 @@ export type EventPayloadByEvent = {
|
|
|
62
62
|
[Event.RemoteBrowse]: RemoteBrowseEvent;
|
|
63
63
|
[Event.PlaybackResume]: PlaybackResumeEvent;
|
|
64
64
|
[Event.RemoteCustomAction]: RemoteCustomActionEvent;
|
|
65
|
+
[Event.RemoteShuffle]: never;
|
|
65
66
|
[Event.MetadataChapterReceived]: AudioMetadataReceivedEvent;
|
|
66
67
|
[Event.MetadataTimedReceived]: AudioMetadataReceivedEvent;
|
|
67
68
|
[Event.MetadataCommonReceived]: AudioCommonMetadataReceivedEvent;
|