capacitor-plugin-playlist 0.10.3 → 0.10.8

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 CHANGED
@@ -1,104 +1,155 @@
1
1
  # capacitor-plugin-playlist
2
2
 
3
- Will probably be published as @dwbn/capacitor-playlist
3
+ Capacitor plugin for **Android**, **iOS**, and **Web** with native audio playlist playback, background support, lock-screen / notification controls, and video handoff.
4
4
 
5
- A capacitor plugin for Android, iOS and Web with native support for audio playlists, background support, and lock screen controls
5
+ Requires **Capacitor 8+** (peer dependency `@capacitor/core >= 8.0.0`).
6
6
 
7
- ## 0. Index
7
+ ## Index
8
8
 
9
- 1. [Background](#1-background)
10
- 2. [Notes](#2-notes)
11
- 3. [Installation](#3-installation)
12
- 4. [Usage](#4-usage)
13
- 5. [Todo](#5-todo)
14
- 6. [Credits](#6-credits)
15
- 7. [License](#7-license)
9
+ 1. [Features](#features)
10
+ 2. [Background](#background)
11
+ 3. [Notes](#notes)
12
+ 4. [Installation](#installation)
13
+ 5. [Usage](#usage)
14
+ 6. [Events](#events)
15
+ 7. [Video handoff](#video-handoff)
16
+ 8. [API](#api)
17
+ 9. [Migrating from cordova-plugin-playlist](#migrating-from-cordova-plugin-playlist)
18
+ 10. [Changes](#changes)
19
+ 11. [Credits](#credits)
20
+ 12. [License](#license)
16
21
 
17
- ## 1. Background
22
+ ## Features
18
23
 
19
- I was using very successfuly cordova-plugin-playlist, many thanks to codinronan an all the contributors!!
20
- Due to upgrades and further development of the app, which used the plugin we decided to go for capacitor instead of
21
- cordova, and here we wanna give back to the community our outcome, any help is appreciated!
24
+ ### Playlist management
22
25
 
23
- ## 2. Notes
26
+ - `setPlaylistItems` — replace entire playlist (optional position retention)
27
+ - `addItem` / `addAllItems` — append tracks
28
+ - `removeItem` / `removeItems` / `clearAllItems` — remove tracks
29
+ - `getPlaylist` — snapshot of current items
30
+ - `setLoop` — loop entire playlist when the last track completes
24
31
 
25
- ### On _Android_, utilizes a wrapper over ExoPlayer called [ExoMedia](https://github.com/brianwernick/ExoMedia). ExoPlayer is a powerful, high-quality player for Android provided by Google
32
+ ### Playback controls
26
33
 
27
- ### On iOS, utilizes a customized AVQueuePlayer in order to provide feedback about track changes, buffering, etc.; given that AVQueuePlayer can keep the audio session running between songs.
34
+ - `play` / `pause`
35
+ - `skipForward` / `skipBack`
36
+ - `seekTo` (seconds)
37
+ - `playTrackByIndex` / `playTrackById` — jump and play
38
+ - `selectTrackByIndex` / `selectTrackById` — select without playing
39
+ - `setPlaybackVolume` (0–1)
40
+ - `setPlaybackRate` (0 pauses, 1 = normal speed)
28
41
 
29
- - This plugin is not designed to play mixable, rapid-fire, low-latency audio, as you would use in a game. A more appropriate cordova plugin for that use case is [cordova-plugin-nativeaudio](https://github.com/floatinghotpot/cordova-plugin-nativeaudio)
42
+ ### Native platform integration
30
43
 
31
- - Cannot mix audio; again the NativeAudio plugin is probably more appropriate. This is due to supporting the lock screen and command center controls: only an app in command of audio can do this, otherwise the controls have no meaning. I would like to add an option to do this, it should be fairly straightforward; at the cost of not supporting the OS-level controls for that invokation.
44
+ | Platform | Engine | OS controls |
45
+ |----------|--------|-------------|
46
+ | Android | [ExoMedia](https://github.com/brianwernick/ExoMedia) + [PlaylistCore](https://github.com/brianwernick/PlaylistCore) | MediaStyle notification, MediaSession, foreground `mediaPlayback` service |
47
+ | iOS | Custom `AVBidirectionalQueuePlayer` | Lock screen + Control Center via `MPNowPlayingInfoCenter` / `MPRemoteCommandCenter` |
48
+ | Web | HTMLAudioElement + optional HLS.js | Browser media controls only |
32
49
 
33
- ## 3. Installation
50
+ ### Background audio
34
51
 
35
- As with most capacitor plugins...
52
+ - Android: foreground media service with `WAKE_LOCK` and `FOREGROUND_SERVICE_MEDIA_PLAYBACK` (Android 14+)
53
+ - iOS: `UIBackgroundModes` → `audio`
54
+ - Position events throttled while WebView is backgrounded; one live snapshot emitted on foreground resume (0.9.1+)
55
+
56
+ ### Tracks and sources
57
+
58
+ - Remote URLs, local files (`file://` or app-resolved paths), and streams
59
+ - Set `isStream: true` on streaming URLs so pause/resume buffering behaves correctly
60
+ - `albumArt` shown in notification / lock screen (Glide on Android)
61
+ - No built-in download manager — resolve offline paths in your app and pass them as `assetUrl`
62
+
63
+ ### Status event stream
64
+
65
+ Single `status` listener with `RmxAudioStatusMessage` events (see [Events](#events)).
66
+
67
+ ### Video handoff
68
+
69
+ When switching from background audio to native fullscreen video:
70
+
71
+ - `prepareForVideoHandoff()` — release audio focus / session
72
+ - `getLastKnownPosition()` — saved head position (seconds)
73
+ - `resumeAfterVideoHandoff({ position, prewarm? })` — re-arm audio after video
74
+
75
+ See [Video handoff](#video-handoff).
76
+
77
+ ### Cordova-compatible wrapper
78
+
79
+ `RmxAudioPlayer` (`src/RmxAudioPlayer.ts`) is a drop-in replacement for cordova-plugin-playlist with `on('status')` / `off('status')` and state getters (`isPlaying`, `currentTrack`, etc.).
80
+
81
+ **Not exposed on `RmxAudioPlayer`:** `getPlaylist`, video handoff methods — call `Playlist` directly for those.
82
+
83
+ ### Not supported
84
+
85
+ - Shuffle
86
+ - Reorder API
87
+ - Mixable / low-latency game audio (use [cordova-plugin-nativeaudio](https://github.com/floatinghotpot/cordova-plugin-nativeaudio) instead)
88
+ - Simultaneous audio mixing (lock-screen controls require exclusive audio focus)
89
+
90
+ ## Background
91
+
92
+ Forked from [cordova-plugin-playlist](https://github.com/Rolamix/cordova-plugin-playlist) for Capacitor.
93
+
94
+ ## Notes
95
+
96
+ ### Android
97
+
98
+ Uses ExoMedia (ExoPlayer wrapper) with PlaylistCore for notification and MediaSession integration.
99
+
100
+ ### iOS
101
+
102
+ Uses a customized AVQueuePlayer (`AVBidirectionalQueuePlayer`) for track-change feedback and continuous audio session between songs. Minimum iOS **18** (0.9.4+). Swift Package Manager supported (0.10.0+).
103
+
104
+ ## Installation
36
105
 
37
106
  ```
38
107
  npm i capacitor-plugin-playlist
39
108
  npx cap sync
40
109
  ```
41
110
 
42
- ### For web
111
+ ### Web
43
112
 
44
- include hlsjs in your build
113
+ Include HLS.js in your build for HLS streams.
45
114
 
46
- #### E.G. for Angular 2+:
115
+ #### Angular example
47
116
 
48
117
  ```
49
118
  npm i hls.js
50
119
  ```
51
120
 
52
- then add to angular.json:
53
- architect => build => options:
121
+ Add to `angular.json` → architect → build → options → scripts:
54
122
 
55
- ```
56
- "scripts": [
57
- {
58
- "input": "node_modules/hls.js/dist/hls.min.js"
59
- }
60
- ]
123
+ ```json
124
+ "scripts": [
125
+ {
126
+ "input": "node_modules/hls.js/dist/hls.min.js"
127
+ }
128
+ ]
61
129
  ```
62
130
 
63
- ##### AndroidManifest.xml:
131
+ ### Android
64
132
 
65
- ```
66
- <uses-permission android:name="android.permission.WAKE_LOCK" />
67
- <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
68
- <application
69
- android:name="org.dwbn.plugins.playlist.App"
70
- >
71
- <service android:enabled="true" android:exported="false"
72
- android:name="org.dwbn.plugins.playlist.service.MediaService">
73
- </service>
74
- </application>
75
- ```
76
-
77
- **Note**: Starting with Android 14, you now need to specify the `foregroundServiceType` and request the appropriate permission:
133
+ #### AndroidManifest.xml
78
134
 
135
+ ```xml
136
+ <uses-permission android:name="android.permission.WAKE_LOCK" />
137
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
138
+ <application android:name="org.dwbn.plugins.playlist.App">
139
+ <service android:enabled="true" android:exported="false"
140
+ android:foregroundServiceType="mediaPlayback"
141
+ android:name="org.dwbn.plugins.playlist.service.MediaService">
142
+ </service>
143
+ </application>
79
144
  ```
80
- <uses-permission android:name="android.permission.WAKE_LOCK" />
81
- <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
82
- <application
83
- android:name="org.dwbn.plugins.playlist.App"
84
- >
85
- <service android:enabled="true" android:exported="false"
86
- android:foregroundServiceType="mediaPlayback"
87
- android:name="org.dwbn.plugins.playlist.service.MediaService">
88
- </service>
89
- </application>
90
- ```
91
-
92
- ##### Gradle Configuration (Gradle 9+)
93
145
 
94
- **Important**: If you're using Gradle 9 or later, you need to ensure the Kotlin plugin is declared in your root `android/build.gradle` file. This plugin no longer includes its own `buildscript` block to avoid conflicts with Gradle 9's plugin loading requirements.
146
+ #### Gradle 9+
95
147
 
96
- Add the Kotlin plugin to your root `android/build.gradle`:
148
+ Ensure the Kotlin plugin is declared in your root `android/build.gradle` (this plugin no longer ships its own `buildscript` block):
97
149
 
98
150
  ```gradle
99
151
  buildscript {
100
- ext.kotlin_version = '2.3.0' // Or your preferred Kotlin version
101
-
152
+ ext.kotlin_version = '2.3.0'
102
153
  repositories {
103
154
  google()
104
155
  mavenCentral()
@@ -106,22 +157,18 @@ buildscript {
106
157
  dependencies {
107
158
  classpath 'com.android.tools.build:gradle:8.13.2'
108
159
  classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
109
- // ... other dependencies
110
160
  }
111
161
  }
112
-
113
162
  ext {
114
- kotlin_version = '2.3.0' // Make it available to subprojects
163
+ kotlin_version = '2.3.0'
115
164
  }
116
165
  ```
117
166
 
118
- The plugin's `build.gradle` will automatically use the Kotlin plugin from the root project. If you see a warning about the Kotlin plugin being loaded multiple times, ensure you've removed any `buildscript` blocks that declare the Kotlin plugin from other subprojects (except the root project).
167
+ #### Glide (notification album art)
119
168
 
120
- ##### Glide image loading for notifiction center
169
+ Create `MyAppGlideModule.java`:
121
170
 
122
- To be able to use glide you need to create a file MyAppGlideModule.java:
123
-
124
- ```
171
+ ```java
125
172
  package org.your.package.namespace;
126
173
 
127
174
  import com.bumptech.glide.annotation.GlideModule;
@@ -131,113 +178,1062 @@ import com.bumptech.glide.module.AppGlideModule;
131
178
  public final class MyAppGlideModule extends AppGlideModule {}
132
179
  ```
133
180
 
134
- also see https://guides.codepath.com/android/Displaying-Images-with-the-Glide-Library
181
+ See https://guides.codepath.com/android/Displaying-Images-with-the-Glide-Library
182
+
183
+ #### Notification icon
184
+
185
+ Create a transparent silhouette icon (e.g. `ic_notification.png`) and pass it via `setOptions`:
186
+
187
+ ```typescript
188
+ await Playlist.setOptions({
189
+ verbose: !environment.production,
190
+ options: { icon: 'ic_notification' },
191
+ });
192
+ ```
193
+
194
+ In Android Studio: right-click `res` → New → Image Asset → Notification Icons.
195
+
196
+ ![](docs/375350860-3f34e812-b283-4014-b566-52c519c511d3.jpg)
197
+
198
+ ![](docs/375351762-aa5ef9a1-5303-4f6a-8218-6c02ea797290.jpg)
135
199
 
136
200
  ### iOS
137
201
 
138
- ##### inside Info.plist:
202
+ Add to `Info.plist`:
203
+
204
+ ```xml
205
+ <key>UIBackgroundModes</key>
206
+ <array>
207
+ <string>audio</string>
208
+ <string>fetch</string>
209
+ </array>
210
+ ```
211
+
212
+ Without `audio` background mode, iOS stops playback when the app backgrounds.
213
+
214
+ ## Usage
215
+
216
+ See also `examples/audio-provider.ts` for an Angular/Ionic integration.
217
+
218
+ ### Basic flow (`Playlist` API)
219
+
220
+ ```typescript
221
+ import { Playlist, AudioTrack, RmxAudioStatusMessage } from 'capacitor-plugin-playlist';
222
+
223
+ await Playlist.setOptions({
224
+ verbose: true,
225
+ resetStreamOnPause: true,
226
+ options: { icon: 'ic_notification' },
227
+ });
228
+
229
+ await Playlist.initialize();
139
230
 
231
+ const handle = await Playlist.addListener('status', ({ status }) => {
232
+ if (status.msgType === RmxAudioStatusMessage.RMXSTATUS_PLAYBACK_POSITION) {
233
+ // update UI progress
234
+ }
235
+ });
236
+
237
+ const track: AudioTrack = {
238
+ trackId: 'track-1',
239
+ assetUrl: 'https://example.com/audio.mp3',
240
+ title: 'Track title',
241
+ artist: 'Artist name',
242
+ album: 'Album name',
243
+ albumArt: 'https://example.com/cover.jpg',
244
+ isStream: false,
245
+ };
246
+
247
+ await Playlist.setPlaylistItems({
248
+ items: [track],
249
+ options: { startPaused: false },
250
+ });
251
+
252
+ await Playlist.play();
253
+ // later: handle.remove();
140
254
  ```
141
- <key>UIBackgroundModes</key>
142
- <array>
143
- <string>audio</string>
144
- <string>fetch</string>
145
- </array>
255
+
256
+ ### `RmxAudioPlayer` wrapper (Cordova migration)
257
+
258
+ ```typescript
259
+ import { RmxAudioPlayer, AudioTrack } from 'capacitor-plugin-playlist';
260
+
261
+ const player = new RmxAudioPlayer();
262
+ await player.initialize();
263
+
264
+ player.on('status', (data) => {
265
+ console.log('status', data.msgType, data);
266
+ });
267
+
268
+ await player.setLoop(true);
269
+ await player.setPlaylistItems([track], { retainPosition: true, playFromId: track.trackId });
270
+ await player.play();
271
+ ```
272
+
273
+ ## Events
274
+
275
+ Subscribe via `Playlist.addListener('status', …)` or `RmxAudioPlayer.on('status', …)`.
276
+
277
+ Each callback receives `{ action: 'status', status: OnStatusCallbackData }` where:
278
+
279
+ - `status.trackId` — current track id, `"NONE"` when idle, `"INVALID"` when playlist completed
280
+ - `status.msgType` — `RmxAudioStatusMessage` enum value
281
+ - `status.value` — payload (shape depends on `msgType`)
282
+
283
+ | msgType | Name | When | Payload |
284
+ |---------|------|------|---------|
285
+ | 5 | ERROR | Playback or network failure | `OnStatusErrorCallbackData` (`code`, `message`) |
286
+ | 10 | LOADING | Track loading started | `OnStatusCallbackUpdateData` |
287
+ | 11 | CANPLAY | Track ready to play | `OnStatusCallbackUpdateData` |
288
+ | 15 | LOADED | Track fully loaded | `OnStatusCallbackUpdateData` |
289
+ | 20 | STALLED | iOS: network stall | `OnStatusCallbackUpdateData` |
290
+ | 25 | BUFFERING | Buffer progress update | `OnStatusCallbackUpdateData` |
291
+ | 30 | PLAYING | Playback started/resumed | `OnStatusCallbackUpdateData` |
292
+ | 35 | PAUSE | Playback paused | `OnStatusCallbackUpdateData` |
293
+ | 40 | PLAYBACK_POSITION | Periodic position tick | `OnStatusCallbackUpdateData` (suppressed while WebView backgrounded) |
294
+ | 45 | SEEK | User or app seeked | `OnStatusCallbackUpdateData` |
295
+ | 50 | COMPLETED | Current track finished | `OnStatusCallbackUpdateData` |
296
+ | 55 | DURATION | Duration first known | `OnStatusCallbackUpdateData` |
297
+ | 60 | STOPPED | All playback stopped | `OnStatusCallbackUpdateData` |
298
+ | 90 | SKIP_FORWARD | Skipped to next track | `OnStatusCallbackUpdateData` |
299
+ | 95 | SKIP_BACK | Skipped to previous track | `OnStatusCallbackUpdateData` |
300
+ | 100 | TRACK_CHANGED | Active track changed | `OnStatusTrackChangedData` |
301
+ | 105 | PLAYLIST_COMPLETED | Entire playlist finished | `OnStatusCallbackUpdateData` |
302
+ | 110 | ITEM_ADDED | Track added | `OnStatusCallbackUpdateData` |
303
+ | 115 | ITEM_REMOVED | Track removed | `OnStatusCallbackUpdateData` |
304
+ | 120 | PLAYLIST_CLEARED | All tracks removed | `OnStatusCallbackUpdateData` |
305
+
306
+ For track changes, prefer handling `TRACK_CHANGED` over `SKIP_FORWARD` / `SKIP_BACK`.
307
+
308
+ ## Video handoff
309
+
310
+ Native audio and native fullscreen video cannot share audio focus. These three methods coordinate a clean handoff when the user opens video while audio was playing (or when switching back to audio after video).
311
+
312
+ Works with any native Capacitor video plugin (or other player) that needs exclusive audio focus.
313
+
314
+ ### Methods
315
+
316
+ | Method | Purpose |
317
+ |--------|---------|
318
+ | `prepareForVideoHandoff()` | Pause audio, capture head position, release audio focus / session |
319
+ | `getLastKnownPosition()` | Read captured position (seconds) after prepare |
320
+ | `resumeAfterVideoHandoff({ position, prewarm? })` | Re-arm audio after video, or prewarm Android FGS before video |
321
+
322
+ Call these on the `Playlist` plugin directly — they are **not** exposed on `RmxAudioPlayer`.
323
+
324
+ ### Lifecycle
325
+
326
+ ```mermaid
327
+ sequenceDiagram
328
+ participant App
329
+ participant Playlist
330
+ participant VideoPlayer
331
+
332
+ Note over App,VideoPlayer: Entering video
333
+ App->>Playlist: prepareForVideoHandoff()
334
+ Playlist-->>App: audio paused, focus released
335
+ App->>Playlist: getLastKnownPosition() optional
336
+ App->>Playlist: resumeAfterVideoHandoff position prewarm true
337
+ Note over Playlist: Android only silent FGS prewarm
338
+ App->>VideoPlayer: initPlayer
339
+ VideoPlayer-->>App: video playing
340
+
341
+ Note over App,VideoPlayer: Exiting video
342
+ App->>Playlist: resumeAfterVideoHandoff position
343
+ Note over Playlist: re-arm session no prewarm
344
+ App->>Playlist: play optional
345
+ Playlist-->>App: audio resumes
146
346
  ```
147
347
 
148
- Android normally will give you ~2-3 minutes of background playback before killing your audio. Adding the WAKE_LOCK permission allows the plugin to utilize additional permissions to continue playing.
348
+ ### Basic sequence
149
349
 
150
- iOS will immediately stop playback when the app goes into the background if you do not include the `audio` `UIBackgroundMode`. iOS has an additional requirement that audio playback must never stop; when it does, the audio session will be terminated and playback cannot continue without user interaction.
350
+ ```typescript
351
+ import { Playlist } from 'capacitor-plugin-playlist';
151
352
 
152
- ### Android notification icon
353
+ // --- Entering native video ---
354
+ await Playlist.prepareForVideoHandoff();
355
+ const { position: audioPosition } = await Playlist.getLastKnownPosition();
153
356
 
154
- To show a better notification icon in Android Lollipop (API 21) and above, create a transparent (silhouette) icon and name the file e.g. as "ic_notification.png".
155
- Then you can use the options like:
357
+ await nativeVideoPlayer.init({ /* url, fullscreen, */ });
156
358
 
359
+ // --- Exiting native video (use video head, not audioPosition) ---
360
+ const videoPosition = 120; // from your video player's position events
361
+ await Playlist.resumeAfterVideoHandoff({ position: videoPosition });
362
+ await Playlist.play(); // if user should resume audible playback
157
363
  ```
158
- await Playlist.setOptions({
159
- verbose: !environment.production,
160
- options: {
161
- icon: 'ic_notification'
162
- },
364
+
365
+ ### Recommended Android sequence (with prewarm)
366
+
367
+ On Android 14+ (especially Android 17), starting or re-promoting the media foreground service from the background can fail or mute playback. **Prewarm while the app is still visible** before video starts:
368
+
369
+ ```typescript
370
+ // 1. Release audio focus
371
+ await Playlist.prepareForVideoHandoff();
372
+
373
+ // 2. Prewarm FGS silently at the video start position (Android)
374
+ await Playlist.resumeAfterVideoHandoff({
375
+ position: Math.floor(videoStartSec),
376
+ prewarm: true,
163
377
  });
378
+
379
+ // 3. Start native video
380
+ await nativeVideoPlayer.init({ /* … */ });
381
+
382
+ // … user watches video; track video position via player events …
383
+
384
+ // 4. After video closes — re-arm without prewarm
385
+ await Playlist.resumeAfterVideoHandoff({ position: Math.floor(videoExitSec) });
386
+ await Playlist.play(); // when ready
164
387
  ```
165
388
 
166
- In Android Studio, you can right-click on `res` folder and choose `New > Image Asset`:
389
+ **What `prewarm: true` does on Android:**
390
+
391
+ - Promotes `MediaService` to foreground (`mediaPlayback` FGS) while the app is foregrounded
392
+ - Prepares the playlist item at `position` but stays **silent** — no audio focus request, no audible playback
393
+ - Prevents video sound from dropping when audio would otherwise re-request focus
394
+ - During prewarm, `Playlist.play()` is a no-op for audible playback (only keeps FGS notification updated)
395
+
396
+ **iOS:** `prewarm` is accepted but ignored. Use `prepareForVideoHandoff()` → video → `resumeAfterVideoHandoff({ position })` → `play()`.
397
+
398
+ **Web:** Both methods are stubs (pause + store position). No native session handoff.
399
+
400
+ ### Platform behaviour
401
+
402
+ | Step | Android | iOS | Web |
403
+ |------|---------|-----|-----|
404
+ | `prepareForVideoHandoff` | Pause, abandon audio focus, store position via `MediaProgress` | Pause, store track time, `AVAudioSession.setActive(false)` | Pause HTMLAudioElement, store `currentTime` |
405
+ | `getLastKnownPosition` | Returns stored handoff position (seconds) | Same | Same |
406
+ | `resumeAfterVideoHandoff` (no prewarm) | Re-request focus; in-place resume if FGS still foreground from prewarm, else `beginPlayback` | Reactivate audio session; reset track-id guard for PLAYING events | Store position only |
407
+ | `resumeAfterVideoHandoff` (prewarm) | Silent FGS + prepare at position, no focus/play | No-op | N/A |
408
+ | After resume | Call `play()` to start audible playback | Call `play()` to start audible playback | Call `play()` on web player |
409
+
410
+ ### Position: audio vs video head
411
+
412
+ - **`getLastKnownPosition()`** after `prepareForVideoHandoff` — last **audio** head before video opened. Useful if video never started or for debugging.
413
+ - **On video exit** — pass the **video** playback position (from your video player's position events), not the stale audio position, so audio resumes where the user left off in the video timeline.
414
+ - Track video head while video plays and persist it if the app may cold-start (e.g. after PiP dismiss on Android).
415
+
416
+ ### Integration checklist
417
+
418
+ 1. Call `prepareForVideoHandoff()` **immediately before** your native video player starts — never after.
419
+ 2. On Android, call `resumeAfterVideoHandoff({ position, prewarm: true })` **after prepare and before video init**, while the WebView is still foregrounded.
420
+ 3. On video exit, call `resumeAfterVideoHandoff({ position })` **without** `prewarm`, then `play()` if playback should resume.
421
+ 4. `resumeAfterVideoHandoff` must complete before `seekTo()` / `play()` on the audio side.
422
+ 5. Do not call `Playlist.release()` between prepare and resume unless you intend to tear down the native player entirely.
423
+ 6. Idempotent exit handling: guard against duplicate `resumeAfterVideoHandoff` calls from concurrent native exit events (merge to a single call with `max(position)`).
424
+
425
+ ### Common pitfalls
426
+
427
+ | Symptom | Likely cause |
428
+ |---------|----------------|
429
+ | Video has no sound shortly after start | Audio re-requested focus after prepare; use Android `prewarm: true` before video starts |
430
+ | Audio silent after long video session | FGS stopped while backgrounded; prewarm before video + in-place resume on exit (0.8.10+) |
431
+ | JS stuck in PAUSED after video (iOS, index > 0) | Missing `play()` after resume, or PLAYING event suppressed — fixed in 0.8.11 |
432
+ | `PLAYBACK_POSITION` flood after background | Expected — position events suppressed while WebView backgrounded (0.9.1+) |
433
+
434
+ See [CHANGELOG.md](./CHANGELOG.md) for version-specific fixes (0.8.8–0.10.3).
435
+
436
+ ## API
437
+
438
+ <docgen-index>
439
+
440
+ * [`addListener('status', ...)`](#addlistenerstatus-)
441
+ * [`setOptions(...)`](#setoptions)
442
+ * [`initialize()`](#initialize)
443
+ * [`release()`](#release)
444
+ * [`setPlaylistItems(...)`](#setplaylistitems)
445
+ * [`addItem(...)`](#additem)
446
+ * [`addAllItems(...)`](#addallitems)
447
+ * [`removeItem(...)`](#removeitem)
448
+ * [`removeItems(...)`](#removeitems)
449
+ * [`clearAllItems()`](#clearallitems)
450
+ * [`getPlaylist()`](#getplaylist)
451
+ * [`play()`](#play)
452
+ * [`pause()`](#pause)
453
+ * [`skipForward()`](#skipforward)
454
+ * [`skipBack()`](#skipback)
455
+ * [`seekTo(...)`](#seekto)
456
+ * [`playTrackByIndex(...)`](#playtrackbyindex)
457
+ * [`playTrackById(...)`](#playtrackbyid)
458
+ * [`selectTrackByIndex(...)`](#selecttrackbyindex)
459
+ * [`selectTrackById(...)`](#selecttrackbyid)
460
+ * [`setPlaybackVolume(...)`](#setplaybackvolume)
461
+ * [`setLoop(...)`](#setloop)
462
+ * [`setPlaybackRate(...)`](#setplaybackrate)
463
+ * [`prepareForVideoHandoff()`](#prepareforvideohandoff)
464
+ * [`resumeAfterVideoHandoff(...)`](#resumeaftervideohandoff)
465
+ * [`getLastKnownPosition()`](#getlastknownposition)
466
+ * [Interfaces](#interfaces)
467
+ * [Type Aliases](#type-aliases)
468
+ * [Enums](#enums)
469
+
470
+ </docgen-index>
471
+
472
+ <docgen-api>
473
+ <!--Update the source file JSDoc comments and rerun docgen to update the docs below-->
474
+
475
+ ### addListener('status', ...)
476
+
477
+ ```typescript
478
+ addListener(eventName: 'status', listenerFunc: PlaylistStatusChangeCallback) => Promise<PluginListenerHandle>
479
+ ```
167
480
 
168
- ![](docs/375350860-3f34e812-b283-4014-b566-52c519c511d3.jpg)
481
+ Subscribe to native playback status events (track changes, position, errors, etc.).
169
482
 
170
- Select type: `Notification Icons` and choose your icon, click `Next` to save it.
483
+ | Param | Type | Description |
484
+ | ------------------ | ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
485
+ | **`eventName`** | <code>'status'</code> | Must be `'status'`. |
486
+ | **`listenerFunc`** | <code><a href="#playliststatuschangecallback">PlaylistStatusChangeCallback</a></code> | Callback receiving `{ action, status }` where `status.msgType` is a <a href="#rmxaudiostatusmessage">`RmxAudioStatusMessage`</a>. |
171
487
 
172
- ![](docs/375351762-aa5ef9a1-5303-4f6a-8218-6c02ea797290.jpg)
488
+ **Returns:** <code>Promise&lt;<a href="#pluginlistenerhandle">PluginListenerHandle</a>&gt;</code>
489
+
490
+ --------------------
173
491
 
174
492
 
175
- ## 4. Usage
493
+ ### setOptions(...)
176
494
 
177
- Be sure to check out the examples folder, where you can find an Angular10/Ionic5 implementation of the Capacitor plugin.
178
- Just drop into your project and go.
179
- Should be quite obvious howto adapt this for other frameworks, or just vanillaJS
495
+ ```typescript
496
+ setOptions(options: AudioPlayerOptions) => Promise<void>
497
+ ```
498
+
499
+ Configure plugin behaviour (verbose logging, stream pause handling, notification icon).
500
+ Can be called at any time; not required before playback.
501
+
502
+ | Param | Type |
503
+ | ------------- | ----------------------------------------------------------------- |
504
+ | **`options`** | <code><a href="#audioplayeroptions">AudioPlayerOptions</a></code> |
180
505
 
181
- ### API
506
+ --------------------
182
507
 
183
- Example:
184
- @see definition.ts
185
508
 
509
+ ### initialize()
510
+
511
+ ```typescript
512
+ initialize() => Promise<void>
186
513
  ```
187
- import {Playlist, AudioTrack} from 'capacitor-plugin-playlist'
188
514
 
189
- const item: AudioTrack = {
190
- trackId: 1,
191
- assetUrl: 'http://your_audio',
192
- albumArt: 'http://some_image'
193
- }
194
- await Playlist.addItem({item})
515
+ Initialise the native player, register status callbacks, and arm lock-screen / notification controls.
516
+ Call once before playback (e.g. on app start).
517
+
518
+ --------------------
519
+
520
+
521
+ ### release()
522
+
523
+ ```typescript
524
+ release() => Promise<void>
195
525
  ```
196
526
 
197
- - TODO!
527
+ Tear down native resources (audio session, media service, observers).
528
+ Call when the app no longer needs background audio (e.g. on logout).
198
529
 
199
- ### Migrating from cordova-plugin-playlist
530
+ --------------------
200
531
 
201
- See the use the shipped RmxAudioPlayer.ts
202
532
 
203
- Its a meant as a drop in replacement
533
+ ### setPlaylistItems(...)
534
+
535
+ ```typescript
536
+ setPlaylistItems(options: PlaylistOptions) => Promise<void>
537
+ ```
204
538
 
205
- in the best case you only change your import. :D
539
+ Replace the entire playlist. Clears all previous items.
540
+ Use `options.retainPosition` to keep the current track and playback position.
206
541
 
207
- ## 5. Todo
542
+ | Param | Type |
543
+ | ------------- | ----------------------------------------------------------- |
544
+ | **`options`** | <code><a href="#playlistoptions">PlaylistOptions</a></code> |
208
545
 
209
- - [iOS] Utilize [AudioPlayer](https://github.com/delannoyk/AudioPlayer) instead of directly implementing AVQueuePlayer. `AudioPlayer` includes some smart network recovery features
210
- - [iOS, Android] Add a full example
546
+ --------------------
211
547
 
212
- ## 6. Credits
213
548
 
214
- There are several plugins that are similar to this one, but all are focused on aspects of the media management experience. This plugin takes inspiration from:
549
+ ### addItem(...)
550
+
551
+ ```typescript
552
+ addItem(options: AddItemOptions) => Promise<void>
553
+ ```
554
+
555
+ Append a single track to the end of the playlist.
556
+
557
+ | Param | Type |
558
+ | ------------- | --------------------------------------------------------- |
559
+ | **`options`** | <code><a href="#additemoptions">AddItemOptions</a></code> |
560
+
561
+ --------------------
562
+
563
+
564
+ ### addAllItems(...)
565
+
566
+ ```typescript
567
+ addAllItems(options: AddAllItemOptions) => Promise<void>
568
+ ```
569
+
570
+ Append multiple tracks to the end of the playlist.
571
+ Raises one `RMXSTATUS_ITEM_ADDED` event per track.
572
+
573
+ | Param | Type |
574
+ | ------------- | --------------------------------------------------------------- |
575
+ | **`options`** | <code><a href="#addallitemoptions">AddAllItemOptions</a></code> |
576
+
577
+ --------------------
578
+
579
+
580
+ ### removeItem(...)
581
+
582
+ ```typescript
583
+ removeItem(options: RemoveItemOptions) => Promise<void>
584
+ ```
585
+
586
+ Remove a track by index (preferred) or id.
587
+ If the removed track is currently playing, the next track starts automatically.
588
+
589
+ | Param | Type |
590
+ | ------------- | --------------------------------------------------------------- |
591
+ | **`options`** | <code><a href="#removeitemoptions">RemoveItemOptions</a></code> |
592
+
593
+ --------------------
594
+
595
+
596
+ ### removeItems(...)
597
+
598
+ ```typescript
599
+ removeItems(options: RemoveItemsOptions) => Promise<void>
600
+ ```
601
+
602
+ Remove multiple tracks in a single batch.
603
+ If the currently playing track is removed, the next available track starts automatically.
604
+
605
+ | Param | Type |
606
+ | ------------- | ----------------------------------------------------------------- |
607
+ | **`options`** | <code><a href="#removeitemsoptions">RemoveItemsOptions</a></code> |
608
+
609
+ --------------------
610
+
611
+
612
+ ### clearAllItems()
613
+
614
+ ```typescript
615
+ clearAllItems() => Promise<void>
616
+ ```
617
+
618
+ Remove all tracks from the playlist. Raises `RMXSTATUS_PLAYLIST_CLEARED` and `RMXSTATUS_STOPPED`.
619
+
620
+ --------------------
621
+
622
+
623
+ ### getPlaylist()
624
+
625
+ ```typescript
626
+ getPlaylist() => Promise<GetPlaylistResult>
627
+ ```
628
+
629
+ Return a snapshot of the current playlist items.
630
+
631
+ **Returns:** <code>Promise&lt;<a href="#getplaylistresult">GetPlaylistResult</a>&gt;</code>
632
+
633
+ --------------------
634
+
635
+
636
+ ### play()
637
+
638
+ ```typescript
639
+ play() => Promise<void>
640
+ ```
641
+
642
+ Start or resume playback of the current track.
643
+ No-op if the playlist is empty.
644
+
645
+ --------------------
646
+
647
+
648
+ ### pause()
649
+
650
+ ```typescript
651
+ pause() => Promise<void>
652
+ ```
653
+
654
+ Pause playback of the current track.
655
+
656
+ --------------------
657
+
658
+
659
+ ### skipForward()
660
+
661
+ ```typescript
662
+ skipForward() => Promise<void>
663
+ ```
664
+
665
+ Skip to the next track. At the end of the playlist, wraps to the beginning when loop is enabled.
666
+
667
+ --------------------
668
+
669
+
670
+ ### skipBack()
671
+
672
+ ```typescript
673
+ skipBack() => Promise<void>
674
+ ```
675
+
676
+ Skip to the previous track. No-op when already at the first track.
677
+
678
+ --------------------
679
+
680
+
681
+ ### seekTo(...)
682
+
683
+ ```typescript
684
+ seekTo(options: SeekToOptions) => Promise<void>
685
+ ```
686
+
687
+ Seek to a position (seconds) in the currently playing track.
688
+ If the position exceeds track length, playback advances to the next track.
689
+
690
+ | Param | Type |
691
+ | ------------- | ------------------------------------------------------- |
692
+ | **`options`** | <code><a href="#seektooptions">SeekToOptions</a></code> |
693
+
694
+ --------------------
695
+
696
+
697
+ ### playTrackByIndex(...)
698
+
699
+ ```typescript
700
+ playTrackByIndex(options: PlayByIndexOptions) => Promise<void>
701
+ ```
702
+
703
+ Jump to the track at the given 0-based index and start playback.
704
+
705
+ | Param | Type |
706
+ | ------------- | ----------------------------------------------------------------- |
707
+ | **`options`** | <code><a href="#playbyindexoptions">PlayByIndexOptions</a></code> |
708
+
709
+ --------------------
710
+
711
+
712
+ ### playTrackById(...)
713
+
714
+ ```typescript
715
+ playTrackById(options: PlayByIdOptions) => Promise<void>
716
+ ```
717
+
718
+ Jump to the track with the given id and start playback.
719
+
720
+ | Param | Type |
721
+ | ------------- | ----------------------------------------------------------- |
722
+ | **`options`** | <code><a href="#playbyidoptions">PlayByIdOptions</a></code> |
723
+
724
+ --------------------
725
+
726
+
727
+ ### selectTrackByIndex(...)
728
+
729
+ ```typescript
730
+ selectTrackByIndex(options: SelectByIndexOptions) => Promise<void>
731
+ ```
732
+
733
+ Select the track at the given index without necessarily starting playback.
734
+
735
+ | Param | Type |
736
+ | ------------- | --------------------------------------------------------------------- |
737
+ | **`options`** | <code><a href="#selectbyindexoptions">SelectByIndexOptions</a></code> |
738
+
739
+ --------------------
740
+
741
+
742
+ ### selectTrackById(...)
743
+
744
+ ```typescript
745
+ selectTrackById(options: SelectByIdOptions) => Promise<void>
746
+ ```
747
+
748
+ Select the track with the given id without necessarily starting playback.
749
+
750
+ | Param | Type |
751
+ | ------------- | --------------------------------------------------------------- |
752
+ | **`options`** | <code><a href="#selectbyidoptions">SelectByIdOptions</a></code> |
753
+
754
+ --------------------
755
+
756
+
757
+ ### setPlaybackVolume(...)
758
+
759
+ ```typescript
760
+ setPlaybackVolume(options: SetPlaybackVolumeOptions) => Promise<void>
761
+ ```
762
+
763
+ Set media stream volume. Float in range [0, 1].
764
+ Hardware volume controls still apply on top of this value.
765
+
766
+ | Param | Type |
767
+ | ------------- | ----------------------------------------------------------------------------- |
768
+ | **`options`** | <code><a href="#setplaybackvolumeoptions">SetPlaybackVolumeOptions</a></code> |
769
+
770
+ --------------------
771
+
772
+
773
+ ### setLoop(...)
774
+
775
+ ```typescript
776
+ setLoop(options: SetLoopOptions) => Promise<void>
777
+ ```
778
+
779
+ When true, the playlist loops back to the first track after the last track completes.
780
+
781
+ | Param | Type |
782
+ | ------------- | --------------------------------------------------------- |
783
+ | **`options`** | <code><a href="#setloopoptions">SetLoopOptions</a></code> |
784
+
785
+ --------------------
786
+
787
+
788
+ ### setPlaybackRate(...)
789
+
790
+ ```typescript
791
+ setPlaybackRate(options: SetPlaybackRateOptions) => Promise<void>
792
+ ```
793
+
794
+ Set playback speed. Float value; 0 pauses, 1 is normal speed.
795
+
796
+ | Param | Type |
797
+ | ------------- | ------------------------------------------------------------------------- |
798
+ | **`options`** | <code><a href="#setplaybackrateoptions">SetPlaybackRateOptions</a></code> |
799
+
800
+ --------------------
801
+
802
+
803
+ ### prepareForVideoHandoff()
804
+
805
+ ```typescript
806
+ prepareForVideoHandoff() => Promise<void>
807
+ ```
808
+
809
+ Release native audio session / focus so a video player can own playback.
810
+
811
+ **Android:** pauses current track, abandons audio focus, stores head position. Does not stop the foreground media service.
812
+ **iOS:** pauses, captures head position, deactivates `AVAudioSession` with `notifyOthersOnDeactivation`.
813
+ **Web:** pauses HTMLAudioElement and stores `currentTime`.
814
+
815
+ Call immediately before native video starts (e.g. your video plugin's init method).
816
+
817
+ --------------------
818
+
819
+
820
+ ### resumeAfterVideoHandoff(...)
821
+
822
+ ```typescript
823
+ resumeAfterVideoHandoff(options: ResumeAfterVideoHandoffOptions) => Promise<ResumeAfterVideoHandoffResult>
824
+ ```
825
+
826
+ Re-arm native audio after video ends or, on Android, prewarm the media service before video starts.
827
+
828
+ **Without `prewarm` (typical exit path):**
829
+ - Android: when `play` is true (default), re-acquires focus and resumes at `position`. When `resumed` is `true`, JS should skip redundant `seekTo`/`play`. When `play` is false, clears handoff retain and returns `{ resumed: false }` so JS can seek without playing.
830
+ - iOS: restores pinned track, reactivates `AVAudioSession`, seeks to `position`, and when `play` is true starts playback (seek-then-play). Returns `{ resumed: true }` when native handled the handoff.
831
+ - Web: stores position only (no native session); returns `{ resumed: false }`.
832
+
833
+ **With `prewarm: true` (Android, before video):** starts `MediaService` in foreground at `position` but stays silent — no audio focus, no audible playback. Always returns `{ resumed: false }`.
834
+
835
+ | Param | Type |
836
+ | ------------- | ----------------------------------------------------------------------------------------- |
837
+ | **`options`** | <code><a href="#resumeaftervideohandoffoptions">ResumeAfterVideoHandoffOptions</a></code> |
838
+
839
+ **Returns:** <code>Promise&lt;<a href="#resumeaftervideohandoffresult">ResumeAfterVideoHandoffResult</a>&gt;</code>
840
+
841
+ --------------------
842
+
843
+
844
+ ### getLastKnownPosition()
845
+
846
+ ```typescript
847
+ getLastKnownPosition() => Promise<GetLastKnownPositionResult>
848
+ ```
849
+
850
+ Return the audio head position (seconds) captured during the most recent `prepareForVideoHandoff`
851
+ or passed to `resumeAfterVideoHandoff`.
852
+
853
+ **Returns:** <code>Promise&lt;<a href="#getlastknownpositionresult">GetLastKnownPositionResult</a>&gt;</code>
854
+
855
+ --------------------
856
+
857
+
858
+ ### Interfaces
859
+
860
+
861
+ #### PluginListenerHandle
862
+
863
+ | Prop | Type |
864
+ | ------------ | ----------------------------------------- |
865
+ | **`remove`** | <code>() =&gt; Promise&lt;void&gt;</code> |
866
+
867
+
868
+ #### PlaylistStatusChangeCallbackArg
869
+
870
+ | Prop | Type |
871
+ | ------------ | --------------------------------------------------------------------- |
872
+ | **`action`** | <code>string</code> |
873
+ | **`status`** | <code><a href="#onstatuscallbackdata">OnStatusCallbackData</a></code> |
874
+
875
+
876
+ #### OnStatusCallbackData
877
+
878
+ Encapsulates the data received by an onStatus callback
879
+
880
+ | Prop | Type | Description |
881
+ | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
882
+ | **`trackId`** | <code>string</code> | The ID of this track. If the track is null or has completed, this value is "NONE" If the playlist is completed, this value is "INVALID" |
883
+ | **`msgType`** | <code><a href="#rmxaudiostatusmessage">RmxAudioStatusMessage</a></code> | The type of status update |
884
+ | **`value`** | <code><a href="#onstatuscallbackupdatedata">OnStatusCallbackUpdateData</a> \| <a href="#onstatustrackchangeddata">OnStatusTrackChangedData</a> \| <a href="#onstatuserrorcallbackdata">OnStatusErrorCallbackData</a></code> | The status payload. For all updates except ERROR, the data package is described by <a href="#onstatuscallbackupdatedata">OnStatusCallbackUpdateData</a>. For Errors, the data is shaped as <a href="#onstatuserrorcallbackdata">OnStatusErrorCallbackData</a> |
885
+
886
+
887
+ #### OnStatusCallbackUpdateData
888
+
889
+ Contains the current track status as of the moment an onStatus update event is emitted.
890
+
891
+ | Prop | Type | Description |
892
+ | --------------------- | ---------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
893
+ | **`trackId`** | <code>string</code> | The ID of this track corresponding to this event. If the track is null or has completed, this value is "NONE". This will happen when skipping to the beginning or end of the playlist. If the playlist is completed, this value is "INVALID" |
894
+ | **`isStream`** | <code>boolean</code> | Boolean indicating whether this is a streaming track. |
895
+ | **`currentIndex`** | <code>number</code> | The current index of the track in the playlist. |
896
+ | **`status`** | <code>'error' \| 'unknown' \| 'ready' \| 'playing' \| 'loading' \| 'paused'</code> | The current status of the track, as a string. This is used to summarize the various event states that a track can be in; e.g. "playing" is true for any number of track statuses. The Javascript interface takes care of this for you; this field is here only for reference. |
897
+ | **`currentPosition`** | <code>number</code> | Current playback position of the reported track. |
898
+ | **`duration`** | <code>number</code> | The known duration of the reported track. For streams or malformed MP3's, this value will be 0. |
899
+ | **`playbackPercent`** | <code>number</code> | Progress of track playback, as a percent, in the range 0 - 100 |
900
+ | **`bufferPercent`** | <code>number</code> | Buffering progress of the track, as a percent, in the range 0 - 100 |
901
+ | **`bufferStart`** | <code>number</code> | The starting position of the buffering progress. For now, this is always reported as 0. |
902
+ | **`bufferEnd`** | <code>number</code> | The maximum position, in seconds, of the track buffer. For now, only the buffer with the maximum playback position is reported, even if there are other segments (due to seeking, for example). Practically speaking you don't need to worry about that, as in both implementations the minor gaps are automatically filled in by the underlying players. |
903
+
904
+
905
+ #### OnStatusTrackChangedData
906
+
907
+ Reports information about the playlist state when a track changes.
908
+ Includes the new track, its index, and the state of the playlist.
909
+
910
+ | Prop | Type | Description |
911
+ | ------------------- | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
912
+ | **`currentItem`** | <code><a href="#audiotrack">AudioTrack</a></code> | The new track that has been selected. May be null if you are at the end of the playlist, or the playlist has been emptied. |
913
+ | **`currentIndex`** | <code>number</code> | The 0-based index of the new track. If the playlist has ended or been cleared, this will be -1. |
914
+ | **`isAtEnd`** | <code>boolean</code> | Indicates whether the playlist is now currently at the last item in the list. |
915
+ | **`isAtBeginning`** | <code>boolean</code> | Indicates whether the playlist is now at the first item in the list |
916
+ | **`hasNext`** | <code>boolean</code> | Indicates if there are additional playlist items after the current item. |
917
+ | **`hasPrevious`** | <code>boolean</code> | Indicates if there are any items before this one in the playlist. |
918
+
919
+
920
+ #### AudioTrack
921
+
922
+ An audio track for playback by the playlist.
923
+
924
+ | Prop | Type | Description |
925
+ | -------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
926
+ | **`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. |
927
+ | **`trackId`** | <code>string</code> | trackId is optional and if not passed in, an auto-generated UUID will be used. |
928
+ | **`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. |
929
+ | **`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. |
930
+ | **`artist`** | <code>string</code> | The track's artist |
931
+ | **`album`** | <code>string</code> | Album the track belongs to |
932
+ | **`title`** | <code>string</code> | Title of the track |
933
+
934
+
935
+ #### OnStatusErrorCallbackData
936
+
937
+ Represents an error reported by the onStatus callback.
938
+
939
+ | Prop | Type | Description |
940
+ | ------------- | --------------------------------------------------------------- | ----------------------- |
941
+ | **`code`** | <code><a href="#rmxaudioerrortype">RmxAudioErrorType</a></code> | Error code |
942
+ | **`message`** | <code>string</code> | The error, as a message |
943
+
944
+
945
+ #### AudioPlayerOptions
946
+
947
+ Options governing the overall behavior of the audio player plugin
948
+
949
+ | Prop | Type | Description |
950
+ | ------------------------ | ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
951
+ | **`verbose`** | <code>boolean</code> | Should the plugin's javascript dump the status message stream to the javascript console? |
952
+ | **`resetStreamOnPause`** | <code>boolean</code> | If true, when pausing a live stream, play will continue from the LIVE POSITION (e.g. the stream jumps forward to the current point in time, rather than picking up where it left off when you paused). If false, the stream will continue where you paused. The drawback of doing this is that when the audio buffer fills, it will jump forward to the current point in time, cause a disjoint in playback. Default is true. |
953
+ | **`options`** | <code><a href="#notificationoptions">NotificationOptions</a></code> | Further options for notifications |
954
+
955
+
956
+ #### NotificationOptions
957
+
958
+ | Prop | Type |
959
+ | ---------- | ------------------- |
960
+ | **`icon`** | <code>string</code> |
961
+
962
+
963
+ #### PlaylistOptions
964
+
965
+ | Prop | Type |
966
+ | ------------- | ----------------------------------------------------------------------------------- |
967
+ | **`items`** | <code><a href="#array">Array</a>&lt;<a href="#audiotrack">AudioTrack</a>&gt;</code> |
968
+ | **`options`** | <code><a href="#playlistitemoptions">PlaylistItemOptions</a></code> |
969
+
970
+
971
+ #### Array
972
+
973
+ | Prop | Type | Description |
974
+ | ------------ | ------------------- | ------------------------------------------------------------------------------------------------------ |
975
+ | **`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. |
976
+
977
+ | Method | Signature | Description |
978
+ | ------------------ | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
979
+ | **toString** | () =&gt; string | Returns a string representation of an array. |
980
+ | **toLocaleString** | () =&gt; string | Returns a string representation of an array. The elements are converted to string using their toLocalString methods. |
981
+ | **pop** | () =&gt; 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. |
982
+ | **push** | (...items: T[]) =&gt; number | Appends new elements to the end of an array, and returns the new length of the array. |
983
+ | **concat** | (...items: <a href="#concatarray">ConcatArray</a>&lt;T&gt;[]) =&gt; T[] | Combines two or more arrays. This method returns a new array without modifying any existing arrays. |
984
+ | **concat** | (...items: (T \| <a href="#concatarray">ConcatArray</a>&lt;T&gt;)[]) =&gt; T[] | Combines two or more arrays. This method returns a new array without modifying any existing arrays. |
985
+ | **join** | (separator?: string \| undefined) =&gt; string | Adds all the elements of an array into a string, separated by the specified separator string. |
986
+ | **reverse** | () =&gt; T[] | Reverses the elements in an array in place. This method mutates the array and returns a reference to the same array. |
987
+ | **shift** | () =&gt; 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. |
988
+ | **slice** | (start?: number \| undefined, end?: number \| undefined) =&gt; 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. |
989
+ | **sort** | (compareFn?: ((a: T, b: T) =&gt; number) \| undefined) =&gt; this | Sorts an array in place. This method mutates the array and returns a reference to the same array. |
990
+ | **splice** | (start: number, deleteCount?: number \| undefined) =&gt; T[] | Removes elements from an array and, if necessary, inserts new elements in their place, returning the deleted elements. |
991
+ | **splice** | (start: number, deleteCount: number, ...items: T[]) =&gt; T[] | Removes elements from an array and, if necessary, inserts new elements in their place, returning the deleted elements. |
992
+ | **unshift** | (...items: T[]) =&gt; number | Inserts new elements at the start of an array, and returns the new length of the array. |
993
+ | **indexOf** | (searchElement: T, fromIndex?: number \| undefined) =&gt; number | Returns the index of the first occurrence of a value in an array, or -1 if it is not present. |
994
+ | **lastIndexOf** | (searchElement: T, fromIndex?: number \| undefined) =&gt; number | Returns the index of the last occurrence of a specified value in an array, or -1 if it is not present. |
995
+ | **every** | &lt;S extends T&gt;(predicate: (value: T, index: number, array: T[]) =&gt; value is S, thisArg?: any) =&gt; this is S[] | Determines whether all the members of an array satisfy the specified test. |
996
+ | **every** | (predicate: (value: T, index: number, array: T[]) =&gt; unknown, thisArg?: any) =&gt; boolean | Determines whether all the members of an array satisfy the specified test. |
997
+ | **some** | (predicate: (value: T, index: number, array: T[]) =&gt; unknown, thisArg?: any) =&gt; boolean | Determines whether the specified callback function returns true for any element of an array. |
998
+ | **forEach** | (callbackfn: (value: T, index: number, array: T[]) =&gt; void, thisArg?: any) =&gt; void | Performs the specified action for each element in an array. |
999
+ | **map** | &lt;U&gt;(callbackfn: (value: T, index: number, array: T[]) =&gt; U, thisArg?: any) =&gt; U[] | Calls a defined callback function on each element of an array, and returns an array that contains the results. |
1000
+ | **filter** | &lt;S extends T&gt;(predicate: (value: T, index: number, array: T[]) =&gt; value is S, thisArg?: any) =&gt; S[] | Returns the elements of an array that meet the condition specified in a callback function. |
1001
+ | **filter** | (predicate: (value: T, index: number, array: T[]) =&gt; unknown, thisArg?: any) =&gt; T[] | Returns the elements of an array that meet the condition specified in a callback function. |
1002
+ | **reduce** | (callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) =&gt; T) =&gt; 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. |
1003
+ | **reduce** | (callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) =&gt; T, initialValue: T) =&gt; T | |
1004
+ | **reduce** | &lt;U&gt;(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) =&gt; U, initialValue: U) =&gt; 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. |
1005
+ | **reduceRight** | (callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) =&gt; T) =&gt; 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. |
1006
+ | **reduceRight** | (callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) =&gt; T, initialValue: T) =&gt; T | |
1007
+ | **reduceRight** | &lt;U&gt;(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) =&gt; U, initialValue: U) =&gt; 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. |
1008
+
1009
+
1010
+ #### ConcatArray
1011
+
1012
+ | Prop | Type |
1013
+ | ------------ | ------------------- |
1014
+ | **`length`** | <code>number</code> |
1015
+
1016
+ | Method | Signature |
1017
+ | --------- | ------------------------------------------------------------------ |
1018
+ | **join** | (separator?: string \| undefined) =&gt; string |
1019
+ | **slice** | (start?: number \| undefined, end?: number \| undefined) =&gt; T[] |
1020
+
1021
+
1022
+ #### PlaylistItemOptions
1023
+
1024
+ Options governing how the items are managed when using setPlaylistItems
1025
+ to update the playlist. This is typically useful if you are retaining items
1026
+ that were in the previous list.
1027
+
1028
+ | Prop | Type | Description |
1029
+ | ---------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
1030
+ | **`retainPosition`** | <code>boolean</code> | If true, the plugin will continue playback from the current playback position after setting the items to the playlist. |
1031
+ | **`playFromPosition`** | <code>number</code> | If retainPosition is true, this value will tell the plugin the exact time to start from, rather than letting the plugin decide based on current playback. |
1032
+ | **`playFromId`** | <code>string</code> | If retainPosition is true, this value will tell the plugin the uid of the "current" item to start from, rather than letting the plugin decide based on current playback. |
1033
+ | **`startPaused`** | <code>boolean</code> | If playback should immediately begin when calling setPlaylistItems on the plugin. Default is false; |
1034
+
1035
+
1036
+ #### AddItemOptions
1037
+
1038
+ | Prop | Type |
1039
+ | ---------- | ------------------------------------------------- |
1040
+ | **`item`** | <code><a href="#audiotrack">AudioTrack</a></code> |
1041
+
1042
+
1043
+ #### AddAllItemOptions
1044
+
1045
+ | Prop | Type |
1046
+ | ----------- | ----------------------------------------------------------------------------------- |
1047
+ | **`items`** | <code><a href="#array">Array</a>&lt;<a href="#audiotrack">AudioTrack</a>&gt;</code> |
1048
+
1049
+
1050
+ #### RemoveItemOptions
1051
+
1052
+ | Prop | Type |
1053
+ | ----------- | ------------------- |
1054
+ | **`id`** | <code>string</code> |
1055
+ | **`index`** | <code>number</code> |
1056
+
1057
+
1058
+ #### RemoveItemsOptions
1059
+
1060
+ | Prop | Type |
1061
+ | ----------- | ------------------------------------------------------------------------------------------------- |
1062
+ | **`items`** | <code><a href="#array">Array</a>&lt;<a href="#removeitemoptions">RemoveItemOptions</a>&gt;</code> |
1063
+
1064
+
1065
+ #### GetPlaylistResult
1066
+
1067
+ | Prop | Type |
1068
+ | ----------- | ----------------------------------------------------------------------------------- |
1069
+ | **`items`** | <code><a href="#array">Array</a>&lt;<a href="#audiotrack">AudioTrack</a>&gt;</code> |
1070
+
1071
+
1072
+ #### SeekToOptions
1073
+
1074
+ | Prop | Type |
1075
+ | -------------- | ------------------- |
1076
+ | **`position`** | <code>number</code> |
1077
+
1078
+
1079
+ #### PlayByIndexOptions
1080
+
1081
+ | Prop | Type |
1082
+ | -------------- | ------------------- |
1083
+ | **`index`** | <code>number</code> |
1084
+ | **`position`** | <code>number</code> |
1085
+
1086
+
1087
+ #### PlayByIdOptions
1088
+
1089
+ | Prop | Type |
1090
+ | -------------- | ------------------- |
1091
+ | **`id`** | <code>string</code> |
1092
+ | **`position`** | <code>number</code> |
1093
+
1094
+
1095
+ #### SelectByIndexOptions
1096
+
1097
+ | Prop | Type |
1098
+ | -------------- | ------------------- |
1099
+ | **`index`** | <code>number</code> |
1100
+ | **`position`** | <code>number</code> |
1101
+
1102
+
1103
+ #### SelectByIdOptions
1104
+
1105
+ | Prop | Type |
1106
+ | -------------- | ------------------- |
1107
+ | **`id`** | <code>string</code> |
1108
+ | **`position`** | <code>number</code> |
1109
+
1110
+
1111
+ #### SetPlaybackVolumeOptions
1112
+
1113
+ | Prop | Type |
1114
+ | ------------ | ------------------- |
1115
+ | **`volume`** | <code>number</code> |
1116
+
1117
+
1118
+ #### SetLoopOptions
1119
+
1120
+ | Prop | Type |
1121
+ | ---------- | -------------------- |
1122
+ | **`loop`** | <code>boolean</code> |
1123
+
1124
+
1125
+ #### SetPlaybackRateOptions
1126
+
1127
+ | Prop | Type |
1128
+ | ---------- | ------------------- |
1129
+ | **`rate`** | <code>number</code> |
1130
+
1131
+
1132
+ #### ResumeAfterVideoHandoffResult
1133
+
1134
+ | Prop | Type | Description |
1135
+ | ------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1136
+ | **`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, and Android last-resort `beginPlayback`. |
1137
+
1138
+
1139
+ #### ResumeAfterVideoHandoffOptions
1140
+
1141
+ | Prop | Type | Description |
1142
+ | -------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1143
+ | **`position`** | <code>number</code> | Resume position in seconds (video exit head or saved audio position). |
1144
+ | **`prewarm`** | <code>boolean</code> | **Android only.** When `true`, promote `MediaService` to foreground and prepare at `position` without requesting audio focus or playing audio. Use immediately after `prepareForVideoHandoff` and before native video starts, while the app is still foregrounded. Ignored on iOS (no-op). Not applicable on web. |
1145
+ | **`play`** | <code>boolean</code> | When `true`, native starts audible playback after seeking to `position`. When `false` (paused video exit), native must not start playback. iOS defaults to `false` when omitted; Android defaults to `true` for legacy callers. |
1146
+
1147
+
1148
+ #### GetLastKnownPositionResult
1149
+
1150
+ | Prop | Type |
1151
+ | -------------- | ------------------- |
1152
+ | **`position`** | <code>number</code> |
1153
+
1154
+
1155
+ ### Type Aliases
1156
+
1157
+
1158
+ #### PlaylistStatusChangeCallback
1159
+
1160
+ <code>(data: <a href="#playliststatuschangecallbackarg">PlaylistStatusChangeCallbackArg</a>): void</code>
1161
+
1162
+
1163
+ ### Enums
1164
+
1165
+
1166
+ #### RmxAudioStatusMessage
1167
+
1168
+ | Members | Value | Description |
1169
+ | ---------------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1170
+ | **`RMXSTATUS_NONE`** | <code>0</code> | The starting state of the plugin. You will never see this value; it changes before the callbacks are even registered to report changes to this value. |
1171
+ | **`RMXSTATUS_REGISTER`** | <code>1</code> | Raised when the plugin registers the callback handler for onStatus callbacks. You will probably not be able to see this (nor do you need to). |
1172
+ | **`RMXSTATUS_INIT`** | <code>2</code> | Reserved for future use |
1173
+ | **`RMXSTATUS_ERROR`** | <code>5</code> | Indicates an error is reported in the 'value' field. |
1174
+ | **`RMXSTATUS_LOADING`** | <code>10</code> | The reported track is being loaded by the player |
1175
+ | **`RMXSTATUS_CANPLAY`** | <code>11</code> | The reported track is able to begin playback |
1176
+ | **`RMXSTATUS_LOADED`** | <code>15</code> | The reported track has loaded 100% of the file (either from disc or network) |
1177
+ | **`RMXSTATUS_STALLED`** | <code>20</code> | (iOS only): Playback has stalled due to insufficient network |
1178
+ | **`RMXSTATUS_BUFFERING`** | <code>25</code> | Reports an update in the reported track's buffering status |
1179
+ | **`RMXSTATUS_PLAYING`** | <code>30</code> | The reported track has started (or resumed) playing |
1180
+ | **`RMXSTATUS_PAUSE`** | <code>35</code> | The reported track has been paused, either by the user or by the system. (iOS only): This value is raised when MP3's are malformed (but still playable). These require the user to explicitly press play again. This can be worked around and is on the TODO list. |
1181
+ | **`RMXSTATUS_PLAYBACK_POSITION`** | <code>40</code> | Reports a change in the reported track's playback position. |
1182
+ | **`RMXSTATUS_SEEK`** | <code>45</code> | The reported track has seeked. On Android, only the plugin consumer can generate this (Notification controls on Android do not include a seek bar). On iOS, the Command Center includes a seek bar so this will be reported when the user has seeked via Command Center. |
1183
+ | **`RMXSTATUS_COMPLETED`** | <code>50</code> | The reported track has completed playback. |
1184
+ | **`RMXSTATUS_DURATION`** | <code>55</code> | The reported track's duration has changed. This is raised once, when duration is updated for the first time. For streams, this value is never reported. |
1185
+ | **`RMXSTATUS_STOPPED`** | <code>60</code> | All playback has stopped, probably because the plugin is shutting down. |
1186
+ | **`RMX_STATUS_SKIP_FORWARD`** | <code>90</code> | The playlist has skipped forward to the next track. On both Android and iOS, this will be raised if the notification controls/Command Center were used to skip. It is unlikely you need to consume this event: RMXSTATUS_TRACK_CHANGED is also reported when this occurs, so you can generalize your track change handling in one place. |
1187
+ | **`RMX_STATUS_SKIP_BACK`** | <code>95</code> | The playlist has skipped back to the previous track. On both Android and iOS, this will be raised if the notification controls/Command Center were used to skip. It is unlikely you need to consume this event: RMXSTATUS_TRACK_CHANGED is also reported when this occurs, so you can generalize your track change handling in one place. |
1188
+ | **`RMXSTATUS_TRACK_CHANGED`** | <code>100</code> | Reported when the current track has changed in the native player. This event contains full data about the new track, including the index and the actual track itself. The type of the 'value' field in this case is <a href="#onstatustrackchangeddata">OnStatusTrackChangedData</a>. |
1189
+ | **`RMXSTATUS_PLAYLIST_COMPLETED`** | <code>105</code> | The entire playlist has completed playback. After this event has been raised, the current item is set to null and the current index to -1. |
1190
+ | **`RMXSTATUS_ITEM_ADDED`** | <code>110</code> | An item has been added to the playlist. For the setPlaylistItems and addAllItems methods, this status is raised once for every track in the collection. |
1191
+ | **`RMXSTATUS_ITEM_REMOVED`** | <code>115</code> | An item has been removed from the playlist. For the removeItems and clearAllItems methods, this status is raised once for every track that was removed. |
1192
+ | **`RMXSTATUS_PLAYLIST_CLEARED`** | <code>120</code> | All items have been removed from the playlist |
1193
+ | **`RMXSTATUS_VIEWDISAPPEAR`** | <code>200</code> | Just for testing.. you don't need this and in fact can never receive it, the plugin is destroyed before it can be raised. |
1194
+
1195
+
1196
+ #### RmxAudioErrorType
1197
+
1198
+ | Members | Value |
1199
+ | --------------------------- | -------------- |
1200
+ | **`RMXERR_NONE_ACTIVE`** | <code>0</code> |
1201
+ | **`RMXERR_ABORTED`** | <code>1</code> |
1202
+ | **`RMXERR_NETWORK`** | <code>2</code> |
1203
+ | **`RMXERR_DECODE`** | <code>3</code> |
1204
+ | **`RMXERR_NONE_SUPPORTED`** | <code>4</code> |
1205
+
1206
+ </docgen-api>
1207
+
1208
+ ## Migrating from cordova-plugin-playlist
1209
+
1210
+ Use the shipped `RmxAudioPlayer` class — in the best case you only change your import:
1211
+
1212
+ ```typescript
1213
+ // before
1214
+ import { RmxAudioPlayer } from 'cordova-plugin-playlist';
1215
+
1216
+ // after
1217
+ import { RmxAudioPlayer } from 'capacitor-plugin-playlist';
1218
+ ```
1219
+
1220
+ For new code or video handoff, prefer the `Playlist` plugin object directly.
1221
+
1222
+ ## Changes
1223
+
1224
+ See [CHANGELOG.md](./CHANGELOG.md) for version history.
1225
+
1226
+ ## Credits
1227
+
1228
+ Inspired by:
215
1229
 
216
1230
  - [cordova-plugin-playlist](https://github.com/Rolamix/cordova-plugin-playlist)
217
1231
  - [cordova-plugin-media](https://github.com/apache/cordova-plugin-media)
218
1232
  - [ExoMedia](https://github.com/brianwernick/ExoMedia)
219
- - [PlaylistCore](https://github.com/brianwernick/PlaylistCore) (provides player controls on top of ExoMedia)
220
- - [Bi-Directional AVQueuePlayer proof of concept](https://github.com/jrtaal/AVBidirectionalQueuePlayer)
1233
+ - [PlaylistCore](https://github.com/brianwernick/PlaylistCore)
1234
+ - [Bi-Directional AVQueuePlayer](https://github.com/jrtaal/AVBidirectionalQueuePlayer)
221
1235
  - [cordova-music-controls-plugin](https://github.com/homerours/cordova-music-controls-plugin)
222
1236
 
223
- ## 7. License
1237
+ ## License
224
1238
 
225
1239
  [The MIT License (MIT)](http://www.opensource.org/licenses/mit-license.html)
226
-
227
- Permission is hereby granted, free of charge, to any person obtaining a copy
228
- of this software and associated documentation files (the "Software"), to deal
229
- in the Software without restriction, including without limitation the rights
230
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
231
- copies of the Software, and to permit persons to whom the Software is
232
- furnished to do so, subject to the following conditions:
233
-
234
- The above copyright notice and this permission notice shall be included in
235
- all copies or substantial portions of the Software.
236
-
237
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
238
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
239
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
240
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
241
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
242
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
243
- THE SOFTWARE.