160 lines
5.5 KiB
Python
160 lines
5.5 KiB
Python
"""
|
|
MonitorWidget: embeds ffplay into the GTK4 window for live stream monitoring.
|
|
|
|
On X11, uses the --wid flag of mpv for native embedding, or reparents
|
|
ffplay's window via xdotool. GTK4 dropped GtkSocket so we use X11-level tricks.
|
|
"""
|
|
|
|
import logging
|
|
import subprocess
|
|
import signal
|
|
from threading import Timer
|
|
|
|
import gi
|
|
gi.require_version("Gtk", "4.0")
|
|
from gi.repository import Gtk, GLib, Gdk
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class MonitorWidget(Gtk.Box):
|
|
"""Widget that embeds an ffplay/mpv window for live stream monitoring."""
|
|
|
|
def __init__(self, **kwargs):
|
|
super().__init__(orientation=Gtk.Orientation.VERTICAL, **kwargs)
|
|
self._proc = None
|
|
self._xid = None
|
|
|
|
self._drawing_area = Gtk.DrawingArea()
|
|
self._drawing_area.set_hexpand(True)
|
|
self._drawing_area.set_vexpand(True)
|
|
self._drawing_area.set_content_height(250)
|
|
self.append(self._drawing_area)
|
|
log.info("MonitorWidget initialized")
|
|
|
|
def start_ffplay(self, input_path):
|
|
log.info("Starting ffplay with input: %s", input_path)
|
|
cmd = [
|
|
"ffplay",
|
|
"-fflags", "nobuffer",
|
|
"-flags", "low_delay",
|
|
"-framedrop",
|
|
"-noborder",
|
|
"-i", str(input_path),
|
|
]
|
|
log.info("ffplay cmd: %s", " ".join(cmd))
|
|
|
|
self._proc = subprocess.Popen(
|
|
cmd,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
)
|
|
log.info("ffplay started: pid=%s", self._proc.pid)
|
|
self._start_stderr_log("ffplay")
|
|
Timer(1.0, self._try_reparent).start()
|
|
|
|
def start_mpv(self, input_path):
|
|
log.info("Starting mpv with input: %s", input_path)
|
|
|
|
surface = self._drawing_area.get_native().get_surface()
|
|
xid = None
|
|
if hasattr(surface, "get_xid"):
|
|
xid = surface.get_xid()
|
|
log.info("Got X11 window ID: %s", xid)
|
|
else:
|
|
log.warning("No get_xid on surface (type=%s), mpv will open separate window", type(surface).__name__)
|
|
|
|
cmd = [
|
|
"mpv",
|
|
"--no-terminal",
|
|
"--no-osc",
|
|
"--no-input-default-bindings",
|
|
"--profile=low-latency",
|
|
"--demuxer-lavf-o=fflags=+nobuffer",
|
|
]
|
|
|
|
if xid:
|
|
cmd.append(f"--wid={xid}")
|
|
|
|
cmd.append(str(input_path))
|
|
log.info("mpv cmd: %s", " ".join(cmd))
|
|
|
|
self._proc = subprocess.Popen(
|
|
cmd,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
)
|
|
log.info("mpv started: pid=%s", self._proc.pid)
|
|
self._start_stderr_log("mpv")
|
|
|
|
def _start_stderr_log(self, name):
|
|
"""Read stderr in background thread and log it."""
|
|
import threading
|
|
|
|
def _read():
|
|
try:
|
|
for line in self._proc.stderr:
|
|
text = line.decode("utf-8", errors="replace").rstrip()
|
|
if text:
|
|
log.info("[%s:stderr] %s", name, text)
|
|
except Exception as e:
|
|
log.warning("[%s:stderr] read error: %s", name, e)
|
|
retcode = self._proc.poll() if self._proc else None
|
|
log.info("[%s] process exited: code=%s", name, retcode)
|
|
|
|
t = threading.Thread(target=_read, daemon=True, name=f"{name}_stderr")
|
|
t.start()
|
|
|
|
def _try_reparent(self):
|
|
if self._proc is None or self._proc.poll() is not None:
|
|
log.warning("ffplay not running, cannot reparent")
|
|
return
|
|
|
|
log.info("Attempting to reparent ffplay window (pid=%s)", self._proc.pid)
|
|
try:
|
|
result = subprocess.run(
|
|
["xdotool", "search", "--pid", str(self._proc.pid)],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=5,
|
|
)
|
|
windows = result.stdout.strip().split("\n")
|
|
log.info("xdotool found windows: %s", windows)
|
|
if windows and windows[0]:
|
|
ffplay_wid = windows[0]
|
|
surface = self._drawing_area.get_native().get_surface()
|
|
if hasattr(surface, "get_xid"):
|
|
parent_xid = surface.get_xid()
|
|
log.info("Reparenting ffplay %s into %s", ffplay_wid, parent_xid)
|
|
subprocess.run(
|
|
["xdotool", "windowreparent", ffplay_wid, str(parent_xid)],
|
|
timeout=5,
|
|
)
|
|
subprocess.run(
|
|
["xdotool", "windowsize", ffplay_wid, "100%", "100%"],
|
|
timeout=5,
|
|
)
|
|
log.info("Reparenting done")
|
|
else:
|
|
log.warning("No get_xid on surface, cannot reparent")
|
|
else:
|
|
log.warning("No windows found for ffplay pid=%s", self._proc.pid)
|
|
except FileNotFoundError:
|
|
log.error("xdotool not found, cannot reparent ffplay window")
|
|
except subprocess.TimeoutExpired:
|
|
log.error("xdotool timed out")
|
|
|
|
def stop(self):
|
|
if self._proc and self._proc.poll() is None:
|
|
log.info("Stopping monitor process pid=%s", self._proc.pid)
|
|
self._proc.send_signal(signal.SIGINT)
|
|
try:
|
|
self._proc.wait(timeout=3)
|
|
log.info("Monitor process stopped gracefully")
|
|
except subprocess.TimeoutExpired:
|
|
self._proc.kill()
|
|
log.warning("Monitor process killed (did not stop in 3s)")
|
|
self._proc = None
|
|
else:
|
|
log.info("Monitor process already stopped or never started")
|