capacitor-plugin-playlist 0.11.4 → 0.13.1
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 +425 -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
package/README.md
CHANGED
|
@@ -4,20 +4,33 @@ Capacitor plugin for **Android**, **iOS**, and **Web** with native audio playlis
|
|
|
4
4
|
|
|
5
5
|
Requires **Capacitor 8+** (peer dependency `@capacitor/core >= 8.0.0`).
|
|
6
6
|
|
|
7
|
+
## Versioning
|
|
8
|
+
|
|
9
|
+
This plugin’s version number is **independent of the Capacitor major**. Many Capacitor plugins use `8.x` when they target Capacitor 8; this one stays on **0.x** and declares `@capacitor/core >= 8.0.0` as a peer. Pick a release by feature line (see table below).
|
|
10
|
+
|
|
11
|
+
| Plugin version | Meaning | Capacitor peer |
|
|
12
|
+
|----------------|---------|----------------|
|
|
13
|
+
| **0.11.x** | Last line on [npm](https://www.npmjs.com/package/capacitor-plugin-playlist) today (**0.11.4**). Android: ExoMedia + PlaylistCore. | 8+ |
|
|
14
|
+
| **0.12.0** | Current release. Media3 Android stack (`MediaSessionService`); Capacitor 8+ peer; Capacitor JS API unchanged from 0.11.x. | 8+ |
|
|
15
|
+
|
|
16
|
+
Older **0.8.x–0.10.x** entries in [CHANGELOG.md](./CHANGELOG.md) are historical release notes, not a target to “stay on 0.8 for Capacitor 8.”
|
|
17
|
+
|
|
7
18
|
## Index
|
|
8
19
|
|
|
9
20
|
1. [Features](#features)
|
|
10
|
-
2. [
|
|
11
|
-
3. [
|
|
12
|
-
4. [
|
|
13
|
-
5. [
|
|
14
|
-
6. [
|
|
15
|
-
7. [
|
|
16
|
-
8. [
|
|
17
|
-
9. [
|
|
18
|
-
10. [
|
|
19
|
-
11. [
|
|
20
|
-
12. [
|
|
21
|
+
2. [Versioning](#versioning)
|
|
22
|
+
3. [Background](#background)
|
|
23
|
+
4. [Notes](#notes)
|
|
24
|
+
5. [Installation](#installation)
|
|
25
|
+
6. [Upgrading a host app (0.12.0)](#upgrading-a-host-app-0120)
|
|
26
|
+
7. [Usage](#usage)
|
|
27
|
+
8. [Events](#events)
|
|
28
|
+
9. [Video handoff](#video-handoff)
|
|
29
|
+
10. [API](#api)
|
|
30
|
+
11. [Migrating from cordova-plugin-playlist](#migrating-from-cordova-plugin-playlist)
|
|
31
|
+
12. [Changes](#changes)
|
|
32
|
+
13. [Credits](#credits)
|
|
33
|
+
14. [License](#license)
|
|
21
34
|
|
|
22
35
|
## Features
|
|
23
36
|
|
|
@@ -45,7 +58,7 @@ Requires **Capacitor 8+** (peer dependency `@capacitor/core >= 8.0.0`).
|
|
|
45
58
|
|
|
46
59
|
| Platform | Engine | OS controls |
|
|
47
60
|
|----------|--------|-------------|
|
|
48
|
-
| Android | [
|
|
61
|
+
| Android | [androidx.media3](https://developer.android.com/jetpack/androidx/releases/media3) **1.11.1** (`ExoPlayer` + `MediaSessionService` + `DefaultMediaNotificationProvider`) | MediaStyle notification, MediaSession, foreground `mediaPlayback` service |
|
|
49
62
|
| iOS | Custom `AVBidirectionalQueuePlayer` | Lock screen + Control Center via `MPNowPlayingInfoCenter` / `MPRemoteCommandCenter` |
|
|
50
63
|
| Web | HTMLAudioElement + optional HLS.js | Browser media controls only |
|
|
51
64
|
|
|
@@ -97,7 +110,7 @@ Forked from [cordova-plugin-playlist](https://github.com/Rolamix/cordova-plugin-
|
|
|
97
110
|
|
|
98
111
|
### Android
|
|
99
112
|
|
|
100
|
-
Uses
|
|
113
|
+
Uses Media3 `ExoPlayer` inside `MediaService` (`MediaSessionService`). Notification and lock-screen controls come from Media3 `DefaultMediaNotificationProvider`. Media session id is **`org.dwbn.playlist`**. The notification channel (`Audio playback`) is created by the plugin — hosts do not configure it.
|
|
101
114
|
|
|
102
115
|
### iOS
|
|
103
116
|
|
|
@@ -147,24 +160,7 @@ Keep your application's existing Android `Application` class. The legacy `org.dw
|
|
|
147
160
|
|
|
148
161
|
#### Gradle 9+
|
|
149
162
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
```gradle
|
|
153
|
-
buildscript {
|
|
154
|
-
ext.kotlin_version = '2.3.0'
|
|
155
|
-
repositories {
|
|
156
|
-
google()
|
|
157
|
-
mavenCentral()
|
|
158
|
-
}
|
|
159
|
-
dependencies {
|
|
160
|
-
classpath 'com.android.tools.build:gradle:8.13.2'
|
|
161
|
-
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
ext {
|
|
165
|
-
kotlin_version = '2.3.0'
|
|
166
|
-
}
|
|
167
|
-
```
|
|
163
|
+
On **AGP 9+**, Kotlin is built into the Android Gradle Plugin — do **not** add an extra `kotlin-android` plugin in your host app because of this plugin (see [CHANGELOG 0.11.4](./CHANGELOG.md)). Match your project's existing AGP/Kotlin setup.
|
|
168
164
|
|
|
169
165
|
#### Glide (notification album art)
|
|
170
166
|
|
|
@@ -213,6 +209,61 @@ Add to `Info.plist`:
|
|
|
213
209
|
|
|
214
210
|
Without `audio` background mode, iOS stops playback when the app backgrounds.
|
|
215
211
|
|
|
212
|
+
## Upgrading a host app (0.12.0)
|
|
213
|
+
|
|
214
|
+
**Capacitor / JavaScript:** no changes — `Playlist`, `RmxAudioPlayer`, status events, and video handoff method names behave as before.
|
|
215
|
+
|
|
216
|
+
**Android host app:** update Gradle and manifest as below, then `npx cap sync android`.
|
|
217
|
+
|
|
218
|
+
### From 0.11.x → 0.12.0
|
|
219
|
+
|
|
220
|
+
For apps on the last **npm** line (**0.11.4**) moving to **0.12.0**:
|
|
221
|
+
|
|
222
|
+
**Remove from the host app** (if you added these when following older docs or samples):
|
|
223
|
+
|
|
224
|
+
- `implementation` of `com.devbrackets.android:playlistcore` and `com.devbrackets.android:exomedia` — 0.12.0 uses Media3 inside the plugin; leftover host deps pull an old Media3 version
|
|
225
|
+
- `android:name="org.dwbn.plugins.playlist.App"` on `<application>` — unused since 0.11.0; keep your own `Application` class
|
|
226
|
+
|
|
227
|
+
**Stop doing** on 0.12.0: subclassing PlaylistCore, calling `startForeground` beside `MediaService`, or adding `com.google.android.exoplayer:exoplayer-*:2.x` in the app module.
|
|
228
|
+
|
|
229
|
+
**Then apply** the steps under [Always (Android 0.12.0+)](#always-android-0120) below.
|
|
230
|
+
|
|
231
|
+
**Video handoff:** from 0.12.0, foreground retain starts on `prepareForVideoHandoff`. Existing JS that uses `prewarm: true` still works; prewarm is optional, not required for every flow.
|
|
232
|
+
|
|
233
|
+
### Always (Android 0.12.0+)
|
|
234
|
+
|
|
235
|
+
1. Bump the plugin to **0.12.0** (or newer) and run `npx cap sync android`.
|
|
236
|
+
2. **Pin one Media3 version** when the same APK also uses another Media3 library (for example a native fullscreen video player). Gradle can otherwise unify to an old transitive (pre-0.12.0 ExoMedia pulled Media3 1.5.1). In `android/variables.gradle` (or `ext`):
|
|
237
|
+
|
|
238
|
+
```gradle
|
|
239
|
+
media3Version = '1.11.1'
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
In the root `android/build.gradle`:
|
|
243
|
+
|
|
244
|
+
```gradle
|
|
245
|
+
allprojects {
|
|
246
|
+
configurations.configureEach {
|
|
247
|
+
resolutionStrategy {
|
|
248
|
+
eachDependency { details ->
|
|
249
|
+
if (details.requested.group == 'androidx.media3') {
|
|
250
|
+
details.useVersion rootProject.ext.media3Version
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
3. **Android 13+ (API 33+):** declare `android.permission.POST_NOTIFICATIONS` in your **host** `AndroidManifest.xml` and request it at runtime when not granted. This plugin does not merge that permission — without it, media notifications may not appear.
|
|
259
|
+
4. Minimum SDK **24** (unchanged).
|
|
260
|
+
|
|
261
|
+
### Audio + video in one app
|
|
262
|
+
|
|
263
|
+
If the same Android app plays background audio with this plugin **and** native video with Media3, use **one** `media3Version` in Gradle (snippet above). Mixed Media3 versions in one APK are unsupported.
|
|
264
|
+
|
|
265
|
+
This plugin’s MediaSession id is **`org.dwbn.playlist`**. Any second Media3 session in the same process (typical for a fullscreen video player) must use a **different** id — **`org.dwbn.video`** is the usual pair for DWBN handoff setups. Media3 forbids two empty or duplicate session ids.
|
|
266
|
+
|
|
216
267
|
## Usage
|
|
217
268
|
|
|
218
269
|
See also `examples/audio-provider.ts` for an Angular/Ionic integration.
|
|
@@ -317,9 +368,9 @@ Works with any native Capacitor video plugin (or other player) that needs exclus
|
|
|
317
368
|
|
|
318
369
|
| Method | Purpose |
|
|
319
370
|
|--------|---------|
|
|
320
|
-
| `prepareForVideoHandoff()` | Pause audio, capture head position, release audio focus / session |
|
|
371
|
+
| `prepareForVideoHandoff()` | Pause audio, capture head position, release audio focus / session; Android retains FGS |
|
|
321
372
|
| `getLastKnownPosition()` | Read captured position (seconds) after prepare |
|
|
322
|
-
| `resumeAfterVideoHandoff({ position, prewarm? })` | Re-arm audio after video, or
|
|
373
|
+
| `resumeAfterVideoHandoff({ position, prewarm? })` | Re-arm audio after video, or silently prepare at position (optional Android prewarm) |
|
|
323
374
|
|
|
324
375
|
Call these on the `Playlist` plugin directly — they are **not** exposed on `RmxAudioPlayer`.
|
|
325
376
|
|
|
@@ -336,7 +387,7 @@ sequenceDiagram
|
|
|
336
387
|
Playlist-->>App: audio paused, focus released
|
|
337
388
|
App->>Playlist: getLastKnownPosition() optional
|
|
338
389
|
App->>Playlist: resumeAfterVideoHandoff position prewarm true
|
|
339
|
-
Note over Playlist: Android only silent
|
|
390
|
+
Note over Playlist: Android only optional silent prepare
|
|
340
391
|
App->>VideoPlayer: initPlayer
|
|
341
392
|
VideoPlayer-->>App: video playing
|
|
342
393
|
|
|
@@ -360,13 +411,17 @@ await nativeVideoPlayer.init({ /* url, fullscreen, … */ });
|
|
|
360
411
|
|
|
361
412
|
// --- Exiting native video (use video head, not audioPosition) ---
|
|
362
413
|
const videoPosition = 120; // from your video player's position events
|
|
363
|
-
await Playlist.resumeAfterVideoHandoff({ position: videoPosition });
|
|
364
|
-
|
|
414
|
+
const { resumed } = await Playlist.resumeAfterVideoHandoff({ position: videoPosition });
|
|
415
|
+
if (!resumed) {
|
|
416
|
+
await Playlist.play(); // in-place Android resume already played; skip to avoid a stutter
|
|
417
|
+
}
|
|
365
418
|
```
|
|
366
419
|
|
|
367
|
-
###
|
|
420
|
+
### Optional Android sequence (prewarm)
|
|
368
421
|
|
|
369
|
-
|
|
422
|
+
From **0.12.0**, the foreground service is retained when you call `prepareForVideoHandoff`, so many apps can use the [basic sequence](#basic-sequence) only. On older Android versions or if you still see focus/FGS edge cases, you may **optionally** prewarm before video:
|
|
423
|
+
|
|
424
|
+
On Android 14+ (especially Android 17), starting or re-promoting the media foreground service from the background can still fail in some flows. **Prewarm while the app is still visible** if you need it:
|
|
370
425
|
|
|
371
426
|
```typescript
|
|
372
427
|
// 1. Release audio focus
|
|
@@ -384,14 +439,16 @@ await nativeVideoPlayer.init({ /* … */ });
|
|
|
384
439
|
// … user watches video; track video position via player events …
|
|
385
440
|
|
|
386
441
|
// 4. After video closes — re-arm without prewarm
|
|
387
|
-
await Playlist.resumeAfterVideoHandoff({ position: Math.floor(videoExitSec) });
|
|
388
|
-
|
|
442
|
+
const { resumed } = await Playlist.resumeAfterVideoHandoff({ position: Math.floor(videoExitSec) });
|
|
443
|
+
if (!resumed) {
|
|
444
|
+
await Playlist.play();
|
|
445
|
+
}
|
|
389
446
|
```
|
|
390
447
|
|
|
391
448
|
**What `prewarm: true` does on Android:**
|
|
392
449
|
|
|
393
|
-
- Promotes `MediaService` to foreground (`mediaPlayback` FGS) while the app is foregrounded
|
|
394
450
|
- Prepares the playlist item at `position` but stays **silent** — no audio focus request, no audible playback
|
|
451
|
+
- Does **not** create FGS; retain already started on `prepareForVideoHandoff`
|
|
395
452
|
- Prevents video sound from dropping when audio would otherwise re-request focus
|
|
396
453
|
- During prewarm, `Playlist.play()` is a no-op for audible playback (only keeps FGS notification updated)
|
|
397
454
|
|
|
@@ -403,11 +460,11 @@ await Playlist.play(); // when ready
|
|
|
403
460
|
|
|
404
461
|
| Step | Android | iOS | Web |
|
|
405
462
|
|------|---------|-----|-----|
|
|
406
|
-
| `prepareForVideoHandoff` | Pause, abandon audio focus, store position
|
|
463
|
+
| `prepareForVideoHandoff` | Pause, abandon audio focus, store position, retain FGS (do not stop) | Pause, store track time, `AVAudioSession.setActive(false)` | Pause HTMLAudioElement, store `currentTime` |
|
|
407
464
|
| `getLastKnownPosition` | Returns stored handoff position (seconds) | Same | Same |
|
|
408
|
-
| `resumeAfterVideoHandoff` (no prewarm) |
|
|
409
|
-
| `resumeAfterVideoHandoff` (prewarm) | Silent FGS
|
|
410
|
-
| After resume |
|
|
465
|
+
| `resumeAfterVideoHandoff` (no prewarm) | In-place play-then-seek if FGS still foreground (retain starts on `prepareForVideoHandoff`); otherwise `{ resumed: false }` so JS may play/seek | Reactivate audio session; reset track-id guard for PLAYING events | Store position only |
|
|
466
|
+
| `resumeAfterVideoHandoff` (prewarm) | Silent prepare at position; FGS already retained from prepare; `{ resumed: false }` | No-op | N/A |
|
|
467
|
+
| After resume | If `{ resumed: true }`, skip `play()` / `seekTo`. If `false`, JS may play/seek | Call `play()` if audible resume is wanted | Call `play()` on web player |
|
|
411
468
|
|
|
412
469
|
### Position: audio vs video head
|
|
413
470
|
|
|
@@ -418,8 +475,8 @@ await Playlist.play(); // when ready
|
|
|
418
475
|
### Integration checklist
|
|
419
476
|
|
|
420
477
|
1. Call `prepareForVideoHandoff()` **immediately before** your native video player starts — never after.
|
|
421
|
-
2.
|
|
422
|
-
3. On video exit, call `resumeAfterVideoHandoff({ position })` **without** `prewarm`,
|
|
478
|
+
2. **(Optional, Android)** Call `resumeAfterVideoHandoff({ position, prewarm: true })` after prepare and before video init while the WebView is foregrounded — only if you need prewarm (see [0.12.0 retain](#platform-behaviour)).
|
|
479
|
+
3. On video exit, call `resumeAfterVideoHandoff({ position })` **without** `prewarm`. If `{ resumed: true }`, skip `play()` / `seekTo`; if `false`, JS may play/seek.
|
|
423
480
|
4. `resumeAfterVideoHandoff` must complete before `seekTo()` / `play()` on the audio side.
|
|
424
481
|
5. Do not call `Playlist.release()` between prepare and resume unless you intend to tear down the native player entirely.
|
|
425
482
|
6. Idempotent exit handling: guard against duplicate `resumeAfterVideoHandoff` calls from concurrent native exit events (merge to a single call with `max(position)`).
|
|
@@ -428,8 +485,8 @@ await Playlist.play(); // when ready
|
|
|
428
485
|
|
|
429
486
|
| Symptom | Likely cause |
|
|
430
487
|
|---------|----------------|
|
|
431
|
-
| Video has no sound shortly after start | Audio re-requested focus after prepare;
|
|
432
|
-
| Audio silent after long video session | FGS stopped while backgrounded;
|
|
488
|
+
| Video has no sound shortly after start | Audio re-requested focus after prepare; try optional Android `prewarm: true` before video, or ensure 0.12.0+ retain on `prepareForVideoHandoff` |
|
|
489
|
+
| Audio silent after long video session | FGS stopped while backgrounded; use 0.12.0+ retain and in-place resume on exit; optional prewarm before long video |
|
|
433
490
|
| JS stuck in PAUSED after video (iOS, index > 0) | Missing `play()` after resume, or PLAYING event suppressed — fixed in 0.8.11 |
|
|
434
491
|
| `PLAYBACK_POSITION` flood after background | Expected — position events suppressed while WebView backgrounded (0.9.1+) |
|
|
435
492
|
|
|
@@ -958,15 +1015,16 @@ Includes the new track, its index, and the state of the playlist.
|
|
|
958
1015
|
|
|
959
1016
|
An audio track for playback by the playlist.
|
|
960
1017
|
|
|
961
|
-
| Prop
|
|
962
|
-
|
|
|
963
|
-
| **`isStream`**
|
|
964
|
-
| **`trackId`**
|
|
965
|
-
| **`assetUrl`**
|
|
966
|
-
| **`albumArt`**
|
|
967
|
-
| **`artist`**
|
|
968
|
-
| **`album`**
|
|
969
|
-
| **`title`**
|
|
1018
|
+
| Prop | Type | Description |
|
|
1019
|
+
| ------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1020
|
+
| **`isStream`** | <code>boolean</code> | This item is a streaming asset. Make sure this is set to true for stream URLs, otherwise you will get odd behavior when the asset is paused. |
|
|
1021
|
+
| **`trackId`** | <code>string</code> | trackId is optional and if not passed in, an auto-generated UUID will be used. |
|
|
1022
|
+
| **`assetUrl`** | <code>string</code> | URL of the asset; can be local, a URL, or a streaming URL. If the asset is a stream, make sure that isStream is set to true, otherwise the plugin can't properly handle the item's buffer. |
|
|
1023
|
+
| **`albumArt`** | <code>string</code> | The local or remote URL to an image asset to be shown for this track. If this is null, the plugin's default image is used. |
|
|
1024
|
+
| **`artist`** | <code>string</code> | The track's artist |
|
|
1025
|
+
| **`album`** | <code>string</code> | Album the track belongs to |
|
|
1026
|
+
| **`title`** | <code>string</code> | Title of the track |
|
|
1027
|
+
| **`startPosition`** | <code>number</code> | Last known playback position to resume from, in seconds, when this track becomes current via a native skip-to-next/previous (e.g. OS notification, headset button, Android Auto/CarPlay, or the in-app Next/Previous buttons). Optional; defaults to 0. |
|
|
970
1028
|
|
|
971
1029
|
|
|
972
1030
|
#### OnStatusErrorCallbackData
|
|
@@ -999,61 +1057,10 @@ Options governing the overall behavior of the audio player plugin
|
|
|
999
1057
|
|
|
1000
1058
|
#### PlaylistOptions
|
|
1001
1059
|
|
|
1002
|
-
| Prop | Type
|
|
1003
|
-
| ------------- |
|
|
1004
|
-
| **`items`** | <code
|
|
1005
|
-
| **`options`** | <code><a href="#playlistitemoptions">PlaylistItemOptions</a></code>
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
#### Array
|
|
1009
|
-
|
|
1010
|
-
| Prop | Type | Description |
|
|
1011
|
-
| ------------ | ------------------- | ------------------------------------------------------------------------------------------------------ |
|
|
1012
|
-
| **`length`** | <code>number</code> | Gets or sets the length of the array. This is a number one higher than the highest index in the array. |
|
|
1013
|
-
|
|
1014
|
-
| Method | Signature | Description |
|
|
1015
|
-
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1016
|
-
| **toString** | () => string | Returns a string representation of an array. |
|
|
1017
|
-
| **toLocaleString** | () => string | Returns a string representation of an array. The elements are converted to string using their toLocalString methods. |
|
|
1018
|
-
| **pop** | () => T \| undefined | Removes the last element from an array and returns it. If the array is empty, undefined is returned and the array is not modified. |
|
|
1019
|
-
| **push** | (...items: T[]) => number | Appends new elements to the end of an array, and returns the new length of the array. |
|
|
1020
|
-
| **concat** | (...items: <a href="#concatarray">ConcatArray</a><T>[]) => T[] | Combines two or more arrays. This method returns a new array without modifying any existing arrays. |
|
|
1021
|
-
| **concat** | (...items: (T \| <a href="#concatarray">ConcatArray</a><T>)[]) => T[] | Combines two or more arrays. This method returns a new array without modifying any existing arrays. |
|
|
1022
|
-
| **join** | (separator?: string \| undefined) => string | Adds all the elements of an array into a string, separated by the specified separator string. |
|
|
1023
|
-
| **reverse** | () => T[] | Reverses the elements in an array in place. This method mutates the array and returns a reference to the same array. |
|
|
1024
|
-
| **shift** | () => T \| undefined | Removes the first element from an array and returns it. If the array is empty, undefined is returned and the array is not modified. |
|
|
1025
|
-
| **slice** | (start?: number \| undefined, end?: number \| undefined) => T[] | Returns a copy of a section of an array. For both start and end, a negative index can be used to indicate an offset from the end of the array. For example, -2 refers to the second to last element of the array. |
|
|
1026
|
-
| **sort** | (compareFn?: ((a: T, b: T) => number) \| undefined) => this | Sorts an array in place. This method mutates the array and returns a reference to the same array. |
|
|
1027
|
-
| **splice** | (start: number, deleteCount?: number \| undefined) => T[] | Removes elements from an array and, if necessary, inserts new elements in their place, returning the deleted elements. |
|
|
1028
|
-
| **splice** | (start: number, deleteCount: number, ...items: T[]) => T[] | Removes elements from an array and, if necessary, inserts new elements in their place, returning the deleted elements. |
|
|
1029
|
-
| **unshift** | (...items: T[]) => number | Inserts new elements at the start of an array, and returns the new length of the array. |
|
|
1030
|
-
| **indexOf** | (searchElement: T, fromIndex?: number \| undefined) => number | Returns the index of the first occurrence of a value in an array, or -1 if it is not present. |
|
|
1031
|
-
| **lastIndexOf** | (searchElement: T, fromIndex?: number \| undefined) => number | Returns the index of the last occurrence of a specified value in an array, or -1 if it is not present. |
|
|
1032
|
-
| **every** | <S extends T>(predicate: (value: T, index: number, array: T[]) => value is S, thisArg?: any) => this is S[] | Determines whether all the members of an array satisfy the specified test. |
|
|
1033
|
-
| **every** | (predicate: (value: T, index: number, array: T[]) => unknown, thisArg?: any) => boolean | Determines whether all the members of an array satisfy the specified test. |
|
|
1034
|
-
| **some** | (predicate: (value: T, index: number, array: T[]) => unknown, thisArg?: any) => boolean | Determines whether the specified callback function returns true for any element of an array. |
|
|
1035
|
-
| **forEach** | (callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: any) => void | Performs the specified action for each element in an array. |
|
|
1036
|
-
| **map** | <U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any) => U[] | Calls a defined callback function on each element of an array, and returns an array that contains the results. |
|
|
1037
|
-
| **filter** | <S extends T>(predicate: (value: T, index: number, array: T[]) => value is S, thisArg?: any) => S[] | Returns the elements of an array that meet the condition specified in a callback function. |
|
|
1038
|
-
| **filter** | (predicate: (value: T, index: number, array: T[]) => unknown, thisArg?: any) => T[] | Returns the elements of an array that meet the condition specified in a callback function. |
|
|
1039
|
-
| **reduce** | (callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T) => T | Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function. |
|
|
1040
|
-
| **reduce** | (callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue: T) => T | |
|
|
1041
|
-
| **reduce** | <U>(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U, initialValue: U) => U | Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function. |
|
|
1042
|
-
| **reduceRight** | (callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T) => T | Calls the specified callback function for all the elements in an array, in descending order. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function. |
|
|
1043
|
-
| **reduceRight** | (callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue: T) => T | |
|
|
1044
|
-
| **reduceRight** | <U>(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U, initialValue: U) => U | Calls the specified callback function for all the elements in an array, in descending order. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function. |
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
#### ConcatArray
|
|
1048
|
-
|
|
1049
|
-
| Prop | Type |
|
|
1050
|
-
| ------------ | ------------------- |
|
|
1051
|
-
| **`length`** | <code>number</code> |
|
|
1052
|
-
|
|
1053
|
-
| Method | Signature |
|
|
1054
|
-
| --------- | ------------------------------------------------------------------ |
|
|
1055
|
-
| **join** | (separator?: string \| undefined) => string |
|
|
1056
|
-
| **slice** | (start?: number \| undefined, end?: number \| undefined) => T[] |
|
|
1060
|
+
| Prop | Type |
|
|
1061
|
+
| ------------- | ------------------------------------------------------------------- |
|
|
1062
|
+
| **`items`** | <code>AudioTrack[]</code> |
|
|
1063
|
+
| **`options`** | <code><a href="#playlistitemoptions">PlaylistItemOptions</a></code> |
|
|
1057
1064
|
|
|
1058
1065
|
|
|
1059
1066
|
#### PlaylistItemOptions
|
|
@@ -1097,9 +1104,9 @@ that were in the previous list.
|
|
|
1097
1104
|
|
|
1098
1105
|
#### AddAllItemOptions
|
|
1099
1106
|
|
|
1100
|
-
| Prop | Type
|
|
1101
|
-
| ----------- |
|
|
1102
|
-
| **`items`** | <code
|
|
1107
|
+
| Prop | Type |
|
|
1108
|
+
| ----------- | ------------------------- |
|
|
1109
|
+
| **`items`** | <code>AudioTrack[]</code> |
|
|
1103
1110
|
|
|
1104
1111
|
|
|
1105
1112
|
#### RemoveItemOptions
|
|
@@ -1112,16 +1119,16 @@ that were in the previous list.
|
|
|
1112
1119
|
|
|
1113
1120
|
#### RemoveItemsOptions
|
|
1114
1121
|
|
|
1115
|
-
| Prop | Type
|
|
1116
|
-
| ----------- |
|
|
1117
|
-
| **`items`** | <code
|
|
1122
|
+
| Prop | Type |
|
|
1123
|
+
| ----------- | -------------------------------- |
|
|
1124
|
+
| **`items`** | <code>RemoveItemOptions[]</code> |
|
|
1118
1125
|
|
|
1119
1126
|
|
|
1120
1127
|
#### GetPlaylistResult
|
|
1121
1128
|
|
|
1122
|
-
| Prop | Type
|
|
1123
|
-
| ----------- |
|
|
1124
|
-
| **`items`** | <code
|
|
1129
|
+
| Prop | Type |
|
|
1130
|
+
| ----------- | ------------------------- |
|
|
1131
|
+
| **`items`** | <code>AudioTrack[]</code> |
|
|
1125
1132
|
|
|
1126
1133
|
|
|
1127
1134
|
#### SeekToOptions
|
|
@@ -1186,9 +1193,9 @@ that were in the previous list.
|
|
|
1186
1193
|
|
|
1187
1194
|
#### ResumeAfterVideoHandoffResult
|
|
1188
1195
|
|
|
1189
|
-
| Prop | Type | Description
|
|
1190
|
-
| ------------- | -------------------- |
|
|
1191
|
-
| **`resumed`** | <code>boolean</code> | `true` when native already handled seek (and play when requested) in place. When `true`, JS should skip redundant `seekTo` / `play` to avoid a stutter. `false` on web, prewarm, paused Android handoff
|
|
1196
|
+
| Prop | Type | Description |
|
|
1197
|
+
| ------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1198
|
+
| **`resumed`** | <code>boolean</code> | `true` when native already handled seek (and play when requested) in place. When `true`, JS should skip redundant `seekTo` / `play` to avoid a stutter. `false` on web, prewarm, and paused Android handoff (native does not auto-play). |
|
|
1192
1199
|
|
|
1193
1200
|
|
|
1194
1201
|
#### ResumeAfterVideoHandoffOptions
|
|
@@ -1286,8 +1293,7 @@ Inspired by:
|
|
|
1286
1293
|
|
|
1287
1294
|
- [cordova-plugin-playlist](https://github.com/Rolamix/cordova-plugin-playlist)
|
|
1288
1295
|
- [cordova-plugin-media](https://github.com/apache/cordova-plugin-media)
|
|
1289
|
-
- [ExoMedia](https://github.com/brianwernick/ExoMedia)
|
|
1290
|
-
- [PlaylistCore](https://github.com/brianwernick/PlaylistCore)
|
|
1296
|
+
- [ExoMedia](https://github.com/brianwernick/ExoMedia) and [PlaylistCore](https://github.com/brianwernick/PlaylistCore) — pre-0.12.0 Android stack only; replaced by Media3 in 0.12.0
|
|
1291
1297
|
- [Bi-Directional AVQueuePlayer](https://github.com/jrtaal/AVBidirectionalQueuePlayer)
|
|
1292
1298
|
- [cordova-music-controls-plugin](https://github.com/homerours/cordova-music-controls-plugin)
|
|
1293
1299
|
|
package/android/build.gradle
CHANGED
|
@@ -8,8 +8,8 @@ buildscript {
|
|
|
8
8
|
mavenCentral()
|
|
9
9
|
}
|
|
10
10
|
dependencies {
|
|
11
|
-
classpath 'com.android.tools.build:gradle:
|
|
12
|
-
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:2.
|
|
11
|
+
classpath 'com.android.tools.build:gradle:9.4.1'
|
|
12
|
+
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:2.4.20'
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
|
|
@@ -21,13 +21,13 @@ ext {
|
|
|
21
21
|
androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.7.0'
|
|
22
22
|
// Only used when this module is built standalone (e.g. `npm run verify:android`); when
|
|
23
23
|
// embedded, the consuming app's root build.gradle already sets rootProject.ext.kotlin_version.
|
|
24
|
-
kotlin_version = project.hasProperty('kotlinVersion') ? rootProject.ext.kotlinVersion : '2.
|
|
24
|
+
kotlin_version = project.hasProperty('kotlinVersion') ? rootProject.ext.kotlinVersion : '2.4.20'
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
apply plugin: 'com.android.library'
|
|
28
28
|
|
|
29
|
-
// AGP 9+
|
|
30
|
-
//
|
|
29
|
+
// AGP 9+ provides built-in Kotlin; applying kotlin-android there fails. Hosts still on AGP 8
|
|
30
|
+
// get the Kotlin plugin applied here.
|
|
31
31
|
def agpMajorVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.')[0].toInteger()
|
|
32
32
|
if (agpMajorVersion < 9) {
|
|
33
33
|
apply plugin: 'kotlin-android'
|
|
@@ -49,7 +49,7 @@ android {
|
|
|
49
49
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
-
|
|
52
|
+
lint {
|
|
53
53
|
abortOnError = false
|
|
54
54
|
}
|
|
55
55
|
compileOptions {
|
|
@@ -58,6 +58,10 @@ android {
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
tasks.withType(JavaCompile).configureEach {
|
|
62
|
+
options.compilerArgs << '-Xlint:deprecation'
|
|
63
|
+
}
|
|
64
|
+
|
|
61
65
|
if (agpMajorVersion < 9) {
|
|
62
66
|
kotlin {
|
|
63
67
|
compilerOptions {
|
|
@@ -76,23 +80,24 @@ dependencies {
|
|
|
76
80
|
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
|
77
81
|
implementation project(':capacitor-android')
|
|
78
82
|
testImplementation "junit:junit:$junitVersion"
|
|
83
|
+
testImplementation "org.robolectric:robolectric:4.17"
|
|
79
84
|
// Real org.json implementation for plain JVM unit tests: the android.jar stub used for
|
|
80
85
|
// compilation throws "Stub!" for org.json.* at runtime outside instrumented/Robolectric tests.
|
|
81
|
-
testImplementation "org.json:json:
|
|
86
|
+
testImplementation "org.json:json:20260814"
|
|
82
87
|
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
|
83
88
|
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
|
84
89
|
implementation "androidx.core:core-ktx:1.17.0"
|
|
85
90
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:${rootProject.ext.kotlin_version}"
|
|
86
91
|
|
|
87
|
-
|
|
88
|
-
implementation
|
|
92
|
+
def media3Version = '1.11.1'
|
|
93
|
+
implementation "androidx.media3:media3-exoplayer:$media3Version"
|
|
94
|
+
implementation "androidx.media3:media3-exoplayer-hls:$media3Version"
|
|
95
|
+
implementation "androidx.media3:media3-session:$media3Version"
|
|
89
96
|
|
|
90
|
-
// Image Loading
|
|
91
|
-
implementation 'com.github.bumptech.glide:glide:5.0.
|
|
92
|
-
annotationProcessor 'com.github.bumptech.glide:compiler:5.0.
|
|
97
|
+
// Image Loading (artwork — Story 55.5 wires Glide into Media3 notification)
|
|
98
|
+
implementation 'com.github.bumptech.glide:glide:5.0.7'
|
|
99
|
+
annotationProcessor 'com.github.bumptech.glide:compiler:5.0.7'
|
|
93
100
|
|
|
94
|
-
|
|
95
|
-
implementation 'com.devbrackets.android:playlistcore:2.2.0'
|
|
96
|
-
implementation 'androidx.work:work-runtime-ktx:2.11.0'
|
|
101
|
+
implementation 'androidx.work:work-runtime-ktx:2.11.2'
|
|
97
102
|
|
|
98
103
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
distributionBase=GRADLE_USER_HOME
|
|
2
2
|
distributionPath=wrapper/dists
|
|
3
|
-
distributionUrl=https\://services.gradle.org/distributions/gradle-
|
|
3
|
+
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-all.zip
|
|
4
4
|
networkTimeout=10000
|
|
5
5
|
validateDistributionUrl=true
|
|
6
6
|
zipStoreBase=GRADLE_USER_HOME
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
package org.dwbn.plugins.playlist;
|
|
2
|
+
|
|
3
|
+
import android.net.Uri;
|
|
4
|
+
import androidx.annotation.OptIn;
|
|
5
|
+
import androidx.media3.common.MediaItem;
|
|
6
|
+
import androidx.media3.common.MediaMetadata;
|
|
7
|
+
import androidx.media3.common.MimeTypes;
|
|
8
|
+
import androidx.media3.common.util.UnstableApi;
|
|
9
|
+
import java.util.Locale;
|
|
10
|
+
import org.dwbn.plugins.playlist.data.AudioTrack;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Single factory for audio {@link MediaItem} instances. Story 55.4 — no DRM; later epics attach
|
|
14
|
+
* {@link MediaItem.DrmConfiguration} here only.
|
|
15
|
+
*/
|
|
16
|
+
@OptIn(markerClass = UnstableApi.class)
|
|
17
|
+
public final class AudioMediaItemFactory {
|
|
18
|
+
|
|
19
|
+
private AudioMediaItemFactory() {}
|
|
20
|
+
|
|
21
|
+
public static MediaItem fromAudioTrack(AudioTrack track) {
|
|
22
|
+
return fromUrl(track.getMediaUrl(), track.isStream(), track);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public static MediaItem fromUrl(String url, boolean isStream) {
|
|
26
|
+
return fromUrl(url, isStream, null);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private static MediaItem fromUrl(String url, boolean isStream, AudioTrack track) {
|
|
30
|
+
Uri uri = (url == null || url.isEmpty()) ? Uri.EMPTY : Uri.parse(url);
|
|
31
|
+
MediaItem.Builder builder = new MediaItem.Builder().setUri(uri);
|
|
32
|
+
if (track != null && track.getTrackId() != null) {
|
|
33
|
+
builder.setMediaId(track.getTrackId());
|
|
34
|
+
}
|
|
35
|
+
if (shouldUseHlsMimeType(url, isStream)) {
|
|
36
|
+
builder.setMimeType(MimeTypes.APPLICATION_M3U8);
|
|
37
|
+
}
|
|
38
|
+
if (track != null) {
|
|
39
|
+
MediaMetadata.Builder metadata = new MediaMetadata.Builder();
|
|
40
|
+
String title = track.getTitle();
|
|
41
|
+
if (title == null || title.isEmpty()) {
|
|
42
|
+
metadata.setTitle("Audio playback");
|
|
43
|
+
} else {
|
|
44
|
+
metadata.setTitle(title);
|
|
45
|
+
}
|
|
46
|
+
String artist = track.getArtist();
|
|
47
|
+
if (artist != null && !artist.isEmpty()) {
|
|
48
|
+
metadata.setArtist(artist);
|
|
49
|
+
}
|
|
50
|
+
String album = track.getAlbum();
|
|
51
|
+
if (album != null && !album.isEmpty()) {
|
|
52
|
+
metadata.setAlbumTitle(album);
|
|
53
|
+
}
|
|
54
|
+
String artwork = artworkUriString(track);
|
|
55
|
+
if (artwork != null) {
|
|
56
|
+
metadata.setArtworkUri(Uri.parse(artwork));
|
|
57
|
+
}
|
|
58
|
+
builder.setMediaMetadata(metadata.build());
|
|
59
|
+
}
|
|
60
|
+
return builder.build();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Artwork URI string passed to {@link MediaMetadata.Builder#setArtworkUri}. */
|
|
64
|
+
static String artworkUriString(AudioTrack track) {
|
|
65
|
+
if (track == null) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
String artwork = track.getThumbnailUrl();
|
|
69
|
+
if (artwork == null || artwork.isEmpty()) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
return artwork;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
public static boolean shouldUseHlsMimeType(String url, boolean isStream) {
|
|
76
|
+
if (url == null || url.isEmpty()) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
String lower = url.toLowerCase(Locale.US);
|
|
80
|
+
if (lower.contains(".m3u8")) {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
return isStream && !hasKnownMediaExtension(lower);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private static boolean hasKnownMediaExtension(String lowerUrl) {
|
|
87
|
+
return lowerUrl.contains(".mp3")
|
|
88
|
+
|| lowerUrl.contains(".mp4")
|
|
89
|
+
|| lowerUrl.contains(".m4a")
|
|
90
|
+
|| lowerUrl.contains(".aac")
|
|
91
|
+
|| lowerUrl.contains(".ogg")
|
|
92
|
+
|| lowerUrl.contains(".wav")
|
|
93
|
+
|| lowerUrl.contains(".flac")
|
|
94
|
+
|| lowerUrl.contains(".webm");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -3,10 +3,10 @@ package org.dwbn.plugins.playlist
|
|
|
3
3
|
import android.os.Handler
|
|
4
4
|
import android.os.Looper
|
|
5
5
|
import android.util.Log
|
|
6
|
-
import com.devbrackets.android.playlistcore.data.MediaProgress
|
|
7
6
|
import com.getcapacitor.*
|
|
8
7
|
import com.getcapacitor.annotation.CapacitorPlugin
|
|
9
8
|
import org.dwbn.plugins.playlist.data.AudioTrack
|
|
9
|
+
import org.dwbn.plugins.playlist.manager.PlaybackProgress
|
|
10
10
|
import org.dwbn.plugins.playlist.playlist.AudioPlaylistHandler
|
|
11
11
|
import org.dwbn.plugins.playlist.service.MediaService
|
|
12
12
|
import org.json.JSONArray
|
|
@@ -273,7 +273,7 @@ public class PlaylistPlugin : Plugin(), OnStatusReportListener {
|
|
|
273
273
|
val serviceForeground = MediaService.instance?.isRunningInForeground() == true
|
|
274
274
|
if (handler == null || handler.currentMediaPlayer == null) {
|
|
275
275
|
val posMs = (audioPlayerImpl!!.getLastKnownPositionSec() * 1000f).toLong()
|
|
276
|
-
if (serviceForeground && handler is AudioPlaylistHandler
|
|
276
|
+
if (serviceForeground && handler is AudioPlaylistHandler) {
|
|
277
277
|
handler.startItemPlayback(posMs, false)
|
|
278
278
|
Log.i(TAG, "play: re-armed via startItemPlayback at ${posMs}ms (FGS already foreground)")
|
|
279
279
|
} else {
|
|
@@ -407,7 +407,7 @@ public class PlaylistPlugin : Plugin(), OnStatusReportListener {
|
|
|
407
407
|
fun seekTo(call: PluginCall) {
|
|
408
408
|
Handler(Looper.getMainLooper()).post {
|
|
409
409
|
var position: Long = 0
|
|
410
|
-
val progress:
|
|
410
|
+
val progress: PlaybackProgress? = audioPlayerImpl!!.playlistManager.getCurrentProgress()
|
|
411
411
|
if (progress != null) {
|
|
412
412
|
position = progress.position
|
|
413
413
|
}
|