capacitor-plugin-playlist 0.11.4 → 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.
Files changed (53) hide show
  1. package/README.md +133 -127
  2. package/android/build.gradle +20 -15
  3. package/android/gradle/wrapper/gradle-wrapper.properties +1 -1
  4. package/android/src/main/java/org/dwbn/plugins/playlist/AudioMediaItemFactory.java +96 -0
  5. package/android/src/main/java/org/dwbn/plugins/playlist/PlaylistPlugin.kt +3 -3
  6. package/android/src/main/java/org/dwbn/plugins/playlist/RmxAudioPlayer.java +145 -188
  7. package/android/src/main/java/org/dwbn/plugins/playlist/RmxPlaybackErrorMapper.kt +26 -0
  8. package/android/src/main/java/org/dwbn/plugins/playlist/data/AudioTrack.kt +28 -28
  9. package/android/src/main/java/org/dwbn/plugins/playlist/handoff/VideoPlayerBridge.kt +102 -0
  10. package/android/src/main/java/org/dwbn/plugins/playlist/manager/PlaybackProgress.kt +8 -0
  11. package/android/src/main/java/org/dwbn/plugins/playlist/manager/PlaylistManager.kt +418 -183
  12. package/android/src/main/java/org/dwbn/plugins/playlist/manager/PlaylistPlaybackPolicy.kt +98 -0
  13. package/android/src/main/java/org/dwbn/plugins/playlist/manager/RmxPlaybackState.kt +11 -0
  14. package/android/src/main/java/org/dwbn/plugins/playlist/playlist/AudioPlaylistHandler.java +107 -211
  15. package/android/src/main/java/org/dwbn/plugins/playlist/service/GlideBitmapLoader.kt +121 -0
  16. package/android/src/main/java/org/dwbn/plugins/playlist/service/HandoffForwardingPlayer.kt +185 -0
  17. package/android/src/main/java/org/dwbn/plugins/playlist/service/MediaNotificationPolicy.kt +253 -0
  18. package/android/src/main/java/org/dwbn/plugins/playlist/service/MediaService.kt +260 -68
  19. package/android/src/main/java/org/dwbn/plugins/playlist/service/PlaylistMediaSessionCallback.kt +39 -0
  20. package/android/src/test/java/org/dwbn/plugins/playlist/AudioMediaItemFactoryMimeTest.kt +98 -0
  21. package/android/src/test/java/org/dwbn/plugins/playlist/AudioMediaItemFactoryTest.java +71 -0
  22. package/android/src/test/java/org/dwbn/plugins/playlist/data/AudioTrackTest.kt +53 -0
  23. package/android/src/test/java/org/dwbn/plugins/playlist/handoff/VideoPlayerBridgeTest.kt +84 -0
  24. package/android/src/test/java/org/dwbn/plugins/playlist/manager/PlaylistPlaybackPolicyTest.kt +128 -0
  25. package/android/src/test/java/org/dwbn/plugins/playlist/playlist/AudioPlaylistHandlerTest.kt +145 -0
  26. package/android/src/test/java/org/dwbn/plugins/playlist/service/GlideBitmapLoaderTest.kt +76 -0
  27. package/android/src/test/java/org/dwbn/plugins/playlist/service/HandoffForwardingPlayerTest.kt +282 -0
  28. package/android/src/test/java/org/dwbn/plugins/playlist/service/MediaNotificationPolicyTest.kt +285 -0
  29. package/android/src/test/java/org/dwbn/plugins/playlist/service/VideoHandoffNotificationCommandsTest.kt +54 -0
  30. package/android/src/test/java/org/dwbn/plugins/playlist/service/VideoHandoffNotificationPolicyTest.kt +47 -0
  31. package/dist/docs.json +12 -846
  32. package/dist/esm/RmxAudioPlayer.d.ts +2 -2
  33. package/dist/esm/RmxAudioPlayer.js.map +1 -1
  34. package/dist/esm/definitions.d.ts +7 -7
  35. package/dist/esm/interfaces.d.ts +7 -1
  36. package/dist/esm/utils.d.ts +1 -1
  37. package/dist/esm/utils.js +2 -2
  38. package/dist/esm/utils.js.map +1 -1
  39. package/dist/esm/web.d.ts +3 -3
  40. package/dist/esm/web.js +5 -5
  41. package/dist/esm/web.js.map +1 -1
  42. package/dist/plugin.cjs.js +7 -7
  43. package/dist/plugin.cjs.js.map +1 -1
  44. package/dist/plugin.js +7 -7
  45. package/dist/plugin.js.map +1 -1
  46. package/ios/Sources/PlaylistPlugin/AVBidirectionalQueuePlayer.swift +45 -6
  47. package/ios/Sources/PlaylistPlugin/AudioTrack.swift +15 -2
  48. package/ios/Tests/PlaylistPluginTests/PositionResumeTests.swift +82 -0
  49. package/package.json +15 -19
  50. package/android/src/main/java/org/dwbn/plugins/playlist/notification/PlaylistNotificationProvider.kt +0 -26
  51. package/android/src/main/java/org/dwbn/plugins/playlist/playlist/AudioApi.kt +0 -114
  52. package/android/src/main/java/org/dwbn/plugins/playlist/playlist/BaseMediaApi.kt +0 -36
  53. package/android/src/main/java/org/dwbn/plugins/playlist/service/MediaImageProvider.kt +0 -83
