react-native-nitro-player 1.7.0-alpha.3 → 1.7.0-alpha.5
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 +9 -0
- package/android/src/main/AndroidManifest.xml +8 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrary.kt +51 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridPlayerQueue.kt +45 -7
- package/android/src/main/java/com/margelo/nitro/nitroplayer/connection/AndroidAutoConnectionDetector.kt +2 -2
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/ExoPlayerCore.kt +17 -2
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/NitroPlayerLogger.kt +2 -2
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerAndroidAuto.kt +41 -31
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerAudioFocus.kt +65 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCast.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +92 -15
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerListener.kt +12 -17
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerNotify.kt +5 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerPlayback.kt +39 -13
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueue.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueueBuild.kt +25 -4
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerRecovery.kt +97 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerSetup.kt +0 -5
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerUrlLoader.kt +22 -5
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadDatabase.kt +3 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/TrackItemJson.kt +6 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/AudioFocusController.kt +87 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/CastSessionController.kt +2 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/ExoPlayerBuilder.kt +3 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaBrowserService.kt +96 -45
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaLibrary.kt +2 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaLibraryManager.kt +147 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaLibraryParser.kt +3 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionCallbackFactory.kt +102 -24
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionManager.kt +51 -3
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/PlaybackService.kt +82 -1
- package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +75 -0
- package/ios/cast/CastQueueDiff.swift +58 -0
- package/ios/cast/CastQueueReconciler.swift +228 -0
- package/ios/core/CastSessionManager.swift +215 -31
- package/ios/core/TrackPlayerCast.swift +96 -65
- package/ios/core/TrackPlayerCore.swift +31 -0
- package/ios/core/TrackPlayerListener.swift +19 -3
- package/ios/core/TrackPlayerPlayback.swift +12 -10
- package/ios/core/TrackPlayerQueue.swift +10 -2
- package/ios/core/TrackPlayerQueueBuild.swift +21 -9
- package/ios/core/TrackPlayerTempQueue.swift +27 -15
- package/ios/core/TrackPlayerUrlLoader.swift +6 -2
- package/ios/playlist/PlaylistManager.swift +60 -1
- package/ios/queue/HybridPlayerQueue.swift +40 -13
- package/lib/hooks/callbackManager.d.ts +2 -0
- package/lib/hooks/callbackManager.js +23 -8
- package/lib/index.js +5 -1
- package/lib/specs/AndroidAutoMediaLibrary.nitro.d.ts +12 -0
- package/lib/specs/TrackPlayer.nitro.d.ts +5 -1
- package/lib/types/AndroidAutoMediaLibrary.d.ts +11 -0
- package/lib/types/PlayerQueue.d.ts +14 -0
- package/lib/utils/androidAutoMediaLibrary.d.ts +13 -1
- package/lib/utils/androidAutoMediaLibrary.js +23 -0
- package/nitrogen/generated/android/NitroPlayerOnLoad.cpp +6 -0
- package/nitrogen/generated/android/c++/JAndroidAudioFocusMode.hpp +61 -0
- package/nitrogen/generated/android/c++/JFunc_void.hpp +75 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__string.hpp +76 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__string_std__string.hpp +76 -0
- package/nitrogen/generated/android/c++/JHybridAndroidAutoMediaLibrarySpec.cpp +29 -0
- package/nitrogen/generated/android/c++/JHybridAndroidAutoMediaLibrarySpec.hpp +6 -0
- package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.cpp +43 -0
- package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.hpp +2 -0
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +4 -0
- package/nitrogen/generated/android/c++/JPlayerConfig.hpp +9 -3
- package/nitrogen/generated/android/c++/JShufflePlaylistOptions.hpp +57 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/AndroidAudioFocusMode.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__string.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__string_std__string.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrarySpec.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridPlayerQueueSpec.kt +8 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PlayerConfig.kt +6 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/ShufflePlaylistOptions.kt +38 -0
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +36 -0
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Umbrella.hpp +6 -0
- package/nitrogen/generated/ios/c++/HybridPlayerQueueSpecSwift.hpp +19 -0
- package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +3 -0
- package/nitrogen/generated/ios/swift/AndroidAudioFocusMode.swift +44 -0
- package/nitrogen/generated/ios/swift/HybridPlayerQueueSpec.swift +2 -0
- package/nitrogen/generated/ios/swift/HybridPlayerQueueSpec_cxx.swift +38 -0
- package/nitrogen/generated/ios/swift/PlayerConfig.swift +12 -1
- package/nitrogen/generated/ios/swift/ShufflePlaylistOptions.swift +42 -0
- package/nitrogen/generated/shared/c++/AndroidAudioFocusMode.hpp +80 -0
- package/nitrogen/generated/shared/c++/HybridAndroidAutoMediaLibrarySpec.cpp +6 -0
- package/nitrogen/generated/shared/c++/HybridAndroidAutoMediaLibrarySpec.hpp +7 -0
- package/nitrogen/generated/shared/c++/HybridPlayerQueueSpec.cpp +2 -0
- package/nitrogen/generated/shared/c++/HybridPlayerQueueSpec.hpp +5 -0
- package/nitrogen/generated/shared/c++/PlayerConfig.hpp +9 -3
- package/nitrogen/generated/shared/c++/ShufflePlaylistOptions.hpp +83 -0
- package/package.json +1 -1
- package/src/hooks/callbackManager.ts +25 -10
- package/src/index.ts +9 -1
- package/src/specs/AndroidAutoMediaLibrary.nitro.ts +18 -0
- package/src/specs/TrackPlayer.nitro.ts +11 -0
- package/src/types/AndroidAutoMediaLibrary.ts +24 -0
- package/src/types/PlayerQueue.ts +17 -0
- package/src/utils/androidAutoMediaLibrary.ts +48 -1
package/README.md
CHANGED
|
@@ -82,6 +82,8 @@ Mutations return **`Promise<void>`** (or **`Promise<string>`** for `createPlayli
|
|
|
82
82
|
| `addTracksToPlaylist(pid, tracks)` | Both | **Async**. Adds multiple tracks to a playlist. |
|
|
83
83
|
| `removeTrackFromPlaylist(pid, tid)` | Both | **Async**. Removes a track from a playlist. |
|
|
84
84
|
| `reorderTrackInPlaylist(pid, tid, idx)` | Both | **Async**. Moves a track to a new position. |
|
|
85
|
+
| `removeTracksFromPlaylist(pid, tids)` | Both | **Async**. Removes several tracks in one call. |
|
|
86
|
+
| `shufflePlaylist(pid, options?)` | Both | **Async**. Shuffles a playlist without stopping playback.|
|
|
85
87
|
|
|
86
88
|
### Platform-Specific APIs
|
|
87
89
|
|
|
@@ -940,6 +942,13 @@ PlayerQueue.removeTrackFromPlaylist(playlistId, trackId)
|
|
|
940
942
|
// Reorder tracks
|
|
941
943
|
PlayerQueue.reorderTrackInPlaylist(playlistId, trackId, newIndex)
|
|
942
944
|
|
|
945
|
+
// Remove several tracks in one native call
|
|
946
|
+
PlayerQueue.removeTracksFromPlaylist(playlistId, [trackId1, trackId2])
|
|
947
|
+
|
|
948
|
+
// Shuffle in place — the playing track keeps playing, no shuffle mode to turn off
|
|
949
|
+
PlayerQueue.shufflePlaylist(playlistId)
|
|
950
|
+
PlayerQueue.shufflePlaylist(playlistId, { keepCurrentTrackFirst: false })
|
|
951
|
+
|
|
943
952
|
// Delete a playlist
|
|
944
953
|
PlayerQueue.deletePlaylist(playlistId)
|
|
945
954
|
```
|
|
@@ -18,6 +18,14 @@
|
|
|
18
18
|
android:name="android.hardware.audio.output"
|
|
19
19
|
android:required="false" />
|
|
20
20
|
|
|
21
|
+
<!-- Visibility of Android Auto's connection-state provider; without it the car never reads as connected -->
|
|
22
|
+
<queries>
|
|
23
|
+
<provider
|
|
24
|
+
android:name="androidx.car.app.connection.provider"
|
|
25
|
+
android:authorities="androidx.car.app.connection"
|
|
26
|
+
tools:ignore="MissingClass" />
|
|
27
|
+
</queries>
|
|
28
|
+
|
|
21
29
|
<application>
|
|
22
30
|
<!-- Google Cast: supplies CastOptions (receiver app ID) to the Cast SDK.
|
|
23
31
|
Apps shipping their own OptionsProvider must override with tools:replace. -->
|
package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrary.kt
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
package com.margelo.nitro.nitroplayer
|
|
2
2
|
|
|
3
3
|
import androidx.annotation.Keep
|
|
4
|
+
import androidx.media3.common.util.UnstableApi
|
|
5
|
+
import androidx.media3.session.CommandButton
|
|
4
6
|
import com.facebook.proguard.annotations.DoNotStrip
|
|
5
7
|
import com.margelo.nitro.NitroModules
|
|
6
8
|
import com.margelo.nitro.core.Promise
|
|
7
9
|
import com.margelo.nitro.nitroplayer.core.TrackPlayerCore
|
|
8
10
|
import com.margelo.nitro.nitroplayer.core.clearAndroidAutoMediaLibrary
|
|
9
11
|
import com.margelo.nitro.nitroplayer.core.setAndroidAutoMediaLibrary
|
|
12
|
+
import com.margelo.nitro.nitroplayer.media.MediaSessionManager
|
|
13
|
+
import com.margelo.nitro.nitroplayer.media.NitroPlayerMediaBrowserService
|
|
14
|
+
import org.json.JSONArray
|
|
10
15
|
|
|
11
16
|
@DoNotStrip
|
|
12
17
|
@Keep
|
|
18
|
+
@UnstableApi
|
|
13
19
|
class HybridAndroidAutoMediaLibrary : HybridAndroidAutoMediaLibrarySpec() {
|
|
14
20
|
private val core: TrackPlayerCore
|
|
15
21
|
|
|
@@ -23,4 +29,49 @@ class HybridAndroidAutoMediaLibrary : HybridAndroidAutoMediaLibrarySpec() {
|
|
|
23
29
|
override fun setMediaLibrary(libraryJson: String): Promise<Unit> = Promise.async { core.setAndroidAutoMediaLibrary(libraryJson) }
|
|
24
30
|
|
|
25
31
|
override fun clearMediaLibrary(): Promise<Unit> = Promise.async { core.clearAndroidAutoMediaLibrary() }
|
|
32
|
+
|
|
33
|
+
override fun onLoadChildren(callback: (requestId: String, parentId: String) -> Unit) {
|
|
34
|
+
core.mediaLibraryManager.childrenRequester = callback
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
override fun onSearch(callback: (requestId: String, query: String) -> Unit) {
|
|
38
|
+
core.mediaLibraryManager.searchRequester = callback
|
|
39
|
+
NitroPlayerMediaBrowserService.setSearchSupported(core.context, true)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
override fun resolveRequest(
|
|
43
|
+
requestId: String,
|
|
44
|
+
itemsJson: String,
|
|
45
|
+
) = core.mediaLibraryManager.resolve(requestId, itemsJson)
|
|
46
|
+
|
|
47
|
+
override fun setSessionButtons(buttonsJson: String) {
|
|
48
|
+
val array = JSONArray(buttonsJson)
|
|
49
|
+
val buttons =
|
|
50
|
+
(0 until array.length()).mapNotNull { i ->
|
|
51
|
+
val button = array.getJSONObject(i)
|
|
52
|
+
val icon = ICONS[button.optString("icon")] ?: return@mapNotNull null
|
|
53
|
+
MediaSessionManager.SessionButton(button.getString("action"), button.optString("title"), icon)
|
|
54
|
+
}
|
|
55
|
+
core.enqueue { core.mediaSessionManager?.setSessionButtons(buttons) }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
override fun onSessionButtonPress(callback: (action: String) -> Unit) {
|
|
59
|
+
core.enqueue { core.mediaSessionManager?.onSessionButtonPress = callback }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
override fun onPlaybackResumption(callback: () -> Unit) = core.mediaLibraryManager.setResumptionRequester(callback)
|
|
63
|
+
|
|
64
|
+
private companion object {
|
|
65
|
+
val ICONS =
|
|
66
|
+
mapOf(
|
|
67
|
+
"heart" to CommandButton.ICON_HEART_UNFILLED,
|
|
68
|
+
"heart_filled" to CommandButton.ICON_HEART_FILLED,
|
|
69
|
+
"star" to CommandButton.ICON_STAR_UNFILLED,
|
|
70
|
+
"star_filled" to CommandButton.ICON_STAR_FILLED,
|
|
71
|
+
"thumb_up" to CommandButton.ICON_THUMB_UP_UNFILLED,
|
|
72
|
+
"thumb_up_filled" to CommandButton.ICON_THUMB_UP_FILLED,
|
|
73
|
+
"plus" to CommandButton.ICON_PLUS,
|
|
74
|
+
"check" to CommandButton.ICON_CHECK_CIRCLE_FILLED,
|
|
75
|
+
)
|
|
76
|
+
}
|
|
26
77
|
}
|
|
@@ -94,16 +94,11 @@ class HybridPlayerQueue : HybridPlayerQueueSpec() {
|
|
|
94
94
|
core.updatePlaylist(playlistId)
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
/** Same semantics as the batch form: unknown ids are skipped, only a missing playlist rejects. */
|
|
97
98
|
override fun removeTrackFromPlaylist(
|
|
98
99
|
playlistId: String,
|
|
99
100
|
trackId: String,
|
|
100
|
-
): Promise<Unit> =
|
|
101
|
-
Promise.async {
|
|
102
|
-
if (!playlistManager.removeTrackFromPlaylist(playlistId, trackId)) {
|
|
103
|
-
throw IllegalArgumentException("Track $trackId not found in playlist $playlistId")
|
|
104
|
-
}
|
|
105
|
-
core.updatePlaylist(playlistId)
|
|
106
|
-
}
|
|
101
|
+
): Promise<Unit> = removeTracksFromPlaylist(playlistId, arrayOf(trackId))
|
|
107
102
|
|
|
108
103
|
override fun reorderTrackInPlaylist(
|
|
109
104
|
playlistId: String,
|
|
@@ -117,6 +112,49 @@ class HybridPlayerQueue : HybridPlayerQueueSpec() {
|
|
|
117
112
|
core.updatePlaylist(playlistId)
|
|
118
113
|
}
|
|
119
114
|
|
|
115
|
+
/** On the player looper: the live queue must reflect the removal before the promise resolves. */
|
|
116
|
+
override fun removeTracksFromPlaylist(
|
|
117
|
+
playlistId: String,
|
|
118
|
+
trackIds: Array<String>,
|
|
119
|
+
): Promise<Unit> {
|
|
120
|
+
val promise = Promise<Unit>()
|
|
121
|
+
core.enqueue {
|
|
122
|
+
val removed = playlistManager.removeTracksFromPlaylist(playlistId, trackIds.toList())
|
|
123
|
+
if (removed == null) {
|
|
124
|
+
promise.reject(IllegalArgumentException("Playlist not found: $playlistId"))
|
|
125
|
+
return@enqueue
|
|
126
|
+
}
|
|
127
|
+
if (removed > 0 && core.getCurrentPlaylistId() == playlistId) core.syncCurrentPlaylistOnQueue()
|
|
128
|
+
promise.resolve(Unit)
|
|
129
|
+
}
|
|
130
|
+
return promise
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** On the player looper: the anchor id is player-thread state. */
|
|
134
|
+
override fun shufflePlaylist(
|
|
135
|
+
playlistId: String,
|
|
136
|
+
options: ShufflePlaylistOptions?,
|
|
137
|
+
): Promise<Unit> {
|
|
138
|
+
val promise = Promise<Unit>()
|
|
139
|
+
core.enqueue {
|
|
140
|
+
val isCurrent = core.getCurrentPlaylistId() == playlistId
|
|
141
|
+
val pin =
|
|
142
|
+
if (isCurrent && options?.keepCurrentTrackFirst != false) {
|
|
143
|
+
core.currentTracks.getOrNull(core.currentTrackIndex)?.id
|
|
144
|
+
} else {
|
|
145
|
+
null
|
|
146
|
+
}
|
|
147
|
+
val changed = playlistManager.shufflePlaylist(playlistId, pin)
|
|
148
|
+
if (changed == null) {
|
|
149
|
+
promise.reject(IllegalArgumentException("Playlist not found: $playlistId"))
|
|
150
|
+
return@enqueue
|
|
151
|
+
}
|
|
152
|
+
if (changed && isCurrent) core.syncCurrentPlaylistOnQueue()
|
|
153
|
+
promise.resolve(Unit)
|
|
154
|
+
}
|
|
155
|
+
return promise
|
|
156
|
+
}
|
|
157
|
+
|
|
120
158
|
// ── Playback control ──────────────────────────────────────────────────────
|
|
121
159
|
|
|
122
160
|
/**
|
|
@@ -59,9 +59,9 @@ class AndroidAutoConnectionDetector(
|
|
|
59
59
|
try {
|
|
60
60
|
val filter = IntentFilter(ACTION_CAR_CONNECTION_UPDATED)
|
|
61
61
|
|
|
62
|
-
//
|
|
62
|
+
// Exported: the broadcast comes from the Android Auto app, a NOT_EXPORTED receiver never gets it
|
|
63
63
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
64
|
-
context.registerReceiver(carConnectionReceiver, filter, Context.
|
|
64
|
+
context.registerReceiver(carConnectionReceiver, filter, Context.RECEIVER_EXPORTED)
|
|
65
65
|
} else {
|
|
66
66
|
context.registerReceiver(carConnectionReceiver, filter)
|
|
67
67
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
package com.margelo.nitro.nitroplayer.core
|
|
2
2
|
|
|
3
|
+
import android.net.Uri
|
|
3
4
|
import androidx.media3.common.MediaItem
|
|
4
5
|
import androidx.media3.common.Player
|
|
5
6
|
|
|
@@ -42,7 +43,18 @@ class ExoPlayerCore(
|
|
|
42
43
|
fun getPlaybackSpeed(): Float = player.playbackParameters.speed
|
|
43
44
|
|
|
44
45
|
// ── Queue mutations ────────────────────────────────────────────────────
|
|
45
|
-
|
|
46
|
+
// A lazy track's empty URI fails as "Source error"; stay idle until updateTracks supplies it
|
|
47
|
+
fun prepare() {
|
|
48
|
+
if (isLazyCurrent()) return
|
|
49
|
+
player.prepare()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// A prepared player loads a new current item by itself, so idle it while that item still has no URI
|
|
53
|
+
fun holdLazyCurrent() {
|
|
54
|
+
if (isLazyCurrent() && player.playbackState != Player.STATE_IDLE) player.stop()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private fun isLazyCurrent(): Boolean = player.currentMediaItem?.localConfiguration?.uri == Uri.EMPTY
|
|
46
58
|
|
|
47
59
|
fun seekToDefaultPosition(windowIndex: Int) = player.seekToDefaultPosition(windowIndex)
|
|
48
60
|
|
|
@@ -51,7 +63,10 @@ class ExoPlayerCore(
|
|
|
51
63
|
fun setMediaItems(
|
|
52
64
|
items: List<MediaItem>,
|
|
53
65
|
resetPosition: Boolean = false,
|
|
54
|
-
)
|
|
66
|
+
) {
|
|
67
|
+
player.setMediaItems(items, resetPosition)
|
|
68
|
+
holdLazyCurrent()
|
|
69
|
+
}
|
|
55
70
|
|
|
56
71
|
fun addMediaItems(items: List<MediaItem>) = player.addMediaItems(items)
|
|
57
72
|
|
|
@@ -4,8 +4,8 @@ import android.util.Log
|
|
|
4
4
|
import com.margelo.nitro.nitroplayer.BuildConfig
|
|
5
5
|
|
|
6
6
|
object NitroPlayerLogger {
|
|
7
|
-
//
|
|
8
|
-
val isEnabled: Boolean = BuildConfig.DEBUG
|
|
7
|
+
// Release builds log after `adb shell setprop log.tag.NitroPlayer DEBUG` and an app restart
|
|
8
|
+
val isEnabled: Boolean = BuildConfig.DEBUG || Log.isLoggable("NitroPlayer", Log.DEBUG)
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Preferred overload: message lambda is only evaluated when logging is enabled.
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
package com.margelo.nitro.nitroplayer.core
|
|
4
4
|
|
|
5
5
|
import com.margelo.nitro.nitroplayer.connection.AndroidAutoConnectionDetector
|
|
6
|
+
import com.margelo.nitro.nitroplayer.media.MediaItem
|
|
6
7
|
import com.margelo.nitro.nitroplayer.media.MediaLibraryParser
|
|
8
|
+
import com.margelo.nitro.nitroplayer.media.MediaType
|
|
7
9
|
import com.margelo.nitro.nitroplayer.media.NitroPlayerMediaBrowserService
|
|
8
10
|
|
|
9
11
|
/**
|
|
@@ -13,56 +15,64 @@ import com.margelo.nitro.nitroplayer.media.NitroPlayerMediaBrowserService
|
|
|
13
15
|
|
|
14
16
|
/** Called on the main thread from TrackPlayerCore.init via handler.post. */
|
|
15
17
|
internal fun TrackPlayerCore.setupAndroidAutoDetector() {
|
|
18
|
+
// The car may have bound the browser before this core existed (cold start from the car)
|
|
19
|
+
if (NitroPlayerMediaBrowserService.isAndroidAutoConnected) setAndroidAutoConnected(true)
|
|
16
20
|
androidAutoConnectionDetector =
|
|
17
21
|
AndroidAutoConnectionDetector(context).apply {
|
|
18
22
|
onConnectionChanged = { connected, _ ->
|
|
19
|
-
handler.post {
|
|
20
|
-
isAndroidAutoConnectedField = connected
|
|
21
|
-
NitroPlayerMediaBrowserService.isAndroidAutoConnected = connected
|
|
22
|
-
notifyAndroidAutoConnection(connected)
|
|
23
|
-
}
|
|
23
|
+
handler.post { setAndroidAutoConnected(connected) }
|
|
24
24
|
}
|
|
25
25
|
registerCarConnectionReceiver()
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
/**
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
29
|
+
/** Android Auto binding the browser is proof of a connection, whatever the provider query said. */
|
|
30
|
+
internal fun TrackPlayerCore.onAndroidAutoBrowserConnected() {
|
|
31
|
+
handler.post { setAndroidAutoConnected(true) }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private fun TrackPlayerCore.setAndroidAutoConnected(connected: Boolean) {
|
|
35
|
+
NitroPlayerMediaBrowserService.isAndroidAutoConnected = connected
|
|
36
|
+
if (isAndroidAutoConnectedField == connected) return
|
|
37
|
+
isAndroidAutoConnectedField = connected
|
|
38
|
+
updateJsKeepAlive()
|
|
39
|
+
notifyAndroidAutoConnection(connected)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Routes a controller's "playlistId:trackId" pick through the logical queue (window + lazy URLs); false for unknown ids. */
|
|
43
|
+
internal fun TrackPlayerCore.playFromMediaId(mediaId: String): Boolean {
|
|
44
|
+
val colonIndex = mediaId.indexOf(':')
|
|
45
|
+
if (colonIndex <= 0) return false
|
|
46
|
+
val playlistId = mediaId.substring(0, colonIndex)
|
|
47
|
+
val trackRef = mediaId.substring(colonIndex + 1)
|
|
48
|
+
if (playlistManager.indexOfTrackRef(playlistId, trackRef) < 0) return false
|
|
49
|
+
startJsRuntimeIfNeeded()
|
|
50
|
+
enqueue {
|
|
51
|
+
val index = playlistManager.indexOfTrackRef(playlistId, trackRef)
|
|
52
|
+
// Resuming what is already loaded keeps its position; a failed item is reloaded instead
|
|
53
|
+
if (currentPlaylistId == playlistId && currentTrackIndex == index && exo.player.playerError == null) {
|
|
54
|
+
// Tapping a song still waiting for its URL asks the app for it again
|
|
55
|
+
if (getCurrentTrack()?.url.isNullOrEmpty()) checkUpcomingTracksForUrls(lookaheadCount, force = true)
|
|
56
|
+
return@enqueue
|
|
45
57
|
}
|
|
58
|
+
if (index >= 0 && playlistManager.loadPlaylist(playlistId, index)) loadPlaylistOnQueue(playlistId, index)
|
|
46
59
|
}
|
|
47
|
-
|
|
48
|
-
private fun TrackPlayerCore.loadPlaylistInternal(playlistId: String) {
|
|
49
|
-
playNextStack.clear()
|
|
50
|
-
upNextQueue.clear()
|
|
51
|
-
currentTemporaryType = TrackPlayerCore.TemporaryType.NONE
|
|
52
|
-
val playlist = playlistManager.getPlaylist(playlistId) ?: return
|
|
53
|
-
currentPlaylistId = playlistId
|
|
54
|
-
updatePlayerQueue(playlist.tracks)
|
|
60
|
+
return true
|
|
55
61
|
}
|
|
56
62
|
|
|
63
|
+
private fun folderIds(items: List<MediaItem>?): List<String> = items.orEmpty().filter { it.mediaType == MediaType.FOLDER }.flatMap { listOf(it.id) + folderIds(it.children) }
|
|
64
|
+
|
|
57
65
|
suspend fun TrackPlayerCore.setAndroidAutoMediaLibrary(libraryJson: String) =
|
|
58
66
|
withPlayerContext {
|
|
59
67
|
val library = MediaLibraryParser.fromJson(libraryJson)
|
|
68
|
+
val previous = folderIds(mediaLibraryManager.getMediaLibrary()?.rootItems)
|
|
60
69
|
mediaLibraryManager.setMediaLibrary(library)
|
|
61
|
-
NitroPlayerMediaBrowserService.getInstance()?.
|
|
70
|
+
NitroPlayerMediaBrowserService.getInstance()?.onLibraryUpdated(previous + folderIds(library.rootItems) + mediaLibraryManager.loadedFolderIds)
|
|
62
71
|
}
|
|
63
72
|
|
|
64
73
|
suspend fun TrackPlayerCore.clearAndroidAutoMediaLibrary() =
|
|
65
74
|
withPlayerContext {
|
|
75
|
+
val previous = folderIds(mediaLibraryManager.getMediaLibrary()?.rootItems)
|
|
66
76
|
mediaLibraryManager.clear()
|
|
67
|
-
NitroPlayerMediaBrowserService.getInstance()?.
|
|
77
|
+
NitroPlayerMediaBrowserService.getInstance()?.onLibraryUpdated(previous + mediaLibraryManager.loadedFolderIds)
|
|
68
78
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
@file:Suppress("ktlint:standard:max-line-length")
|
|
2
|
+
|
|
3
|
+
package com.margelo.nitro.nitroplayer.core
|
|
4
|
+
|
|
5
|
+
import androidx.annotation.OptIn
|
|
6
|
+
import androidx.media3.common.AudioAttributes
|
|
7
|
+
import androidx.media3.common.C
|
|
8
|
+
import androidx.media3.common.util.UnstableApi
|
|
9
|
+
import androidx.media3.exoplayer.ExoPlayer
|
|
10
|
+
import com.margelo.nitro.nitroplayer.AndroidAudioFocusMode
|
|
11
|
+
import com.margelo.nitro.nitroplayer.media.AudioFocusController
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Audio focus policy. ExoPlayer's built-in handling pauses on every transient loss — even the
|
|
15
|
+
* duckable kind a notification asks for — so 'duck' and 'ignore' take focus into our own hands.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
internal const val DUCK_VOLUME_FACTOR = 0.2f
|
|
19
|
+
|
|
20
|
+
@OptIn(UnstableApi::class)
|
|
21
|
+
internal fun TrackPlayerCore.applyAudioFocusMode(mode: AndroidAudioFocusMode) {
|
|
22
|
+
if (mode == audioFocusMode && (mode == AndroidAudioFocusMode.PAUSE) == (audioFocusController == null)) return
|
|
23
|
+
audioFocusMode = mode
|
|
24
|
+
|
|
25
|
+
audioFocusController?.abandon()
|
|
26
|
+
audioFocusController = null
|
|
27
|
+
isDucked = false
|
|
28
|
+
|
|
29
|
+
val local = exoOrNull() ?: return
|
|
30
|
+
// media3 only knows how to pause; it owns focus for the default mode and nothing else.
|
|
31
|
+
local.setAudioAttributes(mediaAudioAttributes(), mode == AndroidAudioFocusMode.PAUSE)
|
|
32
|
+
local.volume = userVolume
|
|
33
|
+
|
|
34
|
+
if (mode == AndroidAudioFocusMode.DUCK) {
|
|
35
|
+
audioFocusController =
|
|
36
|
+
AudioFocusController(
|
|
37
|
+
context,
|
|
38
|
+
onDuck = { ducked -> applyDuck(ducked) },
|
|
39
|
+
onPause = { playerHandler.post { pauseOnQueue() } },
|
|
40
|
+
)
|
|
41
|
+
if (isExoInitialized && exo.player.isPlaying) audioFocusController?.acquire()
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@OptIn(UnstableApi::class)
|
|
46
|
+
internal fun TrackPlayerCore.applyDuck(ducked: Boolean) {
|
|
47
|
+
playerHandler.post {
|
|
48
|
+
isDucked = ducked
|
|
49
|
+
exoOrNull()?.volume = userVolume * if (ducked) DUCK_VOLUME_FACTOR else 1f
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private fun mediaAudioAttributes(): AudioAttributes =
|
|
54
|
+
AudioAttributes
|
|
55
|
+
.Builder()
|
|
56
|
+
.setUsage(C.USAGE_MEDIA)
|
|
57
|
+
.setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
|
|
58
|
+
.build()
|
|
59
|
+
|
|
60
|
+
/** The local ExoPlayer, or null while casting (the CastPlayer has no audio focus of its own). */
|
|
61
|
+
@OptIn(UnstableApi::class)
|
|
62
|
+
internal fun TrackPlayerCore.exoOrNull(): ExoPlayer? {
|
|
63
|
+
if (!isExoInitialized) return null
|
|
64
|
+
return exo.player as? ExoPlayer
|
|
65
|
+
}
|
|
@@ -88,7 +88,7 @@ internal fun TrackPlayerCore.switchToLocalPlayer() {
|
|
|
88
88
|
lastCastWaitTrackId = null
|
|
89
89
|
exo = ExoPlayerCore(controller.localPlayer)
|
|
90
90
|
playerListener?.let { controller.localPlayer.addListener(it) }
|
|
91
|
-
setMediaSessionPlayer(controller.
|
|
91
|
+
setMediaSessionPlayer(controller.localSessionPlayer)
|
|
92
92
|
|
|
93
93
|
val localList = ArrayList<TrackItem>()
|
|
94
94
|
currentTrack?.let { localList.add(it) }
|
|
@@ -12,6 +12,11 @@ import android.os.Bundle
|
|
|
12
12
|
import android.os.Handler
|
|
13
13
|
import android.os.IBinder
|
|
14
14
|
import android.os.Looper
|
|
15
|
+
import com.facebook.react.ReactApplication
|
|
16
|
+
import com.facebook.react.bridge.Arguments
|
|
17
|
+
import com.facebook.react.bridge.ReactContext
|
|
18
|
+
import com.facebook.react.jstasks.HeadlessJsTaskConfig
|
|
19
|
+
import com.facebook.react.jstasks.HeadlessJsTaskContext
|
|
15
20
|
import com.margelo.nitro.NitroModules
|
|
16
21
|
import com.margelo.nitro.nitroplayer.Reason
|
|
17
22
|
import com.margelo.nitro.nitroplayer.RepeatMode
|
|
@@ -22,6 +27,8 @@ import com.margelo.nitro.nitroplayer.connection.AndroidAutoConnectionDetector
|
|
|
22
27
|
import com.margelo.nitro.nitroplayer.download.DownloadManagerCore
|
|
23
28
|
import com.margelo.nitro.nitroplayer.media.ExoPlayerBuilder
|
|
24
29
|
import com.margelo.nitro.nitroplayer.media.MediaLibraryManager
|
|
30
|
+
import com.margelo.nitro.nitroplayer.AndroidAudioFocusMode
|
|
31
|
+
import com.margelo.nitro.nitroplayer.media.AudioFocusController
|
|
25
32
|
import com.margelo.nitro.nitroplayer.media.MediaSessionManager
|
|
26
33
|
import com.margelo.nitro.nitroplayer.media.NitroPlayerPlaybackService
|
|
27
34
|
import com.margelo.nitro.nitroplayer.playlist.PlaylistManager
|
|
@@ -71,9 +78,19 @@ class TrackPlayerCore private constructor(
|
|
|
71
78
|
@Volatile internal var currentRepeatMode: RepeatMode = RepeatMode.OFF
|
|
72
79
|
internal var lookaheadCount: Int = 5
|
|
73
80
|
|
|
81
|
+
internal var audioFocusMode: AndroidAudioFocusMode = AndroidAudioFocusMode.PAUSE
|
|
82
|
+
internal var audioFocusController: AudioFocusController? = null
|
|
83
|
+
|
|
84
|
+
/** Volume the app asked for; ducking scales this instead of overwriting it. */
|
|
85
|
+
internal var userVolume: Float = 1f
|
|
86
|
+
internal var isDucked: Boolean = false
|
|
87
|
+
|
|
74
88
|
internal var remoteSkipForwardIntervalMs: Long = ExoPlayerBuilder.DEFAULT_REMOTE_SKIP_INTERVAL_MS
|
|
75
89
|
internal var remoteSkipBackwardIntervalMs: Long = ExoPlayerBuilder.DEFAULT_REMOTE_SKIP_INTERVAL_MS
|
|
76
90
|
internal var playerListener: androidx.media3.common.Player.Listener? = null
|
|
91
|
+
internal var consecutiveSourceErrors = 0
|
|
92
|
+
internal val urlRequestedAt = HashMap<String, Long>()
|
|
93
|
+
internal var networkRetryCallback: android.net.ConnectivityManager.NetworkCallback? = null
|
|
77
94
|
|
|
78
95
|
// ── Temporary queue ────────────────────────────────────────────────────
|
|
79
96
|
internal var playNextStack: MutableList<TrackItem> = mutableListOf()
|
|
@@ -144,21 +161,47 @@ class TrackPlayerCore private constructor(
|
|
|
144
161
|
playerHandler.postDelayed(progressUpdateRunnable, PROGRESS_INTERVAL_MS)
|
|
145
162
|
}
|
|
146
163
|
|
|
147
|
-
internal
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
164
|
+
internal enum class AnchorOutcome { NONE, KEPT, REMOVED }
|
|
165
|
+
|
|
166
|
+
/** Re-resolves the anchor by id, or moves the cursor to the resume slot when it was removed. */
|
|
167
|
+
internal fun assignCurrentTracks(next: List<TrackItem>): AnchorOutcome {
|
|
168
|
+
val old = currentTracks
|
|
169
|
+
val anchor = old.getOrNull(currentTrackIndex)
|
|
170
|
+
currentTracks = next
|
|
171
|
+
if (anchor == null) return AnchorOutcome.NONE
|
|
172
|
+
// A repeated track keeps its rank among the occurrences, so editing elsewhere does not jump to another copy
|
|
173
|
+
val rank = old.subList(0, currentTrackIndex).count { it.id == anchor.id }
|
|
174
|
+
val occurrences = next.indices.filter { next[it].id == anchor.id }
|
|
175
|
+
val newIndex = occurrences.getOrElse(rank) { occurrences.lastOrNull() ?: -1 }
|
|
176
|
+
if (newIndex >= 0) {
|
|
177
|
+
currentTrackIndex = newIndex
|
|
178
|
+
return AnchorOutcome.KEPT
|
|
161
179
|
}
|
|
180
|
+
val alive = next.mapTo(HashSet()) { it.id }
|
|
181
|
+
val followerId = old.drop(currentTrackIndex + 1).firstOrNull { it.id in alive }?.id
|
|
182
|
+
val resume = followerId?.let { id -> next.indexOfFirst { it.id == id } } ?: next.size
|
|
183
|
+
currentTrackIndex = resume - 1
|
|
184
|
+
return AnchorOutcome.REMOVED
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Applies the stored playlist to the live queue without re-preparing the playing item. */
|
|
188
|
+
internal fun syncCurrentPlaylistOnQueue() {
|
|
189
|
+
playerHandler.removeCallbacks(updateCurrentPlaylistRunnable)
|
|
190
|
+
val id = currentPlaylistId ?: return
|
|
191
|
+
val playlist = playlistManager.getPlaylist(id) ?: return
|
|
192
|
+
assignCurrentTracks(playlist.tracks)
|
|
193
|
+
if (::exo.isInitialized &&
|
|
194
|
+
exo.currentMediaItem != null &&
|
|
195
|
+
exo.currentMediaItemIndex >= 0
|
|
196
|
+
) {
|
|
197
|
+
rebuildQueueFromCurrentPosition()
|
|
198
|
+
} else {
|
|
199
|
+
updatePlayerQueue(playlist.tracks)
|
|
200
|
+
}
|
|
201
|
+
checkUpcomingTracksForUrls(lookaheadCount)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
internal val updateCurrentPlaylistRunnable = Runnable { syncCurrentPlaylistOnQueue() }
|
|
162
205
|
|
|
163
206
|
// ── Service binding ────────────────────────────────────────────────────
|
|
164
207
|
private var serviceBound = false
|
|
@@ -219,6 +262,7 @@ class TrackPlayerCore private constructor(
|
|
|
219
262
|
// ── Singleton ──────────────────────────────────────────────────────────
|
|
220
263
|
companion object {
|
|
221
264
|
internal const val PROGRESS_INTERVAL_MS = 1000L
|
|
265
|
+
private const val KEEP_ALIVE_TASK = "NitroPlayerKeepAlive"
|
|
222
266
|
|
|
223
267
|
@Volatile
|
|
224
268
|
@Suppress("ktlint:standard:property-naming")
|
|
@@ -361,6 +405,34 @@ class TrackPlayerCore private constructor(
|
|
|
361
405
|
}
|
|
362
406
|
}
|
|
363
407
|
|
|
408
|
+
/** Lazy URLs come from JS; a process started by the car or a controller has no runtime until started. */
|
|
409
|
+
internal fun startJsRuntimeIfNeeded() {
|
|
410
|
+
val host = (context as? ReactApplication)?.reactHost ?: return
|
|
411
|
+
if (host.currentReactContext == null) host.start()
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
private var keepAliveTask: Pair<ReactContext, Int>? = null
|
|
415
|
+
|
|
416
|
+
/** Without a resumed activity (car, screen off) RN pauses JS timers; a headless task keeps them running while JS may be needed. */
|
|
417
|
+
internal fun updateJsKeepAlive() {
|
|
418
|
+
val wanted = isAndroidAutoConnectedField || (isExoInitialized && exo.playWhenReady)
|
|
419
|
+
val reactContext = (context as? ReactApplication)?.reactHost?.currentReactContext
|
|
420
|
+
keepAliveTask?.let { (taskContext, taskId) ->
|
|
421
|
+
val tasks = HeadlessJsTaskContext.getInstance(taskContext)
|
|
422
|
+
if (wanted && taskContext === reactContext && tasks.isTaskRunning(taskId)) return
|
|
423
|
+
if (tasks.isTaskRunning(taskId)) tasks.finishTask(taskId)
|
|
424
|
+
keepAliveTask = null
|
|
425
|
+
}
|
|
426
|
+
if (!wanted || reactContext == null || !reactContext.hasActiveReactInstance()) return
|
|
427
|
+
try {
|
|
428
|
+
val config = HeadlessJsTaskConfig(KEEP_ALIVE_TASK, Arguments.createMap(), 0, true)
|
|
429
|
+
keepAliveTask = reactContext to HeadlessJsTaskContext.getInstance(reactContext).startTask(config)
|
|
430
|
+
NitroPlayerLogger.log("TrackPlayerCore") { "keep-alive task started: ${keepAliveTask?.second}" }
|
|
431
|
+
} catch (e: Exception) {
|
|
432
|
+
NitroPlayerLogger.log("TrackPlayerCore") { "keep-alive task not started: ${e.message}" }
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
364
436
|
// ── Simple read-only accessors ─────────────────────────────────────────
|
|
365
437
|
|
|
366
438
|
fun isAndroidAutoConnected(): Boolean = isAndroidAutoConnectedField
|
|
@@ -389,7 +461,12 @@ class TrackPlayerCore private constructor(
|
|
|
389
461
|
|
|
390
462
|
fun removeOnPlaybackProgressChangeListener(id: Long): Boolean = onProgressListeners.remove(id)
|
|
391
463
|
|
|
392
|
-
fun addOnTracksNeedUpdateListener(cb: (List<TrackItem>, Int) -> Unit): Long
|
|
464
|
+
fun addOnTracksNeedUpdateListener(cb: (List<TrackItem>, Int) -> Unit): Long {
|
|
465
|
+
val id = onTracksNeedUpdateListeners.add(cb)
|
|
466
|
+
// Requests raised before JS was listening (headless start) are otherwise lost
|
|
467
|
+
enqueue { checkUpcomingTracksForUrls(lookaheadCount, force = true) }
|
|
468
|
+
return id
|
|
469
|
+
}
|
|
393
470
|
|
|
394
471
|
fun removeOnTracksNeedUpdateListener(id: Long): Boolean = onTracksNeedUpdateListeners.remove(id)
|
|
395
472
|
|
|
@@ -31,6 +31,7 @@ internal class TrackPlayerEventListener(
|
|
|
31
31
|
with(core) {
|
|
32
32
|
// TRACK repeat: REPEAT_MODE_ONE fires this every loop — not a real track change
|
|
33
33
|
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT) return
|
|
34
|
+
exo.holdLazyCurrent()
|
|
34
35
|
|
|
35
36
|
// Remove the track that just finished/was skipped from temp lists
|
|
36
37
|
if ((
|
|
@@ -61,9 +62,13 @@ internal class TrackPlayerEventListener(
|
|
|
61
62
|
val previousTrackIndex = currentTrackIndex
|
|
62
63
|
if (currentTemporaryType == TrackPlayerCore.TemporaryType.NONE && mediaItem != null) {
|
|
63
64
|
val trackId = extractTrackId(mediaItem.mediaId)
|
|
64
|
-
|
|
65
|
+
// Moving through the timeline lands on the next occurrence of a repeated track, not its first
|
|
66
|
+
val advancing = reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO || reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK
|
|
67
|
+
val newIdx = currentTrackIndexOf(trackId, if (advancing) currentTrackIndex + 1 else currentTrackIndex)
|
|
65
68
|
if (newIdx >= 0 && newIdx != currentTrackIndex) currentTrackIndex = newIdx
|
|
66
69
|
}
|
|
70
|
+
// A transient play-out ends here: restore the user's repeat mode.
|
|
71
|
+
core.applyBackendRepeatMode()
|
|
67
72
|
|
|
68
73
|
val wrappedToStart = currentRepeatMode == RepeatMode.PLAYLIST &&
|
|
69
74
|
currentTrackIndex == 0 && previousTrackIndex == currentTracks.size - 1
|
|
@@ -135,6 +140,7 @@ internal class TrackPlayerEventListener(
|
|
|
135
140
|
reason: Int,
|
|
136
141
|
) {
|
|
137
142
|
val r = if (reason == Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST) Reason.USER_ACTION else null
|
|
143
|
+
core.updateJsKeepAlive()
|
|
138
144
|
core.emitStateChange(r)
|
|
139
145
|
}
|
|
140
146
|
|
|
@@ -154,7 +160,10 @@ internal class TrackPlayerEventListener(
|
|
|
154
160
|
}
|
|
155
161
|
|
|
156
162
|
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
|
157
|
-
if (isPlaying)
|
|
163
|
+
if (isPlaying) {
|
|
164
|
+
core.startProgressTicks()
|
|
165
|
+
core.onPlaybackHealthy()
|
|
166
|
+
}
|
|
158
167
|
core.emitStateChange()
|
|
159
168
|
}
|
|
160
169
|
|
|
@@ -171,21 +180,7 @@ internal class TrackPlayerEventListener(
|
|
|
171
180
|
}
|
|
172
181
|
}
|
|
173
182
|
|
|
174
|
-
override fun onPlayerError(error: PlaybackException)
|
|
175
|
-
with(core) {
|
|
176
|
-
NitroPlayerLogger.log(
|
|
177
|
-
"TrackPlayer",
|
|
178
|
-
"Player error: ${error.errorCodeName} - ${error.message}",
|
|
179
|
-
)
|
|
180
|
-
notifyPlaybackStateChange(TrackPlayerState.STOPPED, Reason.ERROR)
|
|
181
|
-
// A source error leaves ExoPlayer in IDLE; skip the dead item and
|
|
182
|
-
// re-prepare so the rest of the queue keeps playing.
|
|
183
|
-
if (exo.hasNextMediaItem()) {
|
|
184
|
-
exo.seekToNext()
|
|
185
|
-
exo.prepare()
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
183
|
+
override fun onPlayerError(error: PlaybackException) = core.handlePlayerError(error)
|
|
189
184
|
|
|
190
185
|
override fun onAudioSessionIdChanged(audioSessionId: Int) {
|
|
191
186
|
if (audioSessionId != 0) {
|