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.
- package/README.md +133 -127
- package/android/build.gradle +20 -15
- 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,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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 (!
|
|
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)
|
|
149
|
-
public
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
@
|
|
179
|
-
|
|
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
|
-
|
|
197
|
-
public void onPlaylistEnded() {
|
|
175
|
+
public void onNativePlaylistCompleted() {
|
|
198
176
|
Log.i(TAG, "onPlaylistEnded");
|
|
199
177
|
playlistManager.setShouldStopPlaylist(false);
|
|
200
178
|
}
|
|
201
179
|
|
|
202
|
-
@
|
|
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
|
-
@
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
309
|
-
onStatus(RmxAudioStatusMessage.RMXSTATUS_CANPLAY, currentItem.getTrackId(), trackStatus);
|
|
310
|
-
trackLoaded = true;
|
|
311
|
-
}
|
|
258
|
+
updateTrackProgress(currentItem, progress);
|
|
312
259
|
|
|
313
|
-
|
|
314
|
-
|
|
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 (
|
|
318
|
-
onStatus(RmxAudioStatusMessage.
|
|
319
|
-
|
|
266
|
+
if (!trackLoaded) {
|
|
267
|
+
onStatus(RmxAudioStatusMessage.RMXSTATUS_CANPLAY, currentItem.getTrackId(), trackStatus);
|
|
268
|
+
trackLoaded = true;
|
|
320
269
|
}
|
|
321
270
|
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
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
|
-
|
|
336
|
-
|
|
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:
|
|
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
|
-
|
|
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();
|
|
384
|
-
bufferPercent = currentItem.getBufferPercent();
|
|
385
|
-
duration = currentItem.getDuration();
|
|
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
|
-
|
|
360
|
+
stopPositionTicker();
|
|
410
361
|
}
|
|
411
362
|
|
|
412
363
|
public void resume() {
|
|
413
364
|
Log.i(TAG, "Resumed, wiring up event listeners");
|
|
414
|
-
|
|
415
|
-
//Makes sure to retrieve the current playback information
|
|
365
|
+
startPositionTicker();
|
|
416
366
|
updateCurrentPlaybackInformation();
|
|
417
367
|
}
|
|
418
368
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
|
|
431
|
-
if (
|
|
432
|
-
|
|
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
|
|
437
|
-
|
|
438
|
-
|
|
400
|
+
private void startPositionTicker() {
|
|
401
|
+
listenersRegistered = true;
|
|
402
|
+
mainHandler.removeCallbacks(positionTicker);
|
|
403
|
+
mainHandler.post(positionTicker);
|
|
439
404
|
}
|
|
440
405
|
|
|
441
|
-
private void
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
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
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
handler.
|
|
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
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
-
|
|
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
|
-
|
|
519
|
-
|
|
520
|
-
if (!(handler instanceof AudioPlaylistHandler)) {
|
|
478
|
+
AudioPlaylistHandler audioHandler = playlistManager.getPlaylistHandler();
|
|
479
|
+
if (audioHandler == null) {
|
|
521
480
|
return false;
|
|
522
481
|
}
|
|
523
|
-
|
|
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
|
+
}
|