@@ -1,61 +1,60 @@
1
1
  package org.dwbn.plugins.playlist;
2
2
 
3
3
  import android.content.Context;
4
+ import android.os.Handler;
5
+ import android.os.Looper;
4
6
  import android.util.Log;
5
7
 
6
8
  import androidx.annotation.NonNull;
7
9
  import androidx.annotation.Nullable;
8
10
  import androidx.annotation.OptIn;
11
+ import androidx.media3.common.PlaybackException;
12
+ import androidx.media3.common.Player;
9
13
  import androidx.media3.common.util.UnstableApi;
10
- import androidx.media3.exoplayer.ExoPlaybackException;
11
-
12
- import com.devbrackets.android.exomedia.listener.OnErrorListener;
13
- import com.devbrackets.android.playlistcore.data.MediaProgress;
14
- import com.devbrackets.android.playlistcore.data.PlaybackState;
15
- import com.devbrackets.android.playlistcore.data.PlaylistItemChange;
16
- import com.devbrackets.android.playlistcore.listener.PlaybackStatusListener;
17
- import com.devbrackets.android.playlistcore.listener.PlaylistListener;
18
- import com.devbrackets.android.playlistcore.listener.ProgressListener;
14
+ import androidx.media3.exoplayer.ExoPlayer;
19
15
 
20
16
  import org.dwbn.plugins.playlist.data.AudioTrack;
21
17
  import org.dwbn.plugins.playlist.manager.MediaControlsListener;
22
18
  import org.dwbn.plugins.playlist.manager.Options;
19
+ import org.dwbn.plugins.playlist.manager.PlaybackProgress;
23
20
  import org.dwbn.plugins.playlist.manager.PlaylistManager;
21
+ import org.dwbn.plugins.playlist.manager.RmxPlaybackState;
24
22
  import org.dwbn.plugins.playlist.playlist.AudioPlaylistHandler;
23
+ import org.dwbn.plugins.playlist.service.MediaNotificationPolicy;
25
24
  import org.dwbn.plugins.playlist.service.MediaService;
26
25
  import org.json.JSONException;
27
26
  import org.json.JSONObject;
28
27
 
