Skip to content

Capacitor plugin for advanced volume control with native Android and iOS implementations

Notifications You must be signed in to change notification settings

odion-cloud/capacitor-volume-control

Repository files navigation

Capacitor Volume Control Plugin

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.

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

⚠️ Migration Notice

v2.0+ Breaking Change:

  • The callback parameter in watchVolume(options, callback) is no longer supported.
  • Use the event listener pattern addListener('volumeButtonPressed', callback) instead.
  • The callback receives { direction: 'up' | 'down' }.

Installation

npm install @odion-cloud/capacitor-volume-control
npx cap sync

βœ… No MainActivity Integration Required!

This plugin uses @capacitor-community/volume-buttons for hardware button detection, which means no MainActivity code changes needed. Just install and use!

Setup

  1. Install dependencies:
npm install
  1. Add the plugin:
npm install @odion-cloud/capacitor-volume-control
  1. Sync with native platforms:
npx cap sync

Usage Examples

Basic Volume Control

import { 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 });

Volume Watching

// First, add the event listener
await VolumeControl.addListener('volumeButtonPressed', (event) => {
  console.log('Volume button pressed:', event.direction); // 'up' or 'down'
});

// Then start watching
await VolumeControl.watchVolume({
  disableSystemVolumeHandler: true, // iOS only
  suppressVolumeIndicator: true,    // Android only
});

// Stop watching when done
await VolumeControl.clearWatch();

Advanced Usage

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 {
      // Add listener first
      await VolumeControl.addListener('volumeButtonPressed', this.handleVolumeChange.bind(this));
      
      // Then start watching
      await VolumeControl.watchVolume({
        disableSystemVolumeHandler: true,
        suppressVolumeIndicator: true
      });
      
      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();

Platform-Specific Examples

Android Specific

// Add listener
await VolumeControl.addListener('volumeButtonPressed', (event) => {
  console.log('Volume button pressed:', event.direction);
});

// Suppress volume indicator on Android
await VolumeControl.watchVolume({
  suppressVolumeIndicator: true
});

// Control different volume types
await VolumeControl.setVolumeLevel({
  value: 0.7,
  type: VolumeType.NOTIFICATION
});

iOS Specific

// Add listener
await VolumeControl.addListener('volumeButtonPressed', (event) => {
  console.log('Volume button pressed:', event.direction);
});

// Disable system volume handler on iOS
await VolumeControl.watchVolume({
  disableSystemVolumeHandler: true
});

// Control voice call volume
await VolumeControl.setVolumeLevel({
  value: 0.9,
  type: VolumeType.VOICE_CALL
});

Error Handling

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({});
  await VolumeControl.watchVolume({}); // This will fail
} catch (error) {
  if (error.message.includes('already been watched')) {
    console.error('Volume watching is already active');
  }
}

React Hook Example

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 {
      // Add listener first
      await VolumeControl.addListener('volumeButtonPressed', (event) => {
        console.log('Volume button pressed:', event.direction);
        // You may want to update UI or state here
      });
      
      // Then start watching
      await VolumeControl.watchVolume({
        disableSystemVolumeHandler: true,
        suppressVolumeIndicator: true
      });
      
      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
  };
}

Vue Composition API Example

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 {
      // Add listener first
      await VolumeControl.addListener('volumeButtonPressed', (event) => {
        console.log('Volume button pressed:', event.direction);
      });
      
      // Then start watching
      await VolumeControl.watchVolume({
        disableSystemVolumeHandler: true,
        suppressVolumeIndicator: true
      });
      
      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
  };
}

Angular Service Example

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 {
      // Add listener first
      await VolumeControl.addListener('volumeButtonPressed', (event) => {
        console.log('Volume button pressed:', event.direction);
        // You may want to update with actual logic
      });
      
      // Then start watching
      await VolumeControl.watchVolume({
        disableSystemVolumeHandler: true,
        suppressVolumeIndicator: true
      });
      
      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;
    }
  }
}

Testing

Run the example:

npm start

Build for production:

npm run build

Test on device:

npx cap run android
npx cap run ios

Platform Support

πŸ“± Supported Devices

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 ⚠️ Development Only All modern browsers Mock implementation for testing

πŸ† Android Version Compatibility

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 ⚠️ Basic Limited external storage access

🎯 Volume Types Support

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

API Reference

Methods

getVolumeLevel(options?)

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)

setVolumeLevel(options)

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)

watchVolume(options)

Start watching for volume button presses.

watchVolume({
  disableSystemVolumeHandler?: boolean;  // iOS: disable system UI
  suppressVolumeIndicator?: boolean;     // Android: hide volume UI
}): Promise<void>

addListener(eventName, callback)

Listen for volume button press events.

addListener(
  'volumeButtonPressed',
  callback: (event: { direction: 'up' | 'down' }) => void
): Promise<PluginListenerHandle>

// Event data: { direction: 'up' | 'down' }

clearWatch()

Stop watching for volume changes.

clearWatch(): Promise<void>

isWatching()

Check if volume watching is currently active.

isWatching(): Promise<{ value: boolean }>

Configuration Options

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

Error Handling

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

Best Practices

  1. Always clean up listeners: Remove event listeners when components unmount with removeAllListeners()
  2. Add listener before watching: Call addListener() before watchVolume() to ensure events are captured
  3. Handle errors gracefully: Wrap volume operations in try-catch blocks
  4. Check watch status: Use isWatching() to avoid duplicate watch calls
  5. Test on real devices: Volume watching requires physical hardware

Support This Project

Help me improve this plugin and build better tools for the community!

🀝 GitHub Sponsors

Support through GitHub's official sponsorship program:

β‚Ώ Cryptocurrency 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

πŸ’» Why Support?

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

🀝 Other Ways to Help

  • ⭐ 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

Contributing

We welcome contributions! Please see our Contributing Guide for details.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Troubleshooting

Android: Kotlin Version Incompatibility

If you encounter the error:

Module was compiled with an incompatible version of Kotlin. The binary version of its metadata is 1.9.0, expected version is 1.7.1

Solution: Update your project's Kotlin version in android/build.gradle:

buildscript {
    ext.kotlin_version = '1.9.10'  // Update from older version
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

This plugin requires Kotlin 1.9.0+ and Android Gradle Plugin 7.4.2+.

iOS: Pod Name Mismatch

If you're using the scoped npm package name and encounter pod installation issues:

Symptom: Pod name in Podfile (OdionCloudCapacitorVolumeControl) doesn't match the podspec file name (CapacitorVolumeControl)

Solution: Reference the package with an alias in package.json:

{
  "dependencies": {
    "capacitor-volume-control": "npm:@odion-cloud/capacitor-volume-control@^1.0.13"
  }
}

Then run:

npm install
npx cap sync ios

Other Common Issues

Volume watching not working on emulators/simulators

  • Volume watching requires physical hardware buttons
  • Test on real devices for volume button detection

Permission denied errors

  • Ensure you've added the required permissions to your AndroidManifest.xml or Info.plist
  • Request permissions at runtime before calling volume control methods

Build errors after updating

  • Clean your build folders:
    cd android && ./gradlew clean
    # or
    cd ios && rm -rf Pods && pod install
  • Invalidate caches in Android Studio: File β†’ Invalidate Caches β†’ Restart

Changelog

See CHANGELOG.md for a list of changes and version history.