123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202 |
- # -*- Mode: Python -*-
- # vi:si:et:sw=4:sts=4:ts=4
- #
- # Copyright (C) 2009-2010 Fluendo, S.L. (www.fluendo.com).
- # Copyright (C) 2009-2010 Marc-Andre Lureau <marcandre.lureau@gmail.com>
- # Copyright (C) 2014 Juan Font Alonso <juanfontalonso@gmail.com>
-
- # This file may be distributed and/or modified under the terms of
- # the GNU General Public License version 2 as published by
- # the Free Software Foundation.
- # This file is distributed without any warranty; without even the implied
- # warranty of merchantability or fitness for a particular purpose.
- # See "LICENSE" in the source distribution for more information.
-
- import logging
- import re
- import urllib2
-
- class M3U8(object):
-
- def __init__(self, url=None):
- self.url = url
- self._programs = [] # main list of programs & bandwidth
- self._files = {} # the current program playlist
- self._first_sequence = None # the first sequence to start fetching
- self._last_sequence = None # the last sequence, to compute reload delay
- self._reload_delay = None # the initial reload delay
- self._update_tries = None # the number consecutive reload tries
- self._last_content = None
- self._endlist = False # wether the list ended and should not be refreshed
- self._encryption_method = None
- self._key_url = None
- self._key = None
-
- def endlist(self):
- return self._endlist
-
- def has_programs(self):
- return len(self._programs) != 0
-
- def get_program_playlist(self, program_id=None, bitrate=None):
- # return the (uri, dict) of the best matching playlist
- if not self.has_programs():
- raise
- _, best = min((abs(int(x['BANDWIDTH']) - bitrate), x)
- for x in self._programs)
- return best['uri'], best
-
- def reload_delay(self):
- # return the time between request updates, in seconds
- if self._endlist or not self._last_sequence:
- raise
-
- if self._update_tries == 0:
- ld = self._files[self._last_sequence]['duration']
- self._reload_delay = min(self.target_duration * 3, ld)
- d = self._reload_delay
- elif self._update_tries == 1:
- d = self._reload_delay * 0.5
- elif self._update_tries == 2:
- d = self._reload_delay * 1.5
- else:
- d = self._reload_delay * 3.0
-
- logging.debug('Reload delay is %r' % d)
- return int(d)
-
- def has_files(self):
- return len(self._files) != 0
-
- def iter_files(self):
- # return an iter on the playlist media files
- if not self.has_files():
- return
-
- if not self._endlist:
- current = max(self._first_sequence, self._last_sequence - 3)
- else:
- # treat differently on-demand playlists?
- current = self._first_sequence
-
- while True:
- try:
- f = self._files[current]
- current += 1
- yield f
- if (f.has_key('endlist')):
- break
- except:
- yield None
-
- def update(self, content):
- # update this "constructed" playlist,
- # return wether it has actually been updated
- if self._last_content and content == self._last_content:
- logging.info("Content didn't change")
- self._update_tries += 1
- return False
-
- self._update_tries = 0
- self._last_content = content
-
- def get_lines_iter(c):
- c = c.decode("utf-8-sig")
- for l in c.split('\n'):
- if l.startswith('#EXT'):
- yield l
- elif l.startswith('#'):
- pass
- else:
- yield l
-
- self._lines = get_lines_iter(content)
-
- first_line = self._lines.next()
-
- if not first_line.startswith('#EXTM3U'):
- logging.error('Invalid first line: %r' % first_line)
- raise
-
- self.target_duration = None
- discontinuity = False
- allow_cache = None
- i = 0
- new_files = []
- for l in self._lines:
- if l.startswith('#EXT-X-STREAM-INF'):
- def to_dict(l):
- i = re.findall('(?:[\w-]*="[\w\.\,]*")|(?:[\w-]*=[\w]*)', l)
- d = {v.split('=')[0]: v.split('=')[1].replace('"','') for v in i}
- return d
- d = to_dict(l[18:])
- print "stream info: " + str(d)
- d['uri'] = self._lines.next()
- self._add_playlist(d)
- elif l.startswith('#EXT-X-TARGETDURATION'):
- self.target_duration = int(l[22:])
- elif l.startswith('#EXT-X-MEDIA-SEQUENCE'):
- self.media_sequence = int(l[22:])
- i = self.media_sequence
- elif l.startswith('#EXT-X-DISCONTINUITY'):
- discontinuity = True
- elif l.startswith('#EXT-X-PROGRAM-DATE-TIME'):
- print l
- elif l.startswith('#EXT-X-ALLOW-CACHE'):
- allow_cache = l[19:]
- elif l.startswith('#EXT-X-KEY'):
- self._encryption_method = l.split(',')[0][18:]
- self._key_url = l.split(',')[1][5:-1]
- response = urllib2.urlopen(self._key_url)
- self._key = response.read()
- response.close()
- elif l.startswith('#EXTINF'):
- v = l[8:].split(',')
- d = dict(file=self._lines.next().strip(),
- title=v[1].strip(),
- duration=float(v[0]),
- sequence=i,
- discontinuity=discontinuity,
- allow_cache=allow_cache)
- discontinuity = False
- i += 1
- new = self._set_file(i, d)
- if i > self._last_sequence:
- self._last_sequence = i
- if new:
- new_files.append(d)
-
-
- elif l.startswith('#EXT-X-ENDLIST'):
- if i > 0:
- self._files[i]['endlist'] = True
- self._endlist = True
- elif l.startswith('#EXT-X-VERSION'):
- pass
- elif len(l.strip()) != 0:
- print l
-
- if not self.has_programs() and not self.target_duration:
- logging.error("Invalid HLS stream: no programs & no duration")
- raise
- if len(new_files):
- logging.debug("got new files in playlist: %r", new_files)
-
- return True
-
- def _add_playlist(self, d):
- self._programs.append(d)
-
- def _set_file(self, sequence, d):
- new = False
- if not self._files.has_key(sequence):
- new = True
- if not self._first_sequence:
- self._first_sequence = sequence
- elif sequence < self._first_sequence:
- self._first_sequence = sequence
- self._files[sequence] = d
- return new
-
- def __repr__(self):
- return "M3U8 %r %r" % (self._programs, self._files)
|