hls-player: some modernization
[hls-player:hls-player.git] / HLS / fetcher.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 from itertools import ifilter
15 import logging
16 import os, os.path
17 import tempfile
18 import urlparse
19
20 from twisted.web import client
21 from twisted.internet import defer, reactor
22 from twisted.internet.task import deferLater
23
24 import HLS
25 from HLS.m3u8 import M3U8
26
27 class HLSFetcher(object):
28
29     def __init__(self, url, options, program=1):
30         self.url = url
31         self.path = options.path
32         self.referer = options.referer
33         if not self.path:
34             self.path = tempfile.mkdtemp()
35         self.program = program
36         self.bitrate = options.bitrate
37         self.n_segments_keep = options.keep
38
39         self._program_playlist = None
40         self._file_playlist = None
41         self._cookies = {}
42         self._cached_files = {}
43
44         self._files = None # the iter of the playlist files download
45         self._next_download = None # the delayed download defer, if any
46         self._file_playlisted = None # the defer to wait until new files are added to playlist
47
48     def _get_page(self, url):
49         def got_page(content):
50             logging.debug("Cookies: %r" % self._cookies)
51             return content
52         url = url.encode("utf-8")
53         if 'HLS_RESET_COOKIES' in os.environ.keys():
54             self._cookies = {}
55         headers = {}
56         if self.referer:
57             headers['Referer'] = self.referer
58         d = client.getPage(url, cookies=self._cookies, headers=headers)
59         d.addCallback(got_page)
60         return d
61
62     def _download_page(self, url, path):
63         # client.downloadPage does not support cookies!
64         def _check(x):
65             logging.debug("Received segment of %r bytes." % len(x))
66             return x
67
68         d = self._get_page(url)
69         f = open(path, 'w')
70         d.addCallback(_check)
71         d.addCallback(lambda x: f.write(x))
72         d.addBoth(lambda _: f.close())
73         d.addCallback(lambda _: path)
74         return d
75
76     def delete_cache(self, f):
77         keys = self._cached_files.keys()
78         for i in ifilter(f, keys):
79             filename = self._cached_files[i]
80             logging.debug("Removing %r" % filename)
81             os.remove(filename)
82             del self._cached_files[i]
83         self._cached_files
84
85     def _got_file(self, path, l, f):
86         logging.debug("Saved " + l + " in " + path)
87         self._cached_files[f['sequence']] = path
88         if self.n_segments_keep != -1:
89             self.delete_cache(lambda x: x <= f['sequence'] - self.n_segments_keep)
90         if self._new_filed:
91             self._new_filed.callback((path, l, f))
92             self._new_filed = None
93         return (path, l, f)
94
95     def _download_file(self, f):
96         l = HLS.make_url(self._file_playlist.url, f['file'])
97         name = urlparse.urlparse(f['file']).path.split('/')[-1]
98         path = os.path.join(self.path, name)
99         d = self._download_page(l, path)
100         d.addCallback(self._got_file, l, f)
101         return d
102
103     def _get_next_file(self, last_file=None):
104         next = self._files.next()
105         if next:
106             delay = 0
107             if last_file:
108                 if not self._cached_files.has_key(last_file['sequence'] - 1) or \
109                         not self._cached_files.has_key(last_file['sequence'] - 2):
110                     delay = 0
111                 elif self._file_playlist.endlist():
112                     delay = 1
113                 else:
114                     delay = 1 # last_file['duration'] doesn't work
115                               # when duration is not in sync with
116                               # player, which can happen easily...
117             return deferLater(reactor, delay, self._download_file, next)
118         elif not self._file_playlist.endlist():
119             self._file_playlisted = defer.Deferred()
120             self._file_playlisted.addCallback(lambda x: self._get_next_file(last_file))
121             return self._file_playlisted
122
123     def _handle_end(self, failure):
124         failure.trap(StopIteration)
125         print "End of media"
126         reactor.stop()
127
128     def _get_files_loop(self, last_file=None):
129         if last_file:
130             (path, l, f) = last_file
131         else:
132             f = None
133         d = self._get_next_file(f)
134         # and loop
135         d.addCallback(self._get_files_loop)
136         d.addErrback(self._handle_end)
137
138     def _playlist_updated(self, pl):
139         if pl.has_programs():
140             # if we got a program playlist, save it and start a program
141             self._program_playlist = pl
142             (program_url, _) = pl.get_program_playlist(self.program, self.bitrate)
143             l = HLS.make_url(self.url, program_url)
144             return self._reload_playlist(M3U8(l))
145         elif pl.has_files():
146             # we got sequence playlist, start reloading it regularly, and get files
147             self._file_playlist = pl
148             if not self._files:
149                 self._files = pl.iter_files()
150             if not pl.endlist():
151                 reactor.callLater(pl.reload_delay(), self._reload_playlist, pl)
152             if self._file_playlisted:
153                 self._file_playlisted.callback(pl)
154                 self._file_playlisted = None
155         else:
156             raise
157         return pl
158
159     def _got_playlist_content(self, content, pl):
160         if not pl.update(content):
161             # if the playlist cannot be loaded, start a reload timer
162             d = deferLater(reactor, pl.reload_delay(), self._fetch_playlist, pl)
163             d.addCallback(self._got_playlist_content, pl)
164             return d
165         return pl
166
167     def _fetch_playlist(self, pl):
168         logging.debug('fetching %r' % pl.url)
169         d = self._get_page(pl.url)
170         return d
171
172     def _reload_playlist(self, pl):
173         d = self._fetch_playlist(pl)
174         d.addCallback(self._got_playlist_content, pl)
175         d.addCallback(self._playlist_updated)
176         return d
177
178     def get_file(self, sequence):
179         d = defer.Deferred()
180         keys = self._cached_files.keys()
181         try:
182             sequence = ifilter(lambda x: x >= sequence, keys).next()
183             filename = self._cached_files[sequence]
184             d.callback(filename)
185         except:
186             d.addCallback(lambda x: self.get_file(sequence))
187             self._new_filed = d
188             keys.sort()
189             logging.debug('waiting for %r (available: %r)' % (sequence, keys))
190         return d
191
192     def start(self):
193         self._files = None
194         d = self._reload_playlist(M3U8(self.url))
195         d.addCallback(lambda _: self._get_files_loop())
196         self._new_filed = defer.Deferred()
197         return self._new_filed
198
199     def stop(self):
200         pass
201