| 1 | #!/usr/bin/python3 |
| 2 | |
| 3 | import os, sys, getopt, io, termios |
| 4 | import socket, json, pprint |
| 5 | |
| 6 | class key(object): |
| 7 | def __init__(self, nm): |
| 8 | self.nm = nm |
| 9 | def __repr__(self): |
| 10 | return "<key %s>" % (self.nm,) |
| 11 | class akey(key): |
| 12 | def __init__(self, cc): |
| 13 | super().__init__(repr(cc)) |
| 14 | self.cc = cc |
| 15 | def __eq__(self, o): |
| 16 | return self.cc == o |
| 17 | |
| 18 | class rawtty(object): |
| 19 | K_LEFT = key("K_LEFT") |
| 20 | K_RIGHT = key("K_RIGHT") |
| 21 | K_UP = key("K_UP") |
| 22 | K_DOWN = key("K_DOWN") |
| 23 | MOD_SHIFT = key("MOD_SHIFT") |
| 24 | MOD_META = key("MOD_META") |
| 25 | MOD_CTRL = key("MOD_CTRL") |
| 26 | |
| 27 | def __init__(self, *, path="/dev/tty"): |
| 28 | self.io = io.FileIO(os.open(path, os.O_RDWR | os.O_NOCTTY), "r+") |
| 29 | attr = termios.tcgetattr(self.io.fileno()) |
| 30 | self.bka = list(attr) |
| 31 | attr[3] &= ~termios.ECHO & ~termios.ICANON |
| 32 | termios.tcsetattr(self.io.fileno(), termios.TCSANOW, attr) |
| 33 | |
| 34 | def getc(self): |
| 35 | b = self.io.read(1) |
| 36 | return None if b == b"" else b[0] |
| 37 | |
| 38 | _csikeys = {'A': K_UP, 'B': K_DOWN, 'C': K_RIGHT, 'D': K_LEFT} |
| 39 | def readkey(self): |
| 40 | c = self.getc() |
| 41 | if c == 27: |
| 42 | c = self.getc() |
| 43 | if c == 27: |
| 44 | return akey("\x1b"), set() |
| 45 | elif c == ord('O'): |
| 46 | return None, set() |
| 47 | elif c == ord('['): |
| 48 | pars = [] |
| 49 | par = None |
| 50 | while True: |
| 51 | c = self.getc() |
| 52 | if 48 <= c <= 57: |
| 53 | if par is None: |
| 54 | par = 0 |
| 55 | par = (par * 10) + (c - 48) |
| 56 | elif c == ord(';'): |
| 57 | pars.append(par) |
| 58 | par = None |
| 59 | else: |
| 60 | if par is not None: |
| 61 | pars.append(par) |
| 62 | break |
| 63 | if c == ord('~'): |
| 64 | key = None |
| 65 | elif chr(c) in self._csikeys: |
| 66 | key = self._csikeys[chr(c)] |
| 67 | else: |
| 68 | key = None |
| 69 | mods = set() |
| 70 | if len(pars) > 1: |
| 71 | if (pars[1] - 1) & 1: |
| 72 | mods.add(self.MOD_SHIFT) |
| 73 | if (pars[1] - 1) & 2: |
| 74 | mods.add(self.MOD_META) |
| 75 | if (pars[1] - 1) & 4: |
| 76 | mods.add(self.MOD_CTRL) |
| 77 | return key, mods |
| 78 | else: |
| 79 | return akey(chr(c)), [self.MOD_META] |
| 80 | else: |
| 81 | return akey(chr(c)), set() |
| 82 | |
| 83 | def close(self): |
| 84 | termios.tcsetattr(self.io.fileno(), termios.TCSANOW, self.bka) |
| 85 | self.io.close() |
| 86 | |
| 87 | def __enter__(self): |
| 88 | return self |
| 89 | |
| 90 | def __exit__(self, *exc): |
| 91 | self.close() |
| 92 | return False |
| 93 | |
| 94 | class target(object): |
| 95 | def __init__(self, path): |
| 96 | self.path = path |
| 97 | self.sk = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) |
| 98 | self.sk.connect(path) |
| 99 | self.obuf = bytearray() |
| 100 | self.ibuf = bytearray() |
| 101 | self.fps = self.getprop("container-fps") |
| 102 | |
| 103 | def write(self, data): |
| 104 | self.obuf.extend(data) |
| 105 | |
| 106 | def flush(self): |
| 107 | while len(self.obuf) > 0: |
| 108 | ret = self.sk.send(self.obuf) |
| 109 | self.obuf[:ret] = b"" |
| 110 | |
| 111 | def recv(self, hint=1024): |
| 112 | data = self.sk.recv(hint) |
| 113 | if data == b"": |
| 114 | raise EOFError() |
| 115 | self.ibuf.extend(data) |
| 116 | |
| 117 | def readline(self): |
| 118 | p = 0 |
| 119 | while True: |
| 120 | p2 = self.ibuf.find(b'\n', p) |
| 121 | if p2 != -1: |
| 122 | ret = bytes(self.ibuf[:p2]) |
| 123 | self.ibuf[:p2 + 1] = b"" |
| 124 | return ret |
| 125 | p = len(self.ibuf) |
| 126 | self.recv() |
| 127 | |
| 128 | def send(self, data): |
| 129 | self.write(data) |
| 130 | self.flush() |
| 131 | |
| 132 | def getresp(self): |
| 133 | while True: |
| 134 | resp = json.loads(self.readline().decode("utf-8")) |
| 135 | if "event" in resp: |
| 136 | continue |
| 137 | return resp |
| 138 | |
| 139 | def runcmd(self, *cmd): |
| 140 | self.send(json.dumps({"command": cmd}).encode("utf-8") + b"\n") |
| 141 | resp = self.getresp() |
| 142 | if "error" not in resp: |
| 143 | sys.stderr.write("mpsync: strange response from %s: %r\n" % (self.path, resp)) |
| 144 | if resp["error"] != "success": |
| 145 | sys.stderr.write("mpsync: error response from %s: %r\n" % (self.path, resp)) |
| 146 | return resp |
| 147 | |
| 148 | def getprop(self, pname): |
| 149 | return self.runcmd("get_property", pname)["data"] |
| 150 | |
| 151 | def setprop(self, pname, val): |
| 152 | self.runcmd("set_property", pname, val) |
| 153 | |
| 154 | def usage(out): |
| 155 | out.write("usage: mpsync [-h] SOCKET...\n") |
| 156 | out.write("players: mpv -input-unix-socket SOCKET -pause [ARGS...] FILE\n") |
| 157 | |
| 158 | opts, args = getopt.getopt(sys.argv[1:], "h") |
| 159 | for o, a in opts: |
| 160 | if o == "-h": |
| 161 | usage(sys.stdout) |
| 162 | sys.exit(0) |
| 163 | if len(args) < 1: |
| 164 | usage(sys.stderr) |
| 165 | sys.exit(1) |
| 166 | for path in args: |
| 167 | if not os.path.exists(path): |
| 168 | sys.stderr.write("mpsync: %s: no such file or directory\n" % path) |
| 169 | sys.exit(1) |
| 170 | |
| 171 | targets = [] |
| 172 | for path in args: |
| 173 | targets.append(target(path)) |
| 174 | |
| 175 | def runcmd(*cmd): |
| 176 | for tgt in targets: |
| 177 | tgt.runcmd(*cmd) |
| 178 | |
| 179 | def simulcmd(*cmd): |
| 180 | cmd = json.dumps({"command": cmd}).encode("utf-8") + b"\n" |
| 181 | for tgt in targets: |
| 182 | tgt.send(cmd) |
| 183 | for tgt in targets: |
| 184 | resp = tgt.getresp() |
| 185 | if "error" not in resp: |
| 186 | sys.stderr.write("mpsync: strange response from %s: %r\n" % (tgt.path, resp)) |
| 187 | if resp["error"] != "success": |
| 188 | sys.stderr.write("mpsync: error response from %s: %r\n" % (tgt.path, resp)) |
| 189 | |
| 190 | def relseek(ss, offsets): |
| 191 | cur = targets[0].getprop("time-pos") |
| 192 | for tgt, off in zip(targets, offsets): |
| 193 | tgt.setprop("time-pos", cur + ss + off) |
| 194 | |
| 195 | def getoffsets(): |
| 196 | opos = targets[0].getprop("time-pos") |
| 197 | ret = [] |
| 198 | for tgt in targets: |
| 199 | ret.append(tgt.getprop("time-pos") - opos) |
| 200 | return ret |
| 201 | |
| 202 | def main(tty): |
| 203 | runcmd("set_property", "hr-seek", "yes") |
| 204 | paused = targets[0].getprop("pause") |
| 205 | mutemode = 0 |
| 206 | offsets = [0.0] * len(targets) |
| 207 | while True: |
| 208 | c, mods = tty.readkey() |
| 209 | if c == 'q': |
| 210 | return |
| 211 | elif c == 'Q': |
| 212 | runcmd("quit") |
| 213 | return |
| 214 | elif c == ' ': |
| 215 | paused = not paused |
| 216 | simulcmd("set_property", "pause", paused) |
| 217 | elif c == rawtty.K_LEFT and not mods: |
| 218 | relseek(-10, offsets) |
| 219 | elif c == rawtty.K_RIGHT and not mods: |
| 220 | relseek(10, offsets) |
| 221 | elif c == rawtty.K_UP and not mods: |
| 222 | relseek(60, offsets) |
| 223 | elif c == rawtty.K_DOWN and not mods: |
| 224 | relseek(-60, offsets) |
| 225 | elif c == rawtty.K_LEFT and mods == {rawtty.MOD_SHIFT}: |
| 226 | relseek(-2, offsets) |
| 227 | elif c == rawtty.K_RIGHT and mods == {rawtty.MOD_SHIFT}: |
| 228 | relseek(2, offsets) |
| 229 | elif c == rawtty.K_UP and mods == {rawtty.MOD_SHIFT}: |
| 230 | relseek(5, offsets) |
| 231 | elif c == rawtty.K_DOWN and mods == {rawtty.MOD_SHIFT}: |
| 232 | relseek(-5, offsets) |
| 233 | elif c == 's': |
| 234 | relseek(0, offsets) |
| 235 | elif c == 'm': |
| 236 | mutemode = (mutemode + 1) % 3 |
| 237 | if mutemode == 0: |
| 238 | for tgt in targets: |
| 239 | tgt.setprop("mute", False) |
| 240 | elif mutemode == 1: |
| 241 | for i, tgt in enumerate(targets): |
| 242 | tgt.setprop("mute", i != 0) |
| 243 | elif mutemode == 2: |
| 244 | for tgt in targets: |
| 245 | tgt.setprop("mute", True) |
| 246 | targets[0].runcmd("show_text", "Audio mode: %s" % ["All", "One", "None"][mutemode]) |
| 247 | elif c == 'S': |
| 248 | offsets = getoffsets() |
| 249 | for tgt, off in zip(targets, offsets): |
| 250 | tgt.runcmd("show_text", "Offset: %i" % round(off * tgt.fps)) |
| 251 | elif c == '.': |
| 252 | runcmd("frame_step") |
| 253 | paused = True |
| 254 | elif c == ',': |
| 255 | runcmd("frame_back_step") |
| 256 | paused = True |
| 257 | |
| 258 | with rawtty() as tty: |
| 259 | try: |
| 260 | main(tty) |
| 261 | except KeyboardInterrupt: |
| 262 | pass |