Skip to content

About

Capacitor plugin for native audio playback — streaming and local files, background playback, lock screen and notification controls, playlists, playback speed, and earpiece/speaker routing on iOS, Android and the web.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

5 stars

Watchers

0 watching

Forks

Latest commit

 

History

73 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

native-audio-player

Play native audio from a Capacitor app — streamed or from a file, in the background, from the lock screen, through the earpiece or the speaker.

npm version npm downloads CI platforms license

✨ Features

  • 🌐 Streams remote http(s) audio, or plays files from the app's storage
  • 🎶 Playlists that keep playing in the background, when the app is minimized
  • 🔓 Native players in the notification and on the lock screen, with artwork and controls
  • ⏩ Playback speed from 0.5× to 2×, with the pitch kept so speech stays listenable
  • 📊 A progress event for progress bars, and getState() to pick up where the player stands
  • 🔈 Toggle between Speaker and Earpiece as audio output, and learn when headphones or Bluetooth take over
  • 📞 Stops for calls, Siri and headphones being unplugged — and tells the app, so its UI never drifts out of sync
  • 📱 Support for Android, iOS, Web (only Speaker)

Maintainers

Maintainer GitHub Social
Stefan Huber stefanhuber Linkedin

Requirements

Requirement Version
Capacitor 8.0.0 or later
iOS 15.0 or later
Android API level 24 (Android 7.0) or later

Install

npm install @smartcompanion/native-audio-player
npx cap sync

Configuration

Platform Configuration
iOS Audio has to be added as Background Mode within Signing & Capabilities of the app, in order to keep audio playing in the background
Android The plugin has a AndroidManifest.xml, which includes all configurations

Usage

import { NativeAudioPlayer } from '@smartcompanion/native-audio-player';

// 1. Hand over the playlist. The first item is selected and prepared,
//    but nothing plays yet — call play() when you are ready.
await NativeAudioPlayer.start({
  items: [
    {
      id: 'elephant',
      title: 'Elephant',
      subtitle: 'Animals',
      audioUri: 'https://example.com/elephant.mp3', // or a local file, see below
      imageUri: elephantImageUri, // shown in the notification / lock screen
    },
    {
      id: 'leopard',
      title: 'Leopard',
      subtitle: 'Animals',
      audioUri: leopardAudioUri,
      imageUri: leopardImageUri,
    },
  ],
});

// 2. Follow the player state. The event fires for your own calls *and* for
//    the native notification and lock screen controls, so this is the single
//    place where your UI stays in sync.
const listener = await NativeAudioPlayer.addListener('audioPlayerChange', async ({ state, id }) => {
  switch (state) {
    case 'playing': // playback started or resumed
    case 'paused': // playback paused
    case 'stopped': // the playlist was cleared by stop()
    case 'skipped': // another item became the current one
      break;
    case 'completed': // the item reached its end — the player stops there
      await NativeAudioPlayer.next();
      await NativeAudioPlayer.play();
      break;
  }
});

// 3. Control playback
await NativeAudioPlayer.play();
await NativeAudioPlayer.pause();
await NativeAudioPlayer.next();
await NativeAudioPlayer.previous();
await NativeAudioPlayer.select({ id: 'leopard' });
await NativeAudioPlayer.seekTo({ position: 30 }); // seconds

await NativeAudioPlayer.setRate({ value: 1.5 }); // 0.5 to 2, pitch kept

const { value: duration } = await NativeAudioPlayer.getDuration(); // seconds
const { value: position } = await NativeAudioPlayer.getPosition(); // seconds

// What the player is doing right now, e.g. for a view that mounts after start():
// { id, state: 'playing' | 'paused' | 'stopped', position, duration }
const snapshot = await NativeAudioPlayer.getState();

// 4. Follow the playhead. Reported while playing, twice a second unless start()
//    was given another progressInterval (in seconds).
const progressListener = await NativeAudioPlayer.addListener('progress', ({ id, position, duration }) => {
  console.log(`${id}: ${position} of ${duration} seconds`);
});

