capacitor-plugin-playlist 0.13.0 → 0.13.2

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.
@@ -216,6 +216,7 @@ class PlaylistManager(private val application: Application) {
216
216
  }
217
217
 
218
218
  beginPlayback(seekStart, options.startPaused)
219
+ mediaServiceRef.get()?.refreshSkipAvailability()
219
220
  }
220
221
 
221
222
  fun addItem(item: AudioTrack?, index: Int = -1) {
@@ -441,6 +442,13 @@ class PlaylistManager(private val application: Application) {
441
442
  return
442
443
  }
443
444
  val index = currentPosition.coerceIn(0, audioTracks.size - 1)
445
+ // Snapshot the position of whatever track is being switched away from — covers the
446
+ // JS "tap a different playlist item" path (playTrackById), not just native skip, so a
447
+ // later native skip back to that track resumes correctly regardless of how it was left.
448
+ val fromIndex = exoPlayer.currentMediaItemIndex
449
+ if (fromIndex != index) {
450
+ audioTracks.getOrNull(fromIndex)?.startPositionMs = exoPlayer.currentPosition
451
+ }
444
452
  currentPosition = index
445
453
  exoPlayer.repeatMode = PlaylistPlaybackPolicy.repeatMode(loop, audioTracks.size)
446
454
  exoPlayer.seekTo(index, seekPosition)
@@ -546,6 +554,7 @@ class PlaylistManager(private val application: Application) {
546
554
  isNextAvailable,
547
555
  isPreviousAvailable
548
556
  )
557
+ mediaServiceRef.get()?.refreshSkipAvailability()
549
558
  }
550
559
 