29
- /**
30
- * The implementation of this player borrows from ExoMedia's demo example
31
- * and utilizes heavily those classes, basically because that is "the" way
32
- * to actually use ExoMedia.
33
- */
34
- public class RmxAudioPlayer implements PlaybackStatusListener<AudioTrack>,
35
- PlaylistListener<AudioTrack>, ProgressListener, OnErrorListener, MediaControlsListener {
28
+ @OptIn(markerClass = UnstableApi.class)
29
+ public class RmxAudioPlayer implements MediaControlsListener {
36
30
 
37
31
  public static String TAG = "PlaylistRmxAudioPlayer";
38
-
39
- // PlaylistCore requires this but we don't use it
40
- // It would be used to switch between playlists. I guess we could
41
- // support that in the future, might be cool.
42
32
  private static final int PLAYLIST_ID = 32;
33
+ private static final long POSITION_TICK_MS = 1000L;
34
+
43
35
  private final PlaylistManager playlistManager;
44
36
  private final OnStatusReportListener statusListener;
37
+ private final Context context;
38
+ private final Handler mainHandler = new Handler(Looper.getMainLooper());
45
39
 
46
40
  private int lastBufferPercent = 0;
47
41
  private long lastDuration = 0;
48
42
  private boolean trackLoaded = false;
49
43
  private boolean resetStreamOnPause = true;
50
- private final Context context;
44
+ private boolean listenersRegistered = false;
45
+ private ExoPlayer attachedPlayer;
46
+
47
+ private final Runnable positionTicker = new Runnable() {
48
+ @Override
49
+ public void run() {
50
+ emitPositionIfNeeded();
51
+ if (listenersRegistered) {
52
+ mainHandler.postDelayed(this, POSITION_TICK_MS);
53
+ }
54
+ }
55
+ };
51
56
 
52
57
  public RmxAudioPlayer(@NonNull OnStatusReportListener statusListener, @NonNull Context context) {
53
- // AudioPlayerPlugin and RmxAudioPlayer are separate classes in order to increase
54
- // the portability of this code.
55
- // Because AudioPlayerPlugin itself holds a strong reference to this class,
56
- // we can hold a strong reference to this shared callback. Normally not a good idea
57
- // but these two objects will always live together (And the plugin couldn't function
58
- // at all if this one gets garbage collected).
59
58
  this.statusListener = statusListener;
60
59
  this.context = context.getApplicationContext();
61
60
  this.playlistManager = PlaylistRuntime.getPlaylistManager(this.context);
@@ -72,7 +71,7 @@ public class RmxAudioPlayer implements PlaybackStatusListener<AudioTrack>,
72
71
 
73
72
  public boolean getResetStreamOnPause() {
74
73
  return resetStreamOnPause;
75
- }
74
+ }
76
75
 
77
76
  public void setResetStreamOnPause(boolean val) {
78
77
  resetStreamOnPause = val;
@@ -104,14 +103,18 @@ public class RmxAudioPlayer implements PlaybackStatusListener<AudioTrack>,
104
103
  playlistManager.setVolume(left, right);
105
104
  }
106
105
 
107
- public void onCompletion(AudioTrack item) {
106
+ /**
107
+ * A track played to its end. {@code nextAvailable} is judged from the finished track, because
108
+ * Media3 has already moved to the next item when it reports the transition.
109
+ */
110
+ public void onCompletion(AudioTrack item, boolean nextAvailable) {
108
111
  if (item != null) {
109
112
  String trackId = item.getTrackId();
110
113
  JSONObject trackStatus = getPlayerStatus(item);
111
114
  onStatus(RmxAudioStatusMessage.RMXSTATUS_COMPLETED, trackId, trackStatus);
112
115
  }
113
116
 
114
- if (!playlistManager.isNextAvailable()) {
117
+ if (!nextAvailable) {
115
118
  onStatus(RmxAudioStatusMessage.RMXSTATUS_PLAYLIST_COMPLETED, "INVALID", null);
116
119
  }
117
120
  }
@@ -145,26 +148,12 @@ public class RmxAudioPlayer implements PlaybackStatusListener<AudioTrack>,
145
148
  onStatus(RmxAudioStatusMessage.RMX_STATUS_SKIP_FORWARD, trackId, param);
146
149
  }
147
150
 
148
- @OptIn(markerClass = UnstableApi.class) @Override
149
- public boolean onError(Exception e) {
150
- String errorMsg = e.toString();
151
- RmxAudioErrorType errorType = RmxAudioErrorType.RMXERR_NONE_SUPPORTED;
152
-
153
- if (e instanceof ExoPlaybackException) {
154
- switch (((ExoPlaybackException) e).type) {
155
- case ExoPlaybackException.TYPE_SOURCE:
156
- errorMsg = "ExoPlaybackException.TYPE_SOURCE: " + ((ExoPlaybackException) e).getSourceException().getMessage();
157
- break;
158
- case ExoPlaybackException.TYPE_RENDERER:
159
- errorType = RmxAudioErrorType.RMXERR_DECODE;
160
- errorMsg = "ExoPlaybackException.TYPE_RENDERER: " + ((ExoPlaybackException) e).getRendererException().getMessage();
161
- break;
162
- case ExoPlaybackException.TYPE_UNEXPECTED:
163
- errorType = RmxAudioErrorType.RMXERR_DECODE;
164
- errorMsg = "ExoPlaybackException.TYPE_UNEXPECTED: " + ((ExoPlaybackException) e).getUnexpectedException().getMessage();
165
- break;
166
- }
167
- }
151
+ @OptIn(markerClass = UnstableApi.class)
152
+ public void onNativePlayerError(PlaybackException e) {
153
+ RmxAudioErrorType errorType = RmxPlaybackErrorMapper.fromMedia3ErrorCode(e.errorCode);
154
+ String errorMsg = RmxPlaybackErrorMapper.isSourceError(e.errorCode)
155
+ ? "PlaybackException.TYPE_SOURCE: " + e.getMessage()
156
+ : "PlaybackException: " + e.getMessage();
168
157
 
169
158
  AudioTrack errorItem = playlistManager.getCurrentErrorTrack();
170
159
  String trackId = errorItem != null ? errorItem.getTrackId() : "INVALID";
@@ -172,20 +161,10 @@ public class RmxAudioPlayer implements PlaybackStatusListener<AudioTrack>,
172
161
  Log.i(TAG, "Error playing audio track: [" + trackId + "]: " + errorMsg);
173
162
  onError(errorType, trackId, errorMsg);
174
163
  playlistManager.setCurrentErrorTrack(null);
175
- return true;
176
164
  }
177
165
 
178
- @Override
179
- public void onMediaPlaybackStarted(AudioTrack item, long currentPosition, long duration) {
180
- Log.i(TAG, "onMediaPlaybackStarted: ==> " + item.getTitle() + ": " + currentPosition + "," + duration);
181
- // this is the first place that valid duration is seen. Immediately before, we get the PLAYING status change,
182
- // and before that, it announces PREPARING twice and all values are 0.
183
- // Problem is, this method is only called if playback is already in progress when the track changes,
184
- // which is useless in most cases. So, these values are actually handled in onProgressUpdated.
185
- }
186
-
187
- @Override
188
- public void onItemPlaybackEnded(AudioTrack item) {
166
+ public void onNativeItemCompleted(@Nullable AudioTrack item, boolean nextAvailable) {
167
+ onCompletion(item, nextAvailable);
189
168
  if (item != null) {
190
169
  String trackId = item.getTrackId();
191
170
  JSONObject trackStatus = getPlayerStatus(item);
@@ -193,14 +172,12 @@ public class RmxAudioPlayer implements PlaybackStatusListener<AudioTrack>,
193
172
  }
194
173
  }
195
174
 
196
- @Override
197
- public void onPlaylistEnded() {
175
+ public void onNativePlaylistCompleted() {
198
176
  Log.i(TAG, "onPlaylistEnded");
199
177
  playlistManager.setShouldStopPlaylist(false);
200
178
  }
201
179
 
202
- @Override
203
- public boolean onPlaylistItemChanged(@Nullable AudioTrack currentItem, boolean hasNext, boolean hasPrevious) {
180
+ public void onNativeTrackChanged(@Nullable AudioTrack currentItem, boolean hasNext, boolean hasPrevious) {
204
181
  JSONObject info = new JSONObject();
205
182
  String trackId = currentItem == null ? "NONE" : currentItem.getTrackId();
206
183
  try {
@@ -219,16 +196,9 @@ public class RmxAudioPlayer implements PlaybackStatusListener<AudioTrack>,
219
196
  trackLoaded = false;
220
197
 
221
198
  onStatus(RmxAudioStatusMessage.RMXSTATUS_TRACK_CHANGED, trackId, info);
222
- return true;
223
199
  }
224
200
 
225
- @Override
226
- public boolean onPlaybackStateChanged(@NonNull PlaybackState playbackState) {
227
- // in testing, I saw PREPARING, then PLAYING, and buffering happened
228
- // during PLAYING. Tapping play/pause toggles PLAYING and PAUSED
229
- // sending a seek command produces SEEKING here
230
- // RETRIEVING is never sent.
231
-
201
+ public void onNativePlaybackStateChanged(@NonNull RmxPlaybackState playbackState) {
232
202
  AudioTrack currentItem = playlistManager.getCurrentItem();
233
203
  JSONObject trackStatus = getPlayerStatus(currentItem);
234
204
  Log.i("onPlaybackStateChanged", playbackState.toString() + ", " + trackStatus.toString() + ", " + currentItem);
@@ -238,15 +208,14 @@ public class RmxAudioPlayer implements PlaybackStatusListener<AudioTrack>,
238
208
  onStatus(RmxAudioStatusMessage.RMXSTATUS_STOPPED, "INVALID", null);
239
209
  break;
240
210
 
241
- case RETRIEVING: // these are all loading states
242
- case PREPARING: {
211
+ case RETRIEVING:
212
+ case PREPARING:
243
213
  if (currentItem != null && currentItem.getTrackId() != null) {
244
214
  onStatus(RmxAudioStatusMessage.RMXSTATUS_LOADING, currentItem.getTrackId(), trackStatus);
245
215
  }
246
216
  break;
247
- }
248
- case SEEKING: {
249
- MediaProgress progress = playlistManager.getCurrentProgress();
217
+ case SEEKING:
218
+ PlaybackProgress progress = playlistManager.getCurrentProgress();
250
219
  if (currentItem != null && currentItem.getTrackId() != null && progress != null) {
251
220
  JSONObject info = new JSONObject();
252
221
  try {
@@ -257,11 +226,8 @@ public class RmxAudioPlayer implements PlaybackStatusListener<AudioTrack>,
257
226
  }
258
227
  }
259
228
  break;
260
- }
261
229
  case PLAYING:
262
230
  if (currentItem != null && currentItem.getTrackId() != null) {
263
- // Can also check here that duration == 0, because that is what happens on the first PLAYING invokation.
264
- // We'll leave this for now.
265
231
  if (!trackLoaded) {
266
232
  onStatus(RmxAudioStatusMessage.RMXSTATUS_CANPLAY, currentItem.getTrackId(), trackStatus);
267
233
  trackLoaded = true;
@@ -274,91 +240,79 @@ public class RmxAudioPlayer implements PlaybackStatusListener<AudioTrack>,
274
240
  onStatus(RmxAudioStatusMessage.RMXSTATUS_PAUSE, currentItem.getTrackId(), trackStatus);
275
241
  }
276
242
  break;
277
- // we'll handle error in the listener. ExoMedia only raises this in the case of catastrophic player failure.
278
243
  case ERROR:
279
244
  default:
280
245
  break;
281
246
  }
282
-
283
- return true;
284
247
  }
285
248
 
286
- @Override
287
- public boolean onProgressUpdated(@NonNull MediaProgress progress) {
288
- // Order matters here. We must update the item's duration and buffer before pulling the track status,
289
- // because those values are adjusted to account for the buffering-reset in ExoPlayer.
249
+ private void emitPositionIfNeeded() {
290
250
  AudioTrack currentItem = playlistManager.getCurrentItem();
291
- PlaybackState playbackState = playlistManager.getCurrentPlaybackState();
292
-
293
- if (currentItem != null) { // I mean, this call makes no sense otherwise..
294
- currentItem.setDuration(progress.getDuration());
295
- currentItem.setBufferPercent(progress.getBufferPercent());
296
- currentItem.setBufferPercentFloat(progress.getBufferPercentFloat());
297
-
298
- JSONObject trackStatus = getPlayerStatus(currentItem);
251
+ RmxPlaybackState playbackState = playlistManager.getCurrentPlaybackState();
252
+ PlaybackProgress progress = playlistManager.getCurrentProgress();
299
253
 
300
- if (progress.getBufferPercent() != lastBufferPercent) {
301
- if (progress.getBufferPercent() >= 100f) {
302
- // Unlike iOS this will get raised continuously.
303
- // Extracting the source event from playlistcore would be really hard.
304
- // The gate above should do the trick.
305
- onStatus(RmxAudioStatusMessage.RMXSTATUS_LOADED, currentItem.getTrackId(), trackStatus);
306
- }
254
+ if (currentItem == null || progress == null) {
255
+ return;
256
+ }
307
257
 
308
- if (!trackLoaded) {
309
- onStatus(RmxAudioStatusMessage.RMXSTATUS_CANPLAY, currentItem.getTrackId(), trackStatus);
310
- trackLoaded = true;
311
- }
258
+ updateTrackProgress(currentItem, progress);
312
259
 
313
- onStatus(RmxAudioStatusMessage.RMXSTATUS_BUFFERING, currentItem.getTrackId(), trackStatus);
314
- lastBufferPercent = progress.getBufferPercent();
260
+ if (progress.getBufferPercent() != lastBufferPercent) {
261
+ JSONObject trackStatus = getPlayerStatus(currentItem);
262
+ if (progress.getBufferPercent() >= 100f) {
263
+ onStatus(RmxAudioStatusMessage.RMXSTATUS_LOADED, currentItem.getTrackId(), trackStatus);
315
264
  }
316
265
 
317
- if (lastDuration != progress.getDuration() && progress.getDuration() > 0) {
318
- onStatus(RmxAudioStatusMessage.RMXSTATUS_DURATION, currentItem.getTrackId(), trackStatus);
319
- lastDuration = progress.getDuration();
266
+ if (!trackLoaded) {
267
+ onStatus(RmxAudioStatusMessage.RMXSTATUS_CANPLAY, currentItem.getTrackId(), trackStatus);
268
+ trackLoaded = true;
320
269
  }
321
270
 
322
- // dont send on prepare, if null
323
- if (playbackState == PlaybackState.PLAYING || playbackState == PlaybackState.SEEKING
324
- || (playbackState == PlaybackState.PREPARING && progress.getDuration() == 0)) {
325
- onStatus(RmxAudioStatusMessage.RMXSTATUS_PLAYBACK_POSITION, currentItem.getTrackId(), trackStatus);
326
- }
271
+ onStatus(RmxAudioStatusMessage.RMXSTATUS_BUFFERING, currentItem.getTrackId(), trackStatus);
272
+ lastBufferPercent = progress.getBufferPercent();
327
273
  }
328
274
 
329
- return true;
275
+ if (lastDuration != progress.getDuration() && progress.getDuration() > 0) {
276
+ onStatus(RmxAudioStatusMessage.RMXSTATUS_DURATION, currentItem.getTrackId(), getPlayerStatus(currentItem));
277
+ lastDuration = progress.getDuration();
278
+ }
279
+
280
+ if (playbackState == RmxPlaybackState.PLAYING || playbackState == RmxPlaybackState.SEEKING
281
+ || (playbackState == RmxPlaybackState.PREPARING && progress.getDuration() == 0)) {
282
+ onStatus(RmxAudioStatusMessage.RMXSTATUS_PLAYBACK_POSITION, currentItem.getTrackId(), getPlayerStatus(currentItem));
283
+ }
284
+ }
285
+
286
+ private void updateTrackProgress(AudioTrack currentItem, PlaybackProgress progress) {
287
+ currentItem.setDuration(progress.getDuration());
288
+ currentItem.setBufferPercent(progress.getBufferPercent());
289
+ currentItem.setBufferPercentFloat(progress.getBufferPercentFloat());
330
290
  }
331
291
 
332
292
  public JSONObject getPlayerStatus(@Nullable AudioTrack statusItem) {
333
- // TODO: Make this its own object.
334
293
  AudioTrack currentItem = statusItem != null ? statusItem : playlistManager.getCurrentItem();
335
- PlaybackState playbackState = playlistManager.getCurrentPlaybackState();
336
- MediaProgress progress = playlistManager.getCurrentProgress();
294
+ RmxPlaybackState playbackState = playlistManager.getCurrentPlaybackState();
295
+ PlaybackProgress progress = playlistManager.getCurrentProgress();
337
296
 
338
297
  String status = "unknown";
339
298
  switch (playbackState) {
340
- case STOPPED: {
299
+ case STOPPED:
341
300
  status = "stopped";
342
301
  break;
343
- }
344
- case ERROR: {
302
+ case ERROR:
345
303
  status = "error";
346
304
  break;
347
- }
348
305
  case RETRIEVING:
349
- case SEEKING: // { status = "seeking"; break; } // seeking === loading
350
- case PREPARING: {
306
+ case SEEKING:
307
+ case PREPARING:
351
308
  status = "loading";
352
309
  break;
353
- }
354
- case PLAYING: {
310
+ case PLAYING:
355
311
  status = "playing";
356
312
  break;
357
- }
358
- case PAUSED: {
313
+ case PAUSED:
359
314
  status = "paused";
360
315
  break;
361
- }
362
316
  default:
363
317
  break;
364
318
  }
@@ -370,19 +324,16 @@ public class RmxAudioPlayer implements PlaybackStatusListener<AudioTrack>,
370
324
  long duration = 0;
371
325
  long position = 0;
372
326
 
373
- // The media players hold onto their current playback position between songs,
374
- // despite my efforts to reset it. So we will just filter out this state.
375
- if (progress != null) { // && !status.equals("loading")) {
327
+ if (progress != null) {
376
328
  position = progress.getPosition();
377
329
  }
378
330
 
379
- // the position and duration vals are in milliseconds.
380
331
  if (currentItem != null) {
381
332
  isStream = currentItem.isStream();
382
333
  trackId = currentItem.getTrackId();
383
- bufferPercentFloat = currentItem.getBufferPercentFloat(); // progress.
384
- bufferPercent = currentItem.getBufferPercent(); // progress.
385
- duration = currentItem.getDuration(); // progress.
334
+ bufferPercentFloat = currentItem.getBufferPercentFloat();
335
+ bufferPercent = currentItem.getBufferPercent();
336
+ duration = currentItem.getDuration();
386
337
  }
387
338
 
388
339
  JSONObject trackStatus = new JSONObject();
@@ -406,41 +357,55 @@ public class RmxAudioPlayer implements PlaybackStatusListener<AudioTrack>,
406
357
 
407
358
  public void pause() {
408
359
  Log.i(TAG, "Pausing, removing event listeners");
409
- removePlaylistListeners();
360
+ stopPositionTicker();
410
361
  }
411
362
 
412
363
  public void resume() {
413
364
  Log.i(TAG, "Resumed, wiring up event listeners");
414
- registerPlaylistListeners();
415
- //Makes sure to retrieve the current playback information
365
+ startPositionTicker();
416
366
  updateCurrentPlaybackInformation();
417
367
  }
418
368
 
419
- private void updateCurrentPlaybackInformation() {
420
- PlaylistItemChange<AudioTrack> itemChange = playlistManager.getCurrentItemChange();
421
- if (itemChange != null) {
422
- onPlaylistItemChanged(itemChange.getCurrentItem(), itemChange.getHasNext(), itemChange.getHasPrevious());
369
+ @OptIn(markerClass = UnstableApi.class)
370
+ public void onPlayerAttached(ExoPlayer player) {
371
+ attachedPlayer = player;
372
+ if (listenersRegistered) {
373
+ startPositionTicker();
423
374
  }
375
+ }
424
376
 
425
- PlaybackState currentPlaybackState = playlistManager.getCurrentPlaybackState();
426
- if (currentPlaybackState != PlaybackState.STOPPED) {
427
- onPlaybackStateChanged(currentPlaybackState);
377
+ public void onPlayerDetached() {
378
+ attachedPlayer = null;
379
+ stopPositionTicker();
380
+ }
381
+
382
+ private void updateCurrentPlaybackInformation() {
383
+ AudioTrack currentItem = playlistManager.getCurrentItem();
384
+ if (currentItem != null) {
385
+ onNativeTrackChanged(
386
+ currentItem,
387
+ playlistManager.isNextAvailable(),
388
+ playlistManager.isPreviousAvailable()
389
+ );
428
390
  }
429
391
 
430
- MediaProgress mediaProgress = playlistManager.getCurrentProgress();
431
- if (mediaProgress != null) {
432
- onProgressUpdated(mediaProgress);
392
+ RmxPlaybackState currentPlaybackState = playlistManager.getCurrentPlaybackState();
393
+ if (currentPlaybackState != RmxPlaybackState.STOPPED) {
394
+ onNativePlaybackStateChanged(currentPlaybackState);
433
395
  }
396
+
397
+ emitPositionIfNeeded();
434
398
  }
435
399
 
436
- private void registerPlaylistListeners() {
437
- playlistManager.registerPlaylistListener(this);
438
- playlistManager.registerProgressListener(this);
400
+ private void startPositionTicker() {
401
+ listenersRegistered = true;
402
+ mainHandler.removeCallbacks(positionTicker);
403
+ mainHandler.post(positionTicker);
439
404
  }
440
405
 
441
- private void removePlaylistListeners() {
442
- playlistManager.unRegisterPlaylistListener(this);
443
- playlistManager.unRegisterProgressListener(this);
406
+ private void stopPositionTicker() {
407
+ listenersRegistered = false;
408
+ mainHandler.removeCallbacks(positionTicker);
444
409
  }
445
410
 
446
411
  private void onError(RmxAudioErrorType errorCode, String trackId, String message) {
@@ -454,18 +419,18 @@ public class RmxAudioPlayer implements PlaybackStatusListener<AudioTrack>,
454
419
  private float lastKnownHandoffPositionSec = 0f;
455
420
 
456
421
  public void prepareForVideoHandoff() {
457
- MediaProgress progress = playlistManager.getCurrentProgress();
422
+ PlaybackProgress progress = playlistManager.getCurrentProgress();
458
423
  if (progress != null) {
459
424
  lastKnownHandoffPositionSec = progress.getPosition() / 1000f;
460
425
  } else {
461
426
  lastKnownHandoffPositionSec = 0f;
462
427
  }
463
- com.devbrackets.android.playlistcore.components.playlisthandler.PlaylistHandler<?> handler =
464
- playlistManager.getPlaylistHandler();
465
- if (handler instanceof AudioPlaylistHandler) {
466
- ((AudioPlaylistHandler<?, ?>) handler).pauseForVideoHandoff();
467
- } else if (handler != null) {
468
- handler.pause(false);
428
+ MediaNotificationPolicy.applyForegroundRetainOnPrepare(
429
+ () -> playlistManager.setVideoHandoffForegroundRetain(true)
430
+ );
431
+ AudioPlaylistHandler handler = playlistManager.getPlaylistHandler();
432
+ if (handler != null) {
433
+ handler.pauseForVideoHandoff();
469
434
  }
470
435
  }
471
436
 
@@ -473,11 +438,6 @@ public class RmxAudioPlayer implements PlaybackStatusListener<AudioTrack>,
473
438
  return resumeAfterVideoHandoff(positionSec, false, true);
474
439
  }
475
440
 
476
- /**
477
- * @return {@code true} when in-place resume already seeked and started playback
478
- * (JS should skip redundant seekTo/play); {@code false} for prewarm, paused handoff,
479
- * or last-resort beginPlayback.
480
- */
481
441
  public boolean resumeAfterVideoHandoff(float positionSec, boolean prewarm) {
482
442
  return resumeAfterVideoHandoff(positionSec, prewarm, true);
483
443
  }
@@ -490,23 +450,23 @@ public class RmxAudioPlayer implements PlaybackStatusListener<AudioTrack>,
490
450
  playlistManager.beginPlayback(positionMs, true);
491
451
  return false;
492
452
  }
493
- if (!play) {
494
- // Paused video exit do not start audio; JS will seekTo only.
495
- playlistManager.setVideoHandoffForegroundRetain(false);
496
- return false;
497
- }
498
- if (tryResumeVideoHandoffInPlace(positionMs)) {
453
+ MediaService service = MediaService.getInstance();
454
+ boolean inForeground = service != null && service.isRunningInForeground();
455
+ if (MediaNotificationPolicy.shouldReportInPlaceResumed(
456
+ false,
457
+ play,
458
+ playlistManager.getVideoHandoffForegroundRetain(),
459
+ inForeground
460
+ ) && tryResumeVideoHandoffInPlace(positionMs)) {
499
461
  return true;
500
462
  }
501
- // Service not foreground — last resort (may be muted on Android 17 when backgrounded).
502
463
  playlistManager.setVideoHandoffForegroundRetain(false);
503
- playlistManager.beginPlayback(positionMs, true);
464
+ MediaNotificationPolicy.applyBeginPlaybackWhenInPlaceUnavailable(
465
+ () -> playlistManager.beginPlayback(positionMs, true)
466
+ );
504
467
  return false;
505
468
  }
506
469
 
507
- /**
508
- * Resume audio at {@code positionMs} without restarting MediaService (Android 17 AudioHardening).
509
- */
510
470
  public boolean tryResumeVideoHandoffInPlace(long positionMs) {
511
471
  if (!playlistManager.getVideoHandoffForegroundRetain()) {
512
472
  return false;
@@ -515,17 +475,14 @@ public class RmxAudioPlayer implements PlaybackStatusListener<AudioTrack>,
515
475
  if (service == null || !service.isRunningInForeground()) {
516
476
  return false;
517
477
  }
518
- com.devbrackets.android.playlistcore.components.playlisthandler.PlaylistHandler<?> handler =
519
- playlistManager.getPlaylistHandler();
520
- if (!(handler instanceof AudioPlaylistHandler)) {
478
+ AudioPlaylistHandler audioHandler = playlistManager.getPlaylistHandler();
479
+ if (audioHandler == null) {
521
480
  return false;
522
481
  }
523
- AudioPlaylistHandler<?, ?> audioHandler = (AudioPlaylistHandler<?, ?>) handler;
524
- com.devbrackets.android.playlistcore.api.MediaPlayerApi<?> mediaPlayer = audioHandler.getCurrentMediaPlayer();
482
+ Player mediaPlayer = audioHandler.getCurrentMediaPlayer();
525
483
  if (mediaPlayer != null) {
526
484
  audioHandler.resumePlaybackAfterVideoHandoff(positionMs);
527
485
  } else {
528
- // Re-prepare within the existing foreground service — do not call beginPlayback/startService.
529
486
  audioHandler.startItemPlayback(positionMs, false);
530
487
  }
531
488
  playlistManager.setVideoHandoffForegroundRetain(false);
@@ -0,0 +1,26 @@
1
+ package org.dwbn.plugins.playlist
2
+
3
+ import androidx.media3.common.PlaybackException
4
+
5
+ /** Maps Media3 playback error codes onto the frozen JS `RmxAudioErrorType` values. */
6
+ object RmxPlaybackErrorMapper {
7
+ @JvmStatic
8
+ fun fromMedia3ErrorCode(errorCode: Int): RmxAudioErrorType =
9
+ when (errorCode) {
10
+ PlaybackException.ERROR_CODE_DECODER_INIT_FAILED,
11
+ PlaybackException.ERROR_CODE_DECODING_FAILED,
12
+ PlaybackException.ERROR_CODE_DECODER_QUERY_FAILED -> RmxAudioErrorType.RMXERR_DECODE
13
+ else -> RmxAudioErrorType.RMXERR_NONE_SUPPORTED
14
+ }
15
+
16
+ @JvmStatic
17
+ fun isSourceError(errorCode: Int): Boolean =
18
+ when (errorCode) {
19
+ PlaybackException.ERROR_CODE_IO_UNSPECIFIED,
20
+ PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED,
21
+ PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT,
22
+ PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS,
23
+ PlaybackException.ERROR_CODE_IO_FILE_NOT_FOUND -> true
24
+ else -> false
25
+ }
26
+ }