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