feat(music): mpv audio playback engine with gapless queue service
Audio-only mpv core on every platform (dedicated com.plezy/mpv_audio_player channels): parameterized android/windows/ linux mpv plugins and a new apple MpvAudioPlayerCore, all skipping video/window paths (vid=no, audio-display=no, gapless-audio=weak). MusicPlaybackService drives an in-memory queue with shuffle/repeat, file-loaded-event gapless arming (property edges coalesce and the android bridge drops them), per-track progress reporting, OS media controls, audio focus, sleep timer, and error auto-skip. PlaybackCoordinator enforces one live native player: starting video disposes the audio core first.
This commit is contained in:
@@ -98,9 +98,11 @@ bool FlutterWindow::OnCreate() {
|
||||
}
|
||||
RegisterPlugins(flutter_controller_->engine());
|
||||
|
||||
// Register mpv player plugin.
|
||||
// Register mpv player plugins (video + dedicated audio-only music core).
|
||||
OutputDebugStringA("FlutterWindow: About to register MpvPlayerPlugin\n");
|
||||
MpvPlayerPluginRegisterWithRegistrar(flutter_controller_->engine()->GetRegistrarForPlugin("MpvPlayerPlugin"));
|
||||
MpvAudioPlayerPluginRegisterWithRegistrar(
|
||||
flutter_controller_->engine()->GetRegistrarForPlugin("MpvAudioPlayerPlugin"));
|
||||
OutputDebugStringA("FlutterWindow: MpvPlayerPlugin registered\n");
|
||||
|
||||
RegisterWindowChannel();
|
||||
|
||||
@@ -73,7 +73,7 @@ void EnsureMpvInnerSubclassed(HWND host) {
|
||||
|
||||
} // namespace
|
||||
|
||||
MpvPlayer::MpvPlayer() {}
|
||||
MpvPlayer::MpvPlayer(bool audio_only) : audio_only_(audio_only) {}
|
||||
|
||||
MpvPlayer::~MpvPlayer() { Dispose(); }
|
||||
|
||||
@@ -88,31 +88,43 @@ bool MpvPlayer::Initialize(HWND view) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create a child window for mpv to render into, parented to the Flutter
|
||||
// |view|. The video child then sits in the view's own per-window layer
|
||||
// stack, above the view's (never-painted) layer-1 content and below the
|
||||
// engine's topmost DComp visual carrying the UI. WS_CLIPSIBLINGS keeps it
|
||||
// from painting over neighboring view children. Mouse input over the video
|
||||
// is delivered to mpv's own inner window (on mpv's thread); the subclass
|
||||
// installed in EnsureMpvInnerSubclassed forwards it back to the view.
|
||||
hwnd_ = ::CreateWindowExW(
|
||||
WS_EX_NOPARENTNOTIFY, L"STATIC", L"", WS_CHILD | WS_CLIPSIBLINGS, 0, 0, 100, 100, view, nullptr,
|
||||
GetModuleHandle(nullptr), nullptr);
|
||||
if (!hwnd_) {
|
||||
mpv_destroy(mpv_);
|
||||
mpv_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
g_forward_target_view = view;
|
||||
if (audio_only_) {
|
||||
// Windowless music core: no HWND, no VO, no video decode. vid=no keeps
|
||||
// embedded cover art from ever becoming a video track, and
|
||||
// force-window/audio-display make sure mpv never opens a video output
|
||||
// for it either.
|
||||
mpv_set_option_string(mpv_, "vid", "no");
|
||||
mpv_set_option_string(mpv_, "force-window", "no");
|
||||
mpv_set_option_string(mpv_, "audio-display", "no");
|
||||
mpv_set_option_string(mpv_, "gapless-audio", "weak");
|
||||
} else {
|
||||
// Create a child window for mpv to render into, parented to the Flutter
|
||||
// |view|. The video child then sits in the view's own per-window layer
|
||||
// stack, above the view's (never-painted) layer-1 content and below the
|
||||
// engine's topmost DComp visual carrying the UI. WS_CLIPSIBLINGS keeps it
|
||||
// from painting over neighboring view children. Mouse input over the video
|
||||
// is delivered to mpv's own inner window (on mpv's thread); the subclass
|
||||
// installed in EnsureMpvInnerSubclassed forwards it back to the view.
|
||||
hwnd_ = ::CreateWindowExW(
|
||||
WS_EX_NOPARENTNOTIFY, L"STATIC", L"", WS_CHILD | WS_CLIPSIBLINGS, 0, 0, 100, 100, view, nullptr,
|
||||
GetModuleHandle(nullptr), nullptr);
|
||||
if (!hwnd_) {
|
||||
mpv_destroy(mpv_);
|
||||
mpv_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
g_forward_target_view = view;
|
||||
|
||||
// Set the wid option to embed mpv in our window.
|
||||
int64_t wid = reinterpret_cast<int64_t>(hwnd_);
|
||||
mpv_set_option(mpv_, "wid", MPV_FORMAT_INT64, &wid);
|
||||
// Set the wid option to embed mpv in our window.
|
||||
int64_t wid = reinterpret_cast<int64_t>(hwnd_);
|
||||
mpv_set_option(mpv_, "wid", MPV_FORMAT_INT64, &wid);
|
||||
|
||||
mpv_set_option_string(mpv_, "vo", "gpu-next");
|
||||
mpv_set_option_string(mpv_, "gpu-api", "auto");
|
||||
// hwdec is set from Flutter via setProperty based on user preference
|
||||
}
|
||||
|
||||
// Configure mpv for embedded playback.
|
||||
mpv_set_option_string(mpv_, "vo", "gpu-next");
|
||||
mpv_set_option_string(mpv_, "gpu-api", "auto");
|
||||
// hwdec is set from Flutter via setProperty based on user preference
|
||||
mpv_set_option_string(mpv_, "keep-open", "yes");
|
||||
mpv_set_option_string(mpv_, "idle", "yes");
|
||||
mpv_set_option_string(mpv_, "input-default-bindings", "no");
|
||||
@@ -122,12 +134,14 @@ bool MpvPlayer::Initialize(HWND view) {
|
||||
mpv_set_option_string(mpv_, "input-media-keys", "no");
|
||||
mpv_set_option_string(mpv_, "osc", "no");
|
||||
|
||||
// Let mpv use display/context detection instead of forcing HDR signaling.
|
||||
mpv_set_option_string(mpv_, "target-colorspace-hint", "auto");
|
||||
if (!audio_only_) {
|
||||
// Let mpv use display/context detection instead of forcing HDR signaling.
|
||||
mpv_set_option_string(mpv_, "target-colorspace-hint", "auto");
|
||||
|
||||
// Fallback tone mapping when display doesn't support HDR
|
||||
mpv_set_option_string(mpv_, "tone-mapping", "auto");
|
||||
mpv_set_option_string(mpv_, "hdr-compute-peak", "auto");
|
||||
// Fallback tone mapping when display doesn't support HDR
|
||||
mpv_set_option_string(mpv_, "tone-mapping", "auto");
|
||||
mpv_set_option_string(mpv_, "hdr-compute-peak", "auto");
|
||||
}
|
||||
|
||||
// When WASAPI becomes unavailable (sleep, device unplug), fall back to null
|
||||
// audio output instead of permanently dropping the audio track. Recovery is
|
||||
@@ -140,15 +154,19 @@ bool MpvPlayer::Initialize(HWND view) {
|
||||
// Initialize mpv.
|
||||
int err = mpv_initialize(mpv_);
|
||||
if (err < 0) {
|
||||
::DestroyWindow(hwnd_);
|
||||
hwnd_ = nullptr;
|
||||
if (hwnd_) {
|
||||
::DestroyWindow(hwnd_);
|
||||
hwnd_ = nullptr;
|
||||
}
|
||||
mpv_destroy(mpv_);
|
||||
mpv_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Observe video-params/sig-peak for HDR detection
|
||||
mpv_observe_property(mpv_, 0, "video-params/sig-peak", MPV_FORMAT_DOUBLE);
|
||||
// Observe video-params/sig-peak for HDR detection (video core only).
|
||||
if (!audio_only_) {
|
||||
mpv_observe_property(mpv_, 0, "video-params/sig-peak", MPV_FORMAT_DOUBLE);
|
||||
}
|
||||
mpv_observe_property(mpv_, 0, "current-ao", MPV_FORMAT_STRING);
|
||||
// Native observation so audio recovery doesn't depend on the Dart side
|
||||
// choosing to observe the device list.
|
||||
@@ -192,11 +210,13 @@ void MpvPlayer::Dispose() {
|
||||
if (hwnd_) {
|
||||
::DestroyWindow(hwnd_);
|
||||
hwnd_ = nullptr;
|
||||
}
|
||||
|
||||
// The subclassed inner window died with hwnd_; clear the forwarding state.
|
||||
g_mpv_inner_hwnd = nullptr;
|
||||
g_mpv_inner_original_proc = nullptr;
|
||||
// The subclassed inner window died with hwnd_; clear the forwarding
|
||||
// state. Only the owner of the window may do this: the audio-only core
|
||||
// (which never has an hwnd_) must not wipe the video instance's state.
|
||||
g_mpv_inner_hwnd = nullptr;
|
||||
g_mpv_inner_original_proc = nullptr;
|
||||
}
|
||||
|
||||
observed_properties_.clear();
|
||||
}
|
||||
|
||||
@@ -23,13 +23,16 @@ class MpvPlayer {
|
||||
public:
|
||||
using EventCallback = std::function<void(const flutter::EncodableValue&)>;
|
||||
|
||||
MpvPlayer();
|
||||
// |audio_only| runs mpv as a windowless music core: no child HWND, no VO,
|
||||
// video decode disabled entirely (vid=no).
|
||||
explicit MpvPlayer(bool audio_only = false);
|
||||
~MpvPlayer();
|
||||
|
||||
// Initializes mpv and creates the video window as a child of the Flutter
|
||||
// |view| window. The flutter-plezy engine presents the UI on a topmost
|
||||
// DirectComposition visual, so the video child composites beneath it in the
|
||||
// same HWND.
|
||||
// same HWND. In audio-only mode |view| is ignored (pass nullptr) and no
|
||||
// window is created.
|
||||
bool Initialize(HWND view);
|
||||
|
||||
// Disposes mpv and the video window.
|
||||
@@ -97,6 +100,7 @@ class MpvPlayer {
|
||||
uint64_t RegisterGetPropertyRequest(GetPropertyCallback callback);
|
||||
GetPropertyCallback TakeGetPropertyRequest(uint64_t request_id);
|
||||
|
||||
const bool audio_only_;
|
||||
mpv_handle* mpv_ = nullptr;
|
||||
HWND hwnd_ = nullptr;
|
||||
|
||||
|
||||
@@ -13,29 +13,41 @@ void MpvPlayerPluginRegisterWithRegistrar(FlutterDesktopPluginRegistrarRef regis
|
||||
flutter::PluginRegistrarManager::GetInstance()->GetRegistrar<flutter::PluginRegistrarWindows>(registrar));
|
||||
}
|
||||
|
||||
void MpvAudioPlayerPluginRegisterWithRegistrar(FlutterDesktopPluginRegistrarRef registrar) {
|
||||
mpv::MpvPlayerPlugin::RegisterWithRegistrar(
|
||||
flutter::PluginRegistrarManager::GetInstance()->GetRegistrar<flutter::PluginRegistrarWindows>(registrar),
|
||||
"com.plezy/mpv_audio_player", /*audio_only=*/true);
|
||||
}
|
||||
|
||||
namespace mpv {
|
||||
|
||||
namespace {
|
||||
constexpr UINT kPlatformTaskMessage = WM_APP + 0x4D50;
|
||||
}
|
||||
constexpr UINT kAudioPlatformTaskMessage = WM_APP + 0x4D51;
|
||||
} // namespace
|
||||
|
||||
void MpvPlayerPlugin::RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar) {
|
||||
auto plugin = std::make_unique<MpvPlayerPlugin>(registrar);
|
||||
void MpvPlayerPlugin::RegisterWithRegistrar(
|
||||
flutter::PluginRegistrarWindows* registrar, const std::string& channel_name, bool audio_only) {
|
||||
auto plugin = std::make_unique<MpvPlayerPlugin>(registrar, channel_name, audio_only);
|
||||
registrar->AddPlugin(std::move(plugin));
|
||||
}
|
||||
|
||||
MpvPlayerPlugin::MpvPlayerPlugin(flutter::PluginRegistrarWindows* registrar)
|
||||
: registrar_(registrar), platform_thread_id_(::GetCurrentThreadId()) {
|
||||
MpvPlayerPlugin::MpvPlayerPlugin(
|
||||
flutter::PluginRegistrarWindows* registrar, const std::string& channel_name, bool audio_only)
|
||||
: registrar_(registrar),
|
||||
platform_thread_id_(::GetCurrentThreadId()),
|
||||
audio_only_(audio_only),
|
||||
platform_task_message_(audio_only ? kAudioPlatformTaskMessage : kPlatformTaskMessage) {
|
||||
// Create method channel.
|
||||
method_channel_ = std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>(
|
||||
registrar->messenger(), "com.plezy/mpv_player", &flutter::StandardMethodCodec::GetInstance());
|
||||
registrar->messenger(), channel_name, &flutter::StandardMethodCodec::GetInstance());
|
||||
|
||||
method_channel_->SetMethodCallHandler(
|
||||
[this](const auto& call, auto result) { HandleMethodCall(call, std::move(result)); });
|
||||
|
||||
// Create event channel.
|
||||
event_channel_ = std::make_unique<flutter::EventChannel<flutter::EncodableValue>>(
|
||||
registrar->messenger(), "com.plezy/mpv_player/events", &flutter::StandardMethodCodec::GetInstance());
|
||||
registrar->messenger(), channel_name + "/events", &flutter::StandardMethodCodec::GetInstance());
|
||||
|
||||
auto handler = std::make_unique<flutter::StreamHandlerFunctions<flutter::EncodableValue>>(
|
||||
[this](
|
||||
@@ -92,7 +104,7 @@ void MpvPlayerPlugin::PostToPlatformThread(std::function<void()> task) {
|
||||
}
|
||||
}
|
||||
|
||||
if (post_wakeup && !::PostMessage(flutter_window_, kPlatformTaskMessage, 0, 0)) {
|
||||
if (post_wakeup && !::PostMessage(flutter_window_, platform_task_message_, 0, 0)) {
|
||||
// Wakeup lost (e.g. message queue full during a log storm); let the next
|
||||
// enqueue retry instead of stranding the queue.
|
||||
std::lock_guard<std::mutex> lock(platform_tasks_mutex_);
|
||||
@@ -133,7 +145,7 @@ void MpvPlayerPlugin::HandleMethodCall(
|
||||
// topmost DComp visual — there is no separate container window to manage.
|
||||
proc_id_ =
|
||||
registrar_->RegisterTopLevelWindowProcDelegate([this](HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) {
|
||||
if (message == kPlatformTaskMessage) {
|
||||
if (message == platform_task_message_) {
|
||||
DrainPlatformTasks();
|
||||
return std::optional<HRESULT>(0);
|
||||
}
|
||||
@@ -156,18 +168,21 @@ void MpvPlayerPlugin::HandleMethodCall(
|
||||
// and below the view's topmost DComp visual carrying the UI (layer 4). As
|
||||
// a *sibling* of the view, either the view's never-painted white content
|
||||
// covers the video or the video covers the UI — the in-subtree placement
|
||||
// is the only ordering that yields white < video < UI.
|
||||
HWND view = GetChildWindow();
|
||||
// is the only ordering that yields white < video < UI. The audio-only
|
||||
// core is windowless, so it gets no view at all.
|
||||
HWND view = audio_only_ ? nullptr : GetChildWindow();
|
||||
|
||||
player_ = std::make_unique<MpvPlayer>();
|
||||
player_ = std::make_unique<MpvPlayer>(audio_only_);
|
||||
bool success = player_->Initialize(view);
|
||||
|
||||
if (success) {
|
||||
// Set up event callback.
|
||||
player_->SetEventCallback([this](const flutter::EncodableValue& event) { SendEvent(event); });
|
||||
|
||||
// Start hidden.
|
||||
player_->SetVisible(false);
|
||||
if (!audio_only_) {
|
||||
// Start hidden.
|
||||
player_->SetVisible(false);
|
||||
}
|
||||
result->Success(flutter::EncodableValue(true));
|
||||
} else {
|
||||
player_.reset(); // Clear the player so we don't have a half-initialized state
|
||||
@@ -343,6 +358,12 @@ void MpvPlayerPlugin::HandleMethodCall(
|
||||
std::get<int32_t>(id_it->second));
|
||||
result->Success();
|
||||
} else if (method == "setVisible") {
|
||||
if (audio_only_) {
|
||||
// Windowless core: nothing to show or hide, tolerate as a success no-op.
|
||||
result->Success();
|
||||
return;
|
||||
}
|
||||
|
||||
const auto* args = method_call.arguments();
|
||||
if (!args || !std::holds_alternative<flutter::EncodableMap>(*args)) {
|
||||
result->Error("INVALID_ARGS", "Expected map argument");
|
||||
@@ -365,6 +386,12 @@ void MpvPlayerPlugin::HandleMethodCall(
|
||||
|
||||
result->Success();
|
||||
} else if (method == "setVideoRect") {
|
||||
if (audio_only_) {
|
||||
// Windowless core: no rect to position, tolerate as a success no-op.
|
||||
result->Success();
|
||||
return;
|
||||
}
|
||||
|
||||
const auto* args = method_call.arguments();
|
||||
if (!args || !std::holds_alternative<flutter::EncodableMap>(*args)) {
|
||||
result->Error("INVALID_ARGS", "Expected map argument");
|
||||
@@ -404,13 +431,16 @@ void MpvPlayerPlugin::HandleMethodCall(
|
||||
player_->SetRect(rect, dpr);
|
||||
}
|
||||
|
||||
result->Success();
|
||||
} else if (audio_only_ && method == "updateFrame") {
|
||||
// No frames to pump on the windowless core; tolerate as a success no-op.
|
||||
result->Success();
|
||||
} else if (method == "isInitialized") {
|
||||
bool initialized = player_ && player_->IsInitialized();
|
||||
result->Success(flutter::EncodableValue(initialized));
|
||||
|
||||
// --- Display mode matching ---
|
||||
} else if (method == "getDisplayModes") {
|
||||
// --- Display mode matching (video instance only) ---
|
||||
} else if (!audio_only_ && method == "getDisplayModes") {
|
||||
HWND hwnd = GetWindow();
|
||||
auto modes = display_mode_manager_.EnumerateDisplayModes(hwnd);
|
||||
flutter::EncodableList list;
|
||||
@@ -418,11 +448,11 @@ void MpvPlayerPlugin::HandleMethodCall(
|
||||
list.push_back(flutter::EncodableValue(DisplayModeToMap(mode)));
|
||||
}
|
||||
result->Success(flutter::EncodableValue(list));
|
||||
} else if (method == "getCurrentDisplayMode") {
|
||||
} else if (!audio_only_ && method == "getCurrentDisplayMode") {
|
||||
HWND hwnd = GetWindow();
|
||||
auto mode = display_mode_manager_.GetCurrentMode(hwnd);
|
||||
result->Success(flutter::EncodableValue(DisplayModeToMap(mode)));
|
||||
} else if (method == "setDisplayMode") {
|
||||
} else if (!audio_only_ && method == "setDisplayMode") {
|
||||
const auto* args = method_call.arguments();
|
||||
if (!args || !std::holds_alternative<flutter::EncodableMap>(*args)) {
|
||||
result->Error("INVALID_ARGS", "Expected map argument");
|
||||
@@ -438,17 +468,17 @@ void MpvPlayerPlugin::HandleMethodCall(
|
||||
bool success =
|
||||
display_mode_manager_.SetDisplayMode(hwnd, get_int("width"), get_int("height"), get_int("refreshRate"));
|
||||
result->Success(flutter::EncodableValue(success));
|
||||
} else if (method == "restoreDisplayMode") {
|
||||
} else if (!audio_only_ && method == "restoreDisplayMode") {
|
||||
HWND hwnd = GetWindow();
|
||||
bool success = display_mode_manager_.RestoreOriginalMode(hwnd);
|
||||
result->Success(flutter::EncodableValue(success));
|
||||
} else if (method == "isHDRSupported") {
|
||||
} else if (!audio_only_ && method == "isHDRSupported") {
|
||||
HWND hwnd = GetWindow();
|
||||
result->Success(flutter::EncodableValue(display_mode_manager_.IsHDRSupported(hwnd)));
|
||||
} else if (method == "isHDREnabled") {
|
||||
} else if (!audio_only_ && method == "isHDREnabled") {
|
||||
HWND hwnd = GetWindow();
|
||||
result->Success(flutter::EncodableValue(display_mode_manager_.IsHDREnabled(hwnd)));
|
||||
} else if (method == "setSystemHDR") {
|
||||
} else if (!audio_only_ && method == "setSystemHDR") {
|
||||
const auto* args = method_call.arguments();
|
||||
if (!args || !std::holds_alternative<flutter::EncodableMap>(*args)) {
|
||||
result->Error("INVALID_ARGS", "Expected map argument");
|
||||
@@ -464,13 +494,13 @@ void MpvPlayerPlugin::HandleMethodCall(
|
||||
HWND hwnd = GetWindow();
|
||||
bool success = display_mode_manager_.SetHDREnabled(hwnd, enabled);
|
||||
result->Success(flutter::EncodableValue(success));
|
||||
} else if (method == "restoreSystemHDR") {
|
||||
} else if (!audio_only_ && method == "restoreSystemHDR") {
|
||||
HWND hwnd = GetWindow();
|
||||
bool success = display_mode_manager_.RestoreOriginalHDRState(hwnd);
|
||||
result->Success(flutter::EncodableValue(success));
|
||||
} else if (method == "isModeChanged") {
|
||||
} else if (!audio_only_ && method == "isModeChanged") {
|
||||
result->Success(flutter::EncodableValue(display_mode_manager_.IsModeChanged()));
|
||||
} else if (method == "isHDRChanged") {
|
||||
} else if (!audio_only_ && method == "isHDRChanged") {
|
||||
result->Success(flutter::EncodableValue(display_mode_manager_.IsHDRChanged()));
|
||||
} else {
|
||||
result->NotImplemented();
|
||||
|
||||
@@ -13,20 +13,30 @@
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <queue>
|
||||
#include <string>
|
||||
|
||||
#include "display_mode_manager.h"
|
||||
#include "mpv_player.h"
|
||||
|
||||
// C-style registration function for the plugin.
|
||||
// C-style registration functions for the video and audio-only plugin
|
||||
// instances.
|
||||
void MpvPlayerPluginRegisterWithRegistrar(FlutterDesktopPluginRegistrarRef registrar);
|
||||
void MpvAudioPlayerPluginRegisterWithRegistrar(FlutterDesktopPluginRegistrarRef registrar);
|
||||
|
||||
namespace mpv {
|
||||
|
||||
class MpvPlayerPlugin : public flutter::Plugin {
|
||||
public:
|
||||
static void RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar);
|
||||
// |channel_name| is the method channel name; the event channel is
|
||||
// |channel_name| + "/events". |audio_only| runs a windowless music core:
|
||||
// no child HWND, no display-mode handling (see MpvPlayer).
|
||||
static void RegisterWithRegistrar(
|
||||
flutter::PluginRegistrarWindows* registrar, const std::string& channel_name = "com.plezy/mpv_player",
|
||||
bool audio_only = false);
|
||||
|
||||
MpvPlayerPlugin(flutter::PluginRegistrarWindows* registrar);
|
||||
MpvPlayerPlugin(
|
||||
flutter::PluginRegistrarWindows* registrar, const std::string& channel_name = "com.plezy/mpv_player",
|
||||
bool audio_only = false);
|
||||
virtual ~MpvPlayerPlugin();
|
||||
|
||||
private:
|
||||
@@ -43,6 +53,11 @@ class MpvPlayerPlugin : public flutter::Plugin {
|
||||
|
||||
flutter::PluginRegistrarWindows* registrar_;
|
||||
DWORD platform_thread_id_;
|
||||
const bool audio_only_;
|
||||
// Per-instance wakeup message: the first window-proc delegate that handles
|
||||
// a message consumes it, so the video and audio instances must not share
|
||||
// one message id or one instance's wakeup would strand the other's queue.
|
||||
const UINT platform_task_message_;
|
||||
HWND flutter_window_ = nullptr;
|
||||
std::unique_ptr<flutter::MethodChannel<flutter::EncodableValue>> method_channel_;
|
||||
std::unique_ptr<flutter::EventChannel<flutter::EncodableValue>> event_channel_;
|
||||
|
||||
Reference in New Issue
Block a user