fetcher: errback when cannot retrieve file
[hls-player:hls-player.git] / HLS / m3u8.py
1 # -*- Mode: Python -*-
2 # vi:si:et:sw=4:sts=4:ts=4
3 #
4 # Copyright (C) 2009-2010 Fluendo, S.L. (www.fluendo.com).
5 # Copyright (C) 2009-2010 Marc-Andre Lureau <marcandre.lureau@gmail.com>
6
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.
13
14 import logging
15
16
17 class M3U8(object):
18
19     def __init__(self, url=None):
20         self.url = url
21
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
30
31     def endlist(self):
32         return self._endlist
33
34     def has_programs(self):
35         return len(self._programs) != 0
36
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():
40             raise
41         _, best = min((abs(int(x['BANDWIDTH']) - bitrate), x)
42                 for x in self._programs)
43         return best['uri'], best
44
45     def reload_delay(self):
46         # return the time between request updates, in seconds
47         if self._endlist or not self._last_sequence:
48             raise
49
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
58         else:
59             d = self._reload_delay * 3.0
60
61         logging.debug('Reload delay is %r' % d)
62         return int(d)
63
64     def has_files(self):
65         return len(self._files) != 0
66
67     def iter_files(self):
68         # return an iter on the playlist media files
69         if not self.has_files():
70             return
71
72         if not self._endlist:
73             current = max(self._first_sequence, self._last_sequence - 3)
74         else:
75             # treat differently on-demand playlists?
76             current = self._first_sequence
77
78         while True:
79             try:
80                 f = self._files[current]
81                 current += 1
82                 yield f
83                 if (f.has_key('endlist')):
84                     break
85             except:
86                 yield None
87
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
94             return False
95
96         self._update_tries = 0
97         self._last_content = content
98
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'):
103                     yield l
104                 elif l.startswith('#'):
105                     pass
106                 else:
107                     yield l
108
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)
113             raise
114
115         self.target_duration = None
116         discontinuity = False
117         allow_cache = None
118         i = 0
119         new_files = []
120         for l in self._lines:
121             if l.startswith('#EXT-X-STREAM-INF'):
122                 def to_dict(l):
123                     i = (f.split('=') for f in l.split(','))
124                     d = dict((k.strip(), v.strip()) for (k,v) in i)
125                     return d
126                 d = to_dict(l[18:])
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'):
135                 discontinuity = True
136             elif l.startswith('#EXT-X-PROGRAM-DATE-TIME'):
137                 print l
138             elif l.startswith('#EXT-X-ALLOW-CACHE'):
139                 allow_cache = l[19:]
140             elif l.startswith('#EXTINF'):
141                 v = l[8:].split(',')
142                 d = dict(file=self._lines.next().strip(),
143                          title=v[1].strip(),
144                          duration=int(v[0]),
145                          sequence=i,
146                          discontinuity=discontinuity,
147                          allow_cache=allow_cache)
148                 discontinuity = False
149                 i += 1
150                 new = self._set_file(i, d)
151                 if i > self._last_sequence:
152                     self._last_sequence = i
153                 if new:
154                     new_files.append(d)
155             elif l.startswith('#EXT-X-ENDLIST'):
156                 if i > 0:
157                     self._files[i]['endlist'] = True
158                 self._endlist = True
159             elif len(l.strip()) != 0:
160                 print l
161
162         if not self.has_programs() and not self.target_duration:
163             logging.error("Invalid HLS stream: no programs & no duration")
164             raise
165         if len(new_files):
166             logging.debug("got new files in playlist: %r", new_files)
167
168         return True
169
170     def _add_playlist(self, d):
171         self._programs.append(d)
172
173     def _set_file(self, sequence, d):
174         new = False
175         if not self._files.has_key(sequence):
176             new = True
177         if not self._first_sequence:
178             self._first_sequence = sequence
179         elif sequence < self._first_sequence:
180             self._first_sequence = sequence
181         self._files[sequence] = d
182         return new
183
184     def __repr__(self):
185         return "M3U8 %r %r" % (self._programs, self._files)