def __init__(self, osd_shmkey): Player.__init__(self) self._xine = xine.Xine() self._vfilter = FilterChain(self._xine) self._stream = self._vo = self._ao = None self._osd_shmkey = int(osd_shmkey) self._osd_shmem = None self._driver_control = None self._window_size = 0, 0 self._window_aspect = -1 self._status = kaa.WeakTimer(self._status_output) self._status_last = None self._vo_settings = None self._stream_settings = { 'pixel-aspect': 1.0, 'scale' : SCALE_KEEP, 'zoom' : 100 } self._xine.set_config_value("effects.goom.fps", 20) self._xine.set_config_value("effects.goom.width", 512) self._xine.set_config_value("effects.goom.height", 384) self._xine.set_config_value("effects.goom.csc_method", "Slow but looks better") # self._xine.set_config_value("video.device.xv_autopaint_colorkey", True) # Config options for multi-threaded decoding with ffmpeg; the first # is needed in order for thread_count to take effect. try: self._xine.set_config_value('video.processing.ffmpeg_choose_speed_over_accuracy', True) try: cpus = kaa.utils.get_num_cpus() self._xine.set_config_value('video.processing.ffmpeg_thread_count', cpus) except RuntimeError: # Couldn't determine number of cpus, so we didn't set the config value. pass except xine.XineError: # One of the config names doesn't exist, probably an older version of # xine-lib. Not a big deal. pass
class XinePlayerChild(Player): def __init__(self, osd_shmkey): Player.__init__(self) self._xine = xine.Xine() self._vfilter = FilterChain(self._xine) self._stream = self._vo = self._ao = None self._osd_shmkey = int(osd_shmkey) self._osd_shmem = None self._driver_control = None self._window_size = 0, 0 self._window_aspect = -1 self._status = kaa.WeakTimer(self._status_output) self._status_last = None self._vo_settings = None self._stream_settings = { 'pixel-aspect': 1.0, 'scale' : SCALE_KEEP, 'zoom' : 100 } self._xine.set_config_value("effects.goom.fps", 20) self._xine.set_config_value("effects.goom.width", 512) self._xine.set_config_value("effects.goom.height", 384) self._xine.set_config_value("effects.goom.csc_method", "Slow but looks better") # self._xine.set_config_value("video.device.xv_autopaint_colorkey", True) # Config options for multi-threaded decoding with ffmpeg; the first # is needed in order for thread_count to take effect. try: self._xine.set_config_value('video.processing.ffmpeg_choose_speed_over_accuracy', True) try: cpus = kaa.utils.get_num_cpus() self._xine.set_config_value('video.processing.ffmpeg_thread_count', cpus) except RuntimeError: # Couldn't determine number of cpus, so we didn't set the config value. pass except xine.XineError: # One of the config names doesn't exist, probably an older version of # xine-lib. Not a big deal. pass # ######################################################################### # Stream information utils # ######################################################################### def _status_output(self): """ Outputs stream status information. """ if not self._stream: return # FIXME: this gets not updated very often, I have no idea why t = self._stream.get_pos_length() status = self._stream.get_status() if status == xine.STATUS_PLAY and None in t: # Status is playing, but pos/time is not known for stream, # which likely means we have seeked and are not done seeking # get, so position is not yet determined. In this case, don't # send a status update to parent yet. return speed = self._stream.get_parameter(xine.PARAM_SPEED) # Line format: pos time length status speed # Where status is one of XINE_STATUS_ constants, and speed # is one of XINE_SPEED constants. cur_status = (t[0], t[1], t[2], status, speed) if cur_status != self._status_last: self._status_last = cur_status self.parent.set_status(*cur_status) def _get_streaminfo(self): """ Get information about the current stream. """ if not self._stream: return {} info = { "vfourcc": self._stream.get_info(xine.STREAM_INFO_VIDEO_FOURCC), "afourcc": self._stream.get_info(xine.STREAM_INFO_AUDIO_FOURCC), "vcodec": self._stream.get_meta_info(xine.META_INFO_VIDEOCODEC), "acodec": self._stream.get_meta_info(xine.META_INFO_AUDIOCODEC), "width": self._stream.get_info(xine.STREAM_INFO_VIDEO_WIDTH), "height": self._stream.get_info(xine.STREAM_INFO_VIDEO_HEIGHT), "aspect": self._stream.get_info(xine.STREAM_INFO_VIDEO_RATIO) / 10000.0, "fps": self._stream.get_info(xine.STREAM_INFO_FRAME_DURATION), "length": self._stream.get_length(), } if self._window_aspect != -1: # Use the aspect ratio as given to the frame output callback # as it tends to be more reliable (particularly for DVDs). info["aspect"] = self._window_aspect if info["aspect"] == 0 and info["height"] > 0: info["aspect"] = info["width"] / float(info["height"]) if info["fps"]: info["fps"] = 90000.0 / info["fps"] return info # ######################################################################### # kaa.xine callbacks # ######################################################################### def _xine_frame_output_cb(self, width, height, aspect): """ Return the frame output position, dimensions and aspect """ if self._vo_settings: #if self._vo_settings[0] and self._vo_settings[1][:2] == (width, height): # Use cached values. Dimensions have not changed between the # last frame. The aspect may be different now because we messed with # it. This is a bug somehow and it happens. So we return the cached # values and reset self._vo_settings[0] so we recalculate when # the aspect changes the next time. # (from tack) why don't we want to recalculate here? We need to, # and it's not a bug that you'd get the same frame size but a # different aspect. Consider an NTSC DVD: the frame size is # always 720x480, but the aspect is either 16/9 or 4/3. By not # calculating the dimensions here we are breaking aspect. #self._vo_settings = False, (width, height, aspect) #return self._vo_settings_calculated if self._vo_settings[1] == (width, height, aspect): # use cache when nothing has changed return self._vo_settings_calculated # If we're here, frame size or aspect changed, so inform parent of # new frame info. self.parent.frame_reconfigure(width, height, float(width) / height * aspect) self._vo_settings = True, (width, height, aspect) vid_w, vid_h, vid_a = width, height, aspect if self._stream_settings['zoom'] < 100 and 0: # FIMXE: this crashes when using a timer to zoom from 100 # in 10% steps. # XXX: the first two 2-tuples of the return value in this # method are (x,y) and (w,h) of the video relative to the window. # You may be able to reproduce this functionality by modifying # those values rather than using VO_CROP. crop_x = vid_w - int(vid_w * self._stream_settings['zoom'] / 100) crop_y = vid_h - int(vid_h * self._stream_settings['zoom'] / 100) self._stream.set_parameter(xine.PARAM_VO_CROP_LEFT, crop_x) self._stream.set_parameter(xine.PARAM_VO_CROP_RIGHT, crop_x) self._stream.set_parameter(xine.PARAM_VO_CROP_TOP, crop_y) self._stream.set_parameter(xine.PARAM_VO_CROP_BOTTOM, crop_y) log.info('calculate frame output') win_w, win_h, win_a = self._xine._get_vo_display_size(vid_w, vid_h, vid_a) if abs(self._window_aspect - win_a) > 0.01: log.debug('VO: %dx%d -> %dx%d', vid_w, vid_h, win_w, win_h) # FIXME: maybe not resize the parent window, make this an option self.parent.resize((win_w, win_h)) self._window_aspect = win_a if self._window_size != (0, 0): win_w, win_h = self._window_size if self._stream_settings['scale'] == SCALE_IGNORE: # ignore aspect. The whole window is used and the video # is scaled to fill it. The aspect is ignore to do that. aspect = (float(vid_w) * win_h) / (float(win_w) * vid_h) * vid_a else: # get aspect from pre-calculated value aspect = self._stream_settings['pixel-aspect'] if self._stream_settings['scale'] == SCALE_4_3: # force 4:3 aspect *= (float(vid_w) * 3) / (float(4) * vid_h) if self._stream_settings['scale'] == SCALE_16_9: # force 16:9 aspect *= (float(vid_w) * 9) / (float(16) * vid_h) # FIXME: add SCALE_ZOOM self._vo_settings_calculated = (0, 0), (0, 0), (win_w, win_h), aspect return self._vo_settings_calculated def _xine_dest_size_cb(self, width, height, aspect): """ Return the output size and aspect. """ w, h = self._window_size return (w, h), 1.0 def _osd_configure(self, width, height, aspect): if not self._osd_shmem: self._osd_shmem = kaa.shm.create_memory(self._osd_shmkey, 2000 * 2000 * 4 + 16) self._osd_shmem.attach() self._osd_shmem.write(chr(BUFFER_UNLOCKED)) # FIXME: don't hardcode buffer dimensions assert(width*height*4 < 2000*2000*4) self.parent.osd_configure(width, height, aspect) return self._osd_shmem.addr + 16, width * 4 def _handle_xine_event(self, event): """ Received event from xine. """ if len(event.data) > 1: del event.data["data"] if event.type == xine.EVENT_UI_CHANNELS_CHANGED: self.parent.set_streaminfo(True, self._get_streaminfo()) elif event.type == xine.EVENT_UI_MESSAGE and \ event.data['type'] == xine.MSG_AUDIO_OUT_UNAVAILABLE: # Failed to open audio driver (async), so create dummy driver and # wire stream to that. self._ao = self._xine.open_audio_driver("none") if self._stream: self._stream.get_audio_source().wire(self._ao) self.parent.xine_event(event.type, event.data) # ############################################################################# # Commands from parent process # ############################################################################# def window_changed(self, wid, size, visible, exposed_regions): """ Window changed or exposed regions. """ if not self._vo: return if size is not None: self._window_size = size if visible is not None: self._vo.send_gui_data(xine.GUI_SEND_VIDEOWIN_VISIBLE, visible) self._vo.send_gui_data(xine.GUI_SEND_DRAWABLE_CHANGED, wid) self._vo_settings = None def _frame_notify_cb(self, fd): size = struct.calcsize("LL8s") packet = os.read(fd, size) shmid, offset, padding = struct.unpack('LL8s', packet) self.parent.frame_notify(shmid, offset) def configure_video(self, wid, size, aspect, colorkey): """ Configure video output. """ if size is not None: self._window_size = size self._vo_visible = True if wid and isinstance(wid, (int, long)): vo_kwargs = { 'passthrough': 'xv', 'wid': wid, 'vsync': self.config.xine.vsync } elif wid and isinstance(wid, str) and wid.startswith('fb'): vo_kwargs = { 'passthrough': 'vidixfb' } else: vo_kwargs = {'passthrough': 'none'} self._vo_visible = False if aspect: self._stream_settings['pixel-aspect'] = aspect # FIXME: this should work but it crashes with an exception that # video.device.xv_colorkey is not defined. # if colorkey is not None: # self._xine.set_config_value("video.device.xv_colorkey", colorkey) """ frame_notify_pipe = os.pipe() kaa.IOMonitor(self._frame_notify_cb, frame_notify_pipe[0]).register(frame_notify_pipe[0]) control_return = [] self._vo = self._xine.open_video_driver( "kaa", control_return = control_return, notify_fd = frame_notify_pipe[1], osd_configure_cb = kaa.WeakCallable(self._osd_configure), frame_output_cb = kaa.WeakCallable(self._xine_frame_output_cb), dest_size_cb = kaa.WeakCallable(self._xine_dest_size_cb), **vo_kwargs) self._driver_control = control_return[0] """ self._vo = self._xine.open_video_driver(vo_kwargs['passthrough'], frame_output_cb = kaa.WeakCallable(self._xine_frame_output_cb), dest_size_cb = kaa.WeakCallable(self._xine_dest_size_cb), **vo_kwargs) # Set new vo on filter chain and configure filters. self._vfilter.set_vo(self._vo) f = self._vfilter.get("tvtime") f.set_parameters(method = self.config.xine.deinterlacer.method, chroma_filter = self.config.xine.deinterlacer.chroma_filter) if USE_EXPAND: f = self._vfilter.get("expand") if size is not None: # FIXME: see notice an USE_EXPAND definition aspect = float(size[0]) / size[1] aspect *= self._stream_settings['pixel-aspect'] f.set_parameters(aspect=aspect) f.set_parameters(enable_automatic_shift = True) if self._driver_control: self._driver_control("set_passthrough", False) def configure_audio(self, driver): """ Configure audio output. """ try: self._ao = self._xine.open_audio_driver(driver=driver) except xine.XineError: # Audio driver initialization failed; initialize a dummy driver # instead. self._ao = self._xine.open_audio_driver("none") return if driver == 'alsa': set = self._xine.set_config_value dev = self.config.audio.device if dev.mono: set('audio.device.alsa_default_device', dev.mono) if dev.stereo: set('audio.device.alsa_front_device', dev.stereo) if dev.surround40: set('audio.device.alsa_surround40_device', dev.surround40) if dev.surround51: set('audio.device.alsa_surround51_device', dev.surround51) if dev.passthrough: set('audio.device.alsa_passthrough_device', dev.passthrough) if self.config.audio.passthrough: set('audio.output.speaker_arrangement', 'Pass Through') else: channels = { 2: 'Stereo 2.0', 4: 'Surround 4.0', 6: 'Surround 5.1' } num = self.config.audio.channels set('audio.output.speaker_arrangement', channels[num]) if self._stream: self._stream.get_audio_source().wire(self._ao) def configure_stream(self, properties): """ Basic stream setup. """ self._stream = self._xine.new_stream(self._ao, self._vo) #self._stream.set_parameter(xine.PARAM_VO_CROP_BOTTOM, 10) self._stream.signals["event"].connect_weak(self._handle_xine_event) # self._noise_post = self._xine.post_init("noise", video_targets = [self._vo]) # self._noise_post.set_parameters(luma_strength = 3, quality = "temporal") # self._stream.get_video_source().wire(self._noise_post.get_default_input()) if not self._vo: return # wire video stream with needed filter chain = [] if properties.get('deinterlace') in (True, 'auto'): chain.append('tvtime') if properties.get('postprocessing'): chain.append('pp') if USE_EXPAND: chain.append('expand') if properties.get('scale'): self._stream_settings['scale'] = properties.get('scale') if properties.get('zoom'): self._stream_settings['zoom'] = properties.get('zoom') self._vfilter.wire(self._stream.get_video_source(), *chain) def open(self, mrl): """ Open mrl to play. """ try: # XXX: this sometimes deadlocks, there's not much we can do # about it, it happens inside xine. :( self._stream.open(mrl) if not self._stream.get_info(xine.STREAM_INFO_HAS_VIDEO)\ and self._vo_visible: self._goom_post = self._xine.post_init( "goom", video_targets = [self._vo], audio_targets=[self._ao]) self._stream.get_audio_source().wire(self._goom_post.get_default_input()) else: self._goom_post = None self._stream.get_audio_source().wire(self._ao) except xine.XineError: self.parent.set_streaminfo(False, self._stream.get_error()) log.error('Open failed: %s', self._stream.get_error()) return False # Check if stream is ok. v_unhandled = self._stream.get_info(xine.STREAM_INFO_HAS_VIDEO) and \ not self._stream.get_info(xine.STREAM_INFO_IGNORE_VIDEO) and \ not self._stream.get_info(xine.STREAM_INFO_VIDEO_HANDLED) a_unhandled = self._stream.get_info(xine.STREAM_INFO_HAS_AUDIO) and \ not self._stream.get_info(xine.STREAM_INFO_IGNORE_AUDIO) and \ not self._stream.get_info(xine.STREAM_INFO_AUDIO_HANDLED) if v_unhandled or a_unhandled: self.parent.set_streaminfo(False, None) log.error('unable to play stream') return False self.parent.set_streaminfo(True, self._get_streaminfo()) self._status.start(0.03) self._vo_settings = None return True def osd_update(self, alpha, visible, invalid_regions): """ Update OSD. """ if not self._osd_shmem: return if alpha != None: self._driver_control("set_osd_alpha", alpha) if visible != None: self._driver_control("set_osd_visibility", visible) if invalid_regions != None: self._driver_control("osd_invalidate_rect", invalid_regions) self._osd_shmem.write(chr(BUFFER_UNLOCKED)) def play(self): """ Start playback. """ status = self._stream.get_status() if status == xine.STATUS_STOP: self._stream.play() xine._debug_show_chain(self._stream._obj) def pause(self): """ Pause playback. """ self._stream.set_parameter(xine.PARAM_SPEED, xine.SPEED_PAUSE) def resume(self): """ Resume playback. """ self._stream.set_parameter(xine.PARAM_SPEED, xine.SPEED_NORMAL) def seek(self, value, type): """ Seek in stream. """ if type == SEEK_RELATIVE: self._stream.seek_relative(value) if type == SEEK_ABSOLUTE: self._stream.seek_absolute(value) if type == SEEK_PERCENTAGE: self._stream.play(pos = (value / 100.0) * 65535) def stop(self): """ Stop playback. """ self._status.stop() if self._stream: self._stream.stop() self._stream.close() self.parent.play_stopped() def die(self): """ Stop process. """ self.stop() sys.exit(0) def set_audio_delay(self, delay): """ Set audio delay. """ # xine-lib wants units in 1/90000 sec, so convert. delay = -int(delay * 90000.0) self._stream.set_parameter(xine.PARAM_AV_OFFSET, delay) def set_frame_output_mode(self, vo, notify, size): """ Set frame output mode. """ if not self._driver_control: # If vo driver used isn't kaa (which may not be for testing/ # debugging purposes) then _driver_control won't be set. # This could also happen if there is no vo set (i.e. audio only). # In either case, there's nothing to do. return if vo != None: self._driver_control("set_passthrough", vo) if notify != None: set_parameters = self._vfilter.get('tvtime').set_parameters if notify: log.info('deinterlace cheap mode: True') set_parameters(cheap_mode = True, framerate_mode = 'half_top') else: log.info('deinterlace cheap mode: False') set_parameters(cheap_mode = False, framerate_mode = 'full') self._driver_control("set_notify_frame", notify) if size != None: self._driver_control("set_notify_frame_size", size) def input(self, input): """ Send input (e.g. DVD navigation) """ self._stream.send_event(input) def set_property(self, prop, value): """ Set a property to a new value. """ if prop == 'scale': self._vo_settings = None self._stream_settings['scale'] = value return if prop == 'zoom': self._vo_settings = None self._stream_settings['zoom'] = value return current = self._vfilter.get_chain() chain = [] if prop == 'deinterlace': if value: chain.append('tvtime') elif 'tvtime' in current: chain.append('tvtime') if prop == 'postprocessing': if value: chain.append('pp') elif 'pp' in current: chain.append('pp') if USE_EXPAND: chain.append('expand') self._vfilter.rewire(*chain)