#include "mpv_player.h" #include "sanitize_utf8.h" namespace mpv { MpvPlayer::MpvPlayer() {} MpvPlayer::~MpvPlayer() { Dispose(); } bool MpvPlayer::Initialize(HWND container, HWND flutter_window) { if (mpv_) { return true; // Already initialized. } container_ = container; flutter_window_ = flutter_window; // Create mpv instance. mpv_ = mpv_create(); if (!mpv_) { return false; } // Create a child window for mpv to render into. hwnd_ = ::CreateWindowW( L"STATIC", L"", WS_CHILD | WS_VISIBLE, 0, 0, 100, 100, container, nullptr, GetModuleHandle(nullptr), nullptr); if (!hwnd_) { mpv_destroy(mpv_); mpv_ = nullptr; return false; } // Set the wid option to embed mpv in our window. int64_t wid = reinterpret_cast(hwnd_); mpv_set_option(mpv_, "wid", MPV_FORMAT_INT64, &wid); // 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"); mpv_set_option_string(mpv_, "input-vo-keyboard", "no"); mpv_set_option_string(mpv_, "osc", "no"); // HDR passthrough - let mpv handle color space mpv_set_option_string(mpv_, "target-colorspace-hint", "yes"); // 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 // handled in the event loop when audio-device-list changes. mpv_set_option_string(mpv_, "audio-fallback-to-null", "yes"); // Default to warn-level logging; Dart side can raise to "v" if debug logging is enabled. mpv_request_log_messages(mpv_, "warn"); // Initialize mpv. int err = mpv_initialize(mpv_); if (err < 0) { ::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); mpv_observe_property(mpv_, 0, "current-ao", MPV_FORMAT_STRING); // Start event loop. StartEventLoop(); return true; } void MpvPlayer::Dispose() { StopEventLoop(); // Cancel pending async requests std::vector status_callbacks; std::vector get_callbacks; { std::lock_guard 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) { callback(-1); } for (auto& callback : get_callbacks) { callback(-1, ""); } if (mpv_) { mpv_terminate_destroy(mpv_); mpv_ = nullptr; } if (hwnd_) { ::DestroyWindow(hwnd_); hwnd_ = nullptr; } observed_properties_.clear(); } void MpvPlayer::Command(const std::vector& args) { CommandAsync(args, nullptr); } void MpvPlayer::CommandAsync(const std::vector& args, CommandCallback callback) { if (!mpv_) { if (callback) callback(0); return; } std::vector c_args; c_args.reserve(args.size() + 1); for (const auto& arg : args) { c_args.push_back(arg.c_str()); } c_args.push_back(nullptr); uint64_t request_id = callback ? RegisterStatusRequest(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); if (cb) cb(result); } } void MpvPlayer::SetProperty(const std::string& name, const std::string& value) { SetPropertyAsync(name, value, nullptr); } void MpvPlayer::SetPropertyAsync(const std::string& name, const std::string& value, StatusCallback callback) { if (!mpv_) { if (callback) callback(0); return; } // 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)); return; } uint64_t request_id = callback ? RegisterStatusRequest(std::move(callback)) : 0; char* property_value = const_cast(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); if (cb) cb(result); } } void MpvPlayer::GetPropertyAsync(const std::string& name, GetPropertyCallback callback) { if (!mpv_) { if (callback) callback(-1, ""); return; } uint64_t request_id = RegisterGetPropertyRequest(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); if (cb) cb(result, ""); } } uint64_t MpvPlayer::RegisterStatusRequest(StatusCallback callback) { std::lock_guard 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 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 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 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); } void MpvPlayer::SetRect(RECT rect, double device_pixel_ratio) { rect_ = rect; device_pixel_ratio_ = device_pixel_ratio; if (hwnd_ && container_ && flutter_window_) { // The rect from Dart is in Flutter client area coordinates (0,0 is top-left of Flutter // content). The container window is positioned to match the Flutter window's full bounds // (including title bar). We need to offset the mpv window within the container to align with // Flutter's client area. // Get the Flutter window's window rect (screen coordinates, includes title bar) RECT window_rect; ::GetWindowRect(flutter_window_, &window_rect); // Get the Flutter window's client rect (client coordinates, 0,0 based) RECT client_rect; ::GetClientRect(flutter_window_, &client_rect); // Convert client area origin to screen coordinates POINT client_origin = {0, 0}; ::ClientToScreen(flutter_window_, &client_origin); // Calculate the offset from window origin to client area origin int client_offset_x = client_origin.x - window_rect.left; int client_offset_y = client_origin.y - window_rect.top; // Position the mpv window within the container, offset by the title bar/border size int left = rect.left + client_offset_x; int top = rect.top + client_offset_y; int width = rect.right - rect.left; int height = rect.bottom - rect.top; ::MoveWindow(hwnd_, left, top, width, height, TRUE); } } void MpvPlayer::SetVisible(bool visible) { if (hwnd_) { ::ShowWindow(hwnd_, visible ? SW_SHOW : SW_HIDE); } } void MpvPlayer::SetLogLevel(const std::string& level) { if (!mpv_) return; mpv_request_log_messages(mpv_, level.c_str()); } void MpvPlayer::SetEventCallback(EventCallback callback) { std::lock_guard lock(callback_mutex_); event_callback_ = std::move(callback); } void MpvPlayer::ReloadAudioOutput() { if (audio_reload_pending_) return; audio_reload_pending_ = true; CommandAsync({"ao-reload"}, [this](int) { audio_reload_pending_ = false; }); } void MpvPlayer::StartEventLoop() { running_ = true; event_thread_ = std::thread(&MpvPlayer::EventLoop, this); } void MpvPlayer::StopEventLoop() { running_ = false; if (event_thread_.joinable()) { // Wake up the event loop. if (mpv_) { mpv_wakeup(mpv_); } event_thread_.join(); } } void MpvPlayer::EventLoop() { while (running_) { mpv_event* event = mpv_wait_event(mpv_, 0.1); if (event->event_id == MPV_EVENT_NONE) { continue; } if (event->event_id == MPV_EVENT_SHUTDOWN) { break; } HandleMpvEvent(event); } } void MpvPlayer::HandleMpvEvent(mpv_event* event) { switch (event->event_id) { case MPV_EVENT_COMMAND_REPLY: case MPV_EVENT_SET_PROPERTY_REPLY: { uint64_t request_id = event->reply_userdata; StatusCallback callback = TakeStatusRequest(request_id); if (callback) { callback(event->error); } break; } case MPV_EVENT_GET_PROPERTY_REPLY: { uint64_t request_id = event->reply_userdata; GetPropertyCallback callback = TakeGetPropertyRequest(request_id); if (callback) { std::string value; if (event->error >= 0) { auto* prop = static_cast(event->data); if (prop && prop->format == MPV_FORMAT_STRING && prop->data) { auto c_value = *static_cast(prop->data); if (c_value) value = SanitizeUtf8(c_value); } } callback(event->error, value); } break; } case MPV_EVENT_LOG_MESSAGE: { auto* msg = static_cast(event->data); char log_msg[512]; snprintf(log_msg, sizeof(log_msg), "MPV [%s] %s: %s", msg->level, msg->prefix, msg->text); OutputDebugStringA(log_msg); flutter::EncodableMap data; data[flutter::EncodableValue("prefix")] = flutter::EncodableValue(SanitizeUtf8(msg->prefix)); data[flutter::EncodableValue("level")] = flutter::EncodableValue(SanitizeUtf8(msg->level)); data[flutter::EncodableValue("text")] = flutter::EncodableValue(SanitizeUtf8(msg->text)); SendEvent("log-message", data); break; } case MPV_EVENT_PROPERTY_CHANGE: { auto* prop = static_cast(event->data); mpv_node node; node.format = prop->format; switch (prop->format) { case MPV_FORMAT_STRING: node.u.string = prop->data ? *static_cast(prop->data) : nullptr; break; case MPV_FORMAT_FLAG: node.u.flag = prop->data ? *static_cast(prop->data) : 0; break; case MPV_FORMAT_INT64: node.u.int64 = prop->data ? *static_cast(prop->data) : 0; break; case MPV_FORMAT_DOUBLE: node.u.double_ = prop->data ? *static_cast(prop->data) : 0.0; break; case MPV_FORMAT_NODE: if (prop->data) { node = *static_cast(prop->data); } break; default: node.format = MPV_FORMAT_NONE; 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(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(prop->data); } current_ao_is_null_ = current_ao && strcmp(current_ao, "null") == 0; } // Audio recovery for sleep/wake or unplugged devices. // Mirrors mpv's TOOLS/lua/ao-null-reload.lua for embedded libmpv. if (strcmp(prop->name, "audio-device-list") == 0 && current_ao_is_null_) { ReloadAudioOutput(); } SendPropertyChange(prop->name, &node); break; } case MPV_EVENT_END_FILE: { auto* end = static_cast(event->data); flutter::EncodableMap data; data[flutter::EncodableValue("reason")] = flutter::EncodableValue(static_cast(end->reason)); if (end->reason == MPV_END_FILE_REASON_ERROR) { data[flutter::EncodableValue("error")] = flutter::EncodableValue(static_cast(end->error)); data[flutter::EncodableValue("message")] = flutter::EncodableValue(SanitizeUtf8(mpv_error_string(end->error))); } SendEvent("end-file", data); break; } case MPV_EVENT_FILE_LOADED: { SendEvent("file-loaded"); break; } case MPV_EVENT_PLAYBACK_RESTART: { SendEvent("playback-restart"); break; } case MPV_EVENT_SEEK: { SendEvent("seek"); break; } default: break; } } 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; flutter::EncodableValue value; if (data) { switch (data->format) { case MPV_FORMAT_STRING: value = flutter::EncodableValue(SanitizeUtf8(data->u.string)); break; case MPV_FORMAT_FLAG: value = flutter::EncodableValue(data->u.flag != 0); break; case MPV_FORMAT_INT64: value = flutter::EncodableValue(data->u.int64); break; case MPV_FORMAT_DOUBLE: value = flutter::EncodableValue(data->u.double_); break; default: value = flutter::EncodableValue(); break; } } flutter::EncodableList list; list.push_back(flutter::EncodableValue(it->second)); list.push_back(value); std::lock_guard lock(callback_mutex_); if (event_callback_) { event_callback_(flutter::EncodableValue(list)); } } void MpvPlayer::SendEvent(const std::string& name, const flutter::EncodableMap& data) { flutter::EncodableMap event; event[flutter::EncodableValue("type")] = flutter::EncodableValue("event"); event[flutter::EncodableValue("name")] = flutter::EncodableValue(name); if (!data.empty()) { event[flutter::EncodableValue("data")] = flutter::EncodableValue(data); } std::lock_guard lock(callback_mutex_); if (event_callback_) { event_callback_(flutter::EncodableValue(event)); } } void MpvPlayer::SetHDREnabled(bool enabled, StatusCallback callback) { hdr_enabled_ = enabled; if (mpv_) { SetPropertyAsync("target-colorspace-hint", enabled ? "yes" : "no", std::move(callback)); } else if (callback) { callback(0); } UpdateHDRMode(last_sig_peak_); } void MpvPlayer::UpdateHDRMode(double sigPeak) { // On Windows, mpv handles HDR passthrough automatically when: // - target-colorspace-hint=yes // - Windows HDR is enabled in Display Settings // - Display supports HDR // No explicit DXGI calls needed - mpv's gpu-next/vulkan handles it } } // namespace mpv