Exemplo n.º 1
0
 def _build_video_player(self, item_info, volume):
     self.player = widgetset.VideoPlayer()
     self.video_display = VideoDisplay(self.player)
     self.video_display.connect('removed', self.on_display_removed)
     self.video_display.connect('cant-play', self._on_cant_play)
     self.video_display.connect('ready-to-play', self._on_ready_to_play)
     if app.config.get(prefs.PLAY_DETACHED):
         self.prepare_detached_playback()
     else:
         self.prepare_attached_playback()
     self.is_playing_audio = False
     app.menu_manager.select_subtitle_encoding(item_info.subtitle_encoding)
     self.initial_subtitle_encoding = item_info.subtitle_encoding
Exemplo n.º 2
0
 def _build_video_player(self, item_info, volume):
     self.player = widgetset.VideoPlayer()
     self.video_display = VideoDisplay(self.player)
     self.video_display.connect('removed', self.on_display_removed)
     self.video_display.connect('cant-play', self._on_cant_play)
     self.video_display.connect('ready-to-play', self._on_ready_to_play)
     if app.config.get(prefs.PLAY_DETACHED):
         self.prepare_detached_playback()
     else:
         self.prepare_attached_playback()
     self.is_playing_audio = False
     app.menu_manager.select_subtitle_encoding(item_info.subtitle_encoding)
     self.initial_subtitle_encoding = item_info.subtitle_encoding
Exemplo n.º 3
0
class PlaybackManager (signals.SignalEmitter):
    
    def __init__(self):
        signals.SignalEmitter.__init__(self)
        self.player = None
        self.video_display = None
        self.removing_video_display = False
        self.detached_window = None
        self.previous_left_width = 0
        self.previous_left_widget = None
        self.is_fullscreen = False
        self.is_playing = False
        self.is_playing_audio = False
        self.is_paused = False
        self.is_suspended = False
        self.shuffle = False
        self.repeat = WidgetStateStore.get_repeat_off() 
        self.open_finished = False
        self.open_successful = False
        self.playlist = None
        self.mark_as_watched_timeout = None
        self.update_timeout = None
        self.selected_tab_list = self.selected_tabs = None
        self.presentation_mode = 'fit-to-bounds'
        self.create_signal('will-start')
        self.create_signal('selecting-file')
        self.create_signal('playing-info-changed')
        self.create_signal('cant-play-file')
        self.create_signal('will-play')
        self.create_signal('did-start-playing')
        self.create_signal('will-play-attached')
        self.create_signal('will-play-detached')
        self.create_signal('will-pause')
        self.create_signal('will-stop')
        self.create_signal('did-stop')
        self.create_signal('will-fullscreen')
        self.create_signal('playback-did-progress')
        self.create_signal('update-shuffle')
        self.create_signal('update-repeat')

    def player_ready(self):
        return self.player is not None and self.open_finished

    def player_playing(self):
        return self.player is not None and self.open_successful

    def get_is_playing_video(self):
        return self.is_playing and not self.is_playing_audio
    is_playing_video = property(get_is_playing_video)

    def set_volume(self, volume):
        self.volume = volume
        if self.player is not None:
            self.player.set_volume(volume)
    
    def set_presentation_mode(self, mode):
        self.presentation_mode = mode
        if self.is_playing:
            if not self.is_fullscreen:
                self.fullscreen()
            self.video_display.renderer.update_for_presentation_mode(mode)
    
    def toggle_paused(self):
        """Pause a playing item, play a paused item, and soft_fail otherwise."""
        if not self.is_playing:
            app.widgetapp.handle_soft_failure('toggle_paused',
                "item not playing or paused in toggle_paused",
                with_exception=False)
            return # in release mode, recover by doing nothing
        if self.is_paused:
            self.play()
        else:
            self.pause()

    def start_with_items(self, item_infos):
        """Start playback, playing a static list of ItemInfos."""
        tracker = itemtrack.ManualItemListTracker.create(item_infos)
        self.start(None, tracker)

    def goto_currently_playing(self):
        """Jump to the currently playing item in the display."""
        playing_item = self.get_playing_item()
        if not self.selected_tab_list or not playing_item:
            return
        if (self.is_playing and not
          (self.is_playing_audio or self.detached_window)):
            # playing a video in the app, so don't bother
            return
        try:
            tab_iter = self.selected_tab_list.iter_map[self.selected_tabs[0].id]
        except KeyError:
            #17495 - item may be from a tab that no longer exists
            self.selected_tab_list = self.selected_tabs = None
            return
        app.tabs._select_from_tab_list(self.selected_tab_list.type, tab_iter)
        display = app.display_manager.current_display
        if display and hasattr(display, 'controller'):
            controller = display.controller
            controller.scroll_to_item(playing_item, manual=True, recenter=True)
        else:
            #17488 - GuideDisplay doesn't have a controller
            logging.debug("current display doesn't have a controller - "
                    "can't switch to")

    def start(self, start_id, item_tracker,
            presentation_mode='fit-to-bounds', force_resume=False):
        """Start playback, playing the items from an ItemTracker"""
        if self.is_playing:
            self.stop()
        self.emit('will-start')

        # Remember where we are, so we can switch to it later
        list_type, selected = app.tabs.selection
        self.selected_tab_list = app.tabs[list_type]
        self.selected_tabs = selected

        play_in_miro = app.config.get(prefs.PLAY_IN_MIRO)
        # Only setup a playlist if we are playing in Miro - otherwise we
        # farm off to an external player for an individual item and the
        # concept of a playlist doesn't really make sense.
        start_item = None
        if play_in_miro:
            self.playlist = PlaybackPlaylist(item_tracker, start_id)
            self.playlist.connect("position-changed",
                self._on_position_changed)
            self.playlist.connect("playing-info-changed",
                self._on_playing_changed)
            self.playlist.set_shuffle(self.shuffle)
            self.playlist.set_repeat(self.repeat)
        else:
            model = item_tracker.item_list.model
            if start_id:
                start_item = model.get_info(start_id)
            else:
                start_item = model.get_first_info()
        self.should_mark_watched = []
        self.presentation_mode = presentation_mode
        self.force_resume = force_resume
        self._play_current(item=start_item)
        if self.presentation_mode != 'fit-to-bounds':
            self.fullscreen()

    def _on_position_changed(self, playlist):
        self._not_skipped_by_user = True
        self._play_current()

    def _on_playing_changed(self, playlist):
        new_info = self.get_playing_item()
        if self.detached_window:
            if self.detached_window.get_title() != new_info.name:
                self.detached_window.set_title(new_info.name)
        if app.config.get(prefs.PLAY_IN_MIRO) and new_info:
            # if playlist is None, new_info will be none as well.
            # Since emitting playing-info-changed with a "None"
            # argument will cause a crash, we only emit it if
            # new_info has a value
            self.emit('playing-info-changed', new_info)
        else:
            logging.warning("trying to update playback info "
                            "even though playback has stopped")

    def prepare_attached_playback(self):
        self.emit('will-play-attached')
        splitter = app.widgetapp.window.splitter
        self.previous_left_width = splitter.get_left_width()
        self.previous_left_widget = splitter.left
        splitter.remove_left()
        splitter.set_left_width(0)
        app.display_manager.push_display(self.video_display)            
    
    def finish_attached_playback(self, unselect=True):
        if (self.video_display is not None and
                app.display_manager.current_display is self.video_display):
            app.display_manager.pop_display(unselect)
        app.widgetapp.window.splitter.set_left_width(self.previous_left_width)
        app.widgetapp.window.splitter.set_left(self.previous_left_widget)
    
    def prepare_detached_playback(self):
        self.emit('will-play-detached')
        detached_window_frame = app.config.get(prefs.DETACHED_WINDOW_FRAME)
        if detached_window_frame is None:
            detached_window_frame = widgetset.Rect(0, 0, 800, 600)
        else:
            detached_window_frame = widgetset.Rect.from_string(detached_window_frame)
        title = self.playlist.currently_playing.name
        self.detached_window = DetachedWindow(title, detached_window_frame)
        self.align = widgetset.DetachedWindowHolder()
        self.align.add(self.video_display.widget)
        self.detached_window.set_content_widget(self.align)
        self.detached_window.show()
    
    def finish_detached_playback(self):
        # this prevents negative x and y values from getting saved
        coords = str(self.detached_window.get_frame())
        coords = ",".join([str(max(0, int(c))) for c in coords.split(",")])
        app.config.set(prefs.DETACHED_WINDOW_FRAME, coords)
        app.config.save()
        self.align.remove()
        self.align = None
        self.detached_window.close(False)
        self.detached_window.destroy()
        self.detached_window = None
    
    def schedule_update(self):
        def notify_and_reschedule():
            if self.update_timeout is not None:
                self.update_timeout = None
                if self.is_playing:
                    if not self.is_suspended:
                        self.notify_update()
                    self.schedule_update()
        if self.update_timeout:
            self.cancel_update_timer()
        self.update_timeout = timer.add(0.5, notify_and_reschedule)

    def cancel_update_timer(self):
        if self.update_timeout is not None:
            timer.cancel(self.update_timeout)
            self.update_timeout = None

    def notify_update(self):
        if self.player_playing():
            elapsed = self.player.get_elapsed_playback_time()
            total = self.player.get_total_playback_time()
            self.emit('playback-did-progress', elapsed, total)

    def on_display_removed(self, display):
        if not self.removing_video_display:
            self._not_skipped_by_user = True
            self.stop()

    def play(self, start_at=0):
        if not self.player:
            logging.warn("no self.player in play(). race condition?")
            return
        duration = self.player.get_total_playback_time()
        self.emit('will-play', duration)
        resume_time = self.playlist.currently_playing.resume_time
        if start_at > 0:
            self.player.play_from_time(start_at)
        elif self.should_resume() and not self.is_paused:
            self.player.play_from_time(resume_time)
        else:
            self.player.play()
        self.notify_update()
        self.schedule_update()
        self.is_paused = False
        self.is_suspended = False
        app.menu_manager.update_menus('playback-changed')

    def should_resume(self):
        if self.force_resume:
            return True

        if(self.shuffle == True or 
           self.repeat != WidgetStateStore.get_repeat_off()):
           return False

        currently_playing = self.playlist.currently_playing
        return self.item_resume_policy(currently_playing)

    def pause(self):
        if self.is_playing:
            self.emit('will-pause')
            self.player.pause()
            self.is_paused = True
            app.menu_manager.update_menus('playback-changed')

    def fullscreen(self):
        if not self.is_playing or not self.video_display:
            return
        self.emit('will-fullscreen')
        self.toggle_fullscreen()

    def stop(self):
        if not self.is_playing:
            return
        if self.get_playing_item() is not None:
            self.update_current_resume_time()
        self.playlist.finished()
        self.playlist = None
        self.cancel_update_timer()
        self.cancel_mark_as_watched()
        self.send_mark_items_watched()
        self.is_playing = False
        self.is_playing_audio = False
        self.is_paused = False
        self.emit('will-stop')
        if self.player is not None:
            self.player.stop()
            self.player = None
        if self.video_display is not None:
            self.remove_video_display()
            self.video_display = None
        self.is_fullscreen = False
        self.previous_left_widget = None
        self.emit('did-stop')
        app.menu_manager.update_menus('playback-changed')

    def get_audio_tracks(self):
        """Get a list of available audio tracks

        :returns: list of (label, track_id) tuples
        """
        if self.player is not None:
            return self.player.get_audio_tracks()
        else:
            return []

    def get_enabled_audio_track(self):
        """Get the currently enabled audio track

        :returns: current track_id or None if we are not playing
        """
        if self.player is not None:
            return self.player.get_enabled_audio_track()
        else:
            return None

    def set_audio_track(self, track_id):
        """Change the currently enabled audio track

        :param track_id: track_id from get_audio_tracks()
        """
        if self.player is not None:
            self.player.set_audio_track(track_id)
        else:
            raise ValueError("Not playing")

    def get_subtitle_tracks(self):
        """Get a list of available subtitle tracks

        :returns: list of (label, track_id) tuples
        """
        if self.player is not None and not self.is_playing_audio:
            return self.player.get_subtitle_tracks()
        else:
            return []

    def get_enabled_subtitle_track(self):
        """Get the currently enabled subtitle track

        :returns: current track_id or None if we are not playing video
        """
        if self.player is not None and not self.is_playing_audio:
            return self.player.get_enabled_subtitle_track()
        else:
            return None

    def set_subtitle_track(self, track_id):
        """Change the currently enabled subtitle track

        :param track_id: track_id from get_subtitle_tracks()
        """
        if self.player is None:
            raise ValueError("Not playing")
        if self.is_playing_audio:
            raise ValueError("Playing Audio")
        self.player.set_subtitle_track(track_id)

    def toggle_shuffle(self):
        self.set_shuffle(not self.shuffle)

    def set_shuffle(self, shuffle):
        if self.shuffle != shuffle:
            self.shuffle = shuffle
            if self.playlist:
                self.playlist.set_shuffle(self.shuffle)
            self.emit('update-shuffle')

    def toggle_repeat(self):
        if self.repeat == WidgetStateStore.get_repeat_playlist():
            self.set_repeat(WidgetStateStore.get_repeat_track())
        elif self.repeat == WidgetStateStore.get_repeat_track():
            self.set_repeat(WidgetStateStore.get_repeat_off())
        elif self.repeat == WidgetStateStore.get_repeat_off():
            self.set_repeat(WidgetStateStore.get_repeat_playlist())
        #handle unknown values
        else:
            self.set_repeat(WidgetStateStore.get_repeat_off())

    def set_repeat(self, repeat):
        if self.repeat != repeat:
            self.repeat = repeat
            if self.playlist:
                self.playlist.set_repeat(self.repeat)
            self.emit('update-repeat')

    def remove_video_display(self):
        self.removing_video_display = True
        if self.detached_window is not None:
            self.video_display.cleanup()
            self.finish_detached_playback()
        else:
            self.finish_attached_playback()
        self.removing_video_display = False

    def update_current_resume_time(self, resume_time=-1):
        if self._not_skipped_by_user:
            return
        if not self.player_playing() and resume_time == -1:
            # we want to see what the current time is, but the player hasn't
            # started playing yet.  Just return
            return
        item_info = self.playlist.currently_playing
        if resume_time == -1:
            resume_time = self.player.get_elapsed_playback_time()
            duration = self.player.get_total_playback_time()
            # if we are 95% of the way into the movie and less than 30
            # seconds before the end, don't save resume time (#11956)
            if resume_time > min(duration * 0.95, duration - 30):
                resume_time = 0
        if resume_time < 3:
            # if we're in the first three seconds, don't save the
            # resume time.
            # Note: this should match mark_as_watched time.
            resume_time = 0
        m = messages.SetItemResumeTime(item_info, resume_time)
        m.send_to_backend()

    def fast_forward(self):
        self.player.play()
        self.set_playback_rate(3.0)
        self.notify_update()
        
    def fast_backward(self):
        self.player.play()
        self.set_playback_rate(-3.0)
        self.notify_update()

    def stop_fast_playback(self):
        if self.is_playing:
            self.set_playback_rate(1.0)
            if self.is_paused:
                self.player.pause()
            self.notify_update()

    def set_playback_rate(self, rate):
        if self.is_playing:
            self.player.set_playback_rate(rate)

    def suspend(self):
        if self.is_playing and not self.is_paused:
            self.player.pause()
        self.is_suspended = True
    
    def resume(self):
        if self.is_playing and not self.is_paused:
            self.player.play()
        self.is_suspended = False

    def seek_to(self, progress):
        self.player.seek_to(progress)
        # Sigh.  We could seek past the end and require a stop, which
        # calls stop and destroys the player.  After we come back,
        # the player is no longer valid and we crash.  There's probably
        # a better way to fix this.
        try:
            total = self.player.get_total_playback_time()
            if total is not None:
                self.emit('playback-did-progress', progress * total, total)
        except StandardError:
            pass

    def on_movie_finished(self):
        m = messages.MarkItemCompleted(self.playlist.currently_playing)
        m.send_to_backend()
        self.update_current_resume_time(0)
        self._not_skipped_by_user = True
        self.play_next_item()

    def schedule_mark_as_watched(self, info):
        # Note: mark_as_watched time should match the minimum resume
        # time in update_current_resume_time.
        self.mark_as_watched_timeout = timer.add(3, self.mark_as_watched, info)

    def cancel_mark_as_watched(self):
        if self.mark_as_watched_timeout is not None:
            timer.cancel(self.mark_as_watched_timeout)
            self.mark_as_watched_timeout = None

    def mark_as_watched(self, info):
        self.mark_as_watched_timeout = None
        # if we're in a state we don't think we should be in, then we don't
        # want to mark the item as watched.
        if not self.playlist or self.get_playing_item().id != info.id:
            logging.warning("mark_as_watched: not marking the item as "
                    "watched because we're in a weird state")
            return
        self.should_mark_watched.append(info)

    def send_mark_items_watched(self):
        messages.SetItemsWatched(self.should_mark_watched, True).send_to_backend()
        self.should_mark_watched = []

    def get_playing_item(self):
        if self.playlist is None:
            return None
        return self.playlist.currently_playing

    def is_playing_id(self, id_):
        return self.playlist and self.playlist.is_playing_id(id_)

    def is_playing_item(self, item_info):
        return self.is_playing_id(item_info.id)

    def _setup_player(self, item_info, volume):
        def _handle_successful_sniff(item_type):
            logging.debug("sniffer got '%s' for %s", item_type,
                          item_info.video_path)
            self._finish_setup_player(item_info, item_type, volume)
        def _handle_unsuccessful_sniff():
            logging.debug("sniffer got 'unplayable' for %s",
                          item_info.video_path)
            self._finish_setup_player(item_info, "unplayable", volume)
        if item_info.media_type_checked:
            typ = item_info.file_type
            if typ == 'other':
                # the backend and frontend use different names for this
                typ = 'unplayable'
            self._finish_setup_player(item_info, typ, volume)
        else:
            widgetset.get_item_type(item_info, _handle_successful_sniff, _handle_unsuccessful_sniff)
    
    def _finish_setup_player(self, item_info, item_type, volume):
        if item_type == 'audio':
            if self.is_playing and self.video_display is not None:
                # if we were previously playing a video get rid of the video
                # display first
                self.player.stop()
                self.player = None
                self.remove_video_display()
                self.video_display = None
            if self.player is None or not self.is_playing:
                self._build_audio_player(item_info, volume)
            self.is_playing = True
            self.player.setup(item_info, volume)
        elif item_type in ('video', 'unplayable'):
            # We send items with type 'other' to the video display to be able 
            # to open them using the 'play externally' display - luc.
            if self.is_playing and self.video_display is None:
                # if we were previously playing an audio file, stop.
                self.stop()
                return
            if self.video_display is None or not self.is_playing:
                self._build_video_player(item_info, volume)
            self.is_playing = True
            self.video_display.setup(item_info, item_type, volume)
            if self.detached_window is not None:
                self.detached_window.set_title(item_info.name)
        self.emit('did-start-playing')
        app.menu_manager.update_menus('playback-changed')

    def _build_video_player(self, item_info, volume):
        self.player = widgetset.VideoPlayer()
        self.video_display = VideoDisplay(self.player)
        self.video_display.connect('removed', self.on_display_removed)
        self.video_display.connect('cant-play', self._on_cant_play)
        self.video_display.connect('ready-to-play', self._on_ready_to_play)
        if app.config.get(prefs.PLAY_DETACHED):
            self.prepare_detached_playback()
        else:
            self.prepare_attached_playback()
        self.is_playing_audio = False
        app.menu_manager.select_subtitle_encoding(item_info.subtitle_encoding)
        self.initial_subtitle_encoding = item_info.subtitle_encoding

    def _build_audio_player(self, item_info, volume):
        self.player = widgetset.AudioPlayer()
        self.player.connect('cant-play', self._on_cant_play)
        self.player.connect('ready-to-play', self._on_ready_to_play)
        self.is_playing_audio = True

    def _play_current(self, item=None):
        # XXX item is a hint in the case of external playback - where a
        # playlist does not make sense and don't want to rely on it being
        # there.
        self.cancel_update_timer()
        self.cancel_mark_as_watched()
        self._not_skipped_by_user = False


        info_to_play = item if item else self.get_playing_item()
        if info_to_play is None: # end of the playlist
            self.stop()
            return

        play_in_miro = app.config.get(prefs.PLAY_IN_MIRO)
        if self.is_playing:
            self.player.stop(will_play_another=play_in_miro)

        if not play_in_miro:
            app.widgetapp.open_file(info_to_play.video_path)
            messages.MarkItemWatched(info_to_play).send_to_backend()
            return

        volume = app.config.get(prefs.VOLUME_LEVEL)
        self.emit('selecting-file', info_to_play)
        self.open_successful = self.open_finished = False
        self._setup_player(info_to_play, volume)

    def _on_ready_to_play(self, obj):
        playing_item = self.get_playing_item()
        if playing_item is None:
            return
        self.open_successful = self.open_finished = True
        if not playing_item.video_watched:
            self.schedule_mark_as_watched(playing_item)
        if isinstance(self.player, widgetset.VideoPlayer):
            self.player.select_subtitle_encoding(self.initial_subtitle_encoding)
        self.play()

    def _on_cant_play(self, obj):
        playing_item = self.get_playing_item()
        if playing_item is None:
            return
        self.open_finished = True
        self._not_skipped_by_user = True
        self.emit('cant-play-file')
        if isinstance(obj, widgetset.AudioPlayer):
            self.play_next_item()

    def _handle_skip(self):
        playing = self.get_playing_item()
        if not self._not_skipped_by_user and playing is not None:
            self.update_current_resume_time()
            messages.MarkItemSkipped(playing).send_to_backend()

    def play_next_item(self):
        if not self.player_ready():
            return
        self._handle_skip()
        if ((not self.item_continuous_playback_mode(
                            self.playlist.currently_playing) and
             self._not_skipped_by_user)):
            self.stop()
        else:
            self.playlist.select_next_item(self._not_skipped_by_user)
            self._play_current()

    def play_prev_item(self, from_user=False):
        """
        :param from_user: whether or not play_prev_item is being
                          called as a resume of the user pressing a
                          'prev' button or menu item.
        """
        # if the user pressed a prev button or menu item and the
        # current elapsed time is 3 seconds or greater, then we seek
        # to the beginning of the item.
        #
        # otherwise, we move to the previous item in the play list.
        if not self.player_ready():
            return
        if from_user:
            current_time = self.player.get_elapsed_playback_time()
            if current_time > 3:
                self.seek_to(0)
                return
        self._handle_skip()
        self.playlist.select_previous_item()
        self._play_current()

    def skip_forward(self):
        if not self.player_ready():
            return
        self.player.skip_forward()

    def skip_backward(self):
        if not self.player_ready():
            return
        self.player.skip_backward()

    def toggle_fullscreen(self):
        if self.is_fullscreen:
            self.exit_fullscreen()
        else:
            self.enter_fullscreen()

    def enter_fullscreen(self):
        if not self.is_fullscreen:
            self.is_fullscreen = True
            self.video_display.enter_fullscreen()
    
    def exit_fullscreen(self):
        if self.is_fullscreen:
            self.is_fullscreen = False
            self.presentation_mode = 'fit-to-bounds'
            self.video_display.exit_fullscreen()

    def toggle_detached_mode(self):
        if self.is_fullscreen:
            return
        if self.detached_window is None:
            self.switch_to_detached_playback()
        else:
            self.switch_to_attached_playback()
        app.menu_manager.update_menus('playback-changed')
            
    def switch_to_attached_playback(self):
        self.cancel_update_timer()
        self.video_display.prepare_switch_to_attached_playback()
        self.finish_detached_playback()
        self.prepare_attached_playback()
        self.schedule_update()
    
    def switch_to_detached_playback(self):
        self.cancel_update_timer()
        self.video_display.prepare_switch_to_detached_playback()
        self.finish_attached_playback(False)
        self.prepare_detached_playback()
        self.schedule_update()

    def open_subtitle_file(self):
        if not self.is_playing:
            return

        pos = self.player.get_elapsed_playback_time()
        def after_successful_select():
            self.play(start_at=pos)

        self.pause()

        title = _('Open Subtitles File...')
        filters = [(_('Subtitle files'), [ext[1:] for ext in filetypes.SUBTITLES_EXTENSIONS])]
        filename = dialogs.ask_for_open_pathname(title, filters=filters, select_multiple=False)
        if filename is None:
            self.play()
            return

        self.player.select_subtitle_file(filename, after_successful_select)

    def select_subtitle_encoding(self, encoding):
        if self.is_playing:
            self.player.select_subtitle_encoding(encoding)
            messages.SetItemSubtitleEncoding(self.get_playing_item(),
                    encoding).send_to_backend()

    def item_resume_policy(self, item_info):
        """
        There are two kinds of resume results we need. 
        ItemRenderer.should_resume_item() calculates whether an item should 
        display a resume button and PlaybackManager.should_resume() calculates
        whether an item should resume when clicked. This method calculates 
        the general resume policy for an item which these other methods then
        use to calculate their final result.
        """
        # FIXME: we should have a better way of deciding
        # which tab something is listed in.  In addition, assume all items
        # from a remote share is either audio or video (no podcast).
        # Figure out if its from a library or feed. Also, if feed_url
        # is None don't consider it a podcast.
        if (item_info.remote or 
           not item_info.feed_id or
          (item_info.feed_url and
          (item_info.feed_url.startswith('dtv:manualFeed') or
           item_info.feed_url.startswith('dtv:directoryfeed') or
           item_info.feed_url.startswith('dtv:search') or
           item_info.feed_url.startswith('dtv:searchDownloads')))):
            if(item_info.file_type == u'video'):
                resume = app.config.get(prefs.RESUME_VIDEOS_MODE)
            else:
                resume = app.config.get(prefs.RESUME_MUSIC_MODE)
        else:
            resume =  app.config.get(prefs.RESUME_PODCASTS_MODE)
        
        result = (item_info.is_playable
               and item_info.resume_time > 0
               and resume
               and app.config.get(prefs.PLAY_IN_MIRO))
        return result

    def item_continuous_playback_mode(self, item_info):
        if (item_info.remote or
           not item_info.feed_id or
          (item_info.feed_url and
          (item_info.feed_url.startswith('dtv:manualFeed') or
           item_info.feed_url.startswith('dtv:directoryfeed') or
           item_info.feed_url.startswith('dtv:search') or
           item_info.feed_url.startswith('dtv:searchDownloads')))):
            if(item_info.file_type == u'video'):
                continuous_playback = app.config.get(
                                          prefs.CONTINUOUS_VIDEO_PLAYBACK_MODE)
            else:
                continuous_playback = app.config.get(
                                          prefs.CONTINUOUS_MUSIC_PLAYBACK_MODE)
        else:
            continuous_playback = app.config.get(
                                      prefs.CONTINUOUS_PODCAST_PLAYBACK_MODE)

        result = continuous_playback and app.config.get(prefs.PLAY_IN_MIRO)
        return result
