@capgo/native-audio 8.3.18 → 8.4.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 +63 -13
- package/android/build.gradle +1 -1
- package/android/src/main/java/ee/forgr/audio/NativeAudio.java +310 -89
- package/dist/docs.json +114 -0
- package/dist/esm/definitions.d.ts +36 -0
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/NativeAudioPlugin/Plugin.swift +193 -15
- package/ios/Sources/NativeAudioPlugin/RemoteAudioAsset.swift +6 -3
- package/package.json +14 -8
package/dist/docs.json
CHANGED
|
@@ -613,6 +613,35 @@
|
|
|
613
613
|
],
|
|
614
614
|
"slug": "addlistenercurrenttime-"
|
|
615
615
|
},
|
|
616
|
+
{
|
|
617
|
+
"name": "addListener",
|
|
618
|
+
"signature": "(eventName: 'playbackState', listenerFunc: PlaybackStateListener) => Promise<PluginListenerHandle>",
|
|
619
|
+
"parameters": [
|
|
620
|
+
{
|
|
621
|
+
"name": "eventName",
|
|
622
|
+
"docs": "",
|
|
623
|
+
"type": "'playbackState'"
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
"name": "listenerFunc",
|
|
627
|
+
"docs": "",
|
|
628
|
+
"type": "PlaybackStateListener"
|
|
629
|
+
}
|
|
630
|
+
],
|
|
631
|
+
"returns": "Promise<PluginListenerHandle>",
|
|
632
|
+
"tags": [
|
|
633
|
+
{
|
|
634
|
+
"name": "since",
|
|
635
|
+
"text": "8.3.15\nreturn {@link PlaybackStateEvent}"
|
|
636
|
+
}
|
|
637
|
+
],
|
|
638
|
+
"docs": "Listen for playback state changes, including notification and lock-screen transport controls.\nEmitted by Android and iOS. The current Web implementation does not emit this event.",
|
|
639
|
+
"complexTypes": [
|
|
640
|
+
"PluginListenerHandle",
|
|
641
|
+
"PlaybackStateListener"
|
|
642
|
+
],
|
|
643
|
+
"slug": "addlistenerplaybackstate-"
|
|
644
|
+
},
|
|
616
645
|
{
|
|
617
646
|
"name": "clearCache",
|
|
618
647
|
"signature": "() => Promise<void>",
|
|
@@ -1307,6 +1336,59 @@
|
|
|
1307
1336
|
"type": "string"
|
|
1308
1337
|
}
|
|
1309
1338
|
]
|
|
1339
|
+
},
|
|
1340
|
+
{
|
|
1341
|
+
"name": "PlaybackStateEvent",
|
|
1342
|
+
"slug": "playbackstateevent",
|
|
1343
|
+
"docs": "",
|
|
1344
|
+
"tags": [],
|
|
1345
|
+
"methods": [],
|
|
1346
|
+
"properties": [
|
|
1347
|
+
{
|
|
1348
|
+
"name": "assetId",
|
|
1349
|
+
"tags": [],
|
|
1350
|
+
"docs": "Asset Id of the audio",
|
|
1351
|
+
"complexTypes": [],
|
|
1352
|
+
"type": "string"
|
|
1353
|
+
},
|
|
1354
|
+
{
|
|
1355
|
+
"name": "state",
|
|
1356
|
+
"tags": [],
|
|
1357
|
+
"docs": "Resolved playback state after a local or remote transport action.",
|
|
1358
|
+
"complexTypes": [
|
|
1359
|
+
"PlaybackStateValue"
|
|
1360
|
+
],
|
|
1361
|
+
"type": "PlaybackStateValue"
|
|
1362
|
+
},
|
|
1363
|
+
{
|
|
1364
|
+
"name": "reason",
|
|
1365
|
+
"tags": [],
|
|
1366
|
+
"docs": "Reason for the state change, for example `play`, `pause`, `remotePlay`, or `complete`.",
|
|
1367
|
+
"complexTypes": [],
|
|
1368
|
+
"type": "string"
|
|
1369
|
+
},
|
|
1370
|
+
{
|
|
1371
|
+
"name": "isPlaying",
|
|
1372
|
+
"tags": [],
|
|
1373
|
+
"docs": "Whether the asset is currently playing.",
|
|
1374
|
+
"complexTypes": [],
|
|
1375
|
+
"type": "boolean"
|
|
1376
|
+
},
|
|
1377
|
+
{
|
|
1378
|
+
"name": "currentTime",
|
|
1379
|
+
"tags": [],
|
|
1380
|
+
"docs": "Current playback position in seconds when available.",
|
|
1381
|
+
"complexTypes": [],
|
|
1382
|
+
"type": "number | undefined"
|
|
1383
|
+
},
|
|
1384
|
+
{
|
|
1385
|
+
"name": "duration",
|
|
1386
|
+
"tags": [],
|
|
1387
|
+
"docs": "Total playback duration in seconds when available.",
|
|
1388
|
+
"complexTypes": [],
|
|
1389
|
+
"type": "number | undefined"
|
|
1390
|
+
}
|
|
1391
|
+
]
|
|
1310
1392
|
}
|
|
1311
1393
|
],
|
|
1312
1394
|
"enums": [],
|
|
@@ -1350,6 +1432,38 @@
|
|
|
1350
1432
|
]
|
|
1351
1433
|
}
|
|
1352
1434
|
]
|
|
1435
|
+
},
|
|
1436
|
+
{
|
|
1437
|
+
"name": "PlaybackStateListener",
|
|
1438
|
+
"slug": "playbackstatelistener",
|
|
1439
|
+
"docs": "",
|
|
1440
|
+
"types": [
|
|
1441
|
+
{
|
|
1442
|
+
"text": "(state: PlaybackStateEvent): void",
|
|
1443
|
+
"complexTypes": [
|
|
1444
|
+
"PlaybackStateEvent"
|
|
1445
|
+
]
|
|
1446
|
+
}
|
|
1447
|
+
]
|
|
1448
|
+
},
|
|
1449
|
+
{
|
|
1450
|
+
"name": "PlaybackStateValue",
|
|
1451
|
+
"slug": "playbackstatevalue",
|
|
1452
|
+
"docs": "",
|
|
1453
|
+
"types": [
|
|
1454
|
+
{
|
|
1455
|
+
"text": "'playing'",
|
|
1456
|
+
"complexTypes": []
|
|
1457
|
+
},
|
|
1458
|
+
{
|
|
1459
|
+
"text": "'paused'",
|
|
1460
|
+
"complexTypes": []
|
|
1461
|
+
},
|
|
1462
|
+
{
|
|
1463
|
+
"text": "'stopped'",
|
|
1464
|
+
"complexTypes": []
|
|
1465
|
+
}
|
|
1466
|
+
]
|
|
1353
1467
|
}
|
|
1354
1468
|
],
|
|
1355
1469
|
"pluginConfigs": []
|
|
@@ -346,6 +346,34 @@ export interface CurrentTimeEvent {
|
|
|
346
346
|
assetId: string;
|
|
347
347
|
}
|
|
348
348
|
export type CurrentTimeListener = (state: CurrentTimeEvent) => void;
|
|
349
|
+
export type PlaybackStateValue = 'playing' | 'paused' | 'stopped';
|
|
350
|
+
export interface PlaybackStateEvent {
|
|
351
|
+
/**
|
|
352
|
+
* Asset Id of the audio
|
|
353
|
+
*/
|
|
354
|
+
assetId: string;
|
|
355
|
+
/**
|
|
356
|
+
* Resolved playback state after a local or remote transport action.
|
|
357
|
+
*/
|
|
358
|
+
state: PlaybackStateValue;
|
|
359
|
+
/**
|
|
360
|
+
* Reason for the state change, for example `play`, `pause`, `remotePlay`, or `complete`.
|
|
361
|
+
*/
|
|
362
|
+
reason: string;
|
|
363
|
+
/**
|
|
364
|
+
* Whether the asset is currently playing.
|
|
365
|
+
*/
|
|
366
|
+
isPlaying: boolean;
|
|
367
|
+
/**
|
|
368
|
+
* Current playback position in seconds when available.
|
|
369
|
+
*/
|
|
370
|
+
currentTime?: number;
|
|
371
|
+
/**
|
|
372
|
+
* Total playback duration in seconds when available.
|
|
373
|
+
*/
|
|
374
|
+
duration?: number;
|
|
375
|
+
}
|
|
376
|
+
export type PlaybackStateListener = (state: PlaybackStateEvent) => void;
|
|
349
377
|
export interface NativeAudio {
|
|
350
378
|
/**
|
|
351
379
|
* Configure the audio player
|
|
@@ -526,6 +554,14 @@ export interface NativeAudio {
|
|
|
526
554
|
* return {@link CurrentTimeEvent}
|
|
527
555
|
*/
|
|
528
556
|
addListener(eventName: 'currentTime', listenerFunc: CurrentTimeListener): Promise<PluginListenerHandle>;
|
|
557
|
+
/**
|
|
558
|
+
* Listen for playback state changes, including notification and lock-screen transport controls.
|
|
559
|
+
* Emitted by Android and iOS. The current Web implementation does not emit this event.
|
|
560
|
+
*
|
|
561
|
+
* @since 8.3.15
|
|
562
|
+
* return {@link PlaybackStateEvent}
|
|
563
|
+
*/
|
|
564
|
+
addListener(eventName: 'playbackState', listenerFunc: PlaybackStateListener): Promise<PluginListenerHandle>;
|
|
529
565
|
/**
|
|
530
566
|
* Clear the audio cache for remote audio files
|
|
531
567
|
* @since 6.5.0
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["import type { PluginListenerHandle } from '@capacitor/core';\n\nexport interface CompletedEvent {\n /**\n * Emit when a play completes\n *\n * @since 5.0.0\n */\n assetId: string;\n}\n\nexport type CompletedListener = (state: CompletedEvent) => void;\n\nexport interface Assets {\n /**\n * Asset Id, unique identifier of the file\n */\n assetId: string;\n}\n\nexport interface AssetVolume {\n /**\n * Asset Id, unique identifier of the file\n */\n assetId: string;\n /**\n * Volume of the audio, between 0.1 and 1.0\n */\n volume: number;\n /**\n * Time over which to fade to the target volume, in seconds. Default is 0s (immediate).\n */\n duration?: number;\n}\n\nexport interface AssetRate {\n /**\n * Asset Id, unique identifier of the file\n */\n assetId: string;\n /**\n * Rate of the audio, between 0.1 and 1.0\n */\n rate: number;\n}\n\nexport interface AssetSetTime {\n /**\n * Asset Id, unique identifier of the file\n */\n assetId: string;\n /**\n * Time to set the audio, in seconds\n */\n time: number;\n}\n\nexport interface AssetPlayOptions {\n /**\n * Asset Id, unique identifier of the file\n */\n assetId: string;\n /**\n * Time to start playing the audio, in seconds\n */\n time?: number;\n /**\n * Delay to start playing the audio, in seconds\n */\n delay?: number;\n\n /**\n * Volume of the audio, between 0.1 and 1.0\n */\n volume?: number;\n\n /**\n * Whether to fade in the audio\n */\n fadeIn?: boolean;\n\n /**\n * Whether to fade out the audio\n */\n fadeOut?: boolean;\n\n /**\n * Fade in duration in seconds.\n * Only used if fadeIn is true.\n * Default is 1s.\n */\n fadeInDuration?: number;\n\n /**\n * Fade out duration in seconds.\n * Only used if fadeOut is true.\n * Default is 1s.\n */\n fadeOutDuration?: number;\n\n /**\n * Time in seconds from the start of the audio to start fading out.\n * Only used if fadeOut is true.\n * Default is fadeOutDuration before end of audio.\n */\n fadeOutStartTime?: number;\n}\n\nexport interface AssetStopOptions {\n /**\n * Asset Id, unique identifier of the file\n */\n assetId: string;\n\n /**\n * Whether to fade out the audio before stopping\n */\n fadeOut?: boolean;\n\n /**\n * Fade out duration in seconds.\n * Default is 1s.\n */\n fadeOutDuration?: number;\n}\n\nexport interface AssetPauseOptions {\n /**\n * Asset Id, unique identifier of the file\n */\n assetId: string;\n\n /**\n * Whether to fade out the audio before pausing\n */\n fadeOut?: boolean;\n\n /**\n * Fade out duration in seconds.\n * Default is 1s.\n */\n fadeOutDuration?: number;\n}\n\nexport interface AssetResumeOptions {\n /**\n * Asset Id, unique identifier of the file\n */\n assetId: string;\n\n /**\n * Whether to fade in the audio during resume\n */\n fadeIn?: boolean;\n\n /**\n * Fade in duration in seconds.\n * Default is 1s.\n */\n fadeInDuration?: number;\n}\n\nexport interface ConfigureOptions {\n /**\n * focus the audio with Audio Focus\n */\n focus?: boolean;\n /**\n * Play the audio in the background\n */\n background?: boolean;\n /**\n * Ignore silent mode, works only on iOS setting this will nuke other audio apps\n */\n ignoreSilent?: boolean;\n /**\n * Show audio playback in the notification center (iOS and Android)\n * When enabled, displays audio metadata (title, artist, album, artwork) in the system notification\n * and Control Center (iOS) or lock screen.\n *\n * **Important iOS Behavior:**\n * Enabling this option changes the audio session category to `.playback` with `.default` mode,\n * which means your app's audio will **interrupt** other apps' audio (like background music from\n * Spotify, Apple Music, etc.) instead of mixing with it. This is required for the Now Playing\n * info to appear in Control Center and on the lock screen.\n *\n * **Trade-offs:**\n * - `showNotification: true` → Shows Now Playing controls, but interrupts other audio\n * - `showNotification: false` → Audio mixes with other apps, but no Now Playing controls\n *\n * Use this when your app is the primary audio source (music players, podcast apps, etc.).\n * Disable this for secondary audio like sound effects or notification sounds where mixing\n * with background music is preferred.\n *\n * @see https://github.com/Cap-go/capacitor-native-audio/issues/202\n */\n showNotification?: boolean;\n /**\n * Enable background audio playback (Android only)\n *\n * When enabled, audio will continue playing when the app is backgrounded or the screen is locked.\n * The plugin will skip the automatic pause/resume logic that normally occurs when the app\n * enters the background or returns to the foreground.\n *\n * **Important Android Requirements:**\n * To use background playback on Android, your app must:\n * 1. Declare the required permissions in `AndroidManifest.xml`:\n * - `<uses-permission android:name=\"android.permission.FOREGROUND_SERVICE\" />`\n * - `<uses-permission android:name=\"android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK\" />`\n * - `<uses-permission android:name=\"android.permission.WAKE_LOCK\" />`\n * 2. Start a Foreground Service with a media-style notification before backgrounding\n * (the plugin does not automatically create or manage the foreground service)\n * 3. Use `showNotification: true` to display playback controls in the notification\n *\n * **Usage Example:**\n * ```typescript\n * await NativeAudio.configure({\n * backgroundPlayback: true,\n * showNotification: true\n * });\n * // Start your foreground service here\n * // Then preload and play audio as normal\n * ```\n *\n * @default false\n * @platform Android\n * @since 8.2.0\n */\n backgroundPlayback?: boolean;\n}\n\n/**\n * Metadata to display in the notification center, Control Center (iOS), and lock screen\n * when `showNotification` is enabled in `configure()`.\n *\n * Note: This metadata will only be displayed if `showNotification: true` is set in the\n * `configure()` method. See {@link ConfigureOptions.showNotification} for important\n * behavior details about audio mixing on iOS.\n */\nexport interface NotificationMetadata {\n /**\n * The title to display in the notification center\n */\n title?: string;\n /**\n * The artist name to display in the notification center\n */\n artist?: string;\n /**\n * The album name to display in the notification center\n */\n album?: string;\n /**\n * URL or local path to the artwork/album art image\n */\n artworkUrl?: string;\n}\n\nexport interface PlayOnceOptions {\n /**\n * Path to the audio file, relative path of the file, absolute url (file://) or remote url (https://)\n * Supported formats:\n * - MP3, WAV (all platforms)\n * - M3U8/HLS streams (iOS and Android)\n */\n assetPath: string;\n /**\n * Volume of the audio, between 0.1 and 1.0\n * @default 1.0\n */\n volume?: number;\n /**\n * Is the audio file a URL, pass true if assetPath is a `file://` url\n * or a streaming URL (m3u8)\n * @default false\n */\n isUrl?: boolean;\n /**\n * Automatically start playback after loading\n * @default true\n */\n autoPlay?: boolean;\n /**\n * Delete the audio file from disk after playback completes\n * Only works for local files (file:// URLs), ignored for remote URLs\n * @default false\n * @since 7.11.0\n */\n deleteAfterPlay?: boolean;\n /**\n * Metadata to display in the notification center when audio is playing.\n * Only used when `showNotification: true` is set in `configure()`.\n *\n * See {@link ConfigureOptions.showNotification} for important details about\n * how this affects audio mixing behavior on iOS.\n *\n * @see NotificationMetadata\n * @since 7.10.0\n */\n notificationMetadata?: NotificationMetadata;\n /**\n * Custom HTTP headers to include when fetching remote audio files.\n * Only used when isUrl is true and assetPath is a remote URL (http/https).\n * Example: { 'x-api-key': 'abc123', 'Authorization': 'Bearer token' }\n *\n * @since 7.10.0\n */\n headers?: Record<string, string>;\n}\n\nexport interface PlayOnceResult {\n /**\n * The internally generated asset ID for this playback\n * Can be used to control playback (pause, stop, etc.) before completion\n */\n assetId: string;\n}\n\nexport interface PreloadOptions {\n /**\n * Path to the audio file, relative path of the file, absolute url (file://) or remote url (https://)\n * Supported formats:\n * - MP3, WAV (all platforms)\n * - M3U8/HLS streams (iOS and Android)\n */\n assetPath: string;\n /**\n * Asset Id, unique identifier of the file\n */\n assetId: string;\n /**\n * Volume of the audio, between 0.1 and 1.0\n */\n volume?: number;\n /**\n * Audio channel number, default is 1\n */\n audioChannelNum?: number;\n /**\n * Is the audio file a URL, pass true if assetPath is a `file://` url\n * or a streaming URL (m3u8)\n */\n isUrl?: boolean;\n /**\n * Metadata to display in the notification center when audio is playing.\n * Only used when `showNotification: true` is set in `configure()`.\n *\n * See {@link ConfigureOptions.showNotification} for important details about\n * how this affects audio mixing behavior on iOS.\n *\n * @see NotificationMetadata\n */\n notificationMetadata?: NotificationMetadata;\n /**\n * Custom HTTP headers to include when fetching remote audio files.\n * Only used when isUrl is true and assetPath is a remote URL (http/https).\n * Example: { 'x-api-key': 'abc123', 'Authorization': 'Bearer token' }\n *\n * @since 7.10.0\n */\n headers?: Record<string, string>;\n}\n\nexport interface CurrentTimeEvent {\n /**\n * Current time of the audio in seconds\n * @since 6.5.0\n */\n currentTime: number;\n /**\n * Asset Id of the audio\n * @since 6.5.0\n */\n assetId: string;\n}\n\nexport type CurrentTimeListener = (state: CurrentTimeEvent) => void;\n\nexport interface NativeAudio {\n /**\n * Configure the audio player\n * @since 5.0.0\n * @param option {@link ConfigureOptions}\n * @returns\n */\n configure(options: ConfigureOptions): Promise<void>;\n\n /**\n * Load an audio file\n * @since 5.0.0\n * @param option {@link PreloadOptions}\n * @returns\n */\n preload(options: PreloadOptions): Promise<void>;\n /**\n * Play an audio file once with automatic cleanup\n *\n * Method designed for simple, single-shot audio playback,\n * such as notification sounds, UI feedback, or other short audio clips\n * that don't require manual state management.\n *\n * **Key Features:**\n * - **Fire-and-forget**: No need to manually preload, play, stop, or unload\n * - **Auto-cleanup**: Asset is automatically unloaded after playback completes\n * - **Optional file deletion**: Can delete local files after playback (useful for temp files)\n * - **Returns assetId**: Can still control playback if needed (pause, stop, etc.)\n *\n * **Use Cases:**\n * - Notification sounds\n * - UI sound effects (button clicks, alerts)\n * - Short audio clips that play once\n * - Temporary audio files that should be cleaned up\n *\n * **Comparison with regular play():**\n * - `play()`: Requires manual preload, play, and unload steps\n * - `playOnce()`: Handles everything automatically with a single call\n *\n * @example\n * ```typescript\n * // Simple one-shot playback\n * await NativeAudio.playOnce({ assetPath: 'audio/notification.mp3' });\n *\n * // Play and delete the file after completion\n * await NativeAudio.playOnce({\n * assetPath: 'file:///path/to/temp/audio.mp3',\n * isUrl: true,\n * deleteAfterPlay: true\n * });\n *\n * // Get the assetId to control playback\n * const { assetId } = await NativeAudio.playOnce({\n * assetPath: 'audio/long-track.mp3',\n * autoPlay: true\n * });\n * // Later, you can stop it manually if needed\n * await NativeAudio.stop({ assetId });\n * ```\n *\n * @since 7.11.0\n * @param options {@link PlayOnceOptions}\n * @returns {Promise<PlayOnceResult>} Object containing the generated assetId\n */\n playOnce(options: PlayOnceOptions): Promise<PlayOnceResult>;\n /**\n * Check if an audio file is preloaded\n *\n * @since 6.1.0\n * @param option {@link Assets}\n * @returns {Promise<boolean>}\n */\n isPreloaded(options: PreloadOptions): Promise<{ found: boolean }>;\n\n /**\n * Play an audio file\n * @since 5.0.0\n * @param option {@link AssetPlayOptions}\n * @returns\n */\n play(options: AssetPlayOptions): Promise<void>;\n\n /**\n * Pause an audio file\n * @since 5.0.0\n * @param option {@link AssetPauseOptions}\n * @returns\n */\n pause(options: AssetPauseOptions): Promise<void>;\n\n /**\n * Resume an audio file\n * @since 5.0.0\n * @param option {@link AssetResumeOptions}\n * @returns\n */\n resume(options: AssetResumeOptions): Promise<void>;\n\n /**\n * Stop an audio file\n * @since 5.0.0\n * @param option {@link Assets}\n * @returns\n */\n loop(options: Assets): Promise<void>;\n\n /**\n * Stop an audio file\n * @since 5.0.0\n * @param option {@link AssetStopOptions}\n * @returns\n */\n stop(options: AssetStopOptions): Promise<void>;\n\n /**\n * Unload an audio file\n * @since 5.0.0\n * @param option {@link Assets}\n * @returns\n */\n unload(options: Assets): Promise<void>;\n\n /**\n * Set the volume of an audio file\n * @since 5.0.0\n * @param option {@link AssetVolume}\n * @returns {Promise<void>}\n */\n setVolume(options: AssetVolume): Promise<void>;\n\n /**\n * Set the rate of an audio file\n * @since 5.0.0\n * @param option {@link AssetRate}\n * @returns {Promise<void>}\n */\n setRate(options: AssetRate): Promise<void>;\n\n /**\n * Set the current time of an audio file\n * @since 6.5.0\n * @param option {@link AssetSetTime}\n * @returns {Promise<void>}\n */\n setCurrentTime(options: AssetSetTime): Promise<void>;\n\n /**\n * Get the current time of an audio file\n * @since 5.0.0\n * @param option {@link Assets}\n * @returns {Promise<{ currentTime: number }>}\n */\n getCurrentTime(options: Assets): Promise<{ currentTime: number }>;\n\n /**\n * Get the duration of an audio file in seconds\n * @since 5.0.0\n * @param option {@link Assets}\n * @returns {Promise<{ duration: number }>}\n */\n getDuration(options: Assets): Promise<{ duration: number }>;\n\n /**\n * Check if an audio file is playing\n *\n * @since 5.0.0\n * @param option {@link Assets}\n * @returns {Promise<boolean>}\n */\n isPlaying(options: Assets): Promise<{ isPlaying: boolean }>;\n\n /**\n * Listen for complete event\n *\n * @since 5.0.0\n * return {@link CompletedEvent}\n */\n addListener(eventName: 'complete', listenerFunc: CompletedListener): Promise<PluginListenerHandle>;\n\n /**\n * Listen for current time updates\n * Emits every 100ms while audio is playing\n *\n * @since 6.5.0\n * return {@link CurrentTimeEvent}\n */\n addListener(eventName: 'currentTime', listenerFunc: CurrentTimeListener): Promise<PluginListenerHandle>;\n /**\n * Clear the audio cache for remote audio files\n * @since 6.5.0\n * @returns {Promise<void>}\n */\n clearCache(): Promise<void>;\n\n /**\n * Set debug mode logging\n * @since 6.5.0\n * @param options - Options to enable or disable debug mode\n */\n setDebugMode(options: { enabled: boolean }): Promise<void>;\n\n /**\n * Get the native Capacitor plugin version\n *\n * @returns {Promise<{ id: string }>} an Promise with version for this device\n * @throws An error if the something went wrong\n */\n getPluginVersion(): Promise<{ version: string }>;\n\n /**\n * Deinitialize the plugin and restore original audio session settings\n * This method stops all playing audio and reverts any audio session changes made by the plugin\n * Use this when you need to ensure compatibility with other audio plugins\n *\n * @since 7.7.0\n * @returns {Promise<void>}\n */\n deinitPlugin(): Promise<void>;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["import type { PluginListenerHandle } from '@capacitor/core';\n\nexport interface CompletedEvent {\n /**\n * Emit when a play completes\n *\n * @since 5.0.0\n */\n assetId: string;\n}\n\nexport type CompletedListener = (state: CompletedEvent) => void;\n\nexport interface Assets {\n /**\n * Asset Id, unique identifier of the file\n */\n assetId: string;\n}\n\nexport interface AssetVolume {\n /**\n * Asset Id, unique identifier of the file\n */\n assetId: string;\n /**\n * Volume of the audio, between 0.1 and 1.0\n */\n volume: number;\n /**\n * Time over which to fade to the target volume, in seconds. Default is 0s (immediate).\n */\n duration?: number;\n}\n\nexport interface AssetRate {\n /**\n * Asset Id, unique identifier of the file\n */\n assetId: string;\n /**\n * Rate of the audio, between 0.1 and 1.0\n */\n rate: number;\n}\n\nexport interface AssetSetTime {\n /**\n * Asset Id, unique identifier of the file\n */\n assetId: string;\n /**\n * Time to set the audio, in seconds\n */\n time: number;\n}\n\nexport interface AssetPlayOptions {\n /**\n * Asset Id, unique identifier of the file\n */\n assetId: string;\n /**\n * Time to start playing the audio, in seconds\n */\n time?: number;\n /**\n * Delay to start playing the audio, in seconds\n */\n delay?: number;\n\n /**\n * Volume of the audio, between 0.1 and 1.0\n */\n volume?: number;\n\n /**\n * Whether to fade in the audio\n */\n fadeIn?: boolean;\n\n /**\n * Whether to fade out the audio\n */\n fadeOut?: boolean;\n\n /**\n * Fade in duration in seconds.\n * Only used if fadeIn is true.\n * Default is 1s.\n */\n fadeInDuration?: number;\n\n /**\n * Fade out duration in seconds.\n * Only used if fadeOut is true.\n * Default is 1s.\n */\n fadeOutDuration?: number;\n\n /**\n * Time in seconds from the start of the audio to start fading out.\n * Only used if fadeOut is true.\n * Default is fadeOutDuration before end of audio.\n */\n fadeOutStartTime?: number;\n}\n\nexport interface AssetStopOptions {\n /**\n * Asset Id, unique identifier of the file\n */\n assetId: string;\n\n /**\n * Whether to fade out the audio before stopping\n */\n fadeOut?: boolean;\n\n /**\n * Fade out duration in seconds.\n * Default is 1s.\n */\n fadeOutDuration?: number;\n}\n\nexport interface AssetPauseOptions {\n /**\n * Asset Id, unique identifier of the file\n */\n assetId: string;\n\n /**\n * Whether to fade out the audio before pausing\n */\n fadeOut?: boolean;\n\n /**\n * Fade out duration in seconds.\n * Default is 1s.\n */\n fadeOutDuration?: number;\n}\n\nexport interface AssetResumeOptions {\n /**\n * Asset Id, unique identifier of the file\n */\n assetId: string;\n\n /**\n * Whether to fade in the audio during resume\n */\n fadeIn?: boolean;\n\n /**\n * Fade in duration in seconds.\n * Default is 1s.\n */\n fadeInDuration?: number;\n}\n\nexport interface ConfigureOptions {\n /**\n * focus the audio with Audio Focus\n */\n focus?: boolean;\n /**\n * Play the audio in the background\n */\n background?: boolean;\n /**\n * Ignore silent mode, works only on iOS setting this will nuke other audio apps\n */\n ignoreSilent?: boolean;\n /**\n * Show audio playback in the notification center (iOS and Android)\n * When enabled, displays audio metadata (title, artist, album, artwork) in the system notification\n * and Control Center (iOS) or lock screen.\n *\n * **Important iOS Behavior:**\n * Enabling this option changes the audio session category to `.playback` with `.default` mode,\n * which means your app's audio will **interrupt** other apps' audio (like background music from\n * Spotify, Apple Music, etc.) instead of mixing with it. This is required for the Now Playing\n * info to appear in Control Center and on the lock screen.\n *\n * **Trade-offs:**\n * - `showNotification: true` → Shows Now Playing controls, but interrupts other audio\n * - `showNotification: false` → Audio mixes with other apps, but no Now Playing controls\n *\n * Use this when your app is the primary audio source (music players, podcast apps, etc.).\n * Disable this for secondary audio like sound effects or notification sounds where mixing\n * with background music is preferred.\n *\n * @see https://github.com/Cap-go/capacitor-native-audio/issues/202\n */\n showNotification?: boolean;\n /**\n * Enable background audio playback (Android only)\n *\n * When enabled, audio will continue playing when the app is backgrounded or the screen is locked.\n * The plugin will skip the automatic pause/resume logic that normally occurs when the app\n * enters the background or returns to the foreground.\n *\n * **Important Android Requirements:**\n * To use background playback on Android, your app must:\n * 1. Declare the required permissions in `AndroidManifest.xml`:\n * - `<uses-permission android:name=\"android.permission.FOREGROUND_SERVICE\" />`\n * - `<uses-permission android:name=\"android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK\" />`\n * - `<uses-permission android:name=\"android.permission.WAKE_LOCK\" />`\n * 2. Start a Foreground Service with a media-style notification before backgrounding\n * (the plugin does not automatically create or manage the foreground service)\n * 3. Use `showNotification: true` to display playback controls in the notification\n *\n * **Usage Example:**\n * ```typescript\n * await NativeAudio.configure({\n * backgroundPlayback: true,\n * showNotification: true\n * });\n * // Start your foreground service here\n * // Then preload and play audio as normal\n * ```\n *\n * @default false\n * @platform Android\n * @since 8.2.0\n */\n backgroundPlayback?: boolean;\n}\n\n/**\n * Metadata to display in the notification center, Control Center (iOS), and lock screen\n * when `showNotification` is enabled in `configure()`.\n *\n * Note: This metadata will only be displayed if `showNotification: true` is set in the\n * `configure()` method. See {@link ConfigureOptions.showNotification} for important\n * behavior details about audio mixing on iOS.\n */\nexport interface NotificationMetadata {\n /**\n * The title to display in the notification center\n */\n title?: string;\n /**\n * The artist name to display in the notification center\n */\n artist?: string;\n /**\n * The album name to display in the notification center\n */\n album?: string;\n /**\n * URL or local path to the artwork/album art image\n */\n artworkUrl?: string;\n}\n\nexport interface PlayOnceOptions {\n /**\n * Path to the audio file, relative path of the file, absolute url (file://) or remote url (https://)\n * Supported formats:\n * - MP3, WAV (all platforms)\n * - M3U8/HLS streams (iOS and Android)\n */\n assetPath: string;\n /**\n * Volume of the audio, between 0.1 and 1.0\n * @default 1.0\n */\n volume?: number;\n /**\n * Is the audio file a URL, pass true if assetPath is a `file://` url\n * or a streaming URL (m3u8)\n * @default false\n */\n isUrl?: boolean;\n /**\n * Automatically start playback after loading\n * @default true\n */\n autoPlay?: boolean;\n /**\n * Delete the audio file from disk after playback completes\n * Only works for local files (file:// URLs), ignored for remote URLs\n * @default false\n * @since 7.11.0\n */\n deleteAfterPlay?: boolean;\n /**\n * Metadata to display in the notification center when audio is playing.\n * Only used when `showNotification: true` is set in `configure()`.\n *\n * See {@link ConfigureOptions.showNotification} for important details about\n * how this affects audio mixing behavior on iOS.\n *\n * @see NotificationMetadata\n * @since 7.10.0\n */\n notificationMetadata?: NotificationMetadata;\n /**\n * Custom HTTP headers to include when fetching remote audio files.\n * Only used when isUrl is true and assetPath is a remote URL (http/https).\n * Example: { 'x-api-key': 'abc123', 'Authorization': 'Bearer token' }\n *\n * @since 7.10.0\n */\n headers?: Record<string, string>;\n}\n\nexport interface PlayOnceResult {\n /**\n * The internally generated asset ID for this playback\n * Can be used to control playback (pause, stop, etc.) before completion\n */\n assetId: string;\n}\n\nexport interface PreloadOptions {\n /**\n * Path to the audio file, relative path of the file, absolute url (file://) or remote url (https://)\n * Supported formats:\n * - MP3, WAV (all platforms)\n * - M3U8/HLS streams (iOS and Android)\n */\n assetPath: string;\n /**\n * Asset Id, unique identifier of the file\n */\n assetId: string;\n /**\n * Volume of the audio, between 0.1 and 1.0\n */\n volume?: number;\n /**\n * Audio channel number, default is 1\n */\n audioChannelNum?: number;\n /**\n * Is the audio file a URL, pass true if assetPath is a `file://` url\n * or a streaming URL (m3u8)\n */\n isUrl?: boolean;\n /**\n * Metadata to display in the notification center when audio is playing.\n * Only used when `showNotification: true` is set in `configure()`.\n *\n * See {@link ConfigureOptions.showNotification} for important details about\n * how this affects audio mixing behavior on iOS.\n *\n * @see NotificationMetadata\n */\n notificationMetadata?: NotificationMetadata;\n /**\n * Custom HTTP headers to include when fetching remote audio files.\n * Only used when isUrl is true and assetPath is a remote URL (http/https).\n * Example: { 'x-api-key': 'abc123', 'Authorization': 'Bearer token' }\n *\n * @since 7.10.0\n */\n headers?: Record<string, string>;\n}\n\nexport interface CurrentTimeEvent {\n /**\n * Current time of the audio in seconds\n * @since 6.5.0\n */\n currentTime: number;\n /**\n * Asset Id of the audio\n * @since 6.5.0\n */\n assetId: string;\n}\n\nexport type CurrentTimeListener = (state: CurrentTimeEvent) => void;\n\nexport type PlaybackStateValue = 'playing' | 'paused' | 'stopped';\n\nexport interface PlaybackStateEvent {\n /**\n * Asset Id of the audio\n */\n assetId: string;\n /**\n * Resolved playback state after a local or remote transport action.\n */\n state: PlaybackStateValue;\n /**\n * Reason for the state change, for example `play`, `pause`, `remotePlay`, or `complete`.\n */\n reason: string;\n /**\n * Whether the asset is currently playing.\n */\n isPlaying: boolean;\n /**\n * Current playback position in seconds when available.\n */\n currentTime?: number;\n /**\n * Total playback duration in seconds when available.\n */\n duration?: number;\n}\n\nexport type PlaybackStateListener = (state: PlaybackStateEvent) => void;\n\nexport interface NativeAudio {\n /**\n * Configure the audio player\n * @since 5.0.0\n * @param option {@link ConfigureOptions}\n * @returns\n */\n configure(options: ConfigureOptions): Promise<void>;\n\n /**\n * Load an audio file\n * @since 5.0.0\n * @param option {@link PreloadOptions}\n * @returns\n */\n preload(options: PreloadOptions): Promise<void>;\n /**\n * Play an audio file once with automatic cleanup\n *\n * Method designed for simple, single-shot audio playback,\n * such as notification sounds, UI feedback, or other short audio clips\n * that don't require manual state management.\n *\n * **Key Features:**\n * - **Fire-and-forget**: No need to manually preload, play, stop, or unload\n * - **Auto-cleanup**: Asset is automatically unloaded after playback completes\n * - **Optional file deletion**: Can delete local files after playback (useful for temp files)\n * - **Returns assetId**: Can still control playback if needed (pause, stop, etc.)\n *\n * **Use Cases:**\n * - Notification sounds\n * - UI sound effects (button clicks, alerts)\n * - Short audio clips that play once\n * - Temporary audio files that should be cleaned up\n *\n * **Comparison with regular play():**\n * - `play()`: Requires manual preload, play, and unload steps\n * - `playOnce()`: Handles everything automatically with a single call\n *\n * @example\n * ```typescript\n * // Simple one-shot playback\n * await NativeAudio.playOnce({ assetPath: 'audio/notification.mp3' });\n *\n * // Play and delete the file after completion\n * await NativeAudio.playOnce({\n * assetPath: 'file:///path/to/temp/audio.mp3',\n * isUrl: true,\n * deleteAfterPlay: true\n * });\n *\n * // Get the assetId to control playback\n * const { assetId } = await NativeAudio.playOnce({\n * assetPath: 'audio/long-track.mp3',\n * autoPlay: true\n * });\n * // Later, you can stop it manually if needed\n * await NativeAudio.stop({ assetId });\n * ```\n *\n * @since 7.11.0\n * @param options {@link PlayOnceOptions}\n * @returns {Promise<PlayOnceResult>} Object containing the generated assetId\n */\n playOnce(options: PlayOnceOptions): Promise<PlayOnceResult>;\n /**\n * Check if an audio file is preloaded\n *\n * @since 6.1.0\n * @param option {@link Assets}\n * @returns {Promise<boolean>}\n */\n isPreloaded(options: PreloadOptions): Promise<{ found: boolean }>;\n\n /**\n * Play an audio file\n * @since 5.0.0\n * @param option {@link AssetPlayOptions}\n * @returns\n */\n play(options: AssetPlayOptions): Promise<void>;\n\n /**\n * Pause an audio file\n * @since 5.0.0\n * @param option {@link AssetPauseOptions}\n * @returns\n */\n pause(options: AssetPauseOptions): Promise<void>;\n\n /**\n * Resume an audio file\n * @since 5.0.0\n * @param option {@link AssetResumeOptions}\n * @returns\n */\n resume(options: AssetResumeOptions): Promise<void>;\n\n /**\n * Stop an audio file\n * @since 5.0.0\n * @param option {@link Assets}\n * @returns\n */\n loop(options: Assets): Promise<void>;\n\n /**\n * Stop an audio file\n * @since 5.0.0\n * @param option {@link AssetStopOptions}\n * @returns\n */\n stop(options: AssetStopOptions): Promise<void>;\n\n /**\n * Unload an audio file\n * @since 5.0.0\n * @param option {@link Assets}\n * @returns\n */\n unload(options: Assets): Promise<void>;\n\n /**\n * Set the volume of an audio file\n * @since 5.0.0\n * @param option {@link AssetVolume}\n * @returns {Promise<void>}\n */\n setVolume(options: AssetVolume): Promise<void>;\n\n /**\n * Set the rate of an audio file\n * @since 5.0.0\n * @param option {@link AssetRate}\n * @returns {Promise<void>}\n */\n setRate(options: AssetRate): Promise<void>;\n\n /**\n * Set the current time of an audio file\n * @since 6.5.0\n * @param option {@link AssetSetTime}\n * @returns {Promise<void>}\n */\n setCurrentTime(options: AssetSetTime): Promise<void>;\n\n /**\n * Get the current time of an audio file\n * @since 5.0.0\n * @param option {@link Assets}\n * @returns {Promise<{ currentTime: number }>}\n */\n getCurrentTime(options: Assets): Promise<{ currentTime: number }>;\n\n /**\n * Get the duration of an audio file in seconds\n * @since 5.0.0\n * @param option {@link Assets}\n * @returns {Promise<{ duration: number }>}\n */\n getDuration(options: Assets): Promise<{ duration: number }>;\n\n /**\n * Check if an audio file is playing\n *\n * @since 5.0.0\n * @param option {@link Assets}\n * @returns {Promise<boolean>}\n */\n isPlaying(options: Assets): Promise<{ isPlaying: boolean }>;\n\n /**\n * Listen for complete event\n *\n * @since 5.0.0\n * return {@link CompletedEvent}\n */\n addListener(eventName: 'complete', listenerFunc: CompletedListener): Promise<PluginListenerHandle>;\n\n /**\n * Listen for current time updates\n * Emits every 100ms while audio is playing\n *\n * @since 6.5.0\n * return {@link CurrentTimeEvent}\n */\n addListener(eventName: 'currentTime', listenerFunc: CurrentTimeListener): Promise<PluginListenerHandle>;\n /**\n * Listen for playback state changes, including notification and lock-screen transport controls.\n * Emitted by Android and iOS. The current Web implementation does not emit this event.\n *\n * @since 8.3.15\n * return {@link PlaybackStateEvent}\n */\n addListener(eventName: 'playbackState', listenerFunc: PlaybackStateListener): Promise<PluginListenerHandle>;\n /**\n * Clear the audio cache for remote audio files\n * @since 6.5.0\n * @returns {Promise<void>}\n */\n clearCache(): Promise<void>;\n\n /**\n * Set debug mode logging\n * @since 6.5.0\n * @param options - Options to enable or disable debug mode\n */\n setDebugMode(options: { enabled: boolean }): Promise<void>;\n\n /**\n * Get the native Capacitor plugin version\n *\n * @returns {Promise<{ id: string }>} an Promise with version for this device\n * @throws An error if the something went wrong\n */\n getPluginVersion(): Promise<{ version: string }>;\n\n /**\n * Deinitialize the plugin and restore original audio session settings\n * This method stops all playing audio and reverts any audio session changes made by the plugin\n * Use this when you need to ensure compatibility with other audio plugins\n *\n * @since 7.7.0\n * @returns {Promise<void>}\n */\n deinitPlugin(): Promise<void>;\n}\n"]}
|
|
@@ -8,11 +8,17 @@ enum MyError: Error {
|
|
|
8
8
|
case runtimeError(String)
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
private enum PlaybackStateValue: String {
|
|
12
|
+
case playing
|
|
13
|
+
case paused
|
|
14
|
+
case stopped
|
|
15
|
+
}
|
|
16
|
+
|
|
11
17
|
// swiftlint:disable file_length
|
|
12
18
|
@objc(NativeAudio)
|
|
13
19
|
// swiftlint:disable:next type_body_length
|
|
14
20
|
public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
15
|
-
private let pluginVersion: String = "8.
|
|
21
|
+
private let pluginVersion: String = "8.4.1"
|
|
16
22
|
public let identifier = "NativeAudio"
|
|
17
23
|
public let jsName = "NativeAudio"
|
|
18
24
|
public let pluginMethods: [CAPPluginMethod] = [
|
|
@@ -172,9 +178,58 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
172
178
|
}
|
|
173
179
|
}
|
|
174
180
|
|
|
181
|
+
private func resolvePlaybackState(assetId: String, audioAsset: AudioAsset?) -> PlaybackStateValue {
|
|
182
|
+
if let audioAsset, audioAsset.isPlaying() {
|
|
183
|
+
return .playing
|
|
184
|
+
}
|
|
185
|
+
if let data = audioAssetData[assetId],
|
|
186
|
+
data["timeBeforePause"] != nil || data["volumeBeforePause"] != nil {
|
|
187
|
+
return .paused
|
|
188
|
+
}
|
|
189
|
+
if currentlyPlayingAssetId == assetId {
|
|
190
|
+
return .paused
|
|
191
|
+
}
|
|
192
|
+
return .stopped
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private func notifyPlaybackState(assetId: String, reason: String, state: PlaybackStateValue? = nil, audioAsset: AudioAsset? = nil) {
|
|
196
|
+
let emit = {
|
|
197
|
+
let asset = audioAsset ?? self.audioList[assetId] as? AudioAsset
|
|
198
|
+
let resolvedState = state ?? self.resolvePlaybackState(assetId: assetId, audioAsset: asset)
|
|
199
|
+
var data: [String: Any] = [
|
|
200
|
+
"assetId": assetId,
|
|
201
|
+
"state": resolvedState.rawValue,
|
|
202
|
+
"reason": reason,
|
|
203
|
+
"isPlaying": resolvedState == .playing
|
|
204
|
+
]
|
|
205
|
+
if let asset {
|
|
206
|
+
data["currentTime"] = asset.getCurrentTime()
|
|
207
|
+
let duration = asset.getDuration()
|
|
208
|
+
if duration.isFinite {
|
|
209
|
+
data["duration"] = duration
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
self.notifyListeners("playbackState", data: data)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if DispatchQueue.getSpecific(key: queueKey) != nil || DispatchQueue.getSpecific(key: audioQueueContextKey) == true {
|
|
216
|
+
emit()
|
|
217
|
+
} else {
|
|
218
|
+
audioQueue.async(execute: emit)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
internal func handlePlaybackCompletion(assetId: String, audioAsset: AudioAsset? = nil) {
|
|
223
|
+
if currentlyPlayingAssetId == assetId {
|
|
224
|
+
currentlyPlayingAssetId = nil
|
|
225
|
+
clearNowPlayingInfo()
|
|
226
|
+
}
|
|
227
|
+
notifyPlaybackState(assetId: assetId, reason: "complete", state: .stopped, audioAsset: audioAsset)
|
|
228
|
+
}
|
|
229
|
+
|
|
175
230
|
/// Must be called on `audioQueue`. If `timeBeforePause` is stored, clears it and seeks (async for remote) before running `resume` + Now Playing refresh.
|
|
176
231
|
/// Mirrors `resume(_:)` (non–fade-in path): restores `volumeBeforePause` via `setVolume`, clears that key from `audioAssetData`, then `resume()`.
|
|
177
|
-
private func resumeAssetFromStoredPausePositionIfNeeded(assetId: String, asset: AudioAsset) {
|
|
232
|
+
private func resumeAssetFromStoredPausePositionIfNeeded(assetId: String, asset: AudioAsset, reason: String = "resume") {
|
|
178
233
|
var restoredTime: TimeInterval?
|
|
179
234
|
if var data = audioAssetData[assetId],
|
|
180
235
|
let time = data["timeBeforePause"] as? TimeInterval {
|
|
@@ -198,10 +253,14 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
198
253
|
self.audioAssetData[assetId] = data
|
|
199
254
|
}
|
|
200
255
|
asset.resume()
|
|
201
|
-
|
|
256
|
+
if self.showNotification {
|
|
257
|
+
self.currentlyPlayingAssetId = assetId
|
|
258
|
+
self.updateNowPlayingInfo(audioId: assetId, audioAsset: asset)
|
|
259
|
+
}
|
|
260
|
+
self.notifyPlaybackState(assetId: assetId, reason: reason, state: .playing, audioAsset: asset)
|
|
202
261
|
}
|
|
203
|
-
if let
|
|
204
|
-
asset.setCurrentTime(time:
|
|
262
|
+
if let resumeTime = restoredTime {
|
|
263
|
+
asset.setCurrentTime(time: resumeTime) { [weak self] in
|
|
205
264
|
guard let self else { return }
|
|
206
265
|
audioQueue.async(flags: .barrier, execute: resumeAndRefreshNowPlaying)
|
|
207
266
|
}
|
|
@@ -226,7 +285,7 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
226
285
|
}
|
|
227
286
|
|
|
228
287
|
if !asset.isPlaying() {
|
|
229
|
-
self.resumeAssetFromStoredPausePositionIfNeeded(assetId: assetId, asset: asset)
|
|
288
|
+
self.resumeAssetFromStoredPausePositionIfNeeded(assetId: assetId, asset: asset, reason: "remotePlay")
|
|
230
289
|
}
|
|
231
290
|
}
|
|
232
291
|
return .success
|
|
@@ -251,6 +310,7 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
251
310
|
|
|
252
311
|
asset.pause()
|
|
253
312
|
self.updatePlaybackState(isPlaying: false, elapsedTime: timeBeforePause, duration: asset.getDuration())
|
|
313
|
+
self.notifyPlaybackState(assetId: assetId, reason: "remotePause", state: .paused, audioAsset: asset)
|
|
254
314
|
}
|
|
255
315
|
return .success
|
|
256
316
|
}
|
|
@@ -280,6 +340,7 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
280
340
|
duration: duration
|
|
281
341
|
)
|
|
282
342
|
}
|
|
343
|
+
self.notifyPlaybackState(assetId: assetId, reason: "remoteStop", state: .stopped, audioAsset: asset)
|
|
283
344
|
}
|
|
284
345
|
return .success
|
|
285
346
|
}
|
|
@@ -304,15 +365,100 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
304
365
|
|
|
305
366
|
asset.pause()
|
|
306
367
|
self.updatePlaybackState(isPlaying: false, elapsedTime: timeBeforePause, duration: asset.getDuration())
|
|
368
|
+
self.notifyPlaybackState(assetId: assetId, reason: "remotePause", state: .paused, audioAsset: asset)
|
|
307
369
|
} else {
|
|
308
|
-
self.resumeAssetFromStoredPausePositionIfNeeded(assetId: assetId, asset: asset)
|
|
370
|
+
self.resumeAssetFromStoredPausePositionIfNeeded(assetId: assetId, asset: asset, reason: "remotePlay")
|
|
309
371
|
}
|
|
310
372
|
}
|
|
311
373
|
return .success
|
|
312
374
|
}
|
|
375
|
+
|
|
376
|
+
// Skip forward command
|
|
377
|
+
commandCenter.skipForwardCommand.preferredIntervals = [NSNumber(value: 15)]
|
|
378
|
+
commandCenter.skipForwardCommand.isEnabled = true
|
|
379
|
+
commandCenter.skipForwardCommand.addTarget { [weak self] event in
|
|
380
|
+
guard let self else { return .commandFailed }
|
|
381
|
+
guard let skipEvent = event as? MPSkipIntervalCommandEvent else { return .commandFailed }
|
|
382
|
+
return self.handleSeekCommand(delta: skipEvent.interval)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Skip backward command
|
|
386
|
+
commandCenter.skipBackwardCommand.preferredIntervals = [NSNumber(value: 15)]
|
|
387
|
+
commandCenter.skipBackwardCommand.isEnabled = true
|
|
388
|
+
commandCenter.skipBackwardCommand.addTarget { [weak self] event in
|
|
389
|
+
guard let self else { return .commandFailed }
|
|
390
|
+
guard let skipEvent = event as? MPSkipIntervalCommandEvent else { return .commandFailed }
|
|
391
|
+
return self.handleSeekCommand(delta: -skipEvent.interval)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Scrub / change position command
|
|
395
|
+
commandCenter.changePlaybackPositionCommand.isEnabled = true
|
|
396
|
+
commandCenter.changePlaybackPositionCommand.addTarget { [weak self] event in
|
|
397
|
+
guard let self else { return .commandFailed }
|
|
398
|
+
guard let positionEvent = event as? MPChangePlaybackPositionCommandEvent else { return .commandFailed }
|
|
399
|
+
return self.handleSeekCommand(targetTime: positionEvent.positionTime)
|
|
400
|
+
}
|
|
313
401
|
}
|
|
314
402
|
// swiftlint:enable function_body_length
|
|
315
403
|
|
|
404
|
+
private func handleSeekCommand(delta: TimeInterval? = nil, targetTime: TimeInterval? = nil) -> MPRemoteCommandHandlerStatus {
|
|
405
|
+
guard let assetId = currentlyPlayingAssetId else {
|
|
406
|
+
return .noSuchContent
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
var asset: AudioAsset?
|
|
410
|
+
audioQueue.sync {
|
|
411
|
+
asset = audioList[assetId] as? AudioAsset
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
guard let audioAsset = asset else {
|
|
415
|
+
return .noSuchContent
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
let duration = audioAsset.getDuration()
|
|
419
|
+
let currentTime = audioAsset.getCurrentTime()
|
|
420
|
+
let requestedTime: TimeInterval
|
|
421
|
+
|
|
422
|
+
if let delta {
|
|
423
|
+
requestedTime = currentTime + delta
|
|
424
|
+
} else if let targetTime {
|
|
425
|
+
requestedTime = targetTime
|
|
426
|
+
} else {
|
|
427
|
+
return .commandFailed
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
let clampedTime: TimeInterval
|
|
431
|
+
if duration.isFinite && duration > 0 {
|
|
432
|
+
clampedTime = min(max(requestedTime, 0), duration)
|
|
433
|
+
} else {
|
|
434
|
+
clampedTime = max(requestedTime, 0)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
audioAsset.setCurrentTime(time: clampedTime) { [weak self, weak audioAsset] in
|
|
438
|
+
guard let self else { return }
|
|
439
|
+
let isPlaying = audioAsset?.isPlaying() ?? false
|
|
440
|
+
let durationValue = duration.isFinite && duration > 0 ? duration : nil
|
|
441
|
+
|
|
442
|
+
if self.showNotification,
|
|
443
|
+
self.currentlyPlayingAssetId == assetId {
|
|
444
|
+
self.updatePlaybackState(
|
|
445
|
+
isPlaying: isPlaying,
|
|
446
|
+
elapsedTime: clampedTime,
|
|
447
|
+
duration: durationValue
|
|
448
|
+
)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Emit a currentTime event so JS can sync UI immediately after remote seek.
|
|
452
|
+
let roundedTime = round(clampedTime * 10) / 10
|
|
453
|
+
self.notifyListeners("currentTime", data: [
|
|
454
|
+
"currentTime": roundedTime,
|
|
455
|
+
"assetId": assetId
|
|
456
|
+
])
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return .success
|
|
460
|
+
}
|
|
461
|
+
|
|
316
462
|
@objc func setDebugMode(_ call: CAPPluginCall) {
|
|
317
463
|
let debug = call.getBool("enabled") ?? false
|
|
318
464
|
Logger.debugModeEnabled = debug
|
|
@@ -629,7 +775,7 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
629
775
|
}
|
|
630
776
|
|
|
631
777
|
// Set up completion handler
|
|
632
|
-
asset.onComplete = {
|
|
778
|
+
asset.onComplete = {
|
|
633
779
|
cleanupHandler()
|
|
634
780
|
}
|
|
635
781
|
|
|
@@ -644,6 +790,7 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
644
790
|
self.updateNowPlayingInfo(audioId: assetId, audioAsset: asset)
|
|
645
791
|
self.updatePlaybackState(isPlaying: true)
|
|
646
792
|
}
|
|
793
|
+
self.notifyPlaybackState(assetId: assetId, reason: "playOnce", state: .playing, audioAsset: asset)
|
|
647
794
|
}
|
|
648
795
|
|
|
649
796
|
// Return the generated assetId
|
|
@@ -715,14 +862,13 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
715
862
|
// Instead, check if all players are done and clear Now Playing if this asset was current
|
|
716
863
|
audioQueue.async { [weak self] in
|
|
717
864
|
guard let self = self else { return }
|
|
865
|
+
var completedAssetId: String?
|
|
718
866
|
|
|
719
867
|
// Find which asset this player belongs to; if it was the currently playing one, clear notification
|
|
720
868
|
for (audioId, asset) in self.audioList {
|
|
721
869
|
if let audioAsset = asset as? AudioAsset, audioAsset.channels.contains(player) {
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
self.clearNowPlayingInfo()
|
|
725
|
-
}
|
|
870
|
+
completedAssetId = audioId
|
|
871
|
+
self.handlePlaybackCompletion(assetId: audioId, audioAsset: audioAsset)
|
|
726
872
|
break
|
|
727
873
|
}
|
|
728
874
|
}
|
|
@@ -739,7 +885,7 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
739
885
|
// Only end the session if no more assets are playing
|
|
740
886
|
if !hasPlayingAssets {
|
|
741
887
|
// If we didn't find the asset above (e.g. playOnce already removed it), clear notification when nothing is playing
|
|
742
|
-
if self.currentlyPlayingAssetId != nil {
|
|
888
|
+
if completedAssetId == nil, self.currentlyPlayingAssetId != nil {
|
|
743
889
|
self.currentlyPlayingAssetId = nil
|
|
744
890
|
self.clearNowPlayingInfo()
|
|
745
891
|
}
|
|
@@ -806,6 +952,7 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
806
952
|
self.updateNowPlayingInfo(audioId: audioId, audioAsset: audioAsset)
|
|
807
953
|
self.updatePlaybackState(isPlaying: true)
|
|
808
954
|
}
|
|
955
|
+
self.notifyPlaybackState(assetId: audioId, reason: "play", state: .playing, audioAsset: audioAsset)
|
|
809
956
|
call.resolve()
|
|
810
957
|
}
|
|
811
958
|
}
|
|
@@ -887,6 +1034,7 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
887
1034
|
}
|
|
888
1035
|
}
|
|
889
1036
|
|
|
1037
|
+
// swiftlint:disable:next function_body_length
|
|
890
1038
|
@objc func resume(_ call: CAPPluginCall) {
|
|
891
1039
|
let audioId = call.getString(Constant.AssetIdKey) ?? ""
|
|
892
1040
|
audioQueue.sync {
|
|
@@ -931,13 +1079,15 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
931
1079
|
self.audioAssetData[audioAsset.assetId] = data
|
|
932
1080
|
}
|
|
933
1081
|
if self.showNotification {
|
|
1082
|
+
self.currentlyPlayingAssetId = audioId
|
|
934
1083
|
self.updateNowPlayingInfo(audioId: audioId, audioAsset: audioAsset)
|
|
935
1084
|
}
|
|
1085
|
+
self.notifyPlaybackState(assetId: audioId, reason: "resume", state: .playing, audioAsset: audioAsset)
|
|
936
1086
|
call.resolve()
|
|
937
1087
|
}
|
|
938
1088
|
|
|
939
|
-
if let
|
|
940
|
-
audioAsset.setCurrentTime(time:
|
|
1089
|
+
if let resumeTime = restoredTime {
|
|
1090
|
+
audioAsset.setCurrentTime(time: resumeTime) { [weak self] in
|
|
941
1091
|
guard let self else { return }
|
|
942
1092
|
self.audioQueue.async(flags: .barrier, execute: finishResume)
|
|
943
1093
|
}
|
|
@@ -980,6 +1130,9 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
980
1130
|
}
|
|
981
1131
|
|
|
982
1132
|
self.endSession()
|
|
1133
|
+
if !fadeOut {
|
|
1134
|
+
self.notifyPlaybackState(assetId: audioAsset.assetId, reason: "pause", state: .paused, audioAsset: audioAsset)
|
|
1135
|
+
}
|
|
983
1136
|
call.resolve()
|
|
984
1137
|
}
|
|
985
1138
|
}
|
|
@@ -1036,6 +1189,13 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
1036
1189
|
}
|
|
1037
1190
|
|
|
1038
1191
|
self.endSession()
|
|
1192
|
+
if !fadeOut {
|
|
1193
|
+
if let audioAsset = self.audioList[audioId] as? AudioAsset {
|
|
1194
|
+
self.notifyPlaybackState(assetId: audioId, reason: "stop", state: .stopped, audioAsset: audioAsset)
|
|
1195
|
+
} else {
|
|
1196
|
+
self.notifyPlaybackState(assetId: audioId, reason: "stop", state: .stopped)
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1039
1199
|
call.resolve()
|
|
1040
1200
|
} catch {
|
|
1041
1201
|
call.reject(error.localizedDescription)
|
|
@@ -1053,6 +1213,12 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
1053
1213
|
}
|
|
1054
1214
|
|
|
1055
1215
|
audioAsset.loop()
|
|
1216
|
+
if self.showNotification {
|
|
1217
|
+
self.currentlyPlayingAssetId = audioAsset.assetId
|
|
1218
|
+
self.updateNowPlayingInfo(audioId: audioAsset.assetId, audioAsset: audioAsset)
|
|
1219
|
+
self.updatePlaybackState(isPlaying: true)
|
|
1220
|
+
}
|
|
1221
|
+
self.notifyPlaybackState(assetId: audioAsset.assetId, reason: "loop", state: .playing, audioAsset: audioAsset)
|
|
1056
1222
|
call.resolve()
|
|
1057
1223
|
}
|
|
1058
1224
|
}
|
|
@@ -1468,6 +1634,7 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
1468
1634
|
call.resolve()
|
|
1469
1635
|
}
|
|
1470
1636
|
|
|
1637
|
+
// swiftlint:disable cyclomatic_complexity
|
|
1471
1638
|
/// Updates the system Now Playing information for the specified audio asset.
|
|
1472
1639
|
///
|
|
1473
1640
|
/// Looks up stored metadata for `audioId` and publishes title, artist, album, artwork (if provided),
|
|
@@ -1529,6 +1696,7 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
1529
1696
|
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
|
|
1530
1697
|
}
|
|
1531
1698
|
}
|
|
1699
|
+
// swiftlint:enable cyclomatic_complexity
|
|
1532
1700
|
|
|
1533
1701
|
/// Clears the Now Playing info when the plugin is no longer the active notifier.
|
|
1534
1702
|
private func clearNowPlayingInfo() {
|
|
@@ -1547,6 +1715,11 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
1547
1715
|
if self.showNotification && self.currentlyPlayingAssetId == assetId {
|
|
1548
1716
|
self.updatePlaybackState(isPlaying: false, elapsedTime: elapsedTime, duration: duration)
|
|
1549
1717
|
}
|
|
1718
|
+
if let audioAsset = self.audioList[assetId] as? AudioAsset {
|
|
1719
|
+
self.notifyPlaybackState(assetId: assetId, reason: "pause", state: .paused, audioAsset: audioAsset)
|
|
1720
|
+
} else {
|
|
1721
|
+
self.notifyPlaybackState(assetId: assetId, reason: "pause", state: .paused)
|
|
1722
|
+
}
|
|
1550
1723
|
}
|
|
1551
1724
|
}
|
|
1552
1725
|
|
|
@@ -1557,6 +1730,11 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
1557
1730
|
if self.showNotification && self.currentlyPlayingAssetId == assetId {
|
|
1558
1731
|
self.updatePlaybackState(isPlaying: false, elapsedTime: elapsedTime, duration: duration)
|
|
1559
1732
|
}
|
|
1733
|
+
if let audioAsset = self.audioList[assetId] as? AudioAsset {
|
|
1734
|
+
self.notifyPlaybackState(assetId: assetId, reason: "stop", state: .stopped, audioAsset: audioAsset)
|
|
1735
|
+
} else {
|
|
1736
|
+
self.notifyPlaybackState(assetId: assetId, reason: "stop", state: .stopped)
|
|
1737
|
+
}
|
|
1560
1738
|
}
|
|
1561
1739
|
}
|
|
1562
1740
|
|