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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,4 @@ app.*.map.json

service-account.json
.env
_codeql_detected_source_root
5 changes: 4 additions & 1 deletion media_key_detector/media_key_detector_windows/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# 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

# 0.0.1

Expand Down
41 changes: 41 additions & 0 deletions media_key_detector/media_key_detector_windows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,52 @@

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

### 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;
}
});
```

[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 @@ -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
Expand All @@ -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
Expand All @@ -27,11 +37,13 @@ class MediaKeyDetectorWindows extends MediaKeyDetectorPlatform {

@override
Future<bool> getIsPlaying() async {
return _isPlaying;
final isPlaying = await methodChannel.invokeMethod<bool>('getIsPlaying');
return isPlaying ?? _isPlaying;
}

@override
Future<void> setIsPlaying({required bool isPlaying}) async {
_isPlaying = isPlaying;
await methodChannel.invokeMethod<void>('setIsPlaying', <String, dynamic>{'isPlaying': isPlaying});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,27 @@
#include <flutter/method_channel.h>
#include <flutter/plugin_registrar_windows.h>
#include <flutter/standard_method_codec.h>
#include <flutter/event_channel.h>
#include <flutter/event_stream_handler_functions.h>

#include <map>
#include <memory>
#include <atomic>

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;

class MediaKeyDetectorWindows : public flutter::Plugin {
public:
static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar);

MediaKeyDetectorWindows();
MediaKeyDetectorWindows(flutter::PluginRegistrarWindows *registrar);

virtual ~MediaKeyDetectorWindows();

Expand All @@ -27,6 +35,21 @@ class MediaKeyDetectorWindows : public flutter::Plugin {
void HandleMethodCall(
const flutter::MethodCall<flutter::EncodableValue> &method_call,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);

// Register global hotkeys for media keys
void RegisterHotkeys();

// Unregister global hotkeys
void UnregisterHotkeys();

// Handle Windows messages
std::optional<LRESULT> HandleWindowProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam);

flutter::PluginRegistrarWindows *registrar_;
std::unique_ptr<flutter::EventSink<>> event_sink_;
std::atomic<bool> is_playing_{false};
int window_proc_id_ = -1;
bool hotkeys_registered_ = false;
};

// static
Expand All @@ -37,31 +60,151 @@ void MediaKeyDetectorWindows::RegisterWithRegistrar(
registrar->messenger(), "media_key_detector_windows",
&flutter::StandardMethodCodec::GetInstance());

auto plugin = std::make_unique<MediaKeyDetectorWindows>();
auto plugin = std::make_unique<MediaKeyDetectorWindows>(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<flutter::EventChannel<flutter::EncodableValue>>(
registrar->messenger(), "media_key_detector_windows_events",
&flutter::StandardMethodCodec::GetInstance());

auto event_handler = std::make_unique<flutter::StreamHandlerFunctions<>>(
[plugin_pointer = plugin.get()](
const flutter::EncodableValue* arguments,
std::unique_ptr<flutter::EventSink<>>&& events)
-> std::unique_ptr<flutter::StreamHandlerError<>> {
plugin_pointer->event_sink_ = std::move(events);
return nullptr;
},
[plugin_pointer = plugin.get()](const flutter::EncodableValue* arguments)
-> std::unique_ptr<flutter::StreamHandlerError<>> {
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<flutter::EncodableValue> &method_call,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> 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<flutter::EncodableMap>(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<bool>(&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);

// 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) {
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);
}
}

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

hotkeys_registered_ = false;
}

std::optional<LRESULT> 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;
}

if (key_index >= 0) {
event_sink_->Success(EncodableValue(key_index));
}

return 0;
}

return std::nullopt;
}

} // namespace

void MediaKeyDetectorWindowsRegisterWithRegistrar(
Expand Down