560 lines
16 KiB
C++
560 lines
16 KiB
C++
#include "mpv_player.h"
|
|
|
|
#include <flutter_linux/flutter_linux.h>
|
|
#include <epoxy/gl.h>
|
|
#include <epoxy/egl.h>
|
|
#include <epoxy/glx.h>
|
|
#include <gdk/gdk.h>
|
|
#ifdef GDK_WINDOWING_X11
|
|
#include <gdk/gdkx.h>
|
|
#endif
|
|
#ifdef GDK_WINDOWING_WAYLAND
|
|
#include <gdk/gdkwayland.h>
|
|
#endif
|
|
#include <clocale>
|
|
#include <cstring>
|
|
#include <string>
|
|
|
|
// Static helper to get proc address - must be defined outside the namespace
|
|
// to have the correct function signature.
|
|
static void* get_opengl_proc_address(void* ctx, const char* name) {
|
|
(void)ctx;
|
|
#ifdef GDK_WINDOWING_WAYLAND
|
|
// On Wayland, use EGL
|
|
if (epoxy_has_egl()) {
|
|
return reinterpret_cast<void*>(eglGetProcAddress(name));
|
|
}
|
|
#endif
|
|
#ifdef GDK_WINDOWING_X11
|
|
// On X11, use GLX
|
|
return reinterpret_cast<void*>(glXGetProcAddressARB(
|
|
reinterpret_cast<const GLubyte*>(name)));
|
|
#else
|
|
// Fallback: try EGL
|
|
return reinterpret_cast<void*>(eglGetProcAddress(name));
|
|
#endif
|
|
}
|
|
|
|
namespace mpv {
|
|
|
|
MpvPlayer::MpvPlayer() {}
|
|
|
|
MpvPlayer::~MpvPlayer() {
|
|
Dispose();
|
|
}
|
|
|
|
bool MpvPlayer::Initialize() {
|
|
if (mpv_) {
|
|
return true; // Already initialized.
|
|
}
|
|
|
|
// MPV requires C locale for numeric formatting
|
|
std::setlocale(LC_NUMERIC, "C");
|
|
|
|
// Create mpv instance.
|
|
mpv_ = mpv_create();
|
|
if (!mpv_) {
|
|
g_warning("MPV: mpv_create() failed");
|
|
return false;
|
|
}
|
|
|
|
// Configure mpv for embedded playback.
|
|
mpv_set_option_string(mpv_, "vo", "libmpv");
|
|
mpv_set_option_string(mpv_, "hwdec", "auto");
|
|
mpv_set_option_string(mpv_, "keep-open", "yes");
|
|
|
|
// HDR tone mapping
|
|
mpv_set_option_string(mpv_, "tone-mapping", "auto");
|
|
mpv_set_option_string(mpv_, "target-colorspace-hint", "no");
|
|
mpv_set_option_string(mpv_, "hdr-compute-peak", "auto");
|
|
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");
|
|
mpv_set_option_string(mpv_, "terminal", "no");
|
|
|
|
// Default to warn-level logging
|
|
mpv_request_log_messages(mpv_, "warn");
|
|
|
|
// Initialize mpv.
|
|
int err = mpv_initialize(mpv_);
|
|
if (err < 0) {
|
|
g_warning("MPV: mpv_initialize() failed: %s", mpv_error_string(err));
|
|
mpv_destroy(mpv_);
|
|
mpv_ = nullptr;
|
|
return false;
|
|
}
|
|
|
|
// Set up event wakeup callback.
|
|
mpv_set_wakeup_callback(mpv_, OnMpvWakeup, this);
|
|
|
|
g_message("MPV: Initialization successful (render context deferred)");
|
|
return true;
|
|
}
|
|
|
|
bool MpvPlayer::InitRenderContext() {
|
|
if (mpv_gl_) {
|
|
return true; // Already created.
|
|
}
|
|
|
|
if (!mpv_) {
|
|
g_warning("MPV: Cannot create render context - mpv not initialized");
|
|
return false;
|
|
}
|
|
|
|
// Set up OpenGL parameters for mpv.
|
|
mpv_opengl_init_params gl_init_params{
|
|
.get_proc_address = get_opengl_proc_address,
|
|
.get_proc_address_ctx = nullptr,
|
|
};
|
|
|
|
mpv_render_param params[] = {
|
|
{MPV_RENDER_PARAM_API_TYPE,
|
|
const_cast<char*>(MPV_RENDER_API_TYPE_OPENGL)},
|
|
{MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, &gl_init_params},
|
|
{MPV_RENDER_PARAM_INVALID, nullptr},
|
|
};
|
|
|
|
int err = mpv_render_context_create(&mpv_gl_, mpv_, params);
|
|
if (err < 0) {
|
|
g_warning("MPV: mpv_render_context_create() failed: %s",
|
|
mpv_error_string(err));
|
|
return false;
|
|
}
|
|
|
|
// Set up render update callback.
|
|
mpv_render_context_set_update_callback(mpv_gl_, OnMpvRenderUpdate, this);
|
|
|
|
g_message("MPV: Render context created successfully");
|
|
return true;
|
|
}
|
|
|
|
void MpvPlayer::Dispose() {
|
|
// 1. Set disposed flag atomically FIRST — all callback paths check this
|
|
if (disposed_.exchange(true)) {
|
|
return;
|
|
}
|
|
|
|
// 2. Clear mpv's native callbacks to prevent new ones from firing
|
|
if (mpv_gl_) {
|
|
mpv_render_context_set_update_callback(mpv_gl_, nullptr, nullptr);
|
|
}
|
|
if (mpv_) {
|
|
mpv_set_wakeup_callback(mpv_, nullptr, nullptr);
|
|
}
|
|
|
|
// 3. Briefly hold mutex to null our callbacks
|
|
{
|
|
std::lock_guard<std::mutex> lock(callback_mutex_);
|
|
redraw_callback_ = nullptr;
|
|
event_callback_ = nullptr;
|
|
}
|
|
|
|
// 4. Cancel pending async commands
|
|
{
|
|
std::lock_guard<std::mutex> cmd_lock(pending_commands_mutex_);
|
|
for (auto& pair : pending_commands_) {
|
|
if (pair.second) pair.second(-1);
|
|
}
|
|
pending_commands_.clear();
|
|
}
|
|
|
|
// 5. Remove pending idle callbacks
|
|
if (event_source_id_ != 0) {
|
|
g_source_remove(event_source_id_);
|
|
event_source_id_ = 0;
|
|
}
|
|
|
|
// 6. Free render context and mpv handle in a background thread.
|
|
// mpv_render_context_free() can block waiting for mpv's render/VO thread,
|
|
// and mpv_terminate_destroy() can block on demuxer/network I/O.
|
|
// Running these off the main thread prevents stalling the GLib main loop.
|
|
auto* gl = mpv_gl_;
|
|
auto* handle = mpv_;
|
|
mpv_gl_ = nullptr;
|
|
mpv_ = nullptr;
|
|
|
|
std::thread([gl, handle]() {
|
|
if (gl) {
|
|
mpv_render_context_free(gl);
|
|
}
|
|
if (handle) {
|
|
mpv_terminate_destroy(handle);
|
|
}
|
|
}).detach();
|
|
|
|
observed_properties_.clear();
|
|
}
|
|
|
|
void MpvPlayer::Command(const std::vector<std::string>& args) {
|
|
if (disposed_ || !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 (disposed_ || !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);
|
|
|
|
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);
|
|
}
|
|
|
|
int result = mpv_command_async(mpv_, request_id, c_args.data());
|
|
if (result < 0) {
|
|
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 (disposed_ || !mpv_) return;
|
|
mpv_set_property_string(mpv_, name.c_str(), value.c_str());
|
|
}
|
|
|
|
std::string MpvPlayer::GetProperty(const std::string& name) {
|
|
if (disposed_ || !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 (disposed_ || !mpv_) return;
|
|
|
|
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::Render(int width, int height, int fbo) {
|
|
if (disposed_ || !mpv_gl_) return;
|
|
|
|
mpv_opengl_fbo mpv_fbo{
|
|
.fbo = fbo,
|
|
.w = width,
|
|
.h = height,
|
|
.internal_format = 0,
|
|
};
|
|
|
|
int flip_y = 0;
|
|
|
|
mpv_render_param params[] = {
|
|
{MPV_RENDER_PARAM_OPENGL_FBO, &mpv_fbo},
|
|
{MPV_RENDER_PARAM_FLIP_Y, &flip_y},
|
|
{MPV_RENDER_PARAM_INVALID, nullptr},
|
|
};
|
|
|
|
mpv_render_context_render(mpv_gl_, params);
|
|
}
|
|
|
|
void MpvPlayer::ReportMouseMove(int x, int y) {
|
|
if (disposed_ || !mpv_) return;
|
|
std::string x_str = std::to_string(x);
|
|
std::string y_str = std::to_string(y);
|
|
const char* args[] = {"mouse", x_str.c_str(), y_str.c_str(), nullptr};
|
|
mpv_command_async(mpv_, 0, args);
|
|
}
|
|
|
|
void MpvPlayer::SetEventCallback(EventCallback callback) {
|
|
std::lock_guard<std::mutex> lock(callback_mutex_);
|
|
event_callback_ = std::move(callback);
|
|
}
|
|
|
|
void MpvPlayer::SetRedrawCallback(RedrawCallback callback) {
|
|
std::lock_guard<std::mutex> lock(callback_mutex_);
|
|
redraw_callback_ = std::move(callback);
|
|
}
|
|
|
|
void MpvPlayer::SetLogLevel(const std::string& level) {
|
|
if (disposed_ || !mpv_) return;
|
|
mpv_request_log_messages(mpv_, level.c_str());
|
|
}
|
|
|
|
void MpvPlayer::OnMpvWakeup(void* ctx) {
|
|
auto* player = static_cast<MpvPlayer*>(ctx);
|
|
|
|
if (player->disposed_) return;
|
|
|
|
g_idle_add_full(
|
|
G_PRIORITY_HIGH_IDLE,
|
|
[](gpointer data) -> gboolean {
|
|
auto* player = static_cast<MpvPlayer*>(data);
|
|
|
|
if (!player->disposed_ && player->mpv_) {
|
|
player->ProcessEvents();
|
|
}
|
|
return G_SOURCE_REMOVE;
|
|
},
|
|
player,
|
|
nullptr);
|
|
}
|
|
|
|
void MpvPlayer::OnMpvRenderUpdate(void* ctx) {
|
|
auto* player = static_cast<MpvPlayer*>(ctx);
|
|
if (player->disposed_) return;
|
|
|
|
bool expected = false;
|
|
if (!player->needs_redraw_.compare_exchange_strong(expected, true)) {
|
|
return;
|
|
}
|
|
|
|
// Schedule redraw on main thread. Calling Flutter's
|
|
// fl_texture_registrar_mark_texture_frame_available directly from mpv's
|
|
// render/VO thread can deadlock during disposal on Wayland: the main thread
|
|
// blocks in mpv_render_context_free() waiting for the VO thread, while the
|
|
// VO thread blocks in the Flutter registrar waiting for the main thread.
|
|
g_idle_add(
|
|
[](gpointer data) -> gboolean {
|
|
auto* player = static_cast<MpvPlayer*>(data);
|
|
if (player->disposed_) return G_SOURCE_REMOVE;
|
|
|
|
std::lock_guard<std::mutex> lock(player->callback_mutex_);
|
|
if (player->redraw_callback_) {
|
|
player->redraw_callback_();
|
|
}
|
|
return G_SOURCE_REMOVE;
|
|
},
|
|
player);
|
|
}
|
|
|
|
bool MpvPlayer::ProcessEvents() {
|
|
if (disposed_ || !mpv_) return false;
|
|
|
|
while (true) {
|
|
mpv_event* event = mpv_wait_event(mpv_, 0);
|
|
if (event->event_id == MPV_EVENT_NONE) {
|
|
break;
|
|
}
|
|
if (event->event_id == MPV_EVENT_SHUTDOWN) {
|
|
return false;
|
|
}
|
|
HandleMpvEvent(event);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void MpvPlayer::HandleMpvEvent(mpv_event* event) {
|
|
switch (event->event_id) {
|
|
case MPV_EVENT_COMMAND_REPLY: {
|
|
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) {
|
|
int error = event->error;
|
|
g_idle_add(
|
|
[](gpointer data) -> gboolean {
|
|
auto* pair = static_cast<std::pair<CommandCallback, int>*>(data);
|
|
if (pair->first) pair->first(pair->second);
|
|
delete pair;
|
|
return G_SOURCE_REMOVE;
|
|
},
|
|
new std::pair<CommandCallback, int>(std::move(callback), error));
|
|
}
|
|
break;
|
|
}
|
|
case MPV_EVENT_LOG_MESSAGE: {
|
|
auto* msg = static_cast<mpv_event_log_message*>(event->data);
|
|
g_message("MPV [%s] %s: %s", msg->level, msg->prefix, msg->text);
|
|
|
|
FlValue* data = fl_value_new_map();
|
|
fl_value_set_string_take(data, "prefix",
|
|
fl_value_new_string(msg->prefix ? msg->prefix : ""));
|
|
fl_value_set_string_take(data, "level",
|
|
fl_value_new_string(msg->level ? msg->level : ""));
|
|
fl_value_set_string_take(data, "text",
|
|
fl_value_new_string(msg->text ? msg->text : ""));
|
|
SendEvent("log-message", data);
|
|
fl_value_unref(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;
|
|
}
|
|
|
|
SendPropertyChange(prop->name, &node);
|
|
break;
|
|
}
|
|
case MPV_EVENT_END_FILE: {
|
|
auto* end = static_cast<mpv_event_end_file*>(event->data);
|
|
FlValue* data = fl_value_new_map();
|
|
fl_value_set_string_take(data, "reason",
|
|
fl_value_new_int(static_cast<int>(end->reason)));
|
|
if (end->reason == MPV_END_FILE_REASON_ERROR) {
|
|
fl_value_set_string_take(data, "error",
|
|
fl_value_new_int(static_cast<int>(end->error)));
|
|
}
|
|
SendEvent("end-file", data);
|
|
fl_value_unref(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;
|
|
}
|
|
}
|
|
|
|
FlValue* MpvPlayer::NodeToFlValue(mpv_node* node) {
|
|
if (!node) return fl_value_new_null();
|
|
|
|
switch (node->format) {
|
|
case MPV_FORMAT_STRING:
|
|
return fl_value_new_string(node->u.string ? node->u.string : "");
|
|
case MPV_FORMAT_FLAG:
|
|
return fl_value_new_bool(node->u.flag != 0);
|
|
case MPV_FORMAT_INT64:
|
|
return fl_value_new_int(node->u.int64);
|
|
case MPV_FORMAT_DOUBLE:
|
|
return fl_value_new_float(node->u.double_);
|
|
case MPV_FORMAT_NODE_ARRAY: {
|
|
FlValue* list = fl_value_new_list();
|
|
for (int i = 0; i < node->u.list->num; i++) {
|
|
fl_value_append_take(list, NodeToFlValue(&node->u.list->values[i]));
|
|
}
|
|
return list;
|
|
}
|
|
case MPV_FORMAT_NODE_MAP: {
|
|
FlValue* map = fl_value_new_map();
|
|
for (int i = 0; i < node->u.list->num; i++) {
|
|
fl_value_set_string_take(
|
|
map, node->u.list->keys[i],
|
|
NodeToFlValue(&node->u.list->values[i]));
|
|
}
|
|
return map;
|
|
}
|
|
default:
|
|
return fl_value_new_null();
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
FlValue* list = fl_value_new_list();
|
|
fl_value_append_take(list, fl_value_new_int(it->second));
|
|
if (data) {
|
|
fl_value_append_take(list, NodeToFlValue(data));
|
|
} else {
|
|
fl_value_append_take(list, fl_value_new_null());
|
|
}
|
|
|
|
std::lock_guard<std::mutex> lock(callback_mutex_);
|
|
if (event_callback_) {
|
|
event_callback_(list);
|
|
}
|
|
fl_value_unref(list);
|
|
}
|
|
|
|
void MpvPlayer::SendEvent(const std::string& name, FlValue* data) {
|
|
FlValue* event_map = fl_value_new_map();
|
|
fl_value_set_string_take(event_map, "type", fl_value_new_string("event"));
|
|
fl_value_set_string_take(event_map, "name", fl_value_new_string(name.c_str()));
|
|
if (data) {
|
|
fl_value_set_string_take(event_map, "data", fl_value_ref(data));
|
|
}
|
|
|
|
std::lock_guard<std::mutex> lock(callback_mutex_);
|
|
if (event_callback_) {
|
|
event_callback_(event_map);
|
|
}
|
|
fl_value_unref(event_map);
|
|
}
|
|
|
|
} // namespace mpv
|