Browse Source

stream: remove RTMP and RTMPDump dependency

- drop RTMP stream implementation
- drop RTMP plugin
- drop RTMPDump dependency
- remove stream.streamprocess and utils.{rtmp,swf}
- remove "rtmp" from default stream types list
- remove "rtmp" from player-passthrough options list
- remove all `--rtmp*` CLI args and `rtmp-*` session options
- remove all `--subprocess-*` CLI args and `subprocess` session options,
  as these were used only by StreamProcess which is no longer needed
- update tests
- update docs and CLI argument help texts
pull/4171/head
bastimeyer 2 months ago
committed by back-to
parent
commit
a7063e1d70
  1. 3
      docs/api.rst
  2. 2
      docs/api_guide.rst
  3. 41
      docs/cli.rst
  4. 6
      docs/install.rst
  5. 5
      docs/players.rst
  6. 18
      script/makeinstaller-assets.json
  7. 13
      script/makeinstaller.sh
  8. 2
      src/streamlink/plugin/plugin.py
  9. 29
      src/streamlink/plugins/rtmp.py
  10. 22
      src/streamlink/session.py
  11. 2
      src/streamlink/stream/__init__.py
  12. 156
      src/streamlink/stream/rtmpdump.py
  13. 178
      src/streamlink/stream/streamprocess.py
  14. 4
      src/streamlink/utils/__init__.py
  15. 36
      src/streamlink/utils/rtmp.py
  16. 8
      src/streamlink/utils/swf.py
  17. 59
      src/streamlink_cli/argparser.py
  18. 2
      src/streamlink_cli/constants.py
  19. 31
      src/streamlink_cli/main.py
  20. 8
      tests/plugin/testplugin.py
  21. 26
      tests/plugins/test_stream.py
  22. 70
      tests/stream/test_stream_streamprocess.py
  23. 11
      tests/stream/test_stream_to_url.py
  24. 13
      tests/test_cmdline.py
  25. 2
      tests/test_plugins_meta.py
  26. 10
      tests/test_session.py
  27. 18
      tests/test_stream_json.py
  28. 22
      tests/test_streamlink_api.py
  29. 12
      tests/utils/test_rtmp.py
  30. 13
      tests/utils/test_swf.py
  31. 7
      win32/config

3
docs/api.rst

@ -56,9 +56,6 @@ different properties are available depending on stream type.
.. autoclass:: HTTPStream
:members:
.. autoclass:: RTMPStream
:members:
.. autoclass:: DASHStream
:members:

2
docs/api_guide.rst

@ -93,7 +93,7 @@ or set options like this:
.. code-block:: python
>>> session.set_option("rtmp-rtmpdump", "/path/to/rtmpdump")
>>> session.set_option("stream-timeout", 30)
See :func:`Streamlink.set_option` to see which options are available.

41
docs/cli.rst

@ -306,45 +306,41 @@ Streamlink supports most of them. It's possible to tell Streamlink
to access a streaming protocol directly instead of relying on a plugin
to extract the streams from a URL for you.
A protocol can be accessed directly by specifying it in the URL format::
protocol://path [key=value]
Accessing a stream that requires extra parameters to be passed along
(e.g. RTMP):
A streaming protocol can be accessed directly by specifying it in the ``protocol://URL`` format
with an optional list of parameters, like so:
.. code-block:: console
$ streamlink "rtmp://streaming.server.net/playpath live=1 swfVfy=http://server.net/flashplayer.swf"
$ streamlink "protocol://https://streamingserver/path key1=value1 key2=value2"
When passing parameters to the built-in stream plugins, the values will either
be treated as plain strings, as is the case in the example above for ``swfVry``,
or they will be interpreted as Python literals. For example, you can pass a
Python dict or Python list as one of the parameters.
Depending on the input URL, the explicit protocol scheme may be omitted.
The following example shows HLS streams (``.m3u8``) and DASH streams (``.mdp``):
.. code-block:: console
$ streamlink "rtmp://streaming.server.net/playpath conn=['B:1', 'S:authMe', 'O:1', 'NN:code:1.23', 'NS:flag:ok', 'O:0']"
$ streamlink "hls://streaming.server.net/playpath params={'token': 'magicToken'}"
In the examples above, ``conn`` will be passed as a Python list:
$ streamlink "https://streamingserver/playlist.m3u8"
$ streamlink "https://streamingserver/manifest.mpd"
.. code-block:: python
When passing parameters to the built-in streaming protocols, the values will either be treated as plain strings
or they will be interpreted as Python literals:
['B:1', 'S:authMe', 'O:1', 'NN:code:1.23', 'NS:flag:ok', 'O:0']
.. code-block:: console
and ``params`` will be passed as a Python dict:
$ streamlink "httpstream://https://streamingserver/path params={'abc':123} json=['foo','bar','baz']"
.. code-block:: python
{'token': 'magicToken'}
params={"key": 123}
json=["foo", "bar", "baz"]
The parameters from the example above are used to make an HTTP ``GET`` request with ``abc=123`` added
to the query string and ``["foo", "bar", "baz"]`` used as the content of the HTTP request's body (the serialized JSON data).
Most streaming protocols only require you to pass a simple URL.
This is an HLS stream:
Some parameters allow you to configure the behavior of the streaming protocol implementation directly:
.. code-block:: console
$ streamlink hls://https://streaming.server.net/playlist.m3u8
$ streamlink "hls://https://streamingserver/path start_offset=123 duration=321 force_restart=True"
Supported streaming protocols
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -354,7 +350,6 @@ Name Prefix
============================== =================================================
Apple HTTP Live Streaming hls:// [1]_
MPEG-DASH [2]_ dash://
Real Time Messaging Protocol rtmp:// rtmpe:// rtmps:// rtmpt:// rtmpte://
Progressive HTTP, HTTPS, etc httpstream:// [1]_
============================== =================================================

6
docs/install.rst

