Compare commits

...

4 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
a7a3e8bb0d Fix code review issues: equality operator bug and improve code clarity
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2026-02-04 14:13:14 +00:00
copilot-swe-agent[bot]
cbb617ab5d Update documentation for media key device source detection
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2026-02-04 14:12:20 +00:00
copilot-swe-agent[bot]
37fbae5eab Add device source detection for Windows media keys using Raw Input API
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2026-02-04 14:11:10 +00:00
copilot-swe-agent[bot]
ae9ee8b513 Initial plan 2026-02-04 14:05:03 +00:00
10 changed files with 302 additions and 14 deletions

View File

@@ -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**:

View File

@@ -25,7 +25,7 @@ class MediaKeyHandler {
_smtc?.disableSmtc();
} else {
mediaKeyDetector.setIsPlaying(isPlaying: false);
mediaKeyDetector.removeListener(_onMediaKeyDetectedListener);
mediaKeyDetector.removeListenerWithDevice(_onMediaKeyDetectedListenerWithDevice);
}
} else {
FlutterVolumeController.addListener(
@@ -80,7 +80,7 @@ class MediaKeyHandler {
);
_smtc!.buttonPressStream.listen(_onMediaKeyPressedListener);
} else {
mediaKeyDetector.addListener(_onMediaKeyDetectedListener);
mediaKeyDetector.addListenerWithDevice(_onMediaKeyDetectedListenerWithDevice);
mediaKeyDetector.setIsPlaying(isPlaying: true);
}
}
@@ -88,7 +88,11 @@ class MediaKeyHandler {
}
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(),

View File

@@ -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();

View File

@@ -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) {
@@ -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 = {

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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
@@ -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';
@@ -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

View File

@@ -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);
}
});
}

View File

