Repository: streamlink/streamlink

Issue:
plugins.piczel: HLS URLs have changed
### Checklist

- [X] This is a [plugin issue](https://streamlink.github.io/plugins.html) 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 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

streamlink 6.4.1

### Description

Streams on Piczel have been broken since ~October 29th. Tested on the latest git master (0dda8ccf). Presumably Piczel rolled out a new HLS streaming setup that has changed the URLs. Streams result in the following error (note, the streamer in this example is marked NSFW):

```
[stream.hls][error] Unable to open URL: https://piczel.tv/hls/61316/index.m3u8 (502 Server Error: Bad Gateway for url: https://piczel.tv/hls/61316/index.m3u8)
[cli][error] Try 1/1: Could not open stream <HLSStream ['hls', 'https://piczel.tv/hls/61316/index.m3u8']> (No data returned from stream)
error: Could not open stream <HLSStream ['hls', 'https://piczel.tv/hls/61316/index.m3u8']>, tried 1 times, exiting
[cli][info] Closing currently open stream...
```

I took a quick glance at the current website version, and I see the following URLs in use now:

https://playback.piczel.tv/live/61316/llhls.m3u8?_HLS_legacy=YES

Which supplies separate audio and video streams, like so:

https://playback.piczel.tv/live/61316/chunklist_1_audio_61316_llhls.m3u8?_HLS_legacy=YES
https://playback.piczel.tv/live/61316/chunklist_0_video_61316_llhls.m3u8?_HLS_legacy=YES

I tried swapping those URLs into MuxedHLSStream, but it only played one chunk before exiting. I didn't see any other plugins using MuxedHLSStream in the current git master, so I'm not sure if I was just using it incorrectly or if there's something more complex going on.

### Debug log

```text
$ streamlink https://piczel.tv/watch/ranon best --loglevel debug ### NOTE: Streamer is marked as NSFW!
[cli][debug] OS:         Linux-6.6.1-arch1-1-x86_64-with-glibc2.38
[cli][debug] Python:     3.11.6
[cli][debug] OpenSSL:    OpenSSL 3.1.4 24 Oct 2023
[cli][debug] Streamlink: 6.4.1
[cli][debug] Dependencies:
[cli][debug]  certifi: 2023.11.17
[cli][debug]  isodate: 0.6.1
[cli][debug]  lxml: 4.9.2
[cli][debug]  pycountry: 22.3.5
[cli][debug]  pycryptodome: 3.19.0
[cli][debug]  PySocks: 1.7.1
[cli][debug]  requests: 2.31.0
[cli][debug]  trio: 0.23.1
[cli][debug]  trio-websocket: 0.11.1
[cli][debug]  typing-extensions: 4.8.0
[cli][debug]  urllib3: 1.26.18
[cli][debug]  websocket-client: 1.6.4
[cli][debug] Arguments:
[cli][debug]  url=https://piczel.tv/watch/ranon
[cli][debug]  stream=['best']
[cli][debug]  --loglevel=debug
[cli][info] Found matching plugin piczel for URL https://piczel.tv/watch/ranon
[cli][info] Available streams: live (worst, best)
[cli][info] Opening stream: live (hls)
[cli][info] Starting player: /usr/bin/vlc
[stream.hls][debug] Reloading playlist
[cli][debug] Pre-buffering 8192 bytes
[stream.hls][error] Unable to open URL: https://piczel.tv/hls/61316/index.m3u8 (502 Server Error: Bad Gateway for url: https://piczel.tv/hls/61316/index.m3u8)
[stream.segmented][debug] Closing worker thread
[stream.segmented][debug] Closing writer thread
[cli][error] Try 1/1: Could not open stream <HLSStream ['hls', 'https://piczel.tv/hls/61316/index.m3u8']> (No data returned from stream)
error: Could not open stream <HLSStream ['hls', 'https://piczel.tv/hls/61316/index.m3u8']>, tried 1 times, exiting
[cli][info] Closing currently open stream...
```



Discussion:
`/live/{id}/llhls.m3u8?_HLS_legacy=YES` is a multivariant playlist. The plugin previously only required direct media playlists, not multivariant playlists, hence the direct `HLSStream` class instantiation instead of the `HLSStream.parse_variant_playlist()` classmethod call.

`HLSStream.parse_variant_playlist()` automatically returns `MuxedHLSStream` instances if multiple media streams need to be muxed.

```diff
diff --git a/src/streamlink/plugins/piczel.py b/src/streamlink/plugins/piczel.py
index 80687e21..07df4c37 100644
--- a/src/streamlink/plugins/piczel.py
+++ b/src/streamlink/plugins/piczel.py
@@ -19,7 +19,7 @@ from streamlink.stream.hls import HLSStream
 ))
 class Piczel(Plugin):
     _URL_STREAMS = "https://piczel.tv/api/streams"
-    _URL_HLS = "https://piczel.tv/hls/{id}/index.m3u8"
+    _URL_HLS = "https://playback.piczel.tv/live/{id}/llhls.m3u8?_HLS_legacy=YES"
 
     def _get_streams(self):
         channel = self.match.group("channel")
@@ -57,7 +57,7 @@ class Piczel(Plugin):
         if not is_live:
             return
 
-        return {"live": HLSStream(self.session, self._URL_HLS.format(id=self.id))}
+        return HLSStream.parse_variant_playlist(self.session, self._URL_HLS.format(id=self.id))
 
 
 __plugin__ = Piczel
```

```
$ streamlink -l debug 'https://piczel.tv/watch/Feretta'
[cli][debug] OS:         Linux-6.6.2-1-git-x86_64-with-glibc2.38
[cli][debug] Python:     3.11.6
[cli][debug] OpenSSL:    OpenSSL 3.1.4 24 Oct 2023
[cli][debug] Streamlink: 6.4.1+0.g0dda8ccf.dirty
[cli][debug] Dependencies:
[cli][debug]  certifi: 2023.11.17
[cli][debug]  isodate: 0.6.1
[cli][debug]  lxml: 4.9.3
[cli][debug]  pycountry: 22.3.5
[cli][debug]  pycryptodome: 3.19.0
[cli][debug]  PySocks: 1.7.1
[cli][debug]  requests: 2.31.0
[cli][debug]  trio: 0.23.1
[cli][debug]  trio-websocket: 0.11.1
[cli][debug]  typing-extensions: 4.8.0
[cli][debug]  urllib3: 2.1.0
[cli][debug]  websocket-client: 1.6.4
[cli][debug] Arguments:
[cli][debug]  url=https://piczel.tv/watch/Feretta
[cli][debug]  --loglevel=debug
[cli][debug]  --player=mpv
[cli][info] Found matching plugin piczel for URL https://piczel.tv/watch/Feretta
[utils.l10n][debug] Language code: en_US
[stream.ffmpegmux][debug] ffmpeg version n6.1 Copyright (c) 2000-2023 the FFmpeg developers
[stream.ffmpegmux][debug]  built with gcc 13.2.1 (GCC) 20230801
[stream.ffmpegmux][debug]  configuration: --prefix=/usr --disable-debug --disable-static --disable-stripping --enable-amf --enable-avisynth --enable-cuda-llvm --enable-lto --enable-fontconfig --enable-gmp --enable-gnutls --enable-gpl --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libdav1d --enable-libdrm --enable-libfreetype --enable-libfribidi --enable-libgsm --enable-libiec61883 --enable-libjack --enable-libjxl --enable-libmodplug --enable-libmp3lame --enable-libopencore_amrnb --enable-libopencore_amrwb --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librav1e --enable-librsvg --enable-libsoxr --enable-libspeex --enable-libsrt --enable-libssh --enable-libsvtav1 --enable-libtheora --enable-libv4l2 --enable-libvidstab --enable-libvmaf --enable-libvorbis --enable-libvpl --enable-libvpx --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxcb --enable-libxml2 --enable-libxvid --enable-libzimg --enable-nvdec --enable-nvenc --enable-opencl --enable-opengl --enable-shared --enable-version3 --enable-vulkan
[stream.ffmpegmux][debug]  libavutil      58. 29.100 / 58. 29.100
[stream.ffmpegmux][debug]  libavcodec     60. 31.102 / 60. 31.102
[stream.ffmpegmux][debug]  libavformat    60. 16.100 / 60. 16.100
[stream.ffmpegmux][debug]  libavdevice    60.  3.100 / 60.  3.100
[stream.ffmpegmux][debug]  libavfilter     9. 12.100 /  9. 12.100
[stream.ffmpegmux][debug]  libswscale      7.  5.100 /  7.  5.100
[stream.ffmpegmux][debug]  libswresample   4. 12.100 /  4. 12.100
[stream.ffmpegmux][debug]  libpostproc    57.  3.100 / 57.  3.100
[stream.hls][debug] Using external audio tracks for stream 1080p (language=None, name=Audio_1)
Available streams: 1080p (worst, best)
```

However, there appears to be a muxing issue with their HLS streams:

```
[cli][info] Opening stream: 1080p (hls-multi)
[cli][info] Starting player: mpv
[stream.ffmpegmux][debug] Opening hls substream
[stream.hls][debug] Reloading playlist
[stream.ffmpegmux][debug] Opening hls substream
[stream.hls][debug] Reloading playlist
[utils.named_pipe][info] Creating pipe streamlinkpipe-97906-1-1996
[utils.named_pipe][info] Creating pipe streamlinkpipe-97906-2-3415
[stream.ffmpegmux][debug] ffmpeg command: /usr/bin/ffmpeg -nostats -y -i /tmp/streamlinkpipe-97906-1-1996 -i /tmp/streamlinkpipe-97906-2-3415 -c:v copy -c:a copy -map 0:v? -map 0:a? -map 1:a -f mpegts pipe:1
[stream.ffmpegmux][debug] Starting copy to pipe: /tmp/streamlinkpipe-97906-1-1996
[stream.ffmpegmux][debug] Starting copy to pipe: /tmp/streamlinkpipe-97906-2-3415
[cli][debug] Pre-buffering 8192 bytes
[stream.hls][debug] First Sequence: 0; Last Sequence: 1617
[stream.hls][debug] Start offset: 0; Duration: None; Start Sequence: 1615; End Sequence: None
[stream.hls][debug] Adding segment 1615 to queue
[stream.hls][debug] Adding segment 1616 to queue
[stream.hls][debug] Adding segment 1617 to queue
[stream.hls][debug] Writing segment 1615 to output
[stream.hls][debug] Segment initialization 1615 complete
[stream.hls][debug] First Sequence: 0; Last Sequence: 1617
[stream.hls][debug] Start offset: 0; Duration: None; Start Sequence: 1615; End Sequence: None
[stream.hls][debug] Adding segment 1615 to queue
[stream.hls][debug] Adding segment 1616 to queue
[stream.hls][debug] Adding segment 1617 to queue
[stream.hls][debug] Writing segment 1615 to output
[stream.hls][debug] Segment initialization 1615 complete
[stream.hls][debug] Writing segment 1615 to output
[stream.hls][debug] Segment 1615 complete
[stream.hls][debug] Writing segment 1616 to output
[stream.hls][debug] Segment initialization 1616 complete
[stream.hls][debug] Writing segment 1615 to output
[stream.hls][debug] Segment 1615 complete
[stream.hls][debug] Writing segment 1616 to output
[stream.hls][debug] Segment initialization 1616 complete
[cli.output][debug] Opening subprocess: ['/home/basti/.local/bin/mpv', '--force-media-title=https://piczel.tv/watch/Feretta', '-']
[stream.hls][debug] Writing segment 1616 to output
[stream.hls][debug] Segment 1616 complete
[stream.hls][debug] Writing segment 1617 to output
[stream.hls][debug] Segment initialization 1617 complete
[stream.hls][debug] Writing segment 1617 to output
[stream.hls][debug] Segment 1617 complete
[stream.hls][debug] Writing segment 1616 to output
[stream.hls][debug] Segment 1616 complete
[stream.hls][debug] Writing segment 1617 to output
[stream.hls][debug] Segment initialization 1617 complete
[stream.hls][debug] Writing segment 1617 to output
[stream.hls][debug] Segment 1617 complete
[cli][debug] Writing stream to output
[stream.ffmpegmux][error] Error while writing to pipe /tmp/streamlinkpipe-97906-1-1996: [Errno 32] Broken pipe
[stream.ffmpegmux][error] Error while writing to pipe /tmp/streamlinkpipe-97906-2-3415: [Errno 32] Broken pipe
[stream.ffmpegmux][debug] Closing ffmpeg thread
[stream.segmented][debug] Closing worker thread
[stream.segmented][debug] Closing writer thread
[stream.segmented][debug] Closing worker thread
[stream.segmented][debug] Closing writer thread
[stream.ffmpegmux][debug] Closed all the substreams
[cli][info] Stream ended
[cli][info] Closing currently open stream...
```
Just had a quick look at this again...

After checking FFmpeg's error output using `--ffmpeg-verbose` it looks like it's having issues with Streamlink's output when applying HLS segment maps, aka initialization sections. Those are currently written to the output on each new segment, which is technically wrong, at least for fragmented mp4 segments (still not sure about MPEG-TS segments, but it's very likely the case here as well). Players like VLC and MPV simply skip those duplicate init sections in the output stream, so the current implementation has been working fine in regular unmuxed output streams, but FFmpeg stops remuxing both video and audio streams, so only the first segment works. See #4310.