@ -295,7 +295,6 @@ Name Notes
**Optional**
--------------------------------------------------------------------------------
`RTMPDump`_ Required to play RTMP streams.
`ffmpeg`_ Required to play streams that are made up of separate
audio and video streams, eg. YouTube 1080p+
==================================== ===========================================
@ -314,7 +313,6 @@ With these two environment variables it is possible to use `pycrypto`_ instead o
.. _Python: https://www.python.org/
.. _python-setuptools: https://pypi.org/project/setuptools/
.. _python-requests: https://docs.python-requests.org/en/master/
.. _RTMPDump: https://rtmpdump.mplayerhq.hu/
.. _pycountry: https://pypi.org/project/pycountry/
.. _pycrypto: https://www.dlitz.net/software/pycrypto/
.. _pycryptodome: https://pycryptodome.readthedocs.io/en/latest/
@ -362,9 +360,7 @@ Release Notes
These installers contain:
- A compiled version of Streamlink that **does not require an existing Python
installation**
- `RTMPDump`_ for viewing RTMP streams
- A compiled version of Streamlink that **does not require an existing Python installation**
- `ffmpeg`_ for muxing streams
and perform the following tasks:

5
docs/players.rst

@ -89,11 +89,6 @@ MPlayer tries to play Twitch streams at the wrong FPS
This is a bug in MPlayer, using the MPlayer fork `mpv`_ instead
is recommended.
VLC hangs when buffering and no playback starts
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Some versions of 64-bit VLC seem to be unable to read the stream created by
rtmpdump. Using the 32-bit version of VLC might help.
Youtube Live does not work with VLC
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
VLC versions below 3 cannot play Youtube Live streams. Please update your

18
script/makeinstaller-assets.json

@ -20,23 +20,5 @@
"to": "BUILDINFO.txt"
}
]
},
{
"url": "https://github.com/streamlink/streamlink-assets/releases/download/2020.4.21/rtmpdump-2.3-windows.zip",
"sha256": "6948aa372f04952d5675917c6ca2c7c0bf6b109de21a4323adc93247fff0189f",
"filename": "rtmpdump-2.3-windows.zip",
"type": "zip",
"sourcedir": "rtmpdump-2.3",
"targetdir": "rtmpdump",
"files": [
{
"from": "rtmpdump.exe",
"to": "rtmpdump.exe"
},
{
"from": "COPYING",
"to": "LICENSE.txt"
}
]
}
]

13
script/makeinstaller.sh

@ -180,15 +180,6 @@ cat > "${build_dir}/installer_tmpl.nsi" <<EOF
[% block sections %]
[[ super() ]]
SubSection /e "Bundled tools" bundled
Section "rtmpdump" rtmpdump
SetOutPath "\$INSTDIR\rtmpdump"
File /r "${files_dir}\rtmpdump\*.*"
SetShellVarContext current
\${ConfigWrite} "\$APPDATA\streamlink\config" "rtmpdump=" "\$INSTDIR\rtmpdump\rtmpdump.exe" \$R0
SetShellVarContext all
SetOutPath -
SectionEnd
Section "FFMPEG" ffmpeg
SetOutPath "\$INSTDIR\ffmpeg"
File /r "${files_dir}\ffmpeg\*.*"
@ -224,7 +215,6 @@ SubSectionEnd
[% block uninstall_files %]
[[ super() ]]
RMDir /r "\$INSTDIR\rtmpdump"
RMDir /r "\$INSTDIR\ffmpeg"
[% endblock %]
@ -254,9 +244,6 @@ StrCmp \$0 \${sec_app} "" +2
StrCmp \$0 \${bundled} "" +2
SendMessage \$R0 \${WM_SETTEXT} 0 "STR:Extra tools used to play some streams"
StrCmp \$0 \${rtmpdump} "" +2
SendMessage \$R0 \${WM_SETTEXT} 0 "STR:rtmpdump is used to play RTMP streams"
StrCmp \$0 \${ffmpeg} "" +2
SendMessage \$R0 \${WM_SETTEXT} 0 "STR:FFMPEG is used to mux separate video and audio streams, for example high quality YouTube videos or DASH streams"

2
src/streamlink/plugin/plugin.py

@ -267,7 +267,7 @@ class Plugin:
@classmethod
def default_stream_types(cls, streams):
stream_types = ["rtmp", "hls", "http"]
stream_types = ["hls", "http"]
for name, stream in iterate_streams(streams):
stream_type = type(stream).shortname()

29
src/streamlink/plugins/rtmp.py

@ -1,29 +0,0 @@
import logging
import re
from streamlink.plugin import Plugin, pluginmatcher
from streamlink.plugin.plugin import parse_params
from streamlink.stream.rtmpdump import RTMPStream
log = logging.getLogger(__name__)
@pluginmatcher(re.compile(
r"(?P<url>rtmp(?:e|s|t|te)?://\S+)(?:\s(?P<params>.+))?"
))
class RTMPPlugin(Plugin):
def _get_streams(self):
data = self.match.groupdict()
params = parse_params(data.get("params"))
params["rtmp"] = data.get("url")
for boolkey in ("live", "realtime", "quiet", "verbose", "debug"):
if boolkey in params:
params[boolkey] = bool(params[boolkey])
log.debug(f"params={params}")
return {"live": RTMPStream(self.session, params)}
__plugin__ = RTMPPlugin

22
src/streamlink/session.py

