mirror of https://github.com/streamlink/streamlink
302 lines
9.5 KiB
Python
302 lines
9.5 KiB
Python
import logging
|
|
import os
|
|
import shlex
|
|
import subprocess
|
|
import sys
|
|
from time import sleep
|
|
|
|
from streamlink.utils.encoding import get_filesystem_encoding, maybe_encode, maybe_decode
|
|
from .compat import is_win32, stdout
|
|
from .constants import DEFAULT_PLAYER_ARGUMENTS, SUPPORTED_PLAYERS
|
|
from .utils import ignored
|
|
|
|
if is_win32:
|
|
import msvcrt
|
|
|
|
log = logging.getLogger("streamlink.cli.output")
|
|
|
|
|
|
class Output(object):
|
|
def __init__(self):
|
|
self.opened = False
|
|
|
|
def open(self):
|
|
self._open()
|
|
self.opened = True
|
|
|
|
def close(self):
|
|
if self.opened:
|
|
self._close()
|
|
|
|
self.opened = False
|
|
|
|
def write(self, data):
|
|
if not self.opened:
|
|
raise IOError("Output is not opened")
|
|
|
|
return self._write(data)
|
|
|
|
def _open(self):
|
|
pass
|
|
|
|
def _close(self):
|
|
pass
|
|
|
|
def _write(self, data):
|
|
pass
|
|
|
|
|
|
class FileOutput(Output):
|
|
def __init__(self, filename=None, fd=None, record=None):
|
|
super(FileOutput, self).__init__()
|
|
self.filename = filename
|
|
self.fd = fd
|
|
self.record = record
|
|
|
|
def _open(self):
|
|
if self.filename:
|
|
self.fd = open(self.filename, "wb")
|
|
|
|
if self.record:
|
|
self.record.open()
|
|
|
|
if is_win32:
|
|
msvcrt.setmode(self.fd.fileno(), os.O_BINARY)
|
|
|
|
def _close(self):
|
|
if self.fd is not stdout:
|
|
self.fd.close()
|
|
if self.record:
|
|
self.record.close()
|
|
|
|
def _write(self, data):
|
|
self.fd.write(data)
|
|
if self.record:
|
|
self.record.write(data)
|
|
|
|
|
|
class PlayerOutput(Output):
|
|
PLAYER_TERMINATE_TIMEOUT = 10.0
|
|
|
|
def __init__(self, cmd, args=DEFAULT_PLAYER_ARGUMENTS, filename=None, quiet=True, kill=True,
|
|
call=False, http=None, namedpipe=None, record=None, title=None):
|
|
super(PlayerOutput, self).__init__()
|
|
self.cmd = cmd
|
|
self.args = args
|
|
self.kill = kill
|
|
self.call = call
|
|
self.quiet = quiet
|
|
|
|
self.filename = filename
|
|
self.namedpipe = namedpipe
|
|
self.http = http
|
|
self.title = title
|
|
self.player = None
|
|
self.player_name = self.supported_player(self.cmd)
|
|
self.record = record
|
|
|
|
if self.namedpipe or self.filename or self.http:
|
|
self.stdin = sys.stdin
|
|
else:
|
|
self.stdin = subprocess.PIPE
|
|
|
|
if self.quiet:
|
|
self.stdout = open(os.devnull, "w")
|
|
self.stderr = open(os.devnull, "w")
|
|
else:
|
|
self.stdout = sys.stdout
|
|
self.stderr = sys.stderr
|
|
|
|
@property
|
|
def running(self):
|
|
sleep(0.5)
|
|
return self.player.poll() is None
|
|
|
|
@classmethod
|
|
def supported_player(cls, cmd):
|
|
"""
|
|
Check if the current player supports adding a title
|
|
|
|
:param cmd: command to test
|
|
:return: name of the player|None
|
|
"""
|
|
if not is_win32:
|
|
# under a POSIX system use shlex to find the actual command
|
|
# under windows this is not an issue because executables end in .exe
|
|
cmd = shlex.split(cmd)[0]
|
|
|
|
cmd = os.path.basename(cmd.lower())
|
|
for player, possiblecmds in SUPPORTED_PLAYERS.items():
|
|
for possiblecmd in possiblecmds:
|
|
if cmd.startswith(possiblecmd):
|
|
return player
|
|
|
|
@classmethod
|
|
def _mpv_title_escape(cls, title_string):
|
|
# mpv has a "disable property-expansion" token which must be handled
|
|
# in order to accurately represent $$ in title
|
|
if r'\$>' in title_string:
|
|
processed_title = ""
|
|
double_dollars = True
|
|
i = dollars = 0
|
|
while i < len(title_string):
|
|
if double_dollars:
|
|
if title_string[i] == "\\":
|
|
if title_string[i + 1] == "$":
|
|
processed_title += "$"
|
|
dollars += 1
|
|
i += 1
|
|
if title_string[i + 1] == ">" and dollars % 2 == 1:
|
|
double_dollars = False
|
|
processed_title += ">"
|
|
i += 1
|
|
else:
|
|
processed_title += "\\"
|
|
elif title_string[i] == "$":
|
|
processed_title += "$$"
|
|
else:
|
|
dollars = 0
|
|
processed_title += title_string[i]
|
|
else:
|
|
if title_string[i:i + 2] == "\\$":
|
|
processed_title += "$"
|
|
i += 1
|
|
else:
|
|
processed_title += title_string[i]
|
|
i += 1
|
|
return processed_title
|
|
else:
|
|
# not possible for property-expansion to be disabled, happy days
|
|
return title_string.replace("$", "$$").replace(r'\$$', "$")
|
|
|
|
def _create_arguments(self):
|
|
if self.namedpipe:
|
|
filename = self.namedpipe.path
|
|
elif self.filename:
|
|
filename = self.filename
|
|
elif self.http:
|
|
filename = self.http.url
|
|
else:
|
|
filename = "-"
|
|
extra_args = []
|
|
|
|
if self.title is not None:
|
|
# vlc
|
|
if self.player_name == "vlc":
|
|
# see https://wiki.videolan.org/Documentation:Format_String/, allow escaping with \$
|
|
self.title = self.title.replace("$", "$$").replace(r'\$$', "$")
|
|
extra_args.extend(["--input-title-format", self.title])
|
|
|
|
# mpv
|
|
if self.player_name == "mpv":
|
|
# see https://mpv.io/manual/stable/#property-expansion, allow escaping with \$, respect mpv's $>
|
|
self.title = self._mpv_title_escape(self.title)
|
|
extra_args.append("--title={}".format(self.title))
|
|
|
|
# potplayer
|
|
if self.player_name == "potplayer":
|
|
if filename != "-":
|
|
# PotPlayer - About - Command Line
|
|
# You can specify titles for URLs by separating them with a backslash (\) at the end of URLs.
|
|
# eg. "http://...\title of this url"
|
|
self.title = self.title.replace('"', '')
|
|
filename = filename[:-1] + '\\' + self.title + filename[-1]
|
|
|
|
args = self.args.format(filename=filename)
|
|
cmd = self.cmd
|
|
|
|
# player command
|
|
if is_win32:
|
|
eargs = maybe_decode(subprocess.list2cmdline(extra_args))
|
|
# do not insert and extra " " when there are no extra_args
|
|
return maybe_encode(u' '.join([cmd] + ([eargs] if eargs else []) + [args]),
|
|
encoding=get_filesystem_encoding())
|
|
return shlex.split(cmd) + extra_args + shlex.split(args)
|
|
|
|
def _open(self):
|
|
try:
|
|
if self.record:
|
|
self.record.open()
|
|
if self.call and self.filename:
|
|
self._open_call()
|
|
else:
|
|
self._open_subprocess()
|
|
finally:
|
|
if self.quiet:
|
|
# Output streams no longer needed in parent process
|
|
self.stdout.close()
|
|
self.stderr.close()
|
|
|
|
def _open_call(self):
|
|
args = self._create_arguments()
|
|
if is_win32:
|
|
fargs = args
|
|
else:
|
|
fargs = subprocess.list2cmdline(args)
|
|
log.debug(u"Calling: {0}".format(fargs))
|
|
subprocess.call(args,
|
|
stdout=self.stdout,
|
|
stderr=self.stderr)
|
|
|
|
def _open_subprocess(self):
|
|
# Force bufsize=0 on all Python versions to avoid writing the
|
|
# unflushed buffer when closing a broken input pipe
|
|
args = self._create_arguments()
|
|
if is_win32:
|
|
fargs = args
|
|
else:
|
|
fargs = subprocess.list2cmdline(args)
|
|
log.debug(u"Opening subprocess: {0}".format(fargs))
|
|
self.player = subprocess.Popen(args,
|
|
stdin=self.stdin, bufsize=0,
|
|
stdout=self.stdout,
|
|
stderr=self.stderr)
|
|
# Wait 0.5 seconds to see if program exited prematurely
|
|
if not self.running:
|
|
raise OSError("Process exited prematurely")
|
|
|
|
if self.namedpipe:
|
|
self.namedpipe.open("wb")
|
|
elif self.http:
|
|
self.http.open()
|
|
|
|
def _close(self):
|
|
# Close input to the player first to signal the end of the
|
|
# stream and allow the player to terminate of its own accord
|
|
if self.namedpipe:
|
|
self.namedpipe.close()
|
|
elif self.http:
|
|
self.http.close()
|
|
elif not self.filename:
|
|
self.player.stdin.close()
|
|
|
|
if self.record:
|
|
self.record.close()
|
|
|
|
if self.kill:
|
|
with ignored(Exception):
|
|
self.player.terminate()
|
|
if not is_win32:
|
|
t, timeout = 0.0, self.PLAYER_TERMINATE_TIMEOUT
|
|
while self.player.poll() is None and t < timeout:
|
|
sleep(0.5)
|
|
t += 0.5
|
|
|
|
if not self.player.returncode:
|
|
self.player.kill()
|
|
self.player.wait()
|
|
|
|
def _write(self, data):
|
|
if self.record:
|
|
self.record.write(data)
|
|
|
|
if self.namedpipe:
|
|
self.namedpipe.write(data)
|
|
elif self.http:
|
|
self.http.write(data)
|
|
else:
|
|
self.player.stdin.write(data)
|
|
|
|
|
|
__all__ = ["PlayerOutput", "FileOutput"]
|