@capgo/native-audio 7.11.2 → 8.1.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/CapgoNativeAudio.podspec +1 -1
- package/Package.swift +1 -1
- package/README.md +124 -0
- package/android/build.gradle +9 -9
- package/android/src/main/java/ee/forgr/audio/NativeAudio.java +347 -97
- package/dist/docs.json +156 -0
- package/dist/esm/definitions.d.ts +107 -0
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +3 -1
- package/dist/esm/web.js +58 -0
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +58 -0
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +58 -0
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/NativeAudioPlugin/AudioAsset.swift +20 -7
- package/ios/Sources/NativeAudioPlugin/Plugin.swift +363 -8
- package/ios/Sources/NativeAudioPlugin/RemoteAudioAsset.swift +31 -2
- package/ios/Tests/NativeAudioPluginTests/PluginTests.swift +371 -6
- package/package.json +14 -14
|
@@ -50,8 +50,11 @@ import java.net.URI;
|
|
|
50
50
|
import java.net.URL;
|
|
51
51
|
import java.util.ArrayList;
|
|
52
52
|
import java.util.HashMap;
|
|
53
|
+
import java.util.HashSet;
|
|
53
54
|
import java.util.Iterator;
|
|
54
55
|
import java.util.Map;
|
|
56
|
+
import java.util.Set;
|
|
57
|
+
import java.util.UUID;
|
|
55
58
|
|
|
56
59
|
@UnstableApi
|
|
57
60
|
@CapacitorPlugin(
|
|
@@ -84,6 +87,13 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
84
87
|
private static final int NOTIFICATION_ID = 1001;
|
|
85
88
|
private static final String CHANNEL_ID = "native_audio_channel";
|
|
86
89
|
|
|
90
|
+
// Track playOnce assets for automatic cleanup
|
|
91
|
+
private Set<String> playOnceAssets = new HashSet<>();
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Initializes plugin runtime state by obtaining the system {@link AudioManager}, preparing the asset map,
|
|
95
|
+
* and recording the device's original audio mode without requesting audio focus.
|
|
96
|
+
*/
|
|
87
97
|
@Override
|
|
88
98
|
public void load() {
|
|
89
99
|
super.load();
|
|
@@ -243,6 +253,11 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
243
253
|
.start();
|
|
244
254
|
}
|
|
245
255
|
|
|
256
|
+
/**
|
|
257
|
+
* Initiates preloading of an audio asset described by the plugin call.
|
|
258
|
+
*
|
|
259
|
+
* @param call the PluginCall containing preload options (for example `assetId`, `assetPath`, `isUrl`, `isComplex`, headers, and optional notification metadata); the call will be resolved or rejected when the preload operation completes.
|
|
260
|
+
*/
|
|
246
261
|
@PluginMethod
|
|
247
262
|
public void preload(final PluginCall call) {
|
|
248
263
|
this.getActivity().runOnUiThread(
|
|
@@ -255,6 +270,192 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
255
270
|
);
|
|
256
271
|
}
|
|
257
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Play an audio asset a single time and automatically remove its resources when finished.
|
|
275
|
+
*
|
|
276
|
+
* <p>Preloads the specified asset, optionally starts playback immediately, and ensures the
|
|
277
|
+
* asset is unloaded and any associated notification metadata are cleared after completion or on
|
|
278
|
+
* error. Supports local file paths and remote URLs, HLS streams when available, custom HTTP
|
|
279
|
+
* headers for remote requests, and optional deletion of local source files after playback.
|
|
280
|
+
*
|
|
281
|
+
* @param call Capacitor PluginCall containing options:
|
|
282
|
+
* <ul>
|
|
283
|
+
* <li><code>assetPath</code> (required): path or URL to the audio file;</li>
|
|
284
|
+
* <li><code>volume</code> (optional): playback volume (0.1–1.0), default 1.0;</li>
|
|
285
|
+
* <li><code>isUrl</code> (optional): treat <code>assetPath</code> as a URL when true, default false;</li>
|
|
286
|
+
* <li><code>autoPlay</code> (optional): start playback immediately when true, default true;</li>
|
|
287
|
+
* <li><code>deleteAfterPlay</code> (optional): delete the local file after playback when true, default false;</li>
|
|
288
|
+
* <li><code>headers</code> (optional): JS object of HTTP headers for remote requests;</li>
|
|
289
|
+
* <li><code>notificationMetadata</code> (optional): object with <code>title</code>, <code>artist</code>,
|
|
290
|
+
* <code>album</code>, <code>artworkUrl</code> for notification display.</li>
|
|
291
|
+
* </ul>
|
|
292
|
+
*/
|
|
293
|
+
@PluginMethod
|
|
294
|
+
public void playOnce(final PluginCall call) {
|
|
295
|
+
this.getActivity().runOnUiThread(
|
|
296
|
+
new Runnable() {
|
|
297
|
+
/**
|
|
298
|
+
* Preloads a temporary audio asset, optionally plays it one time, and schedules automatic cleanup when playback completes.
|
|
299
|
+
*
|
|
300
|
+
* <p>The method generates a unique temporary assetId, validates options (path, volume, local/remote, headers),
|
|
301
|
+
* loads the asset into the plugin's asset map, registers completion listeners to dispatch the completion event
|
|
302
|
+
* and to unload/remove notification metadata and tracking state, and optionally deletes the source file from
|
|
303
|
+
* safe application directories after playback. If configured, it also updates the media notification and returns
|
|
304
|
+
* the generated `assetId` to the caller.
|
|
305
|
+
*/
|
|
306
|
+
@Override
|
|
307
|
+
public void run() {
|
|
308
|
+
try {
|
|
309
|
+
NativeAudio.this.initSoundPool();
|
|
310
|
+
|
|
311
|
+
// Generate unique temporary asset ID
|
|
312
|
+
final String assetId =
|
|
313
|
+
"playOnce_" + System.currentTimeMillis() + "_" + UUID.randomUUID().toString().substring(0, 8);
|
|
314
|
+
|
|
315
|
+
// Extract options
|
|
316
|
+
String assetPath = call.getString("assetPath");
|
|
317
|
+
if (!NativeAudio.this.isStringValid(assetPath)) {
|
|
318
|
+
call.reject("Asset Path is missing - " + assetPath);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
boolean autoPlay = call.getBoolean("autoPlay", true);
|
|
323
|
+
final boolean deleteAfterPlay = call.getBoolean("deleteAfterPlay", false);
|
|
324
|
+
float volume = call.getFloat(VOLUME, 1F);
|
|
325
|
+
boolean isLocalUrl = call.getBoolean("isUrl", false);
|
|
326
|
+
int audioChannelNum = 1; // Single channel for playOnce
|
|
327
|
+
|
|
328
|
+
// Track this as a playOnce asset
|
|
329
|
+
NativeAudio.this.playOnceAssets.add(assetId);
|
|
330
|
+
|
|
331
|
+
// Store notification metadata if provided
|
|
332
|
+
JSObject metadata = call.getObject(NOTIFICATION_METADATA);
|
|
333
|
+
if (metadata != null) {
|
|
334
|
+
Map<String, String> metadataMap = new HashMap<>();
|
|
335
|
+
if (metadata.has("title")) metadataMap.put("title", metadata.getString("title"));
|
|
336
|
+
if (metadata.has("artist")) metadataMap.put("artist", metadata.getString("artist"));
|
|
337
|
+
if (metadata.has("album")) metadataMap.put("album", metadata.getString("album"));
|
|
338
|
+
if (metadata.has("artworkUrl")) metadataMap.put("artworkUrl", metadata.getString("artworkUrl"));
|
|
339
|
+
if (!metadataMap.isEmpty()) {
|
|
340
|
+
NativeAudio.this.notificationMetadataMap.put(assetId, metadataMap);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Preload the asset using the helper method
|
|
345
|
+
try {
|
|
346
|
+
// Check if asset already exists
|
|
347
|
+
if (NativeAudio.this.audioAssetList.containsKey(assetId)) {
|
|
348
|
+
call.reject(ERROR_AUDIO_EXISTS + " - " + assetId);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Load the asset using the helper method
|
|
352
|
+
JSObject headersObj = call.getObject("headers");
|
|
353
|
+
AudioAsset asset = NativeAudio.this.loadAudioAsset(
|
|
354
|
+
assetId,
|
|
355
|
+
assetPath,
|
|
356
|
+
isLocalUrl,
|
|
357
|
+
volume,
|
|
358
|
+
audioChannelNum,
|
|
359
|
+
headersObj
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
// Add to asset list; completion listener is set below with cleanup
|
|
363
|
+
NativeAudio.this.audioAssetList.put(assetId, asset);
|
|
364
|
+
|
|
365
|
+
// Store the file path if we need to delete it later
|
|
366
|
+
// Only delete local file:// URLs, not remote streaming URLs
|
|
367
|
+
final String filePathToDelete = (deleteAfterPlay && assetPath.startsWith("file://")) ? assetPath : null;
|
|
368
|
+
|
|
369
|
+
// Set up completion listener for automatic cleanup
|
|
370
|
+
asset.setCompletionListener(
|
|
371
|
+
new AudioCompletionListener() {
|
|
372
|
+
@Override
|
|
373
|
+
public void onCompletion(String completedAssetId) {
|
|
374
|
+
// Call the original completion dispatcher first
|
|
375
|
+
NativeAudio.this.dispatchComplete(completedAssetId);
|
|
376
|
+
|
|
377
|
+
// Then perform cleanup
|
|
378
|
+
NativeAudio.this.getActivity().runOnUiThread(() -> {
|
|
379
|
+
try {
|
|
380
|
+
// Unload the asset
|
|
381
|
+
AudioAsset assetToUnload = NativeAudio.this.audioAssetList.get(assetId);
|
|
382
|
+
if (assetToUnload != null) {
|
|
383
|
+
assetToUnload.unload();
|
|
384
|
+
NativeAudio.this.audioAssetList.remove(assetId);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Remove from tracking sets
|
|
388
|
+
NativeAudio.this.playOnceAssets.remove(assetId);
|
|
389
|
+
NativeAudio.this.notificationMetadataMap.remove(assetId);
|
|
390
|
+
|
|
391
|
+
// Clear notification if this was the currently playing asset
|
|
392
|
+
if (assetId.equals(NativeAudio.this.currentlyPlayingAssetId)) {
|
|
393
|
+
NativeAudio.this.clearNotification();
|
|
394
|
+
NativeAudio.this.currentlyPlayingAssetId = null;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Delete file if requested
|
|
398
|
+
if (filePathToDelete != null) {
|
|
399
|
+
try {
|
|
400
|
+
File fileToDelete = new File(URI.create(filePathToDelete));
|
|
401
|
+
if (fileToDelete.exists() && fileToDelete.delete()) {
|
|
402
|
+
Log.d(TAG, "Deleted file after playOnce: " + filePathToDelete);
|
|
403
|
+
}
|
|
404
|
+
} catch (Exception e) {
|
|
405
|
+
Log.e(TAG, "Error deleting file after playOnce: " + filePathToDelete, e);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
} catch (Exception e) {
|
|
409
|
+
Log.e(TAG, "Error during playOnce cleanup: " + e.getMessage());
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
// Auto-play if requested
|
|
417
|
+
if (autoPlay) {
|
|
418
|
+
asset.play(0.0);
|
|
419
|
+
|
|
420
|
+
// Update notification if enabled
|
|
421
|
+
if (showNotification) {
|
|
422
|
+
currentlyPlayingAssetId = assetId;
|
|
423
|
+
updateNotification(assetId);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Return the generated assetId
|
|
428
|
+
JSObject result = new JSObject();
|
|
429
|
+
result.put(ASSET_ID, assetId);
|
|
430
|
+
call.resolve(result);
|
|
431
|
+
} catch (Exception ex) {
|
|
432
|
+
// Cleanup on failure
|
|
433
|
+
NativeAudio.this.playOnceAssets.remove(assetId);
|
|
434
|
+
NativeAudio.this.notificationMetadataMap.remove(assetId);
|
|
435
|
+
AudioAsset failedAsset = NativeAudio.this.audioAssetList.get(assetId);
|
|
436
|
+
if (failedAsset != null) {
|
|
437
|
+
failedAsset.unload();
|
|
438
|
+
NativeAudio.this.audioAssetList.remove(assetId);
|
|
439
|
+
}
|
|
440
|
+
call.reject("Failed to load asset for playOnce: " + ex.getMessage());
|
|
441
|
+
}
|
|
442
|
+
} catch (Exception ex) {
|
|
443
|
+
call.reject(ex.getMessage());
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Starts playback of a preloaded audio asset on the main (UI) thread.
|
|
452
|
+
*
|
|
453
|
+
* The PluginCall must include:
|
|
454
|
+
* - "assetId" (String): identifier of the preloaded asset to play.
|
|
455
|
+
* - Optional "time" (number): start position in seconds.
|
|
456
|
+
*
|
|
457
|
+
* @param call the PluginCall containing playback parameters
|
|
458
|
+
*/
|
|
258
459
|
@PluginMethod
|
|
259
460
|
public void play(final PluginCall call) {
|
|
260
461
|
this.getActivity().runOnUiThread(
|
|
@@ -581,6 +782,14 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
581
782
|
notifyListeners("complete", ret);
|
|
582
783
|
}
|
|
583
784
|
|
|
785
|
+
/**
|
|
786
|
+
* Emits a "currentTime" event for the given asset with the playback position rounded to the nearest 0.1 second.
|
|
787
|
+
*
|
|
788
|
+
* The emitted event payload contains `assetId` and `currentTime` (in seconds, rounded to the nearest 0.1).
|
|
789
|
+
*
|
|
790
|
+
* @param assetId the identifier of the audio asset
|
|
791
|
+
* @param currentTime the current playback time in seconds (will be rounded to nearest 0.1)
|
|
792
|
+
*/
|
|
584
793
|
public void notifyCurrentTime(String assetId, double currentTime) {
|
|
585
794
|
// Round to nearest 100ms
|
|
586
795
|
double roundedTime = Math.round(currentTime * 10.0) / 10.0;
|
|
@@ -590,6 +799,133 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
590
799
|
notifyListeners("currentTime", ret);
|
|
591
800
|
}
|
|
592
801
|
|
|
802
|
+
/**
|
|
803
|
+
* Create an AudioAsset for the given identifier and path, supporting remote URLs (including HLS),
|
|
804
|
+
* local file URIs, and assets in the app's public folder.
|
|
805
|
+
*
|
|
806
|
+
* @param assetId unique identifier for the asset
|
|
807
|
+
* @param assetPath file path or URL to the audio resource
|
|
808
|
+
* @param isLocalUrl true when assetPath is a URL (http/https/file), false when it refers to a public asset path
|
|
809
|
+
* @param volume initial playback volume (expected range: 0.1 to 1.0)
|
|
810
|
+
* @param audioChannelNum number of audio channels to configure for the asset
|
|
811
|
+
* @param headersObj optional HTTP headers for remote requests (may be null)
|
|
812
|
+
* @return an initialized AudioAsset instance for the provided path
|
|
813
|
+
* @throws Exception if the asset cannot be located or initialized (includes missing file, invalid path, or other load errors)
|
|
814
|
+
*/
|
|
815
|
+
private AudioAsset loadAudioAsset(
|
|
816
|
+
String assetId,
|
|
817
|
+
String assetPath,
|
|
818
|
+
boolean isLocalUrl,
|
|
819
|
+
float volume,
|
|
820
|
+
int audioChannelNum,
|
|
821
|
+
JSObject headersObj
|
|
822
|
+
) throws Exception {
|
|
823
|
+
if (isLocalUrl) {
|
|
824
|
+
Uri uri = Uri.parse(assetPath);
|
|
825
|
+
if (uri.getScheme() != null && (uri.getScheme().equals("http") || uri.getScheme().equals("https"))) {
|
|
826
|
+
// Remote URL
|
|
827
|
+
Map<String, String> requestHeaders = null;
|
|
828
|
+
if (headersObj != null) {
|
|
829
|
+
requestHeaders = new HashMap<>();
|
|
830
|
+
for (Iterator<String> it = headersObj.keys(); it.hasNext(); ) {
|
|
831
|
+
String key = it.next();
|
|
832
|
+
try {
|
|
833
|
+
String value = headersObj.getString(key);
|
|
834
|
+
if (value != null) {
|
|
835
|
+
requestHeaders.put(key, value);
|
|
836
|
+
}
|
|
837
|
+
} catch (Exception e) {
|
|
838
|
+
Log.w("AudioPlugin", "Skipping non-string header: " + key);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
if (assetPath.endsWith(".m3u8")) {
|
|
844
|
+
// HLS Stream - check if HLS support is available
|
|
845
|
+
if (!HlsAvailabilityChecker.isHlsAvailable()) {
|
|
846
|
+
throw new Exception(
|
|
847
|
+
"HLS streaming (.m3u8) is not available. " + "Set 'hls: true' in capacitor.config.ts and run 'npx cap sync'."
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
AudioAsset streamAudioAsset = createStreamAudioAsset(assetId, uri, volume, requestHeaders);
|
|
851
|
+
if (streamAudioAsset == null) {
|
|
852
|
+
throw new Exception("Failed to create HLS stream player. HLS may not be configured.");
|
|
853
|
+
}
|
|
854
|
+
return streamAudioAsset;
|
|
855
|
+
} else {
|
|
856
|
+
RemoteAudioAsset remoteAudioAsset = new RemoteAudioAsset(this, assetId, uri, audioChannelNum, volume, requestHeaders);
|
|
857
|
+
return remoteAudioAsset;
|
|
858
|
+
}
|
|
859
|
+
} else if (uri.getScheme() != null && uri.getScheme().equals("file")) {
|
|
860
|
+
File file = new File(uri.getPath());
|
|
861
|
+
if (!file.exists()) {
|
|
862
|
+
throw new Exception(ERROR_ASSET_PATH_MISSING + " - " + assetPath);
|
|
863
|
+
}
|
|
864
|
+
ParcelFileDescriptor pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
|
|
865
|
+
AssetFileDescriptor afd = new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH);
|
|
866
|
+
AudioAsset asset = new AudioAsset(this, assetId, afd, audioChannelNum, volume);
|
|
867
|
+
return asset;
|
|
868
|
+
} else {
|
|
869
|
+
// Handle unexpected URI schemes by attempting to treat as local file
|
|
870
|
+
try {
|
|
871
|
+
File file = new File(uri.getPath());
|
|
872
|
+
if (!file.exists()) {
|
|
873
|
+
throw new Exception(ERROR_ASSET_PATH_MISSING + " - " + assetPath);
|
|
874
|
+
}
|
|
875
|
+
ParcelFileDescriptor pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
|
|
876
|
+
AssetFileDescriptor afd = new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH);
|
|
877
|
+
AudioAsset asset = new AudioAsset(this, assetId, afd, audioChannelNum, volume);
|
|
878
|
+
Log.w(TAG, "Unexpected URI scheme '" + uri.getScheme() + "' treated as local file: " + assetPath);
|
|
879
|
+
return asset;
|
|
880
|
+
} catch (Exception e) {
|
|
881
|
+
throw new Exception(
|
|
882
|
+
"Failed to load asset with unexpected URI scheme '" +
|
|
883
|
+
uri.getScheme() +
|
|
884
|
+
"' (expected 'http', 'https', or 'file'). Asset path: " +
|
|
885
|
+
assetPath +
|
|
886
|
+
". Error: " +
|
|
887
|
+
e.getMessage()
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
} else {
|
|
892
|
+
// Handle asset in public folder
|
|
893
|
+
String finalAssetPath = assetPath;
|
|
894
|
+
if (!assetPath.startsWith("public/")) {
|
|
895
|
+
finalAssetPath = "public/" + assetPath;
|
|
896
|
+
}
|
|
897
|
+
Context ctx = getContext().getApplicationContext();
|
|
898
|
+
AssetManager am = ctx.getResources().getAssets();
|
|
899
|
+
AssetFileDescriptor assetFileDescriptor = am.openFd(finalAssetPath);
|
|
900
|
+
AudioAsset asset = new AudioAsset(this, assetId, assetFileDescriptor, audioChannelNum, volume);
|
|
901
|
+
return asset;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* Preloads an audio asset into the plugin's asset list.
|
|
907
|
+
*
|
|
908
|
+
* <p>The provided PluginCall must include:
|
|
909
|
+
* <ul>
|
|
910
|
+
* <li>`assetId` (string) — identifier for the asset</li>
|
|
911
|
+
* <li>`assetPath` (string) — path or URL to the audio resource</li>
|
|
912
|
+
* </ul>
|
|
913
|
+
* Optional keys on the call:
|
|
914
|
+
* <ul>
|
|
915
|
+
* <li>`isUrl` (boolean) — true when `assetPath` is a remote URL</li>
|
|
916
|
+
* <li>`isComplex` (boolean) — when true, `volume` and `audioChannelNum` may be provided</li>
|
|
917
|
+
* <li>`volume` (number) — initial playback volume (default 1.0)</li>
|
|
918
|
+
* <li>`audioChannelNum` (int) — audio channel count (default 1)</li>
|
|
919
|
+
* <li>`headers` (object) — HTTP headers for remote requests</li>
|
|
920
|
+
* <li>`notificationMetadata` (object) — optional metadata (`title`, `artist`, `album`, `artworkUrl`) to attach to the asset</li>
|
|
921
|
+
* </ul>
|
|
922
|
+
*
|
|
923
|
+
* <p>On success the call is resolved with a status indicating success. The method rejects the call
|
|
924
|
+
* when required parameters are missing, when an asset with the same id already exists, or when
|
|
925
|
+
* the asset cannot be loaded.
|
|
926
|
+
*
|
|
927
|
+
* @param call the PluginCall containing asset parameters and options
|
|
928
|
+
*/
|
|
593
929
|
private void preloadAsset(PluginCall call) {
|
|
594
930
|
float volume = 1F;
|
|
595
931
|
int audioChannelNum = 1;
|
|
@@ -639,105 +975,19 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
639
975
|
}
|
|
640
976
|
}
|
|
641
977
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
if (uri.getScheme() != null && (uri.getScheme().equals("http") || uri.getScheme().equals("https"))) {
|
|
646
|
-
// Remote URL
|
|
647
|
-
Log.d("AudioPlugin", "Debug: Remote URL detected: " + uri.toString());
|
|
648
|
-
|
|
649
|
-
// Extract headers if provided
|
|
650
|
-
Map<String, String> requestHeaders = null;
|
|
651
|
-
JSObject headersObj = call.getObject("headers");
|
|
652
|
-
if (headersObj != null) {
|
|
653
|
-
requestHeaders = new HashMap<>();
|
|
654
|
-
for (Iterator<String> it = headersObj.keys(); it.hasNext(); ) {
|
|
655
|
-
String key = it.next();
|
|
656
|
-
try {
|
|
657
|
-
String value = headersObj.getString(key);
|
|
658
|
-
if (value != null) {
|
|
659
|
-
requestHeaders.put(key, value);
|
|
660
|
-
}
|
|
661
|
-
} catch (Exception e) {
|
|
662
|
-
Log.w("AudioPlugin", "Skipping non-string header: " + key);
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
}
|
|
978
|
+
// Use the helper method to load the asset
|
|
979
|
+
JSObject headersObj = call.getObject("headers");
|
|
980
|
+
AudioAsset asset = loadAudioAsset(audioId, assetPath, isLocalUrl, volume, audioChannelNum, headersObj);
|
|
666
981
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
call.reject(
|
|
671
|
-
"HLS streaming (.m3u8) is not available. " +
|
|
672
|
-
"The media3-exoplayer-hls dependency is not included. " +
|
|
673
|
-
"To enable HLS support, set 'hls: true' in capacitor.config.ts under NativeAudio plugin config " +
|
|
674
|
-
"and run 'npx cap sync'. This will increase APK size by ~4MB."
|
|
675
|
-
);
|
|
676
|
-
return;
|
|
677
|
-
}
|
|
678
|
-
// HLS Stream - create via reflection to allow compile-time exclusion
|
|
679
|
-
AudioAsset streamAudioAsset = createStreamAudioAsset(audioId, uri, volume, requestHeaders);
|
|
680
|
-
if (streamAudioAsset == null) {
|
|
681
|
-
call.reject("Failed to create HLS stream player. HLS support may not be properly configured.");
|
|
682
|
-
return;
|
|
683
|
-
}
|
|
684
|
-
audioAssetList.put(audioId, streamAudioAsset);
|
|
685
|
-
call.resolve(status);
|
|
686
|
-
} else {
|
|
687
|
-
// Regular remote audio
|
|
688
|
-
RemoteAudioAsset remoteAudioAsset = new RemoteAudioAsset(
|
|
689
|
-
this,
|
|
690
|
-
audioId,
|
|
691
|
-
uri,
|
|
692
|
-
audioChannelNum,
|
|
693
|
-
volume,
|
|
694
|
-
requestHeaders
|
|
695
|
-
);
|
|
696
|
-
remoteAudioAsset.setCompletionListener(this::dispatchComplete);
|
|
697
|
-
audioAssetList.put(audioId, remoteAudioAsset);
|
|
698
|
-
call.resolve(status);
|
|
699
|
-
}
|
|
700
|
-
} else if (uri.getScheme() != null && uri.getScheme().equals("file")) {
|
|
701
|
-
// Local file URL
|
|
702
|
-
Log.d("AudioPlugin", "Debug: Local file URL detected");
|
|
703
|
-
File file = new File(uri.getPath());
|
|
704
|
-
if (!file.exists()) {
|
|
705
|
-
Log.e("AudioPlugin", "Error: File does not exist - " + file.getAbsolutePath());
|
|
706
|
-
call.reject(ERROR_ASSET_PATH_MISSING + " - " + assetPath);
|
|
707
|
-
return;
|
|
708
|
-
}
|
|
709
|
-
ParcelFileDescriptor pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
|
|
710
|
-
AssetFileDescriptor afd = new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH);
|
|
711
|
-
AudioAsset asset = new AudioAsset(this, audioId, afd, audioChannelNum, volume);
|
|
712
|
-
asset.setCompletionListener(this::dispatchComplete);
|
|
713
|
-
audioAssetList.put(audioId, asset);
|
|
714
|
-
call.resolve(status);
|
|
715
|
-
} else {
|
|
716
|
-
throw new IllegalArgumentException("Invalid URL scheme: " + uri.getScheme());
|
|
717
|
-
}
|
|
718
|
-
} catch (Exception e) {
|
|
719
|
-
Log.e("AudioPlugin", "Error handling URL", e);
|
|
720
|
-
call.reject("Error handling URL: " + e.getMessage());
|
|
721
|
-
}
|
|
722
|
-
} else {
|
|
723
|
-
// Handle asset in public folder
|
|
724
|
-
Log.d("AudioPlugin", "Debug: Handling asset in public folder");
|
|
725
|
-
if (!assetPath.startsWith("public/")) {
|
|
726
|
-
assetPath = "public/" + assetPath;
|
|
727
|
-
}
|
|
728
|
-
try {
|
|
729
|
-
Context ctx = getContext().getApplicationContext();
|
|
730
|
-
AssetManager am = ctx.getResources().getAssets();
|
|
731
|
-
AssetFileDescriptor assetFileDescriptor = am.openFd(assetPath);
|
|
732
|
-
AudioAsset asset = new AudioAsset(this, audioId, assetFileDescriptor, audioChannelNum, volume);
|
|
733
|
-
asset.setCompletionListener(this::dispatchComplete);
|
|
734
|
-
audioAssetList.put(audioId, asset);
|
|
735
|
-
call.resolve(status);
|
|
736
|
-
} catch (IOException e) {
|
|
737
|
-
Log.e("AudioPlugin", "Error opening asset: " + assetPath, e);
|
|
738
|
-
call.reject(ERROR_ASSET_PATH_MISSING + " - " + assetPath);
|
|
739
|
-
}
|
|
982
|
+
if (asset == null) {
|
|
983
|
+
call.reject("Failed to load asset");
|
|
984
|
+
return;
|
|
740
985
|
}
|
|
986
|
+
|
|
987
|
+
// Set completion listener and add to asset list
|
|
988
|
+
asset.setCompletionListener(this::dispatchComplete);
|
|
989
|
+
audioAssetList.put(audioId, asset);
|
|
990
|
+
call.resolve(status);
|
|
741
991
|
} catch (Exception ex) {
|
|
742
992
|
Log.e("AudioPlugin", "Error in preloadAsset", ex);
|
|
743
993
|
call.reject("Error in preloadAsset: " + ex.getMessage());
|
package/dist/docs.json
CHANGED
|
@@ -73,6 +73,46 @@
|
|
|
73
73
|
],
|
|
74
74
|
"slug": "preload"
|
|
75
75
|
},
|
|
76
|
+
{
|
|
77
|
+
"name": "playOnce",
|
|
78
|
+
"signature": "(options: PlayOnceOptions) => Promise<PlayOnceResult>",
|
|
79
|
+
"parameters": [
|
|
80
|
+
{
|
|
81
|
+
"name": "options",
|
|
82
|
+
"docs": "",
|
|
83
|
+
"type": "PlayOnceOptions"
|
|
84
|
+
}
|
|
85
|
+
],
|
|
86
|
+
"returns": "Promise<PlayOnceResult>",
|
|
87
|
+
"tags": [
|
|
88
|
+
{
|
|
89
|
+
"name": "example",
|
|
90
|
+
"text": "```typescript\n// Simple one-shot playback\nawait NativeAudio.playOnce({ assetPath: 'audio/notification.mp3' });\n\n// Play and delete the file after completion\nawait NativeAudio.playOnce({\n assetPath: 'file:///path/to/temp/audio.mp3',\n isUrl: true,\n deleteAfterPlay: true\n});\n\n// Get the assetId to control playback\nconst { assetId } = await NativeAudio.playOnce({\n assetPath: 'audio/long-track.mp3',\n autoPlay: true\n});\n// Later, you can stop it manually if needed\nawait NativeAudio.stop({ assetId });\n```"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"name": "since",
|
|
94
|
+
"text": "7.11.0"
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"name": "param",
|
|
98
|
+
"text": "options"
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"name": "link",
|
|
102
|
+
"text": "PlayOnceOptions}"
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"name": "returns",
|
|
106
|
+
"text": "Object containing the generated assetId"
|
|
107
|
+
}
|
|
108
|
+
],
|
|
109
|
+
"docs": "Play an audio file once with automatic cleanup\n\nMethod designed for simple, single-shot audio playback,\nsuch as notification sounds, UI feedback, or other short audio clips\nthat don't require manual state management.\n\n**Key Features:**\n- **Fire-and-forget**: No need to manually preload, play, stop, or unload\n- **Auto-cleanup**: Asset is automatically unloaded after playback completes\n- **Optional file deletion**: Can delete local files after playback (useful for temp files)\n- **Returns assetId**: Can still control playback if needed (pause, stop, etc.)\n\n**Use Cases:**\n- Notification sounds\n- UI sound effects (button clicks, alerts)\n- Short audio clips that play once\n- Temporary audio files that should be cleaned up\n\n**Comparison with regular play():**\n- `play()`: Requires manual preload, play, and unload steps\n- `playOnce()`: Handles everything automatically with a single call",
|
|
110
|
+
"complexTypes": [
|
|
111
|
+
"PlayOnceResult",
|
|
112
|
+
"PlayOnceOptions"
|
|
113
|
+
],
|
|
114
|
+
"slug": "playonce"
|
|
115
|
+
},
|
|
76
116
|
{
|
|
77
117
|
"name": "isPreloaded",
|
|
78
118
|
"signature": "(options: PreloadOptions) => Promise<{ found: boolean; }>",
|
|
@@ -780,6 +820,122 @@
|
|
|
780
820
|
}
|
|
781
821
|
]
|
|
782
822
|
},
|
|
823
|
+
{
|
|
824
|
+
"name": "PlayOnceResult",
|
|
825
|
+
"slug": "playonceresult",
|
|
826
|
+
"docs": "",
|
|
827
|
+
"tags": [],
|
|
828
|
+
"methods": [],
|
|
829
|
+
"properties": [
|
|
830
|
+
{
|
|
831
|
+
"name": "assetId",
|
|
832
|
+
"tags": [],
|
|
833
|
+
"docs": "The internally generated asset ID for this playback\nCan be used to control playback (pause, stop, etc.) before completion",
|
|
834
|
+
"complexTypes": [],
|
|
835
|
+
"type": "string"
|
|
836
|
+
}
|
|
837
|
+
]
|
|
838
|
+
},
|
|
839
|
+
{
|
|
840
|
+
"name": "PlayOnceOptions",
|
|
841
|
+
"slug": "playonceoptions",
|
|
842
|
+
"docs": "",
|
|
843
|
+
"tags": [],
|
|
844
|
+
"methods": [],
|
|
845
|
+
"properties": [
|
|
846
|
+
{
|
|
847
|
+
"name": "assetPath",
|
|
848
|
+
"tags": [],
|
|
849
|
+
"docs": "Path to the audio file, relative path of the file, absolute url (file://) or remote url (https://)\nSupported formats:\n- MP3, WAV (all platforms)\n- M3U8/HLS streams (iOS and Android)",
|
|
850
|
+
"complexTypes": [],
|
|
851
|
+
"type": "string"
|
|
852
|
+
},
|
|
853
|
+
{
|
|
854
|
+
"name": "volume",
|
|
855
|
+
"tags": [
|
|
856
|
+
{
|
|
857
|
+
"text": "1.0",
|
|
858
|
+
"name": "default"
|
|
859
|
+
}
|
|
860
|
+
],
|
|
861
|
+
"docs": "Volume of the audio, between 0.1 and 1.0",
|
|
862
|
+
"complexTypes": [],
|
|
863
|
+
"type": "number | undefined"
|
|
864
|
+
},
|
|
865
|
+
{
|
|
866
|
+
"name": "isUrl",
|
|
867
|
+
"tags": [
|
|
868
|
+
{
|
|
869
|
+
"text": "false",
|
|
870
|
+
"name": "default"
|
|
871
|
+
}
|
|
872
|
+
],
|
|
873
|
+
"docs": "Is the audio file a URL, pass true if assetPath is a `file://` url\nor a streaming URL (m3u8)",
|
|
874
|
+
"complexTypes": [],
|
|
875
|
+
"type": "boolean | undefined"
|
|
876
|
+
},
|
|
877
|
+
{
|
|
878
|
+
"name": "autoPlay",
|
|
879
|
+
"tags": [
|
|
880
|
+
{
|
|
881
|
+
"text": "true",
|
|
882
|
+
"name": "default"
|
|
883
|
+
}
|
|
884
|
+
],
|
|
885
|
+
"docs": "Automatically start playback after loading",
|
|
886
|
+
"complexTypes": [],
|
|
887
|
+
"type": "boolean | undefined"
|
|
888
|
+
},
|
|
889
|
+
{
|
|
890
|
+
"name": "deleteAfterPlay",
|
|
891
|
+
"tags": [
|
|
892
|
+
{
|
|
893
|
+
"text": "false",
|
|
894
|
+
"name": "default"
|
|
895
|
+
},
|
|
896
|
+
{
|
|
897
|
+
"text": "7.11.0",
|
|
898
|
+
"name": "since"
|
|
899
|
+
}
|
|
900
|
+
],
|
|
901
|
+
"docs": "Delete the audio file from disk after playback completes\nOnly works for local files (file:// URLs), ignored for remote URLs",
|
|
902
|
+
"complexTypes": [],
|
|
903
|
+
"type": "boolean | undefined"
|
|
904
|
+
},
|
|
905
|
+
{
|
|
906
|
+
"name": "notificationMetadata",
|
|
907
|
+
"tags": [
|
|
908
|
+
{
|
|
909
|
+
"text": "NotificationMetadata *",
|
|
910
|
+
"name": "see"
|
|
911
|
+
},
|
|
912
|
+
{
|
|
913
|
+
"text": "7.10.0",
|
|
914
|
+
"name": "since"
|
|
915
|
+
}
|
|
916
|
+
],
|
|
917
|
+
"docs": "Metadata to display in the notification center when audio is playing.\nOnly used when `showNotification: true` is set in `configure()`.\n\nSee {@link ConfigureOptions.showNotification} for important details about\nhow this affects audio mixing behavior on iOS.",
|
|
918
|
+
"complexTypes": [
|
|
919
|
+
"NotificationMetadata"
|
|
920
|
+
],
|
|
921
|
+
"type": "NotificationMetadata"
|
|
922
|
+
},
|
|
923
|
+
{
|
|
924
|
+
"name": "headers",
|
|
925
|
+
"tags": [
|
|
926
|
+
{
|
|
927
|
+
"text": "7.10.0",
|
|
928
|
+
"name": "since"
|
|
929
|
+
}
|
|
930
|
+
],
|
|
931
|
+
"docs": "Custom HTTP headers to include when fetching remote audio files.\nOnly used when isUrl is true and assetPath is a remote URL (http/https).\nExample: { 'x-api-key': 'abc123', 'Authorization': 'Bearer token' }",
|
|
932
|
+
"complexTypes": [
|
|
933
|
+
"Record"
|
|
934
|
+
],
|
|
935
|
+
"type": "Record<string, string>"
|
|
936
|
+
}
|
|
937
|
+
]
|
|
938
|
+
},
|
|
783
939
|
{
|
|
784
940
|
"name": "Assets",
|
|
785
941
|
"slug": "assets",
|