Skip to content
Open
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 packages/audioplayers_windows/windows/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

#include "audio_player.h"
#include "audioplayers_helpers.h"
#include "platform_thread_handler.h" // NEW: Include platform thread handler
Copy link
Copy Markdown
Collaborator

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?


namespace {

Expand Down Expand Up @@ -51,6 +52,9 @@ class AudioplayersWindowsPlugin : public Plugin {
static inline std::unique_ptr<MethodChannel<EncodableValue>> methods{};
static inline std::unique_ptr<MethodChannel<EncodableValue>> globalMethods{};
static inline std::unique_ptr<EventStreamHandler<>> globalEvents{};

// NEW: Store event channels to prevent them from being destroyed
static inline std::map<std::string, std::unique_ptr<EventChannel<EncodableValue>>> playerEventChannels{};

// Called when a method is called on this plugin's channel from Dart.
void HandleMethodCall(const MethodCall<EncodableValue>& method_call,
Expand All @@ -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<MethodChannel<EncodableValue>>(
binaryMessenger, "xyz.luan/audioplayers",
Expand Down Expand Up @@ -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<EncodableValue>& method_call,
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
Expand All @@ -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<EventChannel<EncodableValue>>(
binaryMessenger, "xyz.luan/audioplayers/events/" + playerId,
&StandardMethodCodec::GetInstance());
Expand All @@ -264,6 +283,9 @@ void AudioplayersWindowsPlugin::CreatePlayer(std::string playerId) {
static_cast<StreamHandler<EncodableValue>*>(eventHandler);
std::unique_ptr<StreamHandler<EncodableValue>> _ptr{_obj_stm_handle};
eventChannel->SetStreamHandler(std::move(_ptr));

// NEW: Store the event channel
playerEventChannels[playerId] = std::move(eventChannel);

auto player =
std::make_unique<AudioPlayer>(playerId, methods.get(), eventHandler);
Expand Down
51 changes: 43 additions & 8 deletions packages/audioplayers_windows/windows/event_stream_handler.h
Original file line number Diff line number Diff line change
@@ -1,29 +1,64 @@
#pragma once

#include <flutter/encodable_value.h>
#include <flutter/event_channel.h>

#include <mutex>
#include <memory>

#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 <typename T = EncodableValue>
class EventStreamHandler : public StreamHandler<T> {
public:
EventStreamHandler() = default;

virtual ~EventStreamHandler() = default;

/**
* Send a success event to Flutter.
* Thread-safe: automatically marshals to platform thread if needed.
*/
void Success(std::unique_ptr<T> _data) {
std::unique_lock<std::mutex> _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<T>(std::move(*_data));

audioplayers::PlatformThreadHandler::RunOnPlatformThread([this, sharedData]() {
std::unique_lock<std::mutex> _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<std::mutex> _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<std::mutex> _ul(m_mtx);
if (m_sink.get()) {
m_sink.get()->Error(code, message, details);
}
});
}

protected:
Expand All @@ -38,11 +73,11 @@ class EventStreamHandler : public StreamHandler<T> {
std::unique_ptr<StreamHandlerError<T>> OnCancelInternal(
const T* arguments) override {
std::unique_lock<std::mutex> _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<EventSink<T>> m_sink;
};
};
204 changes: 204 additions & 0 deletions packages/audioplayers_windows/windows/platform_thread_handler.h
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
Loading