Exemplo n.º 4
0
class PlaybackManager (signals.SignalEmitter):
    
    def __init__(self):
        signals.SignalEmitter.__init__(self)
        self.player = None
        self.video_display = None
        self.removing_video_display = False
        self.detached_window = None
        self.previous_left_width = 0
        self.previous_left_widget = None
        self.is_fullscreen = False
        self.is_playing = False
        self.is_playing_audio = False
        self.is_paused = False
        self.is_suspended = False
        self.shuffle = False
        self.repeat = WidgetStateStore.get_repeat_off() 
        self.open_finished = False
        self.open_successful = False
        self.playlist = None
        self.mark_as_watched_timeout = None
        self.update_timeout = None
        self.manual_item_list = None
        self.selected_tab_list = self.selected_tabs = None
        self.presentation_mode = 'fit-to-bounds'
        self.create_signal('will-start')
        self.create_signal('selecting-file')
        self.create_signal('playing-info-changed')
        self.create_signal('cant-play-file')
        self.create_signal('will-play')
        self.create_signal('did-start-playing')
        self.create_signal('will-play-attached')
        self.create_signal('will-play-detached')
        self.create_signal('will-pause')
        self.create_signal('will-stop')
        self.create_signal('did-stop')
        self.create_signal('will-fullscreen')
        self.create_signal('playback-did-progress')
        self.create_signal('update-shuffle')
        self.create_signal('update-repeat')

    def player_ready(self):
        return self.player is not None and self.open_finished

    def player_playing(self):
        return self.player is not None and self.open_successful

    def get_is_playing_video(self):
        return self.is_playing and not self.is_playing_audio
    is_playing_video = property(get_is_playing_video)

    def set_volume(self, volume):
        self.volume = volume
        if self.player is not None:
            self.player.set_volume(volume)
    
    def set_presentation_mode(self, mode):
        self.presentation_mode = mode
        if self.is_playing:
            if not self.is_fullscreen:
                self.fullscreen()
            self.video_display.renderer.update_for_presentation_mode(mode)
    
    def toggle_paused(self):
        """Pause a playing item, play a paused item, and soft_fail otherwise."""
        if not self.is_playing:
            app.widgetapp.handle_soft_failure('toggle_paused',
                "item not playing or paused in toggle_paused",
                with_exception=False)
            return # in release mode, recover by doing nothing
        if self.is_paused:
            self.play()
        else:
            self.pause()

    def start_with_items(self, item_infos):
        """Start playback, playing a static list of ItemInfos."""
        # call stop before anything so that we release our existing
        # manual_item_list (#19932)
        self.stop()
        id_list = [i.id for i in item_infos]
        item_list = app.item_list_pool.get(u'manual', id_list)
        self.manual_item_list = item_list
        self.start(None, item_list)

    def goto_currently_playing(self):
        """Jump to the currently playing item in the display."""
        playing_item = self.get_playing_item()
        if not self.selected_tab_list or not playing_item:
            return
        if (self.is_playing and not
          (self.is_playing_audio or self.detached_window)):
            # playing a video in the app, so don't bother
            return
        try:
            tab_iter = self.selected_tab_list.iter_map[self.selected_tabs[0].id]
        except KeyError:
            #17495 - item may be from a tab that no longer exists
            self.selected_tab_list = self.selected_tabs = None
            return
        app.tabs._select_from_tab_list(self.selected_tab_list.type, tab_iter)
        display = app.display_manager.current_display
        if display and hasattr(display, 'controller'):
            controller = display.controller
            controller.scroll_to_item(playing_item, manual=True, recenter=True)
        else:
            #17488 - GuideDisplay doesn't have a controller
            logging.debug("current display doesn't have a controller - "
                    "can't switch to")

    def start(self, start_id, item_list,
            presentation_mode='fit-to-bounds', force_resume=False):
        """Start playback, playing the items from an ItemTracker"""
        if self.is_playing:
            self.stop()
        self.emit('will-start')

        # Remember where we are, so we can switch to it later
        list_type, selected = app.tabs.selection
        self.selected_tab_list = app.tabs[list_type]
        self.selected_tabs = selected

        play_in_miro = app.config.get(prefs.PLAY_IN_MIRO)
        # Only setup a playlist if we are playing in Miro - otherwise we
        # farm off to an external player for an individual item and the
        # concept of a playlist doesn't really make sense.
        start_item = None
        if play_in_miro:
            self.playlist = PlaybackPlaylist(item_list, start_id,
                                             self.shuffle, self.repeat)
            self.playlist.connect("position-changed",
                self._on_position_changed)
            self.playlist.connect("playing-info-changed",
                self._on_playing_changed)
        else:
            if start_id:
                start_item = item_list.get_item(start_id)
            else:
                start_item = item_list.get_first_item()
        self.should_mark_watched = []
        self.presentation_mode = presentation_mode
        self.force_resume = force_resume
        self._play_current(item=start_item)
        if self.presentation_mode != 'fit-to-bounds':
            self.fullscreen()

    def _on_position_changed(self, playlist):
        self._skipped_by_user = False
        self._play_current()

    def _on_playing_changed(self, playlist):
        new_info = self.get_playing_item()
        if new_info is None or not new_info.is_playable:
            self.stop()
            return
        if self.detached_window:
            if self.detached_window.get_title() != new_info.title:
                self.detached_window.set_title(new_info.title)
        if app.config.get(prefs.PLAY_IN_MIRO):
            self.emit('playing-info-changed', new_info)

    def prepare_attached_playback(self):
        self.emit('will-play-attached')
        splitter = app.widgetapp.window.splitter
        self.previous_left_width = splitter.get_left_width()
        self.previous_left_widget = splitter.left
        splitter.remove_left()
        splitter.set_left_width(0)
        app.display_manager.push_display(self.video_display)            
    
    def finish_attached_playback(self, unselect=True):
        if (self.video_display is not None and
                app.display_manager.current_display is self.video_display):
            app.display_manager.pop_display(unselect)
        app.widgetapp.window.splitter.set_left_width(self.previous_left_width)
        app.widgetapp.window.splitter.set_left(self.previous_left_widget)
    
    def prepare_detached_playback(self):
        self.emit('will-play-detached')
        detached_window_frame = app.config.get(prefs.DETACHED_WINDOW_FRAME)
        if detached_window_frame is None:
            detached_window_frame = widgetset.Rect(0, 0, 800, 600)
        else:
            detached_window_frame = widgetset.Rect.from_string(detached_window_frame)
        title = self.playlist.currently_playing.title
        self.detached_window = DetachedWindow(title, detached_window_frame)
        self.align = widgetset.DetachedWindowHolder()
        self.align.add(self.video_display.widget)
        self.detached_window.set_content_widget(self.align)
        self.detached_window.show()
    
    def finish_detached_playback(self):
        # this prevents negative x and y values from getting saved
        coords = str(self.detached_window.get_frame())
        coords = ",".join([str(max(0, int(c))) for c in coords.split(",")])
        app.config.set(prefs.DETACHED_WINDOW_FRAME, coords)
        app.config.save()
        self.align.remove()
        self.align = None
        self.detached_window.close(False)
        self.detached_window.destroy()
        self.detached_window = None
    
    def schedule_update(self):
        def notify_and_reschedule():
            if self.update_timeout is not None:
                self.update_timeout = None
                if self.is_playing:
                    if not self.is_suspended:
                        self.notify_update()
                    self.schedule_update()
        if self.update_timeout:
            self.cancel_update_timer()
        self.update_timeout = timer.add(0.5, notify_and_reschedule)

    def cancel_update_timer(self):
        if self.update_timeout is not None:
            timer.cancel(self.update_timeout)
            self.update_timeout = None

    def notify_update(self):
        if self.player_playing():
            elapsed = self.player.get_elapsed_playback_time()
            total = self.player.get_total_playback_time()
            if elapsed is not None and total is not None:
                self.emit('playback-did-progress', elapsed, total)
            else:
                logging.warning('notify_update: elapsed = %s total = %s',
                                elapsed, total)

    def on_display_removed(self, display):
        if not self.removing_video_display:
            self._skipped_by_user = False
            self.stop()

    def play(self, start_at=0):
        if not self.player:
            logging.warn("no self.player in play(). race condition?")
            return
        duration = self.player.get_total_playback_time()
        if duration is None or duration <= 0:
            logging.warning('duration is %s', duration)
        self.emit('will-play', duration)
        resume_time = self.playlist.currently_playing.resume_time
        if start_at > 0:
            self.player.play_from_time(start_at)
        elif self.should_resume() and not self.is_paused:
            self.player.play_from_time(resume_time)
        else:
            self.player.play()
        self.notify_update()
        self.schedule_update()
        self.is_paused = False
        self.is_suspended = False
        app.menu_manager.update_menus('playback-changed')

    def should_resume(self):
        if self.force_resume:
            return True

        if(self.shuffle == True or 
           self.repeat != WidgetStateStore.get_repeat_off()):
           return False

        currently_playing = self.playlist.currently_playing
        return self.item_resume_policy(currently_playing)

    def pause(self):
        if self.is_playing:
            self.emit('will-pause')
            self.player.pause()
            self.is_paused = True
            app.menu_manager.update_menus('playback-changed')

    def fullscreen(self):
        if not self.is_playing or not self.video_display:
            return
        self.emit('will-fullscreen')
        self.toggle_fullscreen()

    def stop(self):
        if not self.is_playing:
            return
        if self.get_playing_item() is not None:
            self.update_current_resume_time()
        if self.manual_item_list is not None:
            app.item_list_pool.release(self.manual_item_list)
            self.manual_item_list = None
        self.playlist.finished()
        self.playlist = None
        self.cancel_update_timer()
        self.cancel_mark_as_watched()
        self.send_mark_items_watched()
        self.is_playing = False
        self.is_playing_audio = False
        self.is_paused = False
        self.emit('will-stop')
        if self.player is not None:
            self.player.stop()
            self.player = None
        if self.video_display is not None:
            self.remove_video_display()
            self.video_display = None
        self.is_fullscreen = False
        self.previous_left_widget = None
        self.emit('did-stop')
        app.menu_manager.update_menus('playback-changed')

    def get_audio_tracks(self):
        """Get a list of available audio tracks

        :returns: list of (label, track_id) tuples
        """
        if self.player is not None:
            return self.player.get_audio_tracks()
        else:
            return []

    def get_enabled_audio_track(self):
        """Get the currently enabled audio track

        :returns: current track_id or None if we are not playing
        """
        if self.player is not None:
            return self.player.get_enabled_audio_track()
        else:
            return None

    def set_audio_track(self, track_id):
        """Change the currently enabled audio track

        :param track_id: track_id from get_audio_tracks()
        """
        if self.player is not None:
            self.player.set_audio_track(track_id)
        else:
            raise ValueError("Not playing")

    def get_subtitle_tracks(self):
        """Get a list of available subtitle tracks

        :returns: list of (label, track_id) tuples
        """
        if self.player is not None and not self.is_playing_audio:
            return self.player.get_subtitle_tracks()
        else:
            return []

    def get_enabled_subtitle_track(self):
        """Get the currently enabled subtitle track

        :returns: current track_id or None if we are not playing video
        """
        if self.player is not None and not self.is_playing_audio:
            return self.player.get_enabled_subtitle_track()
        else:
            return None

    def set_subtitle_track(self, track_id):
        """Change the currently enabled subtitle track

        :param track_id: track_id from get_subtitle_tracks()
        """
        if self.player is None:
            raise ValueError("Not playing")
        if self.is_playing_audio:
            raise ValueError("Playing Audio")
        self.player.set_subtitle_track(track_id)

    def toggle_shuffle(self):
        self.set_shuffle(not self.shuffle)

    def set_shuffle(self, shuffle):
        if self.shuffle != shuffle:
            self.shuffle = shuffle
            if self.playlist:
                self.playlist.set_shuffle(self.shuffle)
            self.emit('update-shuffle')

    def toggle_repeat(self):
        if self.repeat == WidgetStateStore.get_repeat_playlist():
            self.set_repeat(WidgetStateStore.get_repeat_track())
        elif self.repeat == WidgetStateStore.get_repeat_track():
            self.set_repeat(WidgetStateStore.get_repeat_off())
        elif self.repeat == WidgetStateStore.get_repeat_off():
            self.set_repeat(WidgetStateStore.get_repeat_playlist())
        #handle unknown values
        else:
            self.set_repeat(WidgetStateStore.get_repeat_off())

    def set_repeat(self, repeat):
        if self.repeat != repeat:
            self.repeat = repeat
            if self.playlist:
                self.playlist.set_repeat(self.repeat)
            self.emit('update-repeat')

    def remove_video_display(self):
        self.removing_video_display = True
        if self.detached_window is not None:
            self.video_display.cleanup()
            self.finish_detached_playback()
        else:
            self.finish_attached_playback()
        self.removing_video_display = False

    def update_current_resume_time(self, resume_time=-1):
        if not self._skipped_by_user:
            return
        if not self.player_playing() and resume_time == -1:
            # we want to see what the current time is, but the player hasn't
            # started playing yet.  Just return
            return
        item_info = self.playlist.currently_playing
        if resume_time == -1:
            resume_time = self.player.get_elapsed_playback_time()
            duration = self.player.get_total_playback_time()
            if duration is None:
                logging.warning('update_current_resume_time: duration is None')
                return
            # if we are 95% of the way into the movie and less than 30
            # seconds before the end, don't save resume time (#11956)
            if resume_time > min(duration * 0.95, duration - 30):
                resume_time = 0
        if resume_time < 3:
            # if we're in the first three seconds, don't save the
            # resume time.
            # Note: this should match mark_as_watched time.
            resume_time = 0
        m = messages.SetItemResumeTime(item_info, resume_time)
        m.send_to_backend()

    def fast_forward(self):
        self.player.play()
        self.set_playback_rate(3.0)
        self.notify_update()
        
    def fast_backward(self):
        self.player.play()
        self.set_playback_rate(-3.0)
        self.notify_update()

    def stop_fast_playback(self):
        if self.is_playing:
            self.set_playback_rate(1.0)
            if self.is_paused:
                self.player.pause()
            self.notify_update()

    def set_playback_rate(self, rate):
        if self.is_playing:
            self.player.set_playback_rate(rate)

    def suspend(self):
        if self.is_playing and not self.is_paused:
            self.player.pause()
        self.is_suspended = True
    
    def resume(self):
        if self.is_playing and not self.is_paused:
            self.player.play()
        self.is_suspended = False

    def seek_to(self, progress):
        self.player.seek_to(progress)
        # Sigh.  We could seek past the end and require a stop, which
        # calls stop and destroys the player.  After we come back,
        # the player is no longer valid and we crash.  There's probably
        # a better way to fix this.
        try:
            total = self.player.get_total_playback_time()
            if total is not None:
                self.emit('playback-did-progress', progress * total, total)
        except StandardError:
            pass

    def on_movie_finished(self):
        self._skipped_by_user = False
        if self.playlist.currently_playing is not None:
            m = messages.MarkItemCompleted(self.playlist.currently_playing)
            m.send_to_backend()
            self.update_current_resume_time(0)
            self.play_next_item()
        else:
            self.stop()

    def schedule_mark_as_watched(self, info):
        # Note: mark_as_watched time should match the minimum resume
        # time in update_current_resume_time.
        self.mark_as_watched_timeout = timer.add(3, self.mark_as_watched, info)

    def cancel_mark_as_watched(self):
        if self.mark_as_watched_timeout is not None:
            timer.cancel(self.mark_as_watched_timeout)
            self.mark_as_watched_timeout = None

    def mark_as_watched(self, info):
        self.mark_as_watched_timeout = None
        # if we're in a state we don't think we should be in, then we don't
        # want to mark the item as watched.
        if not self.playlist or self.get_playing_item().id != info.id:
            logging.warning("mark_as_watched: not marking the item as "
                    "watched because we're in a weird state")
            return
        self.should_mark_watched.append(info)

    def send_mark_items_watched(self):
        messages.SetItemsWatched(self.should_mark_watched, True).send_to_backend()
        self.should_mark_watched = []

    def get_playing_item(self):
        if self.playlist is None:
            return None
        return self.playlist.currently_playing

    def is_playing_id(self, id_):
        return self.playlist and self.playlist.is_playing_id(id_)

    def is_playing_item(self, item_info):
        return self.is_playing_id(item_info.id)

    def _setup_player(self, item_info, volume):
        def _handle_successful_sniff(item_type):
            logging.debug("sniffer got '%s' for %s", item_type,
                          item_info.filename)
            self._finish_setup_player(item_info, item_type, volume)
        def _handle_unsuccessful_sniff():
            logging.debug("sniffer got 'unplayable' for %s",
                          item_info.filename)
            self._finish_setup_player(item_info, "unplayable", volume)
        typ = item_info.file_type
        if typ == 'other':
            # the backend and frontend use different names for this
            typ = 'unplayable'
        self._finish_setup_player(item_info, typ, volume)
    
    def _finish_setup_player(self, item_info, item_type, volume):
        if item_type == 'audio':
            if self.is_playing and self.video_display is not None:
                # if we were previously playing a video get rid of the video
                # display first
                self.player.stop()
                self.player = None
                self.remove_video_display()
                self.video_display = None
            if self.player is None or not self.is_playing:
                self._build_audio_player(item_info, volume)
            self.is_playing = True
            self.player.setup(item_info, volume)
        elif item_type in ('video', 'unplayable'):
            # We send items with type 'other' to the video display to be able 
            # to open them using the 'play externally' display - luc.
            if self.is_playing and self.video_display is None:
                # if we were previously playing an audio file, stop.
                self.stop()
                return
            if self.video_display is None or not self.is_playing:
                self._build_video_player(item_info, volume)
            self.is_playing = True
            self.video_display.setup(item_info, item_type, volume)
            if self.detached_window is not None:
                self.detached_window.set_title(item_info.title)
        self.emit('did-start-playing')
        app.menu_manager.update_menus('playback-changed')

    def _build_video_player(self, item_info, volume):
        self.player = widgetset.VideoPlayer()
        self.video_display = VideoDisplay(self.player)
        self.video_display.connect('removed', self.on_display_removed)
        self.video_display.connect('cant-play', self._on_cant_play)
        self.video_display.connect('ready-to-play', self._on_ready_to_play)
        if app.config.get(prefs.PLAY_DETACHED):
            self.prepare_detached_playback()
        else:
            self.prepare_attached_playback()
        self.is_playing_audio = False
        app.menu_manager.select_subtitle_encoding(item_info.subtitle_encoding)
        self.initial_subtitle_encoding = item_info.subtitle_encoding

    def _build_audio_player(self, item_info, volume):
        self.player = widgetset.AudioPlayer()
        self.player.connect('cant-play', self._on_cant_play)
        self.player.connect('ready-to-play', self._on_ready_to_play)
        self.is_playing_audio = True

    def _play_current(self, item=None):
        # XXX item is a hint in the case of external playback - where a
        # playlist does not make sense and don't want to rely on it being
        # there.
        self.cancel_update_timer()
        self.cancel_mark_as_watched()
        self._skipped_by_user = True

        info_to_play = item if item else self.get_playing_item()
        if info_to_play is None: # end of the playlist
            self.stop()
            return

        play_in_miro = app.config.get(prefs.PLAY_IN_MIRO)
        if self.is_playing:
            self.player.stop(will_play_another=play_in_miro)

        if not play_in_miro:
            app.widgetapp.open_file(info_to_play.filename)
            messages.MarkItemWatched(info_to_play).send_to_backend()
            return

        volume = app.config.get(prefs.VOLUME_LEVEL)
        self.emit('selecting-file', info_to_play)
        self.open_successful = self.open_finished = False
        self._setup_player(info_to_play, volume)

    def _on_ready_to_play(self, obj):
        playing_item = self.get_playing_item()
        if playing_item is None:
            return
        self.open_successful = self.open_finished = True
        if not playing_item.video_watched:
            self.schedule_mark_as_watched(playing_item)
        if isinstance(self.player, widgetset.VideoPlayer):
            self.player.select_subtitle_encoding(self.initial_subtitle_encoding)
        self.play()

    def _on_cant_play(self, obj):
        playing_item = self.get_playing_item()
        if playing_item is None:
            return
        self.open_finished = True
        self._skipped_by_user = False
        self.emit('cant-play-file')
        if isinstance(obj, widgetset.AudioPlayer):
            self.play_next_item()

    def _handle_skip(self):
        playing = self.get_playing_item()
        if self._skipped_by_user and playing is not None:
            self.update_current_resume_time()
            messages.MarkItemSkipped(playing).send_to_backend()

    def play_next_item(self):
        if not self.player_ready():
            return
        self._handle_skip()
        if ((not self.item_continuous_playback_mode(
                            self.playlist.currently_playing) and
             not self._skipped_by_user)):
            if self.repeat:
                self._play_current()
            else: # not repeating, or shuffle
                self.stop()
        else:
            self.playlist.select_next_item(self._skipped_by_user)
            self._play_current()

    def play_prev_item(self, from_user=False):
        """
        :param from_user: whether or not play_prev_item is being
                          called as a resume of the user pressing a
                          'prev' button or menu item.
        """
        # if the user pressed a prev button or menu item and the
        # current elapsed time is 3 seconds or greater, then we seek
        # to the beginning of the item.
        #
        # otherwise, we move to the previous item in the play list.
        if not self.player_ready():
            return
        if from_user:
            current_time = self.player.get_elapsed_playback_time()
            if current_time > 3:
                self.seek_to(0)
                return
        self._handle_skip()
        self.playlist.select_previous_item()
        self._play_current()

    def skip_forward(self):
        if not self.player_ready():
            return
        self.player.skip_forward()

    def skip_backward(self):
        if not self.player_ready():
            return
        self.player.skip_backward()

    def toggle_fullscreen(self):
        if self.is_fullscreen:
            self.exit_fullscreen()
        else:
            self.enter_fullscreen()

    def enter_fullscreen(self):
        if not self.is_fullscreen:
            self.is_fullscreen = True
            self.video_display.enter_fullscreen()
    
    def exit_fullscreen(self):
        if self.is_fullscreen:
            self.is_fullscreen = False
            self.presentation_mode = 'fit-to-bounds'
            self.video_display.exit_fullscreen()

    def toggle_detached_mode(self):
        if self.is_fullscreen:
            return
        if self.detached_window is None:
            self.switch_to_detached_playback()
        else:
            self.switch_to_attached_playback()
        app.menu_manager.update_menus('playback-changed')
            
    def switch_to_attached_playback(self):
        self.cancel_update_timer()
        self.video_display.prepare_switch_to_attached_playback()
        self.finish_detached_playback()
        self.prepare_attached_playback()
        self.schedule_update()
    
    def switch_to_detached_playback(self):
        self.cancel_update_timer()
        self.video_display.prepare_switch_to_detached_playback()
        self.finish_attached_playback(False)
        self.prepare_detached_playback()
        self.schedule_update()

    def open_subtitle_file(self):
        if not self.is_playing:
            return

        pos = self.player.get_elapsed_playback_time()
        def after_successful_select():
            self.play(start_at=pos)

        self.pause()

        title = _('Open Subtitles File...')
        filters = [(_('Subtitle files'), [ext[1:] for ext in filetypes.SUBTITLES_EXTENSIONS])]
        filename = dialogs.ask_for_open_pathname(title, filters=filters, select_multiple=False)
        if filename is None:
            self.play()
            return

        self.player.select_subtitle_file(filename, after_successful_select)

    def select_subtitle_encoding(self, encoding):
        if self.is_playing:
            self.player.select_subtitle_encoding(encoding)
            messages.SetItemSubtitleEncoding(self.get_playing_item(),
                    encoding).send_to_backend()

    def item_resume_policy(self, item_info):
        """
        There are two kinds of resume results we need. 
        ItemRenderer.should_resume_item() calculates whether an item should 
        display a resume button and PlaybackManager.should_resume() calculates
        whether an item should resume when clicked. This method calculates 
        the general resume policy for an item which these other methods then
        use to calculate their final result.
        """
        # FIXME: we should have a better way of deciding
        # which tab something is listed in.  In addition, assume all items
        # from a remote share is either audio or video (no podcast).
        # Figure out if its from a library or feed. Also, if feed_url
        # is None don't consider it a podcast.
        if (item_info.remote or 
           not item_info.feed_id or
          (item_info.feed_url and
          (item_info.feed_url.startswith('dtv:manualFeed') or
           item_info.feed_url.startswith('dtv:directoryfeed') or
           item_info.feed_url.startswith('dtv:search') or
           item_info.feed_url.startswith('dtv:searchDownloads')))):
            if(item_info.file_type == u'video'):
                resume = app.config.get(prefs.RESUME_VIDEOS_MODE)
            else:
                resume = app.config.get(prefs.RESUME_MUSIC_MODE)
        else:
            resume =  app.config.get(prefs.RESUME_PODCASTS_MODE)
        
        result = (item_info.is_playable
               and item_info.resume_time > 0
               and resume
               and app.config.get(prefs.PLAY_IN_MIRO))
        return result

    def item_continuous_playback_mode(self, item_info):
        if (item_info.remote or
           not item_info.feed_id or
          (item_info.feed_url and
          (item_info.feed_url.startswith('dtv:manualFeed') or
           item_info.feed_url.startswith('dtv:directoryfeed') or
           item_info.feed_url.startswith('dtv:search') or
           item_info.feed_url.startswith('dtv:searchDownloads')))):
            if(item_info.file_type == u'video'):
                continuous_playback = app.config.get(
                                          prefs.CONTINUOUS_VIDEO_PLAYBACK_MODE)
            else:
                continuous_playback = app.config.get(
                                          prefs.CONTINUOUS_MUSIC_PLAYBACK_MODE)
        else:
            continuous_playback = app.config.get(
                                      prefs.CONTINUOUS_PODCAST_PLAYBACK_MODE)

        result = continuous_playback and app.config.get(prefs.PLAY_IN_MIRO)
        return result
