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