fix: resolve UI freezes during video playback on Steam Deck

- Default MPV native log level to "warn" instead of "v" (Linux/Windows),
  add setLogLevel method channel to raise it when debug logging is enabled
- Coalesce MPV render requests via atomic compare_exchange on needs_redraw_
- Clear redraw flag before render so new redraws queue during current frame
This commit is contained in:
edde746
2026-02-17 03:11:52 +01:00
parent 223a5d2219
commit c98add8065
10 changed files with 88 additions and 7 deletions
+6
View File
@@ -142,6 +142,12 @@ abstract class Player {
/// Get an MPV property value by name. /// Get an MPV property value by name.
Future<String?> getProperty(String name); Future<String?> getProperty(String name);
/// Set the native MPV log message level (e.g., "warn", "v", "debug").
///
/// This controls the volume of log messages sent from the native player
/// over the event channel. Use "warn" in production and "v" for debugging.
Future<void> setLogLevel(String level);
/// Execute a raw MPV command. /// Execute a raw MPV command.
/// ///
/// [args] - Command and arguments as a list of strings. /// [args] - Command and arguments as a list of strings.
+4
View File
@@ -436,6 +436,10 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player {
// ignore: no-empty-block - base no-op, overridden by platform subclasses // ignore: no-empty-block - base no-op, overridden by platform subclasses
Future<void> setAudioPassthrough(bool enabled) async {} Future<void> setAudioPassthrough(bool enabled) async {}
@override
// ignore: no-empty-block - base no-op, overridden by platform subclasses
Future<void> setLogLevel(String level) async {}
// ============================================ // ============================================
// Lifecycle // Lifecycle
// ============================================ // ============================================
+11
View File
@@ -230,6 +230,17 @@ class PlayerNative extends PlayerBase {
await methodChannel.invokeMethod('command', {'args': args}); await methodChannel.invokeMethod('command', {'args': args});
} }
// ============================================
// Log Level
// ============================================
@override
Future<void> setLogLevel(String level) async {
checkDisposed();
await _ensureInitialized();
await methodChannel.invokeMethod('setLogLevel', {'level': level});
}
// ============================================ // ============================================
// Passthrough // Passthrough
// ============================================ // ============================================
+1
View File
@@ -387,6 +387,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
await player!.setProperty('sub-ass', 'yes'); // Enable libass await player!.setProperty('sub-ass', 'yes'); // Enable libass
await player!.setProperty('demuxer-max-bytes', bufferSizeBytes.toString()); await player!.setProperty('demuxer-max-bytes', bufferSizeBytes.toString());
await player!.setProperty('msg-level', debugLoggingEnabled ? 'all=debug' : 'all=error'); await player!.setProperty('msg-level', debugLoggingEnabled ? 'all=debug' : 'all=error');
await player!.setLogLevel(debugLoggingEnabled ? 'v' : 'warn');
await player!.setProperty('hwdec', _getHwdecValue(enableHardwareDecoding)); await player!.setProperty('hwdec', _getHwdecValue(enableHardwareDecoding));
// Subtitle styling // Subtitle styling
+13 -4
View File
@@ -81,8 +81,8 @@ bool MpvPlayer::Initialize(GtkGLArea* gl_area) {
mpv_set_option_string(mpv_, "osc", "no"); mpv_set_option_string(mpv_, "osc", "no");
mpv_set_option_string(mpv_, "terminal", "no"); mpv_set_option_string(mpv_, "terminal", "no");
// Enable verbose logging for debugging. // Default to warn-level logging; Dart side can raise to "v" if debug logging is enabled.
mpv_request_log_messages(mpv_, "v"); mpv_request_log_messages(mpv_, "warn");
// Initialize mpv. // Initialize mpv.
int err = mpv_initialize(mpv_); int err = mpv_initialize(mpv_);
@@ -315,9 +315,13 @@ void MpvPlayer::SetEventCallback(EventCallback callback) {
void MpvPlayer::RequestRedraw() { void MpvPlayer::RequestRedraw() {
if (disposed_) return; if (disposed_) return;
needs_redraw_.store(true); // Only queue one idle handler at a time
bool expected = false;
if (!needs_redraw_.compare_exchange_strong(expected, true)) {
return; // Already have a pending redraw
}
if (gl_area_) { if (gl_area_) {
// Queue redraw on main thread
GtkGLArea* area = gl_area_; GtkGLArea* area = gl_area_;
g_idle_add_full( g_idle_add_full(
GDK_PRIORITY_REDRAW, GDK_PRIORITY_REDRAW,
@@ -333,6 +337,11 @@ void MpvPlayer::RequestRedraw() {
} }
} }
void MpvPlayer::SetLogLevel(const std::string& level) {
if (disposed_ || !mpv_) return;
mpv_request_log_messages(mpv_, level.c_str());
}
void MpvPlayer::OnMpvWakeup(void* ctx) { void MpvPlayer::OnMpvWakeup(void* ctx) {
auto* player = static_cast<MpvPlayer*>(ctx); auto* player = static_cast<MpvPlayer*>(ctx);
+3
View File
@@ -101,6 +101,9 @@ class MpvPlayer {
/// Request a redraw. /// Request a redraw.
void RequestRedraw(); void RequestRedraw();
/// Sets the MPV log message level (e.g., "warn", "v", "debug").
void SetLogLevel(const std::string& level);
private: private:
/// MPV event wakeup callback (called from mpv thread). /// MPV event wakeup callback (called from mpv thread).
static void OnMpvWakeup(void* ctx); static void OnMpvWakeup(void* ctx);
+17 -1
View File
@@ -181,8 +181,8 @@ static gboolean on_gl_render(GtkGLArea* area,
// Set viewport and render the video frame. // Set viewport and render the video frame.
glViewport(0, 0, width, height); glViewport(0, 0, width, height);
self->player->Render(width, height, fbo);
self->player->ClearRedrawFlag(); self->player->ClearRedrawFlag();
self->player->Render(width, height, fbo);
// Restore GL state after MPV render to prevent Flutter corruption. // Restore GL state after MPV render to prevent Flutter corruption.
glViewport(prev_viewport[0], prev_viewport[1], prev_viewport[2], prev_viewport[3]); glViewport(prev_viewport[0], prev_viewport[1], prev_viewport[2], prev_viewport[3]);
@@ -381,6 +381,22 @@ static void mpv_plugin_handle_method_call(FlMethodChannel* channel,
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
} }
} }
} else if (strcmp(method, "setLogLevel") == 0) {
if (!self->player || !self->initialized) {
response = FL_METHOD_RESPONSE(fl_method_error_response_new(
"NOT_INITIALIZED", "Player not initialized", nullptr));
} else {
FlValue* level_value = fl_value_lookup_string(args, "level");
if (level_value == nullptr ||
fl_value_get_type(level_value) != FL_VALUE_TYPE_STRING) {
response = FL_METHOD_RESPONSE(fl_method_error_response_new(
"INVALID_ARGS", "Missing 'level'", nullptr));
} else {
self->player->SetLogLevel(fl_value_get_string(level_value));
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
}
}
} else if (strcmp(method, "getProperty") == 0) { } else if (strcmp(method, "getProperty") == 0) {
if (!self->player || !self->initialized) { if (!self->player || !self->initialized) {
response = FL_METHOD_RESPONSE(fl_method_error_response_new( response = FL_METHOD_RESPONSE(fl_method_error_response_new(
+7 -2
View File
@@ -84,8 +84,8 @@ bool MpvPlayer::Initialize(HWND container, HWND flutter_window) {
mpv_set_option_string(mpv_, "tone-mapping", "auto"); mpv_set_option_string(mpv_, "tone-mapping", "auto");
mpv_set_option_string(mpv_, "hdr-compute-peak", "auto"); mpv_set_option_string(mpv_, "hdr-compute-peak", "auto");
// Enable logging // Default to warn-level logging; Dart side can raise to "v" if debug logging is enabled.
mpv_request_log_messages(mpv_, "v"); mpv_request_log_messages(mpv_, "warn");
// Initialize mpv. // Initialize mpv.
LogToFile("MpvPlayer::Initialize - calling mpv_initialize()"); LogToFile("MpvPlayer::Initialize - calling mpv_initialize()");
@@ -282,6 +282,11 @@ void MpvPlayer::SetVisible(bool visible) {
} }
} }
void MpvPlayer::SetLogLevel(const std::string& level) {
if (!mpv_) return;
mpv_request_log_messages(mpv_, level.c_str());
}
void MpvPlayer::SetEventCallback(EventCallback callback) { void MpvPlayer::SetEventCallback(EventCallback callback) {
std::lock_guard<std::mutex> lock(callback_mutex_); std::lock_guard<std::mutex> lock(callback_mutex_);
event_callback_ = std::move(callback); event_callback_ = std::move(callback);
+3
View File
@@ -65,6 +65,9 @@ class MpvPlayer {
// Shows or hides the video window. // Shows or hides the video window.
void SetVisible(bool visible); void SetVisible(bool visible);
// Sets the MPV log message level (e.g., "warn", "v", "debug").
void SetLogLevel(const std::string& level);
// Sets the event callback for property changes and events. // Sets the event callback for property changes and events.
void SetEventCallback(EventCallback callback); void SetEventCallback(EventCallback callback);
+23
View File
@@ -248,6 +248,29 @@ void MpvPlayerPlugin::HandleMethodCall(
player_->SetProperty(std::get<std::string>(name_it->second), player_->SetProperty(std::get<std::string>(name_it->second),
std::get<std::string>(value_it->second)); std::get<std::string>(value_it->second));
result->Success(); result->Success();
} 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") { } else if (method == "getProperty") {
if (!player_ || !player_->IsInitialized()) { if (!player_ || !player_->IsInitialized()) {
result->Error("NOT_INITIALIZED", "Player not initialized"); result->Error("NOT_INITIALIZED", "Player not initialized");