Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,5 @@ DerivedData/
Podfile.lock
Package.resolved
/.build
/CLAUDE.md
/.claude
42 changes: 34 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,18 @@ OR
another complete Ionic/Angular application demonstrating every plugin method is available in the **example-app** directory

```typescript
import {NativeAudio} from '@capacitor-community/native-audio'
import { NativeAudio, AudioFocusMode } from '@capacitor-community/native-audio'

/**
* This method will configure the audio focus behavior and fading.
* @param fade - enable audio fade effect
* @param audioFocusMode - audio focus mode: NONE (mixed audio), EXCLUSIVE (pause other audio), DUCK (lower other audio volume)
* @returns void
*/
NativeAudio.configure({
fade: false,
audioFocusMode: AudioFocusMode.DUCK
});

/**
* This method will load more optimized audio files for background into memory.
Expand Down Expand Up @@ -209,9 +219,13 @@ NativeAudio.isPlaying({
configure(options: ConfigureOptions) => Promise<void>
```

| Param | Type |
| ------------- | ------------------------------------------------------------- |
| **`options`** | <code><a href="#configureoptions">ConfigureOptions</a></code> |
Configure plugin behavior for audio focus and fading

| Param | Type | Description |
| ------------- | ------------------------------------------------------------- | --------------------- |
| **`options`** | <code><a href="#configureoptions">ConfigureOptions</a></code> | Configuration options |

**Since:** 1.0.0

--------------------

Expand Down Expand Up @@ -390,10 +404,10 @@ Listen for asset completed playing event

#### ConfigureOptions

| Prop | Type | Description | Default |
| ----------- | -------------------- | ------------------------------------------------- | ------------------ |
| **`fade`** | <code>boolean</code> | Indicating whether or not to fade audio. | <code>false</code> |
| **`focus`** | <code>boolean</code> | Indicating whether or not to disable mixed audio. | <code>false</code> |
| Prop | Type | Description | Default |
| -------------------- | --------------------------------------------------------- | ------------------------- | -------------------------------- |
| **`fade`** | <code>boolean</code> | Audio fade configuration | <code>false</code> |
| **`audioFocusMode`** | <code><a href="#audiofocusmode">AudioFocusMode</a></code> | Audio focus behavior mode | <code>AudioFocusMode.NONE</code> |


#### PreloadOptions
Expand All @@ -413,4 +427,16 @@ Listen for asset completed playing event
| ------------ | ----------------------------------------- |
| **`remove`** | <code>() =&gt; Promise&lt;void&gt;</code> |


### Enums


#### AudioFocusMode

| Members | Value | Description |
| --------------- | ------------------------ | ---------------------------------------------------- |
| **`NONE`** | <code>'none'</code> | Allow mixed audio, no focus management |
| **`EXCLUSIVE`** | <code>'exclusive'</code> | Take exclusive audio focus, pause other audio |
| **`DUCK`** | <code>'duck'</code> | Take audio focus but duck (lower volume) other audio |

</docgen-api>
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.getcapacitor.community.audio;

