Repository: streamlink/streamlink

Issue:
Twitch streams showing Commercial Break In Progress video
- [x] This is a bug report and I have read the contribution guidelines.


### Description

When launching a Twitch stream you're shown a Commercial Break video before the actual stream starts playing. This behavior started tonight.

### Expected / Actual behavior

The commercial video is expected to be ignored.

### Reproduction steps / Explicit stream URLs to test

Launch a Twitch stream.

### Log output

```
C:\Users\User>streamlink https://www.twitch.tv/esl_csgo best --loglevel debug
[cli][debug] OS:         Windows 10
[cli][debug] Python:     3.6.6
[cli][debug] Streamlink: 1.3.1
[cli][debug] Requests(2.22.0), Socks(1.7.1), Websocket(0.57.0)
[cli][info] Found matching plugin twitch for URL https://www.twitch.tv/esl_csgo
[plugin.twitch][debug] Getting live HLS streams for esl_csgo
[utils.l10n][debug] Language code: sv_SE
[cli][info] Available streams: audio_only, 160p (worst), 360p, 480p, 720p, 720p60, 1080p60 (best)
[cli][info] Opening stream: 1080p60 (hls)
[stream.hls][debug] Reloading playlist
[stream.hls][debug] First Sequence: 0; Last Sequence: 2
[stream.hls][debug] Start offset: 0; Duration: None; Start Sequence: 0; End Sequence: None
[stream.hls][debug] Adding segment 0 to queue
[cli][debug] Pre-buffering 8192 bytes
[stream.hls][debug] Adding segment 1 to queue
[stream.hls][debug] Adding segment 2 to queue
[stream.hls][debug] Download of segment 0 complete
[cli][info] Starting player: "C:\Program Files\VideoLAN\VLC\vlc.exe"
[cli.output][debug] Opening subprocess: "C:\Program Files\VideoLAN\VLC\vlc.exe" --input-title-format https://www.twitch.tv/esl_csgo -
[stream.hls][debug] Download of segment 1 complete
[stream.hls][debug] Download of segment 2 complete
[cli][debug] Writing stream to output
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Adding segment 3 to queue
[stream.hls][debug] Download of segment 3 complete
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Adding segment 4 to queue
[stream.hls][debug] Adding segment 5 to queue
[stream.hls][debug] Adding segment 6 to queue
[stream.hls][debug] Download of segment 4 complete
[stream.hls][debug] Download of segment 5 complete
[stream.hls][debug] Download of segment 6 complete
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Adding segment 7 to queue
[stream.hls][debug] Adding segment 8 to queue
[stream.hls][debug] Download of segment 7 complete
[stream.hls][debug] Download of segment 8 complete
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Adding segment 9 to queue
[stream.hls][debug] Adding segment 10 to queue
[stream.hls][debug] Adding segment 11 to queue
[stream.hls][debug] Download of segment 9 complete
[stream.hls][debug] Download of segment 10 complete
[stream.hls][debug] Download of segment 11 complete
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Adding segment 12 to queue
[stream.hls][debug] Adding segment 13 to queue
[stream.hls][debug] Download of segment 12 complete
[stream.hls][debug] Download of segment 13 complete
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Adding segment 14 to queue
[stream.hls][debug] Adding segment 15 to queue
[stream.hls][debug] Adding segment 16 to queue
[stream.hls][debug] Download of segment 14 complete
[stream.hls][debug] Download of segment 15 complete
[stream.hls][debug] Download of segment 16 complete
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Adding segment 17 to queue
[stream.hls][debug] Adding segment 18 to queue
[stream.hls][debug] Download of segment 17 complete
[stream.hls][debug] Download of segment 18 complete
[cli][info] Player closed
[stream.segmented][debug] Closing worker thread
[stream.segmented][debug] Closing writer thread
[stream.segmented][debug] Closing writer thread
[cli][info] Stream ended
[cli][info] Closing currently open stream...
```

