Repository: streamlink/streamlink

Issue:
stream.hls: Exception in HLSStreamWriter
### Checklist

- [X] This is a plugin issue and not a different kind of issue
- [X] [I have read the contribution guidelines](https://github.com/streamlink/streamlink/blob/master/CONTRIBUTING.md#contributing-to-streamlink)
- [X] [I have checked the list of open and recently closed plugin issues](https://github.com/streamlink/streamlink/issues?q=is%3Aissue+label%3A%22plugin+issue%22)
- [X] [I have checked the commit log of the master branch](https://github.com/streamlink/streamlink/commits/master)

### Streamlink version

Latest stable release

### Description

This started happening in the last 24h on twitch streams and is probably related to the the other twitch issue posted. The problem here appears on most streams i try to play with **--twitch-disable-ads** set. Without it it works fine. (I removed the last part of the log, the part after _https://video-weaver_, because my IP was shown and maybe something more I don't know about. If that's not OK feel free to close this.)

### Debug log

```text
$ streamlink https://www.twitch.tv/alanzoka 360p --twitch-disable-hosting --twitch-disable-ads --loglevel debug
[cli][debug] OS:         Linux-5.4.0-131-generic-x86_64-with-glibc2.29
[cli][debug] Python:     3.8.10
[cli][debug] Streamlink: 5.0.1
[cli][debug] Dependencies:
[cli][debug]  isodate: 0.6.0
[cli][debug]  lxml: 4.6.4
[cli][debug]  pycountry: 20.7.3
[cli][debug]  pycryptodome: 3.10.1
[cli][debug]  PySocks: 1.7.1
[cli][debug]  requests: 2.26.0
[cli][debug]  websocket-client: 1.2.1
[cli][debug] Arguments:
[cli][debug]  url=https://www.twitch.tv/alanzoka
[cli][debug]  stream=['360p']
[cli][debug]  --loglevel=debug
[cli][debug]  --twitch-disable-ads=True
[cli][debug]  --twitch-disable-hosting=True
[cli][info] Found matching plugin twitch for URL https://www.twitch.tv/alanzoka
[plugins.twitch][debug] Getting live HLS streams for alanzoka
[plugins.twitch][debug] {'adblock': False, 'geoblock_reason': '', 'hide_ads': False, 'server_ads': True, 'show_ads': True}
[utils.l10n][debug] Language code: en_US
[cli][info] Available streams: audio_only, 160p (worst), 360p, 480p, 720p60, 1080p60 (best)
[cli][info] Opening stream: 360p (hls)
[cli][info] Starting player: /usr/bin/vlc
[plugins.twitch][info] Will skip ad segments
[stream.hls][debug] Reloading playlist
[cli][debug] Pre-buffering 8192 bytes
[plugins.twitch][info] Waiting for pre-roll ads to finish, be patient
[stream.hls][debug] First Sequence: 0; Last Sequence: 3
[stream.hls][debug] Start offset: 0; Duration: None; Start Sequence: 1; End Sequence: None
[stream.hls][debug] Adding segment 1 to queue
[stream.hls][debug] Adding segment 2 to queue
[stream.hls][debug] Adding segment 3 to queue
Exception in thread Thread-TwitchHLSStreamWriter:
Traceback (most recent call last):
  File "/usr/lib/python3.8/threading.py", line 932, in _bootstrap_inner
    self.run()
  File "/home/user/.local/lib/python3.8/site-packages/streamlink/stream/segmented.py", line 209, in run
    self.write(segment, result, *data)
  File "/home/user/.local/lib/python3.8/site-packages/streamlink/stream/hls.py", line 217, in write
    result.raw.drain_conn()
AttributeError: '_HTTPResponse' object has no attribute 'drain_conn'
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Adding segment 4 to queue
[stream.hls][debug] Adding segment 5 to queue
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Adding segment 6 to queue
[stream.hls][debug] Adding segment 7 to queue
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Adding segment 8 to queue
[stream.hls][debug] Adding segment 9 to queue
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Adding segment 10 to queue
[stream.hls][debug] Adding segment 11 to queue
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Adding segment 12 to queue
[stream.hls][debug] Adding segment 13 to queue
[stream.hls][debug] Adding segment 14 to queue
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Adding segment 15 to queue
[stream.hls][debug] Adding segment 16 to queue
[stream.hls][debug] Adding segment 17 to queue
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Adding segment 18 to queue
[stream.hls][debug] Adding segment 19 to queue
[stream.hls][debug] Adding segment 20 to queue
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Adding segment 21 to queue
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Adding segment 22 to queue
[stream.segmented][debug] Closing worker thread
[stream.segmented][debug] Closing writer thread
[cli][error] Try 1/1: Could not open stream <TwitchHLSStream ['hls', 'https://video-weaver... 

------REDACTED-------

tried 1 times, exiting
[cli][info] Closing currently open stream...
```



Discussion:
> ```
> Exception in thread Thread-TwitchHLSStreamWriter:
> Traceback (most recent call last):
>   File "/usr/lib/python3.8/threading.py", line 932, in _bootstrap_inner
>     self.run()
>   File "/home/user/.local/lib/python3.8/site-packages/streamlink/stream/segmented.py", line 209, in run
>     self.write(segment, result, *data)
>   File "/home/user/.local/lib/python3.8/site-packages/streamlink/stream/hls.py", line 217, in write
>     result.raw.drain_conn()
> AttributeError: '_HTTPResponse' object has no attribute 'drain_conn'
> ```

Which version of `urllib3` do you have installed?

Streamlink requires `requests >=2.26.0,<3`, which itself requires `urllib3 >=1.21.1,<1.27` on all supported versions:
https://github.com/streamlink/streamlink/blob/5.0.1/setup.cfg#L48
https://github.com/psf/requests/blob/v2.26.0/setup.py#L48

`urllib3.HTTPResponse.drain_conn()` was apparently only added in its `1.26.0` release:
https://github.com/urllib3/urllib3/commit/29b214a129883301f91ae4a74fd7ef2958cbf7b0

Your install is missing this method, which means it's an outdated version. We either need to set a version requirement for urllib3 or backport the method body.
My urllib3 is Version: 1.25.8. Is that something that comes installed with the python version of the OS install? I'm on Linux Mint 20.3 so that's what it comes installed with if that's the case. Any reason why it started happening now, because I've watched several different streams most days regularly and have never got it before?
`--twitch-disable-ads` triggers a different code path in the `HLSStreamWriter` when writing or filtering out HLS segments. The writer was updated a while ago with a bugfix to clean up stalled HTTP requests in the HTTP connection pool when filtering. To do this, a method from `urllib3` gets called, which was only added in `1.26.0`. While `1.26.0` was released more than two years ago, it's still supported by the specified version ranges of Streamlink and the `requests` library, which defines `urllib3` as a transitive dependency.

According to your log output, you have installed Streamlink via pip in your local user's python environment (via the `--user` flag, or implicitly installed there) at `~/.local/`. If `urllib3` is not part of that user environment, then it'll be looked up by the Python interpreter in your system's python environment at `/usr/share/`. Depending on your distro, this can be outdated. You should however be able to install or update it in your user environment. Just run `pip install --user --upgrade urllib3` or install all the latest dependencies of Streamlink via `pip install --user --upgrade --upgrade-strategy=eager streamlink`. However, it's always better to install python projects in separate environments via the `virtualenv` library, so you don't risk introducing dependency conflicts with other applications:
https://streamlink.github.io/install.html#virtual-environment
The reason why you're only seeing this issue now is that Twitch simply didn't embed any ads for Streamlink to filter out. Now that ads are embedded once again, the filtering code path is used.

Resolving patch:
diff --git a/src/streamlink/plugin/api/http_session.py b/src/streamlink/plugin/api/http_session.py
--- a/src/streamlink/plugin/api/http_session.py
+++ b/src/streamlink/plugin/api/http_session.py
@@ -1,6 +1,6 @@
 import ssl
 import time
-from typing import Any, Callable, Dict, List, Pattern, Tuple
+from typing import Any, Dict, Pattern, Tuple
 
 import requests.adapters
 # noinspection PyPackageRequirements
@@ -13,9 +13,6 @@
 from streamlink.utils.parse import parse_json, parse_xml
 
 
-urllib3_version = tuple(map(int, urllib3.__version__.split(".")[:3]))
-
-
 class _HTTPResponse(urllib3.response.HTTPResponse):
     def __init__(self, *args, **kwargs):
         # Always enforce content length validation!
@@ -42,8 +39,8 @@ def __init__(self, *args, **kwargs):
 requests.adapters.HTTPResponse = _HTTPResponse  # type: ignore[misc]
 
 
-# Never convert percent-encoded characters to uppercase in urllib3>=1.25.4.
-# This is required for sites which compare request URLs byte for byte and return different responses depending on that.
+# Never convert percent-encoded characters to uppercase in urllib3>=1.25.8.
+# This is required for sites which compare request URLs byte by byte and return different responses depending on that.
 # Older versions of urllib3 are not compatible with this override and will always convert to uppercase characters.
 #
 # https://datatracker.ietf.org/doc/html/rfc3986#section-2.1
@@ -53,31 +50,17 @@ def __init__(self, *args, **kwargs):
 # > octets, they are equivalent.  For consistency, URI producers and
 # > normalizers should use uppercase hexadecimal digits for all percent-
 # > encodings.
-if urllib3_version >= (1, 25, 4):
-    class Urllib3UtilUrlPercentReOverride:
-        _re_percent_encoding: Pattern = urllib3.util.url.PERCENT_RE  # type: ignore[attr-defined]
-
-        @classmethod
-        def _num_percent_encodings(cls, string) -> int:
-            return len(cls._re_percent_encoding.findall(string))
-
-        # urllib3>=1.25.8
-        # https://github.com/urllib3/urllib3/blame/1.25.8/src/urllib3/util/url.py#L219-L227
-        @classmethod
-        def subn(cls, repl: Callable, string: str) -> Tuple[str, int]:
-            return string, cls._num_percent_encodings(string)
-
-        # urllib3>=1.25.4,<1.25.8
-        # https://github.com/urllib3/urllib3/blame/1.25.4/src/urllib3/util/url.py#L218-L228
-        @classmethod
-        def findall(cls, string: str) -> List[Any]:
-            class _List(list):
-                def __len__(self) -> int:
-                    return cls._num_percent_encodings(string)
-
-            return _List()
-
-    urllib3.util.url.PERCENT_RE = Urllib3UtilUrlPercentReOverride  # type: ignore[attr-defined]
+class Urllib3UtilUrlPercentReOverride:
+    _re_percent_encoding: Pattern = urllib3.util.url.PERCENT_RE  # type: ignore[attr-defined]
+
+    # urllib3>=1.25.8
+    # https://github.com/urllib3/urllib3/blame/1.25.8/src/urllib3/util/url.py#L219-L227
+    @classmethod
+    def subn(cls, repl: Any, string: str, count: Any = None) -> Tuple[str, int]:
+        return string, len(cls._re_percent_encoding.findall(string))
+
+
+urllib3.util.url.PERCENT_RE = Urllib3UtilUrlPercentReOverride  # type: ignore[attr-defined]
 
 
 # requests.Request.__init__ keywords, except for "hooks"


Test patch:
diff --git a/tests/test_api_http_session.py b/tests/test_api_http_session.py
--- a/tests/test_api_http_session.py
+++ b/tests/test_api_http_session.py
@@ -5,11 +5,10 @@
 import requests
 
 from streamlink.exceptions import PluginError
-from streamlink.plugin.api.http_session import HTTPSession, urllib3_version
+from streamlink.plugin.api.http_session import HTTPSession
 from streamlink.plugin.api.useragents import FIREFOX
 
 
-@pytest.mark.skipif(urllib3_version < (1, 25, 4), reason="test only applicable on urllib3 >=1.25.4")
 class TestUrllib3Overrides:
     @pytest.fixture(scope="class")
     def httpsession(self) -> HTTPSession: