refactor(app): share client and presentation scaffolds

This commit is contained in:
edde746
2026-07-12 17:31:12 +02:00
parent 97f7508067
commit 6506f36843
24 changed files with 2142 additions and 1943 deletions
+47 -196
View File
@@ -2,29 +2,12 @@
#include <windowsx.h>
#include <algorithm>
#include "sanitize_utf8.h"
namespace mpv {
namespace {
// Audio recovery schedule (issue #783: silent WASAPI after wake from sleep).
// Resume reloads fire unconditionally — a post-wake WASAPI session can stay
// "healthy" from mpv's point of view while producing no sound, so there is no
// property to gate on; the second shot covers a first reload that lands while
// the audio stack is still restoring and creates another silent session.
// Null-fallback retries are clock-driven because a failed ao-reload falls
// back to null again without emitting a current-ao change event.
constexpr int kResumeReloadAttempts = 2;
constexpr std::chrono::milliseconds kResumeFirstDelay{1500};
constexpr std::chrono::milliseconds kResumeRetryDelay{4500};
constexpr int kNullRetryBudget = 5;
constexpr std::chrono::milliseconds kNullFirstDelay{500};
constexpr std::chrono::milliseconds kNullBackoffCap{8000};
constexpr std::chrono::milliseconds kDeviceListDebounce{250};
flutter::EncodableValue NodeToEncodableValue(const mpv_node* node) {
if (!node) return flutter::EncodableValue();
@@ -176,7 +159,7 @@ bool MpvPlayer::Initialize(HWND view) {
if (!audio_only_) {
// Let mpv use display/context detection instead of forcing HDR signaling.
mpv_set_option_string(mpv_, "target-colorspace-hint", "auto");
mpv_set_option_string(mpv_, "target-colorspace-hint", plezy::mpv_common::TargetColorspaceHint(hdr_enabled_));
// Fallback tone mapping when display doesn't support HDR
mpv_set_option_string(mpv_, "tone-mapping", "auto");
@@ -203,10 +186,6 @@ bool MpvPlayer::Initialize(HWND view) {
return false;
}
// 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.
@@ -221,24 +200,11 @@ bool MpvPlayer::Initialize(HWND view) {
void MpvPlayer::Dispose() {
StopEventLoop();
// Cancel pending async requests
std::vector<StatusCallback> status_callbacks;
std::vector<GetPropertyCallback> get_callbacks;
{
std::lock_guard<std::mutex> lock(pending_requests_mutex_);
for (auto& pair : pending_status_requests_) {
if (pair.second) status_callbacks.push_back(std::move(pair.second));
}
for (auto& pair : pending_get_property_requests_) {
if (pair.second) get_callbacks.push_back(std::move(pair.second));
}
pending_status_requests_.clear();
pending_get_property_requests_.clear();
}
for (auto& callback : status_callbacks) {
auto cancelled = pending_requests_.CancelAll();
for (auto& callback : cancelled.status) {
callback(-1);
}
for (auto& callback : get_callbacks) {
for (auto& callback : cancelled.properties) {
callback(-1, "");
}
@@ -265,7 +231,7 @@ void MpvPlayer::Dispose() {
std::thread([handle]() { mpv_terminate_destroy(handle); }).detach();
}
observed_properties_.clear();
observed_properties_.Clear();
}
void MpvPlayer::Command(const std::vector<std::string>& args) { CommandAsync(args, nullptr); }
@@ -283,12 +249,12 @@ void MpvPlayer::CommandAsync(const std::vector<std::string>& args, CommandCallba
}
c_args.push_back(nullptr);
uint64_t request_id = callback ? RegisterStatusRequest(std::move(callback)) : 0;
uint64_t request_id = callback ? pending_requests_.RegisterStatus(std::move(callback)) : 0;
// mpv_command_async returns immediately
int result = mpv_command_async(mpv_, request_id, c_args.data());
if (result < 0) {
auto cb = TakeStatusRequest(request_id);
auto cb = pending_requests_.TakeStatus(request_id);
if (cb) cb(result);
}
}
@@ -305,17 +271,16 @@ void MpvPlayer::SetPropertyAsync(const std::string& name, const std::string& val
// Handle custom HDR toggle property (same pattern as iOS/macOS)
if (name == "hdr-enabled") {
bool enabled = (value == "yes" || value == "true" || value == "1");
SetHDREnabled(enabled, std::move(callback));
SetHDREnabled(plezy::mpv_common::ParseEnabledFlag(value), std::move(callback));
return;
}
uint64_t request_id = callback ? RegisterStatusRequest(std::move(callback)) : 0;
uint64_t request_id = callback ? pending_requests_.RegisterStatus(std::move(callback)) : 0;
char* property_value = const_cast<char*>(value.c_str());
int result = mpv_set_property_async(mpv_, request_id, name.c_str(), MPV_FORMAT_STRING, &property_value);
if (result < 0) {
auto cb = TakeStatusRequest(request_id);
auto cb = pending_requests_.TakeStatus(request_id);
if (cb) cb(result);
}
}
@@ -326,73 +291,21 @@ void MpvPlayer::GetPropertyAsync(const std::string& name, GetPropertyCallback ca
return;
}
uint64_t request_id = RegisterGetPropertyRequest(std::move(callback));
uint64_t request_id = pending_requests_.RegisterProperty(std::move(callback));
int result = mpv_get_property_async(mpv_, request_id, name.c_str(), MPV_FORMAT_STRING);
if (result < 0) {
auto cb = TakeGetPropertyRequest(request_id);
auto cb = pending_requests_.TakeProperty(request_id);
if (cb) cb(result, "");
}
}
uint64_t MpvPlayer::RegisterStatusRequest(StatusCallback callback) {
std::lock_guard<std::mutex> lock(pending_requests_mutex_);
uint64_t request_id = next_reply_userdata_++;
pending_status_requests_[request_id] = std::move(callback);
return request_id;
}
MpvPlayer::StatusCallback MpvPlayer::TakeStatusRequest(uint64_t request_id) {
std::lock_guard<std::mutex> lock(pending_requests_mutex_);
auto it = pending_status_requests_.find(request_id);
if (it == pending_status_requests_.end()) return nullptr;
auto callback = std::move(it->second);
pending_status_requests_.erase(it);
return callback;
}
uint64_t MpvPlayer::RegisterGetPropertyRequest(GetPropertyCallback callback) {
std::lock_guard<std::mutex> lock(pending_requests_mutex_);
uint64_t request_id = next_reply_userdata_++;
pending_get_property_requests_[request_id] = std::move(callback);
return request_id;
}
MpvPlayer::GetPropertyCallback MpvPlayer::TakeGetPropertyRequest(uint64_t request_id) {
std::lock_guard<std::mutex> lock(pending_requests_mutex_);
auto it = pending_get_property_requests_.find(request_id);
if (it == pending_get_property_requests_.end()) return nullptr;
auto callback = std::move(it->second);
pending_get_property_requests_.erase(it);
return callback;
}
void MpvPlayer::ObserveProperty(const std::string& name, const std::string& format, int id) {
if (!mpv_) return;
// Check if already observing.
if (observed_properties_.find(name) != observed_properties_.end()) {
return;
}
name_to_id_[name] = id;
mpv_format mpv_fmt = MPV_FORMAT_NONE;
if (format == "string") {
mpv_fmt = MPV_FORMAT_STRING;
} else if (format == "flag" || format == "bool") {
mpv_fmt = MPV_FORMAT_FLAG;
} else if (format == "int64") {
mpv_fmt = MPV_FORMAT_INT64;
} else if (format == "double") {
mpv_fmt = MPV_FORMAT_DOUBLE;
} else if (format == "node") {
mpv_fmt = MPV_FORMAT_NODE;
}
uint64_t userdata = next_reply_userdata_++;
observed_properties_[name] = userdata;
mpv_observe_property(mpv_, userdata, name.c_str(), mpv_fmt);
const auto request = observed_properties_.Register(name, format, id);
if (!request.added) return;
mpv_observe_property(mpv_, request.userdata, name.c_str(), request.format);
}
void MpvPlayer::SetRect(RECT rect, double device_pixel_ratio) {
@@ -429,7 +342,7 @@ void MpvPlayer::SetEventCallback(EventCallback callback) {
void MpvPlayer::NotifyPowerSuspend() { LogRecovery("system suspending"); }
void MpvPlayer::NotifyPowerResume() { resume_reload_requested_.store(true); }
void MpvPlayer::NotifyPowerResume() { audio_recovery_.RequestResume(); }
void MpvPlayer::LogRecovery(const std::string& text) {
char log_msg[512];
@@ -446,52 +359,25 @@ void MpvPlayer::LogRecovery(const std::string& text) {
}
void MpvPlayer::TryAudioReload(const char* reason, int attempt) {
if (audio_reload_pending_) return;
audio_reload_pending_ = true;
LogRecovery("issuing ao-reload (reason=" + std::string(reason) + ", attempt " + std::to_string(attempt) + ")");
std::string reason_str = reason;
CommandAsync({"ao-reload"}, [this, reason_str, attempt](int error) {
audio_reload_pending_ = false;
const std::string reason_copy = reason;
CommandAsync({"ao-reload"}, [this, reason_copy, attempt](int error) {
audio_recovery_.CompleteReload();
LogRecovery(
"ao-reload completed (reason=" + reason_str + ", attempt " + std::to_string(attempt) +
"ao-reload completed (reason=" + reason_copy + ", attempt " + std::to_string(attempt) +
", error=" + std::to_string(error) + ")");
});
}
void MpvPlayer::MaybeRunAudioRecovery() {
const auto now = std::chrono::steady_clock::now();
if (resume_reload_requested_.exchange(false)) {
if (file_loaded_) {
resume_attempts_left_ = kResumeReloadAttempts;
resume_next_attempt_ = now + kResumeFirstDelay;
LogRecovery("power resume detected; scheduling ao-reload in " + std::to_string(kResumeFirstDelay.count()) + "ms");
} else {
LogRecovery("power resume detected; no file loaded, nothing to recover");
}
const auto action = audio_recovery_.NextReload(plezy::mpv_common::AudioRecoveryState::Clock::now());
if (action.reason == plezy::mpv_common::AudioReloadReason::kNone) {
return;
}
if (resume_attempts_left_ > 0 && now >= resume_next_attempt_) {
int attempt = kResumeReloadAttempts - resume_attempts_left_ + 1;
resume_attempts_left_--;
resume_next_attempt_ = now + kResumeRetryDelay;
TryAudioReload("resume", attempt);
}
if (null_attempts_left_ > 0 && now >= null_next_attempt_) {
if (!current_ao_is_null_) {
null_attempts_left_ = 0;
LogRecovery("audio recovered (current-ao no longer null)");
} else {
int attempt = kNullRetryBudget - null_attempts_left_ + 1;
null_attempts_left_--;
null_next_attempt_ = now + null_backoff_;
null_backoff_ = std::min(null_backoff_ * 2, kNullBackoffCap);
TryAudioReload("null-fallback", attempt);
if (null_attempts_left_ == 0) {
LogRecovery("audio recovery budget exhausted; waiting for device list change or power resume");
}
}
const char* reason = action.reason == plezy::mpv_common::AudioReloadReason::kResume ? "resume" : "null-fallback";
TryAudioReload(reason, action.attempt);
if (action.exhausted) {
LogRecovery("audio recovery budget exhausted; waiting for device list change");
}
}
@@ -531,7 +417,7 @@ void MpvPlayer::HandleMpvEvent(mpv_event* event) {
case MPV_EVENT_COMMAND_REPLY:
case MPV_EVENT_SET_PROPERTY_REPLY: {
uint64_t request_id = event->reply_userdata;
StatusCallback callback = TakeStatusRequest(request_id);
StatusCallback callback = pending_requests_.TakeStatus(request_id);
if (callback) {
callback(event->error);
}
@@ -539,7 +425,7 @@ void MpvPlayer::HandleMpvEvent(mpv_event* event) {
}
case MPV_EVENT_GET_PROPERTY_REPLY: {
uint64_t request_id = event->reply_userdata;
GetPropertyCallback callback = TakeGetPropertyRequest(request_id);
GetPropertyCallback callback = pending_requests_.TakeProperty(request_id);
if (callback) {
std::string value;
if (event->error >= 0) {
@@ -594,44 +480,22 @@ void MpvPlayer::HandleMpvEvent(mpv_event* event) {
break;
}
// Handle sig-peak for HDR detection
if (strcmp(prop->name, "video-params/sig-peak") == 0 && prop->format == MPV_FORMAT_DOUBLE && prop->data) {
double sigPeak = *static_cast<double*>(prop->data);
last_sig_peak_ = sigPeak;
UpdateHDRMode(sigPeak);
}
if (strcmp(prop->name, "current-ao") == 0) {
const char* current_ao = nullptr;
if (prop->format == MPV_FORMAT_STRING && prop->data) {
current_ao = *static_cast<char**>(prop->data);
}
bool is_null = current_ao && strcmp(current_ao, "null") == 0;
if (is_null && !current_ao_is_null_) {
// AO fell back to null (audio-fallback-to-null); start recovery.
null_attempts_left_ = kNullRetryBudget;
null_backoff_ = kNullFirstDelay;
null_next_attempt_ = std::chrono::steady_clock::now() + kNullFirstDelay;
LogRecovery(
"current-ao fell back to null; starting recovery (budget " + std::to_string(kNullRetryBudget) + ")");
} else if (!is_null && current_ao_is_null_) {
null_attempts_left_ = 0;
LogRecovery(std::string("current-ao is now '") + (current_ao ? current_ao : "") + "'");
const bool is_null = current_ao && strcmp(current_ao, "null") == 0;
const auto transition =
audio_recovery_.SetCurrentAudioOutputNull(is_null, plezy::mpv_common::AudioRecoveryState::Clock::now());
if (transition == plezy::mpv_common::AudioOutputTransition::kFellBackToNull) {
LogRecovery("current-ao fell back to null; starting recovery");
} else if (transition == plezy::mpv_common::AudioOutputTransition::kRecovered) {
LogRecovery("audio recovered (current-ao no longer null)");
}
current_ao_is_null_ = is_null;
}
// A device (re)appearing while the AO sits on the null fallback is a
// fresh recovery opportunity: refresh the retry budget and pull the next
// attempt close. Gated on the native observation (userdata 0) so the
// Dart-side observation of the same property doesn't double-trigger.
if (strcmp(prop->name, "audio-device-list") == 0 && event->reply_userdata == 0 && current_ao_is_null_) {
auto candidate = std::chrono::steady_clock::now() + kDeviceListDebounce;
if (null_attempts_left_ <= 0 || candidate < null_next_attempt_) {
null_next_attempt_ = candidate;
}
null_attempts_left_ = kNullRetryBudget;
null_backoff_ = kNullFirstDelay;
if (strcmp(prop->name, "audio-device-list") == 0 && event->reply_userdata == 0 &&
audio_recovery_.OnAudioDeviceListChanged(plezy::mpv_common::AudioRecoveryState::Clock::now())) {
LogRecovery("audio-device-list changed while ao=null; rescheduling ao-reload");
}
@@ -639,9 +503,7 @@ void MpvPlayer::HandleMpvEvent(mpv_event* event) {
break;
}
case MPV_EVENT_END_FILE: {
file_loaded_ = false;
resume_attempts_left_ = 0;
null_attempts_left_ = 0;
audio_recovery_.SetFileLoaded(false);
auto* end = static_cast<mpv_event_end_file*>(event->data);
flutter::EncodableMap data;
data[flutter::EncodableValue("reason")] = flutter::EncodableValue(static_cast<int>(end->reason));
@@ -653,7 +515,7 @@ void MpvPlayer::HandleMpvEvent(mpv_event* event) {
break;
}
case MPV_EVENT_FILE_LOADED: {
file_loaded_ = true;
audio_recovery_.SetFileLoaded(true);
SendEvent("file-loaded");
break;
}
@@ -673,13 +535,13 @@ void MpvPlayer::HandleMpvEvent(mpv_event* event) {
void MpvPlayer::SendPropertyChange(const char* name, mpv_node* data) {
if (!name) return;
auto it = name_to_id_.find(name);
if (it == name_to_id_.end()) return;
int id = 0;
if (!observed_properties_.LookupId(name, &id)) return;
// mpv owns event node storage; copy the full tree before the callback can
// queue it beyond the current mpv_wait_event result's lifetime.
flutter::EncodableList list;
list.push_back(flutter::EncodableValue(it->second));
list.push_back(flutter::EncodableValue(id));
list.push_back(NodeToEncodableValue(data));
std::lock_guard<std::mutex> lock(callback_mutex_);
@@ -704,22 +566,11 @@ void MpvPlayer::SendEvent(const std::string& name, const flutter::EncodableMap&
void MpvPlayer::SetHDREnabled(bool enabled, StatusCallback callback) {
hdr_enabled_ = enabled;
if (mpv_) {
SetPropertyAsync("target-colorspace-hint", enabled ? "auto" : "no", std::move(callback));
} else if (callback) {
callback(0);
if (!mpv_) {
if (callback) callback(0);
return;
}
UpdateHDRMode(last_sig_peak_);
}
void MpvPlayer::UpdateHDRMode(double sigPeak) {
// On Windows, mpv handles HDR passthrough automatically when:
// - target-colorspace-hint=auto
// - Windows HDR is enabled in Display Settings
// - Display supports HDR
// No explicit DXGI calls needed - mpv's gpu-next/vulkan handles it
SetPropertyAsync("target-colorspace-hint", plezy::mpv_common::TargetColorspaceHint(enabled), std::move(callback));
}
} // namespace mpv