Skip to content

plugins.twitch: implement disable-ads parameter #2372

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 29, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 96 additions & 3 deletions src/streamlink/plugins/twitch.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# coding=utf-8
import logging
import re
import warnings
from collections import namedtuple
from random import random

import requests
Expand All @@ -13,13 +15,18 @@
from streamlink.stream import (
HTTPStream, HLSStream, FLVPlaylist, extract_flv_header_tags
)
from streamlink.stream.hls import HLSStreamReader, HLSStreamWriter, HLSStreamWorker
from streamlink.stream.hls_playlist import M3U8Parser, load as load_hls_playlist
from streamlink.utils.times import hours_minutes_seconds

try:
from itertools import izip as zip
except ImportError:
pass


log = logging.getLogger(__name__)

QUALITY_WEIGHTS = {
"source": 1080,
"1080": 1080,
Expand Down Expand Up @@ -128,6 +135,82 @@
)


Segment = namedtuple("Segment", "uri duration title key discontinuity scte35 byterange date map")


class TwitchM3U8Parser(M3U8Parser):
def parse_tag_ext_x_scte35_out(self, value):
self.state["scte35"] = True

# unsure if this gets used by Twitch
def parse_tag_ext_x_scte35_out_cont(self, value):
self.state["scte35"] = True

def parse_tag_ext_x_scte35_in(self, value):
self.state["scte35"] = False

def get_segment(self, uri):
byterange = self.state.pop("byterange", None)
extinf = self.state.pop("extinf", (0, None))
date = self.state.pop("date", None)
map_ = self.state.get("map")
key = self.state.get("key")
discontinuity = self.state.pop("discontinuity", False)
scte35 = self.state.pop("scte35", None)

return Segment(
uri,
extinf[0],
extinf[1],
key,
discontinuity,
scte35,
byterange,
date,
map_
)


class TwitchHLSStreamWorker(HLSStreamWorker):
def _reload_playlist(self, text, url):
return load_hls_playlist(text, url, parser=TwitchM3U8Parser)


class TwitchHLSStreamWriter(HLSStreamWriter):
def __init__(self, *args, **kwargs):
HLSStreamWriter.__init__(self, *args, **kwargs)
options = self.session.plugins.get("twitch").options
self.disable_ads = options.get("disable-ads")
if self.disable_ads:
log.info("Will skip ad segments")

def write(self, sequence, *args, **kwargs):
if self.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)


class TwitchHLSStreamReader(HLSStreamReader):
__worker__ = TwitchHLSStreamWorker
__writer__ = TwitchHLSStreamWriter
ads = None


class TwitchHLSStream(HLSStream):
def open(self):
reader = TwitchHLSStreamReader(self)
reader.open()

return reader


class UsherService(object):
def __init__(self, session):
self.session = session
Expand Down Expand Up @@ -277,6 +360,13 @@ class Twitch(Plugin):
action="store_true",
help="""
Do not open the stream if the target channel is hosting another channel.
"""
),
PluginArgument("disable-ads",
action="store_true",
help="""
Skip embedded advertisement segments at the beginning or during a stream.
Will cause these segments to be missing from the stream.
"""
))

Expand Down Expand Up @@ -640,9 +730,12 @@ def _get_hls_streams(self, stream_type="live"):
try:
# If the stream is a VOD that is still being recorded the stream should start at the
# beginning of the recording
streams = HLSStream.parse_variant_playlist(self.session, url,
start_offset=time_offset,
force_restart=not stream_type == "live")
streams = TwitchHLSStream.parse_variant_playlist(
self.session,
url,
start_offset=time_offset,
force_restart=not stream_type == "live"
)
except IOError as err:
err = str(err)
if "404 Client Error" in err or "Failed to parse playlist" in err:
Expand Down
13 changes: 10 additions & 3 deletions src/streamlink/stream/hls.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,9 @@ def __init__(self, *args, **kwargs):
self.duration_offset_start, self.duration_limit,
self.playlist_sequence, self.playlist_end)

def _reload_playlist(self, text, url):
return hls_playlist.load(text, url)

def reload_playlist(self):
if self.closed:
return
Expand All @@ -192,7 +195,7 @@ def reload_playlist(self):
retries=self.playlist_reload_retries,
**self.reader.request_params)
try:
playlist = hls_playlist.load(res.text, res.url)
playlist = self._reload_playlist(res.text, res.url)
except ValueError as err:
raise StreamError(err)

Expand Down Expand Up @@ -472,8 +475,12 @@ def parse_variant_playlist(cls, session_, url, name_key="name",
duration=duration,
**request_params)
else:
stream = HLSStream(session_, playlist.uri, force_restart=force_restart,
start_offset=start_offset, duration=duration, **request_params)
stream = cls(session_,
playlist.uri,
force_restart=force_restart,
start_offset=start_offset,
duration=duration,
**request_params)
streams[name_prefix + stream_name] = stream

return streams
Loading