556 lines
16 KiB
C++
556 lines
16 KiB
C++
#include "mpv_player.h"
|
|
|
|
#include <cstring>
|
|
#include <fstream>
|
|
#include <simdutf.h>
|
|
|
|
// Sanitize a C string that may contain invalid UTF-8 sequences.
|
|
// Uses simdutf for SIMD-accelerated validation (fast path for valid strings),
|
|
// then falls back to iterative replacement with U+FFFD on the rare invalid case.
|
|
// mpv does not guarantee UTF-8 for log messages, error strings, or
|
|
// system-encoded paths — sending these unsanitized through Flutter's
|
|
// StandardMessageCodec causes FormatException crashes.
|
|
static std::string SanitizeUtf8(const char* input) {
|
|
if (!input) return std::string();
|
|
size_t len = strlen(input);
|
|
if (len == 0) return std::string();
|
|
|
|
// Fast path: SIMD-accelerated validation — almost all strings pass this
|
|
if (simdutf::validate_utf8(input, len)) {
|
|
return std::string(input, len);
|
|
}
|
|
|
|
// Slow path: find each invalid position, copy valid prefix, insert U+FFFD,
|
|
// skip the bad byte, and repeat.
|
|
std::string result;
|
|
result.reserve(len);
|
|
size_t pos = 0;
|
|
|
|
while (pos < len) {
|
|
auto r = simdutf::validate_utf8_with_errors(input + pos, len - pos);
|
|
// Copy the valid prefix up to the error
|
|
if (r.count > 0) {
|
|
result.append(input + pos, r.count);
|
|
}
|
|
pos += r.count;
|
|
if (r.error == simdutf::error_code::SUCCESS) {
|
|
break; // remaining tail is valid
|
|
}
|
|
// Replace the invalid byte with U+FFFD and skip it
|
|
result.append("\xEF\xBF\xBD");
|
|
pos++;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static void LogToFile(const char* message) {
|
|
std::ofstream log("C:\\Users\\admin\\mpv_debug.log", std::ios::app);
|
|
if (log.is_open()) {
|
|
log << message << std::endl;
|
|
log.close();
|
|
}
|
|
OutputDebugStringA(message);
|
|
OutputDebugStringA("\n");
|
|
}
|
|
|
|
namespace mpv {
|
|
|
|
MpvPlayer::MpvPlayer() {}
|
|
|
|
MpvPlayer::~MpvPlayer() { Dispose(); }
|
|
|
|
bool MpvPlayer::Initialize(HWND container, HWND flutter_window) {
|
|
LogToFile("MpvPlayer::Initialize called");
|
|
|
|
if (mpv_) {
|
|
LogToFile("MpvPlayer::Initialize - already initialized");
|
|
return true; // Already initialized.
|
|
}
|
|
|
|
container_ = container;
|
|
flutter_window_ = flutter_window;
|
|
char msg[256];
|
|
snprintf(msg, sizeof(msg), "MpvPlayer::Initialize - container: %p", container);
|
|
LogToFile(msg);
|
|
|
|
// Create mpv instance.
|
|
LogToFile("MpvPlayer::Initialize - calling mpv_create()");
|
|
mpv_ = mpv_create();
|
|
if (!mpv_) {
|
|
LogToFile("MPV: mpv_create() failed");
|
|
return false;
|
|
}
|
|
LogToFile("MpvPlayer::Initialize - mpv_create() succeeded");
|
|
|
|
// Create a child window for mpv to render into.
|
|
LogToFile("MpvPlayer::Initialize - creating child window");
|
|
hwnd_ = ::CreateWindowW(L"STATIC", L"", WS_CHILD | WS_VISIBLE, 0, 0, 100, 100,
|
|
container, nullptr, GetModuleHandle(nullptr),
|
|
nullptr);
|
|
if (!hwnd_) {
|
|
DWORD error = GetLastError();
|
|
snprintf(msg, sizeof(msg), "MPV: CreateWindowW failed with error %lu", error);
|
|
LogToFile(msg);
|
|
mpv_destroy(mpv_);
|
|
mpv_ = nullptr;
|
|
return false;
|
|
}
|
|
snprintf(msg, sizeof(msg), "MpvPlayer::Initialize - child window created: %p", hwnd_);
|
|
LogToFile(msg);
|
|
|
|
// Set the wid option to embed mpv in our window.
|
|
int64_t wid = reinterpret_cast<int64_t>(hwnd_);
|
|
int err = mpv_set_option(mpv_, "wid", MPV_FORMAT_INT64, &wid);
|
|
if (err < 0) {
|
|
snprintf(msg, sizeof(msg), "MPV: Failed to set wid option: %d %s", err, mpv_error_string(err));
|
|
LogToFile(msg);
|
|
} else {
|
|
LogToFile("MpvPlayer::Initialize - wid option set successfully");
|
|
}
|
|
|
|
// Configure mpv for embedded playback.
|
|
LogToFile("MpvPlayer::Initialize - setting mpv options");
|
|
// Use gpu-next + Vulkan to avoid D3D11 driver crashes (GH-653)
|
|
mpv_set_option_string(mpv_, "vo", "gpu-next");
|
|
mpv_set_option_string(mpv_, "gpu-api", "vulkan");
|
|
// 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");
|
|
|
|
// Default to warn-level logging; Dart side can raise to "v" if debug logging is enabled.
|
|
mpv_request_log_messages(mpv_, "warn");
|
|
|
|
// Initialize mpv.
|
|
LogToFile("MpvPlayer::Initialize - calling mpv_initialize()");
|
|
err = mpv_initialize(mpv_);
|
|
if (err < 0) {
|
|
snprintf(msg, sizeof(msg), "MPV: mpv_initialize() failed with error %d: %s",
|
|
err, mpv_error_string(err));
|
|
LogToFile(msg);
|
|
::DestroyWindow(hwnd_);
|
|
hwnd_ = nullptr;
|
|
mpv_destroy(mpv_);
|
|
mpv_ = nullptr;
|
|
return false;
|
|
}
|
|
|
|
LogToFile("MPV: Initialization successful");
|
|
|
|
// Observe video-params/sig-peak for HDR detection
|
|
mpv_observe_property(mpv_, 0, "video-params/sig-peak", MPV_FORMAT_DOUBLE);
|
|
|
|
// Start event loop.
|
|
StartEventLoop();
|
|
LogToFile("MpvPlayer::Initialize - event loop started");
|
|
|
|
return true;
|
|
}
|
|
|
|
void MpvPlayer::Dispose() {
|
|
StopEventLoop();
|
|
|
|
// Cancel pending async commands
|
|
{
|
|
std::lock_guard<std::mutex> lock(pending_commands_mutex_);
|
|
for (auto& pair : pending_commands_) {
|
|
if (pair.second) pair.second(-1); // Call with error
|
|
}
|
|
pending_commands_.clear();
|
|
}
|
|
|
|
if (mpv_) {
|
|
mpv_terminate_destroy(mpv_);
|
|
mpv_ = nullptr;
|
|
}
|
|
|
|
if (hwnd_) {
|
|
::DestroyWindow(hwnd_);
|
|
hwnd_ = nullptr;
|
|
}
|
|
|
|
observed_properties_.clear();
|
|
}
|
|
|
|
void MpvPlayer::Command(const std::vector<std::string>& args) {
|
|
if (!mpv_) return;
|
|
|
|
std::vector<const char*> 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);
|
|
|
|
mpv_command(mpv_, c_args.data());
|
|
}
|
|
|
|
void MpvPlayer::CommandAsync(const std::vector<std::string>& args,
|
|
CommandCallback callback) {
|
|
if (!mpv_) {
|
|
if (callback) callback(0);
|
|
return;
|
|
}
|
|
|
|
std::vector<const char*> 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);
|
|
|
|
// Generate unique request ID and store callback
|
|
uint64_t request_id;
|
|
{
|
|
std::lock_guard<std::mutex> lock(pending_commands_mutex_);
|
|
request_id = next_reply_userdata_++;
|
|
pending_commands_[request_id] = std::move(callback);
|
|
}
|
|
|
|
// mpv_command_async returns immediately
|
|
int result = mpv_command_async(mpv_, request_id, c_args.data());
|
|
if (result < 0) {
|
|
// Submission failed, complete immediately with error
|
|
std::lock_guard<std::mutex> lock(pending_commands_mutex_);
|
|
auto it = pending_commands_.find(request_id);
|
|
if (it != pending_commands_.end()) {
|
|
auto cb = std::move(it->second);
|
|
pending_commands_.erase(it);
|
|
if (cb) cb(result);
|
|
}
|
|
}
|
|
}
|
|
|
|
void MpvPlayer::SetProperty(const std::string& name, const std::string& value) {
|
|
if (!mpv_) 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);
|
|
return;
|
|
}
|
|
|
|
mpv_set_property_string(mpv_, name.c_str(), value.c_str());
|
|
}
|
|
|
|
std::string MpvPlayer::GetProperty(const std::string& name) {
|
|
if (!mpv_) return "";
|
|
|
|
char* value = mpv_get_property_string(mpv_, name.c_str());
|
|
if (!value) return "";
|
|
|
|
std::string result(value);
|
|
mpv_free(value);
|
|
return result;
|
|
}
|
|
|
|
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<std::mutex> lock(callback_mutex_);
|
|
event_callback_ = std::move(callback);
|
|
}
|
|
|
|
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: {
|
|
// Handle async command completion
|
|
uint64_t request_id = event->reply_userdata;
|
|
CommandCallback callback;
|
|
{
|
|
std::lock_guard<std::mutex> lock(pending_commands_mutex_);
|
|
auto it = pending_commands_.find(request_id);
|
|
if (it != pending_commands_.end()) {
|
|
callback = std::move(it->second);
|
|
pending_commands_.erase(it);
|
|
}
|
|
}
|
|
if (callback) {
|
|
callback(event->error);
|
|
}
|
|
break;
|
|
}
|
|
case MPV_EVENT_LOG_MESSAGE: {
|
|
auto* msg = static_cast<mpv_event_log_message*>(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<mpv_event_property*>(event->data);
|
|
mpv_node node;
|
|
node.format = prop->format;
|
|
|
|
switch (prop->format) {
|
|
case MPV_FORMAT_STRING:
|
|
node.u.string = prop->data ? *static_cast<char**>(prop->data) : nullptr;
|
|
break;
|
|
case MPV_FORMAT_FLAG:
|
|
node.u.flag = prop->data ? *static_cast<int*>(prop->data) : 0;
|
|
break;
|
|
case MPV_FORMAT_INT64:
|
|
node.u.int64 = prop->data ? *static_cast<int64_t*>(prop->data) : 0;
|
|
break;
|
|
case MPV_FORMAT_DOUBLE:
|
|
node.u.double_ = prop->data ? *static_cast<double*>(prop->data) : 0.0;
|
|
break;
|
|
case MPV_FORMAT_NODE:
|
|
if (prop->data) {
|
|
node = *static_cast<mpv_node*>(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<double*>(prop->data);
|
|
last_sig_peak_ = sigPeak;
|
|
UpdateHDRMode(sigPeak);
|
|
}
|
|
|
|
SendPropertyChange(prop->name, &node);
|
|
break;
|
|
}
|
|
case MPV_EVENT_END_FILE: {
|
|
auto* end = static_cast<mpv_event_end_file*>(event->data);
|
|
flutter::EncodableMap data;
|
|
data[flutter::EncodableValue("reason")] =
|
|
flutter::EncodableValue(static_cast<int>(end->reason));
|
|
if (end->reason == MPV_END_FILE_REASON_ERROR) {
|
|
data[flutter::EncodableValue("error")] =
|
|
flutter::EncodableValue(static_cast<int>(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<std::mutex> 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<std::mutex> lock(callback_mutex_);
|
|
if (event_callback_) {
|
|
event_callback_(flutter::EncodableValue(event));
|
|
}
|
|
}
|
|
|
|
void MpvPlayer::SetHDREnabled(bool enabled) {
|
|
hdr_enabled_ = enabled;
|
|
char msg[128];
|
|
snprintf(msg, sizeof(msg), "[MpvPlayer] HDR enabled: %s", enabled ? "true" : "false");
|
|
LogToFile(msg);
|
|
|
|
if (mpv_) {
|
|
mpv_set_property_string(mpv_, "target-colorspace-hint", enabled ? "yes" : "no");
|
|
}
|
|
|
|
UpdateHDRMode(last_sig_peak_);
|
|
}
|
|
|
|
void MpvPlayer::UpdateHDRMode(double sigPeak) {
|
|
bool isHDRContent = sigPeak > 1.0;
|
|
|
|
char msg[256];
|
|
snprintf(msg, sizeof(msg),
|
|
"[MpvPlayer] HDR mode update (hdrEnabled: %s, sigPeak: %.2f, isHDR: %s)",
|
|
hdr_enabled_ ? "true" : "false",
|
|
sigPeak,
|
|
isHDRContent ? "true" : "false");
|
|
LogToFile(msg);
|
|
|
|
// 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
|