device_volume 2.0.0 copy "device_volume: ^2.0.0" to clipboard
device_volume: ^2.0.0 copied to clipboard

Control and observe the device volume from Flutter through native platform channels on Android, iOS, macOS, Windows, and Linux.

device_volume #

pub.dev CI

Control and observe the system output volume from Flutter on Android, iOS, macOS, Windows, and Linux.

Version 2.0 uses Flutter MethodChannel and EventChannel implementations on every platform. Calls are asynchronous, volume events come from native observers, and no Dart polling, FFI, JNIgen, or background isolate is required.

Features #

  • Read the current volume using one normalized 0–100 scale.
  • Set, increment, and decrement volume where the operating system permits it.
  • Observe native volume changes, including hardware button changes.
  • Address Android media, ring, alarm, notification, voice-call, and system streams.
  • Discover runtime capabilities before presenting controls.
  • Receive the actual state applied by the platform after every operation.
  • Handle stable, typed exceptions with structured native diagnostics.
  • Use CocoaPods or Swift Package Manager on Apple platforms.

Requirements #

  • Dart 3.11.3 or newer within the Dart 3 release line.
  • Flutter 3.3.0 or newer.
  • Android API 24 or newer.
  • iOS 13 or newer.
  • macOS 10.15 or newer.
  • A Windows host with Core Audio.
  • PulseAudio or PipeWire-Pulse on Linux.

The package is developed and validated with Flutter 3.44.6 and Dart 3.12.2. Swift Package Manager integration requires Flutter 3.44 or newer; older Flutter applications continue to use CocoaPods.

Installation #

Add the dependency to your application:

dependencies:
  device_volume: ^2.0.0

Then resolve packages:

flutter pub get

No Dart-side initialization or platform permissions are required for normal media-volume access.

Quick start #

import 'package:device_volume/device_volume.dart';

Future<void> configureVolume() async {
  // Capabilities can vary by platform and by the selected output device.
  final capabilities = await DeviceVolume.getCapabilities();
  if (!capabilities.canRead) return;

  final current = await DeviceVolume.getVolume();
  print('Current volume: ${current.value}%');

  if (capabilities.writeSupport != VolumeWriteSupport.unsupported) {
    // Always use the returned state: native step sizes can change the result.
    final applied = await DeviceVolume.setVolume(50);
    print('Applied volume: ${applied.value}%');
  }
}

Observe native changes:

final subscription = DeviceVolume.streamVolume().listen(
  (state) {
    print('${state.channel.name}: ${state.value}%');
  },
  onError: (Object error) {
    print('Volume observation failed: $error');
  },
);

// Cancel when the owning widget/service is disposed.
await subscription.cancel();

Public API #

All calls use VolumeChannel.media by default.

Future<VolumeState> DeviceVolume.getVolume({
  VolumeChannel channel = VolumeChannel.media,
});

Future<VolumeState> DeviceVolume.setVolume(
  int value, {
  VolumeChannel channel = VolumeChannel.media,
  bool showSystemUi = false,
});

Future<VolumeState> DeviceVolume.incrementVolume({
  VolumeChannel channel = VolumeChannel.media,
  bool showSystemUi = false,
});

Future<VolumeState> DeviceVolume.decrementVolume({
  VolumeChannel channel = VolumeChannel.media,
  bool showSystemUi = false,
});

Stream<VolumeState> DeviceVolume.streamVolume({
  VolumeChannel channel = VolumeChannel.media,
});

Future<VolumeCapabilities> DeviceVolume.getCapabilities();

VolumeState #

VolumeState is an immutable snapshot:

Property Meaning
value Normalized integer from 0 to 100.
min Public minimum, always 0.
max Public maximum, always 100.
normalized Same value represented from 0.0 to 1.0.
isMuted Whether the native output reports a muted/zero state.
channel Logical channel represented by this snapshot.

Native values are normalized in one Dart boundary. For example, Android media volume 7 in a native 0–15 range is exposed as 47.

VolumeCapabilities #

Capabilities describe the current device rather than only its operating system:

final capabilities = await DeviceVolume.getCapabilities();

if (capabilities.supportsChannel(VolumeChannel.ring)) {
  final ring = await DeviceVolume.getVolume(channel: VolumeChannel.ring);
}

writeSupport has three possible values:

  • supported: the platform exposes a supported native write API;
  • bestEffort: a change can be attempted but is not guaranteed;
  • unsupported: the selected platform/device cannot be changed.

Platform support #

Platform Channels Read Write Native events System UI
Android media, ring, alarm, notification, voiceCall, system Yes Yes, unless fixed/policy restricted ContentObserver + deduplication Yes
iOS device media Yes Best effort through MPVolumeView AVAudioSession.outputVolume KVO MPVolumeView
iOS Simulator media Yes No KVO No effective volume UI
macOS media Yes when the route exposes software volume Device dependent CoreAudio property listeners No
Windows media Yes Yes IAudioEndpointVolumeCallback No
Linux media Yes Yes PulseAudio subscriptions No

The capabilities returned at runtime are authoritative. Hardware, output routes, device policy, and operating-system versions can reduce support.

Platform details and restrictions #

Android #