If I apply a dirty hack that prevents duplicate init sections, then the muxing and final stream output works just fine.

I was in the process of rewriting the segment map logic. See my comment in #5601. This is now a requirement for the plugin to work again. Rewriting this however requires re-organizing the `HLSSegment`, `HLSStreamWorker` and `HLSStreamWriter` classes, because init sections need to be cleared on discontinuities.

Resolving patch:
diff --git a/src/streamlink/plugins/piczel.py b/src/streamlink/plugins/piczel.py
--- a/src/streamlink/plugins/piczel.py
+++ b/src/streamlink/plugins/piczel.py
@@ -19,7 +19,7 @@
 ))
 class Piczel(Plugin):
     _URL_STREAMS = "https://piczel.tv/api/streams"
-    _URL_HLS = "https://piczel.tv/hls/{id}/index.m3u8"
+    _URL_HLS = "https://playback.piczel.tv/live/{id}/llhls.m3u8?_HLS_legacy=YES"
 
     def _get_streams(self):
         channel = self.match.group("channel")
@@ -57,7 +57,7 @@ def _get_streams(self):
         if not is_live:
             return
 
-        return {"live": HLSStream(self.session, self._URL_HLS.format(id=self.id))}
+        return HLSStream.parse_variant_playlist(self.session, self._URL_HLS.format(id=self.id))
 
 
 __plugin__ = Piczel


Test patch: