root/deejayd/player/xine.py

Revision 1500, 19.5 kB (checked in by Mickael Royer <mickael.royer@…>, 9 days ago)

[deejayd] improve webradio mode

  • now, we can add multiple urls for a webradio
  • add shoutcast support as a deejayd plugin
Line 
1# Deejayd, a media player daemon
2# Copyright (C) 2007-2009 Mickael Royer <mickael.royer@gmail.com>
3#                         Alexandre Rossi <alexandre.rossi@gmail.com>
4#
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation; either version 2 of the License, or
8# (at your option) any later version.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License along
16# with this program; if not, write to the Free Software Foundation, Inc.,
17# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18
19import os, subprocess
20from os import path
21import kaa.metadata
22from twisted.internet import reactor
23from pytyxi import xine
24
25from deejayd.player import PlayerError
26from deejayd.player._base import *
27from deejayd.ui import log
28
29
30class XinePlayer(UnknownPlayer):
31    name = "xine"
32    supported_extensions = None
33    plugins = None
34
35    def __init__(self,db,config):
36        UnknownPlayer.__init__(self,db,config)
37        self.__xine_options = {
38            "video": self.config.get("xine", "video_output"),
39            "display" : self.config.get("xine", "video_display"),
40            "osd_support" : self.config.getboolean("xine", "osd_support"),
41            "osd_font_size" : self.config.getint("xine", "osd_font_size"),
42            "software_mixer": self.config.getboolean("xine", "software_mixer"),
43            }
44        self.__video_aspects = {
45                "auto": xine.Stream.XINE_VO_ASPECT_AUTO,
46                "1:1": xine.Stream.XINE_VO_ASPECT_SQUARE,
47                "4:3": xine.Stream.XINE_VO_ASPECT_4_3,
48                "16:9": xine.Stream.XINE_VO_ASPECT_ANAMORPHIC,
49                "2.11:1": xine.Stream.XINE_VO_ASPECT_DVB,
50            }
51        self.__default_aspect_ratio = "auto"
52
53        # init main instance
54        try:
55            self.__xine = xine.XinePlayer()
56        except xine.XineError:
57            raise PlayerError(_("Unable to init a xine instance"))
58
59        # init vars
60        self.__supports_gapless = self.__xine.has_gapless()
61
62        self.__audio_volume = 100
63        self.__video_volume = 100
64
65        self.__window = None
66        self.__stream = None
67        self.__osd = None
68
69    def start_play(self):
70        super(XinePlayer, self).start_play()
71        if not self._media_file: return
72
73        # format correctly the uri
74        uri = self._media_file["uri"].encode("utf-8")
75        # For dvd chapter
76        if "chapter" in self._media_file.keys() and \
77                    self._media_file["chapter"] != -1:
78            uri += ".%d" % self._media_file["chapter"]
79        # load subtitle
80        if "external_subtitle" in self._media_file and \
81                self._media_file["external_subtitle"].startswith("file://"):
82            # external subtitle
83            uri += "#subtitle:%s" \
84                    % self._media_file["external_subtitle"].encode("utf-8")
85            self._media_file["subtitle"] = [{"lang": "none", "ix": -2},\
86                                            {"lang": "auto", "ix": -1},\
87                                            {"lang": "external", "ix":0}]
88        elif "subtitle_channels" in self._media_file.keys() and\
89                int(self._media_file["subtitle_channels"]) > 0:
90            self._media_file["subtitle"] = [{"lang": "none", "ix": -2},\
91                    {"lang": "auto", "ix": -1}]
92            for i in range(int(self._media_file["subtitle_channels"])):
93                self._media_file["subtitle"].append(\
94                    {"lang": _("Sub channel %d") % (i+1,), "ix": i})
95        # audio channels
96        if "audio_channels" in self._media_file.keys() and \
97                int(self._media_file["audio_channels"]) > 1:
98            audio_channels = [{"lang":"none","ix":-2},{"lang":"auto","ix":-1}]
99            for i in range(int(self._media_file["audio_channels"])):
100                audio_channels.append(\
101                        {"lang": _("Audio channel %d") % (i+1,), "ix": i})
102            self._media_file["audio"] = audio_channels
103
104        needs_video = self.current_is_video()
105        if self.__stream:
106            stream_should_change = (needs_video and\
107                                    not self.__stream.has_video())\
108                                or (not needs_video and
109                                    self.__stream.has_video())
110        else:
111            stream_should_change = True
112        if stream_should_change:
113            self._create_stream(needs_video)
114
115        def open_uri(uri):
116            try:
117                self.__stream.open(uri)
118                self.__stream.play(0, 0)
119            except xine.XineError:
120                msg = _("Unable to play file %s") % uri
121                log.err(msg)
122                raise PlayerError(msg)
123
124        if self._media_file["type"] == "webradio":
125            while True:
126                try: open_uri(self._media_file["uri"])
127                except PlayerError, ex:
128                    if self._media_file["url-index"] < \
129                                            len(self._media_file["urls"])-1:
130                        self._media_file["url-index"] += 1
131                        self._media_file["uri"] = \
132                                self._media_file["urls"]\
133                                [self._media_file["url-index"]].encode("utf-8")
134                    else:
135                        raise ex
136                else:
137                    break
138        else:
139            try: open_uri(uri)
140            except PlayerError, ex:
141                self._destroy_stream()
142                raise ex
143
144        if self.__window:
145            self.__window.show(self.current_is_video())
146
147        # init video information
148        if needs_video:
149            self._media_file["av_offset"] = 0
150            self._media_file["zoom"] = 100
151            if "audio" in self._media_file:
152                self._media_file["audio_idx"] = self.__stream.get_param(\
153                        xine.Stream.XINE_PARAM_AUDIO_CHANNEL_LOGICAL)
154            if "subtitle" in self._media_file:
155                self._media_file["sub_offset"] = 0
156                self._media_file["subtitle_idx"] = self.__stream.get_param(\
157                        xine.Stream.XINE_PARAM_SPU_CHANNEL)
158            # set video aspect ration to default value
159            self.set_aspectratio(self.__default_aspect_ratio)
160
161    def _change_file(self, new_file, gapless = False):
162        sig = self.get_state() == PLAYER_STOP and True or False
163        if self._media_file == None\
164                or new_file == None\
165                or self._media_file["type"] != new_file["type"]:
166            self._destroy_stream()
167            gapless = False
168
169        self._media_file = new_file
170        if gapless and self.__supports_gapless:
171            self.__stream.set_param(xine.Stream.XINE_PARAM_GAPLESS_SWITCH, 1)
172        self.start_play()
173        if gapless and self.__supports_gapless:
174            self.__stream.set_param(xine.Stream.XINE_PARAM_GAPLESS_SWITCH, 0)
175
176        # replaygain reset
177        self.set_volume(self.get_volume(), sig=False)
178
179        if sig: self.dispatch_signame('player.status')
180        self.dispatch_signame('player.current')
181
182    def pause(self):
183        if self.get_state() == PLAYER_PAUSE:
184            self.__stream.set_param(xine.Stream.XINE_PARAM_SPEED,
185                                    xine.Stream.XINE_SPEED_NORMAL)
186        elif self.get_state() == PLAYER_PLAY:
187            self.__stream.set_param(xine.Stream.XINE_PARAM_SPEED,
188                                    xine.Stream.XINE_SPEED_PAUSE)
189        else: return
190        self.dispatch_signame('player.status')
191
192    def stop(self):
193        if self.get_state() != PLAYER_STOP:
194            self._source.queue_reset()
195            self._change_file(None)
196            self.dispatch_signame('player.status')
197
198    def set_zoom(self, zoom):
199        if zoom > xine.Stream.XINE_VO_ZOOM_MAX\
200        or zoom < xine.Stream.XINE_VO_ZOOM_MIN:
201            raise PlayerError(_("Zoom value not accepted"))
202        self.__stream.set_param(xine.Stream.XINE_PARAM_VO_ZOOM_X, zoom)
203        self.__stream.set_param(xine.Stream.XINE_PARAM_VO_ZOOM_Y, zoom)
204        self._media_file["zoom"] = zoom
205        self._osd_set(_("Zoom: %d percent") % zoom)
206
207    def set_aspectratio(self, aspect_ratio):
208        try: asp = self.__video_aspects[aspect_ratio]
209        except KeyError:
210            raise PlayerError(_("Video aspect ration %s is not known.")\
211                    % aspect_ratio)
212        self.__default_aspect_ratio = aspect_ratio
213        self._media_file["aspect_ratio"] = self.__default_aspect_ratio
214        if self.__stream.has_video():
215            self.__stream.set_param(xine.Stream.XINE_PARAM_VO_ASPECT_RATIO, asp)
216
217    def set_avoffset(self, offset):
218        self.__stream.set_param(xine.Stream.XINE_PARAM_AV_OFFSET, offset * 90)
219        self._media_file["av_offset"] = offset
220        self._osd_set(_("Audio/Video offset: %d ms") % offset)
221
222    def set_suboffset(self, offset):
223        if "subtitle" in self._media_file.keys():
224            self.__stream.set_param(xine.Stream.XINE_PARAM_SPU_OFFSET,
225                                    offset * 90)
226            self._media_file["sub_offset"] = offset
227            self._osd_set(_("Subtitle offset: %d ms") % offset)
228
229    def _player_set_alang(self,lang_idx):
230        self.__stream.set_param(xine.Stream.XINE_PARAM_AUDIO_CHANNEL_LOGICAL,
231                                lang_idx)
232
233    def _player_set_slang(self,lang_idx):
234        self.__stream.set_param(xine.Stream.XINE_PARAM_SPU_CHANNEL, lang_idx)
235
236    def _player_get_alang(self):
237        return self.__stream.get_param(xine.Stream.\
238                XINE_PARAM_AUDIO_CHANNEL_LOGICAL)
239
240    def _player_get_slang(self):
241        return self.__stream.get_param(xine.Stream.XINE_PARAM_SPU_CHANNEL)
242
243    def get_volume(self):
244        if self.current_is_video():
245            return self.__video_volume
246        else:
247            return self.__audio_volume
248
249    def set_volume(self, vol, sig = True):
250        new_volume = min(100, int(vol))
251        if self.current_is_video():
252            self.__video_volume = new_volume
253        else:
254            self.__audio_volume = new_volume
255
256        # replaygain support
257        vol = self.get_volume()
258        if self._replaygain and self._media_file is not None:
259            try: scale = self._media_file.replay_gain()
260            except AttributeError: pass # replaygain not supported
261            else:
262                vol = max(0.0, min(4.0, float(vol)/100.0 * scale))
263                vol = min(100, int(vol * 100))
264        if self.__stream:
265            self.__stream.set_volume(vol)
266        if sig:
267            self._osd_set("Volume: %d" % self.get_volume())
268            self.dispatch_signame('player.status')
269
270    def get_position(self):
271        if not self.__stream: return 0
272        return self.__stream.get_pos()
273
274    def _set_position(self,pos):
275        pos = int(pos * 1000)
276        state = self.get_state()
277        if state == PLAYER_PAUSE:
278            self.__stream.play(0, pos)
279            self.__stream.set_param(xine.Stream.XINE_PARAM_SPEED,
280                                    xine.Stream.XINE_SPEED_PAUSE)
281        elif state == PLAYER_PLAY:
282            self.__stream.play(0, pos)
283        self.dispatch_signame('player.status')
284
285    def get_state(self):
286        if not self.__stream: return PLAYER_STOP
287
288        status = self.__stream.get_status()
289        if status == xine.Stream.XINE_STATUS_PLAY:
290            if self.__stream.get_param(xine.Stream.XINE_PARAM_SPEED)\
291               == xine.Stream.XINE_SPEED_NORMAL:
292                return PLAYER_PLAY
293            return PLAYER_PAUSE
294        return PLAYER_STOP
295
296    def is_supported_uri(self,uri_type):
297        if self.plugins == None:
298            self.plugins = self.__xine.list_input_plugins()
299
300        return uri_type in self.plugins
301
302    def is_supported_format(self,format):
303        if self.supported_extensions == None:
304            self.supported_extensions = self.__xine.get_supported_extensions()
305        return format.strip(".") in self.supported_extensions
306
307    def current_is_video(self):
308        return self._media_file is not None\
309               and self._media_file['type'] == 'video'
310
311    def close(self):
312        UnknownPlayer.close(self)
313        self.__xine.destroy()
314
315    def _create_stream(self, has_video = True):
316        if self.__stream != None:
317            self._destroy_stream()
318
319        # open audio driver
320        driver_name = self.config.get("xine", "audio_output")
321        try:
322            audio_port = xine.AudioDriver(self.__xine, driver_name)
323        except xine.xineError:
324            raise PlayerError(_("Unable to open audio driver"))
325
326        # open video driver
327        if has_video and self._video_support\
328                 and self.__xine_options["video"] != "none":
329            try:
330                video_port = xine.VideoDriver(self.__xine,
331                                              self.__xine_options["video"],
332                                              self.__xine_options["display"],
333                                              self._fullscreen)
334            except xine.XineError:
335                msg = _("Unable to open video driver")
336                log.err(msg)
337                raise PlayerError(msg)
338            else:
339                self.__window = video_port.window
340        else:
341            video_port = None
342
343        # create stream
344        self.__stream = self.__xine.stream_new(audio_port, video_port)
345        self.__stream.set_software_mixer(self.__xine_options["software_mixer"])
346
347        if video_port and self.__xine_options["osd_support"]:
348            self.__osd = self.__stream.osd_new(\
349                                       self.__xine_options["osd_font_size"])
350
351        # add event listener
352        self.__stream.add_event_callback(self._event_callback)
353
354        # restore volume
355        self.__stream.set_volume(self.get_volume())
356
357    def _destroy_stream(self):
358        if self.__stream:
359            self.__stream.destroy()
360            self.__stream = None
361            self.__window = None
362            self.__osd = None
363
364    def _osd_set(self, text):
365        if not self.__osd: return
366        self.__osd.clear()
367        self.__osd.draw_text(60, 20, text.encode("utf-8"))
368        self.__osd.show()
369
370        reactor.callLater(2, self._osd_hide, text)
371
372    def _osd_hide(self, text):
373        if self.__osd:
374            self.__osd.hide(text)
375
376    #
377    # callbacks
378    #
379    def _eof(self):
380        if self._media_file:
381            if self._media_file["type"] == "webradio":
382                # an error happened, try the next url
383                if self._media_file["url-index"] \
384                        < len(self._media_file["urls"])-1:
385                    self._media_file["url-index"] += 1
386                    self._media_file["uri"] = \
387                            self._media_file["urls"]\
388                                [self._media_file["url-index"]].encode("utf-8")
389                    self.start_play()
390                return False
391            else:
392                try: self._media_file.played()
393                except AttributeError: pass
394        new_file = self._source.next(explicit = False)
395        try: self._change_file(new_file, gapless = True)
396        except PlayerError:
397            pass
398        return False
399
400    def _update_metadata(self):
401        if not self._media_file or self._media_file["type"] != "webradio":
402            return False
403
404        # update webradio song info
405        meta = [
406            (xine.Stream.XINE_META_INFO_TITLE, 'song-title'),
407            (xine.Stream.XINE_META_INFO_ARTIST, 'song-artist'),
408            (xine.Stream.XINE_META_INFO_ALBUM, 'song-album'),
409        ]
410        for info, name in meta:
411            text = self.__stream.get_meta_info(info)
412            if not text:
413                continue
414            text = text.decode('UTF-8', 'replace')
415            if name not in self._media_file.keys() or\
416                           self._media_file[name] != text:
417                self._media_file[name] = text
418        self.dispatch_signame('player.current')
419        return False
420
421    # this callback is not called in the main reactor thread
422    # so we have to use callFromThread function instead of callLater
423    # see |http://twistedmatrix.com/documents/current/api/
424    #     |twisted.internet.interfaces.IReactorThreads.callFromThread.html
425    def _event_callback(self, user_data, event):
426        if event.type == xine.Event.XINE_EVENT_UI_PLAYBACK_FINISHED:
427            log.info("Xine event : playback finished")
428            reactor.callFromThread(self._eof)
429        elif event.type == xine.Event.XINE_EVENT_UI_SET_TITLE:
430            log.info("Xine event : set title")
431            reactor.callFromThread(self._update_metadata)
432        elif event.type == xine.Event.XINE_EVENT_UI_MESSAGE:
433            log.info("Xine event : message")
434            try:
435                message = event.message()
436            except xine.XineError, errornum:
437                message = _("Xine error %s") % errornum
438            if message is not None:
439                reactor.callFromThread(log.err, message)
440        return True
441
442
443class DvdParser:
444    DEVICE = "/dev/dvd"
445
446    def __init__(self):
447        try:
448            self.__xine = xine.XinePlayer()
449        except xine.XineError:
450            raise PlayerError(_("Unable to init a xine instance"))
451        self.__mine_stream = self.__xine.stream_new(None, None)
452
453    def get_dvd_info(self):
454        kaa_infos = kaa.metadata.parse(self.DEVICE)
455        if kaa_infos is None:
456            raise PlayerError(_("Unable to identify dvd device"))
457
458        dvd_info = {"title": kaa_infos["label"], 'track': []}
459        longest_track = {"ix": 0, "length": 0}
460        for idx, t in enumerate(kaa_infos['tracks']):
461            try: self.__mine_stream.open("dvd://%d" % (idx+1,))
462            except xine.XineError, ex:
463                raise PlayerError, ex
464            track = {"ix": idx+1, "length": int(t['length']), "chapter": []}
465            if track['length'] > longest_track["length"]:
466                longest_track = track
467
468            # get audio channels info
469            channels_number = len(t['audio'])
470            audio_channels = [{"lang":"none","ix":-2},{"lang":"auto","ix":-1}]
471            for ch in range(0,channels_number):
472                lang = self.__mine_stream.get_audio_lang(ch)
473                audio_channels.append({'ix':ch, "lang":lang.encode("utf-8")})
474            track["audio"] = audio_channels
475
476            # get subtitles channels info
477            channels_number = len(t['subtitles'])
478            sub_channels = [{"lang":"none","ix":-2},{"lang":"auto","ix":-1}]
479            for ch in range(0,channels_number):
480                lang = self.__mine_stream.get_spu_lang(ch)
481                sub_channels.append({'ix':ch, "lang":lang.encode("utf-8")})
482            track["subp"] = sub_channels
483
484            # chapters
485            for c_i,chapter in enumerate(kaa_infos['tracks'][idx]['chapters']):
486                track["chapter"].append({ "ix": c_i+1,\
487                        'length': int(chapter["pos"]) })
488                if c_i > 0:
489                    track["chapter"][c_i-1]['length'] = int(chapter["pos"]) - \
490                            track["chapter"][c_i-1]['length']
491                if c_i == len(kaa_infos['tracks'][idx]['chapters']) - 1:
492                    track["chapter"][c_i]['length'] = track["length"] - \
493                            int(track["chapter"][c_i]['length'])
494
495            dvd_info['track'].append(track)
496
497        dvd_info['longest_track'] = longest_track["ix"]
498        return dvd_info
499
500    def close(self):
501        self.__mine_stream.destroy()
502        self.__xine.destroy()
503
504# vim: ts=4 sw=4 expandtab
Note: See TracBrowser for help on using the browser.