[Screenshot](https://i.imgur.com/BbljzTw.png)


Discussion:
I'm experiencing this and also finding that once the stream loads, the audio is slightly pitched up?
Looks like Twitch has implemented a new embedded ads system again.

In #2372, we've added an HLS segment filter that could detect ad segments between certain HLS tags, so these ads could be ignored in the output stream. These tags however don't seem to exist anymore in the HLS playlist.

**Example HLS playlist**

```
$ curl -s $(streamlink --stream-url twitch.tv/CHANNEL_WITH_PREROLL_ADS best)
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:5
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-TWITCH-ELAPSED-SECS:12435.691
#EXT-X-TWITCH-TOTAL-SECS:12441.697
#EXT-X-START:TIME-OFFSET=0.000
#EXT-X-DATERANGE:ID="stitched-ad-1586899127-30197999985",CLASS="twitch-stitched-ad",START-DATE="2020-04-14T21:18:47.355Z",DURATION=30.198,X-TV-TWITCH-AD-AD-FORMAT="standard_video_ad",X-TV-TWITCH-AD-LINE-ITEM-ID="1985958650101",X-TV-TWITCH-AD-ORDER-ID="6733488350001",X-TV-TWITCH-AD-ADVERIFICATIONS="eyJ4bWxuczp4c2kiOiJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIsIlZlcmlmaWNhdGlvbiI6W3sidmVuZG9yIjoibW9hdC5jb20tb21zZGt0d2l0Y2hob3N0ZWRhbWF6b252aWRlbzk2MzM3MTgzODUyOCIsIkphdmFTY3JpcHRSZXNvdXJjZSI6W3siYXBpRnJhbWV3b3JrIjoib21pZCIsImJyb3dzZXJPcHRpb25hbCI6dHJ1ZSwiVVJJIjoiaHR0cHM6Ly96Lm1vYXRhZHMuY29tL29tc2RrdHdpdGNoaG9zdGVkYW1hem9udmlkZW85NjMzNzE4Mzg1MjgvbW9hdHZpZGVvLmpzIn1dLCJWZXJpZmljYXRpb25QYXJhbWV0ZXJzIjoie1wibW9hdENsaWVudExldmVsMVwiOlwiQWR2ZXJ0aXNlcklkXCIsXCJtb2F0Q2xpZW50TGV2ZWwyXCI6XCJPcmRlcklkXCIsXCJtb2F0Q2xpZW50TGV2ZWwzXCI6XCIxOTg1OTU4NjUwMTAxXCIsXCJtb2F0Q2xpZW50TGV2ZWw0XCI6XCI5MzY2MzYwMzczNzJcIn0ifV19",X-TV-TWITCH-AD-POD-LENGTH="1",X-TV-TWITCH-AD-POD-POSITION="0",X-TV-TWITCH-AD-URL="https://example.com",X-TV-TWITCH-AD-ADVERTISER-ID="5122870240601",X-TV-TWITCH-AD-CREATIVE-ID="936636037372",X-TV-TWITCH-AD-ROLL-TYPE="PREROLL",X-TV-TWITCH-AD-LOUDNESS="-52.25",X-TV-TWITCH-AD-CLICK-TRACKING-URL="https://example.com"
#EXT-X-DATERANGE:ID="source-1586899127",CLASS="twitch-stream-source",START-DATE="2020-04-14T21:18:47.355Z",END-ON-NEXT=YES,X-TV-TWITCH-STREAM-SOURCE="Amazon|936636037372"
#EXT-X-DATERANGE:ID="trigger-1586899127",CLASS="twitch-trigger",START-DATE="2020-04-14T21:18:47.355Z",END-ON-NEXT=YES,X-TV-TWITCH-TRIGGER-URL="https://video-weaver.fra06.hls.ttvnw.net/trigger/CoYDN_6IqMMq3EzZ_PnQOxmwPJCAEYO9hUiLdQxb3ry4B_I2ysyYMy3w3YU8vtxg_fvglNcFeiIClmisA-wvu-NocKe8qvVgHsE_wK8-LPA9GXgT2JTC7K8igFFcG7LhIujhNJRMIvIlzLreUFycDTMWwXKKkaDTKvAroAQOnkmzuG2NU5WIElokIoffAvYnt2PP5HJnY-u3hbykV3m6OKyoWm3tv7Xkjperl6VF2SnLZr3luTGw5SRT0eGEhh9xN-37JfLDBgkH7UPVKKtu0d-mnEMew4w5pmOeXdkHxpj5XdZ6ieg1sjV4SYAVpVLM3cl8ak_TcMKtK-Vs-OokBynS3Det9xcOwNfpuHCbcDy9uJTVQ2q2pBgMNvpxCgq-_NqbzAqeIGi8IsWQ9tWMEEaMUisFvqLDtMCLsKVo4KRGpnvFUDYCW6gPe-T78KQc5DjZu6KQ1sg7ibookcusaaVcucUjYmnfyiz1HOo1Ttydx2Lbc9YXn5l6xlYp2ar8-mIdCCmxxGqSEhCBwq1uaVKAx4YDjwt8z8pXGgw2d4n8IFNtbwqPNl4"
#EXT-X-DATERANGE:ID="quartile-1586899127-0",CLASS="twitch-ad-quartile",START-DATE="2020-04-14T21:18:47.355Z",DURATION=2.002,X-TV-TWITCH-AD-QUARTILE="0"
#EXT-X-DISCONTINUITY
#EXT-X-PROGRAM-DATE-TIME:2020-04-14T21:18:47.355Z
#EXTINF:2.002,Amazon|936636037372
https://video-edge-c2ac3c.fra02.abs.hls.ttvnw.net/v1/segment/CsYE2BpUXchZlaHgViGrudxI2Hk4jZ8Ocd7V73x0DJkF6RNF6oIHCbwXaNpft8-UwsZhxFsSHssiThTsi7zSTIxxCyfN2SWYw4WTYVkw9QpYWMXDhH8ikn6hGlIq3nV_UId-L1m9t4KYpA3-qIdCOCD21Mo_XAISGrzUR6oxnU9esS_87pzD-5mBg-_zTqkqEX35Xd81h7OpCUAXE1jmOqaoSkFod4ukLSsOuRDEEVl54dp6igosdgHLehx_L_7b_6jXBWCR7w3YxLn5cAxgFl8wp4aQFcPtiJjtXY-8s-cKAH3CjIbs7JOMsdtHdU24hUp87MdreJ2OEVlzP-yicyP5AgrtJX-KNEmHw-5SoMpvfObwIARJO4bSs91aCZLNgE_aLM-z_LBlypU2um7SGgjCpbzlP37rhy51wSXkCZFGa8_udLjR_YIEKoRYOOtH2QPxfYgPfYwC1DUafBWnaHS34LYAoYsDjQm7y2Zs0FIlTdJb1mgIfbNMYixQagMnh0xGRyJhFkvU-YhSMsN2OrAOytJ0bty_dARDYXePtZXOnTsh-pbGKtnWbSHJ1v8Xgl7YujW3-6dn2QXd5ZbTWKhhh5n0e81FHqeVNDzNuJAIjPKX4SSggMc9fCnoYE82AM4aKJ1miPfJfp7tDa54oSIy9B1lGwQOBUpiAHV1Ts3ZGpXrIBssyYl0ML85vf9zdwb0z874XGIhr9XVXzpWYJVSKOKVaHsmosxxQ7zncUCxLOKIz3_KQBTIzjAWxOnVUT5fh-7U0D-QEhDPebV1GnPnGRdgtXP66QL2Ggyl9-qKt5naNADMvzI.ts
#EXT-X-PROGRAM-DATE-TIME:2020-04-14T21:18:49.356Z
#EXTINF:2.002,Amazon|936636037372
https://video-edge-c2ac3c.fra02.abs.hls.ttvnw.net/v1/segment/CrsDKfGgfAI3X3UdcNvtkqtXIrkZc8jyEiYTmy8VIsFEabq1fqncpfr5L4ETPREoASsONtCK4N7mVM8rIjMQkE06oelq7G2AuOBwmRWn8jniZJf8y1g1kxfTIsE4oYDWi_3-4NxCE9QMmGXLhyqIfZoXmHoqA_hg3ndBohDXF17KauJaQJmugyDuMDyXvNV_NpTsjzACG93oJ3L0DAeT251hNHxD6ljVIe80UvZviV3_6NKqV89fAznDjYLoFTGQJML9MdVxtEVc3uEh4qVwU7-GjuhLL0Rpnu_Sb0a8lNAEysc9N_DaheYv7jz9sOKAS9exIBW3AAIJbfnqT1NxAv7J-Omw-1ZrbO3xqMLYWSDNXsu_rALdR839OVlgChe-CHJljZIaKHuKOLNEKKCCKVgrxAHRuMAsl3jCFPX107dkAdY4kpCBmcydZ4QaP7_cydpENlrFCzuzoNIgtsohQjXRwQIkavZh5WyTfToCwfoOlZqC2cyxQjWtSWw4HB7350hnB3tBL3XJYfKykvHuShuz1jJTJ2nXjK-bR5IO5VsEIvTqh8oKakuI71L6KDIp1Zz5PH4jHmwY1gtzcegSEE_Xvg6i3IuooX6qEsGVoZYaDIZipqY8rYGeWdQslA.ts
#EXT-X-PROGRAM-DATE-TIME:2020-04-14T21:18:51.358Z
#EXTINF:2.002,Amazon|936636037372
https://video-edge-c2ac3c.fra02.abs.hls.ttvnw.net/v1/segment/CrsDgCt3TVmLthbAPpSmkD6uMWeq-T33VEmFJjfxsnS_Au7ZsYbSOyFzxulBYER80OxzCuqs2DXCH7fPExu4Q4yQYLdS_BJjNJFzm7MS3aVV2twwLd8IJTK2q5JseQ1_Z7r5_68hOdh3C0twR22AM9WiySzZyvRHc1Dc_A6VmgDUUR4JF8dF-v_0cUnBo8LaFLc55r2X41xODYt6qIst9LuQEjB69Ey8IeUSBYhYhzztaUKT00x9YffNFUEb3AQuL5Yc2tySq6Px4ZgJUACDlsgRk7cOW3o-hUFyRXfY40W-SnDOAsYW8LbcF_fxHQxr834M0rdNA7Ojzf4udhTDrhCzDoS1H_wDGjZbmTlnW0NNWyD-LGQIHhDpdwoIVnmwOv91R9qayJT2PCGA3aacOKu2zULUAh4UVD81maewSsi_Cs-28UekG2pYxXQz7pgU89vp6FANnXU9qGa7nYN8KBZ2db-kvjFyI4lMIjofDUUml1eZkylprtdpx21UuM9ePgd4PZKpaAnNoI1T7g0BhUZnzxdhKR1d4DHeUdVPp4wOfEAj3KiM0gN7xdMqVIGW78B0NB3my1IFuj9QryYSEBEnZkzyqjGwl9FK8yM5bHcaDOFe3NE2T_KDw4g8sw.ts
```

The "Commerical Break In Progress" video-loop segments are all annotated like this:
`#EXTINF:2.002,Amazon|936636037372`
> the audio is slightly pitched up?

I noticed this as well.
> the audio is slightly pitched up

Which player are you using? VLC? VLC doesn't handle stream discontinuities well. Use a different player if you're using VLC.
Got it! Works fine in MPC.
> 
> 
> > the audio is slightly pitched up
> 
> Which player are you using? VLC? VLC doesn't handle stream discontinuities well. Use a different player if you're using VLC.

I have the same issue with MPC-HC
Also it's lagging for me and i don't think it's my connection. I think the entire video is slightly speed up so it's catching up and then needs time to buffer.
It's happening on all players for me, can't watch any twitch stream as all show "Commercial Break In Progress".
@dCosminn Use MPV. There also can't be any buffering issues, because this new ad system is completely transparent to Streamlink and it's continuing regularly when the real stream data is made available by Twitch. There is a stream discontinuity though, so it's possible that your player is buffering because of the invalid timestamps.

@FractalHippy This has nothing to do with the player you're using, Twitch is simply serving this video data instead of the real stream content. There is nothing that can be done on the client side, just like watching regular TV. All that can be done is ignoring these HLS segments in the stream output or finding a way of making Twitch serve the actual stream data, which is unlikely, as they are in control with the stream access tokens needed for getting the stream data.

What surprises me though is that the Twitch website is showing HLS playlists with normal data. Unfortunately, I don't see anything special in the request data.
Same thing happening here, too. Here's what I'm running:

```
streamlink --twitch-oauth-token <TOKEN> --twitch-disable-ads twitch.tv/parnstarzilean audio_only
```

I even tried and logged in with an account and purchased Turbo for it which supposedly won't show ads, which didn't work either.

What's also weird is, that it also shows ads in `audio_only` streams. 🙄
I figured out how to fix the pitch issue with the audio once the stream loads from the ad break in VLC. You need to disable the audio track under the audio menu and then enable it again. For whatever reason the audio issue only happens on some streams but the ad part happens on all of them.
@dCosminn You can use vlc with the HLS passtrough option. This seems to work fine.
way to go twitch.  prime, turbo, and subscriptions still get ads...


Please **don't** post "+1", "same", "me too" or other unrelated stuff. It's **not** useful and just spams the thread.

----

@4dams Authentication is **not** supported anymore since Streamlink's `1.3.0` release and using the parameters doesn't have any effect anymore, as you can read here:
https://github.com/streamlink/streamlink/issues/2680#issuecomment-557605851
Also #2846

----

I'm currently working on a patch that blocks the "Commerical Break In Progress" segments. It's already working but not yet finished. Problem is that it delays the stream launch until the real stream is available, which can take up to half a minute. Should fix any player issues caused by the stream discontinuity though.
> @dCosminn You can use vlc with the HLS passtrough option. This seems to work fine.

FYI Letting VLC handle the stream using `--player-passthrough hls` resolves both the ~~"lagging"/"buffering"~~ stream discontinuity  and the audio pitching for me. Everything working as before after an initial 30 seconds or so of "Commercial Break in Progress"
@bastimeyer Thanks a lot for your fast help and for working on this project at this hour! :)

