@mustafaj/capacitor-plugin-playlist 0.9.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/CapacitorPluginPlaylist.podspec +17 -0
- package/README.md +248 -0
- package/android/.project +34 -0
- package/android/build.gradle +69 -0
- package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/android/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/android/gradle.properties +22 -0
- package/android/gradlew +251 -0
- package/android/gradlew.bat +94 -0
- package/android/proguard-rules.pro +21 -0
- package/android/settings.gradle +2 -0
- package/android/src/androidTest/java/com/getcapacitor/android/ExampleInstrumentedTest.java +26 -0
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/App.kt +19 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/FakeR.kt +39 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/OnStatusCallback.kt +34 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/OnStatusReportListener.java +7 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/PlaylistItemOptions.java +52 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/PlaylistPlugin.kt +447 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/RmxAudioErrorType.java +13 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/RmxAudioPlayer.java +487 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/RmxAudioStatusMessage.java +35 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/RmxConstants.java +42 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/TrackRemovalItem.java +12 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/data/AudioTrack.kt +94 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/manager/MediaControlsListener.kt +13 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/manager/Options.kt +77 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/manager/PlaylistManager.kt +308 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/notification/PlaylistNotificationProvider.kt +26 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/playlist/AudioApi.kt +114 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/playlist/AudioPlaylistHandler.java +146 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/playlist/BaseMediaApi.kt +36 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/service/MediaImageProvider.kt +83 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/service/MediaService.kt +98 -0
- package/android/src/main/res/.gitkeep +0 -0
- package/android/src/main/res/drawable/ic_closed_caption_white_24dp.xml +9 -0
- package/android/src/main/res/drawable/ic_demo_icon_adaptive.xml +15 -0
- package/android/src/main/res/drawable/ic_launcher_background.xml +48 -0
- package/android/src/main/res/drawable/ic_launcher_foreground.xml +22 -0
- package/android/src/main/res/drawable/ic_notification_icon.png +0 -0
- package/android/src/main/res/layout/bridge_layout_main.xml +15 -0
- package/android/src/main/res/values/colors.xml +3 -0
- package/android/src/main/res/values/strings.xml +3 -0
- package/android/src/main/res/values/styles.xml +3 -0
- package/android/src/test/java/com/getcapacitor/ExampleUnitTest.java +18 -0
- package/dist/docs.json +2071 -0
- package/dist/esm/Constants.d.ts +164 -0
- package/dist/esm/Constants.js +175 -0
- package/dist/esm/Constants.js.map +1 -0
- package/dist/esm/RmxAudioPlayer.d.ts +181 -0
- package/dist/esm/RmxAudioPlayer.js +344 -0
- package/dist/esm/RmxAudioPlayer.js.map +1 -0
- package/dist/esm/definitions.d.ts +78 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +5 -0
- package/dist/esm/index.js +6 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/interfaces.d.ts +246 -0
- package/dist/esm/interfaces.js +2 -0
- package/dist/esm/interfaces.js.map +1 -0
- package/dist/esm/plugin.d.ts +3 -0
- package/dist/esm/plugin.js +13 -0
- package/dist/esm/plugin.js.map +1 -0
- package/dist/esm/utils.d.ts +15 -0
- package/dist/esm/utils.js +48 -0
- package/dist/esm/utils.js.map +1 -0
- package/dist/esm/web.d.ts +54 -0
- package/dist/esm/web.js +409 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +993 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +996 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Plugin/AVBidirectionalQueuePlayer.swift +269 -0
- package/ios/Plugin/AudioTrack.swift +63 -0
- package/ios/Plugin/Constants.swift +39 -0
- package/ios/Plugin/DispatchQueue.swift +47 -0
- package/ios/Plugin/Info.plist +24 -0
- package/ios/Plugin/Plugin.h +10 -0
- package/ios/Plugin/Plugin.m +30 -0
- package/ios/Plugin/Plugin.swift +208 -0
- package/ios/Plugin/RmxAudioPlayer.swift +1150 -0
- package/ios/Plugin.xcodeproj/project.pbxproj +574 -0
- package/ios/Plugin.xcworkspace/contents.xcworkspacedata +10 -0
- package/ios/Plugin.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/ios/PluginTests/Info.plist +22 -0
- package/ios/PluginTests/PluginTests.swift +35 -0
- package/ios/Podfile +16 -0
- package/package.json +89 -0
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
package org.dwbn.plugins.playlist;
|
|
2
|
+
|
|
3
|
+
import android.util.Log;
|
|
4
|
+
|
|
5
|
+
import androidx.annotation.NonNull;
|
|
6
|
+
import androidx.annotation.Nullable;
|
|
7
|
+
import androidx.annotation.OptIn;
|
|
8
|
+
import androidx.media3.common.util.UnstableApi;
|
|
9
|
+
import androidx.media3.exoplayer.ExoPlaybackException;
|
|
10
|
+
|
|
11
|
+
import com.devbrackets.android.exomedia.listener.OnErrorListener;
|
|
12
|
+
import com.devbrackets.android.playlistcore.data.MediaProgress;
|
|
13
|
+
import com.devbrackets.android.playlistcore.data.PlaybackState;
|
|
14
|
+
import com.devbrackets.android.playlistcore.data.PlaylistItemChange;
|
|
15
|
+
import com.devbrackets.android.playlistcore.listener.PlaybackStatusListener;
|
|
16
|
+
import com.devbrackets.android.playlistcore.listener.PlaylistListener;
|
|
17
|
+
import com.devbrackets.android.playlistcore.listener.ProgressListener;
|
|
18
|
+
|
|
19
|
+
import org.dwbn.plugins.playlist.data.AudioTrack;
|
|
20
|
+
import org.dwbn.plugins.playlist.manager.MediaControlsListener;
|
|
21
|
+
import org.dwbn.plugins.playlist.manager.Options;
|
|
22
|
+
import org.dwbn.plugins.playlist.manager.PlaylistManager;
|
|
23
|
+
import org.json.JSONException;
|
|
24
|
+
import org.json.JSONObject;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The implementation of this player borrows from ExoMedia's demo example
|
|
28
|
+
* and utilizes heavily those classes, basically because that is "the" way
|
|
29
|
+
* to actually use ExoMedia.
|
|
30
|
+
*/
|
|
31
|
+
public class RmxAudioPlayer implements PlaybackStatusListener<AudioTrack>,
|
|
32
|
+
PlaylistListener<AudioTrack>, ProgressListener, OnErrorListener, MediaControlsListener {
|
|
33
|
+
|
|
34
|
+
public static String TAG = "PlaylistRmxAudioPlayer";
|
|
35
|
+
|
|
36
|
+
// PlaylistCore requires this but we don't use it
|
|
37
|
+
// It would be used to switch between playlists. I guess we could
|
|
38
|
+
// support that in the future, might be cool.
|
|
39
|
+
private static final int PLAYLIST_ID = 32;
|
|
40
|
+
private PlaylistManager playlistManager;
|
|
41
|
+
private final OnStatusReportListener statusListener;
|
|
42
|
+
|
|
43
|
+
private int lastBufferPercent = 0;
|
|
44
|
+
private long lastDuration = 0;
|
|
45
|
+
private boolean trackLoaded = false;
|
|
46
|
+
private boolean resetStreamOnPause = true;
|
|
47
|
+
private String pendingSelectionTrackId = null;
|
|
48
|
+
private boolean suppressSelectionPlaybackEvents = false;
|
|
49
|
+
private final App app;
|
|
50
|
+
|
|
51
|
+
public RmxAudioPlayer(@NonNull OnStatusReportListener statusListener, App context) {
|
|
52
|
+
// AudioPlayerPlugin and RmxAudioPlayer are separate classes in order to increase
|
|
53
|
+
// the portability of this code.
|
|
54
|
+
// Because AudioPlayerPlugin itself holds a strong reference to this class,
|
|
55
|
+
// we can hold a strong reference to this shared callback. Normally not a good idea
|
|
56
|
+
// but these two objects will always live together (And the plugin couldn't function
|
|
57
|
+
// at all if this one gets garbage collected).
|
|
58
|
+
this.statusListener = statusListener;
|
|
59
|
+
this.app = context;
|
|
60
|
+
|
|
61
|
+
app.resetPlaylistManager();
|
|
62
|
+
getPlaylistManager();
|
|
63
|
+
playlistManager.setId(PLAYLIST_ID);
|
|
64
|
+
playlistManager.setPlaybackStatusListener(this);
|
|
65
|
+
playlistManager.setOnErrorListener(this);
|
|
66
|
+
playlistManager.setMediaControlsListener(this);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public PlaylistManager getPlaylistManager() {
|
|
70
|
+
playlistManager = app.getPlaylistManager();
|
|
71
|
+
return playlistManager;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public boolean getResetStreamOnPause() {
|
|
75
|
+
return resetStreamOnPause;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
public void setResetStreamOnPause(boolean val) {
|
|
79
|
+
resetStreamOnPause = val;
|
|
80
|
+
getPlaylistManager().setResetStreamOnPause(getResetStreamOnPause());
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
public void setOptions(JSONObject val) {
|
|
84
|
+
Options options = new Options(app, val);
|
|
85
|
+
getPlaylistManager().setOptions(options);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
public void prepareForTrackSelection(@Nullable String trackId) {
|
|
89
|
+
pendingSelectionTrackId = trackId;
|
|
90
|
+
suppressSelectionPlaybackEvents = trackId != null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public void clearTrackSelectionSuppression() {
|
|
94
|
+
pendingSelectionTrackId = null;
|
|
95
|
+
suppressSelectionPlaybackEvents = false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private boolean isSuppressingSelection(@Nullable AudioTrack item) {
|
|
99
|
+
return suppressSelectionPlaybackEvents
|
|
100
|
+
&& pendingSelectionTrackId != null
|
|
101
|
+
&& item != null
|
|
102
|
+
&& pendingSelectionTrackId.equals(item.getTrackId());
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
public float getVolume() {
|
|
106
|
+
return (getVolumeLeft() + getVolumeRight()) / 2f;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
public float getVolumeLeft() {
|
|
110
|
+
return playlistManager.getVolumeLeft();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
public float getVolumeRight() {
|
|
114
|
+
return playlistManager.getVolumeRight();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
public void setVolume(float both) {
|
|
118
|
+
setVolume(both, both);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
public void setVolume(float left, float right) {
|
|
122
|
+
playlistManager.setVolume(left, right);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
public void onCompletion(AudioTrack item) {
|
|
126
|
+
if (item != null) {
|
|
127
|
+
String trackId = item.getTrackId();
|
|
128
|
+
JSONObject trackStatus = getPlayerStatus(item);
|
|
129
|
+
onStatus(RmxAudioStatusMessage.RMXSTATUS_COMPLETED, trackId, trackStatus);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!playlistManager.isNextAvailable()) {
|
|
133
|
+
onStatus(RmxAudioStatusMessage.RMXSTATUS_PLAYLIST_COMPLETED, "INVALID", null);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
@Override
|
|
138
|
+
public void onPrevious(AudioTrack currentItem, int currentIndex) {
|
|
139
|
+
JSONObject param = new JSONObject();
|
|
140
|
+
String trackId = currentItem == null ? "NONE" : currentItem.getTrackId();
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
param.put("currentIndex", currentIndex);
|
|
144
|
+
param.put("currentItem", currentItem != null ? currentItem.toDict() : null);
|
|
145
|
+
} catch (JSONException e) {
|
|
146
|
+
Log.i(TAG, "Error generating onPrevious status message: " + e.toString());
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
onStatus(RmxAudioStatusMessage.RMX_STATUS_SKIP_BACK, trackId, param);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
@Override
|
|
153
|
+
public void onNext(AudioTrack currentItem, int currentIndex) {
|
|
154
|
+
JSONObject param = new JSONObject();
|
|
155
|
+
String trackId = currentItem == null ? "NONE" : currentItem.getTrackId();
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
param.put("currentIndex", currentIndex);
|
|
159
|
+
param.put("currentItem", currentItem != null ? currentItem.toDict() : null);
|
|
160
|
+
} catch (JSONException e) {
|
|
161
|
+
Log.i(TAG, "Error generating onNext status message: " + e.toString());
|
|
162
|
+
}
|
|
163
|
+
onStatus(RmxAudioStatusMessage.RMX_STATUS_SKIP_FORWARD, trackId, param);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
@OptIn(markerClass = UnstableApi.class) @Override
|
|
167
|
+
public boolean onError(Exception e) {
|
|
168
|
+
String errorMsg = e.toString();
|
|
169
|
+
RmxAudioErrorType errorType = RmxAudioErrorType.RMXERR_NONE_SUPPORTED;
|
|
170
|
+
|
|
171
|
+
if (e instanceof ExoPlaybackException) {
|
|
172
|
+
switch (((ExoPlaybackException) e).type) {
|
|
173
|
+
case ExoPlaybackException.TYPE_SOURCE:
|
|
174
|
+
errorMsg = "ExoPlaybackException.TYPE_SOURCE: " + ((ExoPlaybackException) e).getSourceException().getMessage();
|
|
175
|
+
break;
|
|
176
|
+
case ExoPlaybackException.TYPE_RENDERER:
|
|
177
|
+
errorType = RmxAudioErrorType.RMXERR_DECODE;
|
|
178
|
+
errorMsg = "ExoPlaybackException.TYPE_RENDERER: " + ((ExoPlaybackException) e).getRendererException().getMessage();
|
|
179
|
+
break;
|
|
180
|
+
case ExoPlaybackException.TYPE_UNEXPECTED:
|
|
181
|
+
errorType = RmxAudioErrorType.RMXERR_DECODE;
|
|
182
|
+
errorMsg = "ExoPlaybackException.TYPE_UNEXPECTED: " + ((ExoPlaybackException) e).getUnexpectedException().getMessage();
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
AudioTrack errorItem = playlistManager.getCurrentErrorTrack();
|
|
188
|
+
String trackId = errorItem != null ? errorItem.getTrackId() : "INVALID";
|
|
189
|
+
|
|
190
|
+
Log.i(TAG, "Error playing audio track: [" + trackId + "]: " + errorMsg);
|
|
191
|
+
onError(errorType, trackId, errorMsg);
|
|
192
|
+
playlistManager.setCurrentErrorTrack(null);
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
@Override
|
|
197
|
+
public void onMediaPlaybackStarted(AudioTrack item, long currentPosition, long duration) {
|
|
198
|
+
Log.i(TAG, "onMediaPlaybackStarted: ==> " + item.getTitle() + ": " + currentPosition + "," + duration);
|
|
199
|
+
// this is the first place that valid duration is seen. Immediately before, we get the PLAYING status change,
|
|
200
|
+
// and before that, it announces PREPARING twice and all values are 0.
|
|
201
|
+
// Problem is, this method is only called if playback is already in progress when the track changes,
|
|
202
|
+
// which is useless in most cases. So, these values are actually handled in onProgressUpdated.
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
@Override
|
|
206
|
+
public void onItemPlaybackEnded(AudioTrack item) {
|
|
207
|
+
if (item != null) {
|
|
208
|
+
String trackId = item.getTrackId();
|
|
209
|
+
JSONObject trackStatus = getPlayerStatus(item);
|
|
210
|
+
onStatus(RmxAudioStatusMessage.RMXSTATUS_STOPPED, trackId, trackStatus);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
@Override
|
|
215
|
+
public void onPlaylistEnded() {
|
|
216
|
+
Log.i(TAG, "onPlaylistEnded");
|
|
217
|
+
playlistManager.setShouldStopPlaylist(false);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
@Override
|
|
221
|
+
public boolean onPlaylistItemChanged(@Nullable AudioTrack currentItem, boolean hasNext, boolean hasPrevious) {
|
|
222
|
+
JSONObject info = new JSONObject();
|
|
223
|
+
String trackId = currentItem == null ? "NONE" : currentItem.getTrackId();
|
|
224
|
+
try {
|
|
225
|
+
info.put("currentItem", currentItem != null ? currentItem.toDict() : null);
|
|
226
|
+
info.put("currentIndex", playlistManager.getCurrentPosition());
|
|
227
|
+
info.put("isAtEnd", !hasNext);
|
|
228
|
+
info.put("isAtBeginning", !hasPrevious);
|
|
229
|
+
info.put("hasNext", hasNext);
|
|
230
|
+
info.put("hasPrevious", hasPrevious);
|
|
231
|
+
} catch (JSONException e) {
|
|
232
|
+
Log.e(TAG, "Error creating onPlaylistItemChanged message: " + e.toString());
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
lastDuration = 0;
|
|
236
|
+
lastBufferPercent = 0;
|
|
237
|
+
trackLoaded = false;
|
|
238
|
+
|
|
239
|
+
onStatus(RmxAudioStatusMessage.RMXSTATUS_TRACK_CHANGED, trackId, info);
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
@Override
|
|
244
|
+
public boolean onPlaybackStateChanged(@NonNull PlaybackState playbackState) {
|
|
245
|
+
// in testing, I saw PREPARING, then PLAYING, and buffering happened
|
|
246
|
+
// during PLAYING. Tapping play/pause toggles PLAYING and PAUSED
|
|
247
|
+
// sending a seek command produces SEEKING here
|
|
248
|
+
// RETRIEVING is never sent.
|
|
249
|
+
|
|
250
|
+
AudioTrack currentItem = playlistManager.getCurrentItem();
|
|
251
|
+
JSONObject trackStatus = getPlayerStatus(currentItem);
|
|
252
|
+
boolean suppressSelection = isSuppressingSelection(currentItem);
|
|
253
|
+
Log.i("onPlaybackStateChanged", playbackState.toString() + ", " + trackStatus.toString() + ", " + currentItem);
|
|
254
|
+
|
|
255
|
+
switch (playbackState) {
|
|
256
|
+
case STOPPED:
|
|
257
|
+
if (suppressSelection) {
|
|
258
|
+
clearTrackSelectionSuppression();
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
onStatus(RmxAudioStatusMessage.RMXSTATUS_STOPPED, "INVALID", null);
|
|
262
|
+
break;
|
|
263
|
+
|
|
264
|
+
case RETRIEVING: // these are all loading states
|
|
265
|
+
case PREPARING: {
|
|
266
|
+
if (currentItem != null && currentItem.getTrackId() != null) {
|
|
267
|
+
onStatus(RmxAudioStatusMessage.RMXSTATUS_LOADING, currentItem.getTrackId(), trackStatus);
|
|
268
|
+
}
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
case SEEKING: {
|
|
272
|
+
MediaProgress progress = playlistManager.getCurrentProgress();
|
|
273
|
+
if (currentItem != null && currentItem.getTrackId() != null && progress != null) {
|
|
274
|
+
JSONObject info = new JSONObject();
|
|
275
|
+
try {
|
|
276
|
+
info.put("position", progress.getPosition() / 1000f);
|
|
277
|
+
onStatus(RmxAudioStatusMessage.RMXSTATUS_SEEK, currentItem.getTrackId(), info);
|
|
278
|
+
} catch (JSONException e) {
|
|
279
|
+
Log.e(TAG, "Error generating seeking status message: " + e.toString());
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
case PLAYING:
|
|
285
|
+
if (suppressSelection) {
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
if (currentItem != null && currentItem.getTrackId() != null) {
|
|
289
|
+
// Can also check here that duration == 0, because that is what happens on the first PLAYING invokation.
|
|
290
|
+
// We'll leave this for now.
|
|
291
|
+
if (!trackLoaded) {
|
|
292
|
+
onStatus(RmxAudioStatusMessage.RMXSTATUS_CANPLAY, currentItem.getTrackId(), trackStatus);
|
|
293
|
+
trackLoaded = true;
|
|
294
|
+
}
|
|
295
|
+
onStatus(RmxAudioStatusMessage.RMXSTATUS_PLAYING, currentItem.getTrackId(), trackStatus);
|
|
296
|
+
}
|
|
297
|
+
break;
|
|
298
|
+
case PAUSED:
|
|
299
|
+
if (suppressSelection) {
|
|
300
|
+
clearTrackSelectionSuppression();
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
if (currentItem != null && currentItem.getTrackId() != null) {
|
|
304
|
+
onStatus(RmxAudioStatusMessage.RMXSTATUS_PAUSE, currentItem.getTrackId(), trackStatus);
|
|
305
|
+
}
|
|
306
|
+
break;
|
|
307
|
+
// we'll handle error in the listener. ExoMedia only raises this in the case of catastrophic player failure.
|
|
308
|
+
case ERROR:
|
|
309
|
+
if (suppressSelection) {
|
|
310
|
+
clearTrackSelectionSuppression();
|
|
311
|
+
}
|
|
312
|
+
default:
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
@Override
|
|
320
|
+
public boolean onProgressUpdated(@NonNull MediaProgress progress) {
|
|
321
|
+
// Order matters here. We must update the item's duration and buffer before pulling the track status,
|
|
322
|
+
// because those values are adjusted to account for the buffering-reset in ExoPlayer.
|
|
323
|
+
AudioTrack currentItem = playlistManager.getCurrentItem();
|
|
324
|
+
PlaybackState playbackState = playlistManager.getCurrentPlaybackState();
|
|
325
|
+
|
|
326
|
+
if (currentItem != null) { // I mean, this call makes no sense otherwise..
|
|
327
|
+
currentItem.setDuration(progress.getDuration());
|
|
328
|
+
currentItem.setBufferPercent(progress.getBufferPercent());
|
|
329
|
+
currentItem.setBufferPercentFloat(progress.getBufferPercentFloat());
|
|
330
|
+
|
|
331
|
+
JSONObject trackStatus = getPlayerStatus(currentItem);
|
|
332
|
+
boolean suppressSelection = isSuppressingSelection(currentItem);
|
|
333
|
+
|
|
334
|
+
if (progress.getBufferPercent() != lastBufferPercent) {
|
|
335
|
+
if (progress.getBufferPercent() >= 100f) {
|
|
336
|
+
// Unlike iOS this will get raised continuously.
|
|
337
|
+
// Extracting the source event from playlistcore would be really hard.
|
|
338
|
+
// The gate above should do the trick.
|
|
339
|
+
onStatus(RmxAudioStatusMessage.RMXSTATUS_LOADED, currentItem.getTrackId(), trackStatus);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (!trackLoaded) {
|
|
343
|
+
onStatus(RmxAudioStatusMessage.RMXSTATUS_CANPLAY, currentItem.getTrackId(), trackStatus);
|
|
344
|
+
trackLoaded = true;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
onStatus(RmxAudioStatusMessage.RMXSTATUS_BUFFERING, currentItem.getTrackId(), trackStatus);
|
|
348
|
+
lastBufferPercent = progress.getBufferPercent();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (lastDuration != progress.getDuration() && progress.getDuration() > 0) {
|
|
352
|
+
onStatus(RmxAudioStatusMessage.RMXSTATUS_DURATION, currentItem.getTrackId(), trackStatus);
|
|
353
|
+
lastDuration = progress.getDuration();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (!suppressSelection
|
|
357
|
+
&& (playbackState == PlaybackState.PLAYING || playbackState == PlaybackState.SEEKING)) {
|
|
358
|
+
onStatus(RmxAudioStatusMessage.RMXSTATUS_PLAYBACK_POSITION, currentItem.getTrackId(), trackStatus);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
public JSONObject getPlayerStatus(@Nullable AudioTrack statusItem) {
|
|
366
|
+
// TODO: Make this its own object.
|
|
367
|
+
AudioTrack currentItem = statusItem != null ? statusItem : playlistManager.getCurrentItem();
|
|
368
|
+
PlaybackState playbackState = playlistManager.getCurrentPlaybackState();
|
|
369
|
+
MediaProgress progress = playlistManager.getCurrentProgress();
|
|
370
|
+
|
|
371
|
+
String status = "unknown";
|
|
372
|
+
switch (playbackState) {
|
|
373
|
+
case STOPPED: {
|
|
374
|
+
status = "stopped";
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
case ERROR: {
|
|
378
|
+
status = "error";
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
case RETRIEVING:
|
|
382
|
+
case SEEKING: // { status = "seeking"; break; } // seeking === loading
|
|
383
|
+
case PREPARING: {
|
|
384
|
+
status = "loading";
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
case PLAYING: {
|
|
388
|
+
status = "playing";
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
case PAUSED: {
|
|
392
|
+
status = "paused";
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
default:
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
String trackId = "";
|
|
400
|
+
boolean isStream = false;
|
|
401
|
+
float bufferPercentFloat = 0;
|
|
402
|
+
int bufferPercent = 0;
|
|
403
|
+
long duration = 0;
|
|
404
|
+
long position = 0;
|
|
405
|
+
|
|
406
|
+
// The media players hold onto their current playback position between songs,
|
|
407
|
+
// despite my efforts to reset it. So we will just filter out this state.
|
|
408
|
+
if (progress != null && playbackState != PlaybackState.RETRIEVING && playbackState != PlaybackState.PREPARING) {
|
|
409
|
+
position = progress.getPosition();
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// the position and duration vals are in milliseconds.
|
|
413
|
+
if (currentItem != null) {
|
|
414
|
+
isStream = currentItem.isStream();
|
|
415
|
+
trackId = currentItem.getTrackId();
|
|
416
|
+
bufferPercentFloat = currentItem.getBufferPercentFloat(); // progress.
|
|
417
|
+
bufferPercent = currentItem.getBufferPercent(); // progress.
|
|
418
|
+
duration = currentItem.getDuration(); // progress.
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
JSONObject trackStatus = new JSONObject();
|
|
422
|
+
try {
|
|
423
|
+
trackStatus.put("trackId", trackId);
|
|
424
|
+
trackStatus.put("isStream", isStream);
|
|
425
|
+
trackStatus.put("currentIndex", playlistManager.getCurrentPosition());
|
|
426
|
+
trackStatus.put("status", status);
|
|
427
|
+
trackStatus.put("currentPosition", position / 1000.0);
|
|
428
|
+
trackStatus.put("duration", duration / 1000.0);
|
|
429
|
+
trackStatus.put("playbackPercent", duration > 0 ? (((double) position / duration) * 100.0) : 0);
|
|
430
|
+
trackStatus.put("bufferPercent", bufferPercent);
|
|
431
|
+
trackStatus.put("bufferStart", 0.0);
|
|
432
|
+
trackStatus.put("bufferEnd", (bufferPercentFloat * duration) / 1000.0);
|
|
433
|
+
} catch (JSONException e) {
|
|
434
|
+
Log.e(TAG, "Error generating player status: " + e.toString());
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return trackStatus;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
public void pause() {
|
|
441
|
+
Log.i(TAG, "Pausing, removing event listeners");
|
|
442
|
+
removePlaylistListeners();
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
public void resume() {
|
|
446
|
+
Log.i(TAG, "Resumed, wiring up event listeners");
|
|
447
|
+
getPlaylistManager();
|
|
448
|
+
registerPlaylistListeners();
|
|
449
|
+
//Makes sure to retrieve the current playback information
|
|
450
|
+
updateCurrentPlaybackInformation();
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
private void updateCurrentPlaybackInformation() {
|
|
454
|
+
PlaylistItemChange<AudioTrack> itemChange = playlistManager.getCurrentItemChange();
|
|
455
|
+
if (itemChange != null) {
|
|
456
|
+
onPlaylistItemChanged(itemChange.getCurrentItem(), itemChange.getHasNext(), itemChange.getHasPrevious());
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
PlaybackState currentPlaybackState = playlistManager.getCurrentPlaybackState();
|
|
460
|
+
if (currentPlaybackState != PlaybackState.STOPPED) {
|
|
461
|
+
onPlaybackStateChanged(currentPlaybackState);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
MediaProgress mediaProgress = playlistManager.getCurrentProgress();
|
|
465
|
+
if (mediaProgress != null) {
|
|
466
|
+
onProgressUpdated(mediaProgress);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
private void registerPlaylistListeners() {
|
|
471
|
+
playlistManager.registerPlaylistListener(this);
|
|
472
|
+
playlistManager.registerProgressListener(this);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
private void removePlaylistListeners() {
|
|
476
|
+
playlistManager.unRegisterPlaylistListener(this);
|
|
477
|
+
playlistManager.unRegisterProgressListener(this);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
private void onError(RmxAudioErrorType errorCode, String trackId, String message) {
|
|
481
|
+
statusListener.onError(errorCode, trackId, message);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
private void onStatus(RmxAudioStatusMessage what, String trackId, JSONObject param) {
|
|
485
|
+
statusListener.onStatus(what, trackId, param);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
package org.dwbn.plugins.playlist;
|
|
2
|
+
|
|
3
|
+
public enum RmxAudioStatusMessage {
|
|
4
|
+
RMXSTATUS_NONE(0),
|
|
5
|
+
RMXSTATUS_REGISTER(1),
|
|
6
|
+
RMXSTATUS_INIT(2),
|
|
7
|
+
RMXSTATUS_ERROR(5),
|
|
8
|
+
|
|
9
|
+
RMXSTATUS_LOADING(10),
|
|
10
|
+
RMXSTATUS_CANPLAY(11),
|
|
11
|
+
RMXSTATUS_LOADED(15),
|
|
12
|
+
RMXSTATUS_STALLED(20),
|
|
13
|
+
RMXSTATUS_BUFFERING(25),
|
|
14
|
+
RMXSTATUS_PLAYING(30),
|
|
15
|
+
RMXSTATUS_PAUSE(35),
|
|
16
|
+
RMXSTATUS_PLAYBACK_POSITION(40),
|
|
17
|
+
RMXSTATUS_SEEK(45),
|
|
18
|
+
RMXSTATUS_COMPLETED(50),
|
|
19
|
+
RMXSTATUS_DURATION(55),
|
|
20
|
+
RMXSTATUS_STOPPED(60),
|
|
21
|
+
|
|
22
|
+
RMX_STATUS_SKIP_FORWARD(90),
|
|
23
|
+
RMX_STATUS_SKIP_BACK(95),
|
|
24
|
+
RMXSTATUS_TRACK_CHANGED(100),
|
|
25
|
+
RMXSTATUS_PLAYLIST_COMPLETED(105),
|
|
26
|
+
RMXSTATUS_ITEM_ADDED(110),
|
|
27
|
+
RMXSTATUS_ITEM_REMOVED(115),
|
|
28
|
+
RMXSTATUS_PLAYLIST_CLEARED(120),
|
|
29
|
+
|
|
30
|
+
RMXSTATUS_VIEWDISAPPEAR(200); // just for testing
|
|
31
|
+
|
|
32
|
+
private final int id;
|
|
33
|
+
RmxAudioStatusMessage(int id) { this.id = id; }
|
|
34
|
+
public int getValue() { return id; }
|
|
35
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
package org.dwbn.plugins.playlist;
|
|
2
|
+
|
|
3
|
+
public interface RmxConstants {
|
|
4
|
+
String DOCUMENTS_SCHEME_PREFIX = "documents://";
|
|
5
|
+
String HTTP_SCHEME_PREFIX = "http://";
|
|
6
|
+
String HTTPS_SCHEME_PREFIX = "https://";
|
|
7
|
+
String CDVFILE_PREFIX = "cdvfile://";
|
|
8
|
+
|
|
9
|
+
// Playlist item management
|
|
10
|
+
String SET_OPTIONS = "setOptions";
|
|
11
|
+
String INITIALIZE = "initialize";
|
|
12
|
+
String SET_PLAYLIST_ITEMS = "setPlaylistItems";
|
|
13
|
+
String ADD_PLAYLIST_ITEM = "addItem";
|
|
14
|
+
String ADD_PLAYLIST_ITEMS = "addAllItems";
|
|
15
|
+
String REMOVE_PLAYLIST_ITEM = "removeItem";
|
|
16
|
+
String REMOVE_PLAYLIST_ITEMS = "removeItems";
|
|
17
|
+
String CLEAR_PLAYLIST_ITEMS = "clearAllItems";
|
|
18
|
+
|
|
19
|
+
// Playback
|
|
20
|
+
String PLAY = "play";
|
|
21
|
+
String PLAY_BY_INDEX = "playTrackByIndex";
|
|
22
|
+
String PLAY_BY_ID = "playTrackById";
|
|
23
|
+
String SELECT_BY_INDEX = "selectTrackByIndex";
|
|
24
|
+
String SELECT_BY_ID = "selectTrackById";
|
|
25
|
+
String PAUSE = "pause";
|
|
26
|
+
String SKIP_FORWARD = "skipForward";
|
|
27
|
+
String SKIP_BACK = "skipBack";
|
|
28
|
+
String SEEK = "seekTo";
|
|
29
|
+
String SEEK_TO_QUEUE_POSITION = "seekToQueuePosition";
|
|
30
|
+
String SET_PLAYBACK_RATE = "setPlaybackRate";
|
|
31
|
+
String SET_PLAYBACK_VOLUME = "setPlaybackVolume";
|
|
32
|
+
String SET_LOOP_ALL = "setLoopAll";
|
|
33
|
+
|
|
34
|
+
// Getters, should almost always be unneeded since the status is continually reported.
|
|
35
|
+
String GET_PLAYBACK_RATE = "getPlaybackRate";
|
|
36
|
+
String GET_PLAYBACK_VOLUME = "getPlaybackVolume";
|
|
37
|
+
String GET_PLAYBACK_POSITION = "getPlaybackPosition";
|
|
38
|
+
String GET_BUFFER_STATUS = "getCurrentBuffer";
|
|
39
|
+
String GET_QUEUE_POSITION = "getQueuePosition";
|
|
40
|
+
|
|
41
|
+
String RELEASE = "release";
|
|
42
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
package org.dwbn.plugins.playlist.data
|
|
2
|
+
|
|
3
|
+
import com.devbrackets.android.playlistcore.annotation.SupportedMediaType
|
|
4
|
+
import com.devbrackets.android.playlistcore.api.PlaylistItem
|
|
5
|
+
import com.devbrackets.android.playlistcore.manager.BasePlaylistManager
|
|
6
|
+
import org.json.JSONException
|
|
7
|
+
import org.json.JSONObject
|
|
8
|
+
|
|
9
|
+
class AudioTrack (private val config: JSONObject) : PlaylistItem {
|
|
10
|
+
var bufferPercentFloat = 0f
|
|
11
|
+
set(buff) {
|
|
12
|
+
// There is a bug in MediaProgress where if bufferPercent == 100 it sets bufferPercentFloat
|
|
13
|
+
// to 100 instead of to 1.
|
|
14
|
+
field = Math.min(Math.max(bufferPercentFloat, buff), 1f)
|
|
15
|
+
}
|
|
16
|
+
var bufferPercent = 0
|
|
17
|
+
set(buff) {
|
|
18
|
+
field = Math.max(bufferPercent, buff)
|
|
19
|
+
}
|
|
20
|
+
var duration: Long = 0
|
|
21
|
+
set(dur) {
|
|
22
|
+
field = Math.max(0, dur)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
fun toDict(): JSONObject {
|
|
26
|
+
val info = JSONObject()
|
|
27
|
+
try {
|
|
28
|
+
info.put("trackId", trackId)
|
|
29
|
+
info.put("isStream", isStream)
|
|
30
|
+
info.put("assetUrl", mediaUrl)
|
|
31
|
+
info.put("albumArt", thumbnailUrl)
|
|
32
|
+
info.put("artist", artist)
|
|
33
|
+
info.put("album", album)
|
|
34
|
+
info.put("title", title)
|
|
35
|
+
} catch (e: JSONException) {
|
|
36
|
+
// I can think of no reason this would ever fail
|
|
37
|
+
}
|
|
38
|
+
return info
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
override val id: Long
|
|
42
|
+
get() =
|
|
43
|
+
if (trackId == null) {
|
|
44
|
+
0
|
|
45
|
+
} else trackId.hashCode().toLong()
|
|
46
|
+
|
|
47
|
+
val isStream: Boolean
|
|
48
|
+
get() = config.optBoolean("isStream", false)
|
|
49
|
+
|
|
50
|
+
val trackId: String?
|
|
51
|
+
get() {
|
|
52
|
+
val trackId = config.optString("trackId")
|
|
53
|
+
return if (trackId == "") {
|
|
54
|
+
null
|
|
55
|
+
} else trackId
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Would really like to set this to true once the cache has it...
|
|
59
|
+
override val downloaded: Boolean
|
|
60
|
+
get() = false // Would really like to set this to true once the cache has it...
|
|
61
|
+
|
|
62
|
+
// ... at which point we can return a value here.
|
|
63
|
+
override val downloadedMediaUri: String?
|
|
64
|
+
get() = null // ... at which point we can return a value here.
|
|
65
|
+
|
|
66
|
+
@get:SupportedMediaType
|
|
67
|
+
override val mediaType: Int
|
|
68
|
+
get() = BasePlaylistManager.AUDIO
|
|
69
|
+
|
|
70
|
+
override val mediaUrl: String
|
|
71
|
+
get() = config.optString("assetUrl", "")
|
|
72
|
+
|
|
73
|
+
// we should have a good default here.
|
|
74
|
+
override val thumbnailUrl: String?
|
|
75
|
+
get() {
|
|
76
|
+
val albumArt = config.optString("albumArt")
|
|
77
|
+
return if (albumArt == "") {
|
|
78
|
+
null
|
|
79
|
+
} else albumArt // we should have a good default here.
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
override val artworkUrl: String?
|
|
83
|
+
get() = thumbnailUrl
|
|
84
|
+
|
|
85
|
+
override val title: String
|
|
86
|
+
get() = config.optString("title")
|
|
87
|
+
|
|
88
|
+
override val album: String
|
|
89
|
+
get() = config.optString("album")
|
|
90
|
+
|
|
91
|
+
override val artist: String
|
|
92
|
+
get() = config.optString("artist")
|
|
93
|
+
|
|
94
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
package org.dwbn.plugins.playlist.manager
|
|
2
|
+
|
|
3
|
+
import org.dwbn.plugins.playlist.data.AudioTrack
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* Interface to enable the PlaylistManager to send these events out.
|
|
7
|
+
* We could add more like play/pause/toggle/stop, but right now there
|
|
8
|
+
* are other ways to get all the other information.
|
|
9
|
+
*/
|
|
10
|
+
interface MediaControlsListener {
|
|
11
|
+
fun onNext(currentItem: AudioTrack?, currentIndex: Int)
|
|
12
|
+
fun onPrevious(currentItem: AudioTrack?, currentIndex: Int)
|
|
13
|
+
}
|