551
560
  override fun onPositionDiscontinuity(
@@ -123,6 +123,27 @@ internal class HandoffForwardingPlayer(
123
123
  onSkipToPrevious?.invoke() ?: super.seekToPreviousMediaItem()
124
124
  }
125
125
 
126
+ /**
127
+ * The system media notification and hardware media buttons invoke this — Player's "smart"
128
+ * seek, not [seekToNextMediaItem] — so without this override they fall through to
129
+ * [ForwardingPlayer]'s default (the wrapped ExoPlayer's own seekToNext), bypassing
130
+ * [onSkipToNext] and its saved-position resume entirely.
131
+ */
132
+ override fun seekToNext() {
133
+ if (videoPlayer() != null) {
134
+ return
135
+ }
136
+ onSkipToNext?.invoke() ?: super.seekToNext()
137
+ }
138
+
139
+ /** See [seekToNext] — the system-control counterpart of [seekToPreviousMediaItem]. */
140
+ override fun seekToPrevious() {
141
+ if (videoPlayer() != null) {
142
+ return
143
+ }
144
+ onSkipToPrevious?.invoke() ?: super.seekToPrevious()
145
+ }
146
+
126
147
  override fun getAvailableCommands(): Player.Commands {
127
148
  val previous = previousAvailable
128
149
  val next = nextAvailable
@@ -219,6 +219,36 @@ class MediaService : MediaSessionService() {
219
219
  triggerNotificationUpdate()
220
220
  }
221
221
 
222
+ /**
223
+ * Re-grants the media notification controller's skip commands to match current queue state.
224
+ * [PlaylistMediaSessionCallback.onConnect] only runs once per connection (typically before any
225
+ * track is loaded), so without this the system notification and hardware media buttons stay
226
+ * permanently frozen at whatever previous/next availability existed at connect time.
227
+ */
228
+ fun refreshSkipAvailability() {
229
+ val session = mediaSession ?: return
230
+ val controller = session.mediaNotificationControllerInfo ?: return
231
+ val retain = playlistManager.videoHandoffForegroundRetain
232
+ val videoAttached = VideoPlayerBridge.hasActivePlayer()
233
+ session.setAvailableCommands(
234
+ controller,
235
+ MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS,
236
+ MediaNotificationPolicy.playerCommandsForMediaNotification(
237
+ MediaSession.ConnectionResult.DEFAULT_PLAYER_COMMANDS,
238
+ MediaNotificationPolicy.mediaNotificationSkipPreviousEnabled(
239
+ playlistManager.isPreviousAvailable,
240
+ retain,
241
+ videoAttached
242
+ ),
243
+ MediaNotificationPolicy.mediaNotificationSkipNextEnabled(
244
+ playlistManager.isNextAvailable,
245
+ retain,
246
+ videoAttached
247
+ )
248
+ )
249
+ )
250
+ }
251
+
222
252
  /** Re-reads `options.icon` after `setOptions` so a running service picks up the app's icon. */
223
253
  fun applyNotificationIcon() {
224
254
  notificationProvider?.setSmallIcon(smallIconRes())
@@ -75,6 +75,44 @@ class HandoffForwardingPlayerTest {
75
75
  assertEquals(0, recording.seekToPreviousCount)
76
76
  }
77
77
 
78
+ /**
79
+ * seekToNext/seekToPrevious (not the MediaItem variants) are what the system media
80
+ * notification and hardware media buttons actually invoke — regression coverage for the
81
+ * bug where these fell through to the wrapped player's own default and bypassed the
82
+ * playlist's skip/position-resume logic entirely.
83
+ */
84
+ @Test
85
+ fun seekToNext_delegatesToCallback() {
86
+ var skipNextCount = 0
87
+ val recording = RecordingPlayer()
88
+ val forwarding = HandoffForwardingPlayer(
89
+ recording.proxy,
90
+ retain = { false },
91
+ onSkipToNext = { skipNextCount++ }
92
+ )
93
+
94
+ forwarding.seekToNext()
95
+
96
+ assertEquals(1, skipNextCount)
97
+ assertEquals(0, recording.seekToNextSmartCount)
98
+ }
99
+
100
+ @Test
101
+ fun seekToPrevious_delegatesToCallback() {
102
+ var skipPreviousCount = 0
103
+ val recording = RecordingPlayer()
104
+ val forwarding = HandoffForwardingPlayer(
105
+ recording.proxy,
106
+ retain = { false },
107
+ onSkipToPrevious = { skipPreviousCount++ }
108
+ )
109
+
110
+ forwarding.seekToPrevious()
111
+
112
+ assertEquals(1, skipPreviousCount)
113
+ assertEquals(0, recording.seekToPreviousSmartCount)
114
+ }
115
+
78
116
  @Test
79
117
  fun retainWithVideoAttached_playReachesVideoPlayer() {
80
118
  val audio = RecordingPlayer()
@@ -153,10 +191,13 @@ class HandoffForwardingPlayerTest {
153
191
 
154
192
  forwarding.seekToNextMediaItem()
155
193
  forwarding.seekToPreviousMediaItem()
194
+ forwarding.seekToNext()
195
+ forwarding.seekToPrevious()
156
196
 
157
197
  assertEquals(0, skipNextCount)
158
198
  assertEquals(0, skipPreviousCount)
159
199
  assertEquals(0, video.seekToNextCount)
200
+ assertEquals(0, video.seekToNextSmartCount)
160
201
  }
161
202
 
162
203
  @Test
@@ -224,6 +265,8 @@ class HandoffForwardingPlayerTest {
224
265
  var pauseCount = 0
225
266
  var seekToNextCount = 0
226
267
  var seekToPreviousCount = 0
268
+ var seekToNextSmartCount = 0
269
+ var seekToPreviousSmartCount = 0
227
270
  val seekToPositionMsCalls = mutableListOf<Long>()
228
271
  val setPlayWhenReadyCalls = mutableListOf<Boolean>()
229
272
  val proxy: Player = Proxy.newProxyInstance(
@@ -260,6 +303,14 @@ class HandoffForwardingPlayerTest {
260
303
  seekToPreviousCount++
261
304
  null
262
305
  }
306
+ "seekToNext" -> {
307
+ seekToNextSmartCount++
308
+ null
309
+ }
310
+ "seekToPrevious" -> {
311
+ seekToPreviousSmartCount++
312
+ null
313
+ }
263
314
  "isPlaying", "getIsPlaying" -> isPlayingValue
264
315
  "isPlayWhenReady", "getPlayWhenReady" -> playWhenReady
265
316
  "getPlaybackState" -> playbackState
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capacitor-plugin-playlist",
3
- "version": "0.13.0",
3
+ "version": "0.13.2",
4
4
  "description": "Playlist ",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",