@@ -12,6 +12,10 @@
#include <map>
#include <memory>
#include <atomic>
#include <string>
#include <vector>
#include <sstream>
#include <iomanip>
namespace {
@@ -44,6 +48,15 @@ class MediaKeyDetectorWindows : public flutter::Plugin {
// Unregister global hotkeys
void UnregisterHotkeys();
// Register for raw input from keyboard devices
void RegisterRawInput(HWND hwnd);
// Unregister raw input
void UnregisterRawInput(HWND hwnd);
// Get device identifier from device handle
std::string GetDeviceIdentifier(HANDLE hDevice);
// Handle Windows messages
std::optional<LRESULT> HandleWindowProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam);
@@ -52,6 +65,10 @@ class MediaKeyDetectorWindows : public flutter::Plugin {
std::atomic<bool> is_playing_{false};
int window_proc_id_ = -1;
bool hotkeys_registered_ = false;
bool raw_input_registered_ = false;
// Cache for device identifiers
std::map<HANDLE, std::string> device_cache_;
};
// static
@@ -104,6 +121,8 @@ MediaKeyDetectorWindows::MediaKeyDetectorWindows(flutter::PluginRegistrarWindows
}
MediaKeyDetectorWindows::~MediaKeyDetectorWindows() {
HWND hwnd = registrar_->GetView()->GetNativeWindow();
UnregisterRawInput(hwnd);
UnregisterHotkeys();
if (window_proc_id_ != -1) {
registrar_->UnregisterTopLevelWindowProcDelegate(window_proc_id_);
@@ -124,10 +143,13 @@ void MediaKeyDetectorWindows::HandleMethodCall(
if (is_playing_it != arguments->end()) {
if (auto* is_playing = std::get_if<bool>(&is_playing_it->second)) {
is_playing_.store(*is_playing);
HWND hwnd = registrar_->GetView()->GetNativeWindow();
if (*is_playing) {
RegisterHotkeys();
RegisterRawInput(hwnd);
} else {
UnregisterHotkeys();
UnregisterRawInput(hwnd);
}
result->Success();
return;
@@ -185,8 +207,131 @@ void MediaKeyDetectorWindows::UnregisterHotkeys() {
hotkeys_registered_ = false;
}
void MediaKeyDetectorWindows::RegisterRawInput(HWND hwnd) {
if (raw_input_registered_) {
return;
}
// Register for raw input from keyboard devices
RAWINPUTDEVICE rid[1];
// Keyboard devices
rid[0].usUsagePage = 0x01; // Generic Desktop Controls
rid[0].usUsage = 0x06; // Keyboard
rid[0].dwFlags = RIDEV_INPUTSINK; // Receive input even when not in foreground
rid[0].hwndTarget = hwnd;
if (RegisterRawInputDevices(rid, 1, sizeof(rid[0]))) {
raw_input_registered_ = true;
}
}
void MediaKeyDetectorWindows::UnregisterRawInput(HWND hwnd) {
if (!raw_input_registered_) {
return;
}
// Unregister raw input
RAWINPUTDEVICE rid[1];
rid[0].usUsagePage = 0x01;
rid[0].usUsage = 0x06;
rid[0].dwFlags = RIDEV_REMOVE;
rid[0].hwndTarget = nullptr;
RegisterRawInputDevices(rid, 1, sizeof(rid[0]));
raw_input_registered_ = false;
device_cache_.clear();
}
std::string MediaKeyDetectorWindows::GetDeviceIdentifier(HANDLE hDevice) {
// Check cache first
auto it = device_cache_.find(hDevice);
if (it != device_cache_.end()) {
return it->second;
}
// Get device name
UINT size = 0;
GetRawInputDeviceInfoA(hDevice, RIDI_DEVICENAME, nullptr, &size);
if (size == 0) {
return "Unknown Device";
}
std::vector<char> name(size);
if (GetRawInputDeviceInfoA(hDevice, RIDI_DEVICENAME, name.data(), &size) == static_cast<UINT>(-1)) {
return "Unknown Device";
}
std::string deviceName(name.data());
// Cache the result
device_cache_[hDevice] = deviceName;
return deviceName;
}
std::optional<LRESULT> MediaKeyDetectorWindows::HandleWindowProc(
HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) {
// Handle raw input messages for device-specific detection
if (message == WM_INPUT && event_sink_) {
UINT dwSize;
GetRawInputData((HRAWINPUT)lparam, RID_INPUT, nullptr, &dwSize, sizeof(RAWINPUTHEADER));
std::vector<BYTE> buffer(dwSize);
if (GetRawInputData((HRAWINPUT)lparam, RID_INPUT, buffer.data(), &dwSize, sizeof(RAWINPUTHEADER)) != dwSize) {
return std::nullopt;
}
RAWINPUT* raw = (RAWINPUT*)buffer.data();
if (raw->header.dwType == RIM_TYPEKEYBOARD) {
RAWKEYBOARD& keyboard = raw->data.keyboard;
// Check for media keys
int key_index = -1;
// Media keys have VKey codes
// Check for key down event (RI_KEY_MAKE is defined as 0, so we check both explicitly)
if (keyboard.Flags == RI_KEY_MAKE) { // Key down event
switch (keyboard.VKey) {
case VK_MEDIA_PLAY_PAUSE:
key_index = 0; // MediaKey.playPause
break;
case VK_MEDIA_PREV_TRACK:
key_index = 1; // MediaKey.rewind
break;
case VK_MEDIA_NEXT_TRACK:
key_index = 2; // MediaKey.fastForward
break;
case VK_VOLUME_UP:
key_index = 3; // MediaKey.volumeUp
break;
case VK_VOLUME_DOWN:
key_index = 4; // MediaKey.volumeDown
break;
}
if (key_index >= 0) {
// Get device identifier
std::string deviceId = GetDeviceIdentifier(raw->header.hDevice);
// Send event with both key index and device identifier
flutter::EncodableMap event_data;
event_data[EncodableValue("key")] = EncodableValue(key_index);
event_data[EncodableValue("device")] = EncodableValue(deviceId);
event_sink_->Success(EncodableValue(event_data));
return 0;
}
}
}
}
// Fallback to hotkey messages (for compatibility)
if (message == WM_HOTKEY && event_sink_) {
int key_index = -1;
@@ -210,7 +355,12 @@ std::optional<LRESULT> MediaKeyDetectorWindows::HandleWindowProc(
}
if (key_index >= 0) {
event_sink_->Success(EncodableValue(key_index));
// Send event with key index only (no device info for hotkey)
flutter::EncodableMap event_data;
event_data[EncodableValue("key")] = EncodableValue(key_index);
event_data[EncodableValue("device")] = EncodableValue("HID Device");
event_sink_->Success(EncodableValue(event_data));
}
return 0;