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