> @4dams Authentication is **not** supported anymore since Streamlink's `1.3.0` release and using the parameters doesn't have any effect anymore, as you can read here:
> [#2680 (comment)](https://github.com/streamlink/streamlink/issues/2680#issuecomment-557605851)
> Also #2846

I just scrolled through the docs and found these options, I thought they were still working.

As for your patch: Is there any way to work around the delay and get to the main video directly? I'm sorry if that's too much to ask yet, I'm not very familiar with all this. 😟

Thanks again!


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
@@ -119,25 +119,16 @@
     }
 )
 
-
-Segment = namedtuple("Segment", "uri duration title key discontinuity scte35 byterange date map")
+Segment = namedtuple("Segment", "uri duration title key discontinuity ad byterange date map")
 
 LOW_LATENCY_MAX_LIVE_EDGE = 2
 
 
-def parse_condition(attr):
-    def wrapper(func):
-        def method(self, *args, **kwargs):
-            if hasattr(self.stream, attr) and getattr(self.stream, attr, False):
-                func(self, *args, **kwargs)
-        return method
-    return wrapper
-
-
 class TwitchM3U8Parser(M3U8Parser):
-    def __init__(self, base_uri=None, stream=None, **kwargs):
-        M3U8Parser.__init__(self, base_uri, **kwargs)
-        self.stream = stream
+    def __init__(self, base_uri=None, disable_ads=False, low_latency=False, **kwargs):
+        super(TwitchM3U8Parser, self).__init__(base_uri, **kwargs)
+        self.disable_ads = disable_ads
+        self.low_latency = low_latency
         self.has_prefetch_segments = False
 
     def parse(self, *args):