// 5. Route the audio (no-op on the web)
await NativeAudioPlayer.setEarpiece();
await NativeAudioPlayer.setSpeaker();

// 'earpiece' | 'speaker' | 'external', where 'external' means headphones or a
// bluetooth device are connected and the setting above does not apply.
const { output } = await NativeAudioPlayer.getAudioOutput();

// The event covers the routes you do not control either — headphones being
// unplugged, a bluetooth device connecting. It only fires on changes, so query
// getAudioOutput() once for the initial value.
//
// Playback pauses on every one of these changes, so audio meant for the earpiece
// cannot carry on out loud somewhere else. Call play() here to keep going.
const outputListener = await NativeAudioPlayer.addListener('audioOutputChange', ({ output }) => {
  console.log(`audio now plays through the ${output}`);
});

// 6. Clean up — releases the player and removes the native player UI
await listener.remove();
await progressListener.remove();
await outputListener.remove();
await NativeAudioPlayer.stop();

Remote and local files

audioUri and imageUri take an http(s) URL as readily as a local file, on every platform. A remote item is streamed from where it is: start(), next(), previous() and select() answer once enough of it has arrived to know its duration, and reject when it cannot be loaded.

Plain http is refused by default — by App Transport Security on iOS and by the cleartext policy on Android — so serve audio over https, or allow the host in the app's own configuration.

For audio that should play without a connection, download it once — for example with @capacitor/filesystem into Directory.Data — and pass the local file URI instead:

import { Capacitor } from '@capacitor/core';
import { Directory, Filesystem } from '@capacitor/filesystem';

const toLocalUri = async (url: string, filename: string): Promise<string> => {
  if (Capacitor.getPlatform() === 'web') {
    return url;
  }

  const blob = await (await fetch(url)).blob();
  const data = await new Promise<string>((resolve) => {
    const reader = new FileReader();
    reader.onload = () => resolve(reader.result as string);
    reader.readAsDataURL(blob);
  });

  await Filesystem.writeFile({ path: filename, directory: Directory.Data, data });
  return (await Filesystem.getUri({ path: filename, directory: Directory.Data })).uri;
};

const elephantAudioUri = await toLocalUri('https://example.com/elephant.mp3', 'elephant.mp3');

Both a file:// URI, as Filesystem.getUri() returns it, and a plain path are accepted, wherever in the app's storage the file is.

Good to know

  • start() only prepares the playlist — playback begins with play().
  • The player does not advance on its own. When an item finishes you get a completed event and decide what happens next (see the example above).
  • Positions and durations are fractional seconds on every platform. How finely they are resolved is up to the player, so compare them with a tolerance.
  • setEarpiece() / setSpeaker() only override the built-in output. When headphones or Bluetooth are connected, that route stays untouched.
  • The player never starts itself again. Whatever stopped it — an output change, an incoming call, another app taking the audio — resuming is your decision, made by calling play() from the audioPlayerChange listener.

In folder ./example a full usage example is available. This example is also used for automated and manual testing.

Example App Android Media Controls iOS Lock Screen
The example app playing the first item The item in Android's notification shade The item on the iOS lock screen

NativeAudioPlayer behaviour overview

What each call promises, and what it reports while doing it. An event is emitted for a change that actually happened, so a call that changes nothing stays silent.

