diff --git a/packages/audioplayers_windows/windows/CMakeLists.txt b/packages/audioplayers_windows/windows/CMakeLists.txt index 0943f70ff..1bdb035ba 100644 --- a/packages/audioplayers_windows/windows/CMakeLists.txt +++ b/packages/audioplayers_windows/windows/CMakeLists.txt @@ -35,6 +35,7 @@ add_library(${PLUGIN_NAME} SHARED "audio_player.cpp" "audioplayers_helpers.h" "event_stream_handler.h" + "platform_thread_handler.h" # NEW: Added platform thread handler "MediaEngineExtension.h" "MediaEngineExtension.cpp" "MediaFoundationHelpers.h" diff --git a/packages/audioplayers_windows/windows/audioplayers_windows_plugin.cpp b/packages/audioplayers_windows/windows/audioplayers_windows_plugin.cpp index c84e8cbed..04c7ef4b4 100644 --- a/packages/audioplayers_windows/windows/audioplayers_windows_plugin.cpp +++ b/packages/audioplayers_windows/windows/audioplayers_windows_plugin.cpp @@ -17,6 +17,7 @@ #include "audio_player.h" #include "audioplayers_helpers.h" +#include "platform_thread_handler.h" // NEW: Include platform thread handler namespace { @@ -51,6 +52,9 @@ class AudioplayersWindowsPlugin : public Plugin { static inline std::unique_ptr> methods{}; static inline std::unique_ptr> globalMethods{}; static inline std::unique_ptr> globalEvents{}; + + // NEW: Store event channels to prevent them from being destroyed + static inline std::map>> playerEventChannels{}; // Called when a method is called on this plugin's channel from Dart. void HandleMethodCall(const MethodCall& method_call, @@ -70,6 +74,12 @@ class AudioplayersWindowsPlugin : public Plugin { // static void AudioplayersWindowsPlugin::RegisterWithRegistrar( PluginRegistrarWindows* registrar) { + + // NEW: Initialize platform thread handler FIRST (must be on platform thread) + if (!audioplayers::PlatformThreadHandler::Initialize()) { + OutputDebugStringA("[AudioPlayers] ERROR: Failed to initialize PlatformThreadHandler\n"); + } + binaryMessenger = registrar->messenger(); methods = std::make_unique>( binaryMessenger, "xyz.luan/audioplayers", @@ -103,7 +113,13 @@ void AudioplayersWindowsPlugin::RegisterWithRegistrar( AudioplayersWindowsPlugin::AudioplayersWindowsPlugin() {} -AudioplayersWindowsPlugin::~AudioplayersWindowsPlugin() {} +AudioplayersWindowsPlugin::~AudioplayersWindowsPlugin() { + // NEW: Cleanup platform thread handler + audioplayers::PlatformThreadHandler::Shutdown(); + + // Clear event channels + playerEventChannels.clear(); +} void AudioplayersWindowsPlugin::HandleGlobalMethodCall( const MethodCall& method_call, @@ -115,6 +131,7 @@ void AudioplayersWindowsPlugin::HandleGlobalMethodCall( entry.second->Dispose(); } audioPlayers.clear(); + playerEventChannels.clear(); // NEW: Also clear event channels } else if (method_call.method_name().compare("setAudioContext") == 0) { this->OnGlobalLog("Setting AudioContext is not supported on Windows"); } else if (method_call.method_name().compare("emitLog") == 0) { @@ -247,6 +264,7 @@ void AudioplayersWindowsPlugin::HandleMethodCall( } else if (method_call.method_name().compare("dispose") == 0) { player->Dispose(); audioPlayers.erase(playerId); + playerEventChannels.erase(playerId); // NEW: Also remove event channel } else { result->NotImplemented(); return; @@ -255,6 +273,7 @@ void AudioplayersWindowsPlugin::HandleMethodCall( } void AudioplayersWindowsPlugin::CreatePlayer(std::string playerId) { + // NEW: Create and store event channel to keep it alive auto eventChannel = std::make_unique>( binaryMessenger, "xyz.luan/audioplayers/events/" + playerId, &StandardMethodCodec::GetInstance()); @@ -264,6 +283,9 @@ void AudioplayersWindowsPlugin::CreatePlayer(std::string playerId) { static_cast*>(eventHandler); std::unique_ptr> _ptr{_obj_stm_handle}; eventChannel->SetStreamHandler(std::move(_ptr)); + + // NEW: Store the event channel + playerEventChannels[playerId] = std::move(eventChannel); auto player = std::make_unique(playerId, methods.get(), eventHandler); diff --git a/packages/audioplayers_windows/windows/event_stream_handler.h b/packages/audioplayers_windows/windows/event_stream_handler.h index 577f4c0ef..429fa845a 100644 --- a/packages/audioplayers_windows/windows/event_stream_handler.h +++ b/packages/audioplayers_windows/windows/event_stream_handler.h @@ -1,10 +1,23 @@ +#pragma once + #include #include #include +#include + +#include "platform_thread_handler.h" using namespace flutter; +/** + * Thread-safe EventStreamHandler for audioplayers_windows. + * + * This handler ensures that all EventSink calls are made on the platform thread, + * even when called from MediaFoundation's MTA threads. + * + * IMPORTANT: PlatformThreadHandler::Initialize() must be called before using this handler. + */ template class EventStreamHandler : public StreamHandler { public: @@ -12,18 +25,40 @@ class EventStreamHandler : public StreamHandler { virtual ~EventStreamHandler() = default; + /** + * Send a success event to Flutter. + * Thread-safe: automatically marshals to platform thread if needed. + */ void Success(std::unique_ptr _data) { - std::unique_lock _ul(m_mtx); - if (m_sink.get()) - m_sink.get()->Success(*_data.get()); + // Capture data by moving into shared_ptr for safe cross-thread transfer + auto sharedData = std::make_shared(std::move(*_data)); + + audioplayers::PlatformThreadHandler::RunOnPlatformThread([this, sharedData]() { + std::unique_lock _ul(m_mtx); + if (m_sink.get()) { + m_sink.get()->Success(*sharedData); + } + }); } + /** + * Send an error event to Flutter. + * Thread-safe: automatically marshals to platform thread if needed. + */ void Error(const std::string& error_code, const std::string& error_message, const T& error_details) { - std::unique_lock _ul(m_mtx); - if (m_sink.get()) - m_sink.get()->Error(error_code, error_message, error_details); + // Copy parameters for safe cross-thread transfer + auto code = error_code; + auto message = error_message; + auto details = error_details; + + audioplayers::PlatformThreadHandler::RunOnPlatformThread([this, code, message, details]() { + std::unique_lock _ul(m_mtx); + if (m_sink.get()) { + m_sink.get()->Error(code, message, details); + } + }); } protected: @@ -38,11 +73,11 @@ class EventStreamHandler : public StreamHandler { std::unique_ptr> OnCancelInternal( const T* arguments) override { std::unique_lock _ul(m_mtx); - m_sink.release(); + m_sink.reset(); // Use reset() instead of release() to properly clean up return nullptr; } private: std::mutex m_mtx; std::unique_ptr> m_sink; -}; \ No newline at end of file +}; diff --git a/packages/audioplayers_windows/windows/platform_thread_handler.h b/packages/audioplayers_windows/windows/platform_thread_handler.h new file mode 100644 index 000000000..e531c2141 --- /dev/null +++ b/packages/audioplayers_windows/windows/platform_thread_handler.h @@ -0,0 +1,204 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +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; + + /** + * 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(); + 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(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(callbackPtr), 0); + } else { + // Use PostMessage for asynchronous execution + if (!PostMessageW(m_hwnd, WM_AUDIOPLAYERS_CALLBACK, + reinterpret_cast(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(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 s_instance; +}; + +} // namespace audioplayers