#!/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 "" % (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() self.fps = self.getprop("container-fps") 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({"command": 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("get_property", pname)["data"] def setprop(self, pname, val): self.runcmd("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): 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): runcmd("set_property", "hr-seek", "yes") paused = targets[0].getprop("pause") mutemode = 0 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': relseek(0, offsets) elif c == 'm': mutemode = (mutemode + 1) % 3 if mutemode == 0: for tgt in targets: tgt.setprop("mute", False) elif mutemode == 1: for i, tgt in enumerate(targets): tgt.setprop("mute", i != 0) elif mutemode == 2: for tgt in targets: tgt.setprop("mute", True) targets[0].runcmd("show_text", "Audio mode: %s" % ["All", "One", "None"][mutemode]) elif c == 'S': offsets = getoffsets() for tgt, off in zip(targets, offsets): tgt.runcmd("show_text", "Offset: %i" % round(off * tgt.fps)) 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