Exemplo n.º 5
0
class PlaybackManager (signals.SignalEmitter):
    
    def __init__(self):
        signals.SignalEmitter.__init__(self)
        self.player = None
        self.video_display = None
        self.removing_video_display = False
        self.detached_window = None
        self.previous_left_width = 0
        self.previous_left_widget = None
        self.is_fullscreen = False
        self.is_playing = False
        self.is_playing_audio = False
        self.is_paused = False
        self.is_suspended = False
        self.open_finished = False
        self.open_successful = False
        self.playlist = None
        self.position = 0
        self.mark_as_watched_timeout = None
        self.update_timeout = None
        self.presentation_mode = 'fit-to-bounds'
        self.create_signal('selecting-file')
        self.create_signal('cant-play-file')
        self.create_signal('will-play')
        self.create_signal('did-start-playing')
        self.create_signal('will-play-attached')
        self.create_signal('will-play-detached')
        self.create_signal('will-pause')
        self.create_signal('will-stop')
        self.create_signal('did-stop')
        self.create_signal('will-fullscreen')
        self.create_signal('playback-did-progress')
        app.info_updater.item_changed_callbacks.add('manual', 'playback-list',
                self._on_items_changed)

    def player_ready(self):
        return self.player is not None and self.open_finished

    def player_playing(self):
        return self.player is not None and self.open_successful

    def _on_items_changed(self, message):
        if self.playlist is None:
            return
        deleted = message.removed[:]
        for info in message.changed:
            if info.id not in self.id_to_position:
                # item was removed from our playlist already
                continue
            if not info.downloaded:
                deleted.append(info.id)
            else:
                self.playlist[self.id_to_position[info.id]] = info
        if len(deleted) > 0:
            self._handle_items_deleted(deleted)

    def _handle_items_deleted(self, id_list):
        if self.playlist is None:
            return
        to_delete = []
        deleting_current = False
        # Figure out what which items are in our playlist.
        for id in id_list:
            try:
                pos = self.id_to_position[id]
            except KeyError:
                continue
            to_delete.append(pos)
            if pos == self.position:
                deleting_current = True
        if len(to_delete) == 0:
            return
        # Delete those items (we need to do it last to first)
        to_delete.sort(reverse=True)
        for pos in to_delete:
            del self.playlist[pos]
            if pos < self.position:
                self.position -= 1
        if self.position >= len(self.playlist):
            # we deleted the current movie and all the ones after it
            self.stop(save_resume_time=False)
        elif deleting_current:
            self.play_from_position(self.position, save_resume_time=False)
        if self.playlist is not None:
            # Recalculate id_to_position, since the playlist has changed
            self._calc_id_to_position()

    def set_volume(self, volume):
        self.volume = volume
        if self.player is not None:
            self.player.set_volume(volume)
    
    def set_presentation_mode(self, mode):
        self.presentation_mode = mode
        if self.is_playing:
            if not self.is_fullscreen:
                self.fullscreen()
            self.video_display.renderer.update_for_presentation_mode(mode)
    
    def play_pause(self):
        if not self.is_playing or self.is_paused:
            self.play()
        else:
            self.pause()
    
    def start_with_items(self, item_infos, presentation_mode='fit-to-bounds'):
        if self.is_playing:
            self.stop()
        self.playlist = []
        for info in item_infos:
            self._append_item(info)
        self.position = 0
        self._calc_id_to_position()
        self.presentation_mode = presentation_mode
        self._play_current()
        if self.playlist is None:
            # _play_current found that PLAY_IN_MIRO was set to False
            return
        self._start_tracking_items()
        if self.presentation_mode != 'fit-to-bounds':
            self.fullscreen()

    def append_item(self, item_info):
        if not self.is_playing:
            raise ValueError("Can't append items when not playing")
        self._append_item(item_info)
        self.id_to_position[item_info.id] = len(self.playlist) - 1
        # need to reset our TrackItemsManually view, since we now have a new
        # id to track
        self._stop_tracking_items()
        self._start_tracking_items()

    def _append_item(self, item_info):
        if not item_info.is_container_item:
            self.playlist.append(item_info)
        else:
            playlables = [i for i in item_info.children if i.is_playable]
            self.playlist.extend(playlables)

    def _start_tracking_items(self):
        id_list = [info.id for info in self.playlist]
        m = messages.TrackItemsManually('playback-list', id_list)
        m.send_to_backend()

    def _stop_tracking_items(self):
        m = messages.StopTrackingItems('manual', 'playback-list')
        m.send_to_backend()

    def _calc_id_to_position(self):
        self.id_to_position = dict((info.id, i) for i, info in
                enumerate(self.playlist))
    
    def prepare_attached_playback(self):
        self.emit('will-play-attached')
        splitter = app.widgetapp.window.splitter
        self.previous_left_width = splitter.get_left_width()
        self.previous_left_widget = splitter.left
        splitter.remove_left()
        splitter.set_left_width(0)
        app.display_manager.push_display(self.video_display)            
    
    def finish_attached_playback(self, unselect=True):
        app.display_manager.pop_display(unselect)
        app.widgetapp.window.splitter.set_left_width(self.previous_left_width)
        app.widgetapp.window.splitter.set_left(self.previous_left_widget)
    
    def prepare_detached_playback(self):
        self.emit('will-play-detached')
        detached_window_frame = app.config.get(prefs.DETACHED_WINDOW_FRAME)
        if detached_window_frame is None:
            detached_window_frame = widgetset.Rect(0, 0, 800, 600)
        else:
            detached_window_frame = widgetset.Rect.from_string(detached_window_frame)
        title = self.playlist[self.position].name
        self.detached_window = DetachedWindow(title, detached_window_frame)
        self.align = widgetset.DetachedWindowHolder()
        self.align.add(self.video_display.widget)
        self.detached_window.set_content_widget(self.align)
        self.detached_window.show()
    
    def finish_detached_playback(self):
        # this prevents negative x and y values from getting saved
        coords = str(self.detached_window.get_frame())
        coords = ",".join([str(max(0, int(c))) for c in coords.split(",")])
        app.config.set(prefs.DETACHED_WINDOW_FRAME, coords)
        app.config.save()
        self.align.remove()
        self.align = None
        self.detached_window.close(False)
        self.detached_window.destroy()
        self.detached_window = None
    
    def schedule_update(self):
        def notify_and_reschedule():
            if self.update_timeout is not None:
                self.update_timeout = None
                if self.is_playing and not self.is_paused:
                    if not self.is_suspended:
                        self.notify_update()
                    self.schedule_update()
        self.update_timeout = timer.add(0.5, notify_and_reschedule)

    def cancel_update_timer(self):
        if self.update_timeout is not None:
            timer.cancel(self.update_timeout)
            self.update_timeout = None

    def notify_update(self):
        if self.player_playing():
            elapsed = self.player.get_elapsed_playback_time()
            total = self.player.get_total_playback_time()
            self.emit('playback-did-progress', elapsed, total)

    def on_display_removed(self, display):
        if not self.removing_video_display:
            self.stop()

    def play(self, start_at=0):
        if not self.player:
            return
        duration = self.player.get_total_playback_time()
        self.emit('will-play', duration)
        resume_time = self.playlist[self.position].resume_time
        if start_at > 0:
            self.player.play_from_time(start_at)
        elif (app.config.get(prefs.RESUME_VIDEOS_MODE)
               and not self.is_paused):
            self.player.play_from_time(resume_time)
        else:
            self.player.play()
        self.notify_update()
        self.schedule_update()
        self.is_paused = False
        self.is_suspended = False
        app.menu_manager.update_menus()

    def pause(self):
        if self.is_playing:
            self.emit('will-pause')
            self.player.pause()
            self.is_paused = True
            app.menu_manager.update_menus()

    def fullscreen(self):
        if not self.is_playing:
            return
        self.emit('will-fullscreen')
        self.toggle_fullscreen()

    def stop(self, save_resume_time=True):
        if not self.is_playing:
            return
        self._stop_tracking_items()
        if save_resume_time:
            self.update_current_resume_time()
        self.cancel_update_timer()
        self.cancel_mark_as_watched()
        self.is_playing = False
        self.is_playing_audio = False
        self.is_paused = False
        self.emit('will-stop')
        if self.player is not None:
            self.player.stop()
            self.player = None
        if self.video_display is not None:
            self.remove_video_display()
            self.video_display = None
        self.is_fullscreen = False
        self.previous_left_widget = None
        self.position = 0
        self.playlist = None
        self.emit('did-stop')

    def remove_video_display(self):
        self.removing_video_display = True
        if self.detached_window is not None:
            self.video_display.cleanup()
            self.finish_detached_playback()
        else:
            self.finish_attached_playback()
        self.removing_video_display = False

    def update_current_resume_time(self, resume_time=-1):
        if not self.player_playing() and resume_time == -1:
            # we want to see what the current time is, but the player hasn't
            # started playing yet.  Just return
            return
        item_info = self.playlist[self.position]
        if app.config.get(prefs.RESUME_VIDEOS_MODE):
            if resume_time == -1:
                resume_time = self.player.get_elapsed_playback_time()
                # if we are 95% of the way into the movie and less than 30
                # seconds before the end, don't save resume time (#11956)
                if resume_time > min(item_info.duration * 0.95,
                        item_info.duration - 30):
                    resume_time = 0
            if resume_time < 3:
                # if we're in the first three seconds, don't save the
                # resume time.
                # Note: this should match mark_as_watched time.
                resume_time = 0
        else:
            resume_time = 0
        m = messages.SetItemResumeTime(item_info.id, resume_time)
        m.send_to_backend()

    def fast_forward(self):
        self.player.play()
        self.set_playback_rate(3.0)
        self.notify_update()
        
    def fast_backward(self):
        self.player.play()
        self.set_playback_rate(-3.0)
        self.notify_update()

    def stop_fast_playback(self):
        if self.is_playing:
            self.set_playback_rate(1.0)
            if self.is_paused:
                self.player.pause()
            self.notify_update()

    def set_playback_rate(self, rate):
        if self.is_playing:
            self.player.set_playback_rate(rate)

    def suspend(self):
        if self.is_playing and not self.is_paused:
            self.player.pause()
        self.is_suspended = True
    
    def resume(self):
        if self.is_playing and not self.is_paused:
            self.player.play()
        self.is_suspended = False

    def seek_to(self, progress):
        self.player.seek_to(progress)
        # Sigh.  We could seek past the end and require a stop, which
        # calls stop and destroys the player.  After we come back,
        # the player is no longer valid and we crash.  There's probably
        # a better way to fix this.
        try:
            total = self.player.get_total_playback_time()
            if total is not None:
                self.emit('playback-did-progress', progress * total, total)
        except:
            pass

    def on_movie_finished(self):
        id_ = self.playlist[self.position].id
        messages.MarkItemCompleted(id_).send_to_backend()
        self.update_current_resume_time(0)
        self.play_next_item(False)

    def schedule_mark_as_watched(self, id_):
        # Note: mark_as_watched time should match the minimum resume
        # time in update_current_resume_time.
        self.mark_as_watched_timeout = timer.add(3, self.mark_as_watched, id_)

    def cancel_mark_as_watched(self):
        if self.mark_as_watched_timeout is not None:
            timer.cancel(self.mark_as_watched_timeout)
            self.mark_as_watched_timeout = None

    def mark_as_watched(self, id_):
        self.mark_as_watched_timeout = None
        # if we're in a state we don't think we should be in, then we don't
        # want to mark the item as watched.
        if not self.playlist or self.playlist[self.position].id != id_:
            logging.warning("mark_as_watched: not marking the item as watched because we're in a weird state")
            return
        messages.MarkItemWatched(id_).send_to_backend()

    def get_playing_item(self):
        if self.playlist:
            return self.playlist[self.position]
        return None

    def is_playing_id(self, id_):
        return self.playlist and self.playlist[self.position].id == id_

    def _setup_player(self, item_info, volume):
        def _handle_successful_sniff(item_type):
            self._finish_setup_player(item_info, item_type, volume)
        def _handle_unsuccessful_sniff():
            self._finish_setup_player(item_info, "unplayable", volume)
        if item_info.media_type_checked:
            typ = item_info.file_type
            if typ == 'other':
                # the backend and frontend use different names for this
                typ = 'unplayable'
            self._finish_setup_player(item_info, typ, volume)
        else:
            widgetset.get_item_type(item_info, _handle_successful_sniff, _handle_unsuccessful_sniff)
    
    def _finish_setup_player(self, item_info, item_type, volume):
        if item_type == 'audio':
            if self.is_playing and self.video_display is not None:
                # if we were previously playing a video get rid of the video
                # display first
                self.player.stop()
                self.player = None
                self.remove_video_display()
                self.video_display = None
            if self.player is None or not self.is_playing:
                self._build_audio_player(item_info, volume)
            self.is_playing = True
            self.player.setup(item_info, volume)
        elif item_type in ('video', 'unplayable'):
            # We send items with type 'other' to the video display to be able 
            # to open them using the 'play externally' display - luc.
            if self.is_playing and self.video_display is None:
                # if we were previously playing an audio file, stop.
                self.stop()
                return
            if self.video_display is None or not self.is_playing:
                self._build_video_player(item_info, volume)
            self.is_playing = True
            self.video_display.setup(item_info, volume)
            if self.detached_window is not None:
                self.detached_window.set_title(item_info.name)
        self.emit('did-start-playing')
        app.menu_manager.update_menus()

    def _build_video_player(self, item_info, volume):
        self.player = widgetset.VideoPlayer()
        self.video_display = VideoDisplay(self.player)
        self.video_display.connect('removed', self.on_display_removed)
        self.video_display.connect('cant-play', self._on_cant_play)
        self.video_display.connect('ready-to-play', self._on_ready_to_play)
        if app.config.get(prefs.PLAY_DETACHED):
            self.prepare_detached_playback()
        else:
            self.prepare_attached_playback()
        self.is_playing_audio = False
        app.menu_manager.select_subtitle_encoding(item_info.subtitle_encoding)
        self.initial_subtitle_encoding = item_info.subtitle_encoding

    def _build_audio_player(self, item_info, volume):
        self.player = widgetset.AudioPlayer()
        self.player.connect('cant-play', self._on_cant_play)
        self.player.connect('ready-to-play', self._on_ready_to_play)
        self.is_playing_audio = True

    def _select_current(self):
        item_info = self.playlist[self.position]
        if not app.config.get(prefs.PLAY_IN_MIRO):
            if self.is_playing:
                self.stop(save_resume_time=False)
            # FIXME - do this to avoid "currently playing green thing.
            # should be a better way.
            self.playlist = None
            app.widgetapp.open_file(item_info.video_path)
            if not item_info.item_viewed:
                messages.MarkItemWatched(item_info.id).send_to_backend()
            return

        volume = app.config.get(prefs.VOLUME_LEVEL)
        self.emit('selecting-file', item_info)
        self.open_successful = self.open_finished = False
        self._setup_player(item_info, volume)

    def _play_current(self, new_position=None, save_resume_time=True):
        """If you pass in new_position, then this will attempt to play
        that and will update self.position ONLY if the new_position
        doesn't exceed the bounds of the playlist.
        """
        if new_position == None:
            new_position = self.position
        else:
            id_ = self.playlist[self.position].id
            messages.MarkItemSkipped(id_).send_to_backend()

        self.cancel_update_timer()
        self.cancel_mark_as_watched()
        if self.playlist is not None and (0 <= new_position < len(self.playlist)):
            self.position = new_position
            if self.is_playing:
                self.player.stop(True)
            self._select_current()
        else:
            self.stop(save_resume_time)

    def _on_ready_to_play(self, obj):
        self.open_successful = self.open_finished = True
        if not self.playlist[self.position].video_watched:
            self.schedule_mark_as_watched(self.playlist[self.position].id)
        if isinstance(self.player, widgetset.VideoPlayer):
            self.player.select_subtitle_encoding(self.initial_subtitle_encoding)
        self.play()

    def _on_cant_play(self, obj):
        self.open_finished = True
        self.emit('cant-play-file')
        if isinstance(obj, widgetset.AudioPlayer):
            self.play_next_item(False)

    def play_next_item(self, save_resume_time=True):
        if not self.player_ready():
            return
        self.play_from_position(self.position + 1, save_resume_time)

    def play_prev_item(self, save_resume_time=True, from_user=False):
        """
        :param save_resume_time: whether or not to save the resume
                                 time of the currently playing item
                                 before switching to the previous
                                 item.
        :param from_user: whether or not play_prev_item is being
                          called as a resume of the user pressing a
                          'prev' button or menu item.
        """
        # if the user pressed a prev button or menu item and the
        # current elapsed time is 3 seconds or greater, then we seek
        # to the beginning of the item.
        #
        # otherwise, we move to the previous item in the play list.
        if not self.player_ready():
            return
        if from_user:
            current_time = self.player.get_elapsed_playback_time()
            if current_time > 3:
                self.seek_to(0)
                return

        self.play_from_position(self.position - 1, save_resume_time)

    def play_from_position(self, new_position, save_resume_time=True):
        self.cancel_update_timer()
        self.cancel_mark_as_watched()
        if app.config.get(prefs.SINGLE_VIDEO_PLAYBACK_MODE):
            self.stop(save_resume_time)
        else:
            if save_resume_time:
                self.update_current_resume_time()
            self._play_current(new_position, save_resume_time)

    def skip_forward(self):
        if not self.player_ready():
            return
        self.player.skip_forward()

    def skip_backward(self):
        if not self.player_ready():
            return
        self.player.skip_backward()

    def toggle_fullscreen(self):
        if self.is_fullscreen:
            self.exit_fullscreen()
        else:
            self.enter_fullscreen()

    def enter_fullscreen(self):
        self.is_fullscreen = True
        self.video_display.enter_fullscreen()
    
    def exit_fullscreen(self):
        self.is_fullscreen = False
        self.presentation_mode = 'fit-to-bounds'
        self.video_display.exit_fullscreen()

    def toggle_detached_mode(self):
        if self.is_fullscreen:
            return
        if self.detached_window is None:
            self.switch_to_detached_playback()
        else:
            self.switch_to_attached_playback()
        app.menu_manager.update_menus()
            
    def switch_to_attached_playback(self):
        self.cancel_update_timer()
        self.video_display.prepare_switch_to_attached_playback()
        self.finish_detached_playback()
        self.prepare_attached_playback()
        self.schedule_update()
    
    def switch_to_detached_playback(self):
        self.cancel_update_timer()
        self.video_display.prepare_switch_to_detached_playback()
        self.finish_attached_playback(False)
        self.prepare_detached_playback()
        self.schedule_update()

    def open_subtitle_file(self):
        if not self.is_playing:
            return

        pos = self.player.get_elapsed_playback_time()
        def after_successful_select():
            self.play(start_at=pos)

        self.pause()

        title = _('Open Subtitles File...')
        filters = [(_('Subtitle files'), [ext[1:] for ext in filetypes.SUBTITLES_EXTENSIONS])]
        filename = dialogs.ask_for_open_pathname(title, filters=filters, select_multiple=False)
        if filename is None:
            self.play()
            return

        self.player.select_subtitle_file(filename, after_successful_select)

    def select_subtitle_encoding(self, encoding):
        if self.is_playing:
            item_info = self.playlist[self.position]
            self.player.select_subtitle_encoding(encoding)
            messages.SetItemSubtitleEncoding(item_info.id,
                    encoding).send_to_backend()