def play(self): """ Start playback. """ log.debug('mplayer start playback') # we know that self._mp_info has to be there or the object would # not be selected by the generic one. FIXME: verify that! assert(self._mp_info) # create mplayer object self._mplayer = ChildProcess(self._mp_cmd, gdb = log.getEffectiveLevel() == logging.DEBUG) # get argument and filter list args, filters = self._mplayer.args, self._mplayer.filters if 'x11' in self._mp_info['video_drivers']: args.append('-nomouseinput') #if 'outbuf' in self._mp_info['video_filters']: # filters.append("outbuf=%s:yv12" % self._frame_shmkey) if self._properties['deinterlace'] == True or \ (self._properties['deinterlace'] == 'auto' and \ self._media.get('interlaced')): # add deinterlacer filters.append(config.mplayer.deinterlacer) if self._media.get('corrupt'): # File marked as corrupt. This happens for avi and mkv files # with no index. To make seeking work, add -idx args.append('-idx') # FIXME: self._filters_pre / add doesn't get included if no window. if self._window: self.configure_video() else: args.add(vo='null') self.configure_audio() # There is no way to make MPlayer ignore keys from the X11 window. So # this hack makes a temp input file that maps all keys to a dummy (and # non-existent) command which causes MPlayer not to react to any key # presses, allowing us to implement our own handlers. The temp file is # deleted once MPlayer has read it. tempfile = kaa.tempfile('popcorn/mplayer-input.conf') if not os.path.isfile(tempfile): keys = filter(lambda x: x not in string.whitespace, string.printable) keys = list(keys) + self._mp_info["keylist"] fd = open(tempfile, 'w') for key in keys: fd.write("%s noop\n" % key) fd.close() # only prevent input if the player is embedded if config.mplayer.embedded: args.add(input='conf=%s' % tempfile) # set properties subtitle filename and subtitle track if self._properties.get('subtitle-filename'): sub = self._properties.get('subtitle-filename') if os.path.splitext(sub)[1].lower() in ('.ifo', '.idx', '.sub'): args.add(vobsub=os.path.splitext(sub)[0], vobsubid=self._properties.get('subtitle-track')) else: args.add(subfile=sub) elif self._properties.get('subtitle-track') != None: args.add(sid=self._properties.get('subtitle-track')) if self._properties.get('cache') == 'auto': if self._media.scheme == "dvd": args.add(cache=8192) if self._media.scheme == "vcd": args.add(cache=4096) if self._media.scheme == "dvb": args.add(cache=1024) if self._media.scheme == "http": args.add(cache=8192, cache_min=5) else: args.add(cache=5000) else: args.add(cache=self._properties.get('cache')) # connect to signals self._mplayer.signals['readline'].connect_weak(self._child_handle_line) # start playback self._mplayer.start(self._media).connect_weak(self._child_exited)
class MPlayer(MediaPlayer): RE_STATUS = re.compile("V:\s*([\d+\.]+)|A:\s*([\d+\.]+)\s\W") RE_SWS = re.compile("^SwScaler: [0-9]+x[0-9]+ -> ([0-9]+)x([0-9]+)") def __init__(self, properties): super(MPlayer, self).__init__(properties) self.state = STATE_NOT_RUNNING self._mp_cmd = config.mplayer.path if not self._mp_cmd: self._mp_cmd = kaa.utils.which("mplayer") if not self._mp_cmd: raise PlayerError, "No MPlayer executable found in PATH" self._mplayer = None self._filters_pre = [] self._filters_add = [] self._mp_info = _get_mplayer_info(self._mp_cmd, self._handle_mp_info) self._check_new_frame_t = kaa.WeakTimer(self._check_new_frame) self._cur_outbuf_mode = [True, False, None] # vo, shmem, size def __del__(self): if self._frame_shmem: self._frame_shmem.detach() if self._osd_shmem: self._osd_shmem.detach() def _handle_mp_info(self, info, exception=None, traceback=None): if exception is not None: self.state = STATE_NOT_RUNNING # TODO: handle me raise info self._mp_info = info # # child IO # def _child_handle_line(self, line): if re.search("@@@|outbuf|overlay", line, re.I): childlog(line) elif line[:2] not in ("A:", "V:"): childlog(line) if line.startswith("V:") or line.startswith("A:"): m = MPlayer.RE_STATUS.search(line) if m: old_pos = self.position p = (m.group(1) or m.group(2)).replace(",", ".") self.position = float(p) # if self.position - old_pos < 0 or \ # self.position - old_pos > 1: # self.signals["seek"].emit(self.position) # XXX this logic won't work with seek-while-paused patch; state # will be "playing" after a seek. if self.state == STATE_PAUSED: self.state = STATE_PLAYING if self.state == STATE_OPEN: self.set_frame_output_mode() self._mplayer.sub_visibility(False) self.state = STATE_PLAYING self.signals["stream_changed"].emit() elif line.startswith(" ===== PAUSE"): self.state = STATE_PAUSED elif line.startswith("ID_") and line.find("=") != -1: attr, value = line.split('=', 1) attr = attr[3:] info = { "VIDEO_FORMAT": ("vfourcc", str), "VIDEO_BITRATE": ("vbitrate", int), "VIDEO_WIDTH": ("width", int), "VIDEO_HEIGHT": ("height", int), "VIDEO_FPS": ("fps", float), "VIDEO_ASPECT": ("aspect", float), "AUDIO_FORMAT": ("afourcc", str), "AUDIO_CODEC": ("acodec", str), "AUDIO_BITRATE": ("abitrate", int), "AUDIO_NCH": ("channels", int), "LENGTH": ("length", float), "FILENAME": ("filename", str) } if attr in info: self.streaminfo[info[attr][0]] = info[attr][1](value) elif line.startswith("Movie-Aspect"): aspect = line[16:].split(":")[0].replace(",", ".") if aspect[0].isdigit(): self.streaminfo["aspect"] = float(aspect) elif line.startswith("VO:"): m = re.search("=> (\d+)x(\d+)", line) if m: vo_w, vo_h = int(m.group(1)), int(m.group(2)) if "aspect" not in self.streaminfo or \ self.streaminfo["aspect"] == 0: # No aspect defined, so base it on vo size. self.streaminfo["aspect"] = vo_w / float(vo_h) elif line.startswith("overlay:") and line.find("reusing") == -1: m = re.search("(\d+)x(\d+)", line) if m: width, height = int(m.group(1)), int(m.group(2)) try: if self._osd_shmem: self._osd_shmem.detach() except shm.error: pass self._osd_shmem = shm.memory(\ shm.getshmid(self._osd_shmkey)) self._osd_shmem.attach() self.signals["osd_configure"].emit(\ width, height, self._osd_shmem.addr + 16, width, height) elif line.startswith("outbuf:") and line.find("shmem key") != -1: try: if self._frame_shmem: self._frame_shmem.detach() except shm.error: pass self._frame_shmem = shm.memory(shm.getshmid(self._frame_shmkey)) self._frame_shmem.attach() self.set_frame_output_mode() # Sync elif line.startswith("EOF code"): if self.state in (STATE_PLAYING, STATE_PAUSED): # The player may be idle bow, but we can't set the # state. If we do, generic will start a new file while # the mplayer process is still running and that does # not work. Unless we reuse mplayer proccesses we # don't react on EOF and only handle the dead # proccess. # self.state = STATE_IDLE pass elif line.startswith("FATAL:"): log.error(line.strip()) def _child_exited(self, exitcode): log.info('mplayer exited') self.state = STATE_NOT_RUNNING def _is_alive(self): return self._mplayer and self._mplayer.running # # Methods for MediaPlayer subclasses # def open(self, media): """ Open media. """ if self.state != STATE_NOT_RUNNING: raise RuntimeError('mplayer not in STATE_NOT_RUNNING') args = [] if media.scheme == "dvd": file, title = re.search("(.*?)(\/\d+)?$", media.url[4:]).groups() if file.replace('/', ''): if not os.path.isfile(file): raise ValueError, "Invalid ISO file: %s" % file args.extend(('-dvd-device', file)) args.append("dvd://") if title: args[-1] += title[1:] else: args.append(media.url) self._media = media self._media.mplayer_args = args self.state = STATE_OPENING # We have a problem at this point. The 'open' function is used to # open the stream and provide information about it. After that, the # caller can still change stuff before calling play. Mplayer doesn't # work that way so we have to run mplayer with -identify first. args = "-nolirc -nojoystick -identify -vo null -ao null -frames 0 -nocache" ident = kaa.Process(self._mp_cmd) ident.delimiter = ['\n', '\r'] signal = ident.start(args.split(' ') + self._media.mplayer_args) signal.connect_weak(self._ident_exited) ident.signals['readline'].connect_weak(self._child_handle_line) def _ident_exited(self, code): """ mplayer -identify finished """ self.state = STATE_OPEN def configure_video(self): """ Configure arguments and filter for video. """ # get argument and filter list args, filters = self._mplayer.args, self._mplayer.filters # create filter list filters.extend(self._filters_pre[:]) # FIXME: all this code seems to work. But I guess it has # some problems when we don't have an 1:1 pixel aspect # ratio on the monitor and using software scaler. aspect, dsize = self.aspect if hasattr(self._window, 'get_size'): size = self._window.get_size() else: # kaa.candy widget size = self._window.width, self._window.height # This may be needed for some non X based displays args.add(screenw=size[0], screenh=size[1]) if not self._properties['scale'] == SCALE_IGNORE: # Expand to fit the given aspect. In scaled mode we don't # do that which will result in a scaled image filling # the whole screen filters.append('expand=:::::%s/%s' % tuple(aspect)) # The expand filter has some strange side-effect on 4:3 content # on a 16:9 screen. With software scaling instead of black bars # on both sides you see garbage, without sws and xv output mplayer # uses gray bars. The following line removed that. args.append('-noslices') # FIXME: this only works if the window has the the aspect # as the full screen. In all other cases the window is not # fully used but at least with black bars. if self._properties['scale'] == SCALE_ZOOM: # This DOES NOT WORK as it should. The hardware scaler # will ignore the settings and keep aspect and does # not crop as default. When using vo x11 and software # scaling, this lines do what they are supposed to do. filters.append('dsize=%s:%s:1' % size) else: # scale to window size # FIXME: add SCALE_4_3 and SCALE_16_9 filters.append('dsize=%s:%s:0' % size) # add software scaler based on dsize arguments if self._properties.get('software-scaler') or \ config.video.driver in ('x11',): filters.append('scale=0:0') args.add(sws=2) # add postprocessing if self._properties.get('postprocessing'): filters.append('pp') args.add(autoq=100) try: cpus = kaa.utils.get_num_cpus() args.add(lavdopts='fast:skiploopfilter=nonref:threads=%d' % cpus) except RuntimeError: pass # set monitor aspect (needed sometimes, not sure when and why) args.add(monitoraspect='%s:%s' % tuple(aspect)) filters += self._filters_add #if 'overlay' in self._mp_info['video_filters']: # filters.append("overlay=%s" % self._osd_shmkey) if isinstance(self._window, kaa.display.X11Window): if config.mplayer.embedded: args.add(wid="0x%x" % self._window.get_id()) else: args.append('-fs') display = self._window.get_display().get_string() args.add(vo=config.video.driver, display=display, colorkey=config.video.colorkey) elif self._window: args.append('-fs') args.add(vo=config.video.driver, colorkey=config.video.colorkey) else: args.add(vo='null') def configure_audio(self): """ Configure arguments and filter for video. """ # get argument and filter list args, filters = self._mplayer.args, self._mplayer.filters if config.audio.passthrough: args.add(ac='hwac3,hwdts,%s' % config.mplayer.audiocodecs) else: if config.mplayer.audiocodecs: args.add(ac='%s' % config.mplayer.audiocodecs) args.add(channels=config.audio.channels) args.add(ao=config.audio.driver) if config.audio.driver == 'alsa': args[-1] += ":noblock" n_channels = self.streaminfo.get('channels') if self.streaminfo.get('acodec') in ('a52', 'hwac3', 'ffdts', 'hwdts'): device = config.audio.device.passthrough elif n_channels == 1: device = config.audio.device.mono elif n_channels <= 4: device = config.audio.device.surround40 elif n_channels <= 6: device = config.audio.device.surround51 else: device = config.audio.device.stereo if device != '': args[-1] += ':device=' + device.replace(':', '=') # set property audio filename if self._properties.get('audio-filename'): args.add(audiofile=self._properties.get('audio-filename')) # set property audio track if self._properties.get('audio-track') is not None: args.add(aid=self._properties.get('audio-track')) def play(self): """ Start playback. """ log.debug('mplayer start playback') # we know that self._mp_info has to be there or the object would # not be selected by the generic one. FIXME: verify that! assert(self._mp_info) # create mplayer object self._mplayer = ChildProcess(self._mp_cmd, gdb = log.getEffectiveLevel() == logging.DEBUG) # get argument and filter list args, filters = self._mplayer.args, self._mplayer.filters if 'x11' in self._mp_info['video_drivers']: args.append('-nomouseinput') #if 'outbuf' in self._mp_info['video_filters']: # filters.append("outbuf=%s:yv12" % self._frame_shmkey) if self._properties['deinterlace'] == True or \ (self._properties['deinterlace'] == 'auto' and \ self._media.get('interlaced')): # add deinterlacer filters.append(config.mplayer.deinterlacer) if self._media.get('corrupt'): # File marked as corrupt. This happens for avi and mkv files # with no index. To make seeking work, add -idx args.append('-idx') # FIXME: self._filters_pre / add doesn't get included if no window. if self._window: self.configure_video() else: args.add(vo='null') self.configure_audio() # There is no way to make MPlayer ignore keys from the X11 window. So # this hack makes a temp input file that maps all keys to a dummy (and # non-existent) command which causes MPlayer not to react to any key # presses, allowing us to implement our own handlers. The temp file is # deleted once MPlayer has read it. tempfile = kaa.tempfile('popcorn/mplayer-input.conf') if not os.path.isfile(tempfile): keys = filter(lambda x: x not in string.whitespace, string.printable) keys = list(keys) + self._mp_info["keylist"] fd = open(tempfile, 'w') for key in keys: fd.write("%s noop\n" % key) fd.close() # only prevent input if the player is embedded if config.mplayer.embedded: args.add(input='conf=%s' % tempfile) # set properties subtitle filename and subtitle track if self._properties.get('subtitle-filename'): sub = self._properties.get('subtitle-filename') if os.path.splitext(sub)[1].lower() in ('.ifo', '.idx', '.sub'): args.add(vobsub=os.path.splitext(sub)[0], vobsubid=self._properties.get('subtitle-track')) else: args.add(subfile=sub) elif self._properties.get('subtitle-track') != None: args.add(sid=self._properties.get('subtitle-track')) if self._properties.get('cache') == 'auto': if self._media.scheme == "dvd": args.add(cache=8192) if self._media.scheme == "vcd": args.add(cache=4096) if self._media.scheme == "dvb": args.add(cache=1024) if self._media.scheme == "http": args.add(cache=8192, cache_min=5) else: args.add(cache=5000) else: args.add(cache=self._properties.get('cache')) # connect to signals self._mplayer.signals['readline'].connect_weak(self._child_handle_line) # start playback self._mplayer.start(self._media).connect_weak(self._child_exited) def stop(self): """ Stop playback. """ if self._mplayer: self._mplayer.stop() self.state = STATE_SHUTDOWN def pause(self): """ Pause playback. """ self._mplayer.pause() def resume(self): """ Resume playback. """ self._mplayer.pause() def seek(self, value, type): """ SEEK_RELATIVE, SEEK_ABSOLUTE and SEEK_PERCENTAGE. """ s = [SEEK_RELATIVE, SEEK_PERCENTAGE, SEEK_ABSOLUTE] self._mplayer.seek(value, s.index(type)) # # Property settings # @runtime_policy(DEFER_UNTIL_PLAYING) def _set_prop_audio_delay(self, delay): """ Sets audio delay. Positive value defers audio by delay. """ self._mplayer.audio_delay(-delay, 1) @runtime_policy(IGNORE_UNLESS_PLAYING) def _set_prop_audio_track(self, id): """ Change audio track (mpeg and mkv only) """ self._mplayer.switch_audio(id) @runtime_policy(IGNORE_UNLESS_PLAYING) def _set_prop_subtitle_track(self, id): """ Change subtitle track """ self._mplayer.sub_select(id) @runtime_policy(IGNORE_UNLESS_PLAYING) def _set_prop_subtitle_filename(self, filename): """ Change subtitle filename """ self._mplayer.sub_load(filename) @runtime_policy(DEFER_UNTIL_PLAYING) def _set_prop_subtitle_visibility(self, enabled): """ Change subtitle visibility """ self._mplayer.sub_visibility(int(enabled)) # # Methods for filter handling (not yet in generic and base) # # FIXME: no distinction between audio and video filters def prepend_filter(self, filter): """ Add filter to the prepend list. """ self._filters_pre.append(filter) def append_filter(self, filter): """ Add filter to the normal filter list. """ self._filters_add.append(filter) def get_filters(self): """ Return all filter set. """ return self._filters_pre + self._filters_add def remove_filter(self, filter): """ Remove filter for filter list. """ for l in (self._filters_pre, self._filters_add): if filter in l: l.remove(filter) # # Methods and helper for MediaPlayer subclasses for CAP_OSD # def osd_can_update(self): """ Returns True if it is safe to write to the player's shared memory buffer used for OSD, and False otherwise. If this buffer is written to even though this function returns False, the OSD may exhibit corrupt output or tearing during animations. See generic.osd_can_update for details. """ if not self._osd_shmem: return False try: if ord(self._osd_shmem.read(1)) == BUFFER_UNLOCKED: return True except shm.error: self._osd_shmem.detach() self._osd_shmem = None return False def osd_update(self, alpha = None, visible = None, invalid_regions = None): """ Updates the OSD of the player based on the given argments: See generic.osd_update for details. """ cmd = [] if alpha != None: cmd.append("alpha=%d" % alpha) if visible != None: cmd.append("visible=%d" % int(visible)) if invalid_regions: for (x, y, w, h) in invalid_regions: cmd.append("invalidate=%d:%d:%d:%d" % (x, y, w, h)) self._mplayer.overlay(','.join(cmd)) self._overlay_set_lock(BUFFER_LOCKED) def _overlay_set_lock(self, byte): try: if self._osd_shmem and self._osd_shmem.attached: self._osd_shmem.write(chr(byte)) except shm.error: self._osd_shmem.detach() self._osd_shmem = None # # Methods and helper for MediaPlayer subclasses for CAP_CANVAS # def set_frame_output_mode(self, vo = None, notify = None, size = None): """ Controls if and how frames are delivered via the 'frame' signal, and whether or not frames are drawn to the vo driver's video window. See generic.set_frame_output_mode for details. """ if vo != None: self._cur_outbuf_mode[0] = vo if notify != None: self._cur_outbuf_mode[1] = notify if notify: self._check_new_frame_t.start(0.01) else: self._check_new_frame_t.stop() if size != None: self._cur_outbuf_mode[2] = size if not self._is_alive(): return mode = { (False, False): 0, (True, False): 1, (False, True): 2, (True, True): 3 } mode = mode[tuple(self._cur_outbuf_mode[:2])] size = self._cur_outbuf_mode[2] if size == None: self._mplayer.outbuf(mode) else: self._mplayer.outbuf(mode, size[0], size[1]) def unlock_frame_buffer(self): """ Unlocks the frame buffer provided by the last 'frame' signal See generic.unlock_frame_buffer for details. """ try: self._frame_shmem.write(chr(BUFFER_UNLOCKED)) except shm.error: self._frame_shmem.detach() self._frame_shmem = None def _check_new_frame(self): if not self._frame_shmem: return try: lock, width, height, aspect = \ struct.unpack("hhhd", self._frame_shmem.read(16)) except shm.error: self._frame_shmem.detach() self._frame_shmem = None return if lock & BUFFER_UNLOCKED: return if width > 0 and height > 0 and aspect > 0: self.signals["frame"].emit(\ width, height, aspect, self._frame_shmem.addr + 16, "yv12")