@ -10,7 +10,6 @@ import requests.packages.urllib3.util.connection as urllib3_connection
from requests.packages.urllib3.util.connection import allowed_gai_family
from streamlink import __version__, plugins
from streamlink.compat import is_win32
from streamlink.exceptions import NoPluginError, PluginError
from streamlink.logger import StreamlinkLogger
from streamlink.options import Options
@ -47,14 +46,10 @@ class Streamlink:
"hls-start-offset": 0,
"hls-duration": None,
"ringbuffer-size": 1024 * 1024 * 16, # 16 MB
"rtmp-rtmpdump": is_win32 and "rtmpdump.exe" or "rtmpdump",
"rtmp-proxy": None,
"stream-segment-attempts": 3,
"stream-segment-threads": 1,
"stream-segment-timeout": 10.0,
"stream-timeout": 60.0,
"subprocess-errorlog": False,
"subprocess-errorlog-path": None,
"ffmpeg-ffmpeg": None,
"ffmpeg-fout": None,
"ffmpeg-video-transcode": None,
@ -133,23 +128,10 @@ class Streamlink:
requests except the ones covered by
other options, default: ``20.0``
subprocess-errorlog (bool) Log errors from subprocesses to
a file located in the temp directory
subprocess-errorlog-path (str) Log errors from subprocesses to
a specific file
ringbuffer-size (int) The size of the internal ring
buffer used by most stream types,
default: ``16777216`` (16MB)
rtmp-proxy (str) Specify a proxy (SOCKS) that RTMP
streams will use
rtmp-rtmpdump (str) Specify the location of the
rtmpdump executable used by RTMP streams,
e.g. ``/usr/local/bin/rtmpdump``
ffmpeg-ffmpeg (str) Specify the location of the
ffmpeg executable use by Muxing streams
e.g. ``/usr/local/bin/ffmpeg``
@ -275,8 +257,8 @@ class Streamlink:
# deprecated: {dash,hls}-segment-timeout
elif key in ("dash-segment-timeout", "hls-segment-timeout"):
self.options.set("stream-segment-timeout", float(value))
# deprecated: {hls,rtmp,dash,http-stream}-timeout
elif key in ("dash-timeout", "hls-timeout", "http-stream-timeout", "rtmp-timeout"):
# deprecated: {hls,dash,http-stream}-timeout
elif key in ("dash-timeout", "hls-timeout", "http-stream-timeout"):
self.options.set("stream-timeout", float(value))
else:

2
src/streamlink/stream/__init__.py

@ -1,7 +1,5 @@
from streamlink.stream.dash import DASHStream
from streamlink.stream.hls import HLSStream
from streamlink.stream.http import HTTPStream
from streamlink.stream.rtmpdump import RTMPStream
from streamlink.stream.stream import Stream, StreamIO
from streamlink.stream.streamprocess import StreamProcess
from streamlink.stream.wrappers import StreamIOIterWrapper, StreamIOThreadWrapper, StreamIOWrapper

156
src/streamlink/stream/rtmpdump.py

@ -1,156 +0,0 @@
import logging
import re
import subprocess
from operator import itemgetter
from shutil import which
from streamlink import logger
from streamlink.exceptions import StreamError
from streamlink.stream.streamprocess import StreamProcess
from streamlink.utils.rtmp import escape_librtmp, rtmpparse
log = logging.getLogger(__name__)
class RTMPStream(StreamProcess):
"""RTMP stream using rtmpdump.
*Attributes:*
- :attr:`params` A :class:`dict` containing parameters passed to rtmpdump
"""
__shortname__ = "rtmp"
logging_parameters = ("quiet", "verbose", "debug", "q", "V", "z")
def __init__(self, session, params, redirect=False, **kwargs):
StreamProcess.__init__(self, session, params=params, **kwargs)
self.timeout = self.session.options.get("stream-timeout")
self.redirect = redirect
# set rtmpdump logging level
if self.session.options.get("subprocess-errorlog-path") or \
self.session.options.get("subprocess-errorlog"):
# disable any current logging level
for p in self.logging_parameters:
self.parameters.pop(p, None)
if logger.root.level == logging.DEBUG:
self.parameters["debug"] = True
else:
self.parameters["verbose"] = True
@property
def cmd(self):
return self.session.options.get("rtmp-rtmpdump")
def __repr__(self):
return "<RTMPStream({0!r}, redirect={1!r}>".format(self.parameters,
self.redirect)
def __json__(self):
return dict(type=RTMPStream.shortname(),
args=self.arguments,
params=self.parameters)
def open(self):
if self.session.options.get("rtmp-proxy"):
if not self._supports_param("socks"):
raise StreamError("Installed rtmpdump does not support --socks argument")
self.parameters["socks"] = self.session.options.get("rtmp-proxy")
if "jtv" in self.parameters and not self._supports_param("jtv"):
raise StreamError("Installed rtmpdump does not support --jtv argument")
if "weeb" in self.parameters and not self._supports_param("weeb"):
raise StreamError("Installed rtmpdump does not support --weeb argument")
if self.redirect:
self._check_redirect()
self.parameters["flv"] = "-"
return StreamProcess.open(self)
def _check_redirect(self, timeout=20):
params = self.parameters.copy()
# remove any existing logging parameters
for p in self.logging_parameters:
params.pop(p, None)
# and explicitly set verbose
params["verbose"] = True
log.debug("Attempting to find tcURL redirect")
process = self.spawn(params, timeout=timeout, stderr=subprocess.PIPE)
self._update_redirect(process.stderr.read())
def _update_redirect(self, stderr):
tcurl, redirect = None, None
stderr = str(stderr, "utf8")
m = re.search(r"DEBUG: Property: <Name:\s+redirect,\s+STRING:\s+(\w+://.+?)>", stderr)
if m:
redirect = m.group(1)
if redirect:
log.debug(f"Found redirect tcUrl: {redirect}")
if "rtmp" in self.parameters:
tcurl, playpath = rtmpparse(self.parameters["rtmp"])
if playpath:
rtmp = "{redirect}/{playpath}".format(redirect=redirect, playpath=playpath)
else:
rtmp = redirect
self.parameters["rtmp"] = rtmp
if "tcUrl" in self.parameters:
self.parameters["tcUrl"] = redirect
def _supports_param(self, param, timeout=5.0):
try:
rtmpdump = self.spawn(dict(help=True), timeout=timeout, stderr=subprocess.PIPE)
except StreamError as err:
raise StreamError("Error while checking rtmpdump compatibility: {0}".format(err.message))
for line in rtmpdump.stderr.readlines():
m = re.match(r"^--(\w+)", str(line, "ascii"))
if not m:
continue
if m.group(1) == param:
return True
return False
@classmethod
def is_usable(cls, session):
cmd = session.options.get("rtmp-rtmpdump")
return which(cmd) is not None
def to_url(self):
stream_params = dict(self.params)
params = [stream_params.pop("rtmp", "")]
if "swfVfy" in self.params:
stream_params["swfUrl"] = self.params["swfVfy"]
stream_params["swfVfy"] = True
if "swfhash" in self.params:
stream_params["swfVfy"] = True
stream_params.pop("swfhash", None)
stream_params.pop("swfsize", None)
# sort the keys for stability of output
for key, value in sorted(stream_params.items(), key=itemgetter(0)):
if isinstance(value, list):
for svalue in value:
params.append("{0}={1}".format(key, escape_librtmp(svalue)))
else:
params.append("{0}={1}".format(key, escape_librtmp(value)))
return " ".join(params)

