text: from __future__ import print_function
  
  import re
  import struct
  from collections import defaultdict, namedtuple
  from time import time
  
  from livecli.cache import Cache
  from livecli.compat import crypto_AES
  from livecli.stream import hls_playlist
  from livecli.stream.ffmpegmux import FFMPEGMuxer, MuxedStream
  from livecli.stream.http import HTTPStream
  from livecli.stream.segmented import (SegmentedStreamReader,
                                        SegmentedStreamWriter,
                                        SegmentedStreamWorker)
  from ..exceptions import StreamError
  
  Sequence = namedtuple("Sequence", "num segment")
  
  
  def num_to_iv(n):
      return struct.pack(">8xq", n)
  
  
  def pkcs7_decode(paddedData, keySize=16):
      '''
      Remove the PKCS#7 padding
      '''
      # Use ord + [-1:] to support both python 2 and 3
      val = ord(paddedData[-1:])
      if val > keySize:
          raise StreamError("Input is not padded or padding is corrupt, got padding size of {0}".format(val))
  
      return paddedData[:-val]
  
  
  class HLSStreamWriter(SegmentedStreamWriter):
      def __init__(self, reader, *args, **kwargs):
          options = reader.stream.session.options
          kwargs["retries"] = options.get("hls-segment-attempts")
          kwargs["threads"] = options.get("hls-segment-threads")
          kwargs["timeout"] = options.get("hls-segment-timeout")
          kwargs["ignore_names"] = options.get("hls-segment-ignore-names")
          SegmentedStreamWriter.__init__(self, reader, *args, **kwargs)
  
          self.byterange_offsets = defaultdict(int)
          self.key_data = None
          self.key_uri = None
          if self.ignore_names:
              # creates a regex from a list of segment names,
              # this will be used to ignore segments.
              self.ignore_names = list(set(self.ignore_names))
              self.ignore_names = "|".join(list(map(re.escape, self.ignore_names)))
              self.ignore_names_re = re.compile(r"(?:{blacklist})\.ts".format(
                  blacklist=self.ignore_names), re.IGNORECASE)
  
      def create_decryptor(self, key, sequence):
          if key.method != "AES-128":
              raise StreamError("Unable to decrypt cipher {0}", key.method)
  
          if not key.uri:
              raise StreamError("Missing URI to decryption key")
  
          if self.key_uri != key.uri:
              res = self.session.http.get(key.uri, exception=StreamError,
                                          retries=self.retries,
                                          **self.reader.request_params)
              self.key_data = res.content
              self.key_uri = key.uri
  
          iv = key.iv or num_to_iv(sequence)
  
          # Pad IV if needed
          iv = b"\x00" * (16 - len(iv)) + iv
  
          return crypto_AES.new(self.key_data, crypto_AES.MODE_CBC, iv)
  
      def create_request_params(self, sequence):
          request_params = dict(self.reader.request_params)
          headers = request_params.pop("headers", {})
  
          if sequence.segment.byterange:
              bytes_start = self.byterange_offsets[sequence.segment.uri]
              if sequence.segment.byterange.offset is not None:
                  bytes_start = sequence.segment.byterange.offset
  
              bytes_len = max(sequence.segment.byterange.range - 1, 0)
              bytes_end = bytes_start + bytes_len
              headers["Range"] = "bytes={0}-{1}".format(bytes_start, bytes_end)
              self.byterange_offsets[sequence.segment.uri] = bytes_end + 1
  
          request_params["headers"] = headers
  
          return request_params
  
      def fetch(self, sequence, retries=None):
          if self.closed or not retries:
              return
  
          try:
              request_params = self.create_request_params(sequence)
              # skip ignored segment names
              if self.ignore_names and self.ignore_names_re.search(sequence.segment.uri):
                  self.logger.debug("Skipping segment {0}".format(sequence.num))
                  return
  
              return self.session.http.get(sequence.segment.uri,
                                           timeout=self.timeout,
                                           exception=StreamError,
                                           retries=self.retries,
                                           **request_params)
          except StreamError as err:
              self.logger.error("Failed to open segment {0}: {1}", sequence.num, err)
              return
  
      def write(self, sequence, res, chunk_size=8192):
          if sequence.segment.key and sequence.segment.key.method != "NONE":
              try:
                  decryptor = self.create_decryptor(sequence.segment.key,
                                                    sequence.num)
              except StreamError as err:
                  self.logger.error("Failed to create decryptor: {0}", err)
                  self.close()
                  return
  
              data = res.content
              # If the input data is not a multiple of 16, cut off any garbage
              garbage_len = len(data) % 16
              if garbage_len:
                  self.logger.debug("Cutting off {0} bytes of garbage "
                                    "before decrypting", garbage_len)
                  decrypted_chunk = decryptor.decrypt(data[:-garbage_len])
              else:
                  decrypted_chunk = decryptor.decrypt(data)
  
              self.reader.buffer.write(pkcs7_decode(decrypted_chunk))
          else:
              for chunk in res.iter_content(chunk_size):
                  self.reader.buffer.write(chunk)
  
          self.logger.debug("Download of segment {0} complete", sequence.num)
  
  
  class HLSStreamWorker(SegmentedStreamWorker):
      def __init__(self, *args, **kwargs):
          SegmentedStreamWorker.__init__(self, *args, **kwargs)
          self.stream = self.reader.stream
  
          self.playlist_changed = False
          self.playlist_end = None
          self.playlist_sequence = -1
          self.playlist_sequences = []
          self.playlist_reload_time = 15
          self.live_edge = self.session.options.get("hls-live-edge")
          self.playlist_reload_retries = self.session.options.get("hls-playlist-reload-attempts")
          self.duration_offset_start = int(self.stream.start_offset + (self.session.options.get("hls-start-offset") or 0))
          self.duration_limit = self.stream.duration or (int(self.session.options.get("hls-duration")) if self.session.options.get("hls-duration") else None)
          self.hls_live_restart = self.stream.force_restart or self.session.options.get("hls-live-restart")
          self.sequence_ignore_number = self.session.options.get("hls-segment-ignore-number") or False
          self.session_time = int(time())
          self.session_reload = self.session.options.get("hls-session-reload") or False
  
          self.reload_playlist()
  
          if self.playlist_end is None:
              if self.duration_offset_start > 0:
                  self.logger.debug("Time offsets negative for live streams, skipping back {0} seconds",
                                    self.duration_offset_start)
              # live playlist, force offset durations back to None
              self.duration_offset_start = -self.duration_offset_start
  
          if self.duration_offset_start != 0:
              self.playlist_sequence = self.duration_to_sequence(self.duration_offset_start, self.playlist_sequences)
              if self.duration_limit and self.playlist_end:
                  self.playlist_end = self.duration_to_sequence(self.duration_offset_start + self.duration_limit,
                                                                self.playlist_sequences)
          if self.playlist_sequences:
              self.logger.debug("First Sequence: {0}; Last Sequence: {1}",
                                self.playlist_sequences[0].num, self.playlist_sequences[-1].num)
              self.logger.debug("Start offset: {0}; Duration: {1}; Start Sequence: {2}; End Sequence: {3}",
                                self.duration_offset_start, self.duration_limit, self.playlist_sequence,
                                self.playlist_end)
  
      def reload_session(self):
          """ Replace the current stream with a new stream,
              the new stream will be generated from _get_streams()
              after the given reload time.
          """
          self.logger.debug("Reloading session for playlist")
          cache = Cache(
              filename="streamdata.json",
              key_prefix="cache:{0}".format(self.stream.url)
          )
          cache_stream_name = cache.get("cache_stream_name", "best")
          cache_url = cache.get("cache_url")
          if not cache_url:
              # corrupt cache data
              # if more than one instance of streamlink
              # with the same stream_url and hls-session-reload is running
              self.logger.warning("Missing cache data, hls-session-reload is now deactivated")
              self.session_time = int(time() + time())
              return
  
          channel = self.session.resolve_url(cache_url)
          streams = channel._get_streams()
          try:
              # HLSStream with parse_variant_playlist
              self.stream = streams[cache_stream_name]
          except KeyError:
              # if stream_name is '1080p source' but the cache is '1080p'
              for source_stream_name in streams.keys():
                  if cache_stream_name in source_stream_name:
                      self.stream = streams[source_stream_name]
          except TypeError:
              # HLSStream without parse_variant_playlist
              for name, hls_stream in streams:
                  self.stream = hls_stream
  
          new_cache = Cache(
              filename="streamdata.json",
              key_prefix="cache:{0}".format(self.stream.url)
          )
          new_cache.set("cache_stream_name", cache_stream_name, (self.session_reload + 60))
          new_cache.set("cache_url", cache_url, (self.session_reload + 60))
          self.session_time = int(time())
  
      def reload_playlist(self):
          if self.closed:
              return
  
          self.reader.buffer.wait_free()
          self.logger.debug("Reloading playlist")
          res = self.session.http.get(self.stream.url,
                                      exception=StreamError,
                                      retries=self.playlist_reload_retries,
                                      **self.reader.request_params)
          try:
              playlist = hls_playlist.load(res.text, res.url)
          except ValueError as err:
              raise StreamError(err)
  
          if playlist.is_master:
              raise StreamError("Attempted to play a variant playlist, use "
                                "'hls://{0}' instead".format(self.stream.url))
  
          if playlist.iframes_only:
              raise StreamError("Streams containing I-frames only is not playable")
  
          media_sequence = playlist.media_sequence or 0
          sequences = [Sequence(media_sequence + i, s)
                       for i, s in enumerate(playlist.segments)]
  
          if sequences:
              self.process_sequences(playlist, sequences)
  
      def process_sequences(self, playlist, sequences):
          first_sequence, last_sequence = sequences[0], sequences[-1]
  
          if first_sequence.segment.key and first_sequence.segment.key.method != "NONE":
              self.logger.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.playlist_reload_time = (playlist.target_duration or
                                       last_sequence.segment.duration)
          self.playlist_sequences = sequences
  
          if not self.playlist_changed:
              self.playlist_reload_time = max(self.playlist_reload_time / 2, 1)
  
          if playlist.is_endlist:
              self.playlist_end = last_sequence.num
  
          if self.playlist_sequence < 0:
              if self.playlist_end is None and not self.hls_live_restart:
                  edge_index = -(min(len(sequences), max(int(self.live_edge), 1)))
                  edge_sequence = sequences[edge_index]
                  self.playlist_sequence = edge_sequence.num
              else:
                  self.playlist_sequence = first_sequence.num
  
      def valid_sequence(self, sequence):
          if sequence.num >= self.playlist_sequence:
              return True
          elif self.sequence_ignore_number and sequence.num <= (self.playlist_sequence - self.sequence_ignore_number):
              self.logger.warning("Added invalid segment number.")
              return True
          else:
              return False
  
      def duration_to_sequence(self, duration, sequences):
          d = 0
          default = -1
  
          sequences_order = sequences if duration >= 0 else reversed(sequences)
  
          for sequence in sequences_order:
              if d >= abs(duration):
                  return sequence.num
              d += sequence.segment.duration
              default = sequence.num
  
          # could not skip far enough, so return the default
          return default
  
      def iter_segments(self):
          while not self.closed:
              if self.session_reload and (self.session_time + self.session_reload) < int(time()):
                  self.reload_session()
              for sequence in filter(self.valid_sequence, self.playlist_sequences):
                  self.logger.debug("Adding segment {0} to queue", sequence.num)
                  yield sequence
  
                  # End of stream
                  stream_end = self.playlist_end and sequence.num >= self.playlist_end
                  if self.closed or stream_end:
                      return
  
                  self.playlist_sequence = sequence.num + 1
  
              if self.wait(self.playlist_reload_time):
                  try:
                      self.reload_playlist()
                  except StreamError as err:
                      self.logger.warning("Failed to reload playlist: {0}", err)
                      if self.session_reload():
                          self.logger.warning("Unexpected session reload")
                          self.reload_session()
  
  
  class HLSStreamReader(SegmentedStreamReader):
      __worker__ = HLSStreamWorker
      __writer__ = HLSStreamWriter
  
      def __init__(self, stream, *args, **kwargs):
          SegmentedStreamReader.__init__(self, stream, *args, **kwargs)
          self.logger = stream.session.logger.new_module("stream.hls")
          self.request_params = dict(stream.args)
          self.timeout = stream.session.options.get("hls-timeout")
  
          # These params are reserved for internal use
          self.request_params.pop("exception", None)
          self.request_params.pop("stream", None)
          self.request_params.pop("timeout", None)
          self.request_params.pop("url", None)
  
  
  class MuxedHLSStream(MuxedStream):
      __shortname__ = "hls-multi"
  
      def __init__(self, session, video, audio, force_restart=False, ffmpeg_options=None, **args):
          substreams = map(lambda url: HLSStream(session, url, force_restart=force_restart, **args), [video, audio])
          ffmpeg_options = ffmpeg_options or {}
  
          super(MuxedHLSStream, self).__init__(session, *substreams, format="mpegts", **ffmpeg_options)
  
  
  class HLSStream(HTTPStream):
      """Implementation of the Apple HTTP Live Streaming protocol
  
      *Attributes:*
  
      - :attr:`url` The URL to the HLS playlist.
      - :attr:`args` A :class:`dict` containing keyword arguments passed
        to :meth:`requests.request`, such as headers and cookies.
  
      """
  
      __shortname__ = "hls"
  
      def __init__(self, session_, url, force_restart=False, start_offset=0, duration=None, **args):
          HTTPStream.__init__(self, session_, url, **args)
          self.force_restart = force_restart
          self.start_offset = start_offset
          self.duration = duration
  
      def __repr__(self):
          return "<HLSStream({0!r})>".format(self.url)
  
      def __json__(self):
          json = HTTPStream.__json__(self)
  
          # Pretty sure HLS is GET only.
          del json["method"]
          del json["body"]
  
          return json
  
      def open(self):
          reader = HLSStreamReader(self)
          reader.open()
  
          return reader
  
      @classmethod
      def parse_variant_playlist(cls, session_, url, name_key="name",
                                 name_prefix="", check_streams=False,
                                 force_restart=False, name_fmt=None,
                                 start_offset=0, duration=None,
                                 **request_params):
          """Attempts to parse a variant playlist and return its streams.
  
          :param url: The URL of the variant playlist.
          :param name_key: Prefer to use this key as stream name, valid keys are:
                           name, pixels, bitrate.
          :param name_prefix: Add this prefix to the stream names.
          :param check_streams: Only allow streams that are accessible.
          :param force_restart: Start at the first segment even for a live stream
          :param name_fmt: A format string for the name, allowed format keys are
                           name, pixels, bitrate.
          """
          logger = session_.logger.new_module("hls.parse_variant_playlist")
          locale = session_.localization
          # Backwards compatibility with "namekey" and "nameprefix" params.
          name_key = request_params.pop("namekey", name_key)
          name_prefix = request_params.pop("nameprefix", name_prefix)
          audio_select = session_.options.get("hls-audio-select")
  
          res = session_.http.get(url, exception=IOError, **request_params)
  
          try:
              parser = hls_playlist.load(res.text, base_uri=res.url)
          except ValueError as err:
              raise IOError("Failed to parse playlist: {0}".format(err))
  
          streams = {}
          for playlist in filter(lambda p: not p.is_iframe, parser.playlists):
              names = dict(name=None, pixels=None, bitrate=None)
              audio_streams = []
              fallback_audio = None
              default_audio = None
              preferred_audio = None
  
              for media in playlist.media:
                  if media.type == "VIDEO" and media.name:
                      names["name"] = media.name
                  elif media.type == "AUDIO":
                      audio_streams.append(media)
  
              for media in audio_streams:
                  if not fallback_audio and media.default:
                      fallback_audio = media
  
                  # if the media is "audoselect" and it better matches the users preferences, use that
                  # instead of default
                  if not default_audio and (media.autoselect and locale.equivalent(language=media.language)):
                      default_audio = media
  
                  # select the first audio stream that matches the users explict language selection
                  if ((media.language == audio_select or media.name == audio_select) or
                      ((not preferred_audio or media.default) and
                          locale.explicit and
                          locale.equivalent(language=media.language))):
                      preferred_audio = media
  
              # final fallback on the first audio stream listed
              fallback_audio = fallback_audio or (len(audio_streams) and audio_streams[0])
  
              if playlist.stream_info.resolution:
                  width, height = playlist.stream_info.resolution
                  names["pixels"] = "{0}p".format(height)
  
              if playlist.stream_info.bandwidth:
                  bw = playlist.stream_info.bandwidth
  
                  if bw >= 1000:
                      names["bitrate"] = "{0}k".format(int(bw / 1000.0))
                  else:
                      names["bitrate"] = "{0}k".format(bw / 1000.0)
  
              if name_fmt:
                  stream_name = name_fmt.format(**names)
              else:
                  stream_name = (names.get(name_key) or names.get("name") or
                                 names.get("pixels") or names.get("bitrate"))
  
              if not stream_name:
                  continue
              if stream_name in streams:  # rename duplicate streams
                  stream_name = "{0}_alt".format(stream_name)
                  num_alts = len(list(filter(lambda n: n.startswith(stream_name), streams.keys())))
  
                  # We shouldn't need more than 2 alt streams
                  if num_alts >= 2:
                      continue
                  elif num_alts > 0:
                      stream_name = "{0}{1}".format(stream_name, num_alts + 1)
  
              if check_streams:
                  try:
                      session_.http.get(playlist.uri, **request_params)
                  except KeyboardInterrupt:
                      raise
                  except Exception:
                      continue
  
              external_audio = preferred_audio or default_audio or fallback_audio
              if external_audio and external_audio.uri and FFMPEGMuxer.is_usable(session_):
                  logger.debug("Using external audio track for stream {0} (language={1}, name={2})".format(
                      name_prefix + stream_name,
                      external_audio.language, external_audio.name or "N/A"))
  
                  stream = MuxedHLSStream(session_,
                                          video=playlist.uri,
                                          audio=external_audio and external_audio.uri,
                                          force_restart=force_restart,
                                          start_offset=start_offset,
                                          duration=duration,
                                          **request_params)
              else:
                  stream = HLSStream(session_, playlist.uri, force_restart=force_restart,
                                     start_offset=start_offset, duration=duration, **request_params)
              streams[name_prefix + stream_name] = stream
  
          return streams
  