capacitor-plugin-playlist 0.11.2 → 0.13.0
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 +133 -127
- package/android/build.gradle +31 -18
- package/android/gradle/wrapper/gradle-wrapper.properties +1 -1
- package/android/src/main/java/org/dwbn/plugins/playlist/AudioMediaItemFactory.java +96 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/PlaylistPlugin.kt +3 -3
- package/android/src/main/java/org/dwbn/plugins/playlist/RmxAudioPlayer.java +145 -188
- package/android/src/main/java/org/dwbn/plugins/playlist/RmxPlaybackErrorMapper.kt +26 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/data/AudioTrack.kt +28 -28
- package/android/src/main/java/org/dwbn/plugins/playlist/handoff/VideoPlayerBridge.kt +102 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/manager/PlaybackProgress.kt +8 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/manager/PlaylistManager.kt +418 -183
- package/android/src/main/java/org/dwbn/plugins/playlist/manager/PlaylistPlaybackPolicy.kt +98 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/manager/RmxPlaybackState.kt +11 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/playlist/AudioPlaylistHandler.java +107 -211
- package/android/src/main/java/org/dwbn/plugins/playlist/service/GlideBitmapLoader.kt +121 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/service/HandoffForwardingPlayer.kt +185 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/service/MediaNotificationPolicy.kt +253 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/service/MediaService.kt +260 -68
- package/android/src/main/java/org/dwbn/plugins/playlist/service/PlaylistMediaSessionCallback.kt +39 -0
- package/android/src/test/java/org/dwbn/plugins/playlist/AudioMediaItemFactoryMimeTest.kt +98 -0
- package/android/src/test/java/org/dwbn/plugins/playlist/AudioMediaItemFactoryTest.java +71 -0
- package/android/src/test/java/org/dwbn/plugins/playlist/data/AudioTrackTest.kt +53 -0
- package/android/src/test/java/org/dwbn/plugins/playlist/handoff/VideoPlayerBridgeTest.kt +84 -0
- package/android/src/test/java/org/dwbn/plugins/playlist/manager/PlaylistPlaybackPolicyTest.kt +128 -0
- package/android/src/test/java/org/dwbn/plugins/playlist/playlist/AudioPlaylistHandlerTest.kt +145 -0
- package/android/src/test/java/org/dwbn/plugins/playlist/service/GlideBitmapLoaderTest.kt +76 -0
- package/android/src/test/java/org/dwbn/plugins/playlist/service/HandoffForwardingPlayerTest.kt +282 -0
- package/android/src/test/java/org/dwbn/plugins/playlist/service/MediaNotificationPolicyTest.kt +285 -0
- package/android/src/test/java/org/dwbn/plugins/playlist/service/VideoHandoffNotificationCommandsTest.kt +54 -0
- package/android/src/test/java/org/dwbn/plugins/playlist/service/VideoHandoffNotificationPolicyTest.kt +47 -0
- package/dist/docs.json +12 -846
- package/dist/esm/RmxAudioPlayer.d.ts +2 -2
- package/dist/esm/RmxAudioPlayer.js.map +1 -1
- package/dist/esm/definitions.d.ts +7 -7
- package/dist/esm/interfaces.d.ts +7 -1
- package/dist/esm/utils.d.ts +1 -1
- package/dist/esm/utils.js +2 -2
- package/dist/esm/utils.js.map +1 -1
- package/dist/esm/web.d.ts +3 -3
- package/dist/esm/web.js +5 -5
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +7 -7
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +7 -7
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/PlaylistPlugin/AVBidirectionalQueuePlayer.swift +45 -6
- package/ios/Sources/PlaylistPlugin/AudioTrack.swift +15 -2
- package/ios/Tests/PlaylistPluginTests/PositionResumeTests.swift +82 -0
- package/package.json +15 -19
- package/android/src/main/java/org/dwbn/plugins/playlist/notification/PlaylistNotificationProvider.kt +0 -26
- package/android/src/main/java/org/dwbn/plugins/playlist/playlist/AudioApi.kt +0 -114
- package/android/src/main/java/org/dwbn/plugins/playlist/playlist/BaseMediaApi.kt +0 -36
- package/android/src/main/java/org/dwbn/plugins/playlist/service/MediaImageProvider.kt +0 -83
|
@@ -1,31 +1,39 @@
|
|
|
1
1
|
package org.dwbn.plugins.playlist.manager
|
|
2
2
|
|
|
3
3
|
import android.app.Application
|
|
4
|
+
import android.content.Intent
|
|
5
|
+
import android.os.Build
|
|
4
6
|
import android.util.Log
|
|
5
7
|
import androidx.annotation.FloatRange
|
|
6
8
|
import androidx.annotation.IntRange
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
9
|
+
import androidx.annotation.OptIn
|
|
10
|
+
import androidx.media3.common.MediaItem
|
|
11
|
+
import androidx.media3.common.Player
|
|
12
|
+
import androidx.media3.common.util.UnstableApi
|
|
13
|
+
import androidx.media3.exoplayer.ExoPlayer
|
|
14
|
+
import org.dwbn.plugins.playlist.AudioMediaItemFactory
|
|
10
15
|
import org.dwbn.plugins.playlist.PlaylistItemOptions
|
|
16
|
+
import org.dwbn.plugins.playlist.RmxAudioPlayer
|
|
11
17
|
import org.dwbn.plugins.playlist.TrackRemovalItem
|
|
12
18
|
import org.dwbn.plugins.playlist.data.AudioTrack
|
|
13
|
-
import org.dwbn.plugins.playlist.playlist.
|
|
19
|
+
import org.dwbn.plugins.playlist.playlist.AudioPlaylistHandler
|
|
20
|
+
import org.dwbn.plugins.playlist.handoff.VideoPlayerBridge
|
|
21
|
+
import org.dwbn.plugins.playlist.service.MediaNotificationPolicy
|
|
14
22
|
import org.dwbn.plugins.playlist.service.MediaService
|
|
15
23
|
import java.lang.ref.WeakReference
|
|
16
|
-
import java.util
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
* [MediaService] which extends [com.devbrackets.android.playlistcore.service.BasePlaylistService].
|
|
21
|
-
*/
|
|
22
|
-
class PlaylistManager(application: Application) :
|
|
23
|
-
ListPlaylistManager<AudioTrack>(application, MediaService::class.java), OnErrorListener {
|
|
24
|
+
import java.util.ArrayList
|
|
25
|
+
|
|
26
|
+
@OptIn(UnstableApi::class)
|
|
27
|
+
class PlaylistManager(private val application: Application) {
|
|
24
28
|
private val audioTracks: MutableList<AudioTrack> = ArrayList()
|
|
25
29
|
private var volumeLeft = 1.0f
|
|
26
30
|
private var volumeRight = 1.0f
|
|
27
31
|
private var playbackSpeed = 1.0f
|
|
28
32
|
var loop = false
|
|
33
|
+
set(value) {
|
|
34
|
+
field = value
|
|
35
|
+
applyPlayerSettings()
|
|
36
|
+
}
|
|
29
37
|
var isShouldStopPlaylist = false
|
|
30
38
|
var currentErrorTrack: AudioTrack? = null
|
|
31
39
|
|
|
@@ -33,88 +41,173 @@ class PlaylistManager(application: Application) :
|
|
|
33
41
|
var videoHandoffForegroundRetain = false
|
|
34
42
|
var mediaServiceInForeground = false
|
|
35
43
|
|
|
36
|
-
// Really need a way to propagate the settings through the app
|
|
37
44
|
var resetStreamOnPause = true
|
|
38
|
-
var options: Options
|
|
45
|
+
var options: Options = Options(application.baseContext)
|
|
46
|
+
set(value) {
|
|
47
|
+
field = value
|
|
48
|
+
mediaServiceRef.get()?.applyNotificationIcon()
|
|
49
|
+
}
|
|
39
50
|
private var mediaControlsListener = WeakReference<MediaControlsListener?>(null)
|
|
40
|
-
private var
|
|
41
|
-
|
|
42
|
-
|
|
51
|
+
private var playbackStatusListener = WeakReference<RmxAudioPlayer?>(null)
|
|
52
|
+
|
|
53
|
+
private var player: ExoPlayer? = null
|
|
54
|
+
private var mediaServiceRef = WeakReference<MediaService?>(null)
|
|
55
|
+
private var handlerInstance: AudioPlaylistHandler? = null
|
|
56
|
+
private var seeking = false
|
|
57
|
+
private var sequentialErrors = 0
|
|
58
|
+
private var rmxPlaybackState = RmxPlaybackState.STOPPED
|
|
59
|
+
private var pendingBeginPlayback: PendingBegin? = null
|
|
60
|
+
|
|
61
|
+
private data class PendingBegin(val seekPosition: Long, val startPaused: Boolean)
|
|
43
62
|
|
|
44
|
-
|
|
45
|
-
|
|
63
|
+
var currentPosition: Int = INVALID_POSITION
|
|
64
|
+
|
|
65
|
+
val currentItem: AudioTrack?
|
|
66
|
+
get() {
|
|
67
|
+
val index = player?.currentMediaItemIndex ?: currentPosition
|
|
68
|
+
return if (index in audioTracks.indices) audioTracks[index] else null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
val isPlaying: Boolean
|
|
72
|
+
get() = player?.isPlaying == true
|
|
73
|
+
|
|
74
|
+
val playlistHandler: AudioPlaylistHandler?
|
|
75
|
+
get() {
|
|
76
|
+
if (handlerInstance == null) {
|
|
77
|
+
handlerInstance = AudioPlaylistHandler(application, this)
|
|
78
|
+
}
|
|
79
|
+
return handlerInstance
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
fun setOnErrorListener(listener: RmxAudioPlayer?) {
|
|
83
|
+
playbackStatusListener = WeakReference(listener)
|
|
46
84
|
}
|
|
47
85
|
|
|
48
86
|
fun setMediaControlsListener(listener: MediaControlsListener?) {
|
|
49
87
|
mediaControlsListener = WeakReference(listener)
|
|
50
88
|
}
|
|
51
89
|
|
|
52
|
-
|
|
53
|
-
|
|
90
|
+
fun setPlaybackStatusListener(listener: RmxAudioPlayer?) {
|
|
91
|
+
playbackStatusListener = WeakReference(listener)
|
|
92
|
+
}
|
|
54
93
|
|
|
55
|
-
|
|
94
|
+
@Suppress("UNUSED_PARAMETER")
|
|
95
|
+
fun setId(id: Int) {
|
|
96
|
+
// PlaylistCore required a playlist id; Media3 stack does not use it.
|
|
97
|
+
}
|
|
56
98
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
99
|
+
fun attachPlayer(exoPlayer: ExoPlayer) {
|
|
100
|
+
player = exoPlayer
|
|
101
|
+
if (audioTracks.isNotEmpty()) {
|
|
102
|
+
exoPlayer.setMediaItems(audioTracks.map { AudioMediaItemFactory.fromAudioTrack(it) })
|
|
103
|
+
if (currentPosition in audioTracks.indices) {
|
|
104
|
+
exoPlayer.seekTo(currentPosition, 0)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
applyPlayerSettings()
|
|
108
|
+
exoPlayer.addListener(playerListener)
|
|
109
|
+
playbackStatusListener.get()?.onPlayerAttached(exoPlayer)
|
|
110
|
+
pendingBeginPlayback?.let { pending ->
|
|
111
|
+
pendingBeginPlayback = null
|
|
112
|
+
beginPlaybackAt(pending.seekPosition, pending.startPaused)
|
|
60
113
|
}
|
|
61
|
-
return true
|
|
62
114
|
}
|
|
63
115
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
0
|
|
87
|
-
} else {
|
|
88
|
-
(currentPosition + 1).coerceAtMost(itemCount)
|
|
89
|
-
}
|
|
116
|
+
fun detachPlayer() {
|
|
117
|
+
player?.removeListener(playerListener)
|
|
118
|
+
playbackStatusListener.get()?.onPlayerDetached()
|
|
119
|
+
player = null
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
fun attachService(service: MediaService) {
|
|
123
|
+
mediaServiceRef = WeakReference(service)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
fun getPlayer(): ExoPlayer? = player
|
|
127
|
+
|
|
128
|
+
fun isVideoHandoffPrewarmActive(): Boolean = videoHandoffForegroundRetain
|
|
129
|
+
|
|
130
|
+
fun getCurrentPlaybackState(): RmxPlaybackState = rmxPlaybackState
|
|
131
|
+
|
|
132
|
+
fun getCurrentProgress(): PlaybackProgress? {
|
|
133
|
+
val exoPlayer = player ?: return null
|
|
134
|
+
val duration = exoPlayer.duration.coerceAtLeast(0)
|
|
135
|
+
val position = exoPlayer.currentPosition.coerceAtLeast(0)
|
|
136
|
+
val buffered = if (duration > 0) {
|
|
137
|
+
((exoPlayer.bufferedPosition.toFloat() / duration) * 100f).toInt().coerceIn(0, 100)
|
|
90
138
|
} else {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
139
|
+
0
|
|
140
|
+
}
|
|
141
|
+
val bufferedFloat = if (duration > 0) {
|
|
142
|
+
(exoPlayer.bufferedPosition.toFloat() / duration).coerceIn(0f, 1f)
|
|
143
|
+
} else {
|
|
144
|
+
0f
|
|
97
145
|
}
|
|
146
|
+
return PlaybackProgress(position, duration, buffered, bufferedFloat)
|
|
147
|
+
}
|
|
98
148
|
|
|
99
|
-
|
|
149
|
+
val isNextAvailable: Boolean
|
|
150
|
+
get() = PlaylistPlaybackPolicy.nextAvailable(
|
|
151
|
+
player?.currentMediaItemIndex ?: currentPosition,
|
|
152
|
+
audioTracks.size,
|
|
153
|
+
loop
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
val isPreviousAvailable: Boolean
|
|
157
|
+
get() = PlaylistPlaybackPolicy.previousAvailable(
|
|
158
|
+
player?.currentMediaItemIndex ?: currentPosition,
|
|
159
|
+
loop,
|
|
160
|
+
audioTracks.size
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
fun invokeNext() {
|
|
164
|
+
playlistHandler?.next()
|
|
100
165
|
}
|
|
101
166
|
|
|
167
|
+
fun invokePrevious() {
|
|
168
|
+
playlistHandler?.previous()
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
fun skipToNext() {
|
|
172
|
+
val exoPlayer = player ?: return
|
|
173
|
+
if (!isNextAvailable) {
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
val fromIndex = exoPlayer.currentMediaItemIndex
|
|
177
|
+
// Snapshot the position being left so a later skip back within this session resumes
|
|
178
|
+
// correctly instead of restarting the track from its queue-build-time position.
|
|
179
|
+
audioTracks.getOrNull(fromIndex)?.startPositionMs = exoPlayer.currentPosition
|
|
180
|
+
val targetIndex = PlaylistPlaybackPolicy.nextSkipIndex(fromIndex, audioTracks.size, loop)
|
|
181
|
+
val targetPositionMs = audioTracks.getOrNull(targetIndex)?.startPositionMs ?: 0
|
|
182
|
+
exoPlayer.seekTo(targetIndex, targetPositionMs)
|
|
183
|
+
notifySkipForward(fromIndex)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
fun skipToPrevious() {
|
|
187
|
+
val exoPlayer = player ?: return
|
|
188
|
+
if (!isPreviousAvailable) {
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
val fromIndex = exoPlayer.currentMediaItemIndex
|
|
192
|
+
audioTracks.getOrNull(fromIndex)?.startPositionMs = exoPlayer.currentPosition
|
|
193
|
+
val targetIndex = PlaylistPlaybackPolicy.previousSkipIndex(fromIndex, audioTracks.size, loop)
|
|
194
|
+
val targetPositionMs = audioTracks.getOrNull(targetIndex)?.startPositionMs ?: 0
|
|
195
|
+
exoPlayer.seekTo(targetIndex, targetPositionMs)
|
|
196
|
+
notifySkipBack(fromIndex)
|
|
197
|
+
}
|
|
102
198
|
|
|
103
|
-
/*
|
|
104
|
-
* List management
|
|
105
|
-
*/
|
|
106
199
|
fun setAllItems(items: List<AudioTrack>?, options: PlaylistItemOptions) {
|
|
107
200
|
val seekStart = when {
|
|
108
201
|
options.playFromPosition >= 0 -> options.playFromPosition
|
|
109
|
-
options.retainPosition ->
|
|
202
|
+
options.retainPosition -> getCurrentProgress()?.position ?: 0
|
|
110
203
|
else -> 0
|
|
111
204
|
}
|
|
112
205
|
|
|
113
206
|
clearItems()
|
|
114
|
-
|
|
207
|
+
audioTracks.addAll(items.orEmpty())
|
|
208
|
+
player?.setMediaItems(audioTracks.map { AudioMediaItemFactory.fromAudioTrack(it) })
|
|
115
209
|
currentPosition = 0
|
|
116
210
|
|
|
117
|
-
// If the requested id exists, start there; otherwise keep the first track.
|
|
118
211
|
options.playFromId?.let { trackId ->
|
|
119
212
|
val position = findTrackPosition(trackId)
|
|
120
213
|
if (position != INVALID_POSITION) {
|
|
@@ -122,10 +215,6 @@ class PlaylistManager(application: Application) :
|
|
|
122
215
|
}
|
|
123
216
|
}
|
|
124
217
|
|
|
125
|
-
// We assume that if the playlist is fully loaded in one go,
|
|
126
|
-
// that the next thing to happen will be to play. So let's start
|
|
127
|
-
// paused, which will allow the player to pre-buffer until the
|
|
128
|
-
// user says Go.
|
|
129
218
|
beginPlayback(seekStart, options.startPaused)
|
|
130
219
|
}
|
|
131
220
|
|
|
@@ -142,19 +231,21 @@ class PlaylistManager(application: Application) :
|
|
|
142
231
|
|
|
143
232
|
if (insertIndex >= audioTracks.size) {
|
|
144
233
|
audioTracks.add(item)
|
|
234
|
+
player?.addMediaItem(AudioMediaItemFactory.fromAudioTrack(item))
|
|
145
235
|
} else {
|
|
146
236
|
audioTracks.add(insertIndex, item)
|
|
237
|
+
player?.addMediaItem(insertIndex, AudioMediaItemFactory.fromAudioTrack(item))
|
|
147
238
|
if (currentPosition >= insertIndex && currentPosition != INVALID_POSITION) {
|
|
148
239
|
currentPosition++
|
|
149
240
|
}
|
|
150
241
|
}
|
|
151
|
-
items = audioTracks
|
|
152
242
|
|
|
243
|
+
applyPlayerSettings()
|
|
153
244
|
if (countBefore == 0) {
|
|
154
245
|
currentPosition = 0
|
|
155
246
|
beginPlayback(1, true)
|
|
156
|
-
} else
|
|
157
|
-
|
|
247
|
+
} else {
|
|
248
|
+
playlistHandler?.updateMediaControls()
|
|
158
249
|
}
|
|
159
250
|
}
|
|
160
251
|
|
|
@@ -168,13 +259,10 @@ class PlaylistManager(application: Application) :
|
|
|
168
259
|
|
|
169
260
|
val item = audioTracks.removeAt(from)
|
|
170
261
|
audioTracks.add(to, item)
|
|
171
|
-
|
|
262
|
+
player?.moveMediaItem(from, to)
|
|
172
263
|
|
|
173
264
|
currentPosition = adjustCurrentIndexForMove(currentPosition, from, to, INVALID_POSITION)
|
|
174
|
-
|
|
175
|
-
if (this.playlistHandler != null) {
|
|
176
|
-
this.playlistHandler!!.updateMediaControls()
|
|
177
|
-
}
|
|
265
|
+
playlistHandler?.updateMediaControls()
|
|
178
266
|
return true
|
|
179
267
|
}
|
|
180
268
|
|
|
@@ -192,113 +280,115 @@ class PlaylistManager(application: Application) :
|
|
|
192
280
|
|
|
193
281
|
val isCurrent = existing == currentItem
|
|
194
282
|
val wasPlaying = isPlaying
|
|
195
|
-
val progress =
|
|
196
|
-
val seekPosition: Long =
|
|
283
|
+
val progress = getCurrentProgress()
|
|
284
|
+
val seekPosition: Long = progress?.position ?: 0
|
|
197
285
|
|
|
198
|
-
if (isCurrent
|
|
199
|
-
|
|
286
|
+
if (isCurrent) {
|
|
287
|
+
player?.pause()
|
|
200
288
|
}
|
|
201
289
|
|
|
202
290
|
audioTracks[resolvedIndex] = resolvedReplacement
|
|
203
|
-
|
|
291
|
+
player?.replaceMediaItem(resolvedIndex, AudioMediaItemFactory.fromAudioTrack(resolvedReplacement))
|
|
204
292
|
|
|
205
293
|
if (isCurrent) {
|
|
206
294
|
beginPlayback(seekPosition, !wasPlaying)
|
|
207
|
-
} else
|
|
208
|
-
|
|
295
|
+
} else {
|
|
296
|
+
playlistHandler?.updateMediaControls()
|
|
209
297
|
}
|
|
210
298
|
|
|
211
299
|
return resolvedReplacement
|
|
212
300
|
}
|
|
213
301
|
|
|
214
|
-
|
|
215
302
|
fun addAllItems(its: List<AudioTrack>?) {
|
|
216
|
-
val
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
303
|
+
val current = currentItem
|
|
304
|
+
its.orEmpty().forEach { track ->
|
|
305
|
+
audioTracks.add(track)
|
|
306
|
+
player?.addMediaItem(AudioMediaItemFactory.fromAudioTrack(track))
|
|
307
|
+
}
|
|
308
|
+
currentPosition = audioTracks.indexOf(current)
|
|
309
|
+
applyPlayerSettings()
|
|
221
310
|
}
|
|
222
311
|
|
|
223
312
|
fun removeItem(index: Int, itemId: String): AudioTrack? {
|
|
224
313
|
val wasPlaying = isPlaying
|
|
225
|
-
|
|
226
|
-
playlistHandler!!.pause(true)
|
|
227
|
-
}
|
|
228
|
-
var currentPosition = currentPosition
|
|
229
|
-
var foundItem: AudioTrack? = null
|
|
230
|
-
var removingCurrent = false
|
|
314
|
+
player?.pause()
|
|
231
315
|
|
|
232
|
-
|
|
233
|
-
val
|
|
234
|
-
val
|
|
316
|
+
var removingCurrent = false
|
|
317
|
+
val current = currentItem
|
|
318
|
+
val progress = getCurrentProgress()
|
|
319
|
+
val seekPosition: Long = progress?.position ?: 0
|
|
235
320
|
|
|
236
|
-
// If isPlaying is true, and currentItem is not null,
|
|
237
|
-
// that implies that currentItem is the currently playing item.
|
|
238
|
-
// If removingCurrent gets set to true, we are removing the currently playing item,
|
|
239
|
-
// and we need to restart playback once we do.
|
|
240
321
|
val resolvedIndex = resolveItemPosition(index, itemId)
|
|
322
|
+
var foundItem: AudioTrack? = null
|
|
241
323
|
if (resolvedIndex >= 0) {
|
|
242
324
|
foundItem = audioTracks[resolvedIndex]
|
|
243
|
-
if (foundItem ==
|
|
325
|
+
if (foundItem == current) {
|
|
244
326
|
removingCurrent = true
|
|
245
327
|
}
|
|
246
328
|
audioTracks.removeAt(resolvedIndex)
|
|
329
|
+
player?.removeMediaItem(resolvedIndex)
|
|
247
330
|
}
|
|
248
|
-
|
|
249
|
-
currentPosition = if (removingCurrent)
|
|
250
|
-
|
|
331
|
+
|
|
332
|
+
currentPosition = if (removingCurrent) {
|
|
333
|
+
(player?.currentMediaItemIndex ?: INVALID_POSITION).coerceAtLeast(0)
|
|
334
|
+
} else {
|
|
335
|
+
audioTracks.indexOf(current)
|
|
336
|
+
}
|
|
337
|
+
|
|
251
338
|
val seekStart = if (removingCurrent) 0 else seekPosition
|
|
339
|
+
applyPlayerSettings()
|
|
252
340
|
beginPlayback(seekStart, !wasPlaying)
|
|
253
|
-
|
|
254
|
-
this.playlistHandler!!.updateMediaControls()
|
|
255
|
-
}
|
|
341
|
+
playlistHandler?.updateMediaControls()
|
|
256
342
|
return foundItem
|
|
257
343
|
}
|
|
258
344
|
|
|
259
345
|
fun removeAllItems(its: ArrayList<TrackRemovalItem>): ArrayList<AudioTrack> {
|
|
260
346
|
val removedTracks = ArrayList<AudioTrack>()
|
|
261
347
|
val wasPlaying = isPlaying
|
|
262
|
-
|
|
263
|
-
playlistHandler!!.pause(true)
|
|
264
|
-
}
|
|
265
|
-
var currentPosition = currentPosition
|
|
266
|
-
val currentItem = currentItem // may be null
|
|
267
|
-
var removingCurrent = false
|
|
348
|
+
player?.pause()
|
|
268
349
|
|
|
269
|
-
|
|
270
|
-
val
|
|
271
|
-
val
|
|
350
|
+
var removingCurrent = false
|
|
351
|
+
val current = currentItem
|
|
352
|
+
val progress = getCurrentProgress()
|
|
353
|
+
val seekPosition: Long = progress?.position ?: 0
|
|
272
354
|
|
|
273
|
-
|
|
355
|
+
val indicesToRemove = its.mapNotNull { item ->
|
|
274
356
|
val resolvedIndex = resolveItemPosition(item.trackIndex, item.trackId)
|
|
275
|
-
if (resolvedIndex >= 0)
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
357
|
+
if (resolvedIndex >= 0) resolvedIndex else null
|
|
358
|
+
}.distinct().sortedDescending()
|
|
359
|
+
|
|
360
|
+
for (resolvedIndex in indicesToRemove) {
|
|
361
|
+
val foundItem = audioTracks[resolvedIndex]
|
|
362
|
+
if (foundItem == current) {
|
|
363
|
+
removingCurrent = true
|
|
282
364
|
}
|
|
365
|
+
removedTracks.add(foundItem)
|
|
366
|
+
audioTracks.removeAt(resolvedIndex)
|
|
367
|
+
player?.removeMediaItem(resolvedIndex)
|
|
283
368
|
}
|
|
284
|
-
|
|
285
|
-
currentPosition = if (removingCurrent)
|
|
286
|
-
|
|
369
|
+
|
|
370
|
+
currentPosition = if (removingCurrent) {
|
|
371
|
+
(player?.currentMediaItemIndex ?: INVALID_POSITION).coerceAtLeast(0)
|
|
372
|
+
} else {
|
|
373
|
+
audioTracks.indexOf(current)
|
|
374
|
+
}
|
|
375
|
+
|
|
287
376
|
val seekStart = if (removingCurrent) 0 else seekPosition
|
|
377
|
+
applyPlayerSettings()
|
|
288
378
|
beginPlayback(seekStart, !wasPlaying)
|
|
289
379
|
return removedTracks
|
|
290
380
|
}
|
|
291
381
|
|
|
292
382
|
fun clearItems() {
|
|
293
|
-
|
|
383
|
+
player?.stop()
|
|
384
|
+
player?.clearMediaItems()
|
|
294
385
|
audioTracks.clear()
|
|
295
|
-
items = audioTracks
|
|
296
386
|
currentPosition = INVALID_POSITION
|
|
387
|
+
sequentialErrors = 0
|
|
388
|
+
rmxPlaybackState = RmxPlaybackState.STOPPED
|
|
297
389
|
}
|
|
298
390
|
|
|
299
|
-
fun getAllItems(): List<AudioTrack>
|
|
300
|
-
return audioTracks.toList()
|
|
301
|
-
}
|
|
391
|
+
fun getAllItems(): List<AudioTrack> = audioTracks.toList()
|
|
302
392
|
|
|
303
393
|
private fun resolveItemPosition(trackIndex: Int, trackId: String): Int {
|
|
304
394
|
return when {
|
|
@@ -311,13 +401,9 @@ class PlaylistManager(application: Application) :
|
|
|
311
401
|
internal fun findTrackPosition(trackId: String): Int =
|
|
312
402
|
audioTracks.indexOfFirst { it.trackId == trackId }
|
|
313
403
|
|
|
314
|
-
fun getVolumeLeft(): Float
|
|
315
|
-
return volumeLeft
|
|
316
|
-
}
|
|
404
|
+
fun getVolumeLeft(): Float = volumeLeft
|
|
317
405
|
|
|
318
|
-
fun getVolumeRight(): Float
|
|
319
|
-
return volumeRight
|
|
320
|
-
}
|
|
406
|
+
fun getVolumeRight(): Float = volumeRight
|
|
321
407
|
|
|
322
408
|
fun setVolume(
|
|
323
409
|
@FloatRange(from = 0.0, to = 1.0) left: Float,
|
|
@@ -325,56 +411,218 @@ class PlaylistManager(application: Application) :
|
|
|
325
411
|
) {
|
|
326
412
|
volumeLeft = left
|
|
327
413
|
volumeRight = right
|
|
328
|
-
|
|
329
|
-
Log.i("PlaylistManager", "setVolume completing with volume = $left")
|
|
330
|
-
currentMediaPlayer!!.get()!!.setVolume(volumeLeft, volumeRight)
|
|
331
|
-
}
|
|
414
|
+
player?.volume = (left + right) / 2f
|
|
332
415
|
}
|
|
333
416
|
|
|
334
|
-
fun getPlaybackSpeed(): Float
|
|
335
|
-
return playbackSpeed
|
|
336
|
-
}
|
|
417
|
+
fun getPlaybackSpeed(): Float = playbackSpeed
|
|
337
418
|
|
|
338
419
|
fun setPlaybackSpeed(@FloatRange(from = 0.0625, to = 16.0) speed: Float) {
|
|
339
420
|
val validSpeed = speed.coerceIn(0.0625f, 16.0f)
|
|
340
421
|
playbackSpeed = validSpeed
|
|
341
|
-
|
|
342
|
-
handler.currentMediaPlayer?.let { mediaPlayer ->
|
|
343
|
-
if (mediaPlayer is AudioApi) {
|
|
344
|
-
Log.i(TAG, "setPlaybackSpeed completing with speed = $validSpeed")
|
|
345
|
-
mediaPlayer.setPlaybackSpeed(playbackSpeed)
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
}
|
|
422
|
+
player?.setPlaybackSpeed(validSpeed)
|
|
349
423
|
}
|
|
350
424
|
|
|
351
425
|
fun beginPlayback(@IntRange(from = 0) seekPosition: Long, startPaused: Boolean) {
|
|
352
|
-
|
|
426
|
+
if (audioTracks.isEmpty()) {
|
|
427
|
+
return
|
|
428
|
+
}
|
|
429
|
+
beginPlaybackAt(seekPosition, startPaused)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
fun beginPlaybackAt(@IntRange(from = 0) seekPosition: Long, startPaused: Boolean) {
|
|
433
|
+
if (audioTracks.isEmpty()) {
|
|
434
|
+
return
|
|
435
|
+
}
|
|
353
436
|
try {
|
|
354
|
-
|
|
437
|
+
ensureServiceStarted()
|
|
438
|
+
val exoPlayer = player
|
|
439
|
+
if (exoPlayer == null) {
|
|
440
|
+
pendingBeginPlayback = PendingBegin(seekPosition, startPaused)
|
|
441
|
+
return
|
|
442
|
+
}
|
|
443
|
+
val index = currentPosition.coerceIn(0, audioTracks.size - 1)
|
|
444
|
+
currentPosition = index
|
|
445
|
+
exoPlayer.repeatMode = PlaylistPlaybackPolicy.repeatMode(loop, audioTracks.size)
|
|
446
|
+
exoPlayer.seekTo(index, seekPosition)
|
|
447
|
+
exoPlayer.prepare()
|
|
448
|
+
exoPlayer.playWhenReady = !startPaused && !videoHandoffForegroundRetain
|
|
449
|
+
applyPlayerSettings()
|
|
450
|
+
if (MediaNotificationPolicy.shouldRequestLegacyAudioFocus()) {
|
|
451
|
+
// Media3 handleAudioFocus owns pause/resume; do not call AudioManager.requestAudioFocus.
|
|
452
|
+
}
|
|
453
|
+
ensureForeground()
|
|
355
454
|
} catch (e: IllegalStateException) {
|
|
356
|
-
// Android 12+: BackgroundServiceStartNotAllowedException when app is backgrounded
|
|
357
455
|
Log.w(TAG, "beginPlayback: cannot start MediaService while backgrounded: ${e.message}")
|
|
456
|
+
} catch (e: Exception) {
|
|
457
|
+
Log.w(TAG, "beginPlayback: ${e.message}")
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
fun ensureServiceStarted() {
|
|
462
|
+
if (!MediaNotificationPolicy.shouldStartForegroundService(MediaService.instance != null)) {
|
|
358
463
|
return
|
|
359
464
|
}
|
|
465
|
+
val intent = Intent(application, MediaService::class.java)
|
|
360
466
|
try {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
467
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
468
|
+
application.startForegroundService(intent)
|
|
469
|
+
} else {
|
|
470
|
+
application.startService(intent)
|
|
471
|
+
}
|
|
472
|
+
} catch (e: IllegalStateException) {
|
|
473
|
+
Log.w(TAG, "ensureServiceStarted: ${e.message}")
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
fun ensureForeground() {
|
|
478
|
+
// Media3 MediaSessionService owns FGS; do not startForeground in parallel.
|
|
479
|
+
mediaServiceRef.get()?.promoteToForeground()
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
fun updateForegroundNotification() {
|
|
483
|
+
mediaServiceRef.get()?.updateForegroundNotification()
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
private fun applyPlayerSettings() {
|
|
487
|
+
player?.let { exoPlayer ->
|
|
488
|
+
exoPlayer.volume = (volumeLeft + volumeRight) / 2f
|
|
489
|
+
exoPlayer.setPlaybackSpeed(playbackSpeed)
|
|
490
|
+
exoPlayer.repeatMode = PlaylistPlaybackPolicy.repeatMode(loop, audioTracks.size)
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
private fun notifySkipForward(fromIndex: Int) {
|
|
495
|
+
val listener = mediaControlsListener.get()
|
|
496
|
+
val item = if (fromIndex in audioTracks.indices) audioTracks[fromIndex] else currentItem
|
|
497
|
+
listener?.onNext(item, fromIndex)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
private fun notifySkipBack(fromIndex: Int) {
|
|
501
|
+
val listener = mediaControlsListener.get()
|
|
502
|
+
val item = if (fromIndex in audioTracks.indices) audioTracks[fromIndex] else currentItem
|
|
503
|
+
listener?.onPrevious(item, fromIndex)
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
private val playerListener = object : Player.Listener {
|
|
507
|
+
override fun onPlaybackStateChanged(playbackState: Int) {
|
|
508
|
+
rmxPlaybackState = mapPlaybackState(playbackState)
|
|
509
|
+
playbackStatusListener.get()?.onNativePlaybackStateChanged(rmxPlaybackState)
|
|
510
|
+
if (playbackState == Player.STATE_ENDED) {
|
|
511
|
+
handleItemEnded()
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
|
|
516
|
+
MediaNotificationPolicy.applyRetainPlayWhenReadyGuard(
|
|
517
|
+
playWhenReady,
|
|
518
|
+
videoHandoffForegroundRetain,
|
|
519
|
+
VideoPlayerBridge.hasActivePlayer()
|
|
520
|
+
) {
|
|
521
|
+
player?.playWhenReady = false
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
|
526
|
+
if (isPlaying) {
|
|
527
|
+
sequentialErrors = 0
|
|
528
|
+
rmxPlaybackState = RmxPlaybackState.PLAYING
|
|
529
|
+
} else if (player?.playbackState == Player.STATE_READY) {
|
|
530
|
+
rmxPlaybackState = RmxPlaybackState.PAUSED
|
|
531
|
+
}
|
|
532
|
+
playbackStatusListener.get()?.onNativePlaybackStateChanged(rmxPlaybackState)
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
|
536
|
+
val previousIndex = currentPosition
|
|
537
|
+
currentPosition = player?.currentMediaItemIndex ?: currentPosition
|
|
538
|
+
if (PlaylistPlaybackPolicy.isNaturalItemEnd(reason) && previousIndex in audioTracks.indices) {
|
|
539
|
+
notifyItemCompleted(
|
|
540
|
+
audioTracks[previousIndex],
|
|
541
|
+
PlaylistPlaybackPolicy.nextAvailable(previousIndex, audioTracks.size, loop)
|
|
542
|
+
)
|
|
543
|
+
}
|
|
544
|
+
playbackStatusListener.get()?.onNativeTrackChanged(
|
|
545
|
+
currentItem,
|
|
546
|
+
isNextAvailable,
|
|
547
|
+
isPreviousAvailable
|
|
548
|
+
)
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
override fun onPositionDiscontinuity(
|
|
552
|
+
oldPosition: Player.PositionInfo,
|
|
553
|
+
newPosition: Player.PositionInfo,
|
|
554
|
+
reason: Int
|
|
555
|
+
) {
|
|
556
|
+
if (reason == Player.DISCONTINUITY_REASON_SEEK) {
|
|
557
|
+
seeking = true
|
|
558
|
+
rmxPlaybackState = RmxPlaybackState.SEEKING
|
|
559
|
+
playbackStatusListener.get()?.onNativePlaybackStateChanged(rmxPlaybackState)
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
override fun onPlayerError(error: androidx.media3.common.PlaybackException) {
|
|
564
|
+
currentErrorTrack = currentItem
|
|
565
|
+
rmxPlaybackState = RmxPlaybackState.ERROR
|
|
566
|
+
playbackStatusListener.get()?.onNativePlayerError(error)
|
|
567
|
+
skipFailedItem()
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/** PlaylistCore moved past a failing item (up to [PlaylistPlaybackPolicy.MAX_SEQUENTIAL_ERRORS] in a row). */
|
|
572
|
+
private fun skipFailedItem() {
|
|
573
|
+
val exoPlayer = player ?: return
|
|
574
|
+
sequentialErrors++
|
|
575
|
+
val failedIndex = exoPlayer.currentMediaItemIndex
|
|
576
|
+
val hasNext = PlaylistPlaybackPolicy.nextAvailable(failedIndex, audioTracks.size, loop)
|
|
577
|
+
if (!PlaylistPlaybackPolicy.shouldSkipAfterError(sequentialErrors, hasNext)) {
|
|
578
|
+
return
|
|
579
|
+
}
|
|
580
|
+
val resume = PlaylistPlaybackPolicy.playWhenReadyAfterErrorSkip(exoPlayer.playWhenReady, failedIndex)
|
|
581
|
+
val targetIndex = PlaylistPlaybackPolicy.errorSkipWindowIndex(failedIndex, audioTracks.size)
|
|
582
|
+
exoPlayer.seekTo(targetIndex, 0)
|
|
583
|
+
exoPlayer.prepare()
|
|
584
|
+
exoPlayer.playWhenReady = resume && !videoHandoffForegroundRetain
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/** Former PlaylistCore order on a natural item end: COMPLETED, PLAYLIST_COMPLETED when last, then STOPPED. */
|
|
588
|
+
private fun notifyItemCompleted(track: AudioTrack, nextAvailable: Boolean) {
|
|
589
|
+
playbackStatusListener.get()?.onNativeItemCompleted(track, nextAvailable)
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
private fun mapPlaybackState(playbackState: Int): RmxPlaybackState {
|
|
593
|
+
return when (playbackState) {
|
|
594
|
+
Player.STATE_IDLE -> RmxPlaybackState.STOPPED
|
|
595
|
+
Player.STATE_BUFFERING -> {
|
|
596
|
+
if (player?.isPlaying == true) RmxPlaybackState.PLAYING else RmxPlaybackState.PREPARING
|
|
597
|
+
}
|
|
598
|
+
Player.STATE_READY -> {
|
|
599
|
+
if (seeking) {
|
|
600
|
+
seeking = false
|
|
601
|
+
if (player?.isPlaying == true) RmxPlaybackState.PLAYING else RmxPlaybackState.PAUSED
|
|
602
|
+
} else if (player?.isPlaying == true) {
|
|
603
|
+
RmxPlaybackState.PLAYING
|
|
604
|
+
} else {
|
|
605
|
+
RmxPlaybackState.PAUSED
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
Player.STATE_ENDED -> RmxPlaybackState.STOPPED
|
|
609
|
+
else -> RmxPlaybackState.PREPARING
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
private fun handleItemEnded() {
|
|
614
|
+
val track = currentItem
|
|
615
|
+
val nextAvailable = isNextAvailable
|
|
616
|
+
track?.let { notifyItemCompleted(it, nextAvailable) }
|
|
617
|
+
if (!nextAvailable) {
|
|
618
|
+
playbackStatusListener.get()?.onNativePlaylistCompleted()
|
|
365
619
|
}
|
|
366
620
|
}
|
|
367
621
|
|
|
368
622
|
companion object {
|
|
623
|
+
const val INVALID_POSITION = -1
|
|
369
624
|
private const val TAG = "PlaylistManager"
|
|
370
625
|
|
|
371
|
-
/**
|
|
372
|
-
* Computes the new position of the "current" item after moving an item from index
|
|
373
|
-
* [from] to index [to] (post-removal insertion semantics, i.e. matching
|
|
374
|
-
* `MutableList.removeAt(from)` followed by `MutableList.add(to, item)`).
|
|
375
|
-
*
|
|
376
|
-
* Pure function so it can be unit-tested without an Android/Application context.
|
|
377
|
-
*/
|
|
378
626
|
@JvmStatic
|
|
379
627
|
fun adjustCurrentIndexForMove(currentIndex: Int, from: Int, to: Int, invalidPosition: Int): Int {
|
|
380
628
|
if (currentIndex == invalidPosition) {
|
|
@@ -387,13 +635,6 @@ class PlaylistManager(application: Application) :
|
|
|
387
635
|
return if (mid >= to) mid + 1 else mid
|
|
388
636
|
}
|
|
389
637
|
|
|
390
|
-
/**
|
|
391
|
-
* Builds the [AudioTrack] that should replace [existing], backfilling `trackId` from
|
|
392
|
-
* [existing] when [replacement] doesn't specify one (an omitted id signals "keep the
|
|
393
|
-
* existing id" rather than "generate a new one").
|
|
394
|
-
*
|
|
395
|
-
* Pure function so it can be unit-tested without an Android/Application context.
|
|
396
|
-
*/
|
|
397
638
|
@JvmStatic
|
|
398
639
|
fun mergeReplacementTrackId(existing: AudioTrack, replacement: AudioTrack): AudioTrack {
|
|
399
640
|
val replacementConfig = replacement.toDict()
|
|
@@ -403,10 +644,4 @@ class PlaylistManager(application: Application) :
|
|
|
403
644
|
return AudioTrack(replacementConfig)
|
|
404
645
|
}
|
|
405
646
|
}
|
|
406
|
-
|
|
407
|
-
init {
|
|
408
|
-
setParameters(audioTracks, 0)
|
|
409
|
-
options = Options(application.baseContext)
|
|
410
|
-
}
|
|
411
|
-
|
|
412
647
|
}
|