The Android plugin uses the application Context supplied by FlutterPluginBinding and controls public AudioManager stream APIs.

  • showSystemUi: true maps to AudioManager.FLAG_SHOW_UI.
  • Increment/decrement use Android's native stream step.
  • Devices reporting AudioManager.isVolumeFixed expose write support as unsupported.
  • Do Not Disturb can block ring or notification changes. Android may throw a SecurityException, which becomes PermissionDeniedException.
  • The plugin does not open notification-policy settings or request policy access automatically. The host application owns that user flow.
  • Android does not publish a stable public per-stream event. The plugin listens to system settings changes, re-queries supported streams, and deduplicates events without using a Dart timer.

iOS #

Apple exposes AVAudioSession.outputVolume for reading and observing the system output volume, but does not expose a public programmatic setter.

On a physical device, this package performs a best-effort change using the slider supplied by MPVolumeView. The returned VolumeState is read after the request and is the only value applications should treat as applied. The operating system can ignore the request.

The iOS Simulator cannot change system volume. It reports VolumeWriteSupport.unsupported, and write attempts throw UnsupportedOperationException.

Applications intended for App Store distribution should evaluate this best-effort behavior against their own product and review requirements.

macOS #

macOS uses the default CoreAudio output device. The implementation prefers virtual main volume and falls back to scalar master/channel properties.

Some HDMI, DisplayPort, aggregate, and professional audio devices do not expose software volume. Those devices report write support as unsupported or return BackendNotAvailableException; the plugin never reports a successful write without CoreAudio accepting it.

Changing the default output device refreshes listeners and emits the new state.

Windows #

Windows uses IMMDeviceEnumerator and IAudioEndpointVolume for the default multimedia render endpoint. Volume and mute callbacks are marshalled back to the Flutter window thread before sending Dart events. Default endpoint changes release the previous COM interfaces and attach to the new endpoint.

Linux #

Linux uses one reusable PulseAudio context integrated with Flutter's GLib main loop. It works with PulseAudio and PipeWire's pipewire-pulse compatibility service.

If no audio server or default sink is available, calls fail with BackendNotAvailableException. Writes are asynchronous and never run a blocking PulseAudio main loop on Flutter's platform thread.

Linux build dependencies normally include:

sudo apt install libpulse-dev libpulse-mainloop-glib0

Distribution package names can differ.

Error handling #

All package failures extend DeviceVolumeException:

try {
  await DeviceVolume.setVolume(
    80,
    channel: VolumeChannel.ring,
    showSystemUi: true,
  );
} on PermissionDeniedException catch (error) {
  // Android notification policy/DND can reach this branch.
  print(error.details);
} on UnsupportedOperationException catch (error) {
  // The platform or current device cannot perform this operation.
  print(error.message);
} on DeviceVolumeException catch (error) {
  // Stable code plus structured platform diagnostics.
  print('${error.code}: ${error.message}');
}
Exception Stable code
UnsupportedOperationException unsupported_operation
InvalidVolumeValueException invalid_volume_value
PermissionDeniedException permission_denied
BackendNotAvailableException backend_not_available
NativeBackendException native_backend_failure
VolumeObservationException volume_observation_failure
PluginDetachedException plugin_detached

details can contain platform, operation, backend, channel, and native diagnostic information.

Architecture #

The Dart layer uses two fixed channel names:

  • dev.arcas.device_volume/methods for commands;
  • dev.arcas.device_volume/events for native changes.

One native plugin instance is created per Flutter Engine. Native observers and callbacks start when Dart listens, stop when the last subscription is cancelled, and are also released when the engine detaches.

The Dart event transport owns one broadcast EventChannel subscription and filters states by VolumeChannel. This avoids one native observer per Dart listener.

Migrating from 0.1.x #

Version 2.0 intentionally changes the public API from synchronous integers to asynchronous VolumeState objects.

Read volume:

// 0.1.x
final value = DeviceVolume.getVolume();

// 2.0.0
final state = await DeviceVolume.getVolume();
final value = state.value;

Set volume:

// 0.1.x
final value = DeviceVolume.setVolume(50);

// 2.0.0
final state = await DeviceVolume.setVolume(50);
final value = state.value;

Observe volume:

// 0.1.x
DeviceVolume.streamVolume().listen((int value) {});

// 2.0.0
DeviceVolume.streamVolume().listen((VolumeState state) {});

All getVolumeCompute, setVolumeCompute, incrementVolumeCompute, and decrementVolumeCompute methods were removed. Platform channels are already asynchronous and do not require compute().

Testing and contributing #

Run Dart analysis and unit tests:

flutter analyze
flutter test

Run the hardware-aware integration suite from the example application:

cd example
flutter test integration_test -d <device-id>

Integration tests restore the original volume after supported write checks. iOS write behavior must be validated on a physical device; simulator coverage only verifies reads, events, capabilities, and expected write rejection.

Native build checks:

cd example
flutter build apk --debug
flutter build ios --simulator --debug --no-codesign
flutter build macos --debug
flutter build windows --debug
flutter build linux --debug

The repository includes official Dart and Flutter agent skills in .agents/skills. Development validation uses the official Dart/Flutter MCP server for formatting, analysis, tests, runtime errors, devices, and integration flows when an MCP-capable client is available.

License #

MIT. See LICENSE.

1
likes
160
points
135
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Control and observe the device volume from Flutter through native platform channels on Android, iOS, macOS, Windows, and Linux.

Repository (GitHub)
View/report issues

Topics

#volume #audio #method-channel #platform

License

MIT (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on device_volume

Packages that implement device_volume