python-mpv-jsonipc 1.2.2__py3-none-any.whl → 1.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_mpv_jsonipc-1.2.2.dist-info → python_mpv_jsonipc-1.3.0.dist-info}/METADATA +1 -1
- python_mpv_jsonipc-1.3.0.dist-info/RECORD +6 -0
- {python_mpv_jsonipc-1.2.2.dist-info → python_mpv_jsonipc-1.3.0.dist-info}/WHEEL +1 -1
- python_mpv_jsonipc.py +157 -8
- python_mpv_jsonipc-1.2.2.dist-info/RECORD +0 -6
- {python_mpv_jsonipc-1.2.2.dist-info → python_mpv_jsonipc-1.3.0.dist-info}/licenses/LICENSE.md +0 -0
- {python_mpv_jsonipc-1.2.2.dist-info → python_mpv_jsonipc-1.3.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
python_mpv_jsonipc.py,sha256=vsGhKYoGeAl4uw652aZZsMF2weee_i8o7sJV9Ssp80g,31762
|
|
2
|
+
python_mpv_jsonipc-1.3.0.dist-info/licenses/LICENSE.md,sha256=7GQBUOA3dNG1Jk-yaPOCncNnRVURCRMev23QpZ1ew-s,10495
|
|
3
|
+
python_mpv_jsonipc-1.3.0.dist-info/METADATA,sha256=1_LpKl1fCquLmZBb0qYczUSbHynCP6FLGdt92Z5jV4U,2277
|
|
4
|
+
python_mpv_jsonipc-1.3.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
5
|
+
python_mpv_jsonipc-1.3.0.dist-info/top_level.txt,sha256=nGk4yjpVHKmIrL39XdGD2PhCeWF0oMjwey0nsNOd2TM,19
|
|
6
|
+
python_mpv_jsonipc-1.3.0.dist-info/RECORD,,
|
python_mpv_jsonipc.py
CHANGED
|
@@ -2,6 +2,7 @@ import threading
|
|
|
2
2
|
import socket
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
|
+
import re
|
|
5
6
|
import time
|
|
6
7
|
import subprocess
|
|
7
8
|
import random
|
|
@@ -35,6 +36,79 @@ class MPVError(Exception):
|
|
|
35
36
|
def __init__(self, *args, **kwargs):
|
|
36
37
|
super(MPVError, self).__init__(*args, **kwargs)
|
|
37
38
|
|
|
39
|
+
class MPVProcessError(MPVError):
|
|
40
|
+
"""MPV would not start, with whatever MPV said about why.
|
|
41
|
+
|
|
42
|
+
A subclass so that every existing ``except MPVError`` keeps working
|
|
43
|
+
unchanged -- this library is depended on by projects that will never be
|
|
44
|
+
updated, and several of them catch only that.
|
|
45
|
+
|
|
46
|
+
*returncode* is MPV's exit status, if it got far enough to have one.
|
|
47
|
+
*bad_option* is the option MPV refused, when that could be determined.
|
|
48
|
+
*log_output* is what MPV wrote about the failure.
|
|
49
|
+
*retryable* is False when starting again cannot possibly work.
|
|
50
|
+
*argv* is the command line that was tried.
|
|
51
|
+
"""
|
|
52
|
+
def __init__(self, message, returncode=None, bad_option=None,
|
|
53
|
+
log_output=None, retryable=True, argv=None):
|
|
54
|
+
super(MPVProcessError, self).__init__(message)
|
|
55
|
+
self.returncode = returncode
|
|
56
|
+
self.bad_option = bad_option
|
|
57
|
+
self.log_output = log_output
|
|
58
|
+
self.retryable = retryable
|
|
59
|
+
self.argv = argv
|
|
60
|
+
|
|
61
|
+
#: MPV's own wording when it is handed an option it does not have. Matching
|
|
62
|
+
#: it is what turns "MPV exited" into "MPV has no --input-gamepad".
|
|
63
|
+
_OPTION_ERROR = re.compile(r"Error parsing option ([^\s]+) \(option not found\)")
|
|
64
|
+
|
|
65
|
+
def _diagnose_start_failure(argv, timeout=15):
|
|
66
|
+
"""Ask MPV why it refused *argv*. Returns ``(bad_option, output)``.
|
|
67
|
+
|
|
68
|
+
Three details make this reliable, and each was measured rather than
|
|
69
|
+
reasoned about:
|
|
70
|
+
|
|
71
|
+
* **The message comes back on stdout, not stderr.** MPV writes its
|
|
72
|
+
terminal output to stdout; stderr is empty. Capturing the wrong stream
|
|
73
|
+
gets a confident, permanent "no idea why".
|
|
74
|
+
* **``--terminal=yes`` has to come first.** Options are parsed in order
|
|
75
|
+
and this library sets ``--terminal=no`` by default, so with the flags
|
|
76
|
+
left as they are MPV has already silenced itself by the time it reaches
|
|
77
|
+
the option it objects to. The library's own terminal flags are dropped
|
|
78
|
+
rather than fought with.
|
|
79
|
+
* **``--version`` goes last**, so a configuration that turns out to be
|
|
80
|
+
fine parses everything and then exits (~15ms) instead of starting up
|
|
81
|
+
and having to be killed. It exits before binding the IPC socket, so
|
|
82
|
+
this cannot disturb the real MPV that is about to start.
|
|
83
|
+
|
|
84
|
+
An earlier version of this read ``--log-file`` instead, on the theory
|
|
85
|
+
that Windows has no stderr to capture. That log is written
|
|
86
|
+
**unreliably**: MPV does not flush it before exiting, so it arrives
|
|
87
|
+
truncated at a random point -- measured at 6/8 and 7/8 runs containing
|
|
88
|
+
the error, cut off anywhere from 102 bytes in. A pipe has no such
|
|
89
|
+
problem, and is byte-identical across runs.
|
|
90
|
+
|
|
91
|
+
Every failure here is answered with ``(None, ...)``, which puts the
|
|
92
|
+
caller back on the path it took before this existed. Being unable to
|
|
93
|
+
explain a failed start must never turn into a second failure.
|
|
94
|
+
"""
|
|
95
|
+
terminal_flags = ("--terminal=", "--input-terminal=")
|
|
96
|
+
argv = list(argv)
|
|
97
|
+
probe = ([argv[0], "--terminal=yes"]
|
|
98
|
+
+ [a for a in argv[1:] if not a.startswith(terminal_flags)]
|
|
99
|
+
+ ["--version"])
|
|
100
|
+
try:
|
|
101
|
+
completed = subprocess.run(probe,
|
|
102
|
+
stdout=subprocess.PIPE,
|
|
103
|
+
stderr=subprocess.STDOUT,
|
|
104
|
+
timeout=timeout)
|
|
105
|
+
except Exception:
|
|
106
|
+
log.debug("Could not run MPV to diagnose the failure.", exc_info=1)
|
|
107
|
+
return None, None
|
|
108
|
+
output = completed.stdout.decode("utf-8", "replace")
|
|
109
|
+
match = _OPTION_ERROR.search(output)
|
|
110
|
+
return (match.group(1) if match else None), output
|
|
111
|
+
|
|
38
112
|
class WindowsSocket(threading.Thread):
|
|
39
113
|
"""
|
|
40
114
|
Wraps a Windows named pipe in a high-level interface. (Internal)
|
|
@@ -198,6 +272,26 @@ class UnixSocket(threading.Thread):
|
|
|
198
272
|
if self.quit_callback:
|
|
199
273
|
self.quit_callback()
|
|
200
274
|
|
|
275
|
+
def _ipc_endpoint_ready(ipc_socket):
|
|
276
|
+
"""Return True once MPV's IPC endpoint is available.
|
|
277
|
+
|
|
278
|
+
On POSIX the endpoint is a filesystem socket, so ``os.path.exists`` works. On
|
|
279
|
+
Windows it is a *named pipe*, for which ``os.path.exists`` is always False — so we
|
|
280
|
+
probe it with ``WaitNamedPipe`` instead. Using ``os.path.exists`` on Windows made
|
|
281
|
+
the startup poll never detect the pipe, raising a spurious "MPV start timed out".
|
|
282
|
+
"""
|
|
283
|
+
if os.name != 'nt':
|
|
284
|
+
return os.path.exists(ipc_socket)
|
|
285
|
+
try:
|
|
286
|
+
_winapi.WaitNamedPipe(ipc_socket, 0)
|
|
287
|
+
return True
|
|
288
|
+
except FileNotFoundError:
|
|
289
|
+
return False
|
|
290
|
+
except OSError:
|
|
291
|
+
# Pipe exists but all instances are momentarily busy — it is present.
|
|
292
|
+
return True
|
|
293
|
+
|
|
294
|
+
|
|
201
295
|
class MPVProcess:
|
|
202
296
|
"""
|
|
203
297
|
Manages an MPV process, ensuring the socket or pipe is available. (Internal)
|
|
@@ -243,12 +337,13 @@ class MPVProcess:
|
|
|
243
337
|
|
|
244
338
|
args.extend("--{0}={1}".format(v[0].replace("_", "-"), self._mpv_fmt(v[1]))
|
|
245
339
|
for v in arg_pairs)
|
|
340
|
+
self.argv = args
|
|
246
341
|
self.process = subprocess.Popen(args)
|
|
247
342
|
ipc_exists = False
|
|
248
343
|
for _ in range(100): # Give MPV 10 seconds to start.
|
|
249
344
|
time.sleep(0.1)
|
|
250
345
|
self.process.poll()
|
|
251
|
-
if
|
|
346
|
+
if _ipc_endpoint_ready(ipc_socket):
|
|
252
347
|
ipc_exists = True
|
|
253
348
|
log.debug("Found MPV socket.")
|
|
254
349
|
break
|
|
@@ -257,20 +352,29 @@ class MPVProcess:
|
|
|
257
352
|
break
|
|
258
353
|
else:
|
|
259
354
|
self.process.terminate()
|
|
260
|
-
raise
|
|
355
|
+
raise MPVProcessError("MPV start timed out.", argv=args)
|
|
261
356
|
|
|
262
357
|
if not ipc_exists or self.process.returncode is not None:
|
|
263
358
|
self.process.terminate()
|
|
264
|
-
|
|
359
|
+
# Message unchanged: it has been this string for years and is not
|
|
360
|
+
# ours to break. What is new is the argv riding along, which is
|
|
361
|
+
# what lets the caller ask MPV why.
|
|
362
|
+
raise MPVProcessError("MPV not started.",
|
|
363
|
+
returncode=self.process.returncode,
|
|
364
|
+
argv=args)
|
|
265
365
|
|
|
266
366
|
def _set_default(self, prop_dict, key, value):
|
|
267
367
|
if key not in prop_dict:
|
|
268
368
|
prop_dict[key] = value
|
|
269
369
|
|
|
270
370
|
def _mpv_fmt(self, data):
|
|
271
|
-
|
|
371
|
+
# Use identity comparison so numeric arguments are preserved. In Python
|
|
372
|
+
# ``1 == True`` and ``0 == False``, so ``==`` would convert numeric
|
|
373
|
+
# options such as ``video-scale-x=1`` into ``--video-scale-x=yes``,
|
|
374
|
+
# which MPV rejects.
|
|
375
|
+
if data is True:
|
|
272
376
|
return "yes"
|
|
273
|
-
elif data
|
|
377
|
+
elif data is False:
|
|
274
378
|
return "no"
|
|
275
379
|
else:
|
|
276
380
|
return data
|
|
@@ -405,7 +509,8 @@ class MPV:
|
|
|
405
509
|
list is used. Not all commands may actually work when this fallback is used.
|
|
406
510
|
"""
|
|
407
511
|
def __init__(self, start_mpv=True, ipc_socket=None, mpv_location=None,
|
|
408
|
-
log_handler=None, loglevel=None, quit_callback=None, start_retries=5, start_retry_delay_ms=1000,
|
|
512
|
+
log_handler=None, loglevel=None, quit_callback=None, start_retries=5, start_retry_delay_ms=1000,
|
|
513
|
+
diagnose_start_failures=True, **kwargs):
|
|
409
514
|
"""
|
|
410
515
|
Create the interface to MPV and process instance.
|
|
411
516
|
|
|
@@ -415,6 +520,10 @@ class MPV:
|
|
|
415
520
|
*log_handler(level, prefix, text)* is an optional handler for log events. (Default: Disabled)
|
|
416
521
|
*loglevel* is the level for log messages. Levels are fatal, error, warn, info, v, debug, trace. (Default: Disabled)
|
|
417
522
|
*quit_callback* is called when the socket connection to MPV dies.
|
|
523
|
+
*diagnose_start_failures* re-runs MPV once, on the first failed start, to find out
|
|
524
|
+
why it refused to start. If MPV names an option it does not have, MPVProcessError is
|
|
525
|
+
raised immediately with *bad_option* set instead of spending every retry on something
|
|
526
|
+
that cannot succeed. Set it to False for the pre-1.3 behaviour. (Default: True)
|
|
418
527
|
|
|
419
528
|
All other arguments are forwarded to MPV as command-line arguments if *start_mpv* is used.
|
|
420
529
|
"""
|
|
@@ -436,16 +545,56 @@ class MPV:
|
|
|
436
545
|
|
|
437
546
|
if start_mpv:
|
|
438
547
|
# Attempt to start MPV multiple times.
|
|
548
|
+
last_error = None
|
|
549
|
+
diagnosed = False
|
|
550
|
+
log_output = None
|
|
439
551
|
for i in range(start_retries):
|
|
440
552
|
try:
|
|
441
553
|
self.mpv_process = MPVProcess(ipc_socket, mpv_location, **kwargs)
|
|
442
554
|
break
|
|
443
|
-
except MPVError:
|
|
555
|
+
except MPVError as error:
|
|
556
|
+
last_error = error
|
|
444
557
|
log.warning("MPV start failed.", exc_info=1)
|
|
558
|
+
|
|
559
|
+
# Ask MPV why, once, on the first failure. An option MPV
|
|
560
|
+
# does not have cannot start on the fifth attempt either,
|
|
561
|
+
# and spending the whole retry budget on it produces the
|
|
562
|
+
# least useful error this library has ever raised: a
|
|
563
|
+
# timeout that names nothing. The probe costs one ~20ms
|
|
564
|
+
# MPV run and only ever happens on a start that has
|
|
565
|
+
# already failed.
|
|
566
|
+
argv = getattr(error, "argv", None)
|
|
567
|
+
if diagnose_start_failures and argv and not diagnosed:
|
|
568
|
+
diagnosed = True
|
|
569
|
+
bad_option, diagnosis = _diagnose_start_failure(argv)
|
|
570
|
+
# Keep what MPV said even when it does not name an
|
|
571
|
+
# option. Refusing to start over a missing DLL or an
|
|
572
|
+
# unusable VO is the same predicament from the user's
|
|
573
|
+
# side -- something they changed, and nothing to go
|
|
574
|
+
# on -- and on Windows there is no console for them to
|
|
575
|
+
# have seen it in. It rides out on .log_output below.
|
|
576
|
+
log_output = diagnosis
|
|
577
|
+
if bad_option is not None:
|
|
578
|
+
raise MPVProcessError(
|
|
579
|
+
"MPV rejected the option --{0}.".format(
|
|
580
|
+
bad_option),
|
|
581
|
+
returncode=getattr(error, "returncode", None),
|
|
582
|
+
bad_option=bad_option,
|
|
583
|
+
log_output=log_output,
|
|
584
|
+
retryable=False,
|
|
585
|
+
argv=argv)
|
|
586
|
+
|
|
445
587
|
time.sleep(start_retry_delay_ms / 1000)
|
|
446
588
|
continue
|
|
447
589
|
else:
|
|
448
|
-
|
|
590
|
+
# The message is unchanged on purpose. Downstream projects
|
|
591
|
+
# that will never be updated may be matching on it, and this
|
|
592
|
+
# is still the same situation it has always described.
|
|
593
|
+
raise MPVProcessError(
|
|
594
|
+
"MPV process retry limit reached.",
|
|
595
|
+
returncode=getattr(last_error, "returncode", None),
|
|
596
|
+
log_output=log_output,
|
|
597
|
+
argv=getattr(last_error, "argv", None))
|
|
449
598
|
|
|
450
599
|
self.mpv_inter = MPVInter(ipc_socket, self._callback, self._quit_callback)
|
|
451
600
|
self.properties = set(x.replace("-", "_") for x in self.command("get_property", "property-list"))
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
python_mpv_jsonipc.py,sha256=McfALK21Mp48Ekpg8wszSnHlSExtlrpsrw90i9syvng,24041
|
|
2
|
-
python_mpv_jsonipc-1.2.2.dist-info/licenses/LICENSE.md,sha256=7GQBUOA3dNG1Jk-yaPOCncNnRVURCRMev23QpZ1ew-s,10495
|
|
3
|
-
python_mpv_jsonipc-1.2.2.dist-info/METADATA,sha256=AjVHMMb5-bKwYuf89xM3J9jLRPX17v-iXkMvuft8AF0,2277
|
|
4
|
-
python_mpv_jsonipc-1.2.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
5
|
-
python_mpv_jsonipc-1.2.2.dist-info/top_level.txt,sha256=nGk4yjpVHKmIrL39XdGD2PhCeWF0oMjwey0nsNOd2TM,19
|
|
6
|
-
python_mpv_jsonipc-1.2.2.dist-info/RECORD,,
|
{python_mpv_jsonipc-1.2.2.dist-info → python_mpv_jsonipc-1.3.0.dist-info}/licenses/LICENSE.md
RENAMED
|
File without changes
|
|
File without changes
|