178
src/streamlink/stream/streamprocess.py

@ -1,178 +0,0 @@
import logging
import os.path
import subprocess
import tempfile
import time
from operator import itemgetter
from shutil import which
from streamlink.compat import devnull
from streamlink.exceptions import StreamError
from streamlink.stream.stream import Stream
from streamlink.stream.wrappers import StreamIOThreadWrapper
log = logging.getLogger(__name__)
class StreamProcessIO(StreamIOThreadWrapper):
def __init__(self, session, process, fd, **kwargs):
self.process = process
super().__init__(session, fd, **kwargs)
def close(self):
try:
self.process.kill()
except Exception:
pass
finally:
super().close()
class StreamProcess(Stream):
def __init__(self, session, params=None, args=None, timeout=60.0):
"""
:param session: Streamlink session
:param params: keyword arguments mapped to process argument
:param args: positional arguments
:param timeout: timeout for process
"""
super().__init__(session)
self.parameters = params or {}
self.arguments = args or []
self.timeout = timeout
self.errorlog = self.session.options.get("subprocess-errorlog")
self.errorlog_path = self.session.options.get("subprocess-errorlog-path")
if self.errorlog_path:
self.stderr = open(self.errorlog_path, "w")
elif self.errorlog:
self.stderr = tempfile.NamedTemporaryFile(prefix="streamlink", suffix=".err", delete=False)
else:
self.stderr = devnull()
@property
def cmd(self):
raise NotImplementedError
@property
def params(self):
return self.parameters
@classmethod
def is_usable(cls, session):
raise NotImplementedError
def open(self):
if self.is_usable(self.session):
process = self.spawn(self.parameters, self.arguments)
# Wait 0.5 seconds to see if program exited prematurely
time.sleep(0.5)
if not process.poll() is None:
if hasattr(self.stderr, "name"):
raise StreamError(("Error while executing subprocess, "
"error output logged to: {0}").format(self.stderr.name))
else:
raise StreamError("Error while executing subprocess")
return StreamProcessIO(self.session, process, process.stdout, timeout=self.timeout)
else:
raise StreamError(
"{0} is not installed or not supported on your system".format(os.path.basename(self.cmd))
)
@classmethod
def bake(cls, cmd, parameters=None, arguments=None, short_option_prefix="-", long_option_prefix="--"):
cmdline = [cmd]
parameters = parameters or {}
arguments = arguments or []
def to_option(key):
if len(key) == 1: # short argument
return "{0}{1}".format(short_option_prefix, key)
else: # long argument
return "{0}{1}".format(long_option_prefix, key.replace("_", "-"))
# sorted for stability
for k, v in sorted(parameters.items(), key=itemgetter(0)):
if not isinstance(v, list): # long argument
cmdline.append(to_option(k))
if v is not True:
cmdline.append("{0}".format(v))
else: # duplicate the argument if given a list of values
for sv in v:
cmdline.append(to_option(k))
cmdline.append("{0}".format(sv))
# positional arguments last
cmdline.extend(arguments)
return cmdline
def spawn(self, parameters=None, arguments=None, stderr=None, timeout=None,
short_option_prefix="-", long_option_prefix="--"):
"""
Spawn the process defined in `cmd`
parameters is converted to options the short and long option prefixes
if a list is given as the value, the parameter is repeated with each
value
If timeout is set the spawn will block until the process returns or
the timeout expires.
:param parameters: optional parameters
:param arguments: positional arguments
:param stderr: where to redirect stderr to
:param timeout: timeout for short lived process
:param long_option_prefix: option prefix, default -
:param short_option_prefix: long option prefix, default --
:return: spawned process
"""
stderr = stderr or self.stderr
cmd = self.bake(self._check_cmd(), parameters, arguments, short_option_prefix, long_option_prefix)
log.debug(f"Spawning command: {subprocess.list2cmdline(cmd)}")
try:
process = subprocess.Popen(cmd, stderr=stderr, stdout=subprocess.PIPE)
except OSError as err:
raise StreamError("Failed to start process: {0} ({1})".format(self._check_cmd(), str(err)))
if timeout:
elapsed = 0
while elapsed < timeout and not process.poll():
time.sleep(0.25)
elapsed += 0.25
# kill after the timeout has expired and the process still hasn't ended
if not process.poll():
try:
log.debug("Process timeout expired ({0}s), killing process".format(timeout))
process.kill()
except Exception:
pass
process.wait()
return process
def cmdline(self):
return subprocess.list2cmdline(self.bake(self._check_cmd(), self.parameters, self.arguments))
def _check_cmd(self):
if not self.cmd:
raise StreamError("`cmd' attribute not set")
cmd = which(self.cmd)
if not cmd:
raise StreamError("Unable to find `{0}' command".format(self.cmd))
return cmd
__all__ = ["StreamProcess"]

4
src/streamlink/utils/__init__.py

@ -3,8 +3,6 @@ from streamlink.utils.data import search_dict
from streamlink.utils.module import load_module
from streamlink.utils.named_pipe import NamedPipe
from streamlink.utils.parse import parse_html, parse_json, parse_qsd, parse_xml
from streamlink.utils.rtmp import escape_librtmp, rtmpparse
from streamlink.utils.swf import swfdecompress
from streamlink.utils.url import absolute_url, prepend_www, update_qsd, update_scheme, url_concat, url_equal
@ -14,7 +12,5 @@ __all__ = [
"load_module",
"NamedPipe",
"parse_html", "parse_json", "parse_qsd", "parse_xml",
"escape_librtmp", "rtmpparse",
"swfdecompress",
"absolute_url", "prepend_www", "update_qsd", "update_scheme", "url_concat", "url_equal",
]

36
src/streamlink/utils/rtmp.py