Action Audio Player Event Audio Output Event Comment
NativeAudioPlayer.start Prepares the playlist and selects the first item without playing it. Everything that moves the player — play, pause, seekTo, next, previous, select, setEarpiece, setSpeaker — is rejected until start is called. stop is allowed at any time, and getPosition and getDuration answer 0
NativeAudioPlayer.play playing
NativeAudioPlayer.pause paused Keeps the position, so a following play carries on from there. Reported only when something was playing — pausing an already stopped player is accepted and changes nothing, which is what an app that pauses on every step of a scrubber drag does
NativeAudioPlayer.seekTo playing The new position is selected and playing is started, so playing is reported only when that started something — a player already running carries on silently. A position outside the item is pulled to its nearest end
NativeAudioPlayer.next skipped The next audio item is selected or the first if the current is the last item. The position is set to 0 and playing is not started.
NativeAudioPlayer.previous skipped The previous audio item is selected or the last if the current is the first item. The position is set to 0 and playing is not started.
NativeAudioPlayer.select skipped The item is select (reject if selected id is not existing). The position is set to 0 and playing is not started.
NativeAudioPlayer.stop stopped Reported whatever was happening, and no paused alongside it. Clears the playlist. Start is required before the audio player is usable again.
NativeAudioPlayer.setRate Changes the speed without changing the state, so nothing is reported. The rate belongs to the player: it carries across items, output changes, and stop followed by a new start.
NativeAudioPlayer.getState Asking changes nothing. Before start and after stop it answers stopped with an empty id and zero for both times.
An item reaches its end completed The player stops, the item stays selected and position is set to 0. (no separate pause event)
NativeAudioPlayer.setEarpiece NativeAudioPlayer.setSpeaker paused earpiece speaker Only overrides the built-in output, and does nothing audible while headphones or Bluetooth are connected — the output event then keeps reporting external. The built-in output starts on the speaker on both platforms
The user plugs in headphones, connects Bluetooth, or unplugs them paused external earpiece speaker Playback always stops, so audio meant for the earpiece cannot carry on out loud through a different output.
An incoming call, Siri, or another app takes the audio paused Playback stops and stays stopped when the interruption ends — nothing is reported then, so call play() to carry on. Reported only when something was playing, and not reported on the web

Notes on the events themselves:

  • The paused event on an output change is always emitted.
  • Only a change of the resolved output raises audioOutputChange. Swapping external devices for another also triggers an external event again.
  • The order of audioPlayerChange and audioOutputChange for the same output change is not guaranteed to be the same on both platforms — treat them as two reports of one event, not a sequence.
  • Every event reports playback moving, whoever moved it: a call on this plugin, a lock screen or notification control, a headset button, the audio running out, or the system stopping it for something else.
  • The player is never started again on its own. Whatever stopped it — an output change or an interruption — resuming is the app's decision, made by calling play().
  • The progress event is separate from both and only reports while playing: it stops with whatever is reported as stopping the playback above, and starts again with the next playing.

Other Audio Player Plugins

API

setEarpiece()

setEarpiece() => Promise<void>

Set the audio output to the earpiece. Has no audible effect while an external device such as headphones or a bluetooth speaker is connected.

Playback pauses, as it does on every audio output change — see {@link NativeAudioPlayerPlugin.addListener} for audioOutputChange.


setSpeaker()

setSpeaker() => Promise<void>

Set the audio output to the speaker. Has no audible effect while an external device such as headphones or a bluetooth speaker is connected.

Playback pauses, as it does on every audio output change — see {@link NativeAudioPlayerPlugin.addListener} for audioOutputChange.


start(...)

start(options: StartOptions) => Promise<{ id: string; }>

Initialize the audio player with a list of audio items.

Resolves once the first item can be played, so its duration is known by then -- for a remote item that means once enough of it has arrived. Rejects when it cannot be loaded at all, a missing file or a URL that answers with an error, and leaves nothing loaded behind.

Param Type Description
options StartOptions - The options for starting the audio player.

Returns: Promise<{ id: string; }>


stop()

stop() => Promise<void>

Stop the currently playing audio item and clear the playlist.

Reports stopped whatever was happening, since clearing the playlist is a change either way.


play()

play() => Promise<void>

Play the currently selected audio item.

Rejects when there is nothing to play, which is the case before {@link NativeAudioPlayerPlugin.start} and after {@link NativeAudioPlayerPlugin.stop} -- start the playlist again rather than calling this.


