The two prior attempts at #783 failed for lack of a reliable trigger: re-setting audio-device to its own value is a no-op, and gating recovery on audio-device-list changes while current-ao is null never fires in the real flows (a paused player only discovers the dead WASAPI session at unpause, when the device list is already stable; a zombie session that consumes samples silently never goes null). Replace the single gate with a recovery scheduler on the mpv event thread, ticked every wait-event timeout: - WM_POWERBROADCAST resume (both PBT variants, via the plugin's existing window-proc delegate) requests unconditional ao-reloads at +1.5s and +6s after wake — the only trigger that reaches zombie sessions; the second shot covers a first reload landing while the audio stack is still restoring. The handoff is a single atomic flag: no mpv calls or timers from the platform thread, so there is nothing to clean up on dispose. - current-ao falling to null arms clock-driven retries (budget 5, x2 backoff capped at 8s); retries must be clock-driven because a failed ao-reload falls back to null without a property change event. A device-list change while null refreshes the budget, and audio-device-list is now observed natively so recovery does not depend on the Dart side. Every trigger, attempt, and outcome logs as [mpv:audio-recovery] at warn level so exported logs from the reporter are actionable if this still misses. Verified with synthetic PBT_APMRESUMEAUTOMATIC while playing and while paused: full WASAPI reinit both times, audio restarts on unpause. Ref #783
492 lines
18 KiB
C++
492 lines
18 KiB
C++
#include "mpv_plugin.h"
|
|
|
|
static flutter::EncodableMap DisplayModeToMap(const mpv::DisplayMode& mode) {
|
|
flutter::EncodableMap m;
|
|
m[flutter::EncodableValue("width")] = flutter::EncodableValue(static_cast<int32_t>(mode.width));
|
|
m[flutter::EncodableValue("height")] = flutter::EncodableValue(static_cast<int32_t>(mode.height));
|
|
m[flutter::EncodableValue("refreshRate")] = flutter::EncodableValue(static_cast<int32_t>(mode.refresh_rate));
|
|
return m;
|
|
}
|
|
|
|
void MpvPlayerPluginRegisterWithRegistrar(FlutterDesktopPluginRegistrarRef registrar) {
|
|
mpv::MpvPlayerPlugin::RegisterWithRegistrar(
|
|
flutter::PluginRegistrarManager::GetInstance()->GetRegistrar<flutter::PluginRegistrarWindows>(registrar));
|
|
}
|
|
|
|
namespace mpv {
|
|
|
|
namespace {
|
|
constexpr UINT kPlatformTaskMessage = WM_APP + 0x4D50;
|
|
}
|
|
|
|
void MpvPlayerPlugin::RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar) {
|
|
auto plugin = std::make_unique<MpvPlayerPlugin>(registrar);
|
|
registrar->AddPlugin(std::move(plugin));
|
|
}
|
|
|
|
MpvPlayerPlugin::MpvPlayerPlugin(flutter::PluginRegistrarWindows* registrar)
|
|
: registrar_(registrar), platform_thread_id_(::GetCurrentThreadId()) {
|
|
// Create method channel.
|
|
method_channel_ = std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>(
|
|
registrar->messenger(), "com.plezy/mpv_player", &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());
|
|
|
|
auto handler = std::make_unique<flutter::StreamHandlerFunctions<flutter::EncodableValue>>(
|
|
[this](
|
|
const flutter::EncodableValue* arguments,
|
|
std::unique_ptr<flutter::EventSink<flutter::EncodableValue>>&& events)
|
|
-> std::unique_ptr<flutter::StreamHandlerError<flutter::EncodableValue>> {
|
|
event_sink_ = std::move(events);
|
|
return nullptr;
|
|
},
|
|
[this](const flutter::EncodableValue* arguments)
|
|
-> std::unique_ptr<flutter::StreamHandlerError<flutter::EncodableValue>> {
|
|
event_sink_ = nullptr;
|
|
return nullptr;
|
|
});
|
|
|
|
event_channel_->SetStreamHandler(std::move(handler));
|
|
}
|
|
|
|
MpvPlayerPlugin::~MpvPlayerPlugin() {
|
|
// Join the mpv event thread before draining: it enqueues platform tasks,
|
|
// and platform_tasks_/platform_tasks_mutex_ are destroyed before player_
|
|
// (reverse declaration order).
|
|
if (player_) {
|
|
player_->Dispose();
|
|
player_.reset();
|
|
}
|
|
|
|
DrainPlatformTasks();
|
|
|
|
// Unregister window proc delegate.
|
|
if (proc_id_) {
|
|
registrar_->UnregisterTopLevelWindowProcDelegate(proc_id_.value());
|
|
proc_id_ = std::nullopt;
|
|
}
|
|
}
|
|
|
|
HWND MpvPlayerPlugin::GetChildWindow() { return registrar_->GetView()->GetNativeWindow(); }
|
|
|
|
HWND MpvPlayerPlugin::GetWindow() { return ::GetAncestor(GetChildWindow(), GA_ROOT); }
|
|
|
|
void MpvPlayerPlugin::PostToPlatformThread(std::function<void()> task) {
|
|
if (::GetCurrentThreadId() == platform_thread_id_) {
|
|
task();
|
|
return;
|
|
}
|
|
|
|
bool post_wakeup = false;
|
|
{
|
|
std::lock_guard<std::mutex> lock(platform_tasks_mutex_);
|
|
platform_tasks_.push(std::move(task));
|
|
if (!wakeup_posted_ && flutter_window_) {
|
|
wakeup_posted_ = true;
|
|
post_wakeup = true;
|
|
}
|
|
}
|
|
|
|
if (post_wakeup && !::PostMessage(flutter_window_, kPlatformTaskMessage, 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_);
|
|
wakeup_posted_ = false;
|
|
}
|
|
}
|
|
|
|
void MpvPlayerPlugin::DrainPlatformTasks() {
|
|
std::queue<std::function<void()>> tasks;
|
|
{
|
|
std::lock_guard<std::mutex> lock(platform_tasks_mutex_);
|
|
tasks.swap(platform_tasks_);
|
|
wakeup_posted_ = false;
|
|
}
|
|
|
|
while (!tasks.empty()) {
|
|
tasks.front()();
|
|
tasks.pop();
|
|
}
|
|
}
|
|
|
|
void MpvPlayerPlugin::HandleMethodCall(
|
|
const flutter::MethodCall<flutter::EncodableValue>& method_call,
|
|
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
|
|
const auto& method = method_call.method_name();
|
|
|
|
if (method == "initialize") {
|
|
if (proc_id_) {
|
|
registrar_->UnregisterTopLevelWindowProcDelegate(proc_id_.value());
|
|
proc_id_ = std::nullopt;
|
|
}
|
|
|
|
flutter_window_ = GetWindow();
|
|
|
|
// The only top-level message we care about is the platform-task wakeup;
|
|
// mouse-over-video input is forwarded by the mpv inner-window subclass
|
|
// (see MpvPlayer), and compositing/z-order is handled by the engine's
|
|
// 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) {
|
|
DrainPlatformTasks();
|
|
return std::optional<HRESULT>(0);
|
|
}
|
|
// Both resume variants are handled: which of the two arrives (or
|
|
// both) depends on S3 vs Modern Standby; re-arming is idempotent.
|
|
// player_ create/reset also happens on this thread, so the
|
|
// null-check suffices. Never consume the message.
|
|
if (message == WM_POWERBROADCAST && player_ && player_->IsInitialized()) {
|
|
if (wparam == PBT_APMSUSPEND) {
|
|
player_->NotifyPowerSuspend();
|
|
} else if (wparam == PBT_APMRESUMEAUTOMATIC || wparam == PBT_APMRESUMESUSPEND) {
|
|
player_->NotifyPowerResume();
|
|
}
|
|
}
|
|
return std::optional<HRESULT>(std::nullopt);
|
|
});
|
|
|
|
// The video window is a child of the FLUTTER VIEW itself, so DWM's
|
|
// per-window layer model puts it above the view's own (layer 1) content
|
|
// 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();
|
|
|
|
player_ = std::make_unique<MpvPlayer>();
|
|
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);
|
|
result->Success(flutter::EncodableValue(true));
|
|
} else {
|
|
player_.reset(); // Clear the player so we don't have a half-initialized state
|
|
result->Error("INIT_FAILED", "Failed to initialize MPV player");
|
|
}
|
|
} else if (method == "dispose") {
|
|
if (player_) {
|
|
player_->Dispose();
|
|
player_.reset();
|
|
}
|
|
result->Success();
|
|
} else if (method == "command") {
|
|
if (!player_ || !player_->IsInitialized()) {
|
|
result->Error("NOT_INITIALIZED", "Player not initialized");
|
|
return;
|
|
}
|
|
|
|
const auto* args = method_call.arguments();
|
|
if (!args || !std::holds_alternative<flutter::EncodableMap>(*args)) {
|
|
result->Error("INVALID_ARGS", "Expected map argument");
|
|
return;
|
|
}
|
|
|
|
const auto& map = std::get<flutter::EncodableMap>(*args);
|
|
auto it = map.find(flutter::EncodableValue("args"));
|
|
if (it == map.end() || !std::holds_alternative<flutter::EncodableList>(it->second)) {
|
|
result->Error("INVALID_ARGS", "Missing 'args' list");
|
|
return;
|
|
}
|
|
|
|
const auto& list = std::get<flutter::EncodableList>(it->second);
|
|
std::vector<std::string> command_args;
|
|
for (const auto& item : list) {
|
|
if (std::holds_alternative<std::string>(item)) {
|
|
command_args.push_back(std::get<std::string>(item));
|
|
}
|
|
}
|
|
|
|
// Use async command to prevent UI blocking during network operations
|
|
// Move result into shared_ptr for safe capture in callback
|
|
auto result_ptr =
|
|
std::make_shared<std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>>>(std::move(result));
|
|
std::string cmd_name = command_args.empty() ? "unknown" : command_args[0];
|
|
player_->CommandAsync(command_args, [this, result_ptr, cmd_name](int error) {
|
|
PostToPlatformThread([result_ptr, cmd_name, error]() {
|
|
if (error < 0) {
|
|
(*result_ptr)
|
|
->Error("COMMAND_FAILED", "MPV command failed: " + cmd_name + " (error " + std::to_string(error) + ")");
|
|
} else {
|
|
(*result_ptr)->Success();
|
|
}
|
|
});
|
|
});
|
|
return; // Response will be sent asynchronously
|
|
} else if (method == "setProperty") {
|
|
if (!player_ || !player_->IsInitialized()) {
|
|
result->Error("NOT_INITIALIZED", "Player not initialized");
|
|
return;
|
|
}
|
|
|
|
const auto* args = method_call.arguments();
|
|
if (!args || !std::holds_alternative<flutter::EncodableMap>(*args)) {
|
|
result->Error("INVALID_ARGS", "Expected map argument");
|
|
return;
|
|
}
|
|
|
|
const auto& map = std::get<flutter::EncodableMap>(*args);
|
|
auto name_it = map.find(flutter::EncodableValue("name"));
|
|
auto value_it = map.find(flutter::EncodableValue("value"));
|
|
|
|
if (name_it == map.end() || !std::holds_alternative<std::string>(name_it->second)) {
|
|
result->Error("INVALID_ARGS", "Missing 'name'");
|
|
return;
|
|
}
|
|
if (value_it == map.end() || !std::holds_alternative<std::string>(value_it->second)) {
|
|
result->Error("INVALID_ARGS", "Missing 'value'");
|
|
return;
|
|
}
|
|
|
|
auto result_ptr =
|
|
std::make_shared<std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>>>(std::move(result));
|
|
player_->SetPropertyAsync(
|
|
std::get<std::string>(name_it->second), std::get<std::string>(value_it->second),
|
|
[this, result_ptr](int error) { PostToPlatformThread([result_ptr]() { (*result_ptr)->Success(); }); });
|
|
return;
|
|
} else if (method == "setLogLevel") {
|
|
if (!player_ || !player_->IsInitialized()) {
|
|
result->Error("NOT_INITIALIZED", "Player not initialized");
|
|
return;
|
|
}
|
|
|
|
const auto* args = method_call.arguments();
|
|
if (!args || !std::holds_alternative<flutter::EncodableMap>(*args)) {
|
|
result->Error("INVALID_ARGS", "Expected map argument");
|
|
return;
|
|
}
|
|
|
|
const auto& map = std::get<flutter::EncodableMap>(*args);
|
|
auto level_it = map.find(flutter::EncodableValue("level"));
|
|
|
|
if (level_it == map.end() || !std::holds_alternative<std::string>(level_it->second)) {
|
|
result->Error("INVALID_ARGS", "Missing 'level'");
|
|
return;
|
|
}
|
|
|
|
player_->SetLogLevel(std::get<std::string>(level_it->second));
|
|
result->Success();
|
|
} else if (method == "getProperty") {
|
|
if (!player_ || !player_->IsInitialized()) {
|
|
result->Error("NOT_INITIALIZED", "Player not initialized");
|
|
return;
|
|
}
|
|
|
|
const auto* args = method_call.arguments();
|
|
if (!args || !std::holds_alternative<flutter::EncodableMap>(*args)) {
|
|
result->Error("INVALID_ARGS", "Expected map argument");
|
|
return;
|
|
}
|
|
|
|
const auto& map = std::get<flutter::EncodableMap>(*args);
|
|
auto name_it = map.find(flutter::EncodableValue("name"));
|
|
|
|
if (name_it == map.end() || !std::holds_alternative<std::string>(name_it->second)) {
|
|
result->Error("INVALID_ARGS", "Missing 'name'");
|
|
return;
|
|
}
|
|
|
|
auto result_ptr =
|
|
std::make_shared<std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>>>(std::move(result));
|
|
player_->GetPropertyAsync(
|
|
std::get<std::string>(name_it->second), [this, result_ptr](int error, const std::string& value) {
|
|
PostToPlatformThread([result_ptr, error, value]() {
|
|
if (error < 0 || value.empty()) {
|
|
(*result_ptr)->Success();
|
|
} else {
|
|
(*result_ptr)->Success(flutter::EncodableValue(value));
|
|
}
|
|
});
|
|
});
|
|
return;
|
|
} else if (method == "observeProperty") {
|
|
if (!player_ || !player_->IsInitialized()) {
|
|
result->Error("NOT_INITIALIZED", "Player not initialized");
|
|
return;
|
|
}
|
|
|
|
const auto* args = method_call.arguments();
|
|
if (!args || !std::holds_alternative<flutter::EncodableMap>(*args)) {
|
|
result->Error("INVALID_ARGS", "Expected map argument");
|
|
return;
|
|
}
|
|
|
|
const auto& map = std::get<flutter::EncodableMap>(*args);
|
|
auto name_it = map.find(flutter::EncodableValue("name"));
|
|
auto format_it = map.find(flutter::EncodableValue("format"));
|
|
auto id_it = map.find(flutter::EncodableValue("id"));
|
|
|
|
if (name_it == map.end() || !std::holds_alternative<std::string>(name_it->second)) {
|
|
result->Error("INVALID_ARGS", "Missing 'name'");
|
|
return;
|
|
}
|
|
if (format_it == map.end() || !std::holds_alternative<std::string>(format_it->second)) {
|
|
result->Error("INVALID_ARGS", "Missing 'format'");
|
|
return;
|
|
}
|
|
if (id_it == map.end() || !std::holds_alternative<int32_t>(id_it->second)) {
|
|
result->Error("INVALID_ARGS", "Missing 'id'");
|
|
return;
|
|
}
|
|
|
|
player_->ObserveProperty(
|
|
std::get<std::string>(name_it->second), std::get<std::string>(format_it->second),
|
|
std::get<int32_t>(id_it->second));
|
|
result->Success();
|
|
} else if (method == "setVisible") {
|
|
const auto* args = method_call.arguments();
|
|
if (!args || !std::holds_alternative<flutter::EncodableMap>(*args)) {
|
|
result->Error("INVALID_ARGS", "Expected map argument");
|
|
return;
|
|
}
|
|
|
|
const auto& map = std::get<flutter::EncodableMap>(*args);
|
|
auto visible_it = map.find(flutter::EncodableValue("visible"));
|
|
|
|
if (visible_it == map.end() || !std::holds_alternative<bool>(visible_it->second)) {
|
|
result->Error("INVALID_ARGS", "Missing 'visible'");
|
|
return;
|
|
}
|
|
|
|
bool visible = std::get<bool>(visible_it->second);
|
|
|
|
if (player_) {
|
|
player_->SetVisible(visible);
|
|
}
|
|
|
|
result->Success();
|
|
} else if (method == "setVideoRect") {
|
|
const auto* args = method_call.arguments();
|
|
if (!args || !std::holds_alternative<flutter::EncodableMap>(*args)) {
|
|
result->Error("INVALID_ARGS", "Expected map argument");
|
|
return;
|
|
}
|
|
|
|
const auto& map = std::get<flutter::EncodableMap>(*args);
|
|
|
|
auto get_int = [&map](const char* key) -> int {
|
|
auto it = map.find(flutter::EncodableValue(key));
|
|
if (it != map.end()) {
|
|
if (std::holds_alternative<int32_t>(it->second)) {
|
|
return std::get<int32_t>(it->second);
|
|
} else if (std::holds_alternative<int64_t>(it->second)) {
|
|
return static_cast<int>(std::get<int64_t>(it->second));
|
|
}
|
|
}
|
|
return 0;
|
|
};
|
|
|
|
auto get_double = [&map](const char* key) -> double {
|
|
auto it = map.find(flutter::EncodableValue(key));
|
|
if (it != map.end() && std::holds_alternative<double>(it->second)) {
|
|
return std::get<double>(it->second);
|
|
}
|
|
return 1.0;
|
|
};
|
|
|
|
RECT rect;
|
|
rect.left = get_int("left");
|
|
rect.top = get_int("top");
|
|
rect.right = get_int("right");
|
|
rect.bottom = get_int("bottom");
|
|
double dpr = get_double("devicePixelRatio");
|
|
|
|
if (player_) {
|
|
player_->SetRect(rect, dpr);
|
|
}
|
|
|
|
result->Success();
|
|
} else if (method == "isInitialized") {
|
|
bool initialized = player_ && player_->IsInitialized();
|
|
result->Success(flutter::EncodableValue(initialized));
|
|
|
|
// --- Display mode matching ---
|
|
} else if (method == "getDisplayModes") {
|
|
HWND hwnd = GetWindow();
|
|
auto modes = display_mode_manager_.EnumerateDisplayModes(hwnd);
|
|
flutter::EncodableList list;
|
|
for (const auto& mode : modes) {
|
|
list.push_back(flutter::EncodableValue(DisplayModeToMap(mode)));
|
|
}
|
|
result->Success(flutter::EncodableValue(list));
|
|
} else if (method == "getCurrentDisplayMode") {
|
|
HWND hwnd = GetWindow();
|
|
auto mode = display_mode_manager_.GetCurrentMode(hwnd);
|
|
result->Success(flutter::EncodableValue(DisplayModeToMap(mode)));
|
|
} else if (method == "setDisplayMode") {
|
|
const auto* args = method_call.arguments();
|
|
if (!args || !std::holds_alternative<flutter::EncodableMap>(*args)) {
|
|
result->Error("INVALID_ARGS", "Expected map argument");
|
|
return;
|
|
}
|
|
const auto& map = std::get<flutter::EncodableMap>(*args);
|
|
auto get_int = [&map](const char* key) -> int {
|
|
auto it = map.find(flutter::EncodableValue(key));
|
|
if (it != map.end() && std::holds_alternative<int32_t>(it->second)) return std::get<int32_t>(it->second);
|
|
return 0;
|
|
};
|
|
HWND hwnd = GetWindow();
|
|
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") {
|
|
HWND hwnd = GetWindow();
|
|
bool success = display_mode_manager_.RestoreOriginalMode(hwnd);
|
|
result->Success(flutter::EncodableValue(success));
|
|
} else if (method == "isHDRSupported") {
|
|
HWND hwnd = GetWindow();
|
|
result->Success(flutter::EncodableValue(display_mode_manager_.IsHDRSupported(hwnd)));
|
|
} else if (method == "isHDREnabled") {
|
|
HWND hwnd = GetWindow();
|
|
result->Success(flutter::EncodableValue(display_mode_manager_.IsHDREnabled(hwnd)));
|
|
} else if (method == "setSystemHDR") {
|
|
const auto* args = method_call.arguments();
|
|
if (!args || !std::holds_alternative<flutter::EncodableMap>(*args)) {
|
|
result->Error("INVALID_ARGS", "Expected map argument");
|
|
return;
|
|
}
|
|
const auto& map = std::get<flutter::EncodableMap>(*args);
|
|
auto it = map.find(flutter::EncodableValue("enabled"));
|
|
if (it == map.end() || !std::holds_alternative<bool>(it->second)) {
|
|
result->Error("INVALID_ARGS", "Missing 'enabled'");
|
|
return;
|
|
}
|
|
bool enabled = std::get<bool>(it->second);
|
|
HWND hwnd = GetWindow();
|
|
bool success = display_mode_manager_.SetHDREnabled(hwnd, enabled);
|
|
result->Success(flutter::EncodableValue(success));
|
|
} else if (method == "restoreSystemHDR") {
|
|
HWND hwnd = GetWindow();
|
|
bool success = display_mode_manager_.RestoreOriginalHDRState(hwnd);
|
|
result->Success(flutter::EncodableValue(success));
|
|
} else if (method == "isModeChanged") {
|
|
result->Success(flutter::EncodableValue(display_mode_manager_.IsModeChanged()));
|
|
} else if (method == "isHDRChanged") {
|
|
result->Success(flutter::EncodableValue(display_mode_manager_.IsHDRChanged()));
|
|
} else {
|
|
result->NotImplemented();
|
|
}
|
|
}
|
|
|
|
void MpvPlayerPlugin::SendEvent(const flutter::EncodableValue& event) {
|
|
// mpv events arrive on the mpv event thread; Flutter channel APIs are
|
|
// platform-thread-only, so marshal onto the platform thread (the sink
|
|
// null-check then also runs on the same thread as onListen/onCancel).
|
|
PostToPlatformThread([this, event]() {
|
|
if (event_sink_) {
|
|
event_sink_->Success(event);
|
|
}
|
|
});
|
|
}
|
|
|
|
} // namespace mpv
|