Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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