capacitor-plugin-system-volume
v0.4.1
Published
Capacitor plugin for system volume + media routing: a native MPVolumeView slider and AirPlay button on iOS, and AudioManager volume + a Cast device picker on Android, so an on-screen slider stays in sync with the hardware buttons.
Readme
capacitor-plugin-system-volume
A Capacitor plugin for system volume and media routing, native on each platform so an on-screen control stays in sync with the hardware volume buttons:
- iOS — a stylable
MPVolumeViewslider and an AirPlay route button, overlaid on the webview. They're Apple's own controls, so the slider sets the OS output volume and moves with the hardware buttons — something an<input type="range">can't do on iOS (WKWebView can't read or set system volume). - Android —
AudioManagermethods to read and set the system media volume (in sync with the hardware buttons) and a Cast device picker, which you wire to your own styled web controls. Android exposes these as plain APIs, so no native overlay is needed. - Web — no access to OS volume; every method rejects with
unavailable(use a normal slider bound to your media element's ownvolume).
| Capability | iOS | Android | Web |
| --- | --- | --- | --- |
| Read volume — getVolume, volumeChange | ✅ | ✅ | — |
| Set volume | native VolumeSlider overlay | setVolume | — |
| On-screen volume UI | VolumeSlider (MPVolumeView) | your web slider + setVolume | — |
| External-route UI | RoutePicker (AirPlay) | openRoutePicker + routeChange (Cast) | — |
Why
On iOS an HTML <audio>/<video> element's volume is a no-op inside WKWebView
and there is no web API to touch system volume, so the only control that both
reflects the hardware buttons and lets the user set volume is MPVolumeView — a
native UIKit view. This plugin mounts it (and the AirPlay AVRoutePickerView)
into the webview over a placeholder element and keeps it aligned as the page
scrolls and resizes (the compositing technique @capacitor/google-maps uses for
a native map).
Android is friendlier: AudioManager reads and sets the media-stream volume
directly, and the Cast chooser can be opened programmatically — so there the
plugin is just methods, and you render whatever slider and button suit your UI.
Install
npm install capacitor-plugin-system-volume
npx cap syncUsage — iOS (native overlays)
Put an empty placeholder where each control should appear (the native control shows through it) and give it a width and height.
<div id="volume" style="width: 220px; height: 28px;"></div>import { VolumeSlider } from 'capacitor-plugin-system-volume';
const slider = await VolumeSlider.create({
id: 'main',
element: document.getElementById('volume')!,
style: {
minimumTrackColor: '#B4FF39',
maximumTrackColor: '#FFFFFF33',
thumbColor: '#FFFFFF',
thumbRadius: 16,
},
});
// Reflect volume elsewhere in your UI, if you like.
await slider.setOnVolumeChangeListener((value) => {
console.log('system volume is now', value); // 0..1
});
// Later, when the view is torn down:
await slider.destroy();The RoutePicker wrapper mounts an AirPlay button the same way.
Usage — Android (your controls + plugin methods)
Android has no overlay: render your own slider and cast button and drive them
with the plugin. getVolume/setVolume/volumeChange move the system media
volume; openRoutePicker opens the Cast chooser; routeChange tells you when a
device connects so the button can restyle.
import { CapacitorSystemVolume } from 'capacitor-plugin-system-volume';
// Volume: reflect and set the system media volume (0..1).
const { value } = await CapacitorSystemVolume.getVolume();
await CapacitorSystemVolume.setVolume({ value: 0.5 });
await CapacitorSystemVolume.addListener('volumeChange', ({ value }) => {
// hardware buttons moved it — update your slider
});
// Cast: open the device picker, and track connection to style your button.
await CapacitorSystemVolume.addListener('routeChange', ({ connected, name }) => {
// connected → highlight the button and show `name`
});
castButton.onclick = () => CapacitorSystemVolume.openRoutePicker();The Cast picker uses the app's existing CastContext, so choosing a device
drives whatever CastPlayer / handoff the app already has.
getVolume() is also exported standalone (iOS + Android):
import { getVolume } from 'capacitor-plugin-system-volume';
const { value } = await getVolume(); // 0..1Host-app requirements
- iOS:
MPVolumeViewreflects and controls system volume, which requires an activeAVAudioSession. Apps that play audio already have one; a silent app may need a.playback(or.ambient) session for the slider to track real volume. - Android:
openRoutePickerneeds the Cast SDK configured — aCastOptionsProviderdeclared via the manifestOPTIONS_PROVIDER_CLASS_NAMEmeta-data. Without it,openRoutePickerrejects androuteChangenever fires; volume still works.
Notes
- The iOS Simulator does not render
MPVolumeView(no real audio route) — test on a device. - iOS styling is limited to track colours and a thumb image, per what
MPVolumeViewexposes — not arbitrary CSS.
API
The VolumeSlider / RoutePicker wrappers are the everyday iOS surface; on
Android you call the CapacitorSystemVolume methods (getVolume, setVolume,
openRoutePicker, volumeChange, routeChange) directly. Below is the low-level
bridge they build on, generated from the source JSDoc by
@capacitor/docgen — run
npm run docgen to regenerate it.
create(...)createRoutePicker(...)destroy(...)setStyle(...)setRoutePickerStyle(...)onResize(...)onDisplay(...)onScroll(...)getVolume()setVolume(...)openRoutePicker()addListener('volumeChange', ...)addListener('routeChange', ...)- Interfaces
Low-level bridge interface. Most callers use the {@link VolumeSlider} wrapper, which binds these methods to a DOM element and keeps the native frame synced.
create(...)
create(options: { id: string; rect: VolumeSliderRect; style?: VolumeSliderStyle; devicePixelRatio?: number; }) => Promise<void>Mount a native volume slider bound to the element at rect.
| Param | Type |
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| options | { id: string; rect: VolumeSliderRect; style?: VolumeSliderStyle; devicePixelRatio?: number; } |
createRoutePicker(...)
createRoutePicker(options: { id: string; rect: VolumeSliderRect; style?: RoutePickerStyle; }) => Promise<void>Mount a native AirPlay route button bound to the element at rect.
| Param | Type |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| options | { id: string; rect: VolumeSliderRect; style?: RoutePickerStyle; } |
destroy(...)
destroy(options: { id: string; }) => Promise<void>Tear down the overlay (slider or route button) with this id.
| Param | Type |
| ------------- | ---------------------------- |
| options | { id: string; } |
setStyle(...)
setStyle(options: { id: string; style: VolumeSliderStyle; }) => Promise<void>Restyle an existing slider.
| Param | Type |
| ------------- | --------------------------------------------------------------------------------------- |
| options | { id: string; style: VolumeSliderStyle; } |
setRoutePickerStyle(...)
setRoutePickerStyle(options: { id: string; style: RoutePickerStyle; }) => Promise<void>Restyle an existing route button.
| Param | Type |
| ------------- | ------------------------------------------------------------------------------------- |
| options | { id: string; style: RoutePickerStyle; } |
onResize(...)
onResize(options: { id: string; rect: VolumeSliderRect; }) => Promise<void>| Param | Type |
| ------------- | ------------------------------------------------------------------------------------ |
| options | { id: string; rect: VolumeSliderRect; } |
onDisplay(...)
onDisplay(options: { id: string; rect: VolumeSliderRect; }) => Promise<void>| Param | Type |
| ------------- | ------------------------------------------------------------------------------------ |
| options | { id: string; rect: VolumeSliderRect; } |
onScroll(...)
onScroll(options: { id: string; rect: VolumeSliderRect; }) => Promise<void>| Param | Type |
| ------------- | ------------------------------------------------------------------------------------ |
| options | { id: string; rect: VolumeSliderRect; } |
getVolume()
getVolume() => Promise<{ value: number; }>Read the current system output volume, 0–1. iOS and Android.
Returns: Promise<{ value: number; }>
setVolume(...)
setVolume(options: { value: number; }) => Promise<void>Set the system output volume, 0–1. Android only — on iOS the OS
forbids programmatic volume changes, so use the native {@link VolumeSlider}
overlay there instead. Rejects unavailable on web/iOS.
| Param | Type |
| ------------- | ------------------------------- |
| options | { value: number; } |
openRoutePicker()
openRoutePicker() => Promise<void>Open the native Cast device chooser (or, when already casting, the
controller/disconnect dialog) — the Android counterpart of the AirPlay
button. Discovery and the session are shared with the app's CastContext, so
picking a device drives the app's own Cast handoff. Android only; rejects
unavailable on web/iOS (use the {@link RoutePicker} overlay on iOS).
addListener('volumeChange', ...)
addListener(eventName: 'volumeChange', listenerFunc: (data: { value: number; }) => void) => Promise<PluginListenerHandle>Fires whenever the system output volume changes — the hardware buttons,
Control Center / quick settings, or a drag on the native slider. Value is
0–1. iOS and Android.
| Param | Type |
| ------------------ | -------------------------------------------------- |
| eventName | 'volumeChange' |
| listenerFunc | (data: { value: number; }) => void |
Returns: Promise<PluginListenerHandle>
addListener('routeChange', ...)
addListener(eventName: 'routeChange', listenerFunc: (data: { connected: boolean; name: string; }) => void) => Promise<PluginListenerHandle>Fires when the selected media route changes — i.e. casting starts or stops.
connected is true while a Cast (or other external) route is active, and
name is that device's name (empty when playing locally). Lets a web cast
button style itself and show the target. Android only.
| Param | Type |
| ------------------ | --------------------------------------------------------------------- |
| eventName | 'routeChange' |
| listenerFunc | (data: { connected: boolean; name: string; }) => void |
Returns: Promise<PluginListenerHandle>
Interfaces
VolumeSliderRect
The rectangle a native overlay should occupy, in CSS pixels.
| Prop | Type |
| ------------ | ------------------- |
| x | number |
| y | number |
| width | number |
| height | number |
VolumeSliderStyle
Appearance for the native volume slider. Colours are CSS hex strings
(#RRGGBB or #RGB). The native side renders the track and thumb from these,
so match them to your app's accent to blend the Apple control into your UI.
| Prop | Type | Description |
| ----------------------- | ------------------- | ----------------------------------------------------------------------------------- |
| minimumTrackColor | string | Filled portion of the track (left of the thumb). Defaults to the system tint. |
| maximumTrackColor | string | Unfilled portion of the track (right of the thumb). Defaults to a translucent grey. |
| thumbColor | string | The draggable thumb. Defaults to white. |
| thumbRadius | number | Thumb diameter in points. Defaults to 16. |
RoutePickerStyle
Appearance for the native AirPlay route button. Colours are CSS hex strings.
| Prop | Type | Description |
| --------------------- | ------------------- | ------------------------------------------------------------------------------------ |
| tintColor | string | The AirPlay glyph when no external route is active. Defaults to the system tint. |
| activeTintColor | string | The glyph when a route (AirPlay/Bluetooth) is active. Defaults to the system accent. |
PluginListenerHandle
| Prop | Type |
| ------------ | ----------------------------------------- |
| remove | () => Promise<void> |
Maintainers
| Maintainer | GitHub | | ---------- | ----------------------------------------- | | pjaudiomv | pjaudiomv |
Contributors
Thanks goes to these wonderful people (emoji key):
This project follows the all-contributors specification. Contributions of any kind welcome!
License
MIT
