fetcher: use task loop for regular fetches
[hls-player:hls-player.git] / HLS / player.py
1 #!/usr/bin/env python
2 # -*- Mode: Python -*-
3 # vi:si:et:sw=4:sts=4:ts=4
4 #
5 # Copyright (C) 2009-2010 Fluendo, S.L. (www.fluendo.com).
6 # Copyright (C) 2009-2010 Marc-Andre Lureau <marcandre.lureau@gmail.com>
7 # Copyright (C) 2010 Zaheer Abbas Merali  <zaheerabbas at merali dot org>
8 # Copyright (C) 2010 Andoni Morales Alastruey <ylatuya@gmail.com>
9
10 # This file may be distributed and/or modified under the terms of
11 # the GNU General Public License version 2 as published by
12 # the Free Software Foundation.
13 # This file is distributed without any warranty; without even the implied
14 # warranty of merchantability or fitness for a particular purpose.
15 # See "LICENSE" in the source distribution for more information.
16
17 import sys
18 import urlparse
19 import optparse
20 import logging
21 import os
22
23 import pygtk, gtk, gobject
24 gobject.threads_init()
25
26 from twisted.internet import gtk2reactor
27 gtk2reactor.install()
28 from twisted.internet import reactor
29 from twisted.python import log
30
31 from HLS import __version__
32 from HLS.fetcher import HLSFetcher
33 from HLS.m3u8 import M3U8
34
35 if sys.version_info < (2, 4):
36     raise ImportError("Cannot run with Python version < 2.4")
37
38
39 class HLSControler:
40
41     def __init__(self, fetcher=None):
42         self.fetcher = fetcher
43         self.player = None
44
45         self._player_sequence = None
46         self._n_segments_keep = None
47
48     def set_player(self, player):
49         self.player = player
50         if player:
51             self.player.connect_about_to_finish(self.on_player_about_to_finish)
52             self._n_segments_keep = self.fetcher.n_segments_keep
53             self.fetcher.n_segments_keep = -1
54
55     def _start(self, first_file):
56         (path, l, f) = first_file
57         self._player_sequence = f['sequence']
58         if self.player:
59             self.player.set_uri(path)
60             self.player.play()
61
62     def start(self):
63         d = self.fetcher.start()
64         d.addCallback(self._start)
65
66     def _set_next_uri(self):
67         # keep only the past three segments
68         if self._n_segments_keep != -1:
69             self.fetcher.delete_cache(lambda x:
70                 x <= self._player_sequence - self._n_segments_keep)
71         self._player_sequence += 1
72         d = self.fetcher.get_file(self._player_sequence)
73         d.addCallback(self.player.set_uri)
74
75     def on_player_about_to_finish(self):
76         reactor.callFromThread(self._set_next_uri)
77
78
79 class GSTPlayer:
80
81     def __init__(self, display=True):
82         import pygst
83         import gst
84         if display:
85             self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
86             self.window.set_title("Video-Player")
87             self.window.set_default_size(500, 400)
88             self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG)
89             self.window.connect('delete-event', lambda _: reactor.stop())
90             self.movie_window = gtk.DrawingArea()
91             self.window.add(self.movie_window)
92             self.window.show_all()
93
94         self.player = gst.Pipeline("player")
95         self.appsrc = gst.element_factory_make("appsrc", "source")
96         self.appsrc.connect("enough-data", self.on_enough_data)
97         self.appsrc.connect("need-data", self.on_need_data)
98         self.appsrc.set_property("max-bytes", 10000)
99         if display:
100             self.decodebin = gst.element_factory_make("decodebin2", "decodebin")
101             self.decodebin.connect("new-decoded-pad", self.on_decoded_pad)
102             self.player.add(self.appsrc, self.decodebin)
103             gst.element_link_many(self.appsrc, self.decodebin)
104         else:
105             sink = gst.element_factory_make("filesink", "filesink")
106             sink.set_property("location", "/tmp/hls-player.ts")
107             self.player.add(self.appsrc, sink)
108             gst.element_link_many(self.appsrc, sink)
109         bus = self.player.get_bus()
110         bus.add_signal_watch()
111         bus.enable_sync_message_emission()
112         bus.connect("message", self.on_message)
113         bus.connect("sync-message::element", self.on_sync_message)
114         self._playing = False
115         self._need_data = False
116         self._cb = None
117
118     def need_data(self):
119         return self._need_data
120
121     def play(self):
122         import gst
123         self.player.set_state(gst.STATE_PLAYING)
124         self._playing = True
125
126     def stop(self):
127         import gst
128         self.player.set_state(gst.STATE_NULL)
129         self._playing = False
130
131     def set_uri(self, filepath):
132         import gst
133         logging.debug("Pushing %r to appsrc" % filepath)
134         # FIXME: BIG hack to reduce the initial starting time...
135         queue0 = self.decodebin.get_by_name("multiqueue0")
136         if queue0:
137             queue0.set_property("max-size-bytes", 100000)
138         f = open(filepath)
139         self.appsrc.emit('push-buffer', gst.Buffer(f.read()))
140
141     def on_message(self, bus, message):
142         import gst
143         t = message.type
144         if t == gst.MESSAGE_EOS:
145             self.player.set_state(gst.STATE_NULL)
146         elif t == gst.MESSAGE_ERROR:
147             self.player.set_state(gst.STATE_NULL)
148             err, debug = message.parse_error()
149             print "Error: %s" % err, debug
150         elif t == gst.MESSAGE_STATE_CHANGED:
151             if message.src == self.player:
152                 o, n, p = message.parse_state_changed()
153
154     def on_sync_message(self, bus, message):
155         logging.debug("GstMessage: %r" % (message,))
156         if message.structure is None:
157             return
158         message_name = message.structure.get_name()
159         if message_name == "prepare-xwindow-id":
160             imagesink = message.src
161             gtk.gdk.threads_enter()
162             gtk.gdk.display_get_default().sync()
163             imagesink.set_property("force-aspect-ratio", True)
164             imagesink.set_xwindow_id(self.movie_window.window.xid)
165             gtk.gdk.threads_leave()
166
167     def on_decoded_pad(self, decodebin, pad, more_pad):
168         import gst
169         c = pad.get_caps().to_string()
170         if "video" in c:
171             q1 = gst.element_factory_make("queue", "vqueue")
172             q1.props.max_size_buffers = 0
173             q1.props.max_size_time = 0
174             #q1.props.max_size_bytes = 0
175             colorspace = gst.element_factory_make("ffmpegcolorspace", "colorspace")
176             if 'HLS_VIDEOSINK' in os.environ.keys():
177                 videosink = gst.element_factory_make(os.environ['HLS_VIDEOSINK'], "videosink")
178             else:
179                 videosink = gst.element_factory_make("xvimagesink", "videosink")
180             self.player.add(q1, colorspace, videosink)
181             gst.element_link_many(q1, colorspace, videosink)
182             for e in [q1, colorspace, videosink]:
183                 e.set_state(gst.STATE_PLAYING)
184             sink_pad = q1.get_pad("sink")
185             pad.link(sink_pad)
186         elif "audio" in c:
187             q2 = gst.element_factory_make("queue", "aqueue")
188             q2.props.max_size_buffers = 0
189             q2.props.max_size_time = 0
190             #q2.props.max_size_bytes = 0
191             audioconv = gst.element_factory_make("audioconvert", "audioconv")
192             audioresample =  gst.element_factory_make("audioresample", "ar")
193             if 'HLS_AUDIOSINK' in os.environ.keys():
194                 audiosink = gst.element_factory_make(os.environ['HLS_AUDIOSINK'], "audiosink")
195             else:
196                 audiosink = gst.element_factory_make("autoaudiosink", "audiosink")
197
198             self.player.add(q2, audioconv, audioresample, audiosink)
199             gst.element_link_many(q2, audioconv, audioresample, audiosink)
200             for e in [q2, audioconv, audioresample, audiosink]:
201                 e.set_state(gst.STATE_PLAYING)
202             sink_pad = q2.get_pad("sink")
203             pad.link(sink_pad)
204
205     def on_enough_data(self):
206         logging.info("Player is full up!");
207         self._need_data = False;
208
209     def on_need_data(self, src, length):
210         self._need_data = True;
211         self._on_about_to_finish()
212
213     def _on_about_to_finish(self, p=None):
214         if self._cb:
215             self._cb()
216
217     def connect_about_to_finish(self, cb):
218         self._cb = cb
219
220
221 def main():
222     gtk.gdk.threads_init()
223
224     parser = optparse.OptionParser(usage='%prog [options] url...',
225                                    version="%prog " + __version__)
226
227     parser.add_option('-v', '--verbose', action="store_true",
228                       dest='verbose', default=False,
229                       help='print some debugging (default: %default)')
230     parser.add_option('-b', '--bitrate', action="store",
231                       dest='bitrate', default=200000, type="int",
232                       help='desired bitrate (default: %default)')
233     parser.add_option('-u', '--buffer', action="store", metavar="N",
234                       dest='buffer', default=3, type="int",
235                       help='pre-buffer N segments at start')
236     parser.add_option('-k', '--keep', action="store",
237                       dest='keep', default=3, type="int",
238                       help='number of segments ot keep (default: %default, -1: unlimited)')
239     parser.add_option('-r', '--referer', action="store", metavar="URL",
240                       dest='referer', default=None,
241                       help='Sends the "Referer Page" information with URL')
242     parser.add_option('-D', '--no-display', action="store_true",
243                       dest='nodisplay', default=False,
244                       help='display no video (default: %default)')
245     parser.add_option('-s', '--save', action="store_true",
246                       dest='save', default=False,
247                       help='save instead of watch (saves to /tmp/hls-player.ts)')
248     parser.add_option('-p', '--path', action="store", metavar="PATH",
249                       dest='path', default=None,
250                       help='download files to PATH')
251     parser.add_option('-n', '--number', action="store",
252                       dest='n', default=1, type="int",
253                       help='number of player to start (default: %default)')
254
255     options, args = parser.parse_args()
256
257     if len(args) == 0:
258         parser.print_help()
259         sys.exit(1)
260
261     log.PythonLoggingObserver().start()
262     if options.verbose:
263         logging.basicConfig(level=logging.DEBUG,
264                             format='%(asctime)s %(levelname)-8s %(message)s',
265                             datefmt='%d %b %Y %H:%M:%S')
266
267     for url in args:
268         for l in range(options.n):
269
270             if urlparse.urlsplit(url).scheme == '':
271                 url = "http://" + url
272
273             c = HLSControler(HLSFetcher(url, options))
274             if not options.nodisplay:
275                 p = GSTPlayer(display = not options.save)
276                 c.set_player(p)
277
278             c.start()
279
280     reactor.run()
281
282
283 if __name__ == '__main__':
284     sys.exit(main())