@@ -146,21 +137,16 @@ def parse(self, *args):
 
         return m3u8
 
-    @parse_condition("disable_ads")
-    def parse_tag_ext_x_scte35_out(self, value):
-        self.state["scte35"] = True
-
-    # unsure if this gets used by Twitch
-    @parse_condition("disable_ads")
-    def parse_tag_ext_x_scte35_out_cont(self, value):
-        self.state["scte35"] = True
+    def parse_extinf(self, value):
+        duration, title = super(TwitchM3U8Parser, self).parse_extinf(value)
+        if title and str(title).startswith("Amazon") and self.disable_ads:
+            self.state["ad"] = True
 
-    @parse_condition("disable_ads")
-    def parse_tag_ext_x_scte35_in(self, value):
-        self.state["scte35"] = False
+        return duration, title
 
-    @parse_condition("low_latency")
     def parse_tag_ext_x_twitch_prefetch(self, value):
+        if not self.low_latency:
+            return
         self.has_prefetch_segments = True
         segments = self.m3u8.segments
         if segments:
@@ -173,7 +159,7 @@ def get_segment(self, uri):
         map_ = self.state.get("map")
         key = self.state.get("key")
         discontinuity = self.state.pop("discontinuity", False)
-        scte35 = self.state.pop("scte35", None)
+        ad = self.state.pop("ad", False)
 
         return Segment(
             uri,
@@ -181,7 +167,7 @@ def get_segment(self, uri):
             extinf[1],
             key,
             discontinuity,
-            scte35,
+            ad,
             byterange,
             date,
             map_
@@ -189,17 +175,36 @@ def get_segment(self, uri):
 
 
 class TwitchHLSStreamWorker(HLSStreamWorker):
+    def __init__(self, *args, **kwargs):
+        self.playlist_reloads = 0
+        super(TwitchHLSStreamWorker, self).__init__(*args, **kwargs)
+
     def _reload_playlist(self, text, url):
-        return load_hls_playlist(text, url, parser=TwitchM3U8Parser, stream=self.stream)
+        self.playlist_reloads += 1
+        playlist = load_hls_playlist(
+            text,
+            url,
+            parser=TwitchM3U8Parser,
+            disable_ads=self.stream.disable_ads,
+            low_latency=self.stream.low_latency
+        )
+        if (
+            self.stream.disable_ads
+            and self.playlist_reloads == 1
+            and not next((s for s in playlist.segments if not s.ad), False)
+        ):
+            log.info("Waiting for pre-roll ads to finish, be patient")
+
+        return playlist
 
     def _set_playlist_reload_time(self, playlist, sequences):
-        if not self.stream.low_latency:
-            super(TwitchHLSStreamWorker, self)._set_playlist_reload_time(playlist, sequences)
-        else:
+        if self.stream.low_latency and len(sequences) > 0:
             self.playlist_reload_time = sequences[-1].segment.duration
+        else:
+            super(TwitchHLSStreamWorker, self)._set_playlist_reload_time(playlist, sequences)
 
     def process_sequences(self, playlist, sequences):
-        if self.playlist_sequence < 0 and self.stream.low_latency and not playlist.has_prefetch_segments:
+        if self.stream.low_latency and self.playlist_reloads == 1 and not playlist.has_prefetch_segments:
             log.info("This is not a low latency stream")
 
         return super(TwitchHLSStreamWorker, self).process_sequences(playlist, sequences)
@@ -207,38 +212,20 @@ def process_sequences(self, playlist, sequences):
 
 class TwitchHLSStreamWriter(HLSStreamWriter):
     def write(self, sequence, *args, **kwargs):
-        if self.stream.disable_ads:
-            if sequence.segment.scte35 is not None:
-                self.reader.ads = sequence.segment.scte35
-                if self.reader.ads:
-                    log.info("Will skip ads beginning with segment {0}".format(sequence.num))
-                else:
-                    log.info("Will stop skipping ads beginning with segment {0}".format(sequence.num))
-            if self.reader.ads:
-                return
-        return HLSStreamWriter.write(self, sequence, *args, **kwargs)
+        if not (self.stream.disable_ads and sequence.segment.ad):
+            return super(TwitchHLSStreamWriter, self).write(sequence, *args, **kwargs)
 
 
 class TwitchHLSStreamReader(HLSStreamReader):
     __worker__ = TwitchHLSStreamWorker
     __writer__ = TwitchHLSStreamWriter
-    ads = None
 
 
 class TwitchHLSStream(HLSStream):
     def __init__(self, *args, **kwargs):
-        HLSStream.__init__(self, *args, **kwargs)
-
-        disable_ads = self.session.get_plugin_option("twitch", "disable-ads")
-        low_latency = self.session.get_plugin_option("twitch", "low-latency")
-
-        if low_latency and disable_ads:
-            log.info("Low latency streaming with ad filtering is currently not supported")
-            self.session.set_plugin_option("twitch", "low-latency", False)
-            low_latency = False
-
-        self.disable_ads = disable_ads
-        self.low_latency = low_latency
+        super(TwitchHLSStream, self).__init__(*args, **kwargs)
+        self.disable_ads = self.session.get_plugin_option("twitch", "disable-ads")
+        self.low_latency = self.session.get_plugin_option("twitch", "low-latency")
 
     def open(self):
         if self.disable_ads:
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
@@ -232,21 +232,24 @@ def reload_playlist(self):
         sequences = [Sequence(media_sequence + i, s)
                      for i, s in enumerate(playlist.segments)]
 
-        if sequences:
-            self.process_sequences(playlist, sequences)
+        self.process_sequences(playlist, sequences)
 
     def _set_playlist_reload_time(self, playlist, sequences):
         self.playlist_reload_time = (playlist.target_duration
-                                     or sequences[-1].segment.duration)
+                                     or len(sequences) > 0 and sequences[-1].segment.duration)
 
     def process_sequences(self, playlist, sequences):