pause()

pause() => Promise<void>

Pause the currently playing audio item.

Reports paused only when something was playing. Pausing a player that is already stopped is accepted and changes nothing, so it reports nothing -- an app that pauses while a scrubber is dragged calls this many times over one gesture and hears about the first of them.


select(...)

select(options: { id: string; }) => Promise<{ id: string; }>

Select an audio item from the playlist by its id.

Rejects when no item carries that id, rather than resolving with whatever was already selected -- a caller that asked for one item and silently got another has no way to tell. Otherwise it resolves once the item can be played, and rejects when it cannot be loaded, the same as start().

Param Type
options { id: string; }

Returns: Promise<{ id: string; }>


next()

next() => Promise<{ id: string; }>

Skip to the next audio item in the playlist.

Resolves once the item can be played, and rejects when it cannot be loaded, the same as start().

Returns: Promise<{ id: string; }>


previous()

previous() => Promise<{ id: string; }>

Skip to the previous audio item in the playlist.

Resolves once the item can be played, and rejects when it cannot be loaded, the same as start().

Returns: Promise<{ id: string; }>


seekTo(...)

seekTo(options: { position: number; }) => Promise<void>

Seek to a specific position in the currently playing audio item.

Seeking resumes, so a listener who dragged a scrubber hears the audio carry on from where they dropped it. playing is reported only when that actually started something: a player that was already running carries on and reports nothing.

A position outside the item is pulled to the nearest end of it, so seeking past the end leaves the player on the last moment rather than somewhere it cannot play from. That is not the same as the item finishing: completed follows only once the audio actually runs out, which for a player that is already playing happens immediately, and for a paused one waits for the next {@link NativeAudioPlayerPlugin.play}.

Param Type
options { position: number; }

setRate(...)

setRate(options: { value: number; }) => Promise<void>

Set the speed the audio plays at, as a multiple of normal speed: 1 is normal, 2 twice as fast, 0.5 half as fast. The pitch is kept, so speech stays listenable at either end.

The rate belongs to the player rather than to an item, and it stays until it is set again -- across {@link NativeAudioPlayerPlugin.next}, {@link NativeAudioPlayerPlugin.previous}, {@link NativeAudioPlayerPlugin.select}, an output change, and even {@link NativeAudioPlayerPlugin.stop} and a new {@link NativeAudioPlayerPlugin.start}. It can be set at any time, including before anything is loaded, and applies to whatever plays next. Audio that is already playing changes speed straight away.

Changing the rate does not change the playback state, so it reports nothing.

Param Type
options { value: number; }

getRate()

getRate() => Promise<{ value: number; }>

Get the speed the audio plays at, as set through {@link NativeAudioPlayerPlugin.setRate}.

Returns: Promise<{ value: number; }>


getDuration()

getDuration() => Promise<{ value: number; }>

Get the duration of the current audio item in seconds.

Seconds are fractional on every platform. How finely they are resolved is the player's own business, so the same audio can report durations that differ by a few milliseconds across platforms -- compare with a tolerance rather than for equality.

Returns: Promise<{ value: number; }>


getPosition()

getPosition() => Promise<{ value: number; }>

Get the current position of the audio item in seconds.

Seconds are fractional, which is what a progress bar needs to move smoothly. Callers that want whole seconds can round the value themselves.

Returns: Promise<{ value: number; }>


getState()

getState() => Promise<AudioPlayerSnapshot>

Get what the player is doing right now: which item is selected, whether it is playing, and where in it the player is.

The audioPlayerChange event only reports changes, so an app that mounts after the player was started, or that missed an event, has no other way to find out where things stand. Asking changes nothing and reports nothing.

Before {@link NativeAudioPlayerPlugin.start} and after {@link NativeAudioPlayerPlugin.stop} nothing is loaded, which is reported as stopped with an empty id and zero for both times -- rather than rejected, since that is a perfectly good answer to the question.

