A Capacitor plugin for advanced volume control with native Android and iOS implementations. This plugin provides comprehensive volume management capabilities including volume level control, volume change monitoring, and platform-specific features.
- π Volume Level Control: Get and set volume levels for different audio streams
- π Volume Change Monitoring: Watch for volume changes in real-time
- π± Platform-Specific Features:
- Android: Suppress volume indicator, control different volume types
- iOS: Disable system volume handler, voice call volume control
- π― Type Safety: Full TypeScript support with comprehensive type definitions
- π§ Easy Integration: Simple API that works seamlessly with Capacitor apps
v2.0+ Breaking Change:
- The event listener pattern (
addListener('volumeChanged', ...)) andevent.levelare no longer supported. - Use only
watchVolume(options, callback)for hardware button events. The callback receives{ direction: 'up' | 'down' }.
npm install @odion-cloud/capacitor-volume-control
npx cap sync- Install dependencies:
npm install- Add the plugin:
npm install @odion-cloud/capacitor-volume-control- Sync with native platforms:
npx cap syncimport { VolumeControl, VolumeType } from '@odion-cloud/capacitor-volume-control';
// Get current volume
const volume = await VolumeControl.getVolumeLevel();
console.log('Current volume:', volume.value);
// Set volume to 50%
await VolumeControl.setVolumeLevel({ value: 0.5 });// Use watchVolume with callback
await VolumeControl.watchVolume({
disableSystemVolumeHandler: true, // iOS only
suppressVolumeIndicator: true, // Android only
}, (event) => {
console.log('Volume button pressed:', event.direction);
});
// Stop watching
await VolumeControl.clearWatch();import { VolumeControl, VolumeType } from '@odion-cloud/capacitor-volume-control';
class VolumeService {
private isWatching = false;
async initializeVolume() {
try {
// Get current music volume
const musicVolume = await VolumeControl.getVolumeLevel({
type: VolumeType.MUSIC
});
console.log('Music volume:', musicVolume.value);
// Set system volume
await VolumeControl.setVolumeLevel({
value: 0.8,
type: VolumeType.SYSTEM
});
} catch (error) {
console.error('Volume initialization error:', error);
}
}
async startWatching() {
if (this.isWatching) return;
try {
await VolumeControl.watchVolume({
disableSystemVolumeHandler: true,
suppressVolumeIndicator: true
}, this.handleVolumeChange.bind(this));
this.isWatching = true;
console.log('Started volume watching');
} catch (error) {
console.error('Volume watching error:', error);
}
}
async stopWatching() {
try {
await VolumeControl.clearWatch();
this.isWatching = false;
console.log('Stopped volume watching');
} catch (error) {
console.error('Stop watching error:', error);
}
}
private handleVolumeChange(event: { direction: 'up' | 'down' }) {
console.log(`Volume ${event.direction}`);
// Custom volume handling logic
}
async getWatchStatus() {
const status = await VolumeControl.isWatching();
return status.value;
}
}
// Usage
const volumeService = new VolumeService();
// Initialize
await volumeService.initializeVolume();
// Start watching
await volumeService.startWatching();
// Check status
const isWatching = await volumeService.getWatchStatus();
console.log('Is watching:', isWatching);
// Stop watching
await volumeService.stopWatching();// Suppress volume indicator on Android
await VolumeControl.watchVolume({
suppressVolumeIndicator: true
}, (event) => {
console.log('Volume button pressed:', event.direction);
});
// Control different volume types
await VolumeControl.setVolumeLevel({
value: 0.7,
type: VolumeType.NOTIFICATION
});// Disable system volume handler on iOS
await VolumeControl.watchVolume({
disableSystemVolumeHandler: true
}, (event) => {
console.log('Volume button pressed:', event.direction);
});
// Control voice call volume
await VolumeControl.setVolumeLevel({
value: 0.9,
type: VolumeType.VOICE_CALL
});try {
await VolumeControl.setVolumeLevel({ value: 1.5 });
} catch (error) {
if (error.message.includes('between 0.0 and 1.0')) {
console.error('Invalid volume value');
} else {
console.error('Unexpected error:', error);
}
}
try {
await VolumeControl.watchVolume({}, callback);
await VolumeControl.watchVolume({}, callback); // This will fail
} catch (error) {
if (error.message.includes('already been watched')) {
console.error('Volume watching is already active');
}
}import { useEffect, useState } from 'react';
import { VolumeControl } from '@odion-cloud/capacitor-volume-control';
export function useVolumeControl() {
const [volume, setVolume] = useState(0.5);
const [isWatching, setIsWatching] = useState(false);
useEffect(() => {
// Get initial volume
VolumeControl.getVolumeLevel().then(result => {
setVolume(result.value);
});
// Cleanup on unmount
return () => {
VolumeControl.clearWatch();
};
}, []);
const startWatching = async () => {
if (isWatching) return;
try {
await VolumeControl.watchVolume({
disableSystemVolumeHandler: true,
suppressVolumeIndicator: true
}, (event) => {
// You may want to update UI or state here
console.log('Volume button pressed:', event.direction);
});
setIsWatching(true);
} catch (error) {
console.error('Failed to start watching:', error);
}
};
const stopWatching = async () => {
try {
await VolumeControl.clearWatch();
setIsWatching(false);
} catch (error) {
console.error('Failed to stop watching:', error);
}
};
const setVolumeLevel = async (value: number) => {
try {
await VolumeControl.setVolumeLevel({ value });
setVolume(value);
} catch (error) {
console.error('Failed to set volume:', error);
}
};
return {
volume,
isWatching,
startWatching,
stopWatching,
setVolumeLevel
};
}import { ref, onMounted, onUnmounted } from 'vue';
import { VolumeControl } from '@odion-cloud/capacitor-volume-control';
export function useVolumeControl() {
const volume = ref(0.5);
const isWatching = ref(false);
onMounted(async () => {
// Get initial volume
try {
const result = await VolumeControl.getVolumeLevel();
volume.value = result.value;
} catch (error) {
console.error('Failed to get initial volume:', error);
}
});
onUnmounted(async () => {
await stopWatching();
});
const startWatching = async () => {
if (isWatching.value) return;
try {
await VolumeControl.watchVolume({
disableSystemVolumeHandler: true,
suppressVolumeIndicator: true
}, (event) => {
console.log('Volume button pressed:', event.direction);
});
isWatching.value = true;
} catch (error) {
console.error('Failed to start watching:', error);
}
};
const stopWatching = async () => {
try {
await VolumeControl.clearWatch();
isWatching.value = false;
} catch (error) {
console.error('Failed to stop watching:', error);
}
};
const setVolumeLevel = async (value: number) => {
try {
await VolumeControl.setVolumeLevel({ value });
volume.value = value;
} catch (error) {
console.error('Failed to set volume:', error);
}
};
return {
volume,
isWatching,
startWatching,
stopWatching,
setVolumeLevel
};
}import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { VolumeControl } from '@odion-cloud/capacitor-volume-control';
@Injectable({
providedIn: 'root'
})
export class VolumeService {
private volumeSubject = new BehaviorSubject<number>(0.5);
private isWatchingSubject = new BehaviorSubject<boolean>(false);
public volume$ = this.volumeSubject.asObservable();
public isWatching$ = this.isWatchingSubject.asObservable();
constructor() {
this.initializeVolume();
}
private async initializeVolume() {
try {
const result = await VolumeControl.getVolumeLevel();
this.volumeSubject.next(result.value);
} catch (error) {
console.error('Failed to get initial volume:', error);
}
}
async startWatching(): Promise<void> {
if (this.isWatchingSubject.value) return;
try {
await VolumeControl.watchVolume({
disableSystemVolumeHandler: true,
suppressVolumeIndicator: true
}, (event) => {
this.volumeSubject.next(0); // You may want to update with actual logic
console.log('Volume button pressed:', event.direction);
});
this.isWatchingSubject.next(true);
} catch (error) {
console.error('Failed to start watching:', error);
throw error;
}
}
async stopWatching(): Promise<void> {
try {
await VolumeControl.clearWatch();
this.isWatchingSubject.next(false);
} catch (error) {
console.error('Failed to stop watching:', error);
throw error;
}
}
async setVolumeLevel(value: number): Promise<void> {
try {
await VolumeControl.setVolumeLevel({ value });
this.volumeSubject.next(value);
} catch (error) {
console.error('Failed to set volume:', error);
throw error;
}
}
}Run the example:
npm startBuild for production:
npm run buildTest on device:
npx cap run android
npx cap run ios| Platform | Support Level | Minimum Version | Features |
|---|---|---|---|
| Android | β Full Support | Android 6.0+ (API 23+) | All volume types, hardware buttons, real-time monitoring |
| iOS | β Full Support | iOS 13.0+ | Volume control, hardware buttons, audio session management |
| Web | All modern browsers | Mock implementation for testing |
| Android Version | API Level | Support Level | Features |
|---|---|---|---|
| Android 14+ | API 34+ | β Full | All features, visual media permissions |
| Android 13 | API 33 | β Full | Granular media permissions |
| Android 10-12 | API 29-32 | β Full | Scoped storage, external volumes |
| Android 6-9 | API 23-28 | β Full | Runtime permissions, SD card access |
| Android 5 | API 21-22 | Limited external storage access |
| Volume Type | Android | iOS | Description |
|---|---|---|---|
VolumeType.MUSIC |
β | β | Music, videos, games, and other media |
VolumeType.SYSTEM |
β | β | System sounds and notifications |
VolumeType.RING |
β | β | Phone ringtone volume |
VolumeType.NOTIFICATION |
β | β | Notification sounds |
VolumeType.ALARM |
β | β | Alarm clock volume |
VolumeType.VOICE_CALL |
β | β | Voice call volume |
VolumeType.DTMF |
β | β | DTMF tones |
Get the current volume level for a specific audio stream.
getVolumeLevel({
type?: VolumeType; // Volume type to get (default: 'music')
}): Promise<VolumeResult>
// Returns: { value: number } (0.0 to 1.0)Set the volume level for a specific audio stream.
setVolumeLevel({
value: number; // Volume level (0.0 to 1.0)
type?: VolumeType; // Volume type to set (default: 'music')
}): Promise<VolumeResult>
// Returns: { value: number } (the new volume level)Start watching for volume changes with hardware button detection.
watchVolume({
disableSystemVolumeHandler?: boolean; // iOS: disable system UI
suppressVolumeIndicator?: boolean; // Android: hide volume UI
}, callback: (event: { direction: 'up' | 'down' }) => void): Promise<string>
// Callback receives: { direction: 'up' | 'down' }Stop watching for volume changes.
clearWatch(): Promise<void>Check if volume watching is currently active.
isWatching(): Promise<{ value: boolean }>| Option | Platform | Description |
|---|---|---|
suppressVolumeIndicator |
Android | Hide system volume UI when changing volume |
disableSystemVolumeHandler |
iOS | Disable system volume UI and intercept hardware buttons |
type |
Both | Specify volume type (MUSIC, SYSTEM, RING, etc.) |
value |
Both | Volume level between 0.0 and 1.0 |
Common errors and their solutions:
| Error Message | Cause | Solution |
|---|---|---|
Volume value must be between 0.0 and 1.0 |
Invalid volume level | Ensure volume is between 0.0 and 1.0 |
Volume buttons has already been watched |
Multiple watch calls | Call clearWatch() before starting new watch |
Volume slider not available |
iOS setup issue | Check audio session configuration |
Failed to get volume level |
Permission or system error | Verify permissions and device compatibility |
Volume observer registration failed |
Android system issue | Restart app or check system permissions |
Audio session setup failed |
iOS audio session issue | Check audio session category and options |
- Always clean up listeners: Remove event listeners when components unmount
- Use the callback for volume events: Use the callback in
watchVolume()for hardware button events - Handle errors gracefully: Wrap volume operations in try-catch blocks
- Check watch status: Use
isWatching()to avoid duplicate watch calls - Test on real devices: Volume watching requires physical hardware
Help me improve this plugin and build better tools for the community!
Support through GitHub's official sponsorship program:
- GitHub Sponsors - Most transparent and developer-friendly way to support
Support via crypto donations across multiple networks:
| Network | Address |
|---|---|
| Bitcoin (BTC) | bc1q2k0ftm2fgst22kzj683e8gpau3spfa23ttkg26 |
| USDT (Ethereum) | 0xd6f4d8733c8C23e7bEC8Aeba37F4b3D2e93172d1 |
| USDT (BNB Chain) | 0xd6f4d8733c8C23e7bEC8Aeba37F4b3D2e93172d1 |
| USDT (TRON/TRC20) | TXVy781mQ2tCuQ1BrattXWueUHp1wB5fwt |
| USDT (Solana) | GZ8jmSUUzc4dQF7Cthj2atomvpBZWqccR81N9DL4o1Be |
| USDT (TON) | UQAthXSNIlauj3SrzpDAU4VYxgEVV3niOSmeTPCtMBKGfEAE |
Your contributions help me:
- Upgrade to better development hardware
- Improve workspace and productivity
- Dedicate more time to open source projects
- Add new features faster
- Provide better documentation and examples
- β Star the Project - Give us a star on GitHub to show your support!
- π Report Issues - Help improve the plugin by reporting bugs and suggesting features
- π Improve Docs - Contribute to documentation, examples, and tutorials
- π¬ Spread the Word - Share the plugin with other developers who might find it useful
We welcome contributions! Please see our Contributing Guide for details.
This project is licensed under the MIT License - see the LICENSE file for details.
See CHANGELOG.md for a list of changes and version history.