@ -1,36 +0,0 @@
from urllib.parse import urlparse
def escape_librtmp(value): # pragma: no cover
if isinstance(value, bool):
value = "1" if value else "0"
if isinstance(value, int):
value = str(value)
# librtmp expects some characters to be escaped
value = value.replace("\\", "\\5c")
value = value.replace(" ", "\\20")
value = value.replace('"', "\\22")
return value
def rtmpparse(url):
parse = urlparse(url)
netloc = f"{parse.hostname}:{parse.port or 1935}"
split = list(filter(None, parse.path.split("/")))
playpath = None
if len(split) > 2:
app = "/".join(split[:2])
playpath = "/".join(split[2:])
elif len(split) == 2:
app, playpath = split
else:
app = split[0]
if len(parse.query) > 0:
playpath += f"?{parse.query}"
tcurl = f"{parse.scheme}://{netloc}/{app}"
return tcurl, playpath

8
src/streamlink/utils/swf.py

@ -1,8 +0,0 @@
import zlib
def swfdecompress(data):
if data[:3] == b"CWS":
data = b"F" + data[1:8] + zlib.decompress(data[8:])
return data

59
src/streamlink_cli/argparser.py

@ -752,7 +752,7 @@ def build_parser():
not listed will be omitted from the available streams list. A ``*`` can
be used as a wildcard to match any other type of stream, eg. muxed-stream.
Default is "rtmp,hls,http,*".
Default is "hls,http,*".
"""
)
stream.add_argument(
@ -788,8 +788,6 @@ def build_parser():
transport = parser.add_argument_group("Stream transport options")
transport_hls = transport.add_argument_group("HLS options")
transport_rtmp = transport.add_argument_group("RTMP options")
transport_subprocess = transport.add_argument_group("Subprocess options")
transport_ffmpeg = transport.add_argument_group("FFmpeg options")
transport.add_argument(
@ -858,7 +856,7 @@ def build_parser():
help="""
Timeout for reading data from streams.
This applies to all different kinds of stream types, such as DASH, HLS, HTTP, RTMP, etc.
This applies to all different kinds of stream types, such as DASH, HLS, HTTP, etc.
Default is 60.0.
"""
@ -1018,59 +1016,6 @@ def build_parser():
transport.add_argument("--http-stream-timeout", help=argparse.SUPPRESS)
transport_rtmp.add_argument(
"--rtmp-rtmpdump",
metavar="FILENAME",
help="""
RTMPDump is used to access RTMP streams. You can specify the
location of the rtmpdump executable if it is not in your PATH.
Example: "/usr/local/bin/rtmpdump"
"""
)
transport_rtmp.add_argument(
"--rtmp-proxy",
metavar="PROXY",
help="""
A SOCKS proxy that RTMP streams will use.
Example: 127.0.0.1:9050
"""
)
transport_rtmp.add_argument("--rtmpdump", help=argparse.SUPPRESS)
transport_rtmp.add_argument("--rtmp-timeout", help=argparse.SUPPRESS)
transport_subprocess.add_argument(
"--subprocess-cmdline",
action="store_true",
help="""
Print the command-line used internally to play the stream.
This is only available on RTMP streams.
"""
)
transport_subprocess.add_argument(
"--subprocess-errorlog",
action="store_true",
help="""
Log possible errors from internal subprocesses to a temporary file. The
file will be saved in your systems temporary directory.
Useful when debugging rtmpdump related issues.
"""
)
transport_subprocess.add_argument(
"--subprocess-errorlog-path",
type=str,
metavar="PATH",
help="""
Log the subprocess errorlog to a specific file rather than a temporary
file. Takes precedence over subprocess-errorlog.
Useful when debugging rtmpdump related issues.
"""
)
transport_ffmpeg.add_argument(
"--ffmpeg-ffmpeg",
metavar="FILENAME",

2
src/streamlink_cli/constants.py

@ -64,7 +64,7 @@ else:
LOG_DIR = XDG_STATE_HOME / "streamlink" / "logs"
STREAM_SYNONYMS = ["best", "worst", "best-unfiltered", "worst-unfiltered"]
STREAM_PASSTHROUGH = ["hls", "http", "rtmp"]
STREAM_PASSTHROUGH = ["hls", "http"]
__all__ = [
"PLAYER_ARGS_INPUT_DEFAULT", "PLAYER_ARGS_INPUT_FALLBACK",

31
src/streamlink_cli/main.py

@ -25,7 +25,6 @@ from streamlink.cache import Cache
from streamlink.exceptions import FatalPluginError
from streamlink.plugin import Plugin, PluginOptions
from streamlink.stream.stream import Stream, StreamIO
from streamlink.stream.streamprocess import StreamProcess
from streamlink.utils.named_pipe import NamedPipe
from streamlink_cli.argparser import build_parser
from streamlink_cli.compat import DeprecatedPath, is_win32, stdout
@ -39,7 +38,7 @@ try:
ACCEPTABLE_ERRNO += (errno.WSAECONNABORTED,)
except AttributeError:
pass # Not windows
QUIET_OPTIONS = ("json", "stream_url", "subprocess_cmdline", "quiet")
QUIET_OPTIONS = ("json", "stream_url", "quiet")
args = None
console: ConsoleOutput = None
@ -400,8 +399,8 @@ def handle_stream(plugin: Plugin, streams: Streams, stream_name: str) -> None:
"""Decides what to do with the selected stream.
Depending on arguments it can be one of these:
- Output internal command-line
- Output JSON represenation
- Output the stream URL
- Continuously output the stream over HTTP
- Output stream data to selected output
@ -410,21 +409,8 @@ def handle_stream(plugin: Plugin, streams: Streams, stream_name: str) -> None:
stream_name = resolve_stream_name(streams, stream_name)
stream = streams[stream_name]
# Print internal command-line if this stream
# uses a subprocess.
if args.subprocess_cmdline:
if isinstance(stream, StreamProcess):
try:
cmdline = stream.cmdline()
except StreamError as err:
console.exit(err)
console.msg(cmdline)
else:
console.exit("The stream specified cannot be translated to a command")
# Print JSON representation of the stream
elif args.json:
if args.json:
console.msg_json(
stream,
metadata=plugin.get_metadata()
@ -790,13 +776,6 @@ def setup_options():
if args.hls_live_restart:
streamlink.set_option("hls-live-restart", args.hls_live_restart)
if args.rtmp_rtmpdump:
streamlink.set_option("rtmp-rtmpdump", args.rtmp_rtmpdump)
elif args.rtmpdump:
streamlink.set_option("rtmp-rtmpdump", args.rtmpdump)
if args.rtmp_proxy:
streamlink.set_option("rtmp-proxy", args.rtmp_proxy)
# deprecated
if args.hls_segment_attempts:
streamlink.set_option("hls-segment-attempts", args.hls_segment_attempts)
@ -808,8 +787,6 @@ def setup_options():
streamlink.set_option("hls-timeout", args.hls_timeout)
if args.http_stream_timeout:
streamlink.set_option("http-stream-timeout", args.http_stream_timeout)
if args.rtmp_timeout:
streamlink.set_option("rtmp-timeout", args.rtmp_timeout)
# generic stream- arguments take precedence over deprecated stream-type arguments
if args.stream_segment_attempts:
@ -838,8 +815,6 @@ def setup_options():
if args.ffmpeg_start_at_zero:
streamlink.set_option("ffmpeg-start-at-zero", args.ffmpeg_start_at_zero)
streamlink.set_option("subprocess-errorlog", args.subprocess_errorlog)
streamlink.set_option("subprocess-errorlog-path", args.subprocess_errorlog_path)
streamlink.set_option("locale", args.locale)

8
tests/plugin/testplugin.py

@ -7,7 +7,6 @@ from streamlink.plugin import PluginArgument, PluginArguments, pluginmatcher
from streamlink.plugins import Plugin
from streamlink.stream.hls import HLSStream
from streamlink.stream.http import HTTPStream
from streamlink.stream.rtmpdump import RTMPStream
from streamlink.stream.stream import Stream
@ -58,7 +57,6 @@ class TestPlugin(Plugin):
streams = {}
streams["test"] = TestStream(self.session)
streams["rtmp"] = RTMPStream(self.session, dict(rtmp="rtmp://test.se"))
streams["hls"] = HLSStream(self.session, "http://test.se/playlist.m3u8")
streams["http"] = HTTPStream(self.session, "http://test.se/stream")
@ -71,8 +69,10 @@ class TestPlugin(Plugin):
streams["1500k"] = HTTPStream(self.session, "http://test.se/stream")
streams["3000k"] = HTTPStream(self.session, "http://test.se/stream")
streams["480p"] = [HTTPStream(self.session, "http://test.se/stream"),
RTMPStream(self.session, dict(rtmp="rtmp://test.se"))]
streams["480p"] = [
HTTPStream(self.session, "http://test.se/stream"),
HLSStream(self.session, "http://test.se/playlist.m3u8")
]
return streams

26
tests/plugins/test_stream.py

@ -7,7 +7,6 @@ from streamlink import Streamlink
from streamlink.plugin.plugin import parse_params, stream_weight
from streamlink.stream.hls import HLSStream
from streamlink.stream.http import HTTPStream
from streamlink.stream.rtmpdump import RTMPStream
class TestPluginStream(unittest.TestCase):
@ -54,17 +53,6 @@ class TestPluginStream(unittest.TestCase):
self.assertIsInstance(stream, HLSStream)
self.assertEqual(stream.url, url)
def _test_rtmp(self, surl, url, params):
plugin = self.resolve_url(surl)
streams = plugin.streams()
self.assertIn("live", streams)
stream = streams["live"]
self.assertIsInstance(stream, RTMPStream)
self.assertEqual(stream.params["rtmp"], url)
self.assertDictHas(params, stream.params)
def _test_http(self, surl, url, params):
plugin = self.resolve_url(surl)
streams = plugin.streams()
@ -76,20 +64,6 @@ class TestPluginStream(unittest.TestCase):
self.assertEqual(stream.url, url)
self.assertDictHas(params, stream.args)
def test_plugin_rtmp(self):
self._test_rtmp("rtmp://hostname.se/stream",
"rtmp://hostname.se/stream", dict())
self._test_rtmp("rtmp://hostname.se/stream live=1 qarg='a \\'string' noq=test",
"rtmp://hostname.se/stream", dict(live=True, qarg='a \'string', noq="test"))
self._test_rtmp("rtmp://hostname.se/stream live=1 num=47",
"rtmp://hostname.se/stream", dict(live=True, num=47))
self._test_rtmp("rtmp://hostname.se/stream conn=['B:1','S:authMe','O:1','NN:code:1.23','NS:flag:ok','O:0']",
"rtmp://hostname.se/stream",
dict(conn=['B:1', 'S:authMe', 'O:1', 'NN:code:1.23', 'NS:flag:ok', 'O:0']))
def test_plugin_hls(self):
self._test_hls("hls://hostname.se/foo", "https://hostname.se/foo")
self._test_hls("hls://http://hostname.se/foo", "http://hostname.se/foo")

70
tests/stream/test_stream_streamprocess.py

@ -1,70 +0,0 @@
import unittest
from unittest.mock import PropertyMock, patch
import pytest
from streamlink import StreamError
from streamlink import Streamlink
from streamlink.stream.streamprocess import StreamProcess
@pytest.mark.parametrize("parameters,arguments,expected", [
(dict(h=True), None, ["test", "-h"]),
(dict(foo="bar"), None, ["test", "--foo", "bar"]),
(dict(L="big"), None, ["test", "-L", "big"]),
(None, ["foo", "bar"], ["test", "foo", "bar"]),
(dict(extra="nothing", verbose=True, L="big"), None, ["test", "-L", "big", "--extra", "nothing", "--verbose"]),
(dict(extra=["a", "b", "c"]), None, ["test", "--extra", "a", "--extra", "b", "--extra", "c"]),
(dict(e=["a", "b", "c"]), None, ["test", "-e", "a", "-e", "b", "-e", "c"]),
])
def test_bake(parameters, arguments, expected):
assert expected == StreamProcess.bake("test", parameters or {}, arguments or [])
class TestStreamProcess(unittest.TestCase):
def test_bake_different_prefix(self):
self.assertEqual(["test", "/H", "/foo", "bar", "/help"],
StreamProcess.bake("test", dict(help=True, H=True, foo="bar"),
long_option_prefix="/", short_option_prefix="/"))
self.assertEqual(["test", "/?"],
StreamProcess.bake("test", {"?": True},
long_option_prefix="/", short_option_prefix="/"))
@patch('streamlink.stream.streamprocess.StreamProcess.cmd', new_callable=PropertyMock)
def test_check_cmd_none(self, mock_cmd):
s = StreamProcess(Streamlink())
mock_cmd.return_value = None
self.assertRaises(StreamError, s._check_cmd)
@patch('streamlink.stream.streamprocess.which')
@patch('streamlink.stream.streamprocess.StreamProcess.cmd', new_callable=PropertyMock)
def test_check_cmd_cat(self, which, mock_cmd):
s = StreamProcess(Streamlink())
mock_cmd.return_value = "test"
self.assertEqual("test", s._check_cmd())
@patch('streamlink.stream.streamprocess.which')
@patch('streamlink.stream.streamprocess.StreamProcess.cmd', new_callable=PropertyMock)
def test_check_cmd_nofound(self, which, mock_cmd):
s = StreamProcess(Streamlink())
mock_cmd.return_value = "test"
which.return_value = None
self.assertRaises(StreamError, s._check_cmd)
@patch('streamlink.stream.streamprocess.which')
@patch('streamlink.stream.streamprocess.StreamProcess.cmd', new_callable=PropertyMock)
def test_check_cmdline(self, which, mock_cmd):
s = StreamProcess(Streamlink(), params=dict(help=True))
mock_cmd.return_value = "test"
which.return_value = "test"
self.assertEqual("test --help", s.cmdline())
@patch('streamlink.stream.streamprocess.which')
@patch('streamlink.stream.streamprocess.StreamProcess.cmd', new_callable=PropertyMock)
def test_check_cmdline_long(self, which, mock_cmd):
s = StreamProcess(Streamlink(), params=dict(out_file="test file.txt"))
mock_cmd.return_value = "test"
which.return_value = "test"
self.assertEqual("test --out-file \"test file.txt\"", s.cmdline())

11
tests/stream/test_stream_to_url.py

@ -5,7 +5,6 @@ from streamlink import Streamlink
from streamlink.plugins.filmon import FilmOnHLS
from streamlink.stream.hls import HLSStream
from streamlink.stream.http import HTTPStream
from streamlink.stream.rtmpdump import RTMPStream
from streamlink.stream.stream import Stream
from streamlink_cli.utils import stream_to_url
@ -31,16 +30,6 @@ class TestStreamToURL(unittest.TestCase):
self.assertEqual(expected, stream_to_url(stream))
self.assertEqual(expected, stream.to_url())
def test_rtmp_stream(self):
stream = RTMPStream(self.session, {"rtmp": "rtmp://test.se/app/play_path",
"swfVfy": "http://test.se/player.swf",
"swfhash": "test",
"swfsize": 123456,
"playPath": "play_path"})
expected = "rtmp://test.se/app/play_path playPath=play_path swfUrl=http://test.se/player.swf swfVfy=1"
self.assertEqual(expected, stream_to_url(stream))
self.assertEqual(expected, stream.to_url())
@patch("time.time")
@patch("streamlink.plugins.filmon.FilmOnHLS.url", new_callable=PropertyMock)
def test_filmon_stream(self, url, time):

13
tests/test_cmdline.py

@ -73,10 +73,10 @@ class TestCommandLinePOSIX(CommandLineTestCase):
["/usr/bin/player", "--input-title-format", 'Poker "Stars"', "-"])
def test_open_player_extra_args_in_player_pass_through(self):
self._test_args(["streamlink", "--player-passthrough", "rtmp", "-p", "/usr/bin/player",
self._test_args(["streamlink", "--player-passthrough", "hls", "-p", "/usr/bin/player",
"-a", '''--input-title-format "Poker \\"Stars\\"" {filename}''',
"test.se", "rtmp"],
["/usr/bin/player", "--input-title-format", 'Poker "Stars"', "rtmp://test.se"],
"test.se", "hls"],
["/usr/bin/player", "--input-title-format", 'Poker "Stars"', "http://test.se/playlist.m3u8"],
passthrough=True)
def test_single_hyphen_extra_player_args_971(self):
@ -114,10 +114,11 @@ class TestCommandLineWindows(CommandLineTestCase):
'''c:\\Program Files\\Player\\player.exe --input-title-format "Poker \\"Stars\\"" -''')
def test_open_player_extra_args_in_player_pass_through(self):
self._test_args(["streamlink", "--player-passthrough", "rtmp", "-p", "c:\\Program Files\\Player\\player.exe",
self._test_args(["streamlink", "--player-passthrough", "hls", "-p", "c:\\Program Files\\Player\\player.exe",
"-a", '''--input-title-format "Poker \\"Stars\\"" {filename}''',
"test.se", "rtmp"],
'''c:\\Program Files\\Player\\player.exe --input-title-format "Poker \\"Stars\\"" \"rtmp://test.se\"''',
"test.se", "hls"],
'''c:\\Program Files\\Player\\player.exe'''
+ ''' --input-title-format "Poker \\"Stars\\"" \"http://test.se/playlist.m3u8\"''',
passthrough=True)
def test_single_hyphen_extra_player_args_971(self):

2
tests/test_plugins_meta.py

@ -12,7 +12,7 @@ class TestPluginMeta(unittest.TestCase):
"""
longMessage = False
built_in_plugins = [
"http", "rtmp", "hls", "dash", "stream"
"http", "hls", "dash", "stream"
]
title_re = re.compile(r"\n[= ]+\n")

10
tests/test_session.py

@ -11,7 +11,6 @@ from streamlink import NoPluginError, Streamlink
from streamlink.plugin import HIGH_PRIORITY, LOW_PRIORITY, NORMAL_PRIORITY, NO_PRIORITY, Plugin, pluginmatcher
from streamlink.stream.hls import HLSStream
from streamlink.stream.http import HTTPStream
from streamlink.stream.rtmpdump import RTMPStream
class EmptyPlugin(Plugin):
@ -263,21 +262,20 @@ class TestSession(unittest.TestCase):
self.assertTrue("worst" in streams)
self.assertTrue(streams["best"] is streams["1080p"])
self.assertTrue(streams["worst"] is streams["350k"])
self.assertTrue(isinstance(streams["rtmp"], RTMPStream))
self.assertTrue(isinstance(streams["http"], HTTPStream))
self.assertTrue(isinstance(streams["hls"], HLSStream))
def test_plugin_stream_types(self):
session = self.subject()
plugin = self.resolve_url(session, "http://test.se/channel")
streams = plugin.streams(stream_types=["http", "rtmp"])
streams = plugin.streams(stream_types=["http", "hls"])
self.assertTrue(isinstance(streams["480p"], HTTPStream))
self.assertTrue(isinstance(streams["480p_rtmp"], RTMPStream))
self.assertTrue(isinstance(streams["480p_hls"], HLSStream))
streams = plugin.streams(stream_types=["rtmp", "http"])
streams = plugin.streams(stream_types=["hls", "http"])
self.assertTrue(isinstance(streams["480p"], RTMPStream))
self.assertTrue(isinstance(streams["480p"], HLSStream))
self.assertTrue(isinstance(streams["480p_http"], HTTPStream))
def test_plugin_stream_sorting_excludes(self):

18
tests/test_stream_json.py

@ -6,7 +6,6 @@ from requests.utils import DEFAULT_ACCEPT_ENCODING
from streamlink import Streamlink
from streamlink.stream.hls import HLSStream
from streamlink.stream.http import HTTPStream
from streamlink.stream.rtmpdump import RTMPStream
from streamlink.stream.stream import Stream
@ -72,20 +71,3 @@ class TestStreamToJSON(unittest.TestCase):
},
stream.__json__()
)
def test_rtmp_stream(self):
stream = RTMPStream(self.session, {"rtmp": "rtmp://test.se/app/play_path",
"swfVfy": "http://test.se/player.swf",
"swfhash": "test",
"swfsize": 123456,
"playPath": "play_path"})
self.assertEqual(
{"type": "rtmp",
"args": [],
"params": {"rtmp": "rtmp://test.se/app/play_path",
"swfVfy": "http://test.se/player.swf",
"swfhash": "test",
"swfsize": 123456,
"playPath": "play_path"}},
stream.__json__()
)

22
tests/test_streamlink_api.py

@ -17,9 +17,7 @@ def get_session():
class TestStreamlinkAPI(unittest.TestCase):
@patch('streamlink.api.Streamlink', side_effect=get_session)
def test_find_test_plugin(self, session):
self.assertTrue(
"rtmp" in streams("test.se")
)
self.assertIn("hls", streams("test.se"))
@patch('streamlink.api.Streamlink', side_effect=get_session)
def test_no_streams_exception(self, session):
@ -31,18 +29,16 @@ class TestStreamlinkAPI(unittest.TestCase):
@patch('streamlink.api.Streamlink', side_effect=get_session)
def test_stream_type_filter(self, session):
stream_types = ["rtmp", "hls"]
stream_types = ["hls"]
available_streams = streams("test.se", stream_types=stream_types)
self.assertTrue("rtmp" in available_streams)
self.assertTrue("hls" in available_streams)
self.assertTrue("test" not in available_streams)
self.assertTrue("http" not in available_streams)
self.assertIn("hls", available_streams)
self.assertNotIn("test", available_streams)
self.assertNotIn("http", available_streams)
@patch('streamlink.api.Streamlink', side_effect=get_session)
def test_stream_type_wildcard(self, session):
stream_types = ["rtmp", "hls", "*"]
stream_types = ["hls", "*"]
available_streams = streams("test.se", stream_types=stream_types)
self.assertTrue("rtmp" in available_streams)
self.assertTrue("hls" in available_streams)
self.assertTrue("test" in available_streams)
self.assertTrue("http" in available_streams)
self.assertIn("hls", available_streams)
self.assertIn("test", available_streams)
self.assertIn("http", available_streams)

12
tests/utils/test_rtmp.py

@ -1,12 +0,0 @@
import pytest
from streamlink.utils.rtmp import rtmpparse
@pytest.mark.parametrize("url,tcurl,playpath", [
("rtmp://testserver.com/app/playpath?arg=1", "rtmp://testserver.com:1935/app", "playpath?arg=1"),
("rtmp://testserver.com/long/app/playpath?arg=1", "rtmp://testserver.com:1935/long/app", "playpath?arg=1"),
("rtmp://testserver.com/app", "rtmp://testserver.com:1935/app", None),
])
def test_rtmpparse(url, tcurl, playpath):
assert rtmpparse(url) == (tcurl, playpath)

13
tests/utils/test_swf.py

@ -1,13 +0,0 @@
import base64
import unittest
from streamlink.utils.swf import swfdecompress
class TestUtilsSWF(unittest.TestCase):
def test_swf_decompress(self):
# FYI, not a valid SWF
swf = b"FWS " + b"0000" + b"test data 12345"
swf_compressed = b"CWS " + b"0000" + base64.b64decode(b"eJwrSS0uUUhJLElUMDQyNjEFACpTBJo=")
self.assertEqual(swf, swfdecompress(swf_compressed))
self.assertEqual(swf, swfdecompress(swf))

7
win32/config

@ -66,7 +66,7 @@
# Please note that the player needs to support the streaming protocol
# and that custom stream implementations in plugins will become unavailable,
# same as buffering options and those which change the network behavior.
#player-passthrough=http,hls,rtmp
#player-passthrough=http,hls
# By default, Streamlink will close the player when the stream is over.
# Use this option to let the player stay or close itself instead.
@ -75,11 +75,6 @@
# Show the player's console output
#verbose-player
# RTMP streams are downloaded using rtmpdump. A full or relative path
# to the rtmpdump exe should be specified here.
#rtmp-rtmpdump=C:\Program Files (x86)\Streamlink\rtmpdump\rtmpdump.exe
rtmp-rtmpdump=rtmpdump.exe
# FFMPEG is used to mux separate video and audio streams in to a single
# stream so that they can be played. The full or relative path to ffmpeg
# or avconv should be specified here.

Loading…
Cancel
Save