Returns: Promise<AudioPlayerSnapshot>


getAudioOutput()

getAudioOutput() => Promise<{ output: AudioOutput; }>

Get the audio output the player is currently routed to.

Returns: Promise<{ output: AudioOutput; }>


addListener('audioPlayerChange', ...)

addListener(eventName: 'audioPlayerChange', listener: (result: AudioPlayerChange) => void) => Promise<PluginListenerHandle>

Add an event listener for player changes. The listener should accept an event object containing the current state and id of the audio item.

The event reports every change, whichever caused it: a call on this plugin, a lock screen or notification control, a headset button, or the audio itself running out. An app that mirrors playback in its own UI should follow this event rather than assume its own calls are the only thing moving the player.

See {@link AudioPlayerState} for what each state promises.

Param Type
eventName 'audioPlayerChange'
listener (result: AudioPlayerChange) => void

Returns: Promise<PluginListenerHandle>


addListener('progress', ...)

addListener(eventName: 'progress', listener: (result: AudioPlayerProgress) => void) => Promise<PluginListenerHandle>

Add an event listener for the playhead. While the audio is playing it reports where the selected item is, every {@link StartOptions.progressInterval} seconds -- twice a second unless start() was given another interval -- which is what a progress bar needs to follow playback without polling {@link NativeAudioPlayerPlugin.getPosition}.

It reports only while playing. It stops with the playback, whatever stopped it -- a pause, a skip, an output change, an interruption, the item running out, or {@link NativeAudioPlayerPlugin.stop} -- and starts again with the next playing. A player that is not running does not move, so there is nothing to report; ask {@link NativeAudioPlayerPlugin.getState} for where it stands.

It is an event of its own, and adds nothing to audioPlayerChange.

Param Type
eventName 'progress'
listener (result: AudioPlayerProgress) => void

Returns: Promise<PluginListenerHandle>


addListener('audioOutputChange', ...)

addListener(eventName: 'audioOutputChange', listener: (result: AudioOutputChange) => void) => Promise<PluginListenerHandle>

Add an event listener for audio output changes. Fires both when the output is changed through {@link NativeAudioPlayerPlugin.setEarpiece} or {@link NativeAudioPlayerPlugin.setSpeaker} and when the route changes on its own, e.g. when headphones are unplugged or a bluetooth device connects.

Playback pauses on every one of these changes, so that audio meant for the earpiece cannot carry on out loud through a different output. A audioPlayerChange event with the paused state is emitted alongside this one whenever something was playing. Resuming is left to the app: call {@link NativeAudioPlayerPlugin.play} from this listener to play on regardless.

Param Type
eventName 'audioOutputChange'
listener (result: AudioOutputChange) => void

Returns: Promise<PluginListenerHandle>


Interfaces

StartOptions

Options for starting the audio player.

Prop Type Description Default
items Item[] A list of audio items to be initialized in the audio player.
progressInterval number How often the progress event reports the playhead while the audio is playing, in seconds. Anything that is not a positive number falls back to the default. 0.5

Item

Represents an audio item in the playlist.

Prop Type Description
id string The unique identifier for the audio item.
title string The title of the audio item, which is e.g. displayed in the notification player.
subtitle string The subtitle of the audio item, which is e.g. displayed in the notification player.
audioUri string Where the audio is: a local file -- a file:// URI, or a plain path -- or an http(s) URL to stream from. A remote item is played from where it is rather than downloaded first. Plain http is subject to the platform's rules for unencrypted traffic: App Transport Security on iOS and the cleartext policy on Android both refuse it unless the app allows it.
imageUri string The image associated with the audio item, shown e.g. in the notification and on the lock screen. A local file or an http(s) URL, the same as {@link Item.audioUri}.

AudioPlayerSnapshot

The result of {@link NativeAudioPlayerPlugin.getState}.

