Fix stutter by removing StateManager calls from playback path
This commit is contained in:
parent
29acef1e0a
commit
447d370c7f
5 changed files with 149 additions and 62 deletions
|
|
@ -3,7 +3,9 @@
|
|||
#include <mpv/render_gl.h>
|
||||
#include <epoxy/gl.h>
|
||||
#include <dlfcn.h>
|
||||
#include <locale.h>
|
||||
#include <math.h>
|
||||
#include <string.h>
|
||||
|
||||
struct _MpvWidget {
|
||||
GtkWidget parent_instance;
|
||||
|
|
@ -14,6 +16,9 @@ struct _MpvWidget {
|
|||
double position;
|
||||
double duration;
|
||||
gboolean paused;
|
||||
char *pending_url;
|
||||
gboolean initialized;
|
||||
gboolean should_play;
|
||||
};
|
||||
|
||||
enum {
|
||||
|
|
@ -32,7 +37,13 @@ static gboolean
|
|||
mpv_widget_render_frame(gpointer user_data)
|
||||
{
|
||||
MpvWidget *self = MPV_WIDGET(user_data);
|
||||
gtk_gl_area_queue_render(GTK_GL_AREA(self->gl_area));
|
||||
if (!self->mpv_gl)
|
||||
return G_SOURCE_REMOVE;
|
||||
|
||||
int64_t flags = mpv_render_context_update(self->mpv_gl);
|
||||
if (flags & MPV_RENDER_UPDATE_FRAME)
|
||||
gtk_gl_area_queue_render(GTK_GL_AREA(self->gl_area));
|
||||
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
|
|
@ -40,18 +51,16 @@ static void
|
|||
mpv_render_update(void *cb_ctx)
|
||||
{
|
||||
MpvWidget *self = MPV_WIDGET(cb_ctx);
|
||||
if (self->mpv_gl) {
|
||||
int64_t flags = mpv_render_context_update(self->mpv_gl);
|
||||
if (flags & MPV_RENDER_UPDATE_FRAME) {
|
||||
g_idle_add(mpv_widget_render_frame, self);
|
||||
}
|
||||
}
|
||||
if (self->mpv_gl)
|
||||
g_idle_add(mpv_widget_render_frame, self);
|
||||
}
|
||||
|
||||
static void *
|
||||
mpv_gl_get_proc_address(void *fn_ctx, const char *name)
|
||||
{
|
||||
(void)fn_ctx;
|
||||
if (!name)
|
||||
return NULL;
|
||||
return dlsym(RTLD_DEFAULT, name);
|
||||
}
|
||||
|
||||
|
|
@ -64,12 +73,14 @@ mpv_widget_on_realize(GtkWidget *widget, gpointer user_data)
|
|||
|
||||
gtk_gl_area_make_current(GTK_GL_AREA(widget));
|
||||
|
||||
setlocale(LC_NUMERIC, "C");
|
||||
|
||||
self->mpv = mpv_create();
|
||||
if (!self->mpv)
|
||||
return;
|
||||
|
||||
mpv_set_option_string(self->mpv, "vo", "libmpv");
|
||||
mpv_set_option_string(self->mpv, "hwdec", "auto");
|
||||
mpv_set_option_string(self->mpv, "hwdec", "no");
|
||||
mpv_set_option_string(self->mpv, "keep-open", "yes");
|
||||
|
||||
if (mpv_initialize(self->mpv) < 0) {
|
||||
|
|
@ -78,9 +89,10 @@ mpv_widget_on_realize(GtkWidget *widget, gpointer user_data)
|
|||
return;
|
||||
}
|
||||
|
||||
GdkGLContext *gl_context = gtk_gl_area_get_context(GTK_GL_AREA(widget));
|
||||
mpv_opengl_init_params gl_init = {
|
||||
.get_proc_address = mpv_gl_get_proc_address,
|
||||
.get_proc_address_ctx = NULL,
|
||||
.get_proc_address_ctx = gl_context,
|
||||
};
|
||||
|
||||
mpv_render_param params[] = {
|
||||
|
|
@ -98,7 +110,16 @@ mpv_widget_on_realize(GtkWidget *widget, gpointer user_data)
|
|||
mpv_render_context_set_update_callback(
|
||||
self->mpv_gl, mpv_render_update, self);
|
||||
|
||||
self->tick_source = g_timeout_add(250, (GSourceFunc)mpv_widget_tick, self);
|
||||
self->tick_source = g_timeout_add(500, (GSourceFunc)mpv_widget_tick, self);
|
||||
self->initialized = TRUE;
|
||||
|
||||
if (self->pending_url) {
|
||||
mpv_command(self->mpv, (const char *[]){"loadfile", self->pending_url, NULL});
|
||||
mpv_set_property_string(self->mpv, "pause",
|
||||
self->should_play ? "no" : "yes");
|
||||
g_free(self->pending_url);
|
||||
self->pending_url = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
|
|
@ -149,10 +170,12 @@ mpv_widget_on_render(GtkGLArea *area, GdkGLContext *context, gpointer user_data)
|
|||
};
|
||||
|
||||
int flip_y = 1;
|
||||
int block = 0;
|
||||
|
||||
mpv_render_param params[] = {
|
||||
{MPV_RENDER_PARAM_OPENGL_FBO, &fbo},
|
||||
{MPV_RENDER_PARAM_FLIP_Y, &flip_y},
|
||||
{MPV_RENDER_PARAM_BLOCK_FOR_TARGET_TIME, &block},
|
||||
{0}
|
||||
};
|
||||
|
||||
|
|
@ -173,16 +196,16 @@ mpv_widget_tick(gpointer user_data)
|
|||
mpv_event *event = mpv_wait_event(self->mpv, 0);
|
||||
if (event->event_id == MPV_EVENT_NONE)
|
||||
break;
|
||||
if (event->event_id == MPV_EVENT_END_FILE) {
|
||||
if (event->event_id == MPV_EVENT_END_FILE)
|
||||
g_signal_emit(self, signals[SIGNAL_PLAYBACK_STATE_CHANGED], 0, TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
double pos = 0;
|
||||
if (mpv_get_property(self->mpv, "time-pos",
|
||||
MPV_FORMAT_DOUBLE, &pos) == MPV_ERROR_SUCCESS)
|
||||
{
|
||||
if (fabs(pos - self->position) > 0.1) {
|
||||
double diff = fabs(pos - self->position);
|
||||
if (diff > 1.0) {
|
||||
self->position = pos;
|
||||
g_signal_emit(self, signals[SIGNAL_POSITION_CHANGED], 0, pos);
|
||||
}
|
||||
|
|
@ -276,6 +299,9 @@ mpv_widget_dispose(GObject *object)
|
|||
self->tick_source = 0;
|
||||
}
|
||||
|
||||
g_free(self->pending_url);
|
||||
self->pending_url = NULL;
|
||||
|
||||
if (self->gl_area) {
|
||||
gtk_widget_unparent(self->gl_area);
|
||||
self->gl_area = NULL;
|
||||
|
|
@ -305,6 +331,9 @@ mpv_widget_init(MpvWidget *self)
|
|||
self->duration = 0.0;
|
||||
self->paused = TRUE;
|
||||
self->tick_source = 0;
|
||||
self->pending_url = NULL;
|
||||
self->initialized = FALSE;
|
||||
self->should_play = FALSE;
|
||||
}
|
||||
|
||||
static void
|
||||
|
|
@ -367,8 +396,11 @@ mpv_widget_load_url(MpvWidget *self, const char *url)
|
|||
g_return_if_fail(MPV_IS_WIDGET(self));
|
||||
g_return_if_fail(url != NULL);
|
||||
|
||||
if (!self->mpv)
|
||||
if (!self->mpv) {
|
||||
g_free(self->pending_url);
|
||||
self->pending_url = g_strdup(url);
|
||||
return;
|
||||
}
|
||||
|
||||
const char *cmd[] = {"loadfile", url, NULL};
|
||||
mpv_command(self->mpv, cmd);
|
||||
|
|
@ -380,8 +412,11 @@ mpv_widget_play(MpvWidget *self)
|
|||
{
|
||||
g_return_if_fail(MPV_IS_WIDGET(self));
|
||||
|
||||
if (self->mpv)
|
||||
mpv_set_property_string(self->mpv, "pause", "no");
|
||||
if (!self->mpv) {
|
||||
self->should_play = TRUE;
|
||||
return;
|
||||
}
|
||||
mpv_set_property_string(self->mpv, "pause", "no");
|
||||
}
|
||||
|
||||
void
|
||||
|
|
@ -389,8 +424,11 @@ mpv_widget_pause(MpvWidget *self)
|
|||
{
|
||||
g_return_if_fail(MPV_IS_WIDGET(self));
|
||||
|
||||
if (self->mpv)
|
||||
mpv_set_property_string(self->mpv, "pause", "yes");
|
||||
if (!self->mpv) {
|
||||
self->should_play = FALSE;
|
||||
return;
|
||||
}
|
||||
mpv_set_property_string(self->mpv, "pause", "yes");
|
||||
}
|
||||
|
||||
void
|
||||
|
|
|
|||
30
Sources/LuminateCore/PlayerPlaybackState.swift
Normal file
30
Sources/LuminateCore/PlayerPlaybackState.swift
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
//
|
||||
// PlayerPlaybackState.swift
|
||||
//
|
||||
// Copyright 2026 Brendan Szymanski <hello@bscubed.dev>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Shared mutable state for direct C widget manipulation during playback.
|
||||
/// Accessed from signal handlers to update GTK widgets without triggering
|
||||
/// Adwaita's `StateManager.updateViews()`.
|
||||
public final class PlayerPlaybackState {
|
||||
public var seekScale: OpaquePointer?
|
||||
public init() {}
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@
|
|||
//
|
||||
|
||||
import Adwaita
|
||||
import LuminateCore
|
||||
import LuminateUI
|
||||
|
||||
public struct PlayerControls: View {
|
||||
|
|
@ -27,6 +28,7 @@ public struct PlayerControls: View {
|
|||
@Binding var isPlaying: Bool
|
||||
@Binding var position: Double
|
||||
@Binding var duration: Double
|
||||
var playbackState: PlayerPlaybackState?
|
||||
public var onClose: () -> Void
|
||||
public var onSeekBack: () -> Void
|
||||
public var onSeekForward: () -> Void
|
||||
|
|
@ -34,10 +36,11 @@ public struct PlayerControls: View {
|
|||
public var onFullscreen: () -> Void
|
||||
public var onSubtitleAudio: () -> Void
|
||||
|
||||
public init(
|
||||
init(
|
||||
isPlaying: Binding<Bool>,
|
||||
position: Binding<Double>,
|
||||
duration: Binding<Double>,
|
||||
playbackState: PlayerPlaybackState? = nil,
|
||||
onClose: @escaping () -> Void,
|
||||
onSeekBack: @escaping () -> Void,
|
||||
onSeekForward: @escaping () -> Void,
|
||||
|
|
@ -48,6 +51,7 @@ public struct PlayerControls: View {
|
|||
self._isPlaying = isPlaying
|
||||
self._position = position
|
||||
self._duration = duration
|
||||
self.playbackState = playbackState
|
||||
self.onClose = onClose
|
||||
self.onSeekBack = onSeekBack
|
||||
self.onSeekForward = onSeekForward
|
||||
|
|
@ -83,6 +87,7 @@ public struct PlayerControls: View {
|
|||
SeekBar(
|
||||
value: $position,
|
||||
range: 0...(duration > 0 ? duration : 1),
|
||||
playbackState: playbackState,
|
||||
onSeek: { newPos in
|
||||
onSeekAbsolute(newPos)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,8 @@ public struct PlayerView: View {
|
|||
@State private var mpvWidget: OpaquePointer?
|
||||
@State private var tasks = PlayerTasks()
|
||||
|
||||
private let playbackState = PlayerPlaybackState()
|
||||
|
||||
public init(playerState: PlayerState, onClose: @escaping () -> Void) {
|
||||
self.playerState = playerState
|
||||
self.onClose = onClose
|
||||
|
|
@ -58,7 +60,8 @@ public struct PlayerView: View {
|
|||
isPlaying: $isPlaying,
|
||||
position: $position,
|
||||
duration: $duration,
|
||||
onWidgetCreated: { ptr in /*mpvWidget = ptr*/ }
|
||||
playbackState: playbackState,
|
||||
onWidgetCreated: { ptr in _mpvWidget.rawValue = ptr }
|
||||
)
|
||||
.vexpand(true)
|
||||
.hexpand(true)
|
||||
|
|
@ -79,6 +82,7 @@ public struct PlayerView: View {
|
|||
isPlaying: $isPlaying,
|
||||
position: $position,
|
||||
duration: $duration,
|
||||
playbackState: playbackState,
|
||||
onClose: {
|
||||
stopPlayback()
|
||||
onClose()
|
||||
|
|
@ -204,14 +208,22 @@ struct VideoPlayerWidget: Widget {
|
|||
@Binding var isPlaying: Bool
|
||||
@Binding var position: Double
|
||||
@Binding var duration: Double
|
||||
|
||||
var playbackState: PlayerPlaybackState?
|
||||
var onWidgetCreated: ((OpaquePointer) -> Void)?
|
||||
|
||||
init(url: String?, isPlaying: Binding<Bool>, position: Binding<Double>, duration: Binding<Double>, onWidgetCreated: ((OpaquePointer) -> Void)? = nil) {
|
||||
init(
|
||||
url: String?,
|
||||
isPlaying: Binding<Bool>,
|
||||
position: Binding<Double>,
|
||||
duration: Binding<Double>,
|
||||
playbackState: PlayerPlaybackState? = nil,
|
||||
onWidgetCreated: ((OpaquePointer) -> Void)? = nil
|
||||
) {
|
||||
self.url = url
|
||||
self._isPlaying = isPlaying
|
||||
self._position = position
|
||||
self._duration = duration
|
||||
self.playbackState = playbackState
|
||||
self.onWidgetCreated = onWidgetCreated
|
||||
}
|
||||
|
||||
|
|
@ -220,61 +232,49 @@ struct VideoPlayerWidget: Widget {
|
|||
let storage = ViewStorage(mpv_widget_new()?.opaque())
|
||||
|
||||
if let widgetPtr = storage.opaquePointer.map(UnsafeMutableRawPointer.init) {
|
||||
let context = SeekBarBindingContext(
|
||||
position: _position,
|
||||
let ctx = SignalContext(
|
||||
duration: _duration,
|
||||
isPlaying: _isPlaying
|
||||
isPlaying: _isPlaying,
|
||||
playbackState: playbackState
|
||||
)
|
||||
storage.fields["context"] = context
|
||||
let ctxPtr = Unmanaged.passUnretained(context).toOpaque()
|
||||
storage.fields["ctx"] = ctx
|
||||
let ctxPtr = Unmanaged.passUnretained(ctx).toOpaque()
|
||||
|
||||
let posHandler: @convention(c) (
|
||||
OpaquePointer?,
|
||||
Double,
|
||||
UnsafeMutableRawPointer?
|
||||
OpaquePointer?, Double, UnsafeMutableRawPointer?
|
||||
) -> Void = { _, pos, ptr in
|
||||
let binding = Unmanaged<SeekBarBindingContext>.fromOpaque(ptr!).takeUnretainedValue()
|
||||
binding.position.wrappedValue = pos
|
||||
let ctx = Unmanaged<SignalContext>.fromOpaque(ptr!).takeUnretainedValue()
|
||||
if let scale = ctx.playbackState?.seekScale {
|
||||
let range = unsafeBitCast(scale, to: UnsafeMutablePointer<GtkRange>?.self)
|
||||
gtk_range_set_value(range, pos)
|
||||
}
|
||||
}
|
||||
g_signal_connect_data(
|
||||
widgetPtr,
|
||||
"position-changed",
|
||||
widgetPtr, "position-changed",
|
||||
unsafeBitCast(posHandler, to: GCallback.self),
|
||||
ctxPtr,
|
||||
nil,
|
||||
GConnectFlags(rawValue: 1))
|
||||
ctxPtr, nil, GConnectFlags(rawValue: 1))
|
||||
|
||||
let durHandler: @convention(c) (
|
||||
OpaquePointer?,
|
||||
Double,
|
||||
UnsafeMutableRawPointer?
|
||||
OpaquePointer?, Double, UnsafeMutableRawPointer?
|
||||
) -> Void = { _, dur, ptr in
|
||||
let binding = Unmanaged<SeekBarBindingContext>.fromOpaque(ptr!).takeUnretainedValue()
|
||||
binding.duration.wrappedValue = dur
|
||||
let ctx = Unmanaged<SignalContext>.fromOpaque(ptr!).takeUnretainedValue()
|
||||
ctx.duration.wrappedValue = dur
|
||||
}
|
||||
g_signal_connect_data(
|
||||
widgetPtr,
|
||||
"duration-changed",
|
||||
widgetPtr, "duration-changed",
|
||||
unsafeBitCast(durHandler, to: GCallback.self),
|
||||
ctxPtr,
|
||||
nil,
|
||||
GConnectFlags(rawValue: 1))
|
||||
ctxPtr, nil, GConnectFlags(rawValue: 1))
|
||||
|
||||
let stateHandler: @convention(c) (
|
||||
OpaquePointer?,
|
||||
Int32,
|
||||
UnsafeMutableRawPointer?
|
||||
OpaquePointer?, Int32, UnsafeMutableRawPointer?
|
||||
) -> Void = { _, paused, ptr in
|
||||
let binding = Unmanaged<SeekBarBindingContext>.fromOpaque(ptr!).takeUnretainedValue()
|
||||
binding.isPlaying.wrappedValue = paused == 0
|
||||
let ctx = Unmanaged<SignalContext>.fromOpaque(ptr!).takeUnretainedValue()
|
||||
ctx.isPlaying.wrappedValue = paused == 0
|
||||
}
|
||||
g_signal_connect_data(
|
||||
widgetPtr,
|
||||
"playback-state-changed",
|
||||
widgetPtr, "playback-state-changed",
|
||||
unsafeBitCast(stateHandler, to: GCallback.self),
|
||||
ctxPtr,
|
||||
nil,
|
||||
GConnectFlags(rawValue: 1))
|
||||
ctxPtr, nil, GConnectFlags(rawValue: 1))
|
||||
}
|
||||
|
||||
if let ptr = storage.opaquePointer, let onWidgetCreated {
|
||||
|
|
@ -320,16 +320,21 @@ struct VideoPlayerWidget: Widget {
|
|||
|
||||
}
|
||||
|
||||
// MARK: - Signal binding context
|
||||
// MARK: - Signal context
|
||||
|
||||
class SeekBarBindingContext {
|
||||
var position: Binding<Double>
|
||||
class SignalContext {
|
||||
var duration: Binding<Double>
|
||||
var isPlaying: Binding<Bool>
|
||||
weak var playbackState: PlayerPlaybackState?
|
||||
|
||||
init(position: Binding<Double>, duration: Binding<Double>, isPlaying: Binding<Bool>) {
|
||||
self.position = position
|
||||
init(
|
||||
duration: Binding<Double>,
|
||||
isPlaying: Binding<Bool>,
|
||||
playbackState: PlayerPlaybackState?
|
||||
) {
|
||||
self.duration = duration
|
||||
self.isPlaying = isPlaying
|
||||
self.playbackState = playbackState
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
|
||||
import Adwaita
|
||||
import CAdw
|
||||
import LuminateCore
|
||||
|
||||
/// A seekable progress slider wrapping `GtkScale`.
|
||||
///
|
||||
|
|
@ -44,15 +45,18 @@ public struct SeekBar: Widget {
|
|||
|
||||
@Binding var value: Double
|
||||
var range: ClosedRange<Double>
|
||||
var playbackState: PlayerPlaybackState?
|
||||
var onSeek: ((Double) -> Void)?
|
||||
|
||||
public init(
|
||||
value: Binding<Double>,
|
||||
range: ClosedRange<Double>,
|
||||
playbackState: PlayerPlaybackState? = nil,
|
||||
onSeek: ((Double) -> Void)? = nil
|
||||
) {
|
||||
self._value = value
|
||||
self.range = range
|
||||
self.playbackState = playbackState
|
||||
self.onSeek = onSeek
|
||||
}
|
||||
|
||||
|
|
@ -72,6 +76,11 @@ public struct SeekBar: Widget {
|
|||
gtk_range_set_show_fill_level(scale?.cast(), 1)
|
||||
gtk_range_set_restrict_to_fill_level(scale?.cast(), 0)
|
||||
|
||||
// Register scale for direct C-level updates during playback
|
||||
if let scale {
|
||||
playbackState?.seekScale = scale
|
||||
}
|
||||
|
||||
let storage = ViewStorage(scale)
|
||||
|
||||
let context = SeekBarContext()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue