Commit | Line | Data |
---|---|---|
c0897f29 FT |
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() | |
c3fcb10f | 101 | self.fps = self.getprop("container-fps") |
c0897f29 FT |
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 | ||
28b58da2 FT |
139 | def runcmd(self, *cmd): |
140 | self.send(json.dumps({"command": cmd}).encode("utf-8") + b"\n") | |
c0897f29 FT |
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): | |
28b58da2 | 149 | return self.runcmd("get_property", pname)["data"] |
c0897f29 FT |
150 | |
151 | def setprop(self, pname, val): | |
28b58da2 | 152 | self.runcmd("set_property", pname, val) |
c0897f29 FT |
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): | |
c0897f29 | 176 | for tgt in targets: |
28b58da2 | 177 | tgt.runcmd(*cmd) |
c0897f29 FT |
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): | |
c0897f29 | 203 | runcmd("set_property", "hr-seek", "yes") |
28b58da2 FT |
204 | paused = targets[0].getprop("pause") |
205 | mutemode = 0 | |
c0897f29 FT |
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) | |
28b58da2 FT |
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]) | |
c0897f29 FT |
247 | elif c == 'S': |
248 | offsets = getoffsets() | |
28b58da2 | 249 | for tgt, off in zip(targets, offsets): |
c3fcb10f | 250 | tgt.runcmd("show_text", "Offset: %i" % round(off * tgt.fps)) |
c0897f29 FT |
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 |