Repository: streamlink/streamlink

Issue:
HLS streams sometimes fail to end after encountering #EXT-X-ENDLIST
### Checklist

- [X] This is a bug report and not [a different kind of issue](https://github.com/streamlink/streamlink/issues/new/choose)
- [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 bug reports](https://github.com/streamlink/streamlink/issues?q=is%3Aissue+label%3A%22bug%22)
- [x] [I have checked the commit log of the master branch](https://github.com/streamlink/streamlink/commits/master)

### Streamlink version

streamlink 6.1.0+20.ge60d7c87

### Description

Occasionally, HLS streams will keep trying to refresh the playlist even after encountering `#EXT-X-ENDLIST`. This will continue until some other mechanism kicks in to kill the stream (read timeout, queue threshold, etc). This doesn't happen every single time, only occasionally.
I was logging with loglevel=all so the log is very verbose, I've cut out parts I believe are redundant, also cut out sensitive information.
I've found similar issues at #3628 and #2198, but both of them claim it was caused by `#EXT-X-ENDLIST` being missing from the playlist, so they're different from this case.

### Debug log

```text
[05:02:27.624459][cli][debug] OS:         Linux-6.1.44-x86_64-with-glibc2.24
[05:02:27.624924][cli][debug] Python:     3.11.0
[05:02:27.625185][cli][debug] OpenSSL:    OpenSSL 1.1.1s  1 Nov 2022
[05:02:27.625459][cli][debug] Streamlink: 6.1.0+20.ge60d7c87
[05:02:27.626924][cli][debug] Dependencies:
[05:02:27.658497][cli][debug]  certifi: 2022.9.24
[05:02:27.660236][cli][debug]  isodate: 0.6.1
[05:02:27.667291][cli][debug]  lxml: 4.9.1
[05:02:27.677015][cli][debug]  pycountry: 22.3.5
[05:02:27.685567][cli][debug]  pycryptodome: 3.15.0
[05:02:27.688960][cli][debug]  PySocks: 1.7.1
[05:02:27.690272][cli][debug]  requests: 2.28.1
[05:02:27.700613][cli][debug]  trio: 0.22.2
[05:02:27.706916][cli][debug]  trio-websocket: 0.10.3
[05:02:27.721277][cli][debug]  typing-extensions: 4.7.1
[05:02:27.740413][cli][debug]  urllib3: 1.26.12
[05:02:27.750012][cli][debug]  websocket-client: 1.4.2
[05:02:27.750634][cli][debug] Arguments:
[05:02:27.750929][cli][debug]  url=https://www.twitch.tv/user
[05:02:27.751172][cli][debug]  stream=['best']
[05:02:27.751485][cli][debug]  --loglevel=all
[05:02:27.751779][cli][debug]  --logfile=user/2023_09_06_05_02_26_(UTC)_29227.ts.log
[05:02:27.752114][cli][debug]  --output=user/2023_09_06_05_02_26_(UTC)_29227.ts
[05:02:27.752373][cli][debug]  --retry-streams=5.0
[05:02:27.752609][cli][debug]  --retry-max=12
[05:02:27.752936][cli][debug]  --retry-open=10
[05:02:27.753178][cli][debug]  --stream-segment-attempts=20
[05:02:27.753382][cli][debug]  --stream-segment-threads=1
[05:02:27.753616][cli][debug]  --stream-segment-timeout=60.0
[05:02:27.753897][cli][debug]  --stream-timeout=240.0
[05:02:27.754093][cli][debug]  --hls-playlist-reload-attempts=10
[05:02:27.754236][cli][debug]  --hls-playlist-reload-time=segment
[05:02:27.754471][cli][debug]  --hls-segment-queue-threshold=0.0
[05:02:27.754754][cli][debug]  --hls-live-restart=True
[05:02:27.754937][cli][debug]  --webbrowser=False
[05:02:27.755176][cli][debug]  --twitch-api-header=[('Authorization', 'OAuth '), ('Client-ID', '')]
[05:02:27.755329][cli][debug]  --twitch-access-token-param=[('playerType', ''), ('platform', '')]
[05:02:27.756035][cli][info] Found matching plugin twitch for URL https://www.twitch.tv/user
[05:02:27.756229][plugins.twitch][debug] Getting live HLS streams for user
[05:02:28.013601][plugins.twitch][debug] {'adblock': False, 'geoblock_reason': '', 'hide_ads': False, 'server_ads': True, 'show_ads': True}
[05:02:28.194074][utils.l10n][debug] Language code: en_GB
...
[05:02:28.661391][cli][info] Available streams: audio_only, 160p (worst), 360p, 480p, 720p60, 1080p60 (best)
[05:02:28.661602][cli][info] Opening stream: 1080p60 (hls)
[05:02:28.662044][cli][info] Writing output to
...
[05:02:28.662216][cli][debug] Checking file output
[05:02:28.663634][stream.hls][debug] Reloading playlist
[05:02:28.663938][cli][debug] Pre-buffering 8192 bytes
[05:02:29.804419][stream.hls_playlist][all] #EXT-X-VERSION:3
[05:02:29.804803][stream.hls_playlist][all] #EXT-X-TARGETDURATION:6
[05:02:29.805119][stream.hls_playlist][all] #EXT-X-MEDIA-SEQUENCE:0
[05:02:29.805369][stream.hls_playlist][all] #EXT-X-TWITCH-LIVE-SEQUENCE:0
[05:02:29.805602][stream.hls_playlist][all] #EXT-X-TWITCH-ELAPSED-SECS:0.000
[05:02:29.805853][stream.hls_playlist][all] #EXT-X-TWITCH-TOTAL-SECS:10.000
[05:02:29.806088][stream.hls_playlist][all] #EXT-X-DATERANGE:ID="playlist-creation-1693976549",CLASS="timestamp",START-DATE="2023-09-05T22:02:29.739-07:00",END-ON-NEXT=YES,X-SERVER-TIME="1693976549.74"
[05:02:29.823833][stream.hls_playlist][all] #EXT-X-DATERANGE:ID="playlist-session-1693976549",CLASS="twitch-session",START-DATE="2023-09-05T22:02:29.739-07:00",END-ON-NEXT=YES,X-TV-TWITCH-SESSIONID="..."
[05:02:29.824331][stream.hls_playlist][all] #EXT-X-DATERANGE:ID="source-1693976542",CLASS="twitch-stream-source",START-DATE="2023-09-06T05:02:22.709Z",END-ON-NEXT=YES,X-TV-TWITCH-STREAM-SOURCE="live"
[05:02:29.824671][stream.hls_playlist][all] #EXT-X-DATERANGE:ID="trigger-1693976542",CLASS="twitch-trigger",START-DATE="2023-09-06T05:02:22.709Z",END-ON-NEXT=YES,X-TV-TWITCH-TRIGGER-URL="https://video-weaver....Iwuwc"
[05:02:29.824988][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2023-09-06T05:02:22.709Z
[05:02:29.825203][stream.hls_playlist][all] #EXTINF:2.000,live
[05:02:29.825424][stream.hls_playlist][all] https://video-edge-...dXMtd2VzdC0yMLsH.ts
[05:02:29.825682][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2023-09-06T05:02:24.709Z
[05:02:29.825955][stream.hls_playlist][all] #EXTINF:2.000,live
[05:02:29.826242][stream.hls_playlist][all] https://video-edge-...Kgl1cy13ZXN0LTIwuwc.ts
[05:02:29.826528][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2023-09-06T05:02:26.709Z
[05:02:29.826785][stream.hls_playlist][all] #EXTINF:2.000,live
[05:02:29.827001][stream.hls_playlist][all] https://video-edge-...y13ZXN0LTIwuwc.ts
[05:02:29.827297][stream.hls_playlist][all] #EXT-X-TWITCH-PREFETCH:https://video-edge-...1cy13ZXN0LTIwuwc.ts
[05:02:29.827529][stream.hls_playlist][all] #EXT-X-TWITCH-PREFETCH:https://video-edge-...1cy13ZXN0LTIwuwc.ts
[05:02:29.827938][stream.hls][debug] First Sequence: 0; Last Sequence: 2
[05:02:29.828114][stream.hls][debug] Start offset: 0; Duration: None; Start Sequence: 0; End Sequence: None
[05:02:29.828282][stream.hls][debug] Adding segment 0 to queue
[05:02:29.829283][stream.hls][debug] Adding segment 1 to queue
[05:02:29.831928][stream.hls][debug] Adding segment 2 to queue
[05:02:30.663827][stream.hls][debug] Reloading playlist
... (stream continues normally) ...
[06:03:02.663152][stream.hls][debug] Reloading playlist
[06:03:03.067024][stream.hls_playlist][all] #EXT-X-VERSION:3
[06:03:03.067433][stream.hls_playlist][all] #EXT-X-TARGETDURATION:6
[06:03:03.067698][stream.hls_playlist][all] #EXT-X-MEDIA-SEQUENCE:1802
[06:03:03.067965][stream.hls_playlist][all] #EXT-X-TWITCH-LIVE-SEQUENCE:1802
[06:03:03.068209][stream.hls_playlist][all] #EXT-X-TWITCH-ELAPSED-SECS:3604.000
[06:03:03.068424][stream.hls_playlist][all] #EXT-X-TWITCH-TOTAL-SECS:3634.990
[06:03:03.068650][stream.hls_playlist][all] #EXT-X-DATERANGE:ID="playlist-creation-1693976549",CLASS="timestamp",START-DATE="2023-09-06T05:02:29.739Z",END-ON-NEXT=YES,X-SERVER-TIME="1693976549.74"
[06:03:03.069041][stream.hls_playlist][all] #EXT-X-DATERANGE:ID="playlist-session-1693976549",CLASS="twitch-session",START-DATE="2023-09-06T05:02:29.739Z",END-ON-NEXT=YES,X-TV-TWITCH-SESSIONID="..."
[06:03:03.069400][stream.hls_playlist][all] #EXT-X-DATERANGE:ID="source-1693976542",CLASS="twitch-stream-source",START-DATE="2023-09-06T05:02:22.709Z",END-ON-NEXT=YES,X-TV-TWITCH-STREAM-SOURCE="live"
[06:03:03.069770][stream.hls_playlist][all] #EXT-X-DATERANGE:ID="trigger-1693976542",CLASS="twitch-trigger",START-DATE="2023-09-06T05:02:22.709Z",END-ON-NEXT=YES,X-TV-TWITCH-TRIGGER-URL="https://video-weaver....Iwuwc"
[06:03:03.070091][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2023-09-06T06:02:26.709Z
[06:03:03.070390][stream.hls_playlist][all] #EXTINF:2.000,live
[06:03:03.070680][stream.hls_playlist][all] https://video-edge-....ts
[06:03:03.070980][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2023-09-06T06:02:28.709Z
[06:03:03.071231][stream.hls_playlist][all] #EXTINF:2.000,live
[06:03:03.071493][stream.hls_playlist][all] https://video-edge-....ts
[06:03:03.071730][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2023-09-06T06:02:30.709Z
[06:03:03.071994][stream.hls_playlist][all] #EXTINF:2.000,live
[06:03:03.072272][stream.hls_playlist][all] https://video-edge-....ts
[06:03:03.072511][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2023-09-06T06:02:32.709Z
[06:03:03.072863][stream.hls_playlist][all] #EXTINF:2.000,live
[06:03:03.073797][stream.hls_playlist][all] https://video-edge-....ts
[06:03:03.074266][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2023-09-06T06:02:34.709Z
[06:03:03.074597][stream.hls_playlist][all] #EXTINF:2.000,live
[06:03:03.074968][stream.hls_playlist][all] https://video-edge-....ts
[06:03:03.075538][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2023-09-06T06:02:36.709Z
[06:03:03.075771][stream.hls_playlist][all] #EXTINF:2.000,live
[06:03:03.076017][stream.hls_playlist][all] https://video-edge-....ts
[06:03:03.076302][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2023-09-06T06:02:38.709Z
[06:03:03.076847][stream.hls_playlist][all] #EXTINF:2.000,live
[06:03:03.077096][stream.hls_playlist][all] https://video-edge-....ts
[06:03:03.077312][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2023-09-06T06:02:40.709Z
[06:03:03.077547][stream.hls_playlist][all] #EXTINF:2.000,live
[06:03:03.077828][stream.hls_playlist][all] https://video-edge-....ts
[06:03:03.078035][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2023-09-06T06:02:42.709Z
[06:03:03.078249][stream.hls_playlist][all] #EXTINF:2.000,live
[06:03:03.078518][stream.hls_playlist][all] https://video-edge-....ts
[06:03:03.078821][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2023-09-06T06:02:44.709Z
[06:03:03.079106][stream.hls_playlist][all] #EXTINF:2.000,live
[06:03:03.079383][stream.hls_playlist][all] https://video-edge-....ts
[06:03:03.079643][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2023-09-06T06:02:46.709Z
[06:03:03.079952][stream.hls_playlist][all] #EXTINF:2.000,live
[06:03:03.080207][stream.hls_playlist][all] https://video-edge-....ts
[06:03:03.080468][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2023-09-06T06:02:48.709Z
[06:03:03.080775][stream.hls_playlist][all] #EXTINF:2.000,live
[06:03:03.080994][stream.hls_playlist][all] https://video-edge-....ts
[06:03:03.081184][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2023-09-06T06:02:50.709Z
[06:03:03.081369][stream.hls_playlist][all] #EXTINF:2.000,live
[06:03:03.081562][stream.hls_playlist][all] https://video-edge-....ts
[06:03:03.081793][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2023-09-06T06:02:52.709Z
[06:03:03.082044][stream.hls_playlist][all] #EXTINF:2.000,live
[06:03:03.082322][stream.hls_playlist][all] https://video-edge-....ts
[06:03:03.082507][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2023-09-06T06:02:54.709Z
[06:03:03.082786][stream.hls_playlist][all] #EXTINF:1.990,live
[06:03:03.083076][stream.hls_playlist][all] https://video-edge-....ts
[06:03:03.083357][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2023-09-06T06:02:56.699Z
[06:03:03.083652][stream.hls_playlist][all] #EXTINF:1.000,live
[06:03:03.083942][stream.hls_playlist][all] https://video-edge-....ts
[06:03:03.084164][stream.hls_playlist][all] #EXT-X-ENDLIST
[06:03:03.663126][stream.hls][debug] Reloading playlist
...
[06:03:03.810475][stream.hls_playlist][all] #EXT-X-ENDLIST
[06:03:04.663097][stream.hls][debug] Reloading playlist
...
[06:03:05.158972][stream.hls_playlist][all] #EXT-X-ENDLIST
[06:03:05.663178][stream.hls][debug] Reloading playlist
...
[06:03:05.814325][stream.hls_playlist][all] #EXT-X-ENDLIST
[06:03:06.663167][stream.hls][debug] Reloading playlist
...
[06:03:07.145406][stream.hls_playlist][all] #EXT-X-ENDLIST
[06:03:07.663106][stream.hls][debug] Reloading playlist
...
[06:03:07.806248][stream.hls_playlist][all] #EXT-X-ENDLIST
[06:03:08.663129][stream.hls][debug] Reloading playlist
...
[06:03:08.806112][stream.hls_playlist][all] #EXT-X-ENDLIST
[06:03:09.663137][stream.hls][debug] Reloading playlist
...
[06:03:09.847093][stream.hls_playlist][all] #EXT-X-ENDLIST
[06:03:10.663126][stream.hls][debug] Reloading playlist
...
[06:03:10.807157][stream.hls_playlist][all] #EXT-X-ENDLIST
[06:03:11.663171][stream.hls][debug] Reloading playlist
...
[06:03:11.835474][stream.hls_playlist][all] #EXT-X-ENDLIST
[06:03:12.663121][stream.hls][debug] Reloading playlist
...
[06:03:12.805984][stream.hls_playlist][all] #EXT-X-ENDLIST
[06:03:13.663117][stream.hls][debug] Reloading playlist
...
[06:03:13.845596][stream.hls_playlist][all] #EXT-X-ENDLIST
[06:03:14.663132][stream.hls][debug] Reloading playlist
...
[06:03:14.806936][stream.hls_playlist][all] #EXT-X-ENDLIST
[06:03:15.663139][stream.hls][debug] Reloading playlist
...
[06:03:15.837861][stream.hls_playlist][all] #EXT-X-ENDLIST
[06:03:16.663162][stream.hls][debug] Reloading playlist
...
[06:03:16.807616][stream.hls_playlist][all] #EXT-X-ENDLIST
[06:03:17.663268][stream.hls][debug] Reloading playlist
...
[06:03:17.855211][stream.hls_playlist][all] #EXT-X-ENDLIST
[06:03:18.663123][stream.hls][debug] Reloading playlist
...
[06:03:18.809816][stream.hls_playlist][all] #EXT-X-ENDLIST
[06:03:19.663106][stream.hls][debug] Reloading playlist
...
[06:03:19.841545][stream.hls_playlist][all] #EXT-X-ENDLIST
[06:03:20.663269][stream.hls][debug] Reloading playlist
...
[06:03:20.807515][stream.hls_playlist][all] #EXT-X-ENDLIST
[06:03:21.663114][stream.hls][debug] Reloading playlist
...
[06:03:21.845081][stream.hls_playlist][all] #EXT-X-ENDLIST
[06:03:22.663117][stream.hls][debug] Reloading playlist
...
[06:03:22.808568][stream.hls_playlist][all] #EXT-X-ENDLIST
[06:03:23.663105][stream.hls][debug] Reloading playlist
...
[06:03:23.839342][stream.hls_playlist][all] #EXT-X-ENDLIST
[06:03:24.663108][stream.hls][debug] Reloading playlist
...
[06:03:24.813164][stream.hls_playlist][all] #EXT-X-ENDLIST
[06:03:25.663126][stream.hls][debug] Reloading playlist
...
[06:03:25.840110][stream.hls_playlist][all] #EXT-X-ENDLIST
[06:03:26.663163][stream.hls][debug] Reloading playlist
...
[06:03:26.807925][stream.hls_playlist][all] #EXT-X-ENDLIST
[06:03:27.663105][stream.hls][debug] Reloading playlist
...
[06:03:27.840355][stream.hls_playlist][all] #EXT-X-ENDLIST
[06:03:28.663155][stream.hls][debug] Reloading playlist
...
[06:03:28.806406][stream.hls_playlist][all] #EXT-X-ENDLIST
[06:03:29.663111][stream.hls][debug] Reloading playlist
...
[06:03:29.847565][stream.hls_playlist][all] #EXT-X-ENDLIST
[06:03:30.663112][stream.hls][debug] Reloading playlist
...
[06:03:30.804173][stream.hls_playlist][all] #EXT-X-ENDLIST
[06:03:31.663100][stream.hls][debug] Reloading playlist
...
[06:03:31.839352][stream.hls_playlist][all] #EXT-X-ENDLIST
[06:03:32.663195][stream.hls][debug] Reloading playlist
...
[06:03:32.804562][stream.hls_playlist][all] #EXT-X-ENDLIST
[06:03:33.663118][stream.hls][debug] Reloading playlist
...
[06:03:33.842320][stream.hls_playlist][all] #EXT-X-ENDLIST
[06:03:34.663112][stream.hls][debug] Reloading playlist
...
[06:03:34.805074][stream.hls_playlist][all] #EXT-X-ENDLIST
[06:03:35.663097][stream.hls][debug] Reloading playlist
...
[06:03:35.804672][stream.hls_playlist][all] #EXT-X-ENDLIST
[06:03:36.663130][stream.hls][debug] Reloading playlist
[06:04:41.463081][stream.hls][warning] Failed to reload playlist: Unable to open URL: https://video-weaver.....m3u8 (404 Client Error: Not Found for url: https://video-weaver.....m3u8)
[06:04:41.463501][stream.hls][debug] Reloading playlist
[06:05:46.361899][stream.hls][warning] Failed to reload playlist: Unable to open URL: https://video-weaver.....m3u8 (404 Client Error: Not Found for url: https://video-weaver.....m3u8)
[06:05:46.383504][stream.hls][debug] Reloading playlist
[06:06:51.409273][stream.hls][warning] Failed to reload playlist: Unable to open URL: https://video-weaver.....m3u8 (404 Client Error: Not Found for url: https://video-weaver.....m3u8)
[06:06:51.409991][stream.hls][debug] Reloading playlist
[06:06:59.977777][stream.segmented][debug] Closing worker thread
[06:06:59.978834][stream.segmented][debug] Closing writer thread
[06:07:56.289789][stream.hls][warning] Failed to reload playlist: Unable to open URL: https://video-weaver.....m3u8 (404 Client Error: Not Found for url: https://video-weaver.....m3u8)
[06:07:56.311945][cli][info] Stream ended
error: Error when reading from stream: Read timeout, exiting
[06:07:56.316438][cli][info] Closing currently open stream...
```



Discussion:
Thinking about it, the logs immediately preceding the EXT-X-ENDLIST may be useful as well, I had originally cut them thinking they were redundant but stuff like the segment numbers may be useful, so here are those as well:

```log
[06:02:51.805036][stream.hls][debug] Adding segment 1813 to queue
[06:02:52.184364][stream.hls][debug] Writing segment 1813 to output
[06:02:52.187681][stream.hls][debug] Segment 1813 complete
[06:02:53.663803][stream.hls][debug] Reloading playlist
[06:02:53.793217][stream.hls_playlist][all] #EXT-X-VERSION:3
[06:02:53.793525][stream.hls_playlist][all] #EXT-X-TARGETDURATION:6
[06:02:53.793722][stream.hls_playlist][all] #EXT-X-MEDIA-SEQUENCE:1801
[06:02:53.793936][stream.hls_playlist][all] #EXT-X-TWITCH-LIVE-SEQUENCE:1801
...
[06:02:53.805896][stream.hls][debug] Adding segment 1814 to queue
[06:02:54.033768][stream.hls][debug] Writing segment 1814 to output
[06:02:54.042736][stream.hls][debug] Segment 1814 complete
[06:02:55.663795][stream.hls][debug] Reloading playlist
[06:02:55.793338][stream.hls_playlist][all] #EXT-X-VERSION:3
[06:02:55.793686][stream.hls_playlist][all] #EXT-X-TARGETDURATION:6
[06:02:55.793952][stream.hls_playlist][all] #EXT-X-MEDIA-SEQUENCE:1802
[06:02:55.794148][stream.hls_playlist][all] #EXT-X-TWITCH-LIVE-SEQUENCE:1802
...
[06:02:55.806166][stream.hls][debug] Adding segment 1815 to queue
[06:02:56.490534][stream.hls][debug] Writing segment 1815 to output
[06:02:56.496597][stream.hls][debug] Segment 1815 complete
[06:02:57.663801][stream.hls][debug] Reloading playlist
[06:02:57.792855][stream.hls_playlist][all] #EXT-X-VERSION:3
[06:02:57.793142][stream.hls_playlist][all] #EXT-X-TARGETDURATION:6
[06:02:57.793382][stream.hls_playlist][all] #EXT-X-MEDIA-SEQUENCE:1802
[06:02:57.793541][stream.hls_playlist][all] #EXT-X-TWITCH-LIVE-SEQUENCE:1802
...
[06:02:57.804605][stream.hls][debug] Adding segment 1816 to queue
[06:02:58.009763][stream.hls][debug] Writing segment 1816 to output
[06:02:58.012956][stream.hls][debug] Segment 1816 complete
[06:02:59.663139][stream.hls][debug] Reloading playlist
[06:02:59.792351][stream.hls_playlist][all] #EXT-X-VERSION:3
[06:02:59.792676][stream.hls_playlist][all] #EXT-X-TARGETDURATION:6
[06:02:59.792904][stream.hls_playlist][all] #EXT-X-MEDIA-SEQUENCE:1802
[06:02:59.793068][stream.hls_playlist][all] #EXT-X-TWITCH-LIVE-SEQUENCE:1802
...
[06:02:59.804388][stream.hls][debug] Adding segment 1817 to queue
[06:02:59.973558][stream.hls][debug] Writing segment 1817 to output
[06:02:59.974804][stream.hls][debug] Segment 1817 complete
[06:03:00.663144][stream.hls][debug] Reloading playlist
[06:03:00.794212][stream.hls_playlist][all] #EXT-X-VERSION:3
[06:03:00.794557][stream.hls_playlist][all] #EXT-X-TARGETDURATION:6
[06:03:00.794836][stream.hls_playlist][all] #EXT-X-MEDIA-SEQUENCE:1802
[06:03:00.795031][stream.hls_playlist][all] #EXT-X-TWITCH-LIVE-SEQUENCE:1802
...
[06:03:01.668640][stream.hls][debug] Reloading playlist
[06:03:01.800664][stream.hls_playlist][all] #EXT-X-VERSION:3
[06:03:01.801168][stream.hls_playlist][all] #EXT-X-TARGETDURATION:6
[06:03:01.801425][stream.hls_playlist][all] #EXT-X-MEDIA-SEQUENCE:1802
[06:03:01.801774][stream.hls_playlist][all] #EXT-X-TWITCH-LIVE-SEQUENCE:1802
...
[06:03:02.663152][stream.hls][debug] Reloading playlist
[06:03:03.067024][stream.hls_playlist][all] #EXT-X-VERSION:3
[06:03:03.067433][stream.hls_playlist][all] #EXT-X-TARGETDURATION:6
[06:03:03.067698][stream.hls_playlist][all] #EXT-X-MEDIA-SEQUENCE:1802
[06:03:03.067965][stream.hls_playlist][all] #EXT-X-TWITCH-LIVE-SEQUENCE:1802
...
[06:03:03.084164][stream.hls_playlist][all] #EXT-X-ENDLIST
[06:03:03.663126][stream.hls][debug] Reloading playlist
```
`EXT-X-ENDLIST` sets `m3u8.is_endlist` to `True`
https://github.com/streamlink/streamlink/blob/c6cd719369fa611b964545c22bca8f0c00d153d4/src/streamlink/stream/hls_playlist.py#L484-L490

When `m3u8.is_endlist == True`, set `playlist_end` to the final sequence number
https://github.com/streamlink/streamlink/blob/c6cd719369fa611b964545c22bca8f0c00d153d4/src/streamlink/stream/hls.py#L390-L391

`playlist_end` is checked here, inside the `playlist_sequences` iterator loop.
https://github.com/streamlink/streamlink/blob/c6cd719369fa611b964545c22bca8f0c00d153d4/src/streamlink/stream/hls.py#L470-L483

So the above case seems to be an edge case where `#EXT-X-ENDLIST` is appended to the playlist without any new segments being added, so the `playlist_end` check is never done.

It seems like `playlist_end` should be checked somewhere outside of the sequence iterator loop.
> So the above case seems to be an edge case where `#EXT-X-ENDLIST` is appended to the playlist without any new segments being added, so the `playlist_end` check is never done.

Yes, that seems to be correct. Thanks for checking.

Fixing this should be pretty straightforward. I'll have a look once I get the time. I'm currently not at my computer and will be busy first with fixing a Streamlink Twitch GUI emergency issue when I come home.

Resolving patch:
diff --git a/src/streamlink/stream/hls.py b/src/streamlink/stream/hls.py
--- a/src/streamlink/stream/hls.py
+++ b/src/streamlink/stream/hls.py
@@ -477,13 +477,15 @@ def iter_segments(self):
                     log.info(f"Stopping stream early after {self.duration_limit}")
                     return
 
-                # End of stream
-                stream_end = self.playlist_end is not None and sequence.num >= self.playlist_end
-                if self.closed or stream_end:
+                if self.closed:  # pragma: no cover
                     return
 
                 self.playlist_sequence = sequence.num + 1
 
+            # End of stream
+            if self.closed or self.playlist_end is not None and (not queued or self.playlist_sequence > self.playlist_end):
+                return
+
             if queued:
                 self.playlist_sequences_last = now()
             elif self._segment_queue_timing_threshold_reached():


Test patch:
diff --git a/tests/stream/test_hls.py b/tests/stream/test_hls.py
--- a/tests/stream/test_hls.py
+++ b/tests/stream/test_hls.py
@@ -142,7 +142,14 @@ def get_session(self, options=None, *args, **kwargs):
 
     def test_playlist_end(self):
         thread, segments = self.subject([
-            # media sequence = 0
+            Playlist(0, [Segment(0)], end=True),
+        ])
+
+        assert self.await_read(read_all=True) == self.content(segments), "Stream ends and read-all handshake doesn't time out"
+
+    def test_playlist_end_on_empty_reload(self):
+        thread, segments = self.subject([
+            Playlist(0, [Segment(0)]),
             Playlist(0, [Segment(0)], end=True),
         ])
 
@@ -377,6 +384,7 @@ def test_playlist_reload_offset(self) -> None:
             # time_completed = 00:00:01; time_elapsed = 1s
             assert worker.handshake_wait.wait_ready(1), "Arrives at first wait() call"
             assert worker.playlist_sequence == 1, "Has queued first segment"
+            assert worker.playlist_end is None, "Stream hasn't ended yet"
             assert worker.time_wait == 4.0, "Waits for 4 seconds out of the 5 seconds reload time"
             self.await_playlist_wait()
 
@@ -392,6 +400,7 @@ def test_playlist_reload_offset(self) -> None:
             # time_completed = 00:00:12; time_elapsed = 7s (exceeded 5s targetduration)
             assert worker.handshake_wait.wait_ready(1), "Arrives at second wait() call"
             assert worker.playlist_sequence == 2, "Has queued second segment"
+            assert worker.playlist_end is None, "Stream hasn't ended yet"
             assert worker.time_wait == 0.0, "Doesn't wait when reloading took too long"
             self.await_playlist_wait()
 
@@ -407,6 +416,7 @@ def test_playlist_reload_offset(self) -> None:
             # time_completed = 00:00:13; time_elapsed = 1s
             assert worker.handshake_wait.wait_ready(1), "Arrives at third wait() call"
             assert worker.playlist_sequence == 3, "Has queued third segment"
+            assert worker.playlist_end is None, "Stream hasn't ended yet"
             assert worker.time_wait == 4.0, "Waits for 4 seconds out of the 5 seconds reload time"
             self.await_playlist_wait()
 
@@ -422,6 +432,7 @@ def test_playlist_reload_offset(self) -> None:
             # time_completed = 00:00:17; time_elapsed = 0s
             assert worker.handshake_wait.wait_ready(1), "Arrives at fourth wait() call"
             assert worker.playlist_sequence == 4, "Has queued fourth segment"
+            assert worker.playlist_end is None, "Stream hasn't ended yet"
             assert worker.time_wait == 5.0, "Waits for the whole reload time"
             self.await_playlist_wait()
 
@@ -433,7 +444,7 @@ def test_playlist_reload_offset(self) -> None:
             self.await_playlist_reload()
             assert self.await_read(read_all=True) == self.content(segments)
             self.await_close()
-            assert worker.playlist_sequence == 4, "Doesn't update sequence number once ended"
+            assert worker.playlist_end == 4, "Stream has ended"
             assert not worker.handshake_wait.wait_ready(0), "Doesn't wait once ended"
             assert not worker.handshake_reload.wait_ready(0), "Doesn't reload playlist once ended"