berarr
This commit is contained in:
@@ -179,19 +179,19 @@ class MonitorWidget(Gtk.Box):
|
||||
else:
|
||||
# Scrub mode
|
||||
if current == "live":
|
||||
# Transitioning from live: seek review player to live position
|
||||
# Transitioning from live: load MKV at cursor position atomically
|
||||
pos = s.cursor # already set by toggle_live()
|
||||
if self._review_player and self._recording_path:
|
||||
self._review_player.load(self._recording_path)
|
||||
if s.paused:
|
||||
self._review_player.show_frame_at(pos)
|
||||
else:
|
||||
self._review_player.seek(pos)
|
||||
self._review_player.load_at(self._recording_path, pos, pause=s.paused)
|
||||
if not s.paused:
|
||||
self._review_player.play()
|
||||
self._stack.set_visible_child_name("review")
|
||||
else:
|
||||
# Already in review: just apply paused state
|
||||
# Already in review: seek if cursor moved, then apply pause/play
|
||||
if self._review_player:
|
||||
player_pos = self._review_player.time_pos or 0
|
||||
if abs(s.cursor - player_pos) > 1.0:
|
||||
self._review_player.seek(s.cursor)
|
||||
if s.paused:
|
||||
self._review_player.pause()
|
||||
else:
|
||||
|
||||
@@ -92,6 +92,12 @@ class Player:
|
||||
log.info("mpv load: %s", path)
|
||||
self._player.loadfile(str(path), mode="replace")
|
||||
|
||||
def load_at(self, path, seconds, pause=True):
|
||||
"""Load a file and seek to position atomically. Avoids async seek race."""
|
||||
log.info("mpv load_at: %s at %.1fs pause=%s", path, seconds, pause)
|
||||
self._player["pause"] = pause
|
||||
self._player.loadfile(str(path), mode="replace", start=str(seconds))
|
||||
|
||||
def load_live(self, url):
|
||||
"""Load a live stream URL with low-latency options."""
|
||||
self._player["cache"] = "no"
|
||||
|
||||
@@ -10,7 +10,6 @@ consumers read timeline.state directly.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
import gi
|
||||
@@ -88,7 +87,9 @@ class Timeline(GObject.Object):
|
||||
"""Go to live mode at the recording end."""
|
||||
self.state.live = True
|
||||
self.state.paused = False
|
||||
self.state.cursor = self.state.duration
|
||||
if self.state.duration > 0:
|
||||
self.state.cursor = self.state.duration
|
||||
# else: keep current cursor (tick_live will continue from here)
|
||||
self._emit()
|
||||
|
||||
def toggle_live(self, live_player_pos=None):
|
||||
@@ -102,12 +103,17 @@ class Timeline(GObject.Object):
|
||||
self.state.live = False
|
||||
self.state.paused = True
|
||||
if live_player_pos is not None and live_player_pos > 0:
|
||||
pos = max(0.0, min(live_player_pos, self.state.duration))
|
||||
pos = max(0.0, live_player_pos)
|
||||
# Only clamp to duration if duration is known
|
||||
if self.state.duration > 0:
|
||||
pos = min(pos, self.state.duration)
|
||||
self.state.cursor = pos
|
||||
else:
|
||||
self.state.live = True
|
||||
self.state.paused = False
|
||||
self.state.cursor = self.state.duration
|
||||
if self.state.duration > 0:
|
||||
self.state.cursor = self.state.duration
|
||||
# else: keep current tick-based cursor, set_duration will snap later
|
||||
self._emit()
|
||||
|
||||
def play(self):
|
||||
@@ -138,35 +144,28 @@ class Timeline(GObject.Object):
|
||||
|
||||
|
||||
class TimelineControls(Gtk.Box):
|
||||
"""Shared slider + play/pause/live controls.
|
||||
"""Slider + LIVE toggle. Scrub mode is always paused (seek-only, like a video editor).
|
||||
|
||||
Play/Pause and slider are insensitive in live mode.
|
||||
LIVE button is a toggle — active style when live=True.
|
||||
Slider is insensitive in live mode.
|
||||
"""
|
||||
|
||||
def __init__(self, timeline, **kwargs):
|
||||
super().__init__(orientation=Gtk.Orientation.HORIZONTAL, spacing=4, **kwargs)
|
||||
self._timeline = timeline
|
||||
self._updating_slider = False
|
||||
self._dragging = False
|
||||
self._wall_clock_start = None
|
||||
|
||||
self.set_margin_start(4)
|
||||
self.set_margin_end(4)
|
||||
self.set_margin_top(2)
|
||||
self.set_margin_bottom(4)
|
||||
|
||||
# Play/Pause button
|
||||
self._play_btn = Gtk.Button(label="Play")
|
||||
self._play_btn.connect("clicked", self._on_play_clicked)
|
||||
self.append(self._play_btn)
|
||||
|
||||
# Current time label
|
||||
self._time_label = Gtk.Label(label="00:00")
|
||||
self._time_label.set_width_chars(6)
|
||||
self.append(self._time_label)
|
||||
|
||||
# Slider
|
||||
# Slider — disabled in live mode, scrub-seeks on release
|
||||
self._slider = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
self._slider.set_hexpand(True)
|
||||
self._slider.set_range(0, 1)
|
||||
@@ -193,16 +192,8 @@ class TimelineControls(Gtk.Box):
|
||||
timeline.connect("changed", self._on_changed)
|
||||
GLib.timeout_add(1000, self._tick_total)
|
||||
|
||||
def _on_play_clicked(self, btn):
|
||||
s = self._timeline.state
|
||||
if s.paused:
|
||||
self._timeline.play()
|
||||
else:
|
||||
self._timeline.pause()
|
||||
|
||||
def set_live_toggle_callback(self, cb):
|
||||
"""Override the LIVE button handler. cb() should return the live player
|
||||
position (float or None) and call timeline.toggle_live() itself."""
|
||||
"""Override the LIVE button handler."""
|
||||
self._live_toggle_cb = cb
|
||||
|
||||
def _on_live_clicked(self, btn):
|
||||
@@ -226,12 +217,6 @@ class TimelineControls(Gtk.Box):
|
||||
def _on_changed(self, timeline):
|
||||
s = timeline.state
|
||||
|
||||
# Start wall clock when first going live (not on duration, which arrives ~30s later)
|
||||
if s.live and self._wall_clock_start is None:
|
||||
self._wall_clock_start = time.monotonic()
|
||||
|
||||
# Live mode: disable scrub controls
|
||||
self._play_btn.set_sensitive(not s.live)
|
||||
self._slider.set_sensitive(not s.live)
|
||||
|
||||
if s.live:
|
||||
@@ -239,15 +224,9 @@ class TimelineControls(Gtk.Box):
|
||||
else:
|
||||
self._live_btn.remove_css_class("suggested-action")
|
||||
|
||||
# Play button label (only relevant in scrub mode)
|
||||
self._play_btn.set_label("Pause" if not s.paused else "Play")
|
||||
|
||||
# Slider position
|
||||
if not self._dragging:
|
||||
self._updating_slider = True
|
||||
self._slider.set_range(0, max(s.duration, 0.1))
|
||||
self._slider.set_value(s.cursor)
|
||||
self._updating_slider = False
|
||||
|
||||
self._time_label.set_text(self._fmt_time(s.cursor))
|
||||
self._update_duration_label()
|
||||
@@ -258,10 +237,8 @@ class TimelineControls(Gtk.Box):
|
||||
|
||||
def _update_duration_label(self):
|
||||
s = self._timeline.state
|
||||
loaded = s.duration
|
||||
total = (time.monotonic() - self._wall_clock_start) if self._wall_clock_start else loaded
|
||||
self._duration_label.set_text(
|
||||
f"{self._fmt_time(loaded)} / {self._fmt_time(total)}"
|
||||
f"{self._fmt_time(s.cursor)} / {self._fmt_time(s.duration)}"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
||||
Reference in New Issue
Block a user