Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
### Unreleased

**Features**:
- Windows: Add device source detection for media hotkeys
- Distinguish between multiple bluetooth media controllers
- Configure different actions for each controller even when they have the same buttons
- Uses Windows Raw Input API to identify device sources

### 4.6.0 (28-01-2026)

**Features**:
Expand Down
10 changes: 7 additions & 3 deletions lib/utils/media_key_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class MediaKeyHandler {
_smtc?.disableSmtc();
} else {
mediaKeyDetector.setIsPlaying(isPlaying: false);
mediaKeyDetector.removeListener(_onMediaKeyDetectedListener);
mediaKeyDetector.removeListenerWithDevice(_onMediaKeyDetectedListenerWithDevice);
}
} else {
FlutterVolumeController.addListener(
Expand Down Expand Up @@ -80,15 +80,19 @@ class MediaKeyHandler {
);
_smtc!.buttonPressStream.listen(_onMediaKeyPressedListener);
} else {
mediaKeyDetector.addListener(_onMediaKeyDetectedListener);
mediaKeyDetector.addListenerWithDevice(_onMediaKeyDetectedListenerWithDevice);
mediaKeyDetector.setIsPlaying(isPlaying: true);
}
}
});
}

bool _onMediaKeyDetectedListener(MediaKey mediaKey) {
final hidDevice = HidDevice('HID Device');
return _onMediaKeyDetectedListenerWithDevice(mediaKey, 'HID Device');
}

