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