Add device source detection for Windows media keys using Raw Input API

Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-02-04 14:11:10 +00:00
parent ae9ee8b513
commit 37fbae5eab
7 changed files with 234 additions and 9 deletions

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 == key &&
deviceId == deviceId;
@override
int get hashCode => key.hashCode ^ deviceId.hashCode;
}

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,130 @@ 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
if (keyboard.Flags == RI_KEY_MAKE || keyboard.Flags == 0) { // 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 +354,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;