2 # vi:si:et:sw=4:sts=4:ts=4
4 # Copyright (C) 2009-2010 Fluendo, S.L. (www.fluendo.com).
5 # Copyright (C) 2009-2010 Marc-Andre Lureau <marcandre.lureau@gmail.com>
7 # This file may be distributed and/or modified under the terms of
8 # the GNU General Public License version 2 as published by
9 # the Free Software Foundation.
10 # This file is distributed without any warranty; without even the implied
11 # warranty of merchantability or fitness for a particular purpose.
12 # See "LICENSE" in the source distribution for more information.
19 def __init__(self, url=None):
22 self._programs = [] # main list of programs & bandwidth
23 self._files = {} # the current program playlist
24 self._first_sequence = None # the first sequence to start fetching
25 self._last_sequence = None # the last sequence, to compute reload delay
26 self._reload_delay = None # the initial reload delay
27 self._update_tries = None # the number consecutive reload tries
28 self._last_content = None
29 self._endlist = False # wether the list ended and should not be refreshed
34 def has_programs(self):
35 return len(self._programs) != 0
37 def get_program_playlist(self, program_id=None, bitrate=None):
38 # return the (uri, dict) of the best matching playlist
39 if not self.has_programs():
41 _, best = min((abs(int(x['BANDWIDTH']) - bitrate), x)
42 for x in self._programs)
43 return best['uri'], best
45 def reload_delay(self):
46 # return the time between request updates, in seconds
47 if self._endlist or not self._last_sequence:
50 if self._update_tries == 0:
51 ld = self._files[self._last_sequence]['duration']
52 self._reload_delay = min(self.target_duration * 3, ld)
53 d = self._reload_delay
54 elif self._update_tries == 1:
55 d = self._reload_delay * 0.5
56 elif self._update_tries == 2:
57 d = self._reload_delay * 1.5
59 d = self._reload_delay * 3.0
61 logging.debug('Reload delay is %r' % d)
65 return len(self._files) != 0
68 # return an iter on the playlist media files
69 if not self.has_files():
73 current = max(self._first_sequence, self._last_sequence - 3)
75 # treat differently on-demand playlists?
76 current = self._first_sequence
80 f = self._files[current]
83 if (f.has_key('endlist')):
88 def update(self, content):
89 # update this "constructed" playlist,
90 # return wether it has actually been updated
91 if self._last_content and content == self._last_content:
92 logging.info("Content didn't change")
93 self._update_tries += 1
96 self._update_tries = 0
97 self._last_content = content
99 def get_lines_iter(c):
100 c = c.decode("utf-8-sig")
101 for l in c.split('\n'):
102 if l.startswith('#EXT'):
104 elif l.startswith('#'):
109 self._lines = get_lines_iter(content)
110 first_line = self._lines.next()
111 if not first_line.startswith('#EXTM3U'):
112 logging.error('Invalid first line: %r' % first_line)
115 self.target_duration = None
116 discontinuity = False
120 for l in self._lines:
121 if l.startswith('#EXT-X-STREAM-INF'):
123 i = (f.split('=') for f in l.split(','))
124 d = dict((k.strip(), v.strip()) for (k,v) in i)
127 d['uri'] = self._lines.next()
128 self._add_playlist(d)
129 elif l.startswith('#EXT-X-TARGETDURATION'):
130 self.target_duration = int(l[22:])
131 elif l.startswith('#EXT-X-MEDIA-SEQUENCE'):
132 self.media_sequence = int(l[22:])
133 i = self.media_sequence
134 elif l.startswith('#EXT-X-DISCONTINUITY'):
136 elif l.startswith('#EXT-X-PROGRAM-DATE-TIME'):
138 elif l.startswith('#EXT-X-ALLOW-CACHE'):
140 elif l.startswith('#EXTINF'):
142 d = dict(file=self._lines.next().strip(),
145 discontinuity=discontinuity,
146 allow_cache=allow_cache)
148 d['title'] = v[1].strip()
149 discontinuity = False
151 new = self._set_file(i, d)
152 if i > self._last_sequence:
153 self._last_sequence = i
156 elif l.startswith('#EXT-X-ENDLIST'):
158 self._files[i]['endlist'] = True
160 elif len(l.strip()) != 0:
163 if not self.has_programs() and not self.target_duration:
164 logging.error("Invalid HLS stream: no programs & no duration")
167 logging.debug("got new files in playlist: %r", new_files)
171 def _add_playlist(self, d):
172 self._programs.append(d)
174 def _set_file(self, sequence, d):
176 if not self._files.has_key(sequence):
178 if not self._first_sequence:
179 self._first_sequence = sequence
180 elif sequence < self._first_sequence:
181 self._first_sequence = sequence
182 self._files[sequence] = d
186 return "M3U8 %r %r" % (self._programs, self._files)