Prop Type Description
id string The id of the selected audio item, or an empty string while the player is stopped.
state Exclude<AudioPlayerState, 'skipped' | 'completed'> Where the player is: playing, paused -- which covers an item that was just selected or started, one that was paused, and one that played out and was rewound -- or stopped.
position number The position in the selected item in fractional seconds, the same value {@link NativeAudioPlayerPlugin.getPosition} reports.
duration number The duration of the selected item in fractional seconds, the same value {@link NativeAudioPlayerPlugin.getDuration} reports.

PluginListenerHandle

Prop Type
remove () => Promise<void>

AudioPlayerChange

The payload of the audioPlayerChange event.

Prop Type Description
state AudioPlayerState The playback state the player changed to.
id string The id of the audio item the state refers to.

AudioPlayerProgress

The payload of the progress event.

Prop Type Description
id string The id of the audio item that is playing.
position number The position in the item in fractional seconds, the same value {@link NativeAudioPlayerPlugin.getPosition} reports.
duration number The duration of the item in fractional seconds, the same value {@link NativeAudioPlayerPlugin.getDuration} reports.

AudioOutputChange

The payload of the audioOutputChange event.

Prop Type Description
output AudioOutput The audio output the player changed to.

Type Aliases

Exclude

Exclude from T those types that are assignable to U

T extends U ? never : T

AudioPlayerState

The playback state of the player.

The audioPlayerChange event reports every one of these, and {@link NativeAudioPlayerPlugin.getState} the three the player can be in -- skipped and completed describe how it got somewhere rather than where it is, and leave it paused.

Exactly one event is emitted per transition, so a state never arrives paired with another describing the same change -- and a call that changes nothing reports nothing at all. Every state below behaves the same way on iOS, Android and the web.

  • playing -- playback started or resumed.
  • paused -- playback stopped and the position was kept. Emitted for a {@link NativeAudioPlayerPlugin.pause} call that stopped something, for an output change, and for an interruption -- an incoming call, or another app taking the audio -- that stopped something, but never for an item that reached its end: that is completed. The player is not started again when an interruption ends, and nothing is reported then, so an app that wants to carry on has to call {@link NativeAudioPlayerPlugin.play} itself. Interruptions are not reported on the web, where the browser exposes nothing to observe them with.
  • stopped -- nothing is loaded. Emitted for a {@link NativeAudioPlayerPlugin.stop} call whatever was happening, with the id of the item that was selected; it takes a {@link NativeAudioPlayerPlugin.start} to play again. It is also where the player is before the first start().
  • skipped -- the selected item changed through {@link NativeAudioPlayerPlugin.next}, {@link NativeAudioPlayerPlugin.previous} or {@link NativeAudioPlayerPlugin.select}. The new item is selected but not playing, so it takes a {@link NativeAudioPlayerPlugin.play} to start it. The id is the item moved to.
  • completed -- the item played through to its end. The player stops there rather than advancing, so nothing else starts on its own, and the item is rewound so a following {@link NativeAudioPlayerPlugin.play} starts it again from the beginning. The id is the item that finished, and it stays the selected one. Advancing is left to the app: call {@link NativeAudioPlayerPlugin.next} from this listener to play through a playlist.

'playing' | 'paused' | 'stopped' | 'skipped' | 'completed'

AudioOutput

The audio output the player is routed to.

external means the audio is not routed to the built-in earpiece or speaker, which is the case whenever an external device such as headphones or a bluetooth speaker is connected. The earpiece and speaker settings do not apply while an external device is in use, so neither value would describe what is actually heard.

The built-in output starts on the speaker on both platforms, so audio a listener has not asked to keep private is audible without holding the phone to an ear. {@link NativeAudioPlayerPlugin.setEarpiece} is what opts into the earpiece.

'earpiece' | 'speaker' | 'external'

About

Capacitor plugin for native audio playback — streaming and local files, background playback, lock screen and notification controls, playlists, playback speed, and earpiece/speaker routing on iOS, Android and the web.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

5 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages