From f883c78b4b9f0ad7c1ae0d665c5277b945d1981c Mon Sep 17 00:00:00 2001 From: Honney OC Date: Mon, 14 Sep 2026 00:38:31 +0000 Subject: [PATCH] fix: mpv in verify_sync --- verify_sync.py | 266 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 175 insertions(+), 91 deletions(-) diff --git a/verify_sync.py b/verify_sync.py index 9e516b1..779abbf 100644 --- a/verify_sync.py +++ b/verify_sync.py @@ -1,28 +1,23 @@ #!/usr/bin/env python3 """ -Verify sync: mpv video player + dual waveform display. +Verify sync: mpv video player (separate window) + dual waveform display. Modes: --mode short : extracted 30s parts (video_a + wav_a + wav_b) --mode full : synced output MKV (extracts audio streams from it) Features: - - Smooth video playback via mpv (handles all codecs natively) + - Smooth video playback via mpv (separate window, IPC-controlled) - Audio playback: A only, B only, or A+B mix via sounddevice - Combined waveform overlay (A and B in different colors) - Two separate waveform graphs with playhead - Zoomable waveforms (scroll) with linked x-axes - Timeline seek bar synced to video position - Toggle A/B on/off, volume - -Usage: - make verify LAG=0.042 - python verify_sync.py --config config.toml --lag 0.042 --mode short - python verify_sync.py --synced output/movie_synced.mkv --lag 0.042 --mode full """ from __future__ import annotations -import os, sys, math, argparse, subprocess, json, shutil, threading, tempfile, warnings, signal, locale +import os, sys, math, argparse, subprocess, json, shutil, threading, tempfile, warnings, signal, locale, socket, time, socketserver os.environ["LC_NUMERIC"] = "C" locale.setlocale(locale.LC_NUMERIC, "C") @@ -39,19 +34,10 @@ except ModuleNotFoundError: try: import pyqtgraph as pg from PyQt6 import QtWidgets, QtCore, QtGui - HAS_PG = True except ImportError as e: print("ERROR: pyqtgraph/PyQt6 not installed — run make install", file=sys.stderr) sys.exit(1) -try: - import mpv - HAS_MPV = True -except ImportError: - HAS_MPV = False - print("ERROR: python-mpv not installed — pip install python-mpv", file=sys.stderr) - sys.exit(1) - try: import sounddevice as sd HAS_SD = True @@ -175,6 +161,134 @@ class AudioPlayer: self.playing = False +# ─── mpv IPC controller ─── + +class MpvController: + """Control mpv as a standalone process via JSON IPC socket.""" + + def __init__(self, video_path: str): + self._sock_path = f"/tmp/mpv-verify-{os.getpid()}.sock" + self._cmd_id = 0 + self._lock = threading.Lock() + self._proc = None + self._sock = None + + # Remove stale socket + if os.path.exists(self._sock_path): + os.unlink(self._sock_path) + + # Launch mpv with IPC server + self._proc = subprocess.Popen([ + "mpv", + "--no-terminal", + "--keep-open=yes", + "--pause", + "--no-audio", + f"--input-ipc-server={self._sock_path}", + video_path, + ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + # Wait for socket to appear + for _ in range(50): + if os.path.exists(self._sock_path): + break + time.sleep(0.05) + + self._connect() + + def _connect(self): + self._sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self._sock.connect(self._sock_path) + self._sock.settimeout(0.5) + # Read any welcome message + try: + self._sock.recv(4096) + except socket.timeout: + pass + + def _send(self, command: list, timeout: float = 2.0) -> dict | None: + with self._lock: + self._cmd_id += 1 + msg = json.dumps({"command": command, "request_id": self._cmd_id}) + "\n" + self._sock.sendall(msg.encode()) + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + try: + data = self._sock.recv(4096) + if not data: + break + for line in data.decode().strip().split("\n"): + try: + resp = json.loads(line) + if resp.get("request_id") == self._cmd_id: + return resp + except json.JSONDecodeError: + continue + except socket.timeout: + continue + return None + + def get_property(self, prop: str): + resp = self._send(["get_property", prop]) + if resp and resp.get("error") == "success": + return resp.get("data") + return None + + def set_property(self, prop: str, value): + self._send(["set_property", prop, value]) + + @property + def time_pos(self) -> float: + v = self.get_property("time-pos") + return float(v) if v is not None else 0.0 + + @property + def duration(self) -> float: + v = self.get_property("duration") + return float(v) if v is not None else 0.0 + + @property + def paused(self) -> bool: + v = self.get_property("pause") + return bool(v) if v is not None else True + + def play(self): + self.set_property("pause", False) + + def pause(self): + self.set_property("pause", True) + + def seek(self, seconds: float): + self._send(["seek", seconds, "absolute"]) + + def stop(self): + try: + self._send(["stop"], timeout=0.5) + except Exception: + pass + + def terminate(self): + try: + self._send(["quit"], timeout=0.5) + except Exception: + pass + if self._proc: + try: + self._proc.wait(timeout=2) + except subprocess.TimeoutExpired: + self._proc.kill() + if self._sock: + try: + self._sock.close() + except Exception: + pass + if os.path.exists(self._sock_path): + try: + os.unlink(self._sock_path) + except Exception: + pass + + # ─── Main Window ─── class VerifyWindow(QtWidgets.QMainWindow): @@ -196,6 +310,7 @@ class VerifyWindow(QtWidgets.QMainWindow): self.default_volume = volume self.duration = 0.0 self.playing_a_b = False + self.mpv: MpvController | None = None title = "verify_sync" if mode == "short": @@ -204,7 +319,7 @@ class VerifyWindow(QtWidgets.QMainWindow): title += " — full length" title += f" — {os.path.basename(video_path)}" self.setWindowTitle(title) - self.resize(1400, 950) + self.resize(1400, 700) # Load audio print(f"Loading audio A: {wav_a_path}") @@ -218,8 +333,15 @@ class VerifyWindow(QtWidgets.QMainWindow): self._setup_ui() self._draw_waveforms() - # Defer mpv setup until window is shown and winId() is valid - QtCore.QTimer.singleShot(0, self._setup_mpv) + + # Start mpv in its own window + QtCore.QTimer.singleShot(0, self._start_mpv) + + # Sync timer: polls mpv position and updates waveforms + self.sync_timer = QtCore.QTimer() + self.sync_timer.setInterval(33) # ~30fps + self.sync_timer.timeout.connect(self._sync_from_mpv) + self.sync_timer.start() def _setup_ui(self): central = QtWidgets.QWidget() @@ -227,12 +349,6 @@ class VerifyWindow(QtWidgets.QMainWindow): lay = QtWidgets.QVBoxLayout(central) lay.setSpacing(2) - # ── Video display (QWidget container for mpv) ── - self.video_container = QtWidgets.QWidget() - self.video_container.setMinimumHeight(300) - self.video_container.setStyleSheet("background: #000;") - lay.addWidget(self.video_container, stretch=4) - # ── Timeline seek bar ── time_row = QtWidgets.QHBoxLayout() lay.addLayout(time_row) @@ -350,48 +466,22 @@ class VerifyWindow(QtWidgets.QMainWindow): self.info_label.setStyleSheet("color: #aaa; font-size: 11px;") ctrl.addWidget(self.info_label) - def _setup_mpv(self): - """Set up mpv player embedded in the video container widget.""" - self.mpv_player = mpv.MPV( - wid=str(int(self.video_container.winId())), - input_default_bindings=False, - input_vo_keyboard=False, - cursor_autohide=False, - keep_open=True, - osc=False, - osd_bar=False, - ) - # Mute mpv audio — we play audio via sounddevice - self.mpv_player.audio = "no" - - self.mpv_player.play(os.path.abspath(self.video_path)) - - # Timer to sync waveform playheads with mpv position - self.sync_timer = QtCore.QTimer() - self.sync_timer.setInterval(33) # ~30fps - self.sync_timer.timeout.connect(self._sync_from_video) - - # Wait for mpv to load duration - self._duration_timer = QtCore.QTimer() - self._duration_timer.setInterval(100) - self._duration_timer.timeout.connect(self._wait_for_duration) - self._duration_timer.start() - - def _wait_for_duration(self): - """Poll mpv until duration is available.""" + def _start_mpv(self): + """Launch mpv in its own window, controlled via IPC socket.""" + print(f"Launching mpv: {self.video_path}") try: - dur = self.mpv_player.duration - if dur and dur > 0: - self._duration_timer.stop() - vid_dur = float(dur) + self.mpv = MpvController(self.video_path) + vid_dur = self.mpv.duration + if vid_dur > 0: self.info_label.setText( self.info_label.text().replace( f"Audio duration: {self.duration:.1f}s", f"Video: {vid_dur:.1f}s" ) ) - except Exception: - pass + except Exception as e: + print(f"ERROR: failed to start mpv: {e}", file=sys.stderr) + self.info_label.setText(self.info_label.text() + " | mpv: FAILED") def _draw_waveforms(self): max_pts = 200_000 if self.mode == "full" else 80_000 @@ -440,10 +530,12 @@ class VerifyWindow(QtWidgets.QMainWindow): s = t % 60 return f"{h:02d}:{m:02d}:{s:06.3f}" - def _sync_from_video(self): - """Timer callback: sync waveform playheads and seek bar to mpv position.""" + def _sync_from_mpv(self): + """Poll mpv position and update waveforms.""" + if not self.mpv: + return try: - t = float(self.mpv_player.time_pos or 0) + t = self.mpv.time_pos except Exception: t = 0 if t > self.duration: @@ -451,9 +543,10 @@ class VerifyWindow(QtWidgets.QMainWindow): self._set_pos(t) def toggle_play(self): + if not self.mpv: + return try: - paused = self.mpv_player.pause - if paused is False: + if not self.mpv.paused: self.stop_play() else: self.start_play() @@ -461,17 +554,14 @@ class VerifyWindow(QtWidgets.QMainWindow): self.start_play() def start_play(self): - if self.duration <= 0: + if self.duration <= 0 or not self.mpv: return - # Start video playback - self.mpv_player.pause = False + # Start video playback in mpv + self.mpv.play() # Start audio mix via sounddevice - try: - t = float(self.mpv_player.time_pos or 0) - except Exception: - t = 0 + t = self.mpv.time_pos start_samp = int(t * self.sr) if start_samp >= len(self.a_aligned): start_samp = 0 @@ -492,35 +582,33 @@ class VerifyWindow(QtWidgets.QMainWindow): self.audio_player.play(mix, self.sr) self.playing_a_b = True self.btn_play.setText("Pause") - self.sync_timer.start() def stop_play(self): - self.mpv_player.pause = True + if self.mpv: + self.mpv.pause() self.audio_player.stop() self.playing_a_b = False self.btn_play.setText("Play") - self.sync_timer.stop() def _on_seek_start(self): self._seeking = True - try: - paused = self.mpv_player.pause - self._was_playing = (paused is False) - except Exception: - self._was_playing = False - if self._was_playing: - self.stop_play() + if self.mpv: + self._was_playing = not self.mpv.paused + if self._was_playing: + self.stop_play() def _on_seek_move(self, value): frac = value / 1_000_000.0 t = frac * self.duration self._set_pos(t) - self.mpv_player.seek(t, reference='absolute') + if self.mpv: + self.mpv.seek(t) def _on_seek_end(self): frac = self.seek.value() / 1_000_000.0 t = frac * self.duration - self.mpv_player.seek(t, reference='absolute') + if self.mpv: + self.mpv.seek(t) self._set_pos(t) self._seeking = False if getattr(self, '_was_playing', False): @@ -537,11 +625,8 @@ class VerifyWindow(QtWidgets.QMainWindow): self.p_comb.setXRange(0, self.duration, padding=0) def closeEvent(self, event): - try: - self.mpv_player.stop() - self.mpv_player.terminate() - except Exception: - pass + if self.mpv: + self.mpv.terminate() self.audio_player.stop() self.sync_timer.stop() if self.temp_dir and os.path.isdir(self.temp_dir): @@ -634,7 +719,6 @@ def main(): print(f"ERROR: wav B not found: {wav_b}", file=sys.stderr); sys.exit(1) video_path = video_a - # In short mode, use the extracted FFV1 video instead of the original HEVC source if args.mode == "short": stem = os.path.splitext(os.path.basename(video_a))[0] extracted_video = os.path.join(out_dir_extract, f"{stem}_video.mkv")