bool _onMediaKeyDetectedListenerWithDevice(MediaKey mediaKey, String deviceId) {
final hidDevice = HidDevice(deviceId);

var availableDevice = core.connection.controllerDevices.firstOrNullWhere(
(e) => e.toString() == hidDevice.toString(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,24 @@ class MediaKeyDetector {
_platform.addListener(listener);
}

/// Listen for the media key event with device information
void addListenerWithDevice(void Function(MediaKey mediaKey, String deviceId) listener) {
_lazilyInitialize();
_platform.addListenerWithDevice(listener);
}

/// Remove the previously registered listener
void removeListener(void Function(MediaKey mediaKey) listener) {
_lazilyInitialize();
_platform.removeListener(listener);
}

/// Remove the previously registered listener with device information
void removeListenerWithDevice(void Function(MediaKey mediaKey, String deviceId) listener) {
_lazilyInitialize();
_platform.removeListenerWithDevice(listener);
}

void _lazilyInitialize() {
if (!_initialized) {
_platform.initialize();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ abstract class MediaKeyDetectorPlatform extends PlatformInterface {
void initialize();

final List<void Function(MediaKey mediaKey)> _listeners = [];
final List<void Function(MediaKey mediaKey, String deviceId)> _listenersWithDevice = [];

/// Listen for the media key event
void addListener(void Function(MediaKey mediaKey) listener) {
Expand All @@ -58,16 +59,33 @@ abstract class MediaKeyDetectorPlatform extends PlatformInterface {
}
}

/// Listen for the media key event with device information
void addListenerWithDevice(void Function(MediaKey mediaKey, String deviceId) listener) {
if (!_listenersWithDevice.contains(listener)) {
_listenersWithDevice.add(listener);
}
}

/// Remove the previously registered listener
void removeListener(void Function(MediaKey mediaKey) listener) {
_listeners.remove(listener);
}

/// Remove the previously registered listener with device information
void removeListenerWithDevice(void Function(MediaKey mediaKey, String deviceId) listener) {
_listenersWithDevice.remove(listener);
}

/// Trigger all listeners to indicate that the specified media key was pressed
void triggerListeners(MediaKey mediaKey) {
void triggerListeners(MediaKey mediaKey, [String? deviceId]) {
for (final l in _listeners) {
l(mediaKey);
}
if (deviceId != null) {
for (final l in _listenersWithDevice) {
l(mediaKey, deviceId);
}
}
}

final Map<LogicalKeyboardKey, MediaKey> _keyMap = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export './media_key.dart' show MediaKey;
export './media_key_event.dart' show MediaKeyEvent;
export './method_channel_media_key_detector.dart'
show MethodChannelMediaKeyDetector;
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/// Represents a media key event with device information
class MediaKeyEvent {
/// Creates a media key event
const MediaKeyEvent({
required this.key,
required this.deviceId,
});

/// The media key that was pressed
final String key;

/// The unique identifier of the device that sent the event
final String deviceId;

@override
String toString() => 'MediaKeyEvent(key: $key, deviceId: $deviceId)';

@override
bool operator ==(Object other) =>
identical(this, other) ||
other is MediaKeyEvent &&
runtimeType == other.runtimeType &&
key == other.key &&
deviceId == other.deviceId;

@override
int get hashCode => key.hashCode ^ deviceId.hashCode;
}
9 changes: 9 additions & 0 deletions media_key_detector/media_key_detector_windows/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
# 0.0.3

- **NEW**: Add device source detection using Windows Raw Input API
- Media key events now include unique device identifier
- Enables distinguishing between multiple bluetooth media controllers
- Adds `addListenerWithDevice` API for device-aware event handling
- Maintains backward compatibility with existing `addListener` API
- Falls back to RegisterHotKey API for compatibility

# 0.0.2

- Implement global media key detection using Windows RegisterHotKey API
Expand Down
55 changes: 50 additions & 5 deletions media_key_detector/media_key_detector_windows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ The windows implementation of `media_key_detector`.

## Features

This plugin provides global media key detection on Windows using the Windows `RegisterHotKey` API. This allows your application to respond to media keys (play/pause, next track, previous track, volume up, volume down) even when it's not the focused application.
This plugin provides global media key detection on Windows with device source identification. This allows your application to respond to media keys (play/pause, next track, previous track, volume up, volume down) from multiple devices and distinguish which device sent each event.

### Supported Media Keys

Expand All @@ -19,17 +19,33 @@ This plugin provides global media key detection on Windows using the Windows `Re
### Implementation Details

The plugin uses:
- `RegisterHotKey` Windows API for global hotkey registration
- Event channels for communicating media key events to Dart
- Window message handlers to process WM_HOTKEY messages
- **Raw Input API** for device-specific media key detection (primary method)
- `RegisterHotKey` Windows API for global hotkey registration (fallback)
- Event channels for communicating media key events with device information to Dart
- Window message handlers to process WM_INPUT and WM_HOTKEY messages

Hotkeys are registered when `setIsPlaying(true)` is called and automatically unregistered when `setIsPlaying(false)` is called or when the plugin is destroyed.
The Raw Input API allows the plugin to identify which physical device (e.g., keyboard, bluetooth remote) sent the media key event. This enables users with multiple media controllers to configure different actions for each device.

Hotkeys and raw input are registered when `setIsPlaying(true)` is called and automatically unregistered when `setIsPlaying(false)` is called or when the plugin is destroyed.

### Device Source Detection

When a media key is pressed, the plugin provides:
- The media key that was pressed (e.g., playPause, fastForward)
- The unique device identifier of the source device

This enables scenarios where:
- A user has two bluetooth media remotes
- Both remotes have a "play" button
- Each remote can be configured to trigger different actions

## Usage

This package is [endorsed][endorsed_link], which means you can simply use `media_key_detector`
normally. This package will be automatically included in your app when you do.

### Basic Usage (without device information)

```dart
import 'package:media_key_detector/media_key_detector.dart';

Expand Down Expand Up @@ -58,6 +74,35 @@ mediaKeyDetector.addListener((MediaKey key) {
});
```

### Advanced Usage (with device identification)

```dart
import 'package:media_key_detector/media_key_detector.dart';

// Enable media key detection
mediaKeyDetector.setIsPlaying(isPlaying: true);

// Listen for media key events with device information
mediaKeyDetector.addListenerWithDevice((MediaKey key, String deviceId) {
// deviceId contains the unique identifier of the device that sent the event
// For example: "\\?\HID#VID_046D&PID_C52B&MI_00#..."

print('Media key $key pressed by device: $deviceId');

// Configure different actions based on device
if (deviceId.contains('VID_046D')) {
// Handle keys from Logitech device
handleLogitechRemote(key);
} else if (deviceId.contains('VID_05AC')) {
// Handle keys from Apple device
handleAppleKeyboard(key);
} else {
// Handle keys from other devices
handleGenericDevice(key);
}
});
```

[endorsed_link]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin
[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg
[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,26 @@ class MediaKeyDetectorWindows extends MediaKeyDetectorPlatform {
@override
void initialize() {
_eventChannel.receiveBroadcastStream().listen((event) {
final keyIdx = event as int;
MediaKey? key;
if (keyIdx > -1 && keyIdx < MediaKey.values.length) {
key = MediaKey.values[keyIdx];
String? deviceId;

// Check if event is a map (new format with device info)
if (event is Map) {
final keyIdx = event['key'] as int?;
deviceId = event['device'] as String?;

if (keyIdx != null && keyIdx > -1 && keyIdx < MediaKey.values.length) {
key = MediaKey.values[keyIdx];
}
} else if (event is int) {
// Backward compatibility: old format with just key index
if (event > -1 && event < MediaKey.values.length) {
key = MediaKey.values[event];
}
}

if (key != null) {
triggerListeners(key);
triggerListeners(key, deviceId);
}
});
}
Expand Down
Loading