public enum AudioFocusMode {
NONE("none"),
EXCLUSIVE("exclusive"),
DUCK("duck");

private final String value;

AudioFocusMode(String value) {
this.value = value;
}

public String getValue() {
return value;
}

public static AudioFocusMode fromString(String value) {
if (value == null) {
return NONE;
}

for (AudioFocusMode mode : AudioFocusMode.values()) {
if (mode.value.equals(value)) {
return mode;
}
}

return NONE;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public class Constant {
public static final String ASSET_ID = "assetId";
public static final String ASSET_PATH = "assetPath";
public static final String OPT_FADE_MUSIC = "fade";
public static final String OPT_FOCUS_AUDIO = "focus";
public static final String OPT_AUDIO_FOCUS_MODE = "audioFocusMode";
public static final String VOLUME = "volume";
public static final String AUDIO_CHANNEL_NUM = "audioChannelNum";
public static final String LOOP = "loop";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@
import static com.getcapacitor.community.audio.Constant.ERROR_AUDIO_EXISTS;
import static com.getcapacitor.community.audio.Constant.ERROR_AUDIO_ID_MISSING;
import static com.getcapacitor.community.audio.Constant.LOOP;
import static com.getcapacitor.community.audio.Constant.OPT_AUDIO_FOCUS_MODE;
import static com.getcapacitor.community.audio.Constant.OPT_FADE_MUSIC;
import static com.getcapacitor.community.audio.Constant.OPT_FOCUS_AUDIO;
import static com.getcapacitor.community.audio.Constant.VOLUME;

import android.Manifest;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.content.res.AssetManager;
import android.media.AudioAttributes;
import android.media.AudioFocusRequest;
import android.media.AudioManager;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
Expand Down Expand Up @@ -47,6 +49,8 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
private static ArrayList<AudioAsset> resumeList;
private boolean fadeMusic = false;
private AudioManager audioManager;
private AudioFocusRequest audioFocusRequest;
private AudioFocusMode configuredAudioFocusMode = AudioFocusMode.NONE;

@Override
public void load() {
Expand Down Expand Up @@ -110,13 +114,10 @@ public void configure(PluginCall call) {

this.fadeMusic = call.getBoolean(OPT_FADE_MUSIC, false);

if (this.audioManager != null) {
if (call.getBoolean(OPT_FOCUS_AUDIO, false)) {
this.audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
} else {
this.audioManager.abandonAudioFocus(this);
}
}
// Store the audio focus mode configuration for later use
String audioFocusModeString = call.getString(OPT_AUDIO_FOCUS_MODE, AudioFocusMode.NONE.getValue());
this.configuredAudioFocusMode = AudioFocusMode.fromString(audioFocusModeString);

call.resolve();
}

Expand Down Expand Up @@ -271,6 +272,11 @@ public void unload(PluginCall call) {
asset.unload();
audioAssetList.remove(audioId);

// Abandon audio focus when no more assets are loaded
if (audioAssetList.isEmpty()) {
abandonAudioFocus();
}

status = new JSObject();
status.put("status", "OK");
call.resolve(status);
Expand Down Expand Up @@ -394,6 +400,11 @@ private void preloadAsset(PluginCall call) {
AudioAsset asset = new AudioAsset(this, audioId, assetFileDescriptor, audioChannelNum, (float) volume);
audioAssetList.put(audioId, asset);

// Request audio focus when first asset is preloaded
if (audioAssetList.size() == 1) {
requestAudioFocus();
}

JSObject status = new JSObject();
status.put("STATUS", "OK");
call.resolve(status);
Expand Down Expand Up @@ -440,4 +451,55 @@ private void initSoundPool() {
private boolean isStringValid(String value) {
return (value != null && !value.isEmpty() && !value.equals("null"));
}

private void requestAudioFocus() {
if (this.audioManager == null) {
return;
}

int focusGain;
int usage;
int contentType;

switch (this.configuredAudioFocusMode) {
case EXCLUSIVE:
focusGain = AudioManager.AUDIOFOCUS_GAIN;
usage = AudioAttributes.USAGE_MEDIA;
contentType = AudioAttributes.CONTENT_TYPE_MUSIC;
break;
case DUCK:
focusGain = AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK;
usage = AudioAttributes.USAGE_ASSISTANCE_SONIFICATION;
contentType = AudioAttributes.CONTENT_TYPE_SONIFICATION;
break;
case NONE:
return;
default:
return;
}

AudioAttributes audioAttributes = new AudioAttributes.Builder()
.setUsage(usage)
.setContentType(contentType)
.build();

this.audioFocusRequest = new AudioFocusRequest.Builder(focusGain)
.setAudioAttributes(audioAttributes)
.setOnAudioFocusChangeListener(this)
.setAcceptsDelayedFocusGain(false)
.setWillPauseWhenDucked(false)
.build();

int result = this.audioManager.requestAudioFocus(this.audioFocusRequest);
if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
Log.w(TAG, "Audio focus request denied");
}
}

private void abandonAudioFocus() {
if (this.audioFocusRequest != null) {
this.audioManager.abandonAudioFocusRequest(this.audioFocusRequest);
this.audioFocusRequest = null;
}
}
}
13 changes: 13 additions & 0 deletions ios/Sources/NativeAudio/AudioFocusMode.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// AudioFocusMode.swift
// Plugin
//
// Created by Rabter1 on 2025-09-21.
// Copyright Β© 2020 Max Lynch. All rights reserved.
//

public enum AudioFocusMode: String {
case none
case exclusive
case duck
}
2 changes: 1 addition & 1 deletion ios/Sources/NativeAudio/Constant.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

public class Constant {
public static let FadeKey = "fade"
public static let FocusAudio = "focus"
public static let AudioFocusModeKey = "audioFocusMode"
public static let AssetPathKey = "assetPath"
public static let AssetIdKey = "assetId"
public static let Volume = "volume"
Expand Down
26 changes: 22 additions & 4 deletions ios/Sources/NativeAudio/Plugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,33 @@ public class NativeAudio: CAPPlugin, CAPBridgedPlugin {

@objc func configure(_ call: CAPPluginCall) {
self.fadeMusic = call.getBool(Constant.FadeKey, false)
let audioFocusModeString = call.getString(Constant.AudioFocusModeKey, AudioFocusMode.none.rawValue)
let audioFocusMode = AudioFocusMode(rawValue: audioFocusModeString) ?? AudioFocusMode.none

do {
if call.getBool(Constant.FocusAudio, false) {
try self.session.setCategory(AVAudioSession.Category.playback)
} else {
switch audioFocusMode {
case .none:
try self.session.setCategory(AVAudioSession.Category.ambient)
case .exclusive:
try self.session.setCategory(AVAudioSession.Category.playback)
case .duck:
try self.session.setCategory(AVAudioSession.Category.playback, options: .duckOthers)
}
} catch {
print("Failed to set setCategory audio")
}
call.resolve()
}


private func deactivateAudioSession() {
do {
try self.session.setActive(false, options: .notifyOthersOnDeactivation)
} catch {
print("Failed to deactivate audio session")
}
}

@objc func preload(_ call: CAPPluginCall) {
preloadAsset(call, isComplex: true)
}
Expand Down Expand Up @@ -179,7 +194,10 @@ public class NativeAudio: CAPPlugin, CAPBridgedPlugin {
if asset != nil && asset is AudioAsset {
let audioAsset = asset as! AudioAsset
audioAsset.unload()
self.audioList[audioId] = nil
self.audioList.removeValue(forKey: audioId);
if self.audioList.isEmpty {
self.deactivateAudioSession()
}
}
}
call.resolve()
Expand Down
39 changes: 35 additions & 4 deletions src/definitions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,36 @@
import type { PluginListenerHandle } from '@capacitor/core';

export enum AudioFocusMode {
/** Allow mixed audio, no focus management */
NONE = 'none',
/** Take exclusive audio focus, pause other audio */
EXCLUSIVE = 'exclusive',
/** Take audio focus but duck (lower volume) other audio */
DUCK = 'duck',
}

export interface NativeAudio {
/**
* Configure plugin behavior for audio focus and fading
*
* @param options Configuration options
*
* @example
* ```typescript
* // Duck other audio when playing
* await NativeAudio.configure({
* audioFocusMode: AudioFocusMode.DUCK
* });
*
* // Take exclusive focus with fade effect
* await NativeAudio.configure({
* fade: true,
* audioFocusMode: AudioFocusMode.EXCLUSIVE
* });
* ```
*
* @since 1.0.0
*/
configure(options: ConfigureOptions): Promise<void>;
preload(options: PreloadOptions): Promise<void>;
play(options: { assetId: string; time?: number }): Promise<void>;
Expand All @@ -23,15 +53,16 @@ export interface NativeAudio {

export interface ConfigureOptions {
/**
* Indicating whether or not to fade audio.
* Audio fade configuration
* @default false
*/
fade?: boolean;

/**
* Indicating whether or not to disable mixed audio.
* @default false
* Audio focus behavior mode
* @default AudioFocusMode.NONE
*/
focus?: boolean;
audioFocusMode?: AudioFocusMode;
}

export interface PreloadOptions {
Expand Down