+        self._set_playlist_reload_time(playlist, sequences)
+
+        if not sequences:
+            return
+
         first_sequence, last_sequence = sequences[0], sequences[-1]
 
         if first_sequence.segment.key and first_sequence.segment.key.method != "NONE":
             log.debug("Segments in this playlist are encrypted")
 
         self.playlist_changed = ([s.num for s in self.playlist_sequences] != [s.num for s in sequences])
-        self._set_playlist_reload_time(playlist, sequences)
         self.playlist_sequences = sequences
 
         if not self.playlist_changed:


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
@@ -5,7 +5,7 @@
 from streamlink.plugins.twitch import Twitch, TwitchHLSStream
 
 import requests_mock
-from tests.mock import call, patch
+from tests.mock import MagicMock, call, patch
 
 from streamlink.session import Streamlink
 from tests.resources import text
@@ -34,22 +34,21 @@ def test_can_handle_url_negative(self):
             self.assertFalse(Twitch.can_handle_url(url))
 
 
+@patch("streamlink.stream.hls.HLSStreamWorker.wait", MagicMock(return_value=True))
 class TestTwitchHLSStream(unittest.TestCase):
     url_master = "http://mocked/path/master.m3u8"
     url_playlist = "http://mocked/path/playlist.m3u8"
     url_segment = "http://mocked/path/stream{0}.ts"
 
