diff --git a/media_key_detector/media_key_detector_platform_interface/lib/media_key_detector_platform_interface.dart b/media_key_detector/media_key_detector_platform_interface/lib/media_key_detector_platform_interface.dart index 30e4279..a6d5572 100644 --- a/media_key_detector/media_key_detector_platform_interface/lib/media_key_detector_platform_interface.dart +++ b/media_key_detector/media_key_detector_platform_interface/lib/media_key_detector_platform_interface.dart @@ -74,6 +74,8 @@ abstract class MediaKeyDetectorPlatform extends PlatformInterface { LogicalKeyboardKey.mediaPlay: MediaKey.playPause, LogicalKeyboardKey.mediaRewind: MediaKey.rewind, LogicalKeyboardKey.mediaFastForward: MediaKey.fastForward, + LogicalKeyboardKey.audioVolumeUp: MediaKey.volumeUp, + LogicalKeyboardKey.audioVolumeDown: MediaKey.volumeDown, }; /// The default handler to use if this platform doesn't need to implement any diff --git a/media_key_detector/media_key_detector_platform_interface/lib/src/media_key.dart b/media_key_detector/media_key_detector_platform_interface/lib/src/media_key.dart index 6baa44e..1a2d45e 100644 --- a/media_key_detector/media_key_detector_platform_interface/lib/src/media_key.dart +++ b/media_key_detector/media_key_detector_platform_interface/lib/src/media_key.dart @@ -8,4 +8,10 @@ enum MediaKey { /// The fast-forward media button fastForward, + + /// The volume up button + volumeUp, + + /// The volume down button + volumeDown, } diff --git a/media_key_detector/media_key_detector_windows/CHANGELOG.md b/media_key_detector/media_key_detector_windows/CHANGELOG.md index 589555b..406c80b 100644 --- a/media_key_detector/media_key_detector_windows/CHANGELOG.md +++ b/media_key_detector/media_key_detector_windows/CHANGELOG.md @@ -1,6 +1,10 @@ # 0.0.2 -- TBD +- Implement global media key detection using Windows RegisterHotKey API +- Add event channel support for media key events +- Media keys now work even when app is not focused +- Improved error handling for hotkey registration +- Added support for volume up and volume down hotkeys # 0.0.1 diff --git a/media_key_detector/media_key_detector_windows/README.md b/media_key_detector/media_key_detector_windows/README.md index f64f638..0eed398 100644 --- a/media_key_detector/media_key_detector_windows/README.md +++ b/media_key_detector/media_key_detector_windows/README.md @@ -4,11 +4,60 @@ 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. + +### Supported Media Keys + +- Play/Pause (VK_MEDIA_PLAY_PAUSE) +- Next Track (VK_MEDIA_NEXT_TRACK) +- Previous Track (VK_MEDIA_PREV_TRACK) +- Volume Up (VK_VOLUME_UP) +- Volume Down (VK_VOLUME_DOWN) + +### 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 + +Hotkeys are registered when `setIsPlaying(true)` is called and automatically unregistered when `setIsPlaying(false)` is called or when the plugin is destroyed. + ## 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. +```dart +import 'package:media_key_detector/media_key_detector.dart'; + +// Enable media key detection +mediaKeyDetector.setIsPlaying(isPlaying: true); + +// Listen for media key events +mediaKeyDetector.addListener((MediaKey key) { + switch (key) { + case MediaKey.playPause: + // Handle play/pause + break; + case MediaKey.fastForward: + // Handle next track + break; + case MediaKey.rewind: + // Handle previous track + break; + case MediaKey.volumeUp: + // Handle volume up + break; + case MediaKey.volumeDown: + // Handle volume down + break; + } +}); +``` + [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 diff --git a/media_key_detector/media_key_detector_windows/lib/media_key_detector_windows.dart b/media_key_detector/media_key_detector_windows/lib/media_key_detector_windows.dart index 6d9b07d..e856823 100644 --- a/media_key_detector/media_key_detector_windows/lib/media_key_detector_windows.dart +++ b/media_key_detector/media_key_detector_windows/lib/media_key_detector_windows.dart @@ -5,6 +5,7 @@ import 'package:media_key_detector_platform_interface/media_key_detector_platfor /// The Windows implementation of [MediaKeyDetectorPlatform]. class MediaKeyDetectorWindows extends MediaKeyDetectorPlatform { bool _isPlaying = false; + final _eventChannel = const EventChannel('media_key_detector_windows_events'); /// The method channel used to interact with the native platform. @visibleForTesting @@ -17,7 +18,16 @@ class MediaKeyDetectorWindows extends MediaKeyDetectorPlatform { @override void initialize() { - ServicesBinding.instance.keyboard.addHandler(defaultHandler); + _eventChannel.receiveBroadcastStream().listen((event) { + final keyIdx = event as int; + MediaKey? key; + if (keyIdx > -1 && keyIdx < MediaKey.values.length) { + key = MediaKey.values[keyIdx]; + } + if (key != null) { + triggerListeners(key); + } + }); } @override @@ -27,11 +37,13 @@ class MediaKeyDetectorWindows extends MediaKeyDetectorPlatform { @override Future getIsPlaying() async { - return _isPlaying; + final isPlaying = await methodChannel.invokeMethod('getIsPlaying'); + return isPlaying ?? _isPlaying; } @override Future setIsPlaying({required bool isPlaying}) async { _isPlaying = isPlaying; + await methodChannel.invokeMethod('setIsPlaying', {'isPlaying': isPlaying}); } } diff --git a/media_key_detector/media_key_detector_windows/windows/media_key_detector_windows_plugin.cpp b/media_key_detector/media_key_detector_windows/windows/media_key_detector_windows_plugin.cpp index f3587ac..a1e9938 100644 --- a/media_key_detector/media_key_detector_windows/windows/media_key_detector_windows_plugin.cpp +++ b/media_key_detector/media_key_detector_windows/windows/media_key_detector_windows_plugin.cpp @@ -6,19 +6,29 @@ #include #include #include +#include +#include #include #include +#include namespace { using flutter::EncodableValue; +// Hotkey IDs for media keys +constexpr int HOTKEY_PLAY_PAUSE = 1; +constexpr int HOTKEY_NEXT_TRACK = 2; +constexpr int HOTKEY_PREV_TRACK = 3; +constexpr int HOTKEY_VOLUME_UP = 4; +constexpr int HOTKEY_VOLUME_DOWN = 5; + class MediaKeyDetectorWindows : public flutter::Plugin { public: static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar); - MediaKeyDetectorWindows(); + MediaKeyDetectorWindows(flutter::PluginRegistrarWindows *registrar); virtual ~MediaKeyDetectorWindows(); @@ -27,6 +37,21 @@ class MediaKeyDetectorWindows : public flutter::Plugin { void HandleMethodCall( const flutter::MethodCall &method_call, std::unique_ptr> result); + + // Register global hotkeys for media keys + void RegisterHotkeys(); + + // Unregister global hotkeys + void UnregisterHotkeys(); + + // Handle Windows messages + std::optional HandleWindowProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam); + + flutter::PluginRegistrarWindows *registrar_; + std::unique_ptr> event_sink_; + std::atomic is_playing_{false}; + int window_proc_id_ = -1; + bool hotkeys_registered_ = false; }; // static @@ -37,31 +62,163 @@ void MediaKeyDetectorWindows::RegisterWithRegistrar( registrar->messenger(), "media_key_detector_windows", &flutter::StandardMethodCodec::GetInstance()); - auto plugin = std::make_unique(); + auto plugin = std::make_unique(registrar); channel->SetMethodCallHandler( [plugin_pointer = plugin.get()](const auto &call, auto result) { plugin_pointer->HandleMethodCall(call, std::move(result)); }); + // Set up event channel for media key events + auto event_channel = + std::make_unique>( + registrar->messenger(), "media_key_detector_windows_events", + &flutter::StandardMethodCodec::GetInstance()); + + auto event_handler = std::make_unique>( + [plugin_pointer = plugin.get()]( + const flutter::EncodableValue* arguments, + std::unique_ptr>&& events) + -> std::unique_ptr> { + plugin_pointer->event_sink_ = std::move(events); + return nullptr; + }, + [plugin_pointer = plugin.get()](const flutter::EncodableValue* arguments) + -> std::unique_ptr> { + plugin_pointer->event_sink_ = nullptr; + return nullptr; + }); + + event_channel->SetStreamHandler(std::move(event_handler)); + registrar->AddPlugin(std::move(plugin)); } -MediaKeyDetectorWindows::MediaKeyDetectorWindows() {} +MediaKeyDetectorWindows::MediaKeyDetectorWindows(flutter::PluginRegistrarWindows *registrar) + : registrar_(registrar) { + // Register a window procedure to handle hotkey messages + window_proc_id_ = registrar_->RegisterTopLevelWindowProcDelegate( + [this](HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { + return HandleWindowProc(hwnd, message, wparam, lparam); + }); +} -MediaKeyDetectorWindows::~MediaKeyDetectorWindows() {} +MediaKeyDetectorWindows::~MediaKeyDetectorWindows() { + UnregisterHotkeys(); + if (window_proc_id_ != -1) { + registrar_->UnregisterTopLevelWindowProcDelegate(window_proc_id_); + } +} void MediaKeyDetectorWindows::HandleMethodCall( const flutter::MethodCall &method_call, std::unique_ptr> result) { if (method_call.method_name().compare("getPlatformName") == 0) { result->Success(EncodableValue("Windows")); - } - else { + } else if (method_call.method_name().compare("getIsPlaying") == 0) { + result->Success(EncodableValue(is_playing_.load())); + } else if (method_call.method_name().compare("setIsPlaying") == 0) { + const auto* arguments = std::get_if(method_call.arguments()); + if (arguments) { + auto is_playing_it = arguments->find(EncodableValue("isPlaying")); + if (is_playing_it != arguments->end()) { + if (auto* is_playing = std::get_if(&is_playing_it->second)) { + is_playing_.store(*is_playing); + if (*is_playing) { + RegisterHotkeys(); + } else { + UnregisterHotkeys(); + } + result->Success(); + return; + } + } + } + result->Error("INVALID_ARGUMENT", "isPlaying argument is required"); + } else { result->NotImplemented(); } } +void MediaKeyDetectorWindows::RegisterHotkeys() { + if (hotkeys_registered_) { + return; + } + + HWND hwnd = registrar_->GetView()->GetNativeWindow(); + + // Register global hotkeys for media keys + // MOD_NOREPEAT prevents the hotkey from repeating when held down + bool play_pause_ok = RegisterHotKey(hwnd, HOTKEY_PLAY_PAUSE, MOD_NOREPEAT, VK_MEDIA_PLAY_PAUSE); + bool next_ok = RegisterHotKey(hwnd, HOTKEY_NEXT_TRACK, MOD_NOREPEAT, VK_MEDIA_NEXT_TRACK); + bool prev_ok = RegisterHotKey(hwnd, HOTKEY_PREV_TRACK, MOD_NOREPEAT, VK_MEDIA_PREV_TRACK); + bool vol_up_ok = RegisterHotKey(hwnd, HOTKEY_VOLUME_UP, MOD_NOREPEAT, VK_VOLUME_UP); + bool vol_down_ok = RegisterHotKey(hwnd, HOTKEY_VOLUME_DOWN, MOD_NOREPEAT, VK_VOLUME_DOWN); + + // If all registrations succeeded, mark as registered + // If any failed, unregister the successful ones to maintain consistent state + if (play_pause_ok && next_ok && prev_ok && vol_up_ok && vol_down_ok) { + hotkeys_registered_ = true; + } else { + // Clean up any successful registrations + if (play_pause_ok) UnregisterHotKey(hwnd, HOTKEY_PLAY_PAUSE); + if (next_ok) UnregisterHotKey(hwnd, HOTKEY_NEXT_TRACK); + if (prev_ok) UnregisterHotKey(hwnd, HOTKEY_PREV_TRACK); + if (vol_up_ok) UnregisterHotKey(hwnd, HOTKEY_VOLUME_UP); + if (vol_down_ok) UnregisterHotKey(hwnd, HOTKEY_VOLUME_DOWN); + } +} + +void MediaKeyDetectorWindows::UnregisterHotkeys() { + if (!hotkeys_registered_) { + return; + } + + HWND hwnd = registrar_->GetView()->GetNativeWindow(); + + UnregisterHotKey(hwnd, HOTKEY_PLAY_PAUSE); + UnregisterHotKey(hwnd, HOTKEY_NEXT_TRACK); + UnregisterHotKey(hwnd, HOTKEY_PREV_TRACK); + UnregisterHotKey(hwnd, HOTKEY_VOLUME_UP); + UnregisterHotKey(hwnd, HOTKEY_VOLUME_DOWN); + + hotkeys_registered_ = false; +} + +std::optional MediaKeyDetectorWindows::HandleWindowProc( + HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { + if (message == WM_HOTKEY && event_sink_) { + int key_index = -1; + + // Map hotkey ID to media key index + switch (wparam) { + case HOTKEY_PLAY_PAUSE: + key_index = 0; // MediaKey.playPause + break; + case HOTKEY_PREV_TRACK: + key_index = 1; // MediaKey.rewind + break; + case HOTKEY_NEXT_TRACK: + key_index = 2; // MediaKey.fastForward + break; + case HOTKEY_VOLUME_UP: + key_index = 3; // MediaKey.volumeUp + break; + case HOTKEY_VOLUME_DOWN: + key_index = 4; // MediaKey.volumeDown + break; + } + + if (key_index >= 0) { + event_sink_->Success(EncodableValue(key_index)); + } + + return 0; + } + + return std::nullopt; +} + } // namespace void MediaKeyDetectorWindowsRegisterWithRegistrar(