@capgo/native-audio 7.7.8 → 7.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -13
- package/android/build.gradle +3 -0
- package/android/src/main/java/ee/forgr/audio/Constant.java +2 -0
- package/android/src/main/java/ee/forgr/audio/NativeAudio.java +251 -0
- package/dist/docs.json +53 -0
- package/dist/esm/definitions.d.ts +28 -0
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/NativeAudioPlugin/AudioAsset.swift +2 -2
- package/ios/Sources/NativeAudioPlugin/Constant.swift +2 -0
- package/ios/Sources/NativeAudioPlugin/Plugin.swift +228 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,9 +48,11 @@ The only **free**, **full-featured** audio playback plugin for Capacitor:
|
|
|
48
48
|
- **Full control** - Play, pause, resume, loop, seek, volume, playback rate
|
|
49
49
|
- **Multiple channels** - Play multiple audio files simultaneously
|
|
50
50
|
- **Background playback** - Continue playing when app is backgrounded
|
|
51
|
+
- **Notification center display** - Show audio metadata in iOS Control Center and Android notifications
|
|
51
52
|
- **Position tracking** - Real-time currentTime events (100ms intervals)
|
|
52
53
|
- **Modern package management** - Supports both Swift Package Manager (SPM) and CocoaPods (SPM-ready for Capacitor 8)
|
|
53
54
|
- **Same JavaScript API** - Compatible interface with paid alternatives
|
|
55
|
+
- **Support player notification center** - Play, pause, show cover for your user when long playing audio.
|
|
54
56
|
|
|
55
57
|
Perfect for music players, podcast apps, games, meditation apps, and any audio-heavy application.
|
|
56
58
|
|
|
@@ -130,6 +132,70 @@ No configuration required for this plugin.
|
|
|
130
132
|
|
|
131
133
|
[Example repository](https://github.com/bazuka5801/native-audio-example)
|
|
132
134
|
|
|
135
|
+
### Notification Center Display (iOS & Android)
|
|
136
|
+
|
|
137
|
+
You can display audio playback information in the system notification center. This is perfect for music players, podcast apps, and any app that plays audio in the background.
|
|
138
|
+
|
|
139
|
+
**Step 1: Configure the plugin with notification support**
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
import { NativeAudio } from '@capgo/native-audio'
|
|
143
|
+
|
|
144
|
+
// Enable notification center display
|
|
145
|
+
await NativeAudio.configure({
|
|
146
|
+
showNotification: true,
|
|
147
|
+
background: true // Also enable background playback
|
|
148
|
+
});
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Step 2: Preload audio with metadata**
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
await NativeAudio.preload({
|
|
155
|
+
assetId: 'song1',
|
|
156
|
+
assetPath: 'https://example.com/song.mp3',
|
|
157
|
+
isUrl: true,
|
|
158
|
+
notificationMetadata: {
|
|
159
|
+
title: 'My Song Title',
|
|
160
|
+
artist: 'Artist Name',
|
|
161
|
+
album: 'Album Name',
|
|
162
|
+
artworkUrl: 'https://example.com/artwork.jpg' // Can be local or remote URL
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**Step 3: Play the audio**
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
// When you play the audio, it will automatically appear in the notification center
|
|
171
|
+
await NativeAudio.play({ assetId: 'song1' });
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
The notification will:
|
|
175
|
+
- Show the title, artist, and album information
|
|
176
|
+
- Display the artwork/album art (if provided)
|
|
177
|
+
- Include media controls (play/pause/stop buttons)
|
|
178
|
+
- Automatically update when audio is paused/resumed
|
|
179
|
+
- Automatically clear when audio is stopped
|
|
180
|
+
- Work on both iOS and Android
|
|
181
|
+
|
|
182
|
+
**Media Controls:**
|
|
183
|
+
Users can control playback directly from:
|
|
184
|
+
- iOS: Control Center, Lock Screen, CarPlay
|
|
185
|
+
- Android: Notification tray, Lock Screen, Android Auto
|
|
186
|
+
|
|
187
|
+
The media control buttons automatically handle:
|
|
188
|
+
- **Play** - Resumes paused audio
|
|
189
|
+
- **Pause** - Pauses playing audio
|
|
190
|
+
- **Stop** - Stops audio and clears the notification
|
|
191
|
+
|
|
192
|
+
**Notes:**
|
|
193
|
+
- All metadata fields are optional
|
|
194
|
+
- Artwork can be a local file path or remote URL
|
|
195
|
+
- The notification only appears when `showNotification: true` is set in configure()
|
|
196
|
+
- iOS: Uses MPNowPlayingInfoCenter with MPRemoteCommandCenter
|
|
197
|
+
- Android: Uses MediaSession with NotificationCompat.MediaStyle
|
|
198
|
+
|
|
133
199
|
## Example app
|
|
134
200
|
|
|
135
201
|
This repository now ships with an interactive Capacitor project under `example/` that exercises the main APIs on web, iOS, and Android shells.
|
|
@@ -608,23 +674,35 @@ Use this when you need to ensure compatibility with other audio plugins
|
|
|
608
674
|
|
|
609
675
|
#### ConfigureOptions
|
|
610
676
|
|
|
611
|
-
| Prop
|
|
612
|
-
|
|
|
613
|
-
| **`fade`**
|
|
614
|
-
| **`focus`**
|
|
615
|
-
| **`background`**
|
|
616
|
-
| **`ignoreSilent`**
|
|
677
|
+
| Prop | Type | Description |
|
|
678
|
+
| ---------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
679
|
+
| **`fade`** | <code>boolean</code> | Play the audio with Fade effect, only available for IOS |
|
|
680
|
+
| **`focus`** | <code>boolean</code> | focus the audio with Audio Focus |
|
|
681
|
+
| **`background`** | <code>boolean</code> | Play the audio in the background |
|
|
682
|
+
| **`ignoreSilent`** | <code>boolean</code> | Ignore silent mode, works only on iOS setting this will nuke other audio apps |
|
|
683
|
+
| **`showNotification`** | <code>boolean</code> | Show audio playback in the notification center (iOS and Android) When enabled, displays audio metadata (title, artist, album, artwork) in the system notification |
|
|
617
684
|
|
|
618
685
|
|
|
619
686
|
#### PreloadOptions
|
|
620
687
|
|
|
621
|
-
| Prop
|
|
622
|
-
|
|
|
623
|
-
| **`assetPath`**
|
|
624
|
-
| **`assetId`**
|
|
625
|
-
| **`volume`**
|
|
626
|
-
| **`audioChannelNum`**
|
|
627
|
-
| **`isUrl`**
|
|
688
|
+
| Prop | Type | Description |
|
|
689
|
+
| -------------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
690
|
+
| **`assetPath`** | <code>string</code> | Path to the audio file, relative path of the file, absolute url (file://) or remote url (https://) Supported formats: - MP3, WAV (all platforms) - M3U8/HLS streams (iOS and Android) |
|
|
691
|
+
| **`assetId`** | <code>string</code> | Asset Id, unique identifier of the file |
|
|
692
|
+
| **`volume`** | <code>number</code> | Volume of the audio, between 0.1 and 1.0 |
|
|
693
|
+
| **`audioChannelNum`** | <code>number</code> | Audio channel number, default is 1 |
|
|
694
|
+
| **`isUrl`** | <code>boolean</code> | Is the audio file a URL, pass true if assetPath is a `file://` url or a streaming URL (m3u8) |
|
|
695
|
+
| **`notificationMetadata`** | <code><a href="#notificationmetadata">NotificationMetadata</a></code> | Metadata to display in the notification center when audio is playing Only used when showNotification is enabled in configure() |
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
#### NotificationMetadata
|
|
699
|
+
|
|
700
|
+
| Prop | Type | Description |
|
|
701
|
+
| ---------------- | ------------------- | ----------------------------------------------------- |
|
|
702
|
+
| **`title`** | <code>string</code> | The title to display in the notification center |
|
|
703
|
+
| **`artist`** | <code>string</code> | The artist name to display in the notification center |
|
|
704
|
+
| **`album`** | <code>string</code> | The album name to display in the notification center |
|
|
705
|
+
| **`artworkUrl`** | <code>string</code> | URL or local path to the artwork/album art image |
|
|
628
706
|
|
|
629
707
|
|
|
630
708
|
#### Assets
|
package/android/build.gradle
CHANGED
|
@@ -62,4 +62,7 @@ dependencies {
|
|
|
62
62
|
implementation 'androidx.media3:media3-ui:1.5.1'
|
|
63
63
|
implementation 'androidx.media3:media3-database:1.5.1'
|
|
64
64
|
implementation 'androidx.media3:media3-common:1.8.0'
|
|
65
|
+
// Media notification support
|
|
66
|
+
implementation 'androidx.media:media:1.7.0'
|
|
67
|
+
implementation 'androidx.core:core:1.13.1'
|
|
65
68
|
}
|
|
@@ -16,6 +16,8 @@ public class Constant {
|
|
|
16
16
|
public static final String RATE = "rate";
|
|
17
17
|
public static final String AUDIO_CHANNEL_NUM = "audioChannelNum";
|
|
18
18
|
public static final String LOOP = "loop";
|
|
19
|
+
public static final String SHOW_NOTIFICATION = "showNotification";
|
|
20
|
+
public static final String NOTIFICATION_METADATA = "notificationMetadata";
|
|
19
21
|
public static final int INVALID = 0;
|
|
20
22
|
public static final int PREPARED = 1;
|
|
21
23
|
public static final int PENDING_PLAY = 2;
|
|
@@ -9,21 +9,32 @@ import static ee.forgr.audio.Constant.ERROR_AUDIO_ASSET_MISSING;
|
|
|
9
9
|
import static ee.forgr.audio.Constant.ERROR_AUDIO_EXISTS;
|
|
10
10
|
import static ee.forgr.audio.Constant.ERROR_AUDIO_ID_MISSING;
|
|
11
11
|
import static ee.forgr.audio.Constant.LOOP;
|
|
12
|
+
import static ee.forgr.audio.Constant.NOTIFICATION_METADATA;
|
|
12
13
|
import static ee.forgr.audio.Constant.OPT_FOCUS_AUDIO;
|
|
13
14
|
import static ee.forgr.audio.Constant.RATE;
|
|
15
|
+
import static ee.forgr.audio.Constant.SHOW_NOTIFICATION;
|
|
14
16
|
import static ee.forgr.audio.Constant.VOLUME;
|
|
15
17
|
|
|
16
18
|
import android.Manifest;
|
|
19
|
+
import android.app.NotificationChannel;
|
|
20
|
+
import android.app.NotificationManager;
|
|
17
21
|
import android.content.Context;
|
|
18
22
|
import android.content.res.AssetFileDescriptor;
|
|
19
23
|
import android.content.res.AssetManager;
|
|
24
|
+
import android.graphics.Bitmap;
|
|
25
|
+
import android.graphics.BitmapFactory;
|
|
20
26
|
import android.media.AudioManager;
|
|
21
27
|
import android.net.Uri;
|
|
22
28
|
import android.os.Build;
|
|
23
29
|
import android.os.Handler;
|
|
24
30
|
import android.os.Looper;
|
|
25
31
|
import android.os.ParcelFileDescriptor;
|
|
32
|
+
import android.support.v4.media.MediaMetadataCompat;
|
|
33
|
+
import android.support.v4.media.session.MediaSessionCompat;
|
|
34
|
+
import android.support.v4.media.session.PlaybackStateCompat;
|
|
26
35
|
import android.util.Log;
|
|
36
|
+
import androidx.core.app.NotificationCompat;
|
|
37
|
+
import androidx.core.app.NotificationManagerCompat;
|
|
27
38
|
import androidx.media3.common.util.UnstableApi;
|
|
28
39
|
import com.getcapacitor.JSObject;
|
|
29
40
|
import com.getcapacitor.Plugin;
|
|
@@ -64,6 +75,14 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
64
75
|
|
|
65
76
|
private final Map<String, PluginCall> pendingDurationCalls = new HashMap<>();
|
|
66
77
|
|
|
78
|
+
// Notification center support
|
|
79
|
+
private boolean showNotification = false;
|
|
80
|
+
private Map<String, Map<String, String>> notificationMetadataMap = new HashMap<>();
|
|
81
|
+
private MediaSessionCompat mediaSession;
|
|
82
|
+
private String currentlyPlayingAssetId;
|
|
83
|
+
private static final int NOTIFICATION_ID = 1001;
|
|
84
|
+
private static final String CHANNEL_ID = "native_audio_channel";
|
|
85
|
+
|
|
67
86
|
@Override
|
|
68
87
|
public void load() {
|
|
69
88
|
super.load();
|
|
@@ -168,6 +187,7 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
168
187
|
boolean focus = call.getBoolean(OPT_FOCUS_AUDIO, false);
|
|
169
188
|
boolean background = call.getBoolean("background", false);
|
|
170
189
|
this.fadeMusic = call.getBoolean("fade", false);
|
|
190
|
+
this.showNotification = call.getBoolean(SHOW_NOTIFICATION, false);
|
|
171
191
|
|
|
172
192
|
try {
|
|
173
193
|
if (focus) {
|
|
@@ -189,6 +209,11 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
189
209
|
} else {
|
|
190
210
|
this.audioManager.setMode(AudioManager.MODE_NORMAL);
|
|
191
211
|
}
|
|
212
|
+
|
|
213
|
+
if (this.showNotification) {
|
|
214
|
+
setupMediaSession();
|
|
215
|
+
createNotificationChannel();
|
|
216
|
+
}
|
|
192
217
|
} catch (Exception ex) {
|
|
193
218
|
Log.e(TAG, "Error configuring audio", ex);
|
|
194
219
|
}
|
|
@@ -324,6 +349,12 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
324
349
|
if (wasPlaying) {
|
|
325
350
|
resumeList.add(asset);
|
|
326
351
|
}
|
|
352
|
+
|
|
353
|
+
// Update notification when paused
|
|
354
|
+
if (showNotification) {
|
|
355
|
+
updatePlaybackState(PlaybackStateCompat.STATE_PAUSED);
|
|
356
|
+
}
|
|
357
|
+
|
|
327
358
|
call.resolve();
|
|
328
359
|
} else {
|
|
329
360
|
call.reject(ERROR_ASSET_NOT_LOADED + " - " + audioId);
|
|
@@ -347,6 +378,12 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
347
378
|
if (asset != null) {
|
|
348
379
|
asset.resume();
|
|
349
380
|
resumeList.add(asset);
|
|
381
|
+
|
|
382
|
+
// Update notification when resumed
|
|
383
|
+
if (showNotification) {
|
|
384
|
+
updatePlaybackState(PlaybackStateCompat.STATE_PLAYING);
|
|
385
|
+
}
|
|
386
|
+
|
|
350
387
|
call.resolve();
|
|
351
388
|
} else {
|
|
352
389
|
call.reject(ERROR_ASSET_NOT_LOADED + " - " + audioId);
|
|
@@ -372,6 +409,13 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
372
409
|
return;
|
|
373
410
|
}
|
|
374
411
|
stopAudio(audioId);
|
|
412
|
+
|
|
413
|
+
// Clear notification when stopped
|
|
414
|
+
if (showNotification) {
|
|
415
|
+
clearNotification();
|
|
416
|
+
currentlyPlayingAssetId = null;
|
|
417
|
+
}
|
|
418
|
+
|
|
375
419
|
call.resolve();
|
|
376
420
|
} catch (Exception ex) {
|
|
377
421
|
call.reject(ex.getMessage());
|
|
@@ -581,6 +625,19 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
581
625
|
audioChannelNum = call.getInt(AUDIO_CHANNEL_NUM, 1);
|
|
582
626
|
}
|
|
583
627
|
|
|
628
|
+
// Store notification metadata if provided
|
|
629
|
+
JSObject metadata = call.getObject(NOTIFICATION_METADATA);
|
|
630
|
+
if (metadata != null) {
|
|
631
|
+
Map<String, String> metadataMap = new HashMap<>();
|
|
632
|
+
if (metadata.has("title")) metadataMap.put("title", metadata.getString("title"));
|
|
633
|
+
if (metadata.has("artist")) metadataMap.put("artist", metadata.getString("artist"));
|
|
634
|
+
if (metadata.has("album")) metadataMap.put("album", metadata.getString("album"));
|
|
635
|
+
if (metadata.has("artworkUrl")) metadataMap.put("artworkUrl", metadata.getString("artworkUrl"));
|
|
636
|
+
if (!metadataMap.isEmpty()) {
|
|
637
|
+
notificationMetadataMap.put(audioId, metadataMap);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
584
641
|
if (isLocalUrl) {
|
|
585
642
|
try {
|
|
586
643
|
Uri uri = Uri.parse(assetPath);
|
|
@@ -666,6 +723,13 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
666
723
|
asset.play(time);
|
|
667
724
|
}
|
|
668
725
|
}
|
|
726
|
+
|
|
727
|
+
// Update notification if enabled
|
|
728
|
+
if (showNotification) {
|
|
729
|
+
currentlyPlayingAssetId = audioId;
|
|
730
|
+
updateNotification(audioId);
|
|
731
|
+
}
|
|
732
|
+
|
|
669
733
|
call.resolve();
|
|
670
734
|
} else {
|
|
671
735
|
call.reject("Asset is null: " + audioId);
|
|
@@ -746,6 +810,15 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
746
810
|
}
|
|
747
811
|
}
|
|
748
812
|
|
|
813
|
+
// Clear notification and release media session
|
|
814
|
+
if (showNotification) {
|
|
815
|
+
clearNotification();
|
|
816
|
+
if (mediaSession != null) {
|
|
817
|
+
mediaSession.release();
|
|
818
|
+
mediaSession = null;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
749
822
|
// Release audio focus if we requested it
|
|
750
823
|
if (audioFocusRequested && this.audioManager != null) {
|
|
751
824
|
this.audioManager.abandonAudioFocus(this);
|
|
@@ -764,4 +837,182 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
764
837
|
call.reject("Error deinitializing plugin: " + e.getMessage());
|
|
765
838
|
}
|
|
766
839
|
}
|
|
840
|
+
|
|
841
|
+
// Notification and MediaSession methods
|
|
842
|
+
|
|
843
|
+
private void setupMediaSession() {
|
|
844
|
+
if (mediaSession != null) return;
|
|
845
|
+
|
|
846
|
+
mediaSession = new MediaSessionCompat(getContext(), "NativeAudio");
|
|
847
|
+
|
|
848
|
+
mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
|
|
849
|
+
|
|
850
|
+
PlaybackStateCompat.Builder stateBuilder = new PlaybackStateCompat.Builder().setActions(
|
|
851
|
+
PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_STOP
|
|
852
|
+
);
|
|
853
|
+
mediaSession.setPlaybackState(stateBuilder.build());
|
|
854
|
+
|
|
855
|
+
// Set callback for media button events
|
|
856
|
+
mediaSession.setCallback(
|
|
857
|
+
new MediaSessionCompat.Callback() {
|
|
858
|
+
@Override
|
|
859
|
+
public void onPlay() {
|
|
860
|
+
if (currentlyPlayingAssetId != null && audioAssetList.containsKey(currentlyPlayingAssetId)) {
|
|
861
|
+
AudioAsset asset = audioAssetList.get(currentlyPlayingAssetId);
|
|
862
|
+
try {
|
|
863
|
+
if (asset != null && !asset.isPlaying()) {
|
|
864
|
+
asset.resume();
|
|
865
|
+
updatePlaybackState(PlaybackStateCompat.STATE_PLAYING);
|
|
866
|
+
}
|
|
867
|
+
} catch (Exception e) {
|
|
868
|
+
Log.e(TAG, "Error resuming audio from media session", e);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
@Override
|
|
874
|
+
public void onPause() {
|
|
875
|
+
if (currentlyPlayingAssetId != null && audioAssetList.containsKey(currentlyPlayingAssetId)) {
|
|
876
|
+
AudioAsset asset = audioAssetList.get(currentlyPlayingAssetId);
|
|
877
|
+
try {
|
|
878
|
+
if (asset != null) {
|
|
879
|
+
asset.pause();
|
|
880
|
+
updatePlaybackState(PlaybackStateCompat.STATE_PAUSED);
|
|
881
|
+
}
|
|
882
|
+
} catch (Exception e) {
|
|
883
|
+
Log.e(TAG, "Error pausing audio from media session", e);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
@Override
|
|
889
|
+
public void onStop() {
|
|
890
|
+
if (currentlyPlayingAssetId != null) {
|
|
891
|
+
try {
|
|
892
|
+
stopAudio(currentlyPlayingAssetId);
|
|
893
|
+
clearNotification();
|
|
894
|
+
currentlyPlayingAssetId = null;
|
|
895
|
+
} catch (Exception e) {
|
|
896
|
+
Log.e(TAG, "Error stopping audio from media session", e);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
);
|
|
902
|
+
|
|
903
|
+
mediaSession.setActive(true);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
private void createNotificationChannel() {
|
|
907
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
908
|
+
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "Audio Playback", NotificationManager.IMPORTANCE_LOW);
|
|
909
|
+
channel.setDescription("Shows currently playing audio");
|
|
910
|
+
NotificationManager notificationManager = getContext().getSystemService(NotificationManager.class);
|
|
911
|
+
if (notificationManager != null) {
|
|
912
|
+
notificationManager.createNotificationChannel(channel);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
private void updateNotification(String audioId) {
|
|
918
|
+
if (mediaSession == null) return;
|
|
919
|
+
|
|
920
|
+
Map<String, String> metadata = notificationMetadataMap.get(audioId);
|
|
921
|
+
String title = metadata != null && metadata.containsKey("title") ? metadata.get("title") : "Playing";
|
|
922
|
+
String artist = metadata != null && metadata.containsKey("artist") ? metadata.get("artist") : "";
|
|
923
|
+
String album = metadata != null && metadata.containsKey("album") ? metadata.get("album") : "";
|
|
924
|
+
String artworkUrl = metadata != null ? metadata.get("artworkUrl") : null;
|
|
925
|
+
|
|
926
|
+
// Update MediaSession metadata
|
|
927
|
+
MediaMetadataCompat.Builder metadataBuilder = new MediaMetadataCompat.Builder();
|
|
928
|
+
metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title);
|
|
929
|
+
metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artist);
|
|
930
|
+
metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, album);
|
|
931
|
+
|
|
932
|
+
// Load artwork if provided
|
|
933
|
+
if (artworkUrl != null) {
|
|
934
|
+
loadArtwork(artworkUrl, (bitmap) -> {
|
|
935
|
+
if (bitmap != null) {
|
|
936
|
+
metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, bitmap);
|
|
937
|
+
}
|
|
938
|
+
mediaSession.setMetadata(metadataBuilder.build());
|
|
939
|
+
showNotification(title, artist);
|
|
940
|
+
});
|
|
941
|
+
} else {
|
|
942
|
+
mediaSession.setMetadata(metadataBuilder.build());
|
|
943
|
+
showNotification(title, artist);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
updatePlaybackState(PlaybackStateCompat.STATE_PLAYING);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
private void showNotification(String title, String artist) {
|
|
950
|
+
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(getContext(), CHANNEL_ID)
|
|
951
|
+
.setSmallIcon(android.R.drawable.ic_media_play)
|
|
952
|
+
.setContentTitle(title)
|
|
953
|
+
.setContentText(artist)
|
|
954
|
+
.setStyle(
|
|
955
|
+
new androidx.media.app.NotificationCompat.MediaStyle()
|
|
956
|
+
.setMediaSession(mediaSession.getSessionToken())
|
|
957
|
+
.setShowActionsInCompactView(0, 1, 2)
|
|
958
|
+
)
|
|
959
|
+
.addAction(android.R.drawable.ic_media_previous, "Previous", null)
|
|
960
|
+
.addAction(android.R.drawable.ic_media_pause, "Pause", null)
|
|
961
|
+
.addAction(android.R.drawable.ic_media_next, "Next", null)
|
|
962
|
+
.setPriority(NotificationCompat.PRIORITY_LOW)
|
|
963
|
+
.setOnlyAlertOnce(true);
|
|
964
|
+
|
|
965
|
+
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(getContext());
|
|
966
|
+
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
private void clearNotification() {
|
|
970
|
+
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(getContext());
|
|
971
|
+
notificationManager.cancel(NOTIFICATION_ID);
|
|
972
|
+
|
|
973
|
+
if (mediaSession != null) {
|
|
974
|
+
updatePlaybackState(PlaybackStateCompat.STATE_STOPPED);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
private void updatePlaybackState(int state) {
|
|
979
|
+
if (mediaSession == null) return;
|
|
980
|
+
|
|
981
|
+
PlaybackStateCompat.Builder stateBuilder = new PlaybackStateCompat.Builder()
|
|
982
|
+
.setState(state, 0, state == PlaybackStateCompat.STATE_PLAYING ? 1.0f : 0.0f)
|
|
983
|
+
.setActions(PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_STOP);
|
|
984
|
+
mediaSession.setPlaybackState(stateBuilder.build());
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
private void loadArtwork(String urlString, ArtworkCallback callback) {
|
|
988
|
+
new Thread(() -> {
|
|
989
|
+
try {
|
|
990
|
+
Uri uri = Uri.parse(urlString);
|
|
991
|
+
Bitmap bitmap = null;
|
|
992
|
+
|
|
993
|
+
if (uri.getScheme() == null || uri.getScheme().equals("file")) {
|
|
994
|
+
// Local file
|
|
995
|
+
File file = new File(uri.getPath());
|
|
996
|
+
if (file.exists()) {
|
|
997
|
+
bitmap = BitmapFactory.decodeFile(file.getAbsolutePath());
|
|
998
|
+
}
|
|
999
|
+
} else {
|
|
1000
|
+
// Remote URL
|
|
1001
|
+
URL url = new URL(urlString);
|
|
1002
|
+
bitmap = BitmapFactory.decodeStream(url.openConnection().getInputStream());
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
Bitmap finalBitmap = bitmap;
|
|
1006
|
+
new Handler(Looper.getMainLooper()).post(() -> callback.onArtworkLoaded(finalBitmap));
|
|
1007
|
+
} catch (Exception e) {
|
|
1008
|
+
Log.e(TAG, "Error loading artwork", e);
|
|
1009
|
+
new Handler(Looper.getMainLooper()).post(() -> callback.onArtworkLoaded(null));
|
|
1010
|
+
}
|
|
1011
|
+
})
|
|
1012
|
+
.start();
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
interface ArtworkCallback {
|
|
1016
|
+
void onArtworkLoaded(Bitmap bitmap);
|
|
1017
|
+
}
|
|
767
1018
|
}
|
package/dist/docs.json
CHANGED
|
@@ -656,6 +656,13 @@
|
|
|
656
656
|
"docs": "Ignore silent mode, works only on iOS setting this will nuke other audio apps",
|
|
657
657
|
"complexTypes": [],
|
|
658
658
|
"type": "boolean | undefined"
|
|
659
|
+
},
|
|
660
|
+
{
|
|
661
|
+
"name": "showNotification",
|
|
662
|
+
"tags": [],
|
|
663
|
+
"docs": "Show audio playback in the notification center (iOS and Android)\nWhen enabled, displays audio metadata (title, artist, album, artwork) in the system notification",
|
|
664
|
+
"complexTypes": [],
|
|
665
|
+
"type": "boolean | undefined"
|
|
659
666
|
}
|
|
660
667
|
]
|
|
661
668
|
},
|
|
@@ -700,6 +707,52 @@
|
|
|
700
707
|
"docs": "Is the audio file a URL, pass true if assetPath is a `file://` url\nor a streaming URL (m3u8)",
|
|
701
708
|
"complexTypes": [],
|
|
702
709
|
"type": "boolean | undefined"
|
|
710
|
+
},
|
|
711
|
+
{
|
|
712
|
+
"name": "notificationMetadata",
|
|
713
|
+
"tags": [],
|
|
714
|
+
"docs": "Metadata to display in the notification center when audio is playing\nOnly used when showNotification is enabled in configure()",
|
|
715
|
+
"complexTypes": [
|
|
716
|
+
"NotificationMetadata"
|
|
717
|
+
],
|
|
718
|
+
"type": "NotificationMetadata"
|
|
719
|
+
}
|
|
720
|
+
]
|
|
721
|
+
},
|
|
722
|
+
{
|
|
723
|
+
"name": "NotificationMetadata",
|
|
724
|
+
"slug": "notificationmetadata",
|
|
725
|
+
"docs": "",
|
|
726
|
+
"tags": [],
|
|
727
|
+
"methods": [],
|
|
728
|
+
"properties": [
|
|
729
|
+
{
|
|
730
|
+
"name": "title",
|
|
731
|
+
"tags": [],
|
|
732
|
+
"docs": "The title to display in the notification center",
|
|
733
|
+
"complexTypes": [],
|
|
734
|
+
"type": "string | undefined"
|
|
735
|
+
},
|
|
736
|
+
{
|
|
737
|
+
"name": "artist",
|
|
738
|
+
"tags": [],
|
|
739
|
+
"docs": "The artist name to display in the notification center",
|
|
740
|
+
"complexTypes": [],
|
|
741
|
+
"type": "string | undefined"
|
|
742
|
+
},
|
|
743
|
+
{
|
|
744
|
+
"name": "album",
|
|
745
|
+
"tags": [],
|
|
746
|
+
"docs": "The album name to display in the notification center",
|
|
747
|
+
"complexTypes": [],
|
|
748
|
+
"type": "string | undefined"
|
|
749
|
+
},
|
|
750
|
+
{
|
|
751
|
+
"name": "artworkUrl",
|
|
752
|
+
"tags": [],
|
|
753
|
+
"docs": "URL or local path to the artwork/album art image",
|
|
754
|
+
"complexTypes": [],
|
|
755
|
+
"type": "string | undefined"
|
|
703
756
|
}
|
|
704
757
|
]
|
|
705
758
|
},
|
|
@@ -65,6 +65,29 @@ export interface ConfigureOptions {
|
|
|
65
65
|
* Ignore silent mode, works only on iOS setting this will nuke other audio apps
|
|
66
66
|
*/
|
|
67
67
|
ignoreSilent?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Show audio playback in the notification center (iOS and Android)
|
|
70
|
+
* When enabled, displays audio metadata (title, artist, album, artwork) in the system notification
|
|
71
|
+
*/
|
|
72
|
+
showNotification?: boolean;
|
|
73
|
+
}
|
|
74
|
+
export interface NotificationMetadata {
|
|
75
|
+
/**
|
|
76
|
+
* The title to display in the notification center
|
|
77
|
+
*/
|
|
78
|
+
title?: string;
|
|
79
|
+
/**
|
|
80
|
+
* The artist name to display in the notification center
|
|
81
|
+
*/
|
|
82
|
+
artist?: string;
|
|
83
|
+
/**
|
|
84
|
+
* The album name to display in the notification center
|
|
85
|
+
*/
|
|
86
|
+
album?: string;
|
|
87
|
+
/**
|
|
88
|
+
* URL or local path to the artwork/album art image
|
|
89
|
+
*/
|
|
90
|
+
artworkUrl?: string;
|
|
68
91
|
}
|
|
69
92
|
export interface PreloadOptions {
|
|
70
93
|
/**
|
|
@@ -91,6 +114,11 @@ export interface PreloadOptions {
|
|
|
91
114
|
* or a streaming URL (m3u8)
|
|
92
115
|
*/
|
|
93
116
|
isUrl?: boolean;
|
|
117
|
+
/**
|
|
118
|
+
* Metadata to display in the notification center when audio is playing
|
|
119
|
+
* Only used when showNotification is enabled in configure()
|
|
120
|
+
*/
|
|
121
|
+
notificationMetadata?: NotificationMetadata;
|
|
94
122
|
}
|
|
95
123
|
export interface CurrentTimeEvent {
|
|
96
124
|
/**
|
|
@@ -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}\nexport type CompletedListener = (state: CompletedEvent) => void;\nexport interface Assets {\n /**\n * Asset Id, unique identifier of the file\n */\n assetId: string;\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\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 AssetPlayOptions {\n /**\n * Asset Id, unique identifier of the file\n */\n assetId: string;\n /**\n * Time to start playing the audio, in milliseconds\n */\n time?: number;\n /**\n * Delay to start playing the audio, in milliseconds\n */\n delay?: number;\n}\n\nexport interface ConfigureOptions {\n /**\n * Play the audio with Fade effect, only available for IOS\n */\n fade?: boolean;\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\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\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}\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 * 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 * 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 * Play an audio file\n * @since 5.0.0\n * @param option {@link PlayOptions}\n * @returns\n */\n play(options: { assetId: string; time?: number; delay?: number }): Promise<void>;\n /**\n * Pause an audio file\n * @since 5.0.0\n * @param option {@link Assets}\n * @returns\n */\n pause(options: Assets): Promise<void>;\n /**\n * Resume an audio file\n * @since 5.0.0\n * @param option {@link Assets}\n * @returns\n */\n resume(options: Assets): Promise<void>;\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 * Stop an audio file\n * @since 5.0.0\n * @param option {@link Assets}\n * @returns\n */\n stop(options: Assets): Promise<void>;\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 * 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: { assetId: string; volume: number }): Promise<void>;\n /**\n * Set the rate of an audio file\n * @since 5.0.0\n * @param option {@link AssetPlayOptions}\n * @returns {Promise<void>}\n */\n setRate(options: { assetId: string; rate: number }): Promise<void>;\n /**\n * Set the current time of an audio file\n * @since 6.5.0\n * @param option {@link AssetPlayOptions}\n * @returns {Promise<void>}\n */\n setCurrentTime(options: { assetId: string; time: number }): Promise<void>;\n /**\n * Get the current time of an audio file\n * @since 5.0.0\n * @param option {@link AssetPlayOptions}\n * @returns {Promise<{ currentTime: number }>}\n */\n getCurrentTime(options: { assetId: string }): Promise<{ currentTime: number }>;\n /**\n * Get the duration of an audio file\n * @since 5.0.0\n * @param option {@link AssetPlayOptions}\n * @returns {Promise<{ duration: number }>}\n */\n getDuration(options: Assets): Promise<{ duration: number }>;\n /**\n * Check if an audio file is playing\n *\n * @since 5.0.0\n * @param option {@link AssetPlayOptions}\n * @returns {Promise<boolean>}\n */\n isPlaying(options: Assets): Promise<{ isPlaying: boolean }>;\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 * 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 * 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}\nexport type CompletedListener = (state: CompletedEvent) => void;\nexport interface Assets {\n /**\n * Asset Id, unique identifier of the file\n */\n assetId: string;\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\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 AssetPlayOptions {\n /**\n * Asset Id, unique identifier of the file\n */\n assetId: string;\n /**\n * Time to start playing the audio, in milliseconds\n */\n time?: number;\n /**\n * Delay to start playing the audio, in milliseconds\n */\n delay?: number;\n}\n\nexport interface ConfigureOptions {\n /**\n * Play the audio with Fade effect, only available for IOS\n */\n fade?: boolean;\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 */\n showNotification?: boolean;\n}\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 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 is enabled in configure()\n */\n notificationMetadata?: NotificationMetadata;\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}\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 * 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 * 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 * Play an audio file\n * @since 5.0.0\n * @param option {@link PlayOptions}\n * @returns\n */\n play(options: { assetId: string; time?: number; delay?: number }): Promise<void>;\n /**\n * Pause an audio file\n * @since 5.0.0\n * @param option {@link Assets}\n * @returns\n */\n pause(options: Assets): Promise<void>;\n /**\n * Resume an audio file\n * @since 5.0.0\n * @param option {@link Assets}\n * @returns\n */\n resume(options: Assets): Promise<void>;\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 * Stop an audio file\n * @since 5.0.0\n * @param option {@link Assets}\n * @returns\n */\n stop(options: Assets): Promise<void>;\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 * 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: { assetId: string; volume: number }): Promise<void>;\n /**\n * Set the rate of an audio file\n * @since 5.0.0\n * @param option {@link AssetPlayOptions}\n * @returns {Promise<void>}\n */\n setRate(options: { assetId: string; rate: number }): Promise<void>;\n /**\n * Set the current time of an audio file\n * @since 6.5.0\n * @param option {@link AssetPlayOptions}\n * @returns {Promise<void>}\n */\n setCurrentTime(options: { assetId: string; time: number }): Promise<void>;\n /**\n * Get the current time of an audio file\n * @since 5.0.0\n * @param option {@link AssetPlayOptions}\n * @returns {Promise<{ currentTime: number }>}\n */\n getCurrentTime(options: { assetId: string }): Promise<{ currentTime: number }>;\n /**\n * Get the duration of an audio file\n * @since 5.0.0\n * @param option {@link AssetPlayOptions}\n * @returns {Promise<{ duration: number }>}\n */\n getDuration(options: Assets): Promise<{ duration: number }>;\n /**\n * Check if an audio file is playing\n *\n * @since 5.0.0\n * @param option {@link AssetPlayOptions}\n * @returns {Promise<boolean>}\n */\n isPlaying(options: Assets): Promise<{ isPlaying: boolean }>;\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 * 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 * 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"]}
|
|
@@ -26,7 +26,7 @@ public class AudioAsset: NSObject, AVAudioPlayerDelegate {
|
|
|
26
26
|
let FADEDELAY: Float = 0.08
|
|
27
27
|
|
|
28
28
|
// Maximum number of channels to prevent excessive resource usage
|
|
29
|
-
private let
|
|
29
|
+
private let maxChannels = Constant.MaxChannels
|
|
30
30
|
|
|
31
31
|
private var currentTimeTimer: Timer?
|
|
32
32
|
internal var fadeTimer: Timer?
|
|
@@ -65,7 +65,7 @@ public class AudioAsset: NSObject, AVAudioPlayerDelegate {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
// Limit channels to a reasonable maximum to prevent resource issues
|
|
68
|
-
let channelCount = min(max(channels ?? 1, 1),
|
|
68
|
+
let channelCount = min(max(channels ?? 1, 1), maxChannels)
|
|
69
69
|
|
|
70
70
|
owner.executeOnAudioQueue { [weak self] in
|
|
71
71
|
guard let self = self else { return }
|
|
@@ -17,6 +17,8 @@ public class Constant {
|
|
|
17
17
|
public static let Loop = "loop"
|
|
18
18
|
public static let Background = "background"
|
|
19
19
|
public static let IgnoreSilent = "ignoreSilent"
|
|
20
|
+
public static let ShowNotification = "showNotification"
|
|
21
|
+
public static let NotificationMetadata = "notificationMetadata"
|
|
20
22
|
|
|
21
23
|
// Default values - used for consistency across the plugin
|
|
22
24
|
public static let DefaultVolume: Float = 1.0
|
|
@@ -2,6 +2,7 @@ import AVFoundation
|
|
|
2
2
|
import Capacitor
|
|
3
3
|
import CoreAudio
|
|
4
4
|
import Foundation
|
|
5
|
+
import MediaPlayer
|
|
5
6
|
|
|
6
7
|
enum MyError: Error {
|
|
7
8
|
case runtimeError(String)
|
|
@@ -9,9 +10,10 @@ enum MyError: Error {
|
|
|
9
10
|
|
|
10
11
|
/// Please read the Capacitor iOS Plugin Development Guide
|
|
11
12
|
/// here: https://capacitor.ionicframework.com/docs/plugins/ios
|
|
13
|
+
// swiftlint:disable type_body_length file_length
|
|
12
14
|
@objc(NativeAudio)
|
|
13
15
|
public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
14
|
-
private let pluginVersion: String = "7.
|
|
16
|
+
private let pluginVersion: String = "7.9.0"
|
|
15
17
|
public let identifier = "NativeAudio"
|
|
16
18
|
public let jsName = "NativeAudio"
|
|
17
19
|
public let pluginMethods: [CAPPluginMethod] = [
|
|
@@ -54,6 +56,11 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
54
56
|
// Add observer for audio session interruptions
|
|
55
57
|
private var interruptionObserver: Any?
|
|
56
58
|
|
|
59
|
+
// Notification center support
|
|
60
|
+
private var showNotification = false
|
|
61
|
+
private var notificationMetadataMap: [String: [String: String]] = [:]
|
|
62
|
+
private var currentlyPlayingAssetId: String?
|
|
63
|
+
|
|
57
64
|
@objc override public func load() {
|
|
58
65
|
super.load()
|
|
59
66
|
audioQueue.setSpecific(key: queueKey, value: true)
|
|
@@ -63,6 +70,7 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
63
70
|
// Don't setup audio session on load - defer until first use
|
|
64
71
|
// setupAudioSession()
|
|
65
72
|
setupInterruptionHandling()
|
|
73
|
+
setupRemoteCommandCenter()
|
|
66
74
|
|
|
67
75
|
NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: .main) { [weak self] _ in
|
|
68
76
|
guard let strongSelf = self else { return }
|
|
@@ -143,6 +151,89 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
143
151
|
}
|
|
144
152
|
}
|
|
145
153
|
|
|
154
|
+
// swiftlint:disable function_body_length
|
|
155
|
+
private func setupRemoteCommandCenter() {
|
|
156
|
+
let commandCenter = MPRemoteCommandCenter.shared()
|
|
157
|
+
|
|
158
|
+
// Play command
|
|
159
|
+
commandCenter.playCommand.addTarget { [weak self] _ in
|
|
160
|
+
guard let self = self, let assetId = self.currentlyPlayingAssetId else {
|
|
161
|
+
return .noSuchContent
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
self.audioQueue.sync {
|
|
165
|
+
guard let asset = self.audioList[assetId] as? AudioAsset else {
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if !asset.isPlaying() {
|
|
170
|
+
asset.resume()
|
|
171
|
+
self.updatePlaybackState(isPlaying: true)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return .success
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Pause command
|
|
178
|
+
commandCenter.pauseCommand.addTarget { [weak self] _ in
|
|
179
|
+
guard let self = self, let assetId = self.currentlyPlayingAssetId else {
|
|
180
|
+
return .noSuchContent
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
self.audioQueue.sync {
|
|
184
|
+
guard let asset = self.audioList[assetId] as? AudioAsset else {
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
asset.pause()
|
|
189
|
+
self.updatePlaybackState(isPlaying: false)
|
|
190
|
+
}
|
|
191
|
+
return .success
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Stop command
|
|
195
|
+
commandCenter.stopCommand.addTarget { [weak self] _ in
|
|
196
|
+
guard let self = self, let assetId = self.currentlyPlayingAssetId else {
|
|
197
|
+
return .noSuchContent
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
self.audioQueue.sync {
|
|
201
|
+
guard let asset = self.audioList[assetId] as? AudioAsset else {
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
asset.stop()
|
|
206
|
+
self.clearNowPlayingInfo()
|
|
207
|
+
self.currentlyPlayingAssetId = nil
|
|
208
|
+
self.updatePlaybackState(isPlaying: false)
|
|
209
|
+
}
|
|
210
|
+
return .success
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Toggle play/pause command
|
|
214
|
+
commandCenter.togglePlayPauseCommand.addTarget { [weak self] _ in
|
|
215
|
+
guard let self = self, let assetId = self.currentlyPlayingAssetId else {
|
|
216
|
+
return .noSuchContent
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
self.audioQueue.sync {
|
|
220
|
+
guard let asset = self.audioList[assetId] as? AudioAsset else {
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if asset.isPlaying() {
|
|
225
|
+
asset.pause()
|
|
226
|
+
self.updatePlaybackState(isPlaying: false)
|
|
227
|
+
} else {
|
|
228
|
+
asset.resume()
|
|
229
|
+
self.updatePlaybackState(isPlaying: true)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return .success
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// swiftlint:enable function_body_length
|
|
236
|
+
|
|
146
237
|
@objc func configure(_ call: CAPPluginCall) {
|
|
147
238
|
// Save original category on first configure call
|
|
148
239
|
if !audioSessionInitialized {
|
|
@@ -158,6 +249,7 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
158
249
|
let focus = call.getBool(Constant.FocusAudio) ?? false
|
|
159
250
|
let background = call.getBool(Constant.Background) ?? false
|
|
160
251
|
let ignoreSilent = call.getBool(Constant.IgnoreSilent) ?? true
|
|
252
|
+
self.showNotification = call.getBool(Constant.ShowNotification) ?? false
|
|
161
253
|
|
|
162
254
|
// Use a single audio session configuration block for better atomicity
|
|
163
255
|
do {
|
|
@@ -283,6 +375,14 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
283
375
|
} else {
|
|
284
376
|
audioAsset.play(time: time, delay: delay)
|
|
285
377
|
}
|
|
378
|
+
|
|
379
|
+
// Update notification center if enabled
|
|
380
|
+
if self.showNotification {
|
|
381
|
+
self.currentlyPlayingAssetId = audioId
|
|
382
|
+
self.updateNowPlayingInfo(audioId: audioId, audioAsset: audioAsset)
|
|
383
|
+
self.updatePlaybackState(isPlaying: true)
|
|
384
|
+
}
|
|
385
|
+
|
|
286
386
|
call.resolve()
|
|
287
387
|
} else if let audioNumber = asset as? NSNumber {
|
|
288
388
|
self.activateSession()
|
|
@@ -350,6 +450,12 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
350
450
|
}
|
|
351
451
|
self.activateSession()
|
|
352
452
|
audioAsset.resume()
|
|
453
|
+
|
|
454
|
+
// Update notification when resumed
|
|
455
|
+
if self.showNotification {
|
|
456
|
+
self.updatePlaybackState(isPlaying: true)
|
|
457
|
+
}
|
|
458
|
+
|
|
353
459
|
call.resolve()
|
|
354
460
|
}
|
|
355
461
|
}
|
|
@@ -362,6 +468,12 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
362
468
|
}
|
|
363
469
|
|
|
364
470
|
audioAsset.pause()
|
|
471
|
+
|
|
472
|
+
// Update notification when paused
|
|
473
|
+
if self.showNotification {
|
|
474
|
+
self.updatePlaybackState(isPlaying: false)
|
|
475
|
+
}
|
|
476
|
+
|
|
365
477
|
self.endSession()
|
|
366
478
|
call.resolve()
|
|
367
479
|
}
|
|
@@ -378,6 +490,13 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
378
490
|
|
|
379
491
|
do {
|
|
380
492
|
try self.stopAudio(audioId: audioId)
|
|
493
|
+
|
|
494
|
+
// Clear notification when stopped
|
|
495
|
+
if self.showNotification {
|
|
496
|
+
self.clearNowPlayingInfo()
|
|
497
|
+
self.currentlyPlayingAssetId = nil
|
|
498
|
+
}
|
|
499
|
+
|
|
381
500
|
self.endSession()
|
|
382
501
|
call.resolve()
|
|
383
502
|
} catch {
|
|
@@ -468,6 +587,7 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
468
587
|
}
|
|
469
588
|
}
|
|
470
589
|
|
|
590
|
+
// swiftlint:disable cyclomatic_complexity function_body_length
|
|
471
591
|
@objc private func preloadAsset(_ call: CAPPluginCall, isComplex complex: Bool) {
|
|
472
592
|
// Common default values to ensure consistency
|
|
473
593
|
let audioId = call.getString(Constant.AssetIdKey) ?? ""
|
|
@@ -487,6 +607,26 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
487
607
|
return
|
|
488
608
|
}
|
|
489
609
|
|
|
610
|
+
// Store notification metadata if provided
|
|
611
|
+
if let metadata = call.getObject(Constant.NotificationMetadata) {
|
|
612
|
+
var metadataDict: [String: String] = [:]
|
|
613
|
+
if let title = metadata["title"] as? String {
|
|
614
|
+
metadataDict["title"] = title
|
|
615
|
+
}
|
|
616
|
+
if let artist = metadata["artist"] as? String {
|
|
617
|
+
metadataDict["artist"] = artist
|
|
618
|
+
}
|
|
619
|
+
if let album = metadata["album"] as? String {
|
|
620
|
+
metadataDict["album"] = album
|
|
621
|
+
}
|
|
622
|
+
if let artworkUrl = metadata["artworkUrl"] as? String {
|
|
623
|
+
metadataDict["artworkUrl"] = artworkUrl
|
|
624
|
+
}
|
|
625
|
+
if !metadataDict.isEmpty {
|
|
626
|
+
notificationMetadataMap[audioId] = metadataDict
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
490
630
|
if complex {
|
|
491
631
|
volume = min(max(call.getFloat("volume") ?? Constant.DefaultVolume, Constant.MinVolume), Constant.MaxVolume)
|
|
492
632
|
channels = max(call.getInt("channels") ?? Constant.DefaultChannels, 1)
|
|
@@ -593,6 +733,7 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
593
733
|
call.resolve()
|
|
594
734
|
}
|
|
595
735
|
}
|
|
736
|
+
// swiftlint:enable cyclomatic_complexity function_body_length
|
|
596
737
|
|
|
597
738
|
private func stopAudio(audioId: String) throws {
|
|
598
739
|
var asset: AudioAsset?
|
|
@@ -648,6 +789,9 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
648
789
|
}
|
|
649
790
|
}
|
|
650
791
|
|
|
792
|
+
// Clear notification center
|
|
793
|
+
clearNowPlayingInfo()
|
|
794
|
+
|
|
651
795
|
// Restore original audio session settings if we changed them
|
|
652
796
|
if audioSessionInitialized, let originalCategory = originalAudioCategory {
|
|
653
797
|
do {
|
|
@@ -670,4 +814,87 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
670
814
|
call.resolve()
|
|
671
815
|
}
|
|
672
816
|
|
|
817
|
+
// MARK: - Now Playing Info Methods
|
|
818
|
+
|
|
819
|
+
private func updateNowPlayingInfo(audioId: String, audioAsset: AudioAsset) {
|
|
820
|
+
DispatchQueue.main.async { [weak self] in
|
|
821
|
+
guard let self = self else { return }
|
|
822
|
+
|
|
823
|
+
var nowPlayingInfo = [String: Any]()
|
|
824
|
+
|
|
825
|
+
// Get metadata from the map
|
|
826
|
+
if let metadata = self.notificationMetadataMap[audioId] {
|
|
827
|
+
if let title = metadata["title"] {
|
|
828
|
+
nowPlayingInfo[MPMediaItemPropertyTitle] = title
|
|
829
|
+
}
|
|
830
|
+
if let artist = metadata["artist"] {
|
|
831
|
+
nowPlayingInfo[MPMediaItemPropertyArtist] = artist
|
|
832
|
+
}
|
|
833
|
+
if let album = metadata["album"] {
|
|
834
|
+
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = album
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// Load artwork if provided
|
|
838
|
+
if let artworkUrl = metadata["artworkUrl"] {
|
|
839
|
+
self.loadArtwork(from: artworkUrl) { image in
|
|
840
|
+
if let image = image {
|
|
841
|
+
nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: image.size) { _ in
|
|
842
|
+
return image
|
|
843
|
+
}
|
|
844
|
+
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// Add playback info
|
|
851
|
+
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = audioAsset.getDuration()
|
|
852
|
+
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = audioAsset.getCurrentTime()
|
|
853
|
+
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = 1.0
|
|
854
|
+
|
|
855
|
+
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
private func clearNowPlayingInfo() {
|
|
860
|
+
DispatchQueue.main.async {
|
|
861
|
+
MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
private func updatePlaybackState(isPlaying: Bool) {
|
|
866
|
+
DispatchQueue.main.async {
|
|
867
|
+
var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [String: Any]()
|
|
868
|
+
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = isPlaying ? 1.0 : 0.0
|
|
869
|
+
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
private func loadArtwork(from urlString: String, completion: @escaping (UIImage?) -> Void) {
|
|
874
|
+
// Check if it's a local file path or URL
|
|
875
|
+
if let url = URL(string: urlString) {
|
|
876
|
+
if url.scheme == nil || url.isFileURL {
|
|
877
|
+
// Local file
|
|
878
|
+
let path = url.path
|
|
879
|
+
if FileManager.default.fileExists(atPath: path) {
|
|
880
|
+
if let image = UIImage(contentsOfFile: path) {
|
|
881
|
+
completion(image)
|
|
882
|
+
return
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
} else {
|
|
886
|
+
// Remote URL
|
|
887
|
+
URLSession.shared.dataTask(with: url) { data, _, _ in
|
|
888
|
+
if let data = data, let image = UIImage(data: data) {
|
|
889
|
+
completion(image)
|
|
890
|
+
} else {
|
|
891
|
+
completion(nil)
|
|
892
|
+
}
|
|
893
|
+
}.resume()
|
|
894
|
+
return
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
completion(nil)
|
|
898
|
+
}
|
|
899
|
+
|
|
673
900
|
}
|