@capgo/native-audio 7.7.7 → 7.8.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 CHANGED
@@ -38,6 +38,23 @@ Click on video to see example 💥
38
38
 
39
39
  [![YouTube Example](https://img.youtube.com/vi/XpUGlWWtwHs/0.jpg)](https://www.youtube.com/watch?v=XpUGlWWtwHs)
40
40
 
41
+ ## Why Native Audio?
42
+
43
+ The only **free**, **full-featured** audio playback plugin for Capacitor:
44
+
45
+ - **HLS/M3U8 streaming** - Play live audio streams and adaptive bitrate content
46
+ - **Remote URLs** - Stream from HTTP/HTTPS sources with built-in caching
47
+ - **Low-latency playback** - Optimized native audio engine for sound effects and music
48
+ - **Full control** - Play, pause, resume, loop, seek, volume, playback rate
49
+ - **Multiple channels** - Play multiple audio files simultaneously
50
+ - **Background playback** - Continue playing when app is backgrounded
51
+ - **Notification center display** - Show audio metadata in iOS Control Center and Android notifications
52
+ - **Position tracking** - Real-time currentTime events (100ms intervals)
53
+ - **Modern package management** - Supports both Swift Package Manager (SPM) and CocoaPods (SPM-ready for Capacitor 8)
54
+ - **Same JavaScript API** - Compatible interface with paid alternatives
55
+
56
+ Perfect for music players, podcast apps, games, meditation apps, and any audio-heavy application.
57
+
41
58
  ## Maintainers
42
59
 
43
60
  | Maintainer | GitHub | Social |
@@ -114,6 +131,70 @@ No configuration required for this plugin.
114
131
 
115
132
  [Example repository](https://github.com/bazuka5801/native-audio-example)
116
133
 
134
+ ### Notification Center Display (iOS & Android)
135
+
136
+ 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.
137
+
138
+ **Step 1: Configure the plugin with notification support**
139
+
140
+ ```typescript
141
+ import { NativeAudio } from '@capgo/native-audio'
142
+
143
+ // Enable notification center display
144
+ await NativeAudio.configure({
145
+ showNotification: true,
146
+ background: true // Also enable background playback
147
+ });
148
+ ```
149
+
150
+ **Step 2: Preload audio with metadata**
151
+
152
+ ```typescript
153
+ await NativeAudio.preload({
154
+ assetId: 'song1',
155
+ assetPath: 'https://example.com/song.mp3',
156
+ isUrl: true,
157
+ notificationMetadata: {
158
+ title: 'My Song Title',
159
+ artist: 'Artist Name',
160
+ album: 'Album Name',
161
+ artworkUrl: 'https://example.com/artwork.jpg' // Can be local or remote URL
162
+ }
163
+ });
164
+ ```
165
+
166
+ **Step 3: Play the audio**
167
+
168
+ ```typescript
169
+ // When you play the audio, it will automatically appear in the notification center
170
+ await NativeAudio.play({ assetId: 'song1' });
171
+ ```
172
+
173
+ The notification will:
174
+ - Show the title, artist, and album information
175
+ - Display the artwork/album art (if provided)
176
+ - Include media controls (play/pause/stop buttons)
177
+ - Automatically update when audio is paused/resumed
178
+ - Automatically clear when audio is stopped
179
+ - Work on both iOS and Android
180
+
181
+ **Media Controls:**
182
+ Users can control playback directly from:
183
+ - iOS: Control Center, Lock Screen, CarPlay
184
+ - Android: Notification tray, Lock Screen, Android Auto
185
+
186
+ The media control buttons automatically handle:
187
+ - **Play** - Resumes paused audio
188
+ - **Pause** - Pauses playing audio
189
+ - **Stop** - Stops audio and clears the notification
190
+
191
+ **Notes:**
192
+ - All metadata fields are optional
193
+ - Artwork can be a local file path or remote URL
194
+ - The notification only appears when `showNotification: true` is set in configure()
195
+ - iOS: Uses MPNowPlayingInfoCenter with MPRemoteCommandCenter
196
+ - Android: Uses MediaSession with NotificationCompat.MediaStyle
197
+
117
198
  ## Example app
118
199
 
119
200
  This repository now ships with an interactive Capacitor project under `example/` that exercises the main APIs on web, iOS, and Android shells.
@@ -592,23 +673,35 @@ Use this when you need to ensure compatibility with other audio plugins
592
673
 
593
674
  #### ConfigureOptions
594
675
 
595
- | Prop | Type | Description |
596
- | ------------------ | -------------------- | ----------------------------------------------------------------------------- |
597
- | **`fade`** | <code>boolean</code> | Play the audio with Fade effect, only available for IOS |
598
- | **`focus`** | <code>boolean</code> | focus the audio with Audio Focus |
599
- | **`background`** | <code>boolean</code> | Play the audio in the background |
600
- | **`ignoreSilent`** | <code>boolean</code> | Ignore silent mode, works only on iOS setting this will nuke other audio apps |
676
+ | Prop | Type | Description |
677
+ | ---------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
678
+ | **`fade`** | <code>boolean</code> | Play the audio with Fade effect, only available for IOS |
679
+ | **`focus`** | <code>boolean</code> | focus the audio with Audio Focus |
680
+ | **`background`** | <code>boolean</code> | Play the audio in the background |
681
+ | **`ignoreSilent`** | <code>boolean</code> | Ignore silent mode, works only on iOS setting this will nuke other audio apps |
682
+ | **`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 |
601
683
 
602
684
 
603
685
  #### PreloadOptions
604
686
 
605
- | Prop | Type | Description |
606
- | --------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
607
- | **`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) |
608
- | **`assetId`** | <code>string</code> | Asset Id, unique identifier of the file |
609
- | **`volume`** | <code>number</code> | Volume of the audio, between 0.1 and 1.0 |
610
- | **`audioChannelNum`** | <code>number</code> | Audio channel number, default is 1 |
611
- | **`isUrl`** | <code>boolean</code> | Is the audio file a URL, pass true if assetPath is a `file://` url or a streaming URL (m3u8) |
687
+ | Prop | Type | Description |
688
+ | -------------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
689
+ | **`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) |
690
+ | **`assetId`** | <code>string</code> | Asset Id, unique identifier of the file |
691
+ | **`volume`** | <code>number</code> | Volume of the audio, between 0.1 and 1.0 |
692
+ | **`audioChannelNum`** | <code>number</code> | Audio channel number, default is 1 |
693
+ | **`isUrl`** | <code>boolean</code> | Is the audio file a URL, pass true if assetPath is a `file://` url or a streaming URL (m3u8) |
694
+ | **`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() |
695
+
696
+
697
+ #### NotificationMetadata
698
+
699
+ | Prop | Type | Description |
700
+ | ---------------- | ------------------- | ----------------------------------------------------- |
701
+ | **`title`** | <code>string</code> | The title to display in the notification center |
702
+ | **`artist`** | <code>string</code> | The artist name to display in the notification center |
703
+ | **`album`** | <code>string</code> | The album name to display in the notification center |
704
+ | **`artworkUrl`** | <code>string</code> | URL or local path to the artwork/album art image |
612
705
 
613
706
 
614
707
  #### Assets
@@ -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 MAX_CHANNELS = Constant.MaxChannels
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), MAX_CHANNELS)
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.7.7"
16
+ private let pluginVersion: String = "7.8.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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/native-audio",
3
- "version": "7.7.7",
3
+ "version": "7.8.0",
4
4
  "description": "A native plugin for native audio engine",
5
5
  "license": "MIT",
6
6
  "main": "dist/plugin.cjs.js",