|
1 | 1 | # coding=utf-8
|
| 2 | +import logging |
2 | 3 | import re
|
3 | 4 | import warnings
|
| 5 | +from collections import namedtuple |
4 | 6 | from random import random
|
5 | 7 |
|
6 | 8 | import requests
|
|
13 | 15 | from streamlink.stream import (
|
14 | 16 | HTTPStream, HLSStream, FLVPlaylist, extract_flv_header_tags
|
15 | 17 | )
|
| 18 | +from streamlink.stream.hls import HLSStreamReader, HLSStreamWriter, HLSStreamWorker |
| 19 | +from streamlink.stream.hls_playlist import M3U8Parser, load as load_hls_playlist |
16 | 20 | from streamlink.utils.times import hours_minutes_seconds
|
17 | 21 |
|
18 | 22 | try:
|
19 | 23 | from itertools import izip as zip
|
20 | 24 | except ImportError:
|
21 | 25 | pass
|
22 | 26 |
|
| 27 | + |
| 28 | +log = logging.getLogger(__name__) |
| 29 | + |
23 | 30 | QUALITY_WEIGHTS = {
|
24 | 31 | "source": 1080,
|
25 | 32 | "1080": 1080,
|
|
128 | 135 | )
|
129 | 136 |
|
130 | 137 |
|
| 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 | + |
131 | 214 | class UsherService(object):
|
132 | 215 | def __init__(self, session):
|
133 | 216 | self.session = session
|
@@ -277,6 +360,13 @@ class Twitch(Plugin):
|
277 | 360 | action="store_true",
|
278 | 361 | help="""
|
279 | 362 | 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. |
280 | 370 | """
|
281 | 371 | ))
|
282 | 372 |
|
@@ -640,9 +730,12 @@ def _get_hls_streams(self, stream_type="live"):
|
640 | 730 | try:
|
641 | 731 | # If the stream is a VOD that is still being recorded the stream should start at the
|
642 | 732 | # 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 | + ) |
646 | 739 | except IOError as err:
|
647 | 740 | err = str(err)
|
648 | 741 | if "404 Client Error" in err or "Failed to parse playlist" in err:
|
|
0 commit comments