Skip to content

Commit 6251bd3

Browse files
authored
Merge pull request streamlink#2372 from bastimeyer/plugins/twitch/disable-ads
plugins.twitch: implement disable-ads parameter
2 parents 34f0172 + 39bb87f commit 6251bd3

File tree

4 files changed

+403
-107
lines changed

4 files changed

+403
-107
lines changed

src/streamlink/plugins/twitch.py

Lines changed: 96 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# coding=utf-8
2+
import logging
23
import re
34
import warnings
5+
from collections import namedtuple
46
from random import random
57

68
import requests
@@ -13,13 +15,18 @@
1315
from streamlink.stream import (
1416
HTTPStream, HLSStream, FLVPlaylist, extract_flv_header_tags
1517
)
18+
from streamlink.stream.hls import HLSStreamReader, HLSStreamWriter, HLSStreamWorker
19+
from streamlink.stream.hls_playlist import M3U8Parser, load as load_hls_playlist
1620
from streamlink.utils.times import hours_minutes_seconds
1721

1822
try:
1923
from itertools import izip as zip
2024
except ImportError:
2125
pass
2226

27+
28+
log = logging.getLogger(__name__)
29+
2330
QUALITY_WEIGHTS = {
2431
"source": 1080,
2532
"1080": 1080,
@@ -128,6 +135,82 @@
128135
)
129136

130137

138+
Segment = namedtuple("Segment", "uri duration title key discontinuity scte35 byterange date map")
139+
140+
141+
class TwitchM3U8Parser(M3U8Parser):
142+
def parse_tag_ext_x_scte35_out(self, value):
143+
self.state["scte35"] = True
144+
145+
# unsure if this gets used by Twitch
146+
def parse_tag_ext_x_scte35_out_cont(self, value):
147+
self.state["scte35"] = True
148+
149+
def parse_tag_ext_x_scte35_in(self, value):
150+
self.state["scte35"] = False
151+
152+
def get_segment(self, uri):
153+
byterange = self.state.pop("byterange", None)
154+
extinf = self.state.pop("extinf", (0, None))
155+
date = self.state.pop("date", None)
156+
map_ = self.state.get("map")
157+
key = self.state.get("key")
158+
discontinuity = self.state.pop("discontinuity", False)
159+
scte35 = self.state.pop("scte35", None)
160+
161+
return Segment(
162+
uri,
163+
extinf[0],
164+
extinf[1],
165+
key,
166+
discontinuity,
167+
scte35,
168+
byterange,
169+
date,
170+
map_
171+
)
172+
173+
174+
class TwitchHLSStreamWorker(HLSStreamWorker):
175+
def _reload_playlist(self, text, url):
176+
return load_hls_playlist(text, url, parser=TwitchM3U8Parser)
177+
178+
179+
class TwitchHLSStreamWriter(HLSStreamWriter):
180+
def __init__(self, *args, **kwargs):
181+
HLSStreamWriter.__init__(self, *args, **kwargs)
182+
options = self.session.plugins.get("twitch").options
183+
self.disable_ads = options.get("disable-ads")
184+
if self.disable_ads:
185+
log.info("Will skip ad segments")
186+
187+
def write(self, sequence, *args, **kwargs):
188+
if self.disable_ads:
189+
if sequence.segment.scte35 is not None:
190+
self.reader.ads = sequence.segment.scte35
191+
if self.reader.ads:
192+
log.info("Will skip ads beginning with segment {0}".format(sequence.num))
193+
else:
194+
log.info("Will stop skipping ads beginning with segment {0}".format(sequence.num))
195+
if self.reader.ads:
196+
return
197+
return HLSStreamWriter.write(self, sequence, *args, **kwargs)
198+
199+
200+
class TwitchHLSStreamReader(HLSStreamReader):
201+
__worker__ = TwitchHLSStreamWorker
202+
__writer__ = TwitchHLSStreamWriter
203+
ads = None
204+
205+
206+
class TwitchHLSStream(HLSStream):
207+
def open(self):
208+
reader = TwitchHLSStreamReader(self)
209+
reader.open()
210+
211+
return reader
212+
213+
131214
class UsherService(object):
132215
def __init__(self, session):
133216
self.session = session
@@ -277,6 +360,13 @@ class Twitch(Plugin):
277360
action="store_true",
278361
help="""
279362
Do not open the stream if the target channel is hosting another channel.
363+
"""
364+
),
365+
PluginArgument("disable-ads",
366+
action="store_true",
367+
help="""
368+
Skip embedded advertisement segments at the beginning or during a stream.
369+
Will cause these segments to be missing from the stream.
280370
"""
281371
))
282372

@@ -640,9 +730,12 @@ def _get_hls_streams(self, stream_type="live"):
640730
try:
641731
# If the stream is a VOD that is still being recorded the stream should start at the
642732
# beginning of the recording
643-
streams = HLSStream.parse_variant_playlist(self.session, url,
644-
start_offset=time_offset,
645-
force_restart=not stream_type == "live")
733+
streams = TwitchHLSStream.parse_variant_playlist(
734+
self.session,
735+
url,
736+
start_offset=time_offset,
737+
force_restart=not stream_type == "live"
738+
)
646739
except IOError as err:
647740
err = str(err)
648741
if "404 Client Error" in err or "Failed to parse playlist" in err:

src/streamlink/stream/hls.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,9 @@ def __init__(self, *args, **kwargs):
181181
self.duration_offset_start, self.duration_limit,
182182
self.playlist_sequence, self.playlist_end)
183183

184+
def _reload_playlist(self, text, url):
185+
return hls_playlist.load(text, url)
186+
184187
def reload_playlist(self):
185188
if self.closed:
186189
return
@@ -192,7 +195,7 @@ def reload_playlist(self):
192195
retries=self.playlist_reload_retries,
193196
**self.reader.request_params)
194197
try:
195-
playlist = hls_playlist.load(res.text, res.url)
198+
playlist = self._reload_playlist(res.text, res.url)
196199
except ValueError as err:
197200
raise StreamError(err)
198201

@@ -472,8 +475,12 @@ def parse_variant_playlist(cls, session_, url, name_key="name",
472475
duration=duration,
473476
**request_params)
474477
else:
475-
stream = HLSStream(session_, playlist.uri, force_restart=force_restart,
476-
start_offset=start_offset, duration=duration, **request_params)
478+
stream = cls(session_,
479+
playlist.uri,
480+
force_restart=force_restart,
481+
start_offset=start_offset,
482+
duration=duration,
483+
**request_params)
477484
streams[name_prefix + stream_name] = stream
478485

479486
return streams

0 commit comments

Comments
 (0)