codecov[bot]: # [Codecov](https://codecov.io/gh/streamlink/streamlink/pull/2858?src=pr&el=h1) Report
> Merging [#2858](https://codecov.io/gh/streamlink/streamlink/pull/2858?src=pr&el=desc) into [master](https://codecov.io/gh/streamlink/streamlink/commit/c5d84455911b0b26fb524e527170b0f10f84cf64&el=desc) will **not change** coverage by `%`.
> The diff coverage is `0.00%`.


```diff
@@           Coverage Diff           @@
##           master    #2858   +/-   ##
=======================================
  Coverage   52.67%   52.67%           
=======================================
  Files         251      251           
  Lines       15753    15753           
=======================================
  Hits         8298     8298           
  Misses       7455     7455           
```

brouxco: Side-effect: resolves #1158
fgietzen: > I had a look at #2822. @fgietzen are you sure ffmpeg is installed on your system ?

Unfortunately not, I have not thought of that.
beardypig: @brouxco thanks for the contribution, code wise it looks good - I will test it out later. 
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 3114eed5..a9516b02 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -32,16 +32,16 @@ jobs:
         with:
           python-version: ${{ matrix.python }}
       - name: Install dependencies
-        continue-on-error: ${{ matrix.continue }}
+        continue-on-error: ${{ matrix.continue || false }}
         run: bash ./script/install-dependencies.sh
       - name: Test
-        continue-on-error: ${{ matrix.continue }}
+        continue-on-error: ${{ matrix.continue || false }}
         run: pytest -r a --cov --cov-report=xml
       - name: Lint
-        continue-on-error: ${{ matrix.continue }}
+        continue-on-error: ${{ matrix.continue || false }}
         run: flake8
       - name: Upload coverage data
-        continue-on-error: ${{ matrix.continue }}
+        continue-on-error: ${{ matrix.continue || false }}
         uses: codecov/codecov-action@v1
 
   documentation:
diff --git a/docs/_themes/sphinx_rtd_theme_violet/static/css/theme.css b/docs/_themes/sphinx_rtd_theme_violet/static/css/theme.css
index 26763c0c..86f10b22 100644
--- a/docs/_themes/sphinx_rtd_theme_violet/static/css/theme.css
+++ b/docs/_themes/sphinx_rtd_theme_violet/static/css/theme.css
@@ -4826,3 +4826,14 @@ table.table-custom-layout tbody tr td {
   overflow: unset;
   white-space: unset;
 }
+
+/* github avatar pictures on the donate page */
+.github-avatar {
+  float: left;
+  width: 150px;
+  height: 150px;
+  margin: 0 1rem 1rem 0;
+}
+.github-avatar + .container > ul > li {
+  list-style: none;
+}
diff --git a/docs/api_guide.rst b/docs/api_guide.rst
index 70efcdb6..797b685f 100644
--- a/docs/api_guide.rst
+++ b/docs/api_guide.rst
@@ -3,8 +3,6 @@
 API Guide
 =========
 
-.. module:: streamlink
-
 This API is what powers the :ref:`cli` but is also available to developers that wish
 to make use of the data Streamlink can retrieve in their own application.
 
diff --git a/docs/donate.rst b/docs/donate.rst
index 575852c7..326a4ad2 100644
--- a/docs/donate.rst
+++ b/docs/donate.rst
@@ -1,40 +1,64 @@
 Donating
 --------
 
-Thank you for considering a donation to the Streamlink team! Streamlink is an
-organization composed of a geographically diverse group of individuals.
-We accept donations/tips in a variety of ways. If you would like to donate
-directly to a specific user, please review their details in the team section
-below. If you would prefer to donate to the team as a whole, please use our
-`Open Collective <https://opencollective.com/streamlink>`_. Note that
-donations are not tax deductible as Streamlink does not operate as a charitable
-organization. Your donation to the team or a team member may be used in any
-way and does not come with expectations of work to be provided or as payment
-for future work.
-
----------------
-Streamlink Team
----------------
-
-Bastimeyer 
+Thank you for considering a donation to the Streamlink team!
+
+Streamlink is an organization composed of a geographically diverse group of
+individuals. Donations/tips can be sent in a variety of ways. If you would like
+to donate directly to a specific user, please review their details in the
+team section below.
+
+If you would prefer to donate to the team as a whole, please use our
+`Open Collective <https://opencollective.com/streamlink>`_.
+
+Note that donations are not tax deductible, as Streamlink does not operate as
+a charitable organization. Your donation to the team or a team member may be
+used in any way and does not come with expectations of work to be provided or
+as payment for future work.
+
+.. raw:: html
+
+   <h1>Individual team member donations</h1>
+
+Bastimeyer
 ^^^^^^^^^^
 
-- `Github <https://github.com/bastimeyer>`__
-- `Bitcoin <https://blockchain.info/qr?data=1EZg8eBz4RdPb8pEzYD9JEzr9Fyitzj8j8>`__ (`1EZg8eBz4RdPb8pEzYD9JEzr9Fyitzj8j8`)
-- `Dogecoin <https://blockchain.info/qr?data=DGkVP7AMUHr2TbVnWy6QyZqSMvCkeEo1ab>`__ (`DGkVP7AMUHr2TbVnWy6QyZqSMvCkeEo1ab`)
-- `Flattr <https://flattr.com/thing/3956088>`__
-- `Paypal <https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YUCGRLVALHS8C&item_name=Streamlink%20Twitch%20GUI>`__
+.. container:: clearfix
+
+   .. image:: https://github.com/bastimeyer.png?size=150
+      :class: github-avatar
+
+   .. container::
 
-Beardypig 
+      - `Github <https://github.com/bastimeyer>`__
+      - `Github Sponsors <https://github.com/sponsors/bastimeyer>`__
+      - `Paypal <https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YUCGRLVALHS8C&item_name=Streamlink%20Twitch%20GUI>`__
+      - `Bitcoin <https://blockchain.info/qr?data=1EZg8eBz4RdPb8pEzYD9JEzr9Fyitzj8j8>`__ :code:`1EZg8eBz4RdPb8pEzYD9JEzr9Fyitzj8j8`
+
+Beardypig
 ^^^^^^^^^
 
-- `Github <https://github.com/beardypig>`__
-- `Bitcoin <https://blockchain.info/qr?data=1Ey3KZ9SwTj6QyASE6vgJVebUiJsW1HuRh>`__ (`1Ey3KZ9SwTj6QyASE6vgJVebUiJsW1HuRh`)
+.. container:: clearfix
+
+   .. image:: https://github.com/beardypig.png?size=150
+      :class: github-avatar
+
+   .. container::
+
+      - `Github <https://github.com/beardypig>`__
+      - `Bitcoin <https://blockchain.info/qr?data=1Ey3KZ9SwTj6QyASE6vgJVebUiJsW1HuRh>`__ :code:`1Ey3KZ9SwTj6QyASE6vgJVebUiJsW1HuRh`
 
 Gravyboat
 ^^^^^^^^^
 
-- `Github <https://github.com/gravyboat>`__
-- `Bitcoin <https://blockchain.info/qr?data=1PpdFh9LkTsjtG2AAcrWqk6RiFrC2iTCxj>`__ (`1PpdFh9LkTsjtG2AAcrWqk6RiFrC2iTCxj`)
-- `Flattr <https://flattr.com/@gravyboat>`__
-- `Gratipay <https://gratipay.com/~gravyboat/>`__
+.. container:: clearfix
+
+   .. image:: https://github.com/gravyboat.png?size=150
+      :class: github-avatar
+
+   .. container::
+
+      - `Github <https://github.com/gravyboat>`__
+      - `Bitcoin <https://blockchain.info/qr?data=1PpdFh9LkTsjtG2AAcrWqk6RiFrC2iTCxj>`__ :code:`1PpdFh9LkTsjtG2AAcrWqk6RiFrC2iTCxj`
+      - `Flattr <https://flattr.com/@gravyboat>`__
+      - `Gratipay <https://gratipay.com/~gravyboat/>`__
diff --git a/docs/index.rst b/docs/index.rst
index fd774572..2d2fa53a 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -1,32 +1,36 @@
 Overview
 --------
 
-Streamlink is a :ref:`command-line utility <cli>` that pipes video streams
+Streamlink is a :ref:`command-line utility <cli>` which pipes video streams
 from various services into a video player, such as `VLC`_.
-The main purpose of Streamlink is to allow the user to avoid buggy and CPU
-heavy flash plugins but still be able to enjoy various streamed content.
+The main purpose of Streamlink is to avoid resource-heavy and unoptimized websites,
+while still allowing the user to enjoy various streamed content.
 There is also an :ref:`API <api_guide>` available for developers who want access
-to the video stream data. This project was forked from Livestreamer, which is
-no longer maintained.
+to the stream data.
 
-- Latest release: |version| (https://github.com/streamlink/streamlink/releases/latest)
-- GitHub: https://github.com/streamlink/streamlink
-- Issue tracker: https://github.com/streamlink/streamlink/issues
-- PyPI: https://pypi.org/project/streamlink/
-- Free software: Simplified BSD license
+This project was forked from Livestreamer, which is no longer maintained.
+
+Latest release (|version|)
+    https://github.com/streamlink/streamlink/releases/latest
+GitHub
+    https://github.com/streamlink/streamlink
+Issue tracker
+    https://github.com/streamlink/streamlink/issues
+PyPI
+    https://pypi.org/project/streamlink/
+Free software
+    Simplified BSD license
 
 Features
 --------
 
 Streamlink is built upon a plugin system which allows support for new services
-to be easily added. Currently most of the big streaming services are supported,
-such as:
+to be easily added. Most of the big streaming services are supported, such as:
 
-- `Dailymotion <https://www.dailymotion.com/live>`_
-- `Livestream <https://livestream.com/>`_
-- `Twitch <https://www.twitch.tv/>`_
-- `UStream <http://www.ustream.tv/explore/all>`_
-- `YouTube <https://www.youtube.com/>`_
+- `Twitch.tv <https://www.twitch.tv/>`_
+- `YouTube.com <https://www.youtube.com/>`_
+- `Livestream.com <https://livestream.com/>`_
+- `Dailymotion.com <https://www.dailymotion.com/live>`_
 
 ... and many more. A full list of plugins currently included can be found
 on the :ref:`plugin_matrix` page.
@@ -34,18 +38,17 @@ on the :ref:`plugin_matrix` page.
 Quickstart
 ----------
 
-The default behaviour of Streamlink is to playback a stream in the default
-player (`VLC <https://www.videolan.org/>`_).
+The default behavior of Streamlink is to play back streams in the `VLC <https://www.videolan.org/>`_ player.
 
 .. sourcecode:: console
 
-    # pip install streamlink
     $ streamlink twitch.tv/day9tv best
     [cli][info] Found matching plugin twitch for URL twitch.tv/day9tv
-    [cli][info] Opening stream: source (hls)
+    [cli][info] Available streams: audio_only, 160p (worst), 360p, 480p, 720p, 720p60, 1080p60 (best)
+    [cli][info] Opening stream: 1080p60 (hls)
     [cli][info] Starting player: vlc
 
-For more in-depth usage and install instructions see the `User guide`_.
+For more in-depth usage and install instructions, please refer to the `User guide`_.
 
 User guide
 ----------
diff --git a/docs/install.rst b/docs/install.rst
index 97ffe9e9..6c125b2b 100644
--- a/docs/install.rst
+++ b/docs/install.rst
@@ -166,13 +166,15 @@ Windows port. version                RosadinTV <RosadinTV at outlook.com> |br|
 ==================================== ===========================================
 
 
-Source code
------------
+PyPI package and source code
+----------------------------
 
-If a package is not available on your platform (or if it's out of date), Streamlink can be installed from source.
+If a package is not available on your platform, or if it's out of date,
+Streamlink can be installed via `pip`_, the Python package manager.
+
+In addition to using `pip`_, Streamlink can also be installed from source
+via `setuptools`_ after cloning the `git`_ repository.
 
-This can be done in a couple of different ways, for example by using `pip`_, the Python package manager,
-or by checking out the source code with `git`_ and installing it via setuptools. |br|
 Using :command:`easy_install` is no longer recommended.
 
 .. note::
@@ -230,6 +232,7 @@ Version                              Installing
 ==================================== ===========================================
 
 .. _pip: https://pip.readthedocs.org/en/latest/installing.html
+.. _setuptools: https://setuptools.readthedocs.io/en/latest/
 .. _git: https://git-scm.com/
 .. _Latest release (pip): https://pypi.python.org/pypi/streamlink
 .. _Latest release (easy_install): https://pypi.python.org/pypi/streamlink
@@ -348,16 +351,17 @@ Release                              Notes
 ==================================== ====================================
 `Stable release`_                    Download the installer from the `GitHub releases page`_.
 
-`Development build`_                 For testing purposes only! Updated once per day. |br|
-                                     Download the installer from `Bintray`_. |br|
-                                     See the `list of recent changes`_ since the last stable release.
+`Development build`_                 For testing purposes only! Built each day at midnight (UTC). |br|
+                                     Download the zipped installer from the `build artifacts`_ section of one of
+                                     the recent scheduled builds. Build artifacts are stored by Github for 90 days. |br|
+                                     See the `commit log`_ for a list of changes since the last stable release.
 ==================================== ====================================
 
 .. _Stable release:
 .. _GitHub releases page: https://github.com/streamlink/streamlink/releases/latest
 .. _Development build:
-.. _Bintray: https://bintray.com/streamlink/streamlink-nightly/streamlink/_latestVersion/#files
-.. _list of recent changes: https://bintray.com/streamlink/streamlink-nightly/streamlink/_latestVersion/#release
+.. _build artifacts: https://github.com/streamlink/streamlink/actions?query=event%3Aschedule+is%3Asuccess+branch%3Amaster
+.. _commit log: https://github.com/streamlink/streamlink/commits/master
 
 These installers contain:
 
diff --git a/src/streamlink/plugins/tv360.py b/src/streamlink/plugins/tv360.py
index 215afa81..f35ed6a5 100644
--- a/src/streamlink/plugins/tv360.py
+++ b/src/streamlink/plugins/tv360.py
@@ -1,32 +1,19 @@
 from __future__ import print_function
+
 import re
-from functools import partial
 
 from streamlink.plugin import Plugin
 from streamlink.plugin.api import validate
 from streamlink.stream import HLSStream
-from streamlink.utils import parse_json
 
 
 class TV360(Plugin):
-    url_re = re.compile(r"https?://(?:www.)?tv360.com.tr/CanliYayin")
-    data_re = re.compile(r'''div.*?data-tp=(?P<q>["'])(?P<data>.*?)(?P=q)''', re.DOTALL)
-    _js_to_json = partial(re.compile(r"""(\w+):(["']|\d+,|true|false)""").sub, r'"\1":\2')
-    data_schema = validate.Schema(
-        validate.transform(data_re.search),
-        validate.any(
-            None,
-            validate.all(
-                validate.get("data"),
-                validate.transform(_js_to_json),
-                validate.transform(lambda x: x.replace("'", '"')),
-                validate.transform(parse_json),
-                {
-                    "tp_type": "hls4",
-                    "tp_file": validate.url(),
-                }
-            )
-        )
+    url_re = re.compile(r"https?://(?:www.)?tv360.com.tr/canli-yayin")
+    hls_re = re.compile(r'''hls.loadSource\(["'](http.*m3u8)["']\)''', re.DOTALL)
+
+    hls_schema = validate.Schema(
+        validate.transform(hls_re.search),
+        validate.any(None, validate.all(validate.get(1)))
     )
 
     @classmethod
@@ -35,10 +22,10 @@ class TV360(Plugin):
 
     def _get_streams(self):
         res = self.session.http.get(self.url)
-        data = self.data_schema.validate(res.text)
+        hls_url = self.hls_re.search(res.text)
 
-        if data:
-            return HLSStream.parse_variant_playlist(self.session, data["tp_file"])
+        if hls_url:
+            return HLSStream.parse_variant_playlist(self.session, hls_url.group(1))
 
 
 __plugin__ = TV360
diff --git a/src/streamlink/plugins/youtube.py b/src/streamlink/plugins/youtube.py
index 26d12466..9229b093 100644
--- a/src/streamlink/plugins/youtube.py
+++ b/src/streamlink/plugins/youtube.py
@@ -16,75 +16,42 @@ from streamlink.utils.encoding import maybe_decode
 log = logging.getLogger(__name__)
 
 
-def parse_stream_map(stream_map):
-    if not stream_map:
-        return []
-
-    return [parse_query(s) for s in stream_map.split(",")]
-
-
-def parse_fmt_list(formatsmap):
-    formats = {}
-    if not formatsmap:
-        return formats
-
-    for format in formatsmap.split(","):
-        s = format.split("/")
-        (w, h) = s[1].split("x")
-        formats[int(s[0])] = "{0}p".format(h)
-
-    return formats
-
-
 _config_schema = validate.Schema(
     {
-        validate.optional("fmt_list"): validate.all(
-            validate.text,
-            validate.transform(parse_fmt_list)
-        ),
-        validate.optional("url_encoded_fmt_stream_map"): validate.all(
-            validate.text,
-            validate.transform(parse_stream_map),
-            [{
-                "itag": validate.all(
-                    validate.text,
-                    validate.transform(int)
-                ),
-                "quality": validate.text,
-                "url": validate.url(scheme="http"),
-                validate.optional("s"): validate.text,
-                validate.optional("stereo3d"): validate.all(
-                    validate.text,
-                    validate.transform(int),
-                    validate.transform(bool)
-                ),
-            }]
-        ),
-        validate.optional("adaptive_fmts"): validate.all(
-            validate.text,
-            validate.transform(parse_stream_map),
-            [{
-                validate.optional("s"): validate.text,
-                "type": validate.all(
-                    validate.text,
-                    validate.transform(lambda t: t.split(";")[0].split("/")),
-                    [validate.text, validate.text]
-                ),
-                "url": validate.all(
-                    validate.url(scheme="http")
-                )
-            }]
-        ),
-        validate.optional("hlsvp"): validate.text,
         validate.optional("player_response"): validate.all(
             validate.text,
             validate.transform(parse_json),
             {
                 validate.optional("streamingData"): {
                     validate.optional("hlsManifestUrl"): validate.text,
+                    validate.optional("formats"): [{
+                        "itag": int,
+                        validate.optional("url"): validate.text,
+                        validate.optional("cipher"): validate.text,
+                        "qualityLabel": validate.text
+                    }],
+                    validate.optional("adaptiveFormats"): [{
+                        "itag": int,
+                        "mimeType": validate.all(
+                            validate.text,
+                            validate.transform(
+                                lambda t:
+                                    [t.split(';')[0].split('/')[0], t.split(';')[1].split('=')[1].strip('"')]
+                            ),
+                            [validate.text, validate.text],
+                        ),
+                        validate.optional("url"): validate.url(scheme="http"),
+                        validate.optional("cipher"): validate.text,
+                        validate.optional("qualityLabel"): validate.text,
+                        validate.optional("bitrate"): int
+                    }]
                 },
                 validate.optional("videoDetails"): {
                     validate.optional("isLive"): validate.transform(bool),
+                    validate.optional("isLiveContent"): validate.transform(bool),
+                    validate.optional("author"): validate.text,
+                    validate.optional("title"): validate.all(validate.text,
+                                                             validate.transform(maybe_decode))
                 },
                 validate.optional("playabilityStatus"): {
                     validate.optional("status"): validate.text,
@@ -93,14 +60,6 @@ _config_schema = validate.Schema(
                 },
             },
         ),
-        validate.optional("live_playback"): validate.transform(bool),
-        validate.optional("reason"): validate.all(validate.text, validate.transform(maybe_decode)),
-        validate.optional("livestream"): validate.text,
-        validate.optional("live_playback"): validate.text,
-        validate.optional("author"): validate.all(validate.text,
-                                                  validate.transform(maybe_decode)),
-        validate.optional("title"): validate.all(validate.text,
-                                                 validate.transform(maybe_decode)),
         "status": validate.text
     }
 )
@@ -146,6 +105,7 @@ class YouTube(Plugin):
         }
     )
 
+    # There are missing itags
     adp_video = {
         137: "1080p",
         299: "1080p60",  # HFR
@@ -155,6 +115,9 @@ class YouTube(Plugin):
         315: "2160p60",  # HFR
         138: "2160p",
         302: "720p60",  # HFR
+        135: "480p",
+        133: "240p",
+        160: "144p",
     }
     adp_audio = {
         140: 128,
@@ -231,16 +194,15 @@ class YouTube(Plugin):
         self.author = data["author_name"]
         self.title = data["title"]
 
-    def _create_adaptive_streams(self, info, streams, protected):
+    def _create_adaptive_streams(self, info, streams):
         adaptive_streams = {}
         best_audio_itag = None
 
-        # Extract audio streams from the DASH format list
-        for stream_info in info.get("adaptive_fmts", []):
-            if stream_info.get("s"):
-                protected = True
+        # Extract audio streams from the adaptive format list
+        streaming_data = info.get("player_response", {}).get("streamingData", {})
+        for stream_info in streaming_data.get("adaptiveFormats", []):
+            if "url" not in stream_info:
                 continue
-
             stream_params = dict(parse_qsl(stream_info["url"]))
             if "itag" not in stream_params:
                 continue
@@ -248,7 +210,8 @@ class YouTube(Plugin):
             # extract any high quality streams only available in adaptive formats
             adaptive_streams[itag] = stream_info["url"]
 
-            stream_type, stream_format = stream_info["type"]
+            stream_type, stream_format = stream_info["mimeType"]
+
             if stream_type == "audio":
                 stream = HTTPStream(self.session, stream_info["url"])
                 name = "audio_{0}".format(stream_format)
@@ -272,7 +235,7 @@ class YouTube(Plugin):
                                                 HTTPStream(self.session, vurl),
                                                 HTTPStream(self.session, aurl))
 
-        return streams, protected
+        return streams
 
     def _find_video_id(self, url):
 
@@ -325,16 +288,16 @@ class YouTube(Plugin):
 
             res = self.session.http.get(self._video_info_url, params=params)
             info_parsed = parse_query(res.content if is_py2 else res.text, name="config", schema=_config_schema)
-            if (info_parsed.get("player_response", {}).get("playabilityStatus", {}).get("status") != "OK"
-                    or info_parsed.get("status") == "fail"):
-                reason = (info_parsed.get("player_response", {}).get("playabilityStatus", {}).get("reason")
-                          or info_parsed.get("reason"))
+            player_response = info_parsed.get("player_response", {})
+            playability_status = player_response.get("playabilityStatus", {})
+            if (playability_status.get("status") != "OK"):
+                reason = playability_status.get("reason")
                 log.debug("get_video_info - {0}: {1}".format(
                     count, reason)
                 )
                 continue
-            self.author = info_parsed.get("author")
-            self.title = info_parsed.get("title")
+            self.author = player_response.get("videoDetails", {}).get("author")
+            self.title = player_response.get("videoDetails", {}).get("title")
             log.debug("get_video_info - {0}: Found data".format(count))
             break
 
@@ -354,35 +317,34 @@ class YouTube(Plugin):
             log.error("Could not get video info")
             return
 
-        if info.get("livestream") == '1' or info.get("live_playback") == '1' \
-                or info.get("player_response", {}).get("videoDetails", {}).get("isLive"):
+        if (info.get("player_response", {}).get("videoDetails", {}).get("isLiveContent")
+           or info.get("player_response", {}).get("videoDetails", {}).get("isLive")):
             log.debug("This video is live.")
             is_live = True
 
-        formats = info.get("fmt_list")
         streams = {}
         protected = False
-        for stream_info in info.get("url_encoded_fmt_stream_map", []):
-            if stream_info.get("s"):
-                protected = True
-                continue
+        if (info.get("player_response", {}).get("streamingData", {}).get("adaptiveFormats", [{}])[0].get("cipher")
+           or info.get("player_response", {}).get("streamingData", {}).get("formats", [{}])[0].get("cipher")):
+            protected = True
+            log.debug("This video may be protected.")
 
+        for stream_info in info.get("player_response", {}).get("streamingData", {}).get("formats", []):
+            if "url" not in stream_info:
+                continue
             stream = HTTPStream(self.session, stream_info["url"])
-            name = formats.get(stream_info["itag"]) or stream_info["quality"]
-
-            if stream_info.get("stereo3d"):
-                name += "_3d"
+            name = stream_info["qualityLabel"]
 
             streams[name] = stream
 
         if not is_live:
-            streams, protected = self._create_adaptive_streams(info, streams, protected)
+            streams = self._create_adaptive_streams(info, streams)
 
-        hls_playlist = info.get("hlsvp") or info.get("player_response", {}).get("streamingData", {}).get("hlsManifestUrl")
-        if hls_playlist:
+        hls_manifest = info.get("player_response", {}).get("streamingData", {}).get("hlsManifestUrl")
+        if hls_manifest:
             try:
                 hls_streams = HLSStream.parse_variant_playlist(
-                    self.session, hls_playlist, namekey="pixels"
+                    self.session, hls_manifest, namekey="pixels"
                 )
                 streams.update(hls_streams)
             except IOError as err:
diff --git a/src/streamlink/utils/encoding.py b/src/streamlink/utils/encoding.py
index 19adb484..acf538ca 100644
--- a/src/streamlink/utils/encoding.py
+++ b/src/streamlink/utils/encoding.py
@@ -15,7 +15,10 @@ def get_filesystem_encoding():
 
 def maybe_encode(text, encoding="utf8"):
     if is_py2:
-        return text.encode(encoding)
+        if isinstance(text, unicode):
+            return text.encode(encoding)
+        else:
+            return text
     else:
         return text
 
diff --git a/src/streamlink_cli/main.py b/src/streamlink_cli/main.py
index 8f438ca0..a32a2cd9 100644
--- a/src/streamlink_cli/main.py
+++ b/src/streamlink_cli/main.py
@@ -30,7 +30,7 @@ from streamlink.utils import LazyFormatter
 import streamlink.logger as logger
 from .argparser import build_parser
 from .compat import stdout, is_win32
-from streamlink.utils.encoding import maybe_encode
+from streamlink.utils.encoding import maybe_decode, get_filesystem_encoding
 from .console import ConsoleOutput, ConsoleUserInputRequester
 from .constants import CONFIG_FILES, PLUGINS_DIR, STREAM_SYNONYMS, DEFAULT_STREAM_METADATA
 from .output import FileOutput, PlayerOutput
@@ -146,7 +146,7 @@ def create_http_server(host=None, port=0):
 def create_title(plugin=None):
     if args.title and plugin:
         title = LazyFormatter.format(
-            maybe_encode(args.title),
+            maybe_decode(args.title, get_filesystem_encoding()),
             title=lambda: plugin.get_title() or DEFAULT_STREAM_METADATA["title"],
             author=lambda: plugin.get_author() or DEFAULT_STREAM_METADATA["author"],
             category=lambda: plugin.get_category() or DEFAULT_STREAM_METADATA["category"],
diff --git a/src/streamlink_cli/output.py b/src/streamlink_cli/output.py
index 76cc8f9f..bc5989b5 100644
--- a/src/streamlink_cli/output.py
+++ b/src/streamlink_cli/output.py
@@ -185,13 +185,13 @@ class PlayerOutput(Output):
             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])
+                extra_args.extend([u"--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))
+                extra_args.append(u"--title={}".format(self.title))
 
             # potplayer
             if self.player_name == "potplayer":
@@ -209,8 +209,7 @@ class PlayerOutput(Output):
         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 u' '.join([cmd] + ([eargs] if eargs else []) + [args])
         return shlex.split(cmd) + extra_args + shlex.split(args)
 
     def _open(self):
@@ -234,7 +233,8 @@ class PlayerOutput(Output):
         else:
             fargs = subprocess.list2cmdline(args)
         log.debug(u"Calling: {0}".format(fargs))
-        subprocess.call(args,
+
+        subprocess.call(maybe_encode(args, get_filesystem_encoding()),
                         stdout=self.stdout,
                         stderr=self.stderr)
 
@@ -247,7 +247,8 @@ class PlayerOutput(Output):
         else:
             fargs = subprocess.list2cmdline(args)
         log.debug(u"Opening subprocess: {0}".format(fargs))
-        self.player = subprocess.Popen(args,
+
+        self.player = subprocess.Popen(maybe_encode(args, get_filesystem_encoding()),
                                        stdin=self.stdin, bufsize=0,
                                        stdout=self.stdout,
                                        stderr=self.stderr)

Youtube plugin not working with some vods
<!--
Thanks for reporting a plugin issue!
USE THE TEMPLATE. Otherwise your plugin issue may be rejected.

First, see the contribution guidelines:
https://github.com/streamlink/streamlink/blob/master/CONTRIBUTING.md#contributing-to-streamlink

Also check the list of open and closed plugin issues:
https://github.com/streamlink/streamlink/issues?q=is%3Aissue+label%3A%22plugin+issue%22

Please see the text preview to avoid unnecessary formatting errors.
-->


## Plugin Issue

<!-- Replace [ ] with [x] in order to check the box -->
- [x ] This is a plugin issue and I have read the contribution guidelines.


### Description

<!-- Explain the plugin issue as thoroughly as you can. -->
On some vods i get no playable streams found and cant seem to identify why.


### Reproduction steps / Explicit stream URLs to test

<!-- How can we reproduce this? Please note the exact steps below using the list format supplied. If you need more steps please add them. -->

1. Try to load streamlink https://www.youtube.com/watch?v=FmDUUGOTFCM


### Log output

<!--
TEXT LOG OUTPUT IS REQUIRED for a plugin issue!
Use the `--loglevel debug` parameter and avoid using parameters which suppress log output.
https://streamlink.github.io/cli.html#cmdoption-l

Make sure to **remove usernames and passwords**
You can copy the output to https://gist.github.com/ or paste it below.
-->

```
streamlink https://www.youtube.com/watch?v=jXg93FFZXWk -l debug
[cli][debug] OS:         Windows 10
[cli][debug] Python:     3.6.6
[cli][debug] Streamlink: 1.3.0
[cli][debug] Requests(2.22.0), Socks(1.7.1), Websocket(0.56.0)
[cli][info] Found matching plugin youtube for URL https://www.youtube.com/watch?v=jXg93FFZXWk
[plugin.youtube][debug] Video ID from URL
[plugin.youtube][debug] Using video ID: jXg93FFZXWk
[plugin.youtube][debug] get_video_info - 1: Found data
[plugin.youtube][debug] MuxedStream: v 137 a 251 = 1080p
Available streams: audio_mp4, audio_webm, 360p (worst), 720p, 1080p (best)

streamlink https://www.youtube.com/watch?v=FmDUUGOTFCM -l debug
[cli][debug] OS:         Windows 10
[cli][debug] Python:     3.6.6
[cli][debug] Streamlink: 1.3.0
[cli][debug] Requests(2.22.0), Socks(1.7.1), Websocket(0.56.0)
[cli][info] Found matching plugin youtube for URL https://www.youtube.com/watch?v=FmDUUGOTFCM
[plugin.youtube][debug] Video ID from URL
[plugin.youtube][debug] Using video ID: FmDUUGOTFCM
[plugin.youtube][debug] get_video_info - 1: Found data
error: No playable streams found on this URL: https://www.youtube.com/watch?v=FmDUUGOTFCM
```


### Additional comments, screenshots, etc.



[Love Streamlink? Please consider supporting our collective. Thanks!](https://opencollective.com/streamlink/donate)

diff --git a/tests/__init__.py b/tests/__init__.py
index a6e20084..e572a37f 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -1,17 +1,29 @@
+import os
 import warnings
 
+import pytest
+
+from streamlink.compat import is_py2, is_py3
+
 
 def catch_warnings(record=False, module=None):
     def _catch_warnings_wrapper(f):
         def _catch_warnings(*args, **kwargs):
             with warnings.catch_warnings(record=True, module=module) as w:
                 if record:
-                    return f(*(args + (w, )), **kwargs)
+                    return f(*(args + (w,)), **kwargs)
                 else:
                     return f(*args, **kwargs)
+
         return _catch_warnings
 
     return _catch_warnings_wrapper
 
 
-__all__ = ['ignore_warnings']
+windows_only = pytest.mark.skipif(os.name != "nt", reason="test only applicable on Window")
+posix_only = pytest.mark.skipif(os.name != "posix", reason="test only applicable on a POSIX OS")
+py3_only = pytest.mark.skipif(not is_py3, reason="test only applicable for Python 3")
+py2_only = pytest.mark.skipif(not is_py2, reason="test only applicable for Python 2")
+
+
+__all__ = ['catch_warnings', 'windows_only', 'posix_only', 'py2_only', 'py3_only']
diff --git a/tests/plugins/test_tv360.py b/tests/plugins/test_tv360.py
index 306b62b4..0e87c18b 100644
--- a/tests/plugins/test_tv360.py
+++ b/tests/plugins/test_tv360.py
@@ -6,7 +6,7 @@ from streamlink.plugins.tv360 import TV360
 class TestPluginTV360(unittest.TestCase):
     def test_can_handle_url(self):
         should_match = [
-            'http://tv360.com.tr/CanliYayin',
+            'http://tv360.com.tr/canli-yayin',
         ]
         for url in should_match:
             self.assertTrue(TV360.can_handle_url(url))
diff --git a/tests/test_cli_playerout.py b/tests/test_cli_playerout.py
new file mode 100644
index 00000000..e2c2464a
--- /dev/null
+++ b/tests/test_cli_playerout.py
@@ -0,0 +1,71 @@
+# -*- coding: utf-8 -*-
+from streamlink.utils import get_filesystem_encoding
+from streamlink_cli.output import PlayerOutput
+from tests import posix_only, windows_only, py2_only, py3_only
+from tests.mock import patch, ANY
+
+UNICODE_TITLE = u"기타치는소율 with UL섬 "
+
+
+@posix_only
+@patch('subprocess.Popen')
+def test_output_mpv_unicode_title_posix(popen):
+    po = PlayerOutput("mpv", title=UNICODE_TITLE)
+    popen().poll.side_effect = lambda: None
+    po.open()
+    popen.assert_called_with(['mpv', u"--title=" + UNICODE_TITLE, '-'],
+                             bufsize=ANY, stderr=ANY, stdout=ANY, stdin=ANY)
+
+
+@posix_only
+@patch('subprocess.Popen')
+def test_output_vlc_unicode_title_posix(popen):
+    po = PlayerOutput("vlc", title=UNICODE_TITLE)
+    popen().poll.side_effect = lambda: None
+    po.open()
+    popen.assert_called_with(['vlc', u'--input-title-format', UNICODE_TITLE, '-'],
+                             bufsize=ANY, stderr=ANY, stdout=ANY, stdin=ANY)
+
+
+@py2_only
+@windows_only
+@patch('subprocess.Popen')
+def test_output_mpv_unicode_title_windows_py2(popen):
+    po = PlayerOutput("mpv.exe", title=UNICODE_TITLE)
+    popen().poll.side_effect = lambda: None
+    po.open()
+    popen.assert_called_with("mpv.exe \"--title=" + UNICODE_TITLE.encode(get_filesystem_encoding()) + "\" -",
+                             bufsize=ANY, stderr=ANY, stdout=ANY, stdin=ANY)
+
+
+@py2_only
+@windows_only
+@patch('subprocess.Popen')
+def test_output_vlc_unicode_title_windows_py2(popen):
+    po = PlayerOutput("vlc.exe", title=UNICODE_TITLE)
+    popen().poll.side_effect = lambda: None
+    po.open()
+    popen.assert_called_with("vlc.exe --input-title-format \"" + UNICODE_TITLE.encode(get_filesystem_encoding()) + "\" -",
+                             bufsize=ANY, stderr=ANY, stdout=ANY, stdin=ANY)
+
+
+@py3_only
+@windows_only
+@patch('subprocess.Popen')
+def test_output_mpv_unicode_title_windows_py3(popen):
+    po = PlayerOutput("mpv.exe", title=UNICODE_TITLE)
+    popen().poll.side_effect = lambda: None
+    po.open()
+    popen.assert_called_with("mpv.exe \"--title=" + UNICODE_TITLE + "\" -",
+                             bufsize=ANY, stderr=ANY, stdout=ANY, stdin=ANY)
+
+
+@py3_only
+@windows_only
+@patch('subprocess.Popen')
+def test_output_vlc_unicode_title_windows_py3(popen):
+    po = PlayerOutput("vlc.exe", title=UNICODE_TITLE)
+    popen().poll.side_effect = lambda: None
+    po.open()
+    popen.assert_called_with("vlc.exe --input-title-format \"" + UNICODE_TITLE + "\" -",
+                             bufsize=ANY, stderr=ANY, stdout=ANY, stdin=ANY)
diff --git a/tests/test_cmdline_title.py b/tests/test_cmdline_title.py
index f00f47da..258c079c 100644
--- a/tests/test_cmdline_title.py
+++ b/tests/test_cmdline_title.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 import unittest
 
-from streamlink.compat import is_win32, is_py3
+from streamlink.compat import is_win32, is_py3, is_py2
 from streamlink.utils import get_filesystem_encoding
 from tests.test_cmdline import CommandLineTestCase
 
@@ -28,6 +28,10 @@ class TestCommandLineWithTitlePOSIX(CommandLineTestCase):
         self._test_args(["streamlink", "-p", "/usr/bin/mpv", "--title", "{title}", "http://test.se", "test"],
                         ["/usr/bin/mpv", "--title=Test Title", "-"])
 
+    def test_unicode_title_2444(self):
+        self._test_args(["streamlink", "-p", "mpv", "-t", "★", "http://test.se", "test"],
+                        ["mpv", u'--title=\u2605', "-"])
+
 
 @unittest.skipIf(not is_win32, "test only applicable on Windows")
 class TestCommandLineWithTitleWindows(CommandLineTestCase):
@@ -77,7 +81,7 @@ class TestCommandLineWithTitleWindows(CommandLineTestCase):
             passthrough=True
         )
 
-    @unittest.skipIf(is_py3, "Encoding is different in Python 2")
+    @unittest.skipUnless(is_py2, "Encoding is different in Python 2")
     def test_open_player_with_unicode_author_pot_py2(self):
         self._test_args(
             ["streamlink", "-p", "\"c:\\Program Files\\DAUM\\PotPlayer\\PotPlayerMini64.exe\"",
@@ -88,7 +92,7 @@ class TestCommandLineWithTitleWindows(CommandLineTestCase):
             passthrough=True
         )
 
-    @unittest.skipIf(not is_py3, "Encoding is different in Python 2")
+    @unittest.skipUnless(is_py3, "Encoding is different in Python 3")
     def test_open_player_with_unicode_author_pot_py3(self):
         self._test_args(
             ["streamlink", "-p", "\"c:\\Program Files\\DAUM\\PotPlayer\\PotPlayerMini64.exe\"",
@@ -106,3 +110,13 @@ class TestCommandLineWithTitleWindows(CommandLineTestCase):
             + "\"http://test.se/playlist.m3u8\\http://test.se/stream\"",
             passthrough=True
         )
+
+    @unittest.skipUnless(is_py2, "test only valid for Python 2")
+    def test_unicode_title_2444_py2(self):
+        self._test_args(["streamlink", "-p", "mpv", "-t", u"★".encode(get_filesystem_encoding()), "http://test.se", "test"],
+                        "mpv --title=" + u"★".encode(get_filesystem_encoding()) + " -")
+
+    @unittest.skipUnless(is_py3, "test only valid for Python 3")
+    def test_unicode_title_2444_py3(self):
+        self._test_args(["streamlink", "-p", "mpv", "-t", "★", "http://test.se", "test"],
+                        "mpv --title=★ -")

pip install -e .[dev]
parse_log_pytest
requirements.txt
apt-get update apt-get install -y gcc
3.7
dev-requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
certifi @ file:///croot/certifi_1671487769961/work/certifi
charset-normalizer==3.4.1
coverage==7.2.7
exceptiongroup==1.2.2
flake8==5.0.4
freezegun==1.5.1
idna==3.10
importlib-metadata==4.2.0
iniconfig==2.0.0
iso-639==0.4.5
iso3166==2.1.1
isodate==0.7.2
mccabe==0.7.0
mock==5.2.0
packaging==24.0
pluggy==1.2.0
pycodestyle==2.9.1
pycryptodome==3.22.0
pyflakes==2.5.0
PySocks==1.7.1
pytest==7.4.4
pytest-cov==4.1.0
python-dateutil==2.9.0.post0
requests==2.31.0
requests-mock==1.12.1
six==1.17.0
-e git+https://github.com/streamlink/streamlink.git@222ab719fbdbf9f388277cda52e39ccdfcd1cacb#egg=streamlink
tomli==2.0.1
typing_extensions==4.7.1
urllib3==2.0.7
websocket-client==1.6.1
zipp==3.15.0

name: streamlink
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=22.3.1=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - charset-normalizer==3.4.1
      - coverage==7.2.7
      - exceptiongroup==1.2.2
      - flake8==5.0.4
      - freezegun==1.5.1
      - idna==3.10
      - importlib-metadata==4.2.0
      - iniconfig==2.0.0
      - iso-639==0.4.5
      - iso3166==2.1.1
      - isodate==0.7.2
      - mccabe==0.7.0
      - mock==5.2.0
      - packaging==24.0
      - pluggy==1.2.0
      - pycodestyle==2.9.1
      - pycryptodome==3.22.0
      - pyflakes==2.5.0
      - pysocks==1.7.1
      - pytest==7.4.4
      - pytest-cov==4.1.0
      - python-dateutil==2.9.0.post0
      - requests==2.31.0
      - requests-mock==1.12.1
      - six==1.17.0
      - tomli==2.0.1
      - typing-extensions==4.7.1
      - urllib3==2.0.7
      - websocket-client==1.6.1
      - zipp==3.15.0
prefix: /opt/conda/envs/streamlink

tests/plugins/test_tv360.py::TestPluginTV360::test_can_handle_url
tests/test_api_http_session.py::TestPluginAPIHTTPSession::test_json_encoding tests/test_api_http_session.py::TestPluginAPIHTTPSession::test_json_encoding_override tests/test_api_http_session.py::TestPluginAPIHTTPSession::test_read_timeout tests/plugins/test_tv360.py::TestPluginTV360::test_can_handle_url_negative tests/test_cli_playerout.py::test_output_mpv_unicode_title_posix tests/test_cli_playerout.py::test_output_vlc_unicode_title_posix tests/test_cmdline_title.py::TestCommandLineWithTitlePOSIX::test_open_player_with_default_title_vlc tests/test_cmdline_title.py::TestCommandLineWithTitlePOSIX::test_open_player_with_default_title_vlc_args tests/test_cmdline_title.py::TestCommandLineWithTitlePOSIX::test_open_player_with_title_mpv tests/test_cmdline_title.py::TestCommandLineWithTitlePOSIX::test_open_player_with_title_vlc tests/test_cmdline_title.py::TestCommandLineWithTitlePOSIX::test_open_player_with_unicode_author_vlc tests/test_cmdline_title.py::TestCommandLineWithTitlePOSIX::test_unicode_title_2444
src/streamlink/plugins/youtube.py src/streamlink_cli/main.py src/streamlink/plugins/tv360.py docs/donate.rst .github/workflows/main.yml src/streamlink/utils/encoding.py docs/install.rst src/streamlink_cli/output.py docs/api_guide.rst docs/index.rst docs/_themes/sphinx_rtd_theme_violet/static/css/theme.css
src/streamlink/plugins/youtube.py src/streamlink_cli/main.py src/streamlink/plugins/tv360.py docs/donate.rst .github/workflows/main.yml src/streamlink/utils/encoding.py docs/install.rst src/streamlink_cli/output.py docs/api_guide.rst docs/index.rst docs/_themes/sphinx_rtd_theme_violet/static/css/theme.css