-    scte35_out = "#EXT-X-DISCONTINUITY\n#EXT-X-SCTE35-OUT\n"
-    scte35_out_cont = "#EXT-X-SCTE35-OUT-CONT\n"
-    scte35_in = "#EXT-X-DISCONTINUITY\n#EXT-X-SCTE35-IN\n"
     segment = "#EXTINF:1.000,\nstream{0}.ts\n"
+    segment_ad = "#EXTINF:1.000,Amazon|123456789\nstream{0}.ts\n"
     prefetch = "#EXT-X-TWITCH-PREFETCH:{0}\n"
 
     def getMasterPlaylist(self):
         with text("hls/test_master.m3u8") as pl:
             return pl.read()
 
-    def getPlaylist(self, media_sequence, items, prefetch=None):
+    def getPlaylist(self, media_sequence, items, ads=False, prefetch=None):
         playlist = """
 #EXTM3U
 #EXT-X-VERSION:5
@@ -57,11 +56,9 @@ def getPlaylist(self, media_sequence, items, prefetch=None):
 #EXT-X-MEDIA-SEQUENCE:{0}
 """.format(media_sequence)
 
+        segment = self.segment if not ads else self.segment_ad
         for item in items:
-            if type(item) != int:
-                playlist += item
-            else:
-                playlist += self.segment.format(item)
+            playlist += segment.format(item)
         for item in prefetch or []:
             playlist += self.prefetch.format(self.url_segment.format(item))
 
@@ -97,117 +94,52 @@ def get_result(self, streams, playlists, **kwargs):
             return streamlink, data, mocked
 
     @patch("streamlink.plugins.twitch.log")
-    def test_hls_scte35_start_with_end(self, mock_logging):
-        streams = ["[{0}]".format(i).encode("ascii") for i in range(12)]
+    def test_hls_disable_ads_preroll(self, mock_logging):
+        streams = ["[{0}]".format(i).encode("ascii") for i in range(6)]
         playlists = [
-            self.getPlaylist(0, [self.scte35_out, 0, 1, 2, 3]),
-            self.getPlaylist(4, [self.scte35_in, 4, 5, 6, 7]),
-            self.getPlaylist(8, [8, 9, 10, 11]) + "#EXT-X-ENDLIST\n"
+            self.getPlaylist(0, [0, 1], ads=True),
+            self.getPlaylist(2, [2, 3], ads=True),
+            self.getPlaylist(4, [4, 5]) + "#EXT-X-ENDLIST\n"
         ]
         streamlink, result, mocked = self.get_result(streams, playlists, disable_ads=True)
 
-        expected = b''.join(streams[4:12])
-        self.assertEqual(expected, result)
-        for i in range(0, 12):
+        self.assertEqual(result, b''.join(streams[4:6]))
+        for i in range(0, 6):
             self.assertTrue(mocked[self.url_segment.format(i)].called, i)
         mock_logging.info.assert_has_calls([
             call("Will skip ad segments"),
-            call("Will skip ads beginning with segment 0"),
-            call("Will stop skipping ads beginning with segment 4")
+            call("Waiting for pre-roll ads to finish, be patient")
         ])
+        self.assertEqual(mock_logging.info.call_count, 2)
 
     @patch("streamlink.plugins.twitch.log")
-    def test_hls_scte35_no_start(self, mock_logging):
-        streams = ["[{0}]".format(i).encode("ascii") for i in range(8)]
+    def test_hls_disable_ads_no_preroll(self, mock_logging):
+        streams = ["[{0}]".format(i).encode("ascii") for i in range(6)]
         playlists = [
-            self.getPlaylist(0, [0, 1, 2, 3]),
-            self.getPlaylist(4, [self.scte35_in, 4, 5, 6, 7]) + "#EXT-X-ENDLIST\n"
+            self.getPlaylist(0, [0, 1]),
+            self.getPlaylist(2, [2, 3], ads=True),
+            self.getPlaylist(4, [4, 5]) + "#EXT-X-ENDLIST\n"
         ]
         streamlink, result, mocked = self.get_result(streams, playlists, disable_ads=True)
 
-        expected = b''.join(streams[0:8])
-        self.assertEqual(expected, result)
-        for i in range(0, 8):
+        self.assertEqual(result, b''.join(streams[0:2]) + b''.join(streams[4:6]))
+        for i in range(0, 6):
             self.assertTrue(mocked[self.url_segment.format(i)].called, i)
         mock_logging.info.assert_has_calls([
             call("Will skip ad segments")
         ])
 
     @patch("streamlink.plugins.twitch.log")
-    def test_hls_scte35_no_start_with_cont(self, mock_logging):
-        streams = ["[{0}]".format(i).encode("ascii") for i in range(8)]
-        playlists = [
-            self.getPlaylist(0, [self.scte35_out_cont, 0, 1, 2, 3]),
-            self.getPlaylist(4, [self.scte35_in, 4, 5, 6, 7]) + "#EXT-X-ENDLIST\n"
-        ]
-        streamlink, result, mocked = self.get_result(streams, playlists, disable_ads=True)
-
-        expected = b''.join(streams[4:8])
-        self.assertEqual(expected, result)
-        for i in range(0, 8):
-            self.assertTrue(mocked[self.url_segment.format(i)].called, i)
-        mock_logging.info.assert_has_calls([
-            call("Will skip ad segments"),
-            call("Will skip ads beginning with segment 0"),
-            call("Will stop skipping ads beginning with segment 4")
-        ])
-
-    @patch("streamlink.plugins.twitch.log")
-    def test_hls_scte35_no_end(self, mock_logging):
-        streams = ["[{0}]".format(i).encode("ascii") for i in range(12)]
+    def test_hls_no_disable_ads(self, mock_logging):
+        streams = ["[{0}]".format(i).encode("ascii") for i in range(4)]
         playlists = [
-            self.getPlaylist(0, [0, 1, 2, 3]),
-            self.getPlaylist(4, [self.scte35_out, 4, 5, 6, 7]),
-            self.getPlaylist(8, [8, 9, 10, 11]) + "#EXT-X-ENDLIST\n"
+            self.getPlaylist(0, [0, 1], ads=True),
+            self.getPlaylist(2, [2, 3]) + "#EXT-X-ENDLIST\n"
         ]
-        streamlink, result, mocked = self.get_result(streams, playlists, disable_ads=True)
+        streamlink, result, mocked = self.get_result(streams, playlists, disable_ads=False)
 
-        expected = b''.join(streams[0:4])
-        self.assertEqual(expected, result)
-        for i in range(0, 12):
-            self.assertTrue(mocked[self.url_segment.format(i)].called, i)
-        mock_logging.info.assert_has_calls([
-            call("Will skip ad segments"),
-            call("Will skip ads beginning with segment 4")
-        ])
-
-    @patch("streamlink.plugins.twitch.log")
-    def test_hls_scte35_in_between(self, mock_logging):
-        streams = ["[{0}]".format(i).encode("ascii") for i in range(20)]
-        playlists = [
-            self.getPlaylist(0, [0, 1, 2, 3]),
-            self.getPlaylist(4, [4, 5, self.scte35_out, 6, 7]),
-            self.getPlaylist(8, [8, 9, 10, 11]),
-            self.getPlaylist(12, [12, 13, self.scte35_in, 14, 15]),
-            self.getPlaylist(16, [16, 17, 18, 19]) + "#EXT-X-ENDLIST\n"
-        ]
-        streamlink, result, mocked = self.get_result(streams, playlists, disable_ads=True)
-
-        expected = b''.join(streams[0:6]) + b''.join(streams[14:20])
-        self.assertEqual(expected, result)
-        for i in range(0, 20):
-            self.assertTrue(mocked[self.url_segment.format(i)].called, i)
-        mock_logging.info.assert_has_calls([
-            call("Will skip ad segments"),
-            call("Will skip ads beginning with segment 6"),
-            call("Will stop skipping ads beginning with segment 14")
-        ])
-
-    @patch("streamlink.plugins.twitch.log")
-    def test_hls_scte35_no_disable_ads(self, mock_logging):
-        streams = ["[{0}]".format(i).encode("ascii") for i in range(20)]
-        playlists = [
-            self.getPlaylist(0, [0, 1, 2, 3]),
-            self.getPlaylist(4, [4, 5, self.scte35_out, 6, 7]),
-            self.getPlaylist(8, [8, 9, 10, 11]),
-            self.getPlaylist(12, [12, 13, self.scte35_in, 14, 15]),
-            self.getPlaylist(16, [16, 17, 18, 19]) + "#EXT-X-ENDLIST\n"
-        ]
-        streamlink, result, mocked = self.get_result(streams, playlists)
-
-        expected = b''.join(streams[0:20])
-        self.assertEqual(expected, result)
-        for i in range(0, 20):
+        self.assertEqual(result, b''.join(streams[0:4]))
+        for i in range(0, 4):
             self.assertTrue(mocked[self.url_segment.format(i)].called, i)
         mock_logging.info.assert_has_calls([])
 
@@ -215,8 +147,8 @@ def test_hls_scte35_no_disable_ads(self, mock_logging):
     def test_hls_prefetch(self, mock_logging):
         streams = ["[{0}]".format(i).encode("ascii") for i in range(10)]
         playlists = [
-            self.getPlaylist(0, [0, 1, 2, 3], [4, 5]),
-            self.getPlaylist(4, [4, 5, 6, 7], [8, 9]) + "#EXT-X-ENDLIST\n"
+            self.getPlaylist(0, [0, 1, 2, 3], prefetch=[4, 5]),
+            self.getPlaylist(4, [4, 5, 6, 7], prefetch=[8, 9]) + "#EXT-X-ENDLIST\n"
         ]
         streamlink, result, mocked = self.get_result(streams, playlists, low_latency=True)
 
@@ -237,8 +169,8 @@ def test_hls_prefetch(self, mock_logging):
     def test_hls_prefetch_no_low_latency(self, mock_logging):
         streams = ["[{0}]".format(i).encode("ascii") for i in range(10)]
         playlists = [
-            self.getPlaylist(0, [0, 1, 2, 3], [4, 5]),
-            self.getPlaylist(4, [4, 5, 6, 7], [8, 9]) + "#EXT-X-ENDLIST\n"
+            self.getPlaylist(0, [0, 1, 2, 3], prefetch=[4, 5]),
+            self.getPlaylist(4, [4, 5, 6, 7], prefetch=[8, 9]) + "#EXT-X-ENDLIST\n"
         ]
         streamlink, result, mocked = self.get_result(streams, playlists)
 
@@ -253,28 +185,12 @@ def test_hls_prefetch_no_low_latency(self, mock_logging):
             self.assertFalse(mocked[self.url_segment.format(i)].called, i)
         mock_logging.info.assert_has_calls([])
 
-    @patch("streamlink.plugins.twitch.log")
-    def test_hls_no_low_latency_with_disable_ads(self, mock_logging):
-        streams = ["[{0}]".format(i).encode("ascii") for i in range(10)]
-        playlists = [
-            self.getPlaylist(0, [0, 1, 2, 3], [4, 5]),
-            self.getPlaylist(4, [4, 5, 6, 7], [8, 9]) + "#EXT-X-ENDLIST\n"
-        ]
-        streamlink, result, mocked = self.get_result(streams, playlists, low_latency=True, disable_ads=True)
-
-        self.assertFalse(streamlink.get_plugin_option("twitch", "low-latency"))
-        self.assertTrue(streamlink.get_plugin_option("twitch", "disable-ads"))
-
-        mock_logging.info.assert_has_calls([
-            call("Low latency streaming with ad filtering is currently not supported")
-        ])
-
     @patch("streamlink.plugins.twitch.log")
     def test_hls_no_low_latency_no_prefetch(self, mock_logging):
         streams = ["[{0}]".format(i).encode("ascii") for i in range(10)]
         playlists = [
-            self.getPlaylist(0, [0, 1, 2, 3], []),
-            self.getPlaylist(4, [4, 5, 6, 7], []) + "#EXT-X-ENDLIST\n"
+            self.getPlaylist(0, [0, 1, 2, 3], prefetch=[]),
+            self.getPlaylist(4, [4, 5, 6, 7], prefetch=[]) + "#EXT-X-ENDLIST\n"
         ]
         streamlink, result, mocked = self.get_result(streams, playlists, low_latency=True)