react-native-nitro-player 0.0.1 → 0.3.0-alpha.10
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/README.md +282 -2
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAudioDevices.kt +37 -29
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +24 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +408 -16
- package/ios/HybridAudioRoutePicker.swift +47 -46
- package/ios/HybridTrackPlayer.swift +22 -0
- package/ios/core/TrackPlayerCore.swift +538 -48
- package/lib/hooks/callbackManager.d.ts +28 -0
- package/lib/hooks/callbackManager.js +76 -0
- package/lib/hooks/index.d.ts +7 -0
- package/lib/hooks/index.js +3 -0
- package/lib/hooks/useActualQueue.d.ts +48 -0
- package/lib/hooks/useActualQueue.js +98 -0
- package/lib/hooks/useNowPlaying.d.ts +36 -0
- package/lib/hooks/useNowPlaying.js +87 -0
- package/lib/hooks/useOnChangeTrack.d.ts +33 -6
- package/lib/hooks/useOnChangeTrack.js +65 -9
- package/lib/hooks/useOnPlaybackStateChange.d.ts +32 -6
- package/lib/hooks/useOnPlaybackStateChange.js +65 -9
- package/lib/hooks/usePlaylist.d.ts +48 -0
- package/lib/hooks/usePlaylist.js +136 -0
- package/lib/index.d.ts +1 -0
- package/lib/specs/TrackPlayer.nitro.d.ts +6 -0
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +46 -9
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +5 -0
- package/nitrogen/generated/android/c++/JRepeatMode.hpp +62 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt +20 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/RepeatMode.kt +22 -0
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +9 -0
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Umbrella.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +44 -4
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +5 -0
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +64 -0
- package/nitrogen/generated/ios/swift/RepeatMode.swift +44 -0
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +5 -0
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +12 -3
- package/nitrogen/generated/shared/c++/RepeatMode.hpp +80 -0
- package/package.json +13 -12
- package/src/hooks/callbackManager.ts +96 -0
- package/src/hooks/index.ts +7 -0
- package/src/hooks/useActualQueue.ts +116 -0
- package/src/hooks/useNowPlaying.ts +97 -0
- package/src/hooks/useOnChangeTrack.ts +77 -13
- package/src/hooks/useOnPlaybackStateChange.ts +83 -13
- package/src/hooks/usePlaylist.ts +161 -0
- package/src/index.ts +1 -1
- package/src/specs/TrackPlayer.nitro.ts +7 -0
|
@@ -4,6 +4,8 @@ package com.margelo.nitro.nitroplayer.core
|
|
|
4
4
|
|
|
5
5
|
import android.content.Context
|
|
6
6
|
import android.net.Uri
|
|
7
|
+
import androidx.media3.common.AudioAttributes
|
|
8
|
+
import androidx.media3.common.C
|
|
7
9
|
import androidx.media3.common.MediaItem
|
|
8
10
|
import androidx.media3.common.MediaMetadata
|
|
9
11
|
import androidx.media3.common.Player
|
|
@@ -13,6 +15,7 @@ import com.margelo.nitro.core.NullType
|
|
|
13
15
|
import com.margelo.nitro.nitroplayer.NitroPlayerPackage
|
|
14
16
|
import com.margelo.nitro.nitroplayer.PlayerState
|
|
15
17
|
import com.margelo.nitro.nitroplayer.Reason
|
|
18
|
+
import com.margelo.nitro.nitroplayer.RepeatMode
|
|
16
19
|
import com.margelo.nitro.nitroplayer.TrackItem
|
|
17
20
|
import com.margelo.nitro.nitroplayer.TrackPlayerState
|
|
18
21
|
import com.margelo.nitro.nitroplayer.Variant_NullType_String
|
|
@@ -40,6 +43,7 @@ class TrackPlayerCore private constructor(
|
|
|
40
43
|
private var isAndroidAutoConnected: Boolean = false
|
|
41
44
|
private var androidAutoConnectionDetector: AndroidAutoConnectionDetector? = null
|
|
42
45
|
var onAndroidAutoConnectionChange: ((Boolean) -> Unit)? = null
|
|
46
|
+
private var previousMediaItem: MediaItem? = null
|
|
43
47
|
private val progressUpdateRunnable =
|
|
44
48
|
object : Runnable {
|
|
45
49
|
override fun run() {
|
|
@@ -58,6 +62,19 @@ class TrackPlayerCore private constructor(
|
|
|
58
62
|
var onSeek: ((Double, Double) -> Unit)? = null
|
|
59
63
|
var onPlaybackProgressChange: ((Double, Double, Boolean?) -> Unit)? = null
|
|
60
64
|
|
|
65
|
+
// Temporary tracks for addToUpNext and playNext
|
|
66
|
+
private var playNextStack: MutableList<TrackItem> = mutableListOf() // LIFO - last added plays first
|
|
67
|
+
private var upNextQueue: MutableList<TrackItem> = mutableListOf() // FIFO - first added plays first
|
|
68
|
+
private var currentTemporaryType: TemporaryType = TemporaryType.NONE
|
|
69
|
+
private var currentTracks: List<TrackItem> = emptyList()
|
|
70
|
+
|
|
71
|
+
// Enum to track what type of track is currently playing
|
|
72
|
+
private enum class TemporaryType {
|
|
73
|
+
NONE, // Playing from original playlist
|
|
74
|
+
PLAY_NEXT, // Currently in playNextStack
|
|
75
|
+
UP_NEXT, // Currently in upNextQueue
|
|
76
|
+
}
|
|
77
|
+
|
|
61
78
|
companion object {
|
|
62
79
|
@Volatile
|
|
63
80
|
@Suppress("ktlint:standard:property-naming")
|
|
@@ -71,25 +88,43 @@ class TrackPlayerCore private constructor(
|
|
|
71
88
|
|
|
72
89
|
init {
|
|
73
90
|
handler.post {
|
|
74
|
-
//
|
|
75
|
-
//
|
|
91
|
+
// ============================================================
|
|
92
|
+
// GAPLESS PLAYBACK CONFIGURATION
|
|
93
|
+
// ============================================================
|
|
94
|
+
// Configure LoadControl for maximum gapless playback
|
|
95
|
+
// Large buffers ensure next track is fully ready before current ends
|
|
76
96
|
val loadControl =
|
|
77
97
|
DefaultLoadControl
|
|
78
98
|
.Builder()
|
|
79
99
|
.setBufferDurationsMs(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
).setBackBuffer(
|
|
100
|
+
30_000, // MIN_BUFFER_MS: 30 seconds minimum buffer
|
|
101
|
+
120_000, // MAX_BUFFER_MS: 2 minutes maximum buffer (enables preloading next tracks)
|
|
102
|
+
2_500, // BUFFER_FOR_PLAYBACK_MS: 2.5s before playback starts
|
|
103
|
+
5_000, // BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS: 5s after rebuffer
|
|
104
|
+
).setBackBuffer(30_000, true) // Keep 30s back buffer for seamless seek-back
|
|
105
|
+
.setTargetBufferBytes(C.LENGTH_UNSET) // No size limit - prioritize time
|
|
85
106
|
.setPrioritizeTimeOverSizeThresholds(true) // Prioritize time-based buffering
|
|
86
107
|
.build()
|
|
87
108
|
|
|
109
|
+
// Configure audio attributes for optimal music playback
|
|
110
|
+
// This enables gapless audio processing in the audio pipeline
|
|
111
|
+
val audioAttributes =
|
|
112
|
+
AudioAttributes
|
|
113
|
+
.Builder()
|
|
114
|
+
.setUsage(C.USAGE_MEDIA)
|
|
115
|
+
.setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
|
|
116
|
+
.build()
|
|
117
|
+
|
|
88
118
|
player =
|
|
89
119
|
ExoPlayer
|
|
90
120
|
.Builder(context)
|
|
91
121
|
.setLoadControl(loadControl)
|
|
122
|
+
.setAudioAttributes(audioAttributes, true) // handleAudioFocus = true for gapless
|
|
123
|
+
.setHandleAudioBecomingNoisy(true) // Pause when headphones disconnected
|
|
124
|
+
.setPauseAtEndOfMediaItems(false) // Don't pause between items - key for gapless!
|
|
92
125
|
.build()
|
|
126
|
+
|
|
127
|
+
println("🎵 TrackPlayerCore: Gapless playback configured - 120s buffer, audio focus handling enabled")
|
|
93
128
|
mediaSessionManager =
|
|
94
129
|
MediaSessionManager(context, player, playlistManager).apply {
|
|
95
130
|
setTrackPlayerCore(this@TrackPlayerCore)
|
|
@@ -122,6 +157,60 @@ class TrackPlayerCore private constructor(
|
|
|
122
157
|
mediaItem: MediaItem?,
|
|
123
158
|
reason: Int,
|
|
124
159
|
) {
|
|
160
|
+
println("\n🔄 onMediaItemTransition called")
|
|
161
|
+
println(
|
|
162
|
+
" reason: ${when (reason) {
|
|
163
|
+
Player.MEDIA_ITEM_TRANSITION_REASON_AUTO -> "AUTO (track ended)"
|
|
164
|
+
Player.MEDIA_ITEM_TRANSITION_REASON_SEEK -> "SEEK"
|
|
165
|
+
Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED -> "PLAYLIST_CHANGED"
|
|
166
|
+
Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT -> "REPEAT"
|
|
167
|
+
else -> "UNKNOWN($reason)"
|
|
168
|
+
}}",
|
|
169
|
+
)
|
|
170
|
+
println(" previousMediaItem: ${previousMediaItem?.mediaId}")
|
|
171
|
+
println(" new mediaItem: ${mediaItem?.mediaId}")
|
|
172
|
+
println(" playNextStack: ${playNextStack.map { it.id }}")
|
|
173
|
+
println(" upNextQueue: ${upNextQueue.map { it.id }}")
|
|
174
|
+
|
|
175
|
+
// Remove finished track from temporary lists
|
|
176
|
+
// Handle AUTO (natural end) and SEEK (skip next) transitions
|
|
177
|
+
if ((
|
|
178
|
+
reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO ||
|
|
179
|
+
reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK
|
|
180
|
+
) &&
|
|
181
|
+
previousMediaItem != null
|
|
182
|
+
) {
|
|
183
|
+
previousMediaItem?.mediaId?.let { mediaId ->
|
|
184
|
+
val trackId = extractTrackId(mediaId)
|
|
185
|
+
println("🏁 Track finished/skipped, checking for removal: $trackId")
|
|
186
|
+
|
|
187
|
+
// Find and remove from playNext stack (like iOS does)
|
|
188
|
+
val playNextIndex = playNextStack.indexOfFirst { it.id == trackId }
|
|
189
|
+
if (playNextIndex >= 0) {
|
|
190
|
+
val track = playNextStack.removeAt(playNextIndex)
|
|
191
|
+
println(" ✅ Removed from playNext stack: ${track.title}")
|
|
192
|
+
} else {
|
|
193
|
+
// Find and remove from upNext queue
|
|
194
|
+
val upNextIndex = upNextQueue.indexOfFirst { it.id == trackId }
|
|
195
|
+
if (upNextIndex >= 0) {
|
|
196
|
+
val track = upNextQueue.removeAt(upNextIndex)
|
|
197
|
+
println(" ✅ Removed from upNext queue: ${track.title}")
|
|
198
|
+
} else {
|
|
199
|
+
println(" ℹ️ Was an original playlist track")
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
println(" ⏭️ Skipping removal (reason=$reason, prev=${previousMediaItem != null})")
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Store current item as previous for next transition
|
|
208
|
+
previousMediaItem = mediaItem
|
|
209
|
+
|
|
210
|
+
// Update temporary type for current track
|
|
211
|
+
currentTemporaryType = determineCurrentTemporaryType()
|
|
212
|
+
println(" Updated currentTemporaryType: $currentTemporaryType")
|
|
213
|
+
|
|
125
214
|
// Handle playlist switching if needed
|
|
126
215
|
mediaItem?.mediaId?.let { mediaId ->
|
|
127
216
|
if (mediaId.contains(':')) {
|
|
@@ -141,7 +230,8 @@ class TrackPlayerCore private constructor(
|
|
|
141
230
|
}
|
|
142
231
|
}
|
|
143
232
|
|
|
144
|
-
|
|
233
|
+
// Use getCurrentTrack() which handles temporary tracks properly
|
|
234
|
+
val track = getCurrentTrack()
|
|
145
235
|
if (track != null) {
|
|
146
236
|
val r =
|
|
147
237
|
when (reason) {
|
|
@@ -209,6 +299,12 @@ class TrackPlayerCore private constructor(
|
|
|
209
299
|
*/
|
|
210
300
|
fun loadPlaylist(playlistId: String) {
|
|
211
301
|
handler.post {
|
|
302
|
+
// Clear temporary tracks when loading new playlist
|
|
303
|
+
playNextStack.clear()
|
|
304
|
+
upNextQueue.clear()
|
|
305
|
+
currentTemporaryType = TemporaryType.NONE
|
|
306
|
+
println(" 🧹 Cleared temporary tracks")
|
|
307
|
+
|
|
212
308
|
val playlist = playlistManager.getPlaylist(playlistId)
|
|
213
309
|
if (playlist != null) {
|
|
214
310
|
currentPlaylistId = playlistId
|
|
@@ -295,6 +391,9 @@ class TrackPlayerCore private constructor(
|
|
|
295
391
|
}
|
|
296
392
|
|
|
297
393
|
private fun updatePlayerQueue(tracks: List<TrackItem>) {
|
|
394
|
+
// Store the original tracks
|
|
395
|
+
currentTracks = tracks
|
|
396
|
+
|
|
298
397
|
// Create MediaItems with playlist info in mediaId for Android Auto
|
|
299
398
|
val mediaItems =
|
|
300
399
|
tracks.mapIndexed { index, track ->
|
|
@@ -365,6 +464,12 @@ class TrackPlayerCore private constructor(
|
|
|
365
464
|
println("🎵 TrackPlayerCore: playSong() called - songId: $songId, fromPlaylist: $fromPlaylist")
|
|
366
465
|
|
|
367
466
|
handler.post {
|
|
467
|
+
// Clear temporary tracks when directly playing a song
|
|
468
|
+
playNextStack.clear()
|
|
469
|
+
upNextQueue.clear()
|
|
470
|
+
currentTemporaryType = TemporaryType.NONE
|
|
471
|
+
println(" 🧹 Cleared temporary tracks")
|
|
472
|
+
|
|
368
473
|
var targetPlaylistId: String? = null
|
|
369
474
|
var songIndex: Int = -1
|
|
370
475
|
|
|
@@ -463,8 +568,15 @@ class TrackPlayerCore private constructor(
|
|
|
463
568
|
|
|
464
569
|
fun skipToPrevious() {
|
|
465
570
|
handler.post {
|
|
466
|
-
|
|
571
|
+
// If playing temporary track, just seek to beginning (temps not navigable backwards)
|
|
572
|
+
if (currentTemporaryType != TemporaryType.NONE) {
|
|
573
|
+
println("🔄 TrackPlayerCore: Playing temporary track - seeking to beginning")
|
|
574
|
+
player.seekTo(0)
|
|
575
|
+
} else if (player.hasPreviousMediaItem()) {
|
|
467
576
|
player.seekToPreviousMediaItem()
|
|
577
|
+
} else {
|
|
578
|
+
// Already at first track, seek to beginning
|
|
579
|
+
player.seekTo(0)
|
|
468
580
|
}
|
|
469
581
|
}
|
|
470
582
|
}
|
|
@@ -476,6 +588,21 @@ class TrackPlayerCore private constructor(
|
|
|
476
588
|
}
|
|
477
589
|
}
|
|
478
590
|
|
|
591
|
+
fun setRepeatMode(mode: RepeatMode): Boolean {
|
|
592
|
+
println("🔁 TrackPlayerCore: setRepeatMode called with mode: $mode")
|
|
593
|
+
handler.post {
|
|
594
|
+
val exoRepeatMode =
|
|
595
|
+
when (mode) {
|
|
596
|
+
RepeatMode.OFF -> Player.REPEAT_MODE_OFF
|
|
597
|
+
RepeatMode.TRACK -> Player.REPEAT_MODE_ONE
|
|
598
|
+
RepeatMode.PLAYLIST -> Player.REPEAT_MODE_ALL
|
|
599
|
+
}
|
|
600
|
+
player.repeatMode = exoRepeatMode
|
|
601
|
+
println("🔁 TrackPlayerCore: ExoPlayer repeat mode set to: $exoRepeatMode")
|
|
602
|
+
}
|
|
603
|
+
return true
|
|
604
|
+
}
|
|
605
|
+
|
|
479
606
|
fun getState(): PlayerState {
|
|
480
607
|
// Check if we're already on the main thread
|
|
481
608
|
if (android.os.Looper.myLooper() == handler.looper) {
|
|
@@ -506,13 +633,8 @@ class TrackPlayerCore private constructor(
|
|
|
506
633
|
|
|
507
634
|
private fun getStateInternal(): PlayerState =
|
|
508
635
|
if (::player.isInitialized) {
|
|
509
|
-
|
|
510
|
-
val track =
|
|
511
|
-
if (currentMediaItem != null) {
|
|
512
|
-
findTrack(currentMediaItem)
|
|
513
|
-
} else {
|
|
514
|
-
null
|
|
515
|
-
}
|
|
636
|
+
// Use getCurrentTrack() which handles temporary tracks properly
|
|
637
|
+
val track = getCurrentTrack()
|
|
516
638
|
|
|
517
639
|
// Convert nullable TrackItem to Variant_NullType_TrackItem
|
|
518
640
|
val currentTrack: Variant_NullType_TrackItem? =
|
|
@@ -589,12 +711,45 @@ class TrackPlayerCore private constructor(
|
|
|
589
711
|
fun getCurrentTrack(): TrackItem? {
|
|
590
712
|
if (!::player.isInitialized) return null
|
|
591
713
|
val currentMediaItem = player.currentMediaItem ?: return null
|
|
714
|
+
|
|
715
|
+
// If playing a temporary track, return that
|
|
716
|
+
if (currentTemporaryType != TemporaryType.NONE) {
|
|
717
|
+
val trackId = extractTrackId(currentMediaItem.mediaId)
|
|
718
|
+
|
|
719
|
+
when (currentTemporaryType) {
|
|
720
|
+
TemporaryType.PLAY_NEXT -> {
|
|
721
|
+
return playNextStack.firstOrNull { it.id == trackId }
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
TemporaryType.UP_NEXT -> {
|
|
725
|
+
return upNextQueue.firstOrNull { it.id == trackId }
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
else -> {}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Otherwise return from original playlist
|
|
592
733
|
return findTrack(currentMediaItem)
|
|
593
734
|
}
|
|
594
735
|
|
|
736
|
+
private fun extractTrackId(mediaId: String): String =
|
|
737
|
+
if (mediaId.contains(':')) {
|
|
738
|
+
// Format: "playlistId:trackId"
|
|
739
|
+
mediaId.substring(mediaId.indexOf(':') + 1)
|
|
740
|
+
} else {
|
|
741
|
+
mediaId
|
|
742
|
+
}
|
|
743
|
+
|
|
595
744
|
// Public method to play from a specific index (for Android Auto)
|
|
596
745
|
fun playFromIndex(index: Int) {
|
|
597
746
|
handler.post {
|
|
747
|
+
// Clear temporary tracks when jumping to specific index
|
|
748
|
+
playNextStack.clear()
|
|
749
|
+
upNextQueue.clear()
|
|
750
|
+
currentTemporaryType = TemporaryType.NONE
|
|
751
|
+
println(" 🧹 Cleared temporary tracks")
|
|
752
|
+
|
|
598
753
|
if (::player.isInitialized && index >= 0 && index < player.mediaItemCount) {
|
|
599
754
|
player.seekToDefaultPosition(index)
|
|
600
755
|
player.playWhenReady = true
|
|
@@ -602,6 +757,163 @@ class TrackPlayerCore private constructor(
|
|
|
602
757
|
}
|
|
603
758
|
}
|
|
604
759
|
|
|
760
|
+
// MARK: - Temporary Track Management
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Add a track to the up-next queue (FIFO - first added plays first)
|
|
764
|
+
* Track will be inserted after currently playing track and any playNext tracks
|
|
765
|
+
*/
|
|
766
|
+
fun addToUpNext(trackId: String) {
|
|
767
|
+
handler.post {
|
|
768
|
+
println("📋 TrackPlayerCore: addToUpNext($trackId)")
|
|
769
|
+
|
|
770
|
+
// Find the track from current playlist or all playlists
|
|
771
|
+
val track = findTrackById(trackId)
|
|
772
|
+
if (track == null) {
|
|
773
|
+
println("❌ TrackPlayerCore: Track $trackId not found")
|
|
774
|
+
return@post
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Add to end of upNext queue (FIFO)
|
|
778
|
+
upNextQueue.add(track)
|
|
779
|
+
println(" ✅ Added '${track.title}' to upNext queue (position: ${upNextQueue.size})")
|
|
780
|
+
|
|
781
|
+
// Rebuild the player queue if actively playing
|
|
782
|
+
if (::player.isInitialized && player.currentMediaItem != null) {
|
|
783
|
+
rebuildQueueFromCurrentPosition()
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Add a track to play next (LIFO - last added plays first)
|
|
790
|
+
* Track will be inserted immediately after currently playing track
|
|
791
|
+
*/
|
|
792
|
+
fun playNext(trackId: String) {
|
|
793
|
+
handler.post {
|
|
794
|
+
println("⏭️ TrackPlayerCore: playNext($trackId)")
|
|
795
|
+
|
|
796
|
+
// Find the track from current playlist or all playlists
|
|
797
|
+
val track = findTrackById(trackId)
|
|
798
|
+
if (track == null) {
|
|
799
|
+
println("❌ TrackPlayerCore: Track $trackId not found")
|
|
800
|
+
return@post
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// Insert at beginning of playNext stack (LIFO)
|
|
804
|
+
playNextStack.add(0, track)
|
|
805
|
+
println(" ✅ Added '${track.title}' to playNext stack (position: 1)")
|
|
806
|
+
|
|
807
|
+
// Rebuild the player queue if actively playing
|
|
808
|
+
if (::player.isInitialized && player.currentMediaItem != null) {
|
|
809
|
+
rebuildQueueFromCurrentPosition()
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Rebuild the ExoPlayer queue from current position with temporary tracks
|
|
816
|
+
* Order: [current] + [playNext stack] + [upNext queue] + [remaining original]
|
|
817
|
+
*/
|
|
818
|
+
private fun rebuildQueueFromCurrentPosition() {
|
|
819
|
+
if (!::player.isInitialized) return
|
|
820
|
+
|
|
821
|
+
println("\n🔄 TrackPlayerCore: REBUILDING QUEUE FROM CURRENT POSITION")
|
|
822
|
+
println(" currentIndex: ${player.currentMediaItemIndex}")
|
|
823
|
+
println(" currentMediaItem: ${player.currentMediaItem?.mediaId}")
|
|
824
|
+
println(" playNextStack (${playNextStack.size}): ${playNextStack.map { "${it.id}:${it.title}" }}")
|
|
825
|
+
println(" upNextQueue (${upNextQueue.size}): ${upNextQueue.map { "${it.id}:${it.title}" }}")
|
|
826
|
+
|
|
827
|
+
val currentIndex = player.currentMediaItemIndex
|
|
828
|
+
if (currentIndex < 0) return
|
|
829
|
+
|
|
830
|
+
// Build new queue order:
|
|
831
|
+
// [playNext stack] + [upNext queue] + [remaining original tracks]
|
|
832
|
+
val newQueueTracks = mutableListOf<TrackItem>()
|
|
833
|
+
|
|
834
|
+
// Add playNext stack (LIFO - most recently added plays first)
|
|
835
|
+
// Stack is already in correct order since we insert at position 0
|
|
836
|
+
newQueueTracks.addAll(playNextStack)
|
|
837
|
+
|
|
838
|
+
// Add upNext queue (in order, FIFO)
|
|
839
|
+
newQueueTracks.addAll(upNextQueue)
|
|
840
|
+
|
|
841
|
+
// Add remaining original tracks
|
|
842
|
+
if (currentIndex + 1 < currentTracks.size) {
|
|
843
|
+
val remaining = currentTracks.subList(currentIndex + 1, currentTracks.size)
|
|
844
|
+
println(" remaining original (${remaining.size}): ${remaining.map { it.id }}")
|
|
845
|
+
newQueueTracks.addAll(remaining)
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
println(" New queue total: ${newQueueTracks.size} tracks")
|
|
849
|
+
println(" Queue order: ${newQueueTracks.map { it.id }}")
|
|
850
|
+
|
|
851
|
+
// Create MediaItems for new tracks
|
|
852
|
+
val playlistId = currentPlaylistId ?: ""
|
|
853
|
+
val newMediaItems =
|
|
854
|
+
newQueueTracks.map { track ->
|
|
855
|
+
val mediaId = if (playlistId.isNotEmpty()) "$playlistId:${track.id}" else track.id
|
|
856
|
+
println(" Creating MediaItem: mediaId=$mediaId, title=${track.title}")
|
|
857
|
+
track.toMediaItem(mediaId)
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Remove all items after current
|
|
861
|
+
val removedCount = player.mediaItemCount - currentIndex - 1
|
|
862
|
+
println(" Removing $removedCount items after current")
|
|
863
|
+
while (player.mediaItemCount > currentIndex + 1) {
|
|
864
|
+
player.removeMediaItem(currentIndex + 1)
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// Add new items
|
|
868
|
+
player.addMediaItems(newMediaItems)
|
|
869
|
+
|
|
870
|
+
println(" ✅ Queue rebuilt. Player now has ${player.mediaItemCount} items")
|
|
871
|
+
for (i in 0 until player.mediaItemCount) {
|
|
872
|
+
println(" [$i]: ${player.getMediaItemAt(i).mediaId}")
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* Find a track by ID from current playlist or all playlists
|
|
878
|
+
*/
|
|
879
|
+
private fun findTrackById(trackId: String): TrackItem? {
|
|
880
|
+
// First check current playlist
|
|
881
|
+
currentTracks.find { it.id == trackId }?.let { return it }
|
|
882
|
+
|
|
883
|
+
// Then check all playlists
|
|
884
|
+
val allPlaylists = playlistManager.getAllPlaylists()
|
|
885
|
+
for (playlist in allPlaylists) {
|
|
886
|
+
playlist.tracks.find { it.id == trackId }?.let { return it }
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
return null
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/**
|
|
893
|
+
* Determine what type of track is currently playing
|
|
894
|
+
*/
|
|
895
|
+
private fun determineCurrentTemporaryType(): TemporaryType {
|
|
896
|
+
val currentItem = player.currentMediaItem ?: return TemporaryType.NONE
|
|
897
|
+
val trackId =
|
|
898
|
+
if (currentItem.mediaId.contains(':')) {
|
|
899
|
+
currentItem.mediaId.substring(currentItem.mediaId.indexOf(':') + 1)
|
|
900
|
+
} else {
|
|
901
|
+
currentItem.mediaId
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Check if in playNext stack
|
|
905
|
+
if (playNextStack.any { it.id == trackId }) {
|
|
906
|
+
return TemporaryType.PLAY_NEXT
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// Check if in upNext queue
|
|
910
|
+
if (upNextQueue.any { it.id == trackId }) {
|
|
911
|
+
return TemporaryType.UP_NEXT
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
return TemporaryType.NONE
|
|
915
|
+
}
|
|
916
|
+
|
|
605
917
|
// Clean up resources
|
|
606
918
|
fun destroy() {
|
|
607
919
|
handler.post {
|
|
@@ -636,4 +948,84 @@ class TrackPlayerCore private constructor(
|
|
|
636
948
|
NitroPlayerMediaBrowserService.getInstance()?.onPlaylistsUpdated()
|
|
637
949
|
}
|
|
638
950
|
}
|
|
951
|
+
|
|
952
|
+
// Set volume (0-100 range, converted to 0.0-1.0 for ExoPlayer)
|
|
953
|
+
fun setVolume(volume: Double): Boolean =
|
|
954
|
+
if (::player.isInitialized) {
|
|
955
|
+
handler.post {
|
|
956
|
+
// Clamp volume to 0-100 range
|
|
957
|
+
val clampedVolume = volume.coerceIn(0.0, 100.0)
|
|
958
|
+
// Convert to 0.0-1.0 range for ExoPlayer
|
|
959
|
+
val normalizedVolume = (clampedVolume / 100.0).toFloat()
|
|
960
|
+
player.volume = normalizedVolume
|
|
961
|
+
println("🔊 TrackPlayerCore: Volume set to $clampedVolume% (normalized: $normalizedVolume)")
|
|
962
|
+
}
|
|
963
|
+
true
|
|
964
|
+
} else {
|
|
965
|
+
println("⚠️ TrackPlayerCore: Cannot set volume - player not initialized")
|
|
966
|
+
false
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/**
|
|
970
|
+
* Get the actual queue with temporary tracks
|
|
971
|
+
* Returns: [original_before_current] + [current] + [playNext_stack] + [upNext_queue] + [original_after_current]
|
|
972
|
+
*/
|
|
973
|
+
fun getActualQueue(): List<TrackItem> {
|
|
974
|
+
// Check if we're already on the main thread
|
|
975
|
+
if (android.os.Looper.myLooper() == handler.looper) {
|
|
976
|
+
return getActualQueueInternal()
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// Use CountDownLatch to wait for the result on the main thread
|
|
980
|
+
val latch = CountDownLatch(1)
|
|
981
|
+
var result: List<TrackItem>? = null
|
|
982
|
+
|
|
983
|
+
handler.post {
|
|
984
|
+
try {
|
|
985
|
+
result = getActualQueueInternal()
|
|
986
|
+
} finally {
|
|
987
|
+
latch.countDown()
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
try {
|
|
992
|
+
// Wait up to 5 seconds for the result
|
|
993
|
+
latch.await(5, TimeUnit.SECONDS)
|
|
994
|
+
} catch (e: InterruptedException) {
|
|
995
|
+
println("⚠️ TrackPlayerCore: Interrupted while waiting for actual queue")
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
return result ?: emptyList()
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
private fun getActualQueueInternal(): List<TrackItem> {
|
|
1002
|
+
val queue = mutableListOf<TrackItem>()
|
|
1003
|
+
|
|
1004
|
+
if (!::player.isInitialized) return emptyList()
|
|
1005
|
+
|
|
1006
|
+
val currentIndex = player.currentMediaItemIndex
|
|
1007
|
+
if (currentIndex < 0) return emptyList()
|
|
1008
|
+
|
|
1009
|
+
// Add tracks before current (original playlist)
|
|
1010
|
+
if (currentIndex > 0 && currentIndex <= currentTracks.size) {
|
|
1011
|
+
queue.addAll(currentTracks.subList(0, currentIndex))
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// Add current track
|
|
1015
|
+
getCurrentTrack()?.let { queue.add(it) }
|
|
1016
|
+
|
|
1017
|
+
// Add playNext stack (LIFO - most recently added plays first)
|
|
1018
|
+
// Stack is already in correct order since we insert at position 0
|
|
1019
|
+
queue.addAll(playNextStack)
|
|
1020
|
+
|
|
1021
|
+
// Add upNext queue (in order, FIFO)
|
|
1022
|
+
queue.addAll(upNextQueue)
|
|
1023
|
+
|
|
1024
|
+
// Add remaining original tracks
|
|
1025
|
+
if (currentIndex + 1 < currentTracks.size) {
|
|
1026
|
+
queue.addAll(currentTracks.subList(currentIndex + 1, currentTracks.size))
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
return queue
|
|
1030
|
+
}
|
|
639
1031
|
}
|
|
@@ -1,53 +1,54 @@
|
|
|
1
|
-
import Foundation
|
|
2
|
-
import UIKit
|
|
3
1
|
import AVKit
|
|
2
|
+
import Foundation
|
|
4
3
|
import NitroModules
|
|
4
|
+
import UIKit
|
|
5
5
|
|
|
6
6
|
class HybridAudioRoutePicker: HybridAudioRoutePickerSpec {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
// Remove the view after a short delay
|
|
47
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
|
48
|
-
routePickerView.removeFromSuperview()
|
|
49
|
-
}
|
|
7
|
+
|
|
8
|
+
// Approximate memory footprint of this instance's reference
|
|
9
|
+
private func getSizeOf(_ object: AnyObject) -> Int {
|
|
10
|
+
// For class instances, MemoryLayout reports the size of the reference (pointer)
|
|
11
|
+
return MemoryLayout.size(ofValue: object)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
var memorySize: Int {
|
|
15
|
+
return getSizeOf(self)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
func showRoutePicker() throws {
|
|
19
|
+
DispatchQueue.main.async {
|
|
20
|
+
// Create AVRoutePickerView
|
|
21
|
+
let routePickerView = AVRoutePickerView()
|
|
22
|
+
routePickerView.frame = CGRect(x: 0, y: 0, width: 44, height: 44)
|
|
23
|
+
routePickerView.tintColor = .systemBlue
|
|
24
|
+
routePickerView.activeTintColor = .systemBlue
|
|
25
|
+
|
|
26
|
+
// Get the key window
|
|
27
|
+
guard
|
|
28
|
+
let window = UIApplication.shared.connectedScenes
|
|
29
|
+
.compactMap({ $0 as? UIWindowScene })
|
|
30
|
+
.flatMap({ $0.windows })
|
|
31
|
+
.first(where: { $0.isKeyWindow })
|
|
32
|
+
else {
|
|
33
|
+
print("HybridAudioRoutePicker: Could not find key window")
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Add the route picker to the window temporarily
|
|
38
|
+
window.addSubview(routePickerView)
|
|
39
|
+
|
|
40
|
+
// Trigger the route picker button programmatically
|
|
41
|
+
for view in routePickerView.subviews {
|
|
42
|
+
if let button = view as? UIButton {
|
|
43
|
+
button.sendActions(for: .touchUpInside)
|
|
44
|
+
break
|
|
50
45
|
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Remove the view after a short delay
|
|
49
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
|
50
|
+
routePickerView.removeFromSuperview()
|
|
51
|
+
}
|
|
51
52
|
}
|
|
53
|
+
}
|
|
52
54
|
}
|
|
53
|
-
|
|
@@ -48,10 +48,26 @@ final class HybridTrackPlayer: HybridTrackPlayerSpec {
|
|
|
48
48
|
core.seek(position: position)
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
func addToUpNext(trackId: String) throws {
|
|
52
|
+
core.addToUpNext(trackId: trackId)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
func playNext(trackId: String) throws {
|
|
56
|
+
core.playNext(trackId: trackId)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
func getActualQueue() throws -> [TrackItem] {
|
|
60
|
+
return core.getActualQueue()
|
|
61
|
+
}
|
|
62
|
+
|
|
51
63
|
func getState() throws -> PlayerState {
|
|
52
64
|
return core.getState()
|
|
53
65
|
}
|
|
54
66
|
|
|
67
|
+
func setRepeatMode(mode: RepeatMode) throws -> Bool {
|
|
68
|
+
return core.setRepeatMode(mode: mode)
|
|
69
|
+
}
|
|
70
|
+
|
|
55
71
|
// MARK: - Configuration
|
|
56
72
|
|
|
57
73
|
func configure(config: PlayerConfig) throws {
|
|
@@ -97,4 +113,10 @@ final class HybridTrackPlayer: HybridTrackPlayerSpec {
|
|
|
97
113
|
func isAndroidAutoConnected() throws -> Bool {
|
|
98
114
|
return false
|
|
99
115
|
}
|
|
116
|
+
|
|
117
|
+
// MARK: - Volume Control
|
|
118
|
+
|
|
119
|
+
func setVolume(volume: Double) throws -> Bool {
|
|
120
|
+
return core.setVolume(volume: volume)
|
|
121
|
+
}
|
|
100
122
|
}
|