Check in mpsync.
authorFredrik Tolf <fredrik@dolda2000.com>
Fri, 15 Apr 2016 22:50:15 +0000 (00:50 +0200)
committerFredrik Tolf <fredrik@dolda2000.com>
Fri, 15 Apr 2016 22:50:15 +0000 (00:50 +0200)
mpsync [new file with mode: 0755]

diff --git a/mpsync b/mpsync
new file mode 100755 (executable)
index 0000000..8783e28
--- /dev/null
+++ b/mpsync
@@ -0,0 +1,246 @@
+#!/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