Repository: streamlink/streamlink

Issue:
plugins.twitch: ad filtering via `--twitch-disable-ads` (partially) broken again (5.1.1)
### 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 build from the master branch

### Description

There appears to be new change on Twitch's end which breaks the current implementation of the plugin's ad filtering via [`--twitch-disable-ads`](https://streamlink.github.io/cli.html#cmdoption-twitch-disable-ads), so the ads / "purple screen" gets incorrectly included in Streamlink's output.

**Please read the meta thread before reading or commenting:**
https://github.com/streamlink/streamlink/issues/4949

----

This might be caused by an incorrect extrapolation of the start time of ads when there are more than two prefetch segments. I'll need to have a closer look and reproduce this in tests with the data from the actual HLS playlists.

### Debug log

```text
[20:48:36.680641][plugins.twitch][info] Will skip ad segments
[20:48:36.680696][plugins.twitch][info] Low latency streaming (HLS live edge: 2)
[20:48:36.681417][stream.hls][debug] Reloading playlist
[20:48:36.681535][cli][debug] Pre-buffering 8192 bytes
[20:48:37.138190][stream.hls_playlist][all] #EXT-X-VERSION:3
[20:48:37.138292][stream.hls_playlist][all] #EXT-X-TARGETDURATION:6
[20:48:37.138344][stream.hls_playlist][all] #EXT-X-MEDIA-SEQUENCE:5760
[20:48:37.138386][stream.hls_playlist][all] #EXT-X-TWITCH-LIVE-SEQUENCE:5760
[20:48:37.138426][stream.hls_playlist][all] #EXT-X-TWITCH-ELAPSED-SECS:10040.433
[20:48:37.138463][stream.hls_playlist][all] #EXT-X-TWITCH-TOTAL-SECS:10074.084
[20:48:37.138500][stream.hls_playlist][all] #EXT-X-DATERANGE:ID="playlist-creation-1670010517",CLASS="timestamp",START-DATE="2022-12-02T11:48:37.128-08:00",END-ON-NEXT=YES,X-SERVER-TIME="1670010517.13"
[20:48:37.142943][stream.hls_playlist][all] #EXT-X-DATERANGE:ID="playlist-session-1670010517",CLASS="twitch-session",START-DATE="2022-12-02T11:48:37.128-08:00",END-ON-NEXT=YES,X-TV-TWITCH-SESSIONID="8657890014260199662"
[20:48:37.143064][stream.hls_playlist][all] #EXT-X-DATERANGE:ID="source-1670010487",CLASS="twitch-stream-source",START-DATE="2022-12-02T19:48:07.117Z",END-ON-NEXT=YES,X-TV-TWITCH-STREAM-SOURCE="live"
[20:48:37.143167][stream.hls_playlist][all] #EXT-X-DATERANGE:ID="trigger-1670010487",CLASS="twitch-trigger",START-DATE="2022-12-02T19:48:07.117Z",END-ON-NEXT=YES,X-TV-TWITCH-TRIGGER-URL="https://video-weaver.fra05.hls.ttvnw.net/trigger/SOME-BASE64-TOKEN"
[20:48:37.143278][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2022-12-02T19:48:07.117Z
[20:48:37.143345][stream.hls_playlist][all] #EXTINF:2.000,live
[20:48:37.143409][stream.hls_playlist][all] https://video-edge-469c16.muc01.abs.hls.ttvnw.net/v1/segment/segment.ts
[20:48:37.143489][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2022-12-02T19:48:09.117Z
[20:48:37.143558][stream.hls_playlist][all] #EXTINF:1.750,live
[20:48:37.143615][stream.hls_playlist][all] https://video-edge-469c16.muc01.abs.hls.ttvnw.net/v1/segment/segment.ts
[20:48:37.143689][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2022-12-02T19:48:10.867Z
[20:48:37.143758][stream.hls_playlist][all] #EXTINF:2.000,live
[20:48:37.143816][stream.hls_playlist][all] https://video-edge-469c16.muc01.abs.hls.ttvnw.net/v1/segment/segment.ts
[20:48:37.143888][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2022-12-02T19:48:12.867Z
[20:48:37.143957][stream.hls_playlist][all] #EXTINF:2.000,live
[20:48:37.144013][stream.hls_playlist][all] https://video-edge-469c16.muc01.abs.hls.ttvnw.net/v1/segment/segment.ts
[20:48:37.144079][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2022-12-02T19:48:14.867Z
[20:48:37.144144][stream.hls_playlist][all] #EXTINF:2.000,live
[20:48:37.144208][stream.hls_playlist][all] https://video-edge-469c16.muc01.abs.hls.ttvnw.net/v1/segment/segment.ts
[20:48:37.144275][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2022-12-02T19:48:16.867Z
[20:48:37.144341][stream.hls_playlist][all] #EXTINF:2.000,live
[20:48:37.144399][stream.hls_playlist][all] https://video-edge-469c16.muc01.abs.hls.ttvnw.net/v1/segment/segment.ts
[20:48:37.144466][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2022-12-02T19:48:18.867Z
[20:48:37.144538][stream.hls_playlist][all] #EXTINF:1.800,live
[20:48:37.144598][stream.hls_playlist][all] https://video-edge-469c16.muc01.abs.hls.ttvnw.net/v1/segment/segment.ts
[20:48:37.144666][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2022-12-02T19:48:20.667Z
[20:48:37.144730][stream.hls_playlist][all] #EXTINF:1.283,live
[20:48:37.144788][stream.hls_playlist][all] https://video-edge-469c16.muc01.abs.hls.ttvnw.net/v1/segment/segment.ts
[20:48:37.144868][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2022-12-02T19:48:21.950Z
[20:48:37.144936][stream.hls_playlist][all] #EXTINF:2.350,live
[20:48:37.144996][stream.hls_playlist][all] https://video-edge-469c16.muc01.abs.hls.ttvnw.net/v1/segment/segment.ts
[20:48:37.145063][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2022-12-02T19:48:24.300Z
[20:48:37.145130][stream.hls_playlist][all] #EXTINF:1.184,live
[20:48:37.145188][stream.hls_playlist][all] https://video-edge-469c16.muc01.abs.hls.ttvnw.net/v1/segment/segment.ts
[20:48:37.145257][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2022-12-02T19:48:25.484Z
[20:48:37.145323][stream.hls_playlist][all] #EXTINF:2.000,live
[20:48:37.145382][stream.hls_playlist][all] https://video-edge-469c16.muc01.abs.hls.ttvnw.net/v1/segment/segment.ts
[20:48:37.145450][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2022-12-02T19:48:27.484Z
[20:48:37.145515][stream.hls_playlist][all] #EXTINF:2.000,live
[20:48:37.145574][stream.hls_playlist][all] https://video-edge-469c16.muc01.abs.hls.ttvnw.net/v1/segment/segment.ts
[20:48:37.145641][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2022-12-02T19:48:29.484Z
[20:48:37.145707][stream.hls_playlist][all] #EXTINF:2.000,live
[20:48:37.145765][stream.hls_playlist][all] https://video-edge-469c16.muc01.abs.hls.ttvnw.net/v1/segment/segment.ts
[20:48:37.145838][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2022-12-02T19:48:31.484Z
[20:48:37.145903][stream.hls_playlist][all] #EXTINF:1.033,live
[20:48:37.145961][stream.hls_playlist][all] https://video-edge-469c16.muc01.abs.hls.ttvnw.net/v1/segment/segment.ts
[20:48:37.146026][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2022-12-02T19:48:32.517Z
[20:48:37.146089][stream.hls_playlist][all] #EXTINF:1.600,live
[20:48:37.146143][stream.hls_playlist][all] https://video-edge-469c16.muc01.abs.hls.ttvnw.net/v1/segment/segment.ts
[20:48:37.146212][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2022-12-02T19:48:34.117Z
[20:48:37.146276][stream.hls_playlist][all] #EXTINF:2.217,live
[20:48:37.146329][stream.hls_playlist][all] https://video-edge-469c16.muc01.abs.hls.ttvnw.net/v1/segment/segment.ts
[20:48:37.146403][stream.hls_playlist][all] #EXT-X-TWITCH-PREFETCH:https://video-edge-469c16.muc01.abs.hls.ttvnw.net/v1/segment/segment.ts
[20:48:37.146500][stream.hls_playlist][all] #EXT-X-TWITCH-PREFETCH:https://video-edge-469c16.muc01.abs.hls.ttvnw.net/v1/segment/segment.ts
[20:48:37.146632][stream.hls][debug] First Sequence: 5760; Last Sequence: 5777
[20:48:37.146681][stream.hls][debug] Start offset: 0; Duration: None; Start Sequence: 5776; End Sequence: None
[20:48:37.146723][stream.hls][debug] Adding segment 5776 to queue
[20:48:37.147975][stream.hls][debug] Adding segment 5777 to queue
[20:48:37.303129][stream.hls][debug] Writing segment 5776 to output
[20:48:37.305258][cli.output][debug] Opening subprocess: mpv
[20:48:37.806454][cli][debug] Writing stream to output
[20:48:38.087492][stream.hls][debug] Segment 5776 complete
[20:48:38.133442][stream.hls][debug] Writing segment 5777 to output
[20:48:38.975280][stream.hls][debug] Reloading playlist
[20:48:39.026840][stream.hls_playlist][all] #EXT-X-VERSION:3
[20:48:39.026920][stream.hls_playlist][all] #EXT-X-TARGETDURATION:6
[20:48:39.026969][stream.hls_playlist][all] #EXT-X-MEDIA-SEQUENCE:5761
[20:48:39.027010][stream.hls_playlist][all] #EXT-X-TWITCH-LIVE-SEQUENCE:5761
[20:48:39.027049][stream.hls_playlist][all] #EXT-X-TWITCH-ELAPSED-SECS:10042.433
[20:48:39.027086][stream.hls_playlist][all] #EXT-X-TWITCH-TOTAL-SECS:10077.316
[20:48:39.027124][stream.hls_playlist][all] #EXT-X-DATERANGE:ID="playlist-creation-1670010517",CLASS="timestamp",START-DATE="2022-12-02T19:48:37.128Z",END-ON-NEXT=YES,X-SERVER-TIME="1670010517.13"
[20:48:39.027235][stream.hls_playlist][all] #EXT-X-DATERANGE:ID="playlist-session-1670010517",CLASS="twitch-session",START-DATE="2022-12-02T19:48:37.128Z",END-ON-NEXT=YES,X-TV-TWITCH-SESSIONID="8657890014260199662"
[20:48:39.027339][stream.hls_playlist][all] #EXT-X-DATERANGE:ID="source-1670010487",CLASS="twitch-stream-source",START-DATE="2022-12-02T19:48:07.117Z",END-ON-NEXT=YES,X-TV-TWITCH-STREAM-SOURCE="live"
[20:48:39.027437][stream.hls_playlist][all] #EXT-X-DATERANGE:ID="trigger-1670010487",CLASS="twitch-trigger",START-DATE="2022-12-02T19:48:07.117Z",END-ON-NEXT=YES,X-TV-TWITCH-TRIGGER-URL="https://video-weaver.fra05.hls.ttvnw.net/trigger/SOME-BASE64-TOKEN"
[20:48:39.027542][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2022-12-02T19:48:09.117Z
[20:48:39.027608][stream.hls_playlist][all] #EXTINF:1.750,live
[20:48:39.027669][stream.hls_playlist][all] https://video-edge-469c16.muc01.abs.hls.ttvnw.net/v1/segment/segment.ts
[20:48:39.027744][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2022-12-02T19:48:10.867Z
[20:48:39.027810][stream.hls_playlist][all] #EXTINF:2.000,live
[20:48:39.027874][stream.hls_playlist][all] https://video-edge-469c16.muc01.abs.hls.ttvnw.net/v1/segment/segment.ts
[20:48:39.027949][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2022-12-02T19:48:12.867Z
[20:48:39.028015][stream.hls_playlist][all] #EXTINF:2.000,live
[20:48:39.028069][stream.hls_playlist][all] https://video-edge-469c16.muc01.abs.hls.ttvnw.net/v1/segment/segment.ts
[20:48:39.028136][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2022-12-02T19:48:14.867Z
[20:48:39.028207][stream.hls_playlist][all] #EXTINF:2.000,live
[20:48:39.028262][stream.hls_playlist][all] https://video-edge-469c16.muc01.abs.hls.ttvnw.net/v1/segment/segment.ts
[20:48:39.028328][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2022-12-02T19:48:16.867Z
[20:48:39.028393][stream.hls_playlist][all] #EXTINF:2.000,live
[20:48:39.028450][stream.hls_playlist][all] https://video-edge-469c16.muc01.abs.hls.ttvnw.net/v1/segment/segment.ts
[20:48:39.028521][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2022-12-02T19:48:18.867Z
[20:48:39.028586][stream.hls_playlist][all] #EXTINF:1.800,live
[20:48:39.028643][stream.hls_playlist][all] https://video-edge-469c16.muc01.abs.hls.ttvnw.net/v1/segment/segment.ts
[20:48:39.028710][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2022-12-02T19:48:20.667Z
[20:48:39.028774][stream.hls_playlist][all] #EXTINF:1.283,live
[20:48:39.028831][stream.hls_playlist][all] https://video-edge-469c16.muc01.abs.hls.ttvnw.net/v1/segment/segment.ts
[20:48:39.028890][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2022-12-02T19:48:21.950Z
[20:48:39.028953][stream.hls_playlist][all] #EXTINF:2.350,live
[20:48:39.029011][stream.hls_playlist][all] https://video-edge-469c16.muc01.abs.hls.ttvnw.net/v1/segment/segment.ts
[20:48:39.029070][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2022-12-02T19:48:24.300Z
[20:48:39.029133][stream.hls_playlist][all] #EXTINF:1.184,live
[20:48:39.029190][stream.hls_playlist][all] https://video-edge-469c16.muc01.abs.hls.ttvnw.net/v1/segment/segment.ts
[20:48:39.029249][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2022-12-02T19:48:25.484Z
[20:48:39.029312][stream.hls_playlist][all] #EXTINF:2.000,live
[20:48:39.029370][stream.hls_playlist][all] https://video-edge-469c16.muc01.abs.hls.ttvnw.net/v1/segment/segment.ts
[20:48:39.029429][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2022-12-02T19:48:27.484Z
[20:48:39.029491][stream.hls_playlist][all] #EXTINF:2.000,live
[20:48:39.029549][stream.hls_playlist][all] https://video-edge-469c16.muc01.abs.hls.ttvnw.net/v1/segment/segment.ts
[20:48:39.029609][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2022-12-02T19:48:29.484Z
[20:48:39.029670][stream.hls_playlist][all] #EXTINF:2.000,live
[20:48:39.029728][stream.hls_playlist][all] https://video-edge-469c16.muc01.abs.hls.ttvnw.net/v1/segment/segment.ts
[20:48:39.029788][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2022-12-02T19:48:31.484Z
[20:48:39.029850][stream.hls_playlist][all] #EXTINF:1.033,live
[20:48:39.029907][stream.hls_playlist][all] https://video-edge-469c16.muc01.abs.hls.ttvnw.net/v1/segment/segment.ts
[20:48:39.029967][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2022-12-02T19:48:32.517Z
[20:48:39.030029][stream.hls_playlist][all] #EXTINF:1.600,live
[20:48:39.030087][stream.hls_playlist][all] https://video-edge-469c16.muc01.abs.hls.ttvnw.net/v1/segment/segment.ts
[20:48:39.030147][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2022-12-02T19:48:34.117Z
[20:48:39.030209][stream.hls_playlist][all] #EXTINF:2.217,live
[20:48:39.030262][stream.hls_playlist][all] https://video-edge-469c16.muc01.abs.hls.ttvnw.net/v1/segment/segment.ts
[20:48:39.030326][stream.hls_playlist][all] #EXT-X-PROGRAM-DATE-TIME:2022-12-02T19:48:36.334Z
[20:48:39.030390][stream.hls_playlist][all] #EXTINF:1.833,live
[20:48:39.030445][stream.hls_playlist][all] https://video-edge-469c16.muc01.abs.hls.ttvnw.net/v1/segment/segment.ts
[20:48:39.030513][stream.hls_playlist][all] #EXT-X-TWITCH-PREFETCH:https://video-edge-469c16.muc01.abs.hls.ttvnw.net/v1/segment/segment.ts
[20:48:39.030596][stream.hls_playlist][all] #EXT-X-DATERANGE:ID="stitched-ad-1670010520-30168000000",CLASS="twitch-stitched-ad",START-DATE="2022-12-02T19:48:40.768Z",DURATION=30.168,X-TV-TWITCH-AD-CLICK-TRACKING-URL="https://example.com",X-TV-TWITCH-AD-LOUDNESS="-10.980000",X-TV-TWITCH-AD-POD-POSITION="0",X-TV-TWITCH-AD-ROLL-TYPE="MIDROLL",X-TV-TWITCH-AD-RADS-TOKEN="SOME-BASE64-TOKEN",X-TV-TWITCH-AD-CLICK-BEACON-ID="clickDropBeacon",X-TV-TWITCH-AD-CREATIVE-ID="SOME-ID",X-TV-TWITCH-AD-AD-FORMAT="standard_video_ad",X-TV-TWITCH-AD-COMMERCIAL-ID="SOME-COMMERCIAL-ID",X-TV-TWITCH-AD-POD-LENGTH="1",X-TV-TWITCH-AD-AD-SESSION-ID="SOME-ID",X-TV-TWITCH-AD-URL="https://help.twitch.tv/s/article/ad-experience-on-twitch",X-TV-TWITCH-AD-LINE-ITEM-ID="SOME-ID"
[20:48:39.030723][stream.hls_playlist][all] #EXT-X-DATERANGE:ID="source-1670010520",CLASS="twitch-stream-source",START-DATE="2022-12-02T19:48:40.768Z",END-ON-NEXT=YES,X-TV-TWITCH-STREAM-SOURCE="Amazon|2474283100494"
[20:48:39.030809][stream.hls_playlist][all] #EXT-X-DATERANGE:ID="quartile-1670010520-0",CLASS="twitch-ad-quartile",START-DATE="2022-12-02T19:48:40.768Z",DURATION=2.000,X-TV-TWITCH-AD-QUARTILE="0"
[20:48:39.030893][stream.hls_playlist][all] #EXT-X-DISCONTINUITY
[20:48:39.030950][stream.hls_playlist][all] #EXT-X-TWITCH-PREFETCH:https://video-edge-469c16.muc01.abs.hls.ttvnw.net/v1/segment/segment.ts
[20:48:39.031039][stream.hls_playlist][all] #EXT-X-TWITCH-PREFETCH:https://video-edge-469c16.muc01.abs.hls.ttvnw.net/v1/segment/segment.ts
[20:48:39.031171][stream.hls][debug] Adding segment 5778 to queue
[20:48:39.031268][stream.hls][debug] Adding segment 5779 to queue
[20:48:40.567511][stream.hls][debug] Segment 5777 complete
[20:48:40.567671][stream.hls][debug] Writing segment 5778 to output
[20:48:40.570485][stream.hls][debug] Segment 5778 complete
[20:48:40.570615][stream.hls][debug] Discarding segment 5779
```



Discussion:
The problem on this stream is that the regular segments have a variable durations, so when prefetch segments are included, those durations are unknown and the start times of the following prefetch segments therefore have to be extrapolated. The ad segment is annotated, but due to the extrapolation, there can be an incorrect offset.

So either the ad start time needs to be checked differently, or the included discontinuity tag needs to be used, which it is currently not (by any of Streamlink's HLS implementations).

Resolving patch:
diff --git a/src/streamlink/plugins/twitch.py b/src/streamlink/plugins/twitch.py
--- a/src/streamlink/plugins/twitch.py
+++ b/src/streamlink/plugins/twitch.py
@@ -75,12 +75,15 @@ def parse_tag_ext_x_twitch_prefetch(self, value):
         # Use the last duration for extrapolating the start time of the prefetch segment, which is needed for checking
         # whether it is an ad segment and matches the parsed date ranges or not
         date = last.date + timedelta(seconds=last.duration)
-        ad = self._is_segment_ad(date)
+        # Don't pop() the discontinuity state in prefetch segments (at the bottom of the playlist)
+        discontinuity = self.state.get("discontinuity", False)
+        # Always treat prefetch segments after a discontinuity as ad segments
+        ad = discontinuity or self._is_segment_ad(date)
         segment = last._replace(
             uri=self.uri(value),
             duration=duration,
             title=None,
-            discontinuity=self.state.pop("discontinuity", False),
+            discontinuity=discontinuity,
             date=date,
             ad=ad,
             prefetch=True,


Test patch:
diff --git a/tests/plugins/test_twitch.py b/tests/plugins/test_twitch.py
--- a/tests/plugins/test_twitch.py
+++ b/tests/plugins/test_twitch.py
@@ -330,34 +330,37 @@ def test_hls_low_latency_has_prefetch_disable_ads_has_preroll(self, mock_log):
 
     @patch("streamlink.plugins.twitch.log")
     def test_hls_low_latency_has_prefetch_disable_ads_no_preroll_with_prefetch_ads(self, mock_log):
+        # segment 1 has a shorter duration, to mess with the extrapolation of the prefetch start times
         # segments 3-6 are ads
-        ads = TagDateRangeAd(start=DATETIME_BASE + timedelta(seconds=3), duration=4)
+        Seg, Pre = Segment, SegmentPrefetch
+        ads = [
+            Tag("EXT-X-DISCONTINUITY"),
+            TagDateRangeAd(start=DATETIME_BASE + timedelta(seconds=3), duration=4),
+        ]
+        # noinspection PyTypeChecker
         thread, segments = self.subject([
             # regular stream data with prefetch segments
-            Playlist(0, [Segment(0), Segment(1), SegmentPrefetch(2), SegmentPrefetch(3)]),
+            Playlist(0, [Seg(0), Seg(1, duration=0.5), Pre(2), Pre(3)]),
             # three prefetch segments, one regular (2) and two ads (3 and 4)
-            Playlist(1, [Segment(1), SegmentPrefetch(2), ads, SegmentPrefetch(3), SegmentPrefetch(4)]),
+            Playlist(1, [Seg(1, duration=0.5), Pre(2)] + ads + [Pre(3), Pre(4)]),
             # all prefetch segments are gone once regular prefetch segments have shifted
-            Playlist(2, [Segment(2), ads, Segment(3), Segment(4), Segment(5)]),
+            Playlist(2, [Seg(2, duration=1.5)] + ads + [Seg(3), Seg(4), Seg(5)]),
             # still no prefetch segments while ads are playing
-            Playlist(3, [ads, Segment(3), Segment(4), Segment(5), Segment(6)]),
+            Playlist(3, ads + [Seg(3), Seg(4), Seg(5), Seg(6)]),
             # new prefetch segments on the first regular segment occurrence
-            Playlist(4, [ads, Segment(4), Segment(5), Segment(6), Segment(7), SegmentPrefetch(8), SegmentPrefetch(9)]),
-            Playlist(5, [ads, Segment(5), Segment(6), Segment(7), Segment(8), SegmentPrefetch(9), SegmentPrefetch(10)]),
-            Playlist(6, [ads, Segment(6), Segment(7), Segment(8), Segment(9), SegmentPrefetch(10), SegmentPrefetch(11)]),
-            Playlist(7, [Segment(7), Segment(8), Segment(9), Segment(10), SegmentPrefetch(11), SegmentPrefetch(12)], end=True),
+            Playlist(4, ads + [Seg(4), Seg(5), Seg(6), Seg(7), Pre(8), Pre(9)]),
+            Playlist(5, ads + [Seg(5), Seg(6), Seg(7), Seg(8), Pre(9), Pre(10)]),
+            Playlist(6, ads + [Seg(6), Seg(7), Seg(8), Seg(9), Pre(10), Pre(11)]),
+            Playlist(7, [Seg(7), Seg(8), Seg(9), Seg(10), Pre(11), Pre(12)], end=True),
         ], disable_ads=True, low_latency=True)
 
         self.await_write(11)
         content = self.await_read(read_all=True)
-        self.assertEqual(
-            content,
-            self.content(segments, cond=lambda s: 2 <= s.num <= 3 or 7 <= s.num)
-        )
-        self.assertEqual(mock_log.info.mock_calls, [
+        assert content == self.content(segments, cond=lambda s: 2 <= s.num <= 3 or 7 <= s.num)
+        assert mock_log.info.mock_calls == [
             call("Will skip ad segments"),
             call("Low latency streaming (HLS live edge: 2)"),
-        ])
+        ]
 
     @patch("streamlink.plugins.twitch.log")
     def test_hls_low_latency_no_prefetch_disable_ads_has_preroll(self, mock_log):