-
-
Notifications
You must be signed in to change notification settings - Fork 887
fix(windows): marshal EventSink calls to platform thread #1961
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
OlehHumeniuk
wants to merge
2
commits into
bluefireteam:main
Choose a base branch
from
OlehHumeniuk:fix/windows-platform-thread-issue
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 1 commit
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
204 changes: 204 additions & 0 deletions
204
packages/audioplayers_windows/windows/platform_thread_handler.h
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,204 @@ | ||
| #pragma once | ||
|
|
||
| #include <windows.h> | ||
| #include <functional> | ||
| #include <queue> | ||
| #include <mutex> | ||
| #include <memory> | ||
| #include <atomic> | ||
|
|
||
| namespace audioplayers { | ||
|
|
||
| // Custom window message for dispatching callbacks to platform thread | ||
| #define WM_AUDIOPLAYERS_CALLBACK (WM_USER + 100) | ||
|
|
||
| /** | ||
| * PlatformThreadHandler - ensures callbacks are executed on the Flutter platform thread. | ||
| * | ||
| * This class creates a hidden window to receive Windows messages, allowing callbacks | ||
| * from MTA threads (MediaFoundation) to be safely dispatched to the platform thread. | ||
| * | ||
| * Usage: | ||
| * 1. Initialize once during plugin registration: PlatformThreadHandler::Initialize() | ||
| * 2. Use RunOnPlatformThread() from any thread to execute code on platform thread | ||
| * 3. Shutdown when plugin is destroyed: PlatformThreadHandler::Shutdown() | ||
| */ | ||
| class PlatformThreadHandler { | ||
| public: | ||
| using Callback = std::function<void()>; | ||
|
|
||
| /** | ||
| * Initialize the handler. Must be called from the platform thread. | ||
| * Creates a hidden window for message dispatching. | ||
| */ | ||
| static bool Initialize() { | ||
| if (s_instance) { | ||
| return true; // Already initialized | ||
| } | ||
|
|
||
| s_instance = std::make_unique<PlatformThreadHandler>(); | ||
| return s_instance->CreateMessageWindow(); | ||
| } | ||
|
|
||
| /** | ||
| * Shutdown and cleanup. Should be called when plugin is destroyed. | ||
| */ | ||
| static void Shutdown() { | ||
| if (s_instance) { | ||
| s_instance->DestroyMessageWindow(); | ||
| s_instance.reset(); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Check if the handler is initialized. | ||
| */ | ||
| static bool IsInitialized() { | ||
| return s_instance != nullptr && s_instance->m_hwnd != nullptr; | ||
| } | ||
|
|
||
| /** | ||
| * Execute a callback on the platform thread. | ||
| * If already on the platform thread, executes immediately. | ||
| * Otherwise, posts a message to the hidden window. | ||
| * | ||
| * @param callback The function to execute on the platform thread. | ||
| * @param synchronous If true, blocks until callback completes. Default is false (async). | ||
| */ | ||
| static void RunOnPlatformThread(Callback callback, bool synchronous = false) { | ||
| if (!s_instance || !s_instance->m_hwnd) { | ||
| // Fallback: execute directly (not ideal, but prevents crash) | ||
| // Log warning in debug builds | ||
| #ifdef _DEBUG | ||
| OutputDebugStringA("[AudioPlayers] Warning: PlatformThreadHandler not initialized, executing callback directly\n"); | ||
| #endif | ||
| callback(); | ||
| return; | ||
| } | ||
|
|
||
| // Check if we're already on the platform thread | ||
| if (GetCurrentThreadId() == s_instance->m_platformThreadId) { | ||
| callback(); | ||
| return; | ||
| } | ||
|
|
||
| // Dispatch to platform thread via message queue | ||
| s_instance->PostCallback(std::move(callback), synchronous); | ||
| } | ||
|
|
||
| /** | ||
| * Get the platform thread ID. | ||
| */ | ||
| static DWORD GetPlatformThreadId() { | ||
| return s_instance ? s_instance->m_platformThreadId : 0; | ||
| } | ||
|
|
||
| private: | ||
| PlatformThreadHandler() | ||
| : m_hwnd(nullptr), | ||
| m_platformThreadId(GetCurrentThreadId()) {} | ||
|
|
||
| ~PlatformThreadHandler() { | ||
| DestroyMessageWindow(); | ||
| } | ||
|
|
||
| // Non-copyable | ||
| PlatformThreadHandler(const PlatformThreadHandler&) = delete; | ||
| PlatformThreadHandler& operator=(const PlatformThreadHandler&) = delete; | ||
|
|
||
| bool CreateMessageWindow() { | ||
| // Register window class | ||
| WNDCLASSEXW wc = {}; | ||
| wc.cbSize = sizeof(WNDCLASSEXW); | ||
| wc.lpfnWndProc = WindowProc; | ||
| wc.hInstance = GetModuleHandle(nullptr); | ||
| wc.lpszClassName = L"AudioPlayersMessageWindow"; | ||
|
|
||
| if (!RegisterClassExW(&wc)) { | ||
| DWORD error = GetLastError(); | ||
| if (error != ERROR_CLASS_ALREADY_EXISTS) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| // Create hidden message-only window | ||
| m_hwnd = CreateWindowExW( | ||
| 0, | ||
| L"AudioPlayersMessageWindow", | ||
| L"", | ||
| 0, | ||
| 0, 0, 0, 0, | ||
| HWND_MESSAGE, // Message-only window | ||
| nullptr, | ||
| GetModuleHandle(nullptr), | ||
| this // Pass this pointer for use in WindowProc | ||
| ); | ||
|
|
||
| if (!m_hwnd) { | ||
| return false; | ||
| } | ||
|
|
||
| // Store this pointer in window user data | ||
| SetWindowLongPtrW(m_hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(this)); | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| void DestroyMessageWindow() { | ||
| if (m_hwnd) { | ||
| DestroyWindow(m_hwnd); | ||
| m_hwnd = nullptr; | ||
| } | ||
| } | ||
|
|
||
| void PostCallback(Callback callback, bool synchronous) { | ||
| // Create callback wrapper on heap | ||
| auto* callbackPtr = new CallbackWrapper{std::move(callback), synchronous}; | ||
|
|
||
| if (synchronous) { | ||
| // Use SendMessage for synchronous execution (blocks until processed) | ||
| SendMessageW(m_hwnd, WM_AUDIOPLAYERS_CALLBACK, | ||
| reinterpret_cast<WPARAM>(callbackPtr), 0); | ||
| } else { | ||
| // Use PostMessage for asynchronous execution | ||
| if (!PostMessageW(m_hwnd, WM_AUDIOPLAYERS_CALLBACK, | ||
| reinterpret_cast<WPARAM>(callbackPtr), 0)) { | ||
| // PostMessage failed, cleanup and execute directly as fallback | ||
| delete callbackPtr; | ||
| #ifdef _DEBUG | ||
| OutputDebugStringA("[AudioPlayers] Warning: PostMessage failed\n"); | ||
| #endif | ||
| } | ||
| } | ||
| } | ||
|
|
||
| static LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { | ||
| if (msg == WM_AUDIOPLAYERS_CALLBACK) { | ||
| auto* wrapper = reinterpret_cast<CallbackWrapper*>(wParam); | ||
| if (wrapper) { | ||
| try { | ||
| wrapper->callback(); | ||
| } catch (...) { | ||
| #ifdef _DEBUG | ||
| OutputDebugStringA("[AudioPlayers] Exception in callback\n"); | ||
| #endif | ||
| } | ||
| delete wrapper; | ||
| } | ||
| return 0; | ||
| } | ||
| return DefWindowProcW(hwnd, msg, wParam, lParam); | ||
| } | ||
|
|
||
| struct CallbackWrapper { | ||
| Callback callback; | ||
| bool synchronous; | ||
| }; | ||
|
|
||
| HWND m_hwnd; | ||
| DWORD m_platformThreadId; | ||
|
|
||
| static inline std::unique_ptr<PlatformThreadHandler> s_instance; | ||
| }; | ||
|
|
||
| } // namespace audioplayers |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While we love documentation, adding a comment to each inserted line doesn't really help us and just generates noise. Could you remove the comments where not necessary?