mirror of
https://github.com/jonasbark/swiftcontrol.git
synced 2026-02-18 00:17:40 +01:00
Compare commits
4 Commits
main
...
copilot/ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7a3e8bb0d | ||
|
|
cbb617ab5d | ||
|
|
37fbae5eab | ||
|
|
ae9ee8b513 |
@@ -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**:
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user