--- /dev/null
+#!/usr/bin/python3
+
+import os, sys, getopt, io, termios
+import socket, json, pprint
+
+class key(object):
+ def __init__(self, nm):
+ self.nm = nm
+ def __repr__(self):
+ return "<key %s>" % (self.nm,)
+class akey(key):
+ def __init__(self, cc):
+ super().__init__(repr(cc))
+ self.cc = cc
+ def __eq__(self, o):
+ return self.cc == o
+
+class rawtty(object):
+ K_LEFT = key("K_LEFT")
+ K_RIGHT = key("K_RIGHT")
+ K_UP = key("K_UP")
+ K_DOWN = key("K_DOWN")
+ MOD_SHIFT = key("MOD_SHIFT")
+ MOD_META = key("MOD_META")
+ MOD_CTRL = key("MOD_CTRL")
+
+ def __init__(self, *, path="/dev/tty"):
+ self.io = io.FileIO(os.open(path, os.O_RDWR | os.O_NOCTTY), "r+")
+ attr = termios.tcgetattr(self.io.fileno())
+ self.bka = list(attr)
+ attr[3] &= ~termios.ECHO & ~termios.ICANON
+ termios.tcsetattr(self.io.fileno(), termios.TCSANOW, attr)
+
+ def getc(self):
+ b = self.io.read(1)
+ return None if b == b"" else b[0]
+
+ _csikeys = {'A': K_UP, 'B': K_DOWN, 'C': K_RIGHT, 'D': K_LEFT}
+ def readkey(self):
+ c = self.getc()
+ if c == 27:
+ c = self.getc()
+ if c == 27:
+ return akey("\x1b"), set()
+ elif c == ord('O'):
+ return None, set()
+ elif c == ord('['):
+ pars = []
+ par = None
+ while True:
+ c = self.getc()
+ if 48 <= c <= 57:
+ if par is None:
+ par = 0
+ par = (par * 10) + (c - 48)
+ elif c == ord(';'):
+ pars.append(par)
+ par = None
+ else:
+ if par is not None:
+ pars.append(par)
+ break
+ if c == ord('~'):
+ key = None
+ elif chr(c) in self._csikeys:
+ key = self._csikeys[chr(c)]
+ else:
+ key = None
+ mods = set()
+ if len(pars) > 1:
+ if (pars[1] - 1) & 1:
+ mods.add(self.MOD_SHIFT)
+ if (pars[1] - 1) & 2:
+ mods.add(self.MOD_META)
+ if (pars[1] - 1) & 4:
+ mods.add(self.MOD_CTRL)
+ return key, mods
+ else:
+ return akey(chr(c)), [self.MOD_META]
+ else:
+ return akey(chr(c)), set()
+
+ def close(self):
+ termios.tcsetattr(self.io.fileno(), termios.TCSANOW, self.bka)
+ self.io.close()
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, *exc):
+ self.close()
+ return False
+
+class target(object):
+ def __init__(self, path):
+ self.path = path
+ self.sk = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ self.sk.connect(path)
+ self.obuf = bytearray()
+ self.ibuf = bytearray()
+
+ def write(self, data):
+ self.obuf.extend(data)
+
+ def flush(self):
+ while len(self.obuf) > 0:
+ ret = self.sk.send(self.obuf)
+ self.obuf[:ret] = b""
+
+ def recv(self, hint=1024):
+ data = self.sk.recv(hint)
+ if data == b"":
+ raise EOFError()
+ self.ibuf.extend(data)
+
+ def readline(self):
+ p = 0
+ while True:
+ p2 = self.ibuf.find(b'\n', p)
+ if p2 != -1:
+ ret = bytes(self.ibuf[:p2])
+ self.ibuf[:p2 + 1] = b""
+ return ret
+ p = len(self.ibuf)
+ self.recv()
+
+ def send(self, data):
+ self.write(data)
+ self.flush()
+
+ def getresp(self):
+ while True:
+ resp = json.loads(self.readline().decode("utf-8"))
+ if "event" in resp:
+ continue
+ return resp
+
+ def runcmd(self, cmd):
+ self.send(json.dumps(cmd).encode("utf-8") + b"\n")
+ resp = self.getresp()
+ if "error" not in resp:
+ sys.stderr.write("mpsync: strange response from %s: %r\n" % (self.path, resp))
+ if resp["error"] != "success":
+ sys.stderr.write("mpsync: error response from %s: %r\n" % (self.path, resp))
+ return resp
+
+ def getprop(self, pname):
+ return self.runcmd({"command": ["get_property", pname]})["data"]
+
+ def setprop(self, pname, val):
+ self.runcmd({"command": ["set_property", pname, val]})
+
+def usage(out):
+ out.write("usage: mpsync [-h] SOCKET...\n")
+ out.write("players: mpv -input-unix-socket SOCKET -pause [ARGS...] FILE\n")
+
+opts, args = getopt.getopt(sys.argv[1:], "h")
+for o, a in opts:
+ if o == "-h":
+ usage(sys.stdout)
+ sys.exit(0)
+if len(args) < 1:
+ usage(sys.stderr)
+ sys.exit(1)
+for path in args:
+ if not os.path.exists(path):
+ sys.stderr.write("mpsync: %s: no such file or directory\n" % path)
+ sys.exit(1)
+
+targets = []
+for path in args:
+ targets.append(target(path))
+
+def runcmd(*cmd):
+ cmd = {"command": cmd}
+ for tgt in targets:
+ tgt.runcmd(cmd)
+
+def simulcmd(*cmd):
+ cmd = json.dumps({"command": cmd}).encode("utf-8") + b"\n"
+ for tgt in targets:
+ tgt.send(cmd)
+ for tgt in targets:
+ resp = tgt.getresp()
+ if "error" not in resp:
+ sys.stderr.write("mpsync: strange response from %s: %r\n" % (tgt.path, resp))
+ if resp["error"] != "success":
+ sys.stderr.write("mpsync: error response from %s: %r\n" % (tgt.path, resp))
+
+def relseek(ss, offsets):
+ cur = targets[0].getprop("time-pos")
+ for tgt, off in zip(targets, offsets):
+ tgt.setprop("time-pos", cur + ss + off)
+
+def getoffsets():
+ opos = targets[0].getprop("time-pos")
+ ret = []
+ for tgt in targets:
+ ret.append(tgt.getprop("time-pos") - opos)
+ return ret
+
+def main(tty):
+ paused = targets[0].getprop("pause")
+ runcmd("set_property", "hr-seek", "yes")
+ offsets = [0.0] * len(targets)
+ while True:
+ c, mods = tty.readkey()
+ if c == 'q':
+ return
+ elif c == 'Q':
+ runcmd("quit")
+ return
+ elif c == ' ':
+ paused = not paused
+ simulcmd("set_property", "pause", paused)
+ elif c == rawtty.K_LEFT and not mods:
+ relseek(-10, offsets)
+ elif c == rawtty.K_RIGHT and not mods:
+ relseek(10, offsets)
+ elif c == rawtty.K_UP and not mods:
+ relseek(60, offsets)
+ elif c == rawtty.K_DOWN and not mods:
+ relseek(-60, offsets)
+ elif c == rawtty.K_LEFT and mods == {rawtty.MOD_SHIFT}:
+ relseek(-2, offsets)
+ elif c == rawtty.K_RIGHT and mods == {rawtty.MOD_SHIFT}:
+ relseek(2, offsets)
+ elif c == rawtty.K_UP and mods == {rawtty.MOD_SHIFT}:
+ relseek(5, offsets)
+ elif c == rawtty.K_DOWN and mods == {rawtty.MOD_SHIFT}:
+ relseek(-5, offsets)
+ elif c == 'S':
+ offsets = getoffsets()
+ print(offsets)
+ elif c == '.':
+ runcmd("frame_step")
+ paused = True
+ elif c == ',':
+ runcmd("frame_back_step")
+ paused = True
+
+with rawtty() as tty:
+ try:
+ main(tty)
+ except KeyboardInterrupt:
+ pass