@rntp/player 5.0.0-beta.2 → 5.0.0-beta.4
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/RNTPPlayer.podspec +1 -0
- package/android/src/main/java/com/doublesymmetry/trackplayer/TrackPlayerModule.kt +60 -0
- package/android/src/main/java/com/doublesymmetry/trackplayer/TrackPlayerPlaybackService.kt +156 -0
- package/android/src/main/java/com/doublesymmetry/trackplayer/models/BrowseTree.kt +51 -20
- package/android/src/main/java/com/doublesymmetry/trackplayer/models/EmitEventType.kt +11 -0
- package/ios/CarPlay/BrowseTreeStore.swift +40 -0
- package/ios/CarPlay/RNTPCarPlaySceneDelegate.swift +283 -0
- package/ios/TrackPlayer.swift +135 -1
- package/ios/TrackPlayerBridge.mm +9 -0
- package/ios/models/EmitEvent.swift +10 -0
- package/lib/commonjs/NativeTrackPlayer.js.map +1 -1
- package/lib/commonjs/audio.js +76 -7
- package/lib/commonjs/audio.js.map +1 -1
- package/lib/commonjs/events/SleepTimerTriggered.js +2 -0
- package/lib/commonjs/events/SleepTimerTriggered.js.map +1 -0
- package/lib/commonjs/events/index.js +13 -0
- package/lib/commonjs/events/index.js.map +1 -1
- package/lib/module/NativeTrackPlayer.js.map +1 -1
- package/lib/module/audio.js +72 -7
- package/lib/module/audio.js.map +1 -1
- package/lib/module/events/SleepTimerTriggered.js +2 -0
- package/lib/module/events/SleepTimerTriggered.js.map +1 -0
- package/lib/module/events/index.js +2 -0
- package/lib/module/events/index.js.map +1 -1
- package/lib/typescript/src/NativeTrackPlayer.d.ts +4 -0
- package/lib/typescript/src/NativeTrackPlayer.d.ts.map +1 -1
- package/lib/typescript/src/audio.d.ts +48 -5
- package/lib/typescript/src/audio.d.ts.map +1 -1
- package/lib/typescript/src/events/SleepTimerTriggered.d.ts +5 -0
- package/lib/typescript/src/events/SleepTimerTriggered.d.ts.map +1 -0
- package/lib/typescript/src/events/index.d.ts +5 -1
- package/lib/typescript/src/events/index.d.ts.map +1 -1
- package/lib/typescript/src/interfaces/BrowseTree.d.ts +35 -5
- package/lib/typescript/src/interfaces/BrowseTree.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/NativeTrackPlayer.ts +6 -0
- package/src/audio.ts +84 -8
- package/src/events/SleepTimerTriggered.ts +4 -0
- package/src/events/index.ts +4 -0
- package/src/interfaces/BrowseTree.ts +40 -5
package/RNTPPlayer.podspec
CHANGED
|
@@ -502,6 +502,66 @@ class TrackPlayerModule internal constructor(private val context: ReactApplicati
|
|
|
502
502
|
|
|
503
503
|
// endregion
|
|
504
504
|
|
|
505
|
+
// region Sleep Timer
|
|
506
|
+
|
|
507
|
+
@ReactMethod
|
|
508
|
+
override fun sleepAfterTime(seconds: Double, fadeOutSeconds: Double) {
|
|
509
|
+
controller.run { mc ->
|
|
510
|
+
val args = Bundle().apply {
|
|
511
|
+
putDouble("seconds", seconds)
|
|
512
|
+
putDouble("fadeOutSeconds", fadeOutSeconds)
|
|
513
|
+
}
|
|
514
|
+
mc?.sendCustomCommand(
|
|
515
|
+
SessionCommand(TrackPlayerPlaybackService.COMMAND_SLEEP_AFTER_TIME, Bundle.EMPTY),
|
|
516
|
+
args
|
|
517
|
+
)
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
@ReactMethod
|
|
522
|
+
override fun sleepAfterMediaItemAtIndex(index: Double) {
|
|
523
|
+
controller.run { mc ->
|
|
524
|
+
val args = Bundle().apply { putInt("index", index.toInt()) }
|
|
525
|
+
mc?.sendCustomCommand(
|
|
526
|
+
SessionCommand(TrackPlayerPlaybackService.COMMAND_SLEEP_AFTER_MEDIA_ITEM, Bundle.EMPTY),
|
|
527
|
+
args
|
|
528
|
+
)
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
533
|
+
override fun getSleepTimer(): WritableMap? {
|
|
534
|
+
val prefs = context.getSharedPreferences(PLAYER_PREFS_NAME, android.content.Context.MODE_PRIVATE)
|
|
535
|
+
val jsonStr = prefs.getString(TrackPlayerPlaybackService.SLEEP_TIMER_STATE_KEY, null) ?: return null
|
|
536
|
+
return try {
|
|
537
|
+
val json = JSONObject(jsonStr)
|
|
538
|
+
val type = json.getString("type")
|
|
539
|
+
val map = Arguments.createMap()
|
|
540
|
+
map.putString("type", type)
|
|
541
|
+
if (type == "time") {
|
|
542
|
+
map.putDouble("remainingSeconds", json.getDouble("remainingSeconds"))
|
|
543
|
+
map.putDouble("fadeOutSeconds", json.getDouble("fadeOutSeconds"))
|
|
544
|
+
} else {
|
|
545
|
+
map.putInt("index", json.getInt("index"))
|
|
546
|
+
}
|
|
547
|
+
map
|
|
548
|
+
} catch (e: Exception) {
|
|
549
|
+
null
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
@ReactMethod
|
|
554
|
+
override fun cancelSleepTimer() {
|
|
555
|
+
controller.run { mc ->
|
|
556
|
+
mc?.sendCustomCommand(
|
|
557
|
+
SessionCommand(TrackPlayerPlaybackService.COMMAND_CANCEL_SLEEP_TIMER, Bundle.EMPTY),
|
|
558
|
+
Bundle.EMPTY
|
|
559
|
+
)
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// endregion
|
|
564
|
+
|
|
505
565
|
// region Destroy
|
|
506
566
|
|
|
507
567
|
@ReactMethod
|
|
@@ -56,6 +56,7 @@ import com.doublesymmetry.trackplayer.models.WakeMode
|
|
|
56
56
|
import android.os.Handler
|
|
57
57
|
import android.os.Looper
|
|
58
58
|
import com.doublesymmetry.trackplayer.models.PlaybackProgressUpdatedEvent
|
|
59
|
+
import com.doublesymmetry.trackplayer.models.SleepTimerTriggeredEvent
|
|
59
60
|
import org.json.JSONObject
|
|
60
61
|
|
|
61
62
|
class TrackPlayerPlaybackService: MediaLibraryService(), MediaLibraryService.MediaLibrarySession.Callback {
|
|
@@ -68,6 +69,14 @@ class TrackPlayerPlaybackService: MediaLibraryService(), MediaLibraryService.Med
|
|
|
68
69
|
private var progressSyncTimer: java.util.Timer? = null
|
|
69
70
|
private var progressSyncExecutor: java.util.concurrent.ExecutorService? = null
|
|
70
71
|
|
|
72
|
+
private var sleepTimerType: String? = null // "time" or "mediaItem"
|
|
73
|
+
private var sleepTimerRemainingSeconds: Double = 0.0
|
|
74
|
+
private var sleepTimerFadeOutSeconds: Double = 0.0
|
|
75
|
+
private var sleepTimerTargetIndex: Int? = null
|
|
76
|
+
private var sleepTimer: java.util.Timer? = null
|
|
77
|
+
private var sleepTimerPreFadeVolume: Float? = null
|
|
78
|
+
private var sleepTimerPreviousIndex: Int? = null
|
|
79
|
+
|
|
71
80
|
@OptIn(UnstableApi::class)
|
|
72
81
|
override fun onCreate() {
|
|
73
82
|
super.onCreate()
|
|
@@ -136,6 +145,26 @@ class TrackPlayerPlaybackService: MediaLibraryService(), MediaLibraryService.Med
|
|
|
136
145
|
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
|
137
146
|
if (isPlaying) startProgressSyncTimer() else stopProgressSyncTimer(fireFinalTick = true)
|
|
138
147
|
}
|
|
148
|
+
|
|
149
|
+
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
|
150
|
+
val currentIndex = activePlayer?.currentMediaItemIndex ?: 0
|
|
151
|
+
if (sleepTimerType == "mediaItem") {
|
|
152
|
+
val targetIndex = sleepTimerTargetIndex
|
|
153
|
+
if (targetIndex != null && sleepTimerPreviousIndex == targetIndex && currentIndex != targetIndex) {
|
|
154
|
+
this@TrackPlayerPlaybackService.emitEvent(SleepTimerTriggeredEvent("mediaItem"))
|
|
155
|
+
cancelSleepTimerInternal(restoreVolume = false)
|
|
156
|
+
// Defer pause to next run loop — calling during transition gets overridden
|
|
157
|
+
Handler(Looper.getMainLooper()).post { activePlayer?.pause() }
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
sleepTimerPreviousIndex = currentIndex
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
override fun onTimelineChanged(timeline: androidx.media3.common.Timeline, reason: Int) {
|
|
164
|
+
if (sleepTimerType == "mediaItem" && (activePlayer?.mediaItemCount ?: 0) == 0) {
|
|
165
|
+
cancelSleepTimerInternal(restoreVolume = false)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
139
168
|
})
|
|
140
169
|
val player = createForwardingPlayer(activePlayer, config)
|
|
141
170
|
|
|
@@ -275,6 +304,7 @@ class TrackPlayerPlaybackService: MediaLibraryService(), MediaLibraryService.Med
|
|
|
275
304
|
simpleCache?.release()
|
|
276
305
|
simpleCache = null
|
|
277
306
|
sharedCache = null
|
|
307
|
+
cancelSleepTimerInternal(restoreVolume = false)
|
|
278
308
|
stopProgressSyncTimer(fireFinalTick = false)
|
|
279
309
|
progressSyncExecutor?.shutdown()
|
|
280
310
|
progressSyncExecutor = null
|
|
@@ -355,6 +385,37 @@ class TrackPlayerPlaybackService: MediaLibraryService(), MediaLibraryService.Med
|
|
|
355
385
|
activePlayer?.seekToPreviousMediaItem()
|
|
356
386
|
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
|
|
357
387
|
}
|
|
388
|
+
COMMAND_SLEEP_AFTER_TIME -> {
|
|
389
|
+
val seconds = args.getDouble("seconds")
|
|
390
|
+
val fadeOutSeconds = args.getDouble("fadeOutSeconds")
|
|
391
|
+
cancelSleepTimerInternal(restoreVolume = true)
|
|
392
|
+
sleepTimerType = "time"
|
|
393
|
+
sleepTimerRemainingSeconds = seconds
|
|
394
|
+
sleepTimerFadeOutSeconds = fadeOutSeconds.coerceAtMost(seconds)
|
|
395
|
+
persistSleepTimerState()
|
|
396
|
+
|
|
397
|
+
if (seconds <= 0) {
|
|
398
|
+
activePlayer?.pause()
|
|
399
|
+
this@TrackPlayerPlaybackService.emitEvent(SleepTimerTriggeredEvent("time"))
|
|
400
|
+
cancelSleepTimerInternal(restoreVolume = true)
|
|
401
|
+
} else {
|
|
402
|
+
startSleepCountdownTimer()
|
|
403
|
+
}
|
|
404
|
+
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
|
|
405
|
+
}
|
|
406
|
+
COMMAND_SLEEP_AFTER_MEDIA_ITEM -> {
|
|
407
|
+
val index = args.getInt("index")
|
|
408
|
+
cancelSleepTimerInternal(restoreVolume = true)
|
|
409
|
+
sleepTimerType = "mediaItem"
|
|
410
|
+
sleepTimerTargetIndex = index
|
|
411
|
+
sleepTimerPreviousIndex = activePlayer?.currentMediaItemIndex
|
|
412
|
+
persistSleepTimerState()
|
|
413
|
+
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
|
|
414
|
+
}
|
|
415
|
+
COMMAND_CANCEL_SLEEP_TIMER -> {
|
|
416
|
+
cancelSleepTimerInternal(restoreVolume = true)
|
|
417
|
+
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
|
|
418
|
+
}
|
|
358
419
|
}
|
|
359
420
|
return super.onCustomCommand(session, controller, customCommand, args)
|
|
360
421
|
}
|
|
@@ -404,12 +465,20 @@ class TrackPlayerPlaybackService: MediaLibraryService(), MediaLibraryService.Med
|
|
|
404
465
|
return Futures.immediateFuture(LibraryResult.ofItemList(ImmutableList.copyOf(categoryItems), params))
|
|
405
466
|
}
|
|
406
467
|
|
|
468
|
+
// Check if it's a category
|
|
407
469
|
val category = browseTree.findCategory(parentId)
|
|
408
470
|
if (category != null) {
|
|
409
471
|
val mediaItems = category.items.map { it.toMediaItem() }
|
|
410
472
|
return Futures.immediateFuture(LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), params))
|
|
411
473
|
}
|
|
412
474
|
|
|
475
|
+
// Check if it's a browsable item with children
|
|
476
|
+
val browseItem = browseTree.findItem(parentId)
|
|
477
|
+
if (browseItem != null && browseItem.children != null) {
|
|
478
|
+
val mediaItems = browseItem.children.map { it.toMediaItem() }
|
|
479
|
+
return Futures.immediateFuture(LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), params))
|
|
480
|
+
}
|
|
481
|
+
|
|
413
482
|
return Futures.immediateFuture(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE))
|
|
414
483
|
}
|
|
415
484
|
|
|
@@ -475,6 +544,9 @@ class TrackPlayerPlaybackService: MediaLibraryService(), MediaLibraryService.Med
|
|
|
475
544
|
.add(SessionCommand(COMMAND_SEEK_TO_NEXT, Bundle.EMPTY))
|
|
476
545
|
.add(SessionCommand(COMMAND_SEEK_TO_PREVIOUS, Bundle.EMPTY))
|
|
477
546
|
.add(SessionCommand(COMMAND_UPDATE_PROGRESS_SYNC_HEADERS, Bundle.EMPTY))
|
|
547
|
+
.add(SessionCommand(COMMAND_SLEEP_AFTER_TIME, Bundle.EMPTY))
|
|
548
|
+
.add(SessionCommand(COMMAND_SLEEP_AFTER_MEDIA_ITEM, Bundle.EMPTY))
|
|
549
|
+
.add(SessionCommand(COMMAND_CANCEL_SLEEP_TIMER, Bundle.EMPTY))
|
|
478
550
|
.build()
|
|
479
551
|
|
|
480
552
|
// endregion
|
|
@@ -554,6 +626,86 @@ class TrackPlayerPlaybackService: MediaLibraryService(), MediaLibraryService.Med
|
|
|
554
626
|
|
|
555
627
|
// endregion
|
|
556
628
|
|
|
629
|
+
// region Sleep Timer
|
|
630
|
+
|
|
631
|
+
private fun cancelSleepTimerInternal(restoreVolume: Boolean) {
|
|
632
|
+
sleepTimer?.cancel()
|
|
633
|
+
sleepTimer = null
|
|
634
|
+
if (restoreVolume) {
|
|
635
|
+
sleepTimerPreFadeVolume?.let { activePlayer?.volume = it }
|
|
636
|
+
}
|
|
637
|
+
sleepTimerPreFadeVolume = null
|
|
638
|
+
sleepTimerType = null
|
|
639
|
+
sleepTimerRemainingSeconds = 0.0
|
|
640
|
+
sleepTimerFadeOutSeconds = 0.0
|
|
641
|
+
sleepTimerTargetIndex = null
|
|
642
|
+
sleepTimerPreviousIndex = null
|
|
643
|
+
persistSleepTimerState()
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
private fun onSleepTimerTick() {
|
|
647
|
+
sleepTimerRemainingSeconds -= 1.0
|
|
648
|
+
persistSleepTimerState()
|
|
649
|
+
|
|
650
|
+
// Handle fade-out (before zero-check so final tick sets volume to 0)
|
|
651
|
+
if (sleepTimerFadeOutSeconds > 0 && sleepTimerRemainingSeconds < sleepTimerFadeOutSeconds) {
|
|
652
|
+
if (sleepTimerPreFadeVolume == null) {
|
|
653
|
+
sleepTimerPreFadeVolume = activePlayer?.volume ?: 1f
|
|
654
|
+
}
|
|
655
|
+
val progress = maxOf(0.0, sleepTimerRemainingSeconds / sleepTimerFadeOutSeconds).toFloat()
|
|
656
|
+
activePlayer?.volume = (sleepTimerPreFadeVolume ?: 1f) * progress
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (sleepTimerRemainingSeconds <= 0) {
|
|
660
|
+
sleepTimerRemainingSeconds = 0.0
|
|
661
|
+
activePlayer?.pause()
|
|
662
|
+
this@TrackPlayerPlaybackService.emitEvent(SleepTimerTriggeredEvent("time"))
|
|
663
|
+
// Restore volume after pausing so next playback isn't muted
|
|
664
|
+
cancelSleepTimerInternal(restoreVolume = true)
|
|
665
|
+
return
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
private fun startSleepCountdownTimer() {
|
|
670
|
+
sleepTimer?.cancel()
|
|
671
|
+
val handler = Handler(Looper.getMainLooper())
|
|
672
|
+
sleepTimer = java.util.Timer().apply {
|
|
673
|
+
scheduleAtFixedRate(object : java.util.TimerTask() {
|
|
674
|
+
override fun run() {
|
|
675
|
+
handler.post { onSleepTimerTick() }
|
|
676
|
+
}
|
|
677
|
+
}, 1000L, 1000L)
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
private fun pauseSleepCountdownTimer() {
|
|
682
|
+
sleepTimer?.cancel()
|
|
683
|
+
sleepTimer = null
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
private fun persistSleepTimerState() {
|
|
687
|
+
val prefs = getSharedPreferences(TrackPlayerModule.PLAYER_PREFS_NAME, MODE_PRIVATE)
|
|
688
|
+
val editor = prefs.edit()
|
|
689
|
+
val type = sleepTimerType
|
|
690
|
+
if (type == null) {
|
|
691
|
+
editor.remove(SLEEP_TIMER_STATE_KEY)
|
|
692
|
+
} else {
|
|
693
|
+
val json = JSONObject().apply {
|
|
694
|
+
put("type", type)
|
|
695
|
+
if (type == "time") {
|
|
696
|
+
put("remainingSeconds", sleepTimerRemainingSeconds)
|
|
697
|
+
put("fadeOutSeconds", sleepTimerFadeOutSeconds)
|
|
698
|
+
} else {
|
|
699
|
+
put("index", sleepTimerTargetIndex ?: 0)
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
editor.putString(SLEEP_TIMER_STATE_KEY, json.toString())
|
|
703
|
+
}
|
|
704
|
+
editor.apply()
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// endregion
|
|
708
|
+
|
|
557
709
|
// region Progress Sync
|
|
558
710
|
|
|
559
711
|
private fun startProgressSyncTimer() {
|
|
@@ -651,6 +803,10 @@ class TrackPlayerPlaybackService: MediaLibraryService(), MediaLibraryService.Med
|
|
|
651
803
|
const val COMMAND_SEEK_TO_PREVIOUS = "trackplayer.seek_to_previous"
|
|
652
804
|
const val COMMAND_UPDATE_PROGRESS_SYNC_HEADERS = "trackplayer.update_progress_sync_headers"
|
|
653
805
|
const val PROGRESS_SYNC_SAVED_KEY = "progress_sync_saved"
|
|
806
|
+
const val COMMAND_SLEEP_AFTER_TIME = "trackplayer.sleep_after_time"
|
|
807
|
+
const val COMMAND_SLEEP_AFTER_MEDIA_ITEM = "trackplayer.sleep_after_media_item"
|
|
808
|
+
const val COMMAND_CANCEL_SLEEP_TIMER = "trackplayer.cancel_sleep_timer"
|
|
809
|
+
const val SLEEP_TIMER_STATE_KEY = "sleep_timer_state"
|
|
654
810
|
|
|
655
811
|
@Volatile
|
|
656
812
|
var sharedCache: SimpleCache? = null
|
|
@@ -26,14 +26,18 @@ data class BrowseCategory(
|
|
|
26
26
|
@Serializable
|
|
27
27
|
data class BrowseMediaItem(
|
|
28
28
|
val mediaId: String,
|
|
29
|
-
val url: String,
|
|
29
|
+
val url: String? = null,
|
|
30
30
|
val title: String? = null,
|
|
31
31
|
val artist: String? = null,
|
|
32
32
|
val albumTitle: String? = null,
|
|
33
33
|
val artworkUrl: String? = null,
|
|
34
34
|
val duration: Double? = null,
|
|
35
35
|
val isLive: Boolean? = null,
|
|
36
|
+
val children: List<BrowseMediaItem>? = null,
|
|
36
37
|
) {
|
|
38
|
+
val isPlayable: Boolean get() = url != null
|
|
39
|
+
val isBrowsable: Boolean get() = children != null && url == null
|
|
40
|
+
|
|
37
41
|
fun toMediaItem(): MediaItem {
|
|
38
42
|
val extras = Bundle()
|
|
39
43
|
duration?.let { extras.putDouble("duration", it) }
|
|
@@ -44,16 +48,20 @@ data class BrowseMediaItem(
|
|
|
44
48
|
.setArtist(artist)
|
|
45
49
|
.setAlbumTitle(albumTitle)
|
|
46
50
|
.setArtworkUri(artworkUrl?.let { Uri.parse(it) })
|
|
47
|
-
.setIsBrowsable(
|
|
48
|
-
.setIsPlayable(
|
|
51
|
+
.setIsBrowsable(isBrowsable)
|
|
52
|
+
.setIsPlayable(isPlayable)
|
|
53
|
+
.setMediaType(if (isBrowsable) MediaMetadata.MEDIA_TYPE_FOLDER_MIXED else MediaMetadata.MEDIA_TYPE_MUSIC)
|
|
49
54
|
.setExtras(extras)
|
|
50
55
|
.build()
|
|
51
56
|
|
|
52
57
|
val builder = MediaItem.Builder()
|
|
53
58
|
.setMediaId(mediaId)
|
|
54
|
-
.setUri(url)
|
|
55
59
|
.setMediaMetadata(metadata)
|
|
56
60
|
|
|
61
|
+
if (url != null) {
|
|
62
|
+
builder.setUri(url)
|
|
63
|
+
}
|
|
64
|
+
|
|
57
65
|
if (isLive == true) {
|
|
58
66
|
builder.setLiveConfiguration(MediaItem.LiveConfiguration.Builder().build())
|
|
59
67
|
}
|
|
@@ -75,7 +83,7 @@ data class BrowseTree(
|
|
|
75
83
|
categories.find { it.mediaId == mediaId }
|
|
76
84
|
|
|
77
85
|
fun findItem(mediaId: String): BrowseMediaItem? =
|
|
78
|
-
categories.flatMap { it.items
|
|
86
|
+
categories.flatMap { findItemRecursive(it.items, mediaId) }.firstOrNull()
|
|
79
87
|
|
|
80
88
|
companion object {
|
|
81
89
|
private const val BROWSE_TREE_KEY = "browse_tree"
|
|
@@ -98,24 +106,47 @@ data class BrowseTree(
|
|
|
98
106
|
val title = catMap.getString("title") ?: continue
|
|
99
107
|
val itemsArray = catMap.getArray("items") ?: continue
|
|
100
108
|
|
|
101
|
-
val items =
|
|
102
|
-
for (j in 0 until itemsArray.size()) {
|
|
103
|
-
val itemMap = itemsArray.getMap(j) ?: continue
|
|
104
|
-
items.add(BrowseMediaItem(
|
|
105
|
-
mediaId = itemMap.getString("mediaId") ?: continue,
|
|
106
|
-
url = itemMap.getString("url") as? String ?: continue,
|
|
107
|
-
title = if (itemMap.hasKey("title")) itemMap.getString("title") else null,
|
|
108
|
-
artist = if (itemMap.hasKey("artist")) itemMap.getString("artist") else null,
|
|
109
|
-
albumTitle = if (itemMap.hasKey("albumTitle")) itemMap.getString("albumTitle") else null,
|
|
110
|
-
artworkUrl = if (itemMap.hasKey("artworkUrl")) itemMap.getString("artworkUrl") else null,
|
|
111
|
-
duration = if (itemMap.hasKey("duration")) itemMap.getDouble("duration") else null,
|
|
112
|
-
isLive = if (itemMap.hasKey("isLive")) itemMap.getBoolean("isLive") else null,
|
|
113
|
-
))
|
|
114
|
-
}
|
|
115
|
-
|
|
109
|
+
val items = parseItems(itemsArray)
|
|
116
110
|
categories.add(BrowseCategory(mediaId = mediaId, title = title, items = items))
|
|
117
111
|
}
|
|
118
112
|
return BrowseTree(categories)
|
|
119
113
|
}
|
|
114
|
+
|
|
115
|
+
private fun parseItems(data: ReadableArray): List<BrowseMediaItem> {
|
|
116
|
+
val items = mutableListOf<BrowseMediaItem>()
|
|
117
|
+
for (i in 0 until data.size()) {
|
|
118
|
+
val itemMap = data.getMap(i) ?: continue
|
|
119
|
+
val mediaId = itemMap.getString("mediaId") ?: continue
|
|
120
|
+
|
|
121
|
+
val children = if (itemMap.hasKey("children")) {
|
|
122
|
+
itemMap.getArray("children")?.let { parseItems(it) }
|
|
123
|
+
} else null
|
|
124
|
+
|
|
125
|
+
items.add(BrowseMediaItem(
|
|
126
|
+
mediaId = mediaId,
|
|
127
|
+
url = if (itemMap.hasKey("url")) itemMap.getString("url") else null,
|
|
128
|
+
title = if (itemMap.hasKey("title")) itemMap.getString("title") else null,
|
|
129
|
+
artist = if (itemMap.hasKey("artist")) itemMap.getString("artist") else null,
|
|
130
|
+
albumTitle = if (itemMap.hasKey("albumTitle")) itemMap.getString("albumTitle") else null,
|
|
131
|
+
artworkUrl = if (itemMap.hasKey("artworkUrl")) itemMap.getString("artworkUrl") else null,
|
|
132
|
+
duration = if (itemMap.hasKey("duration")) itemMap.getDouble("duration") else null,
|
|
133
|
+
isLive = if (itemMap.hasKey("isLive")) itemMap.getBoolean("isLive") else null,
|
|
134
|
+
children = children,
|
|
135
|
+
))
|
|
136
|
+
}
|
|
137
|
+
return items
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private fun findItemRecursive(items: List<BrowseMediaItem>, mediaId: String): List<BrowseMediaItem> {
|
|
141
|
+
for (item in items) {
|
|
142
|
+
if (item.mediaId == mediaId) return listOf(item)
|
|
143
|
+
val children = item.children
|
|
144
|
+
if (children != null) {
|
|
145
|
+
val found = findItemRecursive(children, mediaId)
|
|
146
|
+
if (found.isNotEmpty()) return found
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return emptyList()
|
|
150
|
+
}
|
|
120
151
|
}
|
|
121
152
|
}
|
|
@@ -21,6 +21,7 @@ enum class EmitEventType(val value: String) {
|
|
|
21
21
|
REMOTE_SKIP_FORWARD("event.remote-skip-forward"),
|
|
22
22
|
REMOTE_SKIP_BACKWARD("event.remote-skip-backward"),
|
|
23
23
|
PLAYBACK_PROGRESS_UPDATED("event.playback-progress-updated"),
|
|
24
|
+
SLEEP_TIMER_TRIGGERED("event.sleep-timer-triggered"),
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
interface EmitEvent {
|
|
@@ -116,3 +117,13 @@ data class PlaybackProgressUpdatedEvent(
|
|
|
116
117
|
)
|
|
117
118
|
}
|
|
118
119
|
}
|
|
120
|
+
|
|
121
|
+
data class SleepTimerTriggeredEvent(
|
|
122
|
+
val sleepType: String,
|
|
123
|
+
): EmitEvent {
|
|
124
|
+
override val type = EmitEventType.SLEEP_TIMER_TRIGGERED
|
|
125
|
+
|
|
126
|
+
override fun pairs(): Array<Pair<String, Any>> {
|
|
127
|
+
return arrayOf("type" to sleepType)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright (c) Double Symmetry GmbH
|
|
3
|
+
// Commercial use requires a license. See https://rntp.dev/pricing
|
|
4
|
+
//
|
|
5
|
+
|
|
6
|
+
import Foundation
|
|
7
|
+
|
|
8
|
+
class BrowseTreeStore {
|
|
9
|
+
static let shared = BrowseTreeStore()
|
|
10
|
+
|
|
11
|
+
static let didChangeNotification = Notification.Name("RNTPBrowseTreeDidChange")
|
|
12
|
+
static let nowPlayingChangedNotification = Notification.Name("RNTPNowPlayingChanged")
|
|
13
|
+
|
|
14
|
+
/// The current browse tree categories as raw dictionaries from JS.
|
|
15
|
+
private(set) var categories: [[String: Any]] = []
|
|
16
|
+
|
|
17
|
+
/// Reference to the AudioPlayer, set during setupPlayer().
|
|
18
|
+
weak var player: AudioPlayer?
|
|
19
|
+
|
|
20
|
+
/// The mediaId of the currently playing item, if any.
|
|
21
|
+
private(set) var currentMediaId: String?
|
|
22
|
+
|
|
23
|
+
private init() {}
|
|
24
|
+
|
|
25
|
+
func updateNowPlaying(mediaId: String?) {
|
|
26
|
+
currentMediaId = mediaId
|
|
27
|
+
NotificationCenter.default.post(name: Self.nowPlayingChangedNotification, object: nil)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
func update(categories: [[String: Any]]) {
|
|
31
|
+
self.categories = categories
|
|
32
|
+
NotificationCenter.default.post(name: Self.didChangeNotification, object: nil)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
func clear() {
|
|
36
|
+
self.categories = []
|
|
37
|
+
self.player = nil
|
|
38
|
+
NotificationCenter.default.post(name: Self.didChangeNotification, object: nil)
|
|
39
|
+
}
|
|
40
|
+
}
|