Repository: streamlink/streamlink

Issue:
plugins.Pluto: Freezes at commercials and plays no audio on TV Shows
### 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

Latest build from the master branch

### Description

Hello! Pluto TV plugin have problems. The plugin plays fine, but it freezes at commercials and some TV shows don't have commercials, it plays no audio because it thinks it at a commercial. This happens on all Pluto TV channels.

### Debug log

```text
C:\Windows\system32>streamlink -l debug https://pluto.tv/live-tv/forever-kids best
[cli][debug] OS:         Windows 10
[cli][debug] Python:     3.11.5
[cli][debug] Streamlink: 6.1.0
[cli][debug] Dependencies:
[cli][debug]  certifi: 2023.7.22
[cli][debug]  isodate: 0.6.1
[cli][debug]  lxml: 4.9.3
[cli][debug]  pycountry: 22.3.5
[cli][debug]  pycryptodome: 3.18.0
[cli][debug]  PySocks: 1.7.1
[cli][debug]  requests: 2.31.0
[cli][debug]  trio: 0.22.2
[cli][debug]  trio-websocket: 0.10.3
[cli][debug]  typing-extensions: 4.7.1
[cli][debug]  urllib3: 2.0.4
[cli][debug]  websocket-client: 1.6.1
[cli][debug] Arguments:
[cli][debug]  url=https://pluto.tv/live-tv/forever-kids
[cli][debug]  stream=['best']
[cli][debug]  --loglevel=debug
[cli][debug]  --ffmpeg-ffmpeg=C:\Program Files (x86)\Streamlink\ffmpeg\ffmpeg.exe
[cli][info] Found matching plugin pluto for URL https://pluto.tv/live-tv/forever-kids
[plugins.pluto][debug] slug=forever-kids
[plugins.pluto][debug] app_version=7.7.0-18f7ab32608969ea5bcbce8d0e23b9d0e1b24717
[stream.hls][warning] Encountered a stream discontinuity. This is unsupported and will result in incoherent output data.
```



Discussion:
> it freezes at commercials

Which is intended behavior of this plugin's custom HLS implementation, as you can see here:
https://github.com/streamlink/streamlink/blame/6.1.0/src/streamlink/plugins/pluto.py#L21-L25

> it plays no audio

Try using a different player. These are unmuxed streams, so the output will be exactly the same as the site provides. Stream discontinuities cause issues with the playback. The warning message tells you that the output is incoherent.

> [stream.hls][warning] Encountered a stream discontinuity. This is unsupported and will result in incoherent output data.

This comes from the fact that segments get filtered out.
See #4535.

> some TV shows don't have commercials

I can't judge this since I don't use the site/plugin, but the filtering segment URL regex linked above doesn't look like it would do that.

Post the full log output of those streams then. The log output you've posted is incomplete, despite the issue template explicitly telling you not to remove stuff.

Setting the log level to `all` ([`--loglevel=all`](https://streamlink.github.io/cli.html#cmdoption-loglevel)) will show you the contents of each HLS playlist request, which is very verbose, but it'll let you see which segments will get filtered out.
Had another look and it seems like the plugin is incorrectly setting query string parameters on the HLS playlist request, which results in the streaming server returning different content.

This here is incorrect:
https://github.com/streamlink/streamlink/blame/6.1.0/src/streamlink/plugins/pluto.py#L114-L115

It should be `dict(parse_qsl(...))`.

Resolving patch:
diff --git a/src/streamlink/plugins/pluto.py b/src/streamlink/plugins/pluto.py
--- a/src/streamlink/plugins/pluto.py
+++ b/src/streamlink/plugins/pluto.py
@@ -10,7 +10,7 @@
 
 import logging
 import re
-from urllib.parse import parse_qs, urljoin
+from urllib.parse import parse_qsl, urljoin
 from uuid import uuid4
 
 from streamlink.plugin import Plugin, pluginmatcher
@@ -23,7 +23,7 @@
 
 
 class PlutoHLSStreamWriter(HLSStreamWriter):
-    ad_re = re.compile(r"_ad/creative/|dai\.google\.com|Pluto_TV_OandO/.*Bumper")
+    ad_re = re.compile(r"_ad/creative/|dai\.google\.com|Pluto_TV_OandO/.*(Bumper|plutotv_filler)")
 
     def should_filter_sequence(self, sequence):
         return self.ad_re.search(sequence.segment.uri) is not None or super().should_filter_sequence(sequence)
@@ -115,9 +115,13 @@ def _get_api_data(self, kind, slug, slugfilter=None):
         )
 
     def _get_playlist(self, host, path, params, token):
-        qs = parse_qs(params)
-        qs["jwt"] = token
-        yield from PlutoHLSStream.parse_variant_playlist(self.session, update_qsd(urljoin(host, path), qs)).items()
+        qsd = dict(parse_qsl(params))
+        qsd["jwt"] = token
+
+        url = urljoin(host, path)
+        url = update_qsd(url, qsd)
+
+        return PlutoHLSStream.parse_variant_playlist(self.session, url)
 
     @staticmethod
     def _get_media_data(data, key, slug):


Test patch: