-
Notifications
You must be signed in to change notification settings - Fork 5
/
m3u8.py
202 lines (174 loc) · 7.01 KB
/
m3u8.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# -*- 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 <[email protected]>
# Copyright (C) 2014 Juan Font Alonso <[email protected]>
# 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)