Ejemplo n.º 1
0
    def init_player(self):

        def mpv_log(loglevel, component, message):
            self.print('Mpv log: [{}] {}: {}'.format(loglevel, component, message))

        mpv_args = []
        mpv_kw = {
            'wid': int(self.ui.video.winId()),
            'keep-open': 'yes',
            'rebase-start-time': 'no',
            'framedrop': 'no',
            'osd-level': '2',
            'osd-fractions': 'yes',
        }
        for opt in self.mpv_options:
            if '=' in opt:
                k, v = opt.split('=', 1)
                mpv_kw[k] = v
            else:
                mpv_args.append(opt)

        player = MPV(*mpv_args, log_handler=mpv_log, **mpv_kw)
        self.player = player
        player.pause = True

        def on_player_loaded():
            if self.ffmpeg_bin:
                self.check_ffmpeg_seek_problem()
            self.load_state()
            self.ui.loading.hide()
            self.state_loaded = True

        self.player_loaded.connect(on_player_loaded)

        def on_playback_len(s):
            self.playback_len = s
            player.unobserve_property('duration', on_playback_len)

        def on_playback_pos(s):
            if self.playback_pos is None:
                self.player_loaded.emit()
            self.playback_pos = s
            self.statusbar_update.emit()
            self.ui.seekbar.update()

        player.observe_property('time-pos', on_playback_pos)
        player.observe_property('duration', on_playback_len)
        player.play(self.filename)
Ejemplo n.º 2
0
class AudioPlayer:
    def __init__(self, filename):
        self.filename = filename

        self.mpv = MPV(input_default_bindings=True, input_vo_keyboard=True)
        self.mpv.pause = True
        self.mpv.play(self.filename)

        self.mpv.wait_for_property("length")
        self.length = self.mpv.length * 1000

    def position(self):
        return self.mpv.time_pos * 1000

    def progress(self):
        return self.position() / self.length

    def pause(self):
        self.mpv.pause = True

    def play(self):
        self.mpv.pause = False

    def toggle_pause(self):
        self.mpv.pause = not self.mpv.pause

    def seek_progress(self, progress):
        self.mpv.seek(self.mpv.length * progress, "absolute", "exact")

    def seek_relative(self, ms):
        self.mpv.time_pos += ms / 1000

    def observe_position(self, observer):
        def converting_observer(prop, value):
            observer(value * 1000)

        self.mpv.observe_property("time-pos", converting_observer)

    def __del__(self):
        # TODO may never be called, see https://github.com/RafeKettler/magicmethods/blob/master/magicmethods.pdf
        self.mpv.quit()
        self.mpv.terminate()
Ejemplo n.º 3
0
class MpvWidget(QOpenGLWidget):
    _schedule_update = pyqtSignal()

    def __init__(self, api: Api, parent: Optional[QWidget] = None) -> None:
        super().__init__(parent)
        self._api = api

        locale.setlocale(locale.LC_NUMERIC, "C")

        self._destroyed = False
        self._need_subs_refresh = False

        self.mpv = MPV(vo="opengl-cb",
                       ytdl=False,
                       loglevel="info",
                       log_handler=print)
        self.mpv_gl = _mpv_get_sub_api(self.mpv.handle,
                                       MpvSubApi.MPV_SUB_API_OPENGL_CB)
        self.on_update_c = OpenGlCbUpdateFn(self.on_update)
        self.on_update_fake_c = OpenGlCbUpdateFn(self.on_update_fake)
        self.get_proc_addr_c = OpenGlCbGetProcAddrFn(get_proc_addr)
        _mpv_opengl_cb_set_update_callback(self.mpv_gl, self.on_update_c, None)
        self.frameSwapped.connect(self.swapped,
                                  Qt.ConnectionType.DirectConnection)

        for key, value in {
                # "config": False,
                "quiet": False,
                "msg-level": "all=info",
                "osc": False,
                "osd-bar": False,
                "input-cursor": False,
                "input-vo-keyboard": False,
                "input-default-bindings": False,
                "ytdl": False,
                "sub-auto": False,
                "audio-file-auto": False,
                "vo": "null" if api.args.no_video else "libmpv",
                "hwdec": "no",
                "pause": True,
                "idle": True,
                "blend-subtitles": "video",
                "video-sync": "display-vdrop",
                "keepaspect": True,
                "stop-playback-on-init-failure": False,
                "keep-open": True,
                "track-auto-selection": False,
        }.items():
            setattr(self.mpv, key, value)

        self._opengl = None

        self._timer = QTimer(parent=None)
        self._timer.setInterval(api.cfg.opt["video"]["subs_sync_interval"])
        self._timer.timeout.connect(self._refresh_subs_if_needed)

        api.subs.loaded.connect(self._on_subs_load)
        api.video.stream_created.connect(self._on_video_state_change)
        api.video.stream_unloaded.connect(self._on_video_state_change)
        api.video.current_stream_switched.connect(self._on_video_state_change)
        api.audio.stream_created.connect(self._on_audio_state_change)
        api.audio.stream_unloaded.connect(self._on_audio_state_change)
        api.audio.current_stream_switched.connect(self._on_audio_state_change)
        api.playback.request_seek.connect(self._on_request_seek,
                                          Qt.ConnectionType.DirectConnection)
        api.playback.request_playback.connect(self._on_request_playback)
        api.playback.playback_speed_changed.connect(
            self._on_playback_speed_change)
        api.playback.volume_changed.connect(self._on_volume_change)
        api.playback.mute_changed.connect(self._on_mute_change)
        api.playback.pause_changed.connect(self._on_pause_change)
        api.video.view.zoom_changed.connect(self._on_video_zoom_change)
        api.video.view.pan_changed.connect(self._on_video_pan_change)
        api.gui.terminated.connect(self.shutdown)
        self._schedule_update.connect(self.update)

        self.mpv.observe_property("time-pos", self._on_mpv_time_pos_change)
        self.mpv.observe_property("track-list", self._on_mpv_track_list_change)
        self.mpv.observe_property("pause", self._on_mpv_pause_change)

        self._timer.start()

    def initializeGL(self) -> None:
        _mpv_opengl_cb_init_gl(self.mpv_gl, None, self.get_proc_addr_c, None)

    def paintGL(self) -> None:
        ratio = self.devicePixelRatioF()
        w = int(self.width() * ratio)
        h = int(self.height() * ratio)
        _mpv_opengl_cb_draw(self.mpv_gl, self.defaultFramebufferObject(), w,
                            -h)

    @pyqtSlot()
    def maybe_update(self) -> None:
        if self._destroyed:
            return
        if self.window().isMinimized():
            self.makeCurrent()
            self.paintGL()
            self.context().swapBuffers(self.context().surface())
            self.swapped()
            self.doneCurrent()
        else:
            self.update()

    def on_update(self, ctx: Any = None) -> None:
        # maybe_update method should run on the thread that creates the
        # OpenGLContext, which in general is the main thread.
        # QMetaObject.invokeMethod can do this trick.
        QMetaObject.invokeMethod(self, "maybe_update")

    def on_update_fake(self, ctx: Any = None) -> None:
        pass

    def swapped(self) -> None:
        _mpv_opengl_cb_report_flip(self.mpv_gl, 0)

    def closeEvent(self, _: Any) -> None:
        self.makeCurrent()
        if self.mpv_gl:
            _mpv_opengl_cb_set_update_callback(self.mpv_gl,
                                               self.on_update_fake_c, None)
        _mpv_opengl_cb_uninit_gl(self.mpv_gl)

    def _on_subs_load(self) -> None:
        self._api.subs.script_info.changed.subscribe(
            lambda _event: self._on_subs_change())
        self._api.subs.events.changed.subscribe(
            lambda _event: self._on_subs_change())
        self._api.subs.styles.changed.subscribe(
            lambda _event: self._on_subs_change())

    def _on_video_state_change(self, stream: VideoStream) -> None:
        self._sync_media()
        self._need_subs_refresh = True

    def _on_audio_state_change(self, stream: AudioStream) -> None:
        self._sync_media()

    def _sync_media(self) -> None:
        self.mpv.pause = True
        self.mpv.loadfile("null://")
        external_files: set[str] = set()
        for video_stream in self._api.video.streams:
            external_files.add(str(video_stream.path))
        for audio_stream in self._api.audio.streams:
            external_files.add(str(audio_stream.path))
        self.mpv.external_files = list(external_files)
        if not external_files:
            self._api.playback.state = PlaybackFrontendState.NOT_READY
        else:
            self._api.playback.state = PlaybackFrontendState.LOADING

    def shutdown(self) -> None:
        self._destroyed = True
        self.makeCurrent()
        if self._opengl:
            self._opengl.set_update_callback(lambda: None)
            self._opengl.close()
        self.deleteLater()
        self._timer.stop()

    def _refresh_subs_if_needed(self) -> None:
        if self._need_subs_refresh:
            self._refresh_subs()

    def _refresh_subs(self) -> None:
        if not self._api.playback.is_ready:
            return
        if self.mpv.sub:
            try:
                self.mpv.command("sub_remove")
            except mpv.MPVError:
                pass
        with io.StringIO() as handle:
            write_ass(self._api.subs.ass_file, handle)
            self.mpv.command("sub_add", "memory://" + handle.getvalue())
        self._need_subs_refresh = False

    def _set_end(self, end: Optional[int]) -> None:
        if not self._api.playback.is_ready:
            return
        if end is None:
            ret = "none"
        else:
            end = max(0, end - 1)
            ret = ms_to_str(end)
        if self.mpv.end != ret:
            self.mpv.end = ret

    def _on_request_seek(self, pts: int, precise: bool) -> None:
        self._set_end(None)  # mpv refuses to seek beyond --end
        self.mpv.seek(ms_to_str(pts), "absolute", "exact")

    def _on_request_playback(self, start: Optional[int],
                             end: Optional[int]) -> None:
        if start is not None:
            self.mpv.seek(ms_to_str(start), "absolute")
        self._set_end(end)
        self.mpv.pause = False

    def _on_playback_speed_change(self) -> None:
        self.mpv.speed = float(self._api.playback.playback_speed)

    def _on_volume_change(self) -> None:
        self.mpv.volume = float(self._api.playback.volume)

    def _on_mute_change(self) -> None:
        self.mpv.mute = self._api.playback.is_muted

    def _on_pause_change(self, is_paused: bool) -> None:
        self._set_end(None)
        self.mpv.pause = is_paused

    def _on_video_zoom_change(self) -> None:
        # ignore errors coming from setting extreme values
        try:
            self.mpv.video_zoom = float(self._api.video.view.zoom)
        except mpv.MPVError:
            pass

    def _on_video_pan_change(self) -> None:
        # ignore errors coming from setting extreme values
        try:
            self.mpv.video_pan_x = float(self._api.video.view.pan_x)
            self.mpv.video_pan_y = float(self._api.video.view.pan_y)
        except mpv.MPVError:
            pass

    def _on_subs_change(self) -> None:
        self._need_subs_refresh = True

    def _on_mpv_unload(self) -> None:
        self._api.playback.state = PlaybackFrontendState.NOT_READY

    def _on_mpv_load(self) -> None:
        self._api.playback.state = PlaybackFrontendState.READY
        self._need_subs_refresh = True

    def _on_track_list_ready(self, track_list: Any) -> None:
        # self._api.log.debug(json.dumps(track_list, indent=4))
        vid: Optional[int] = None
        aid: Optional[int] = None

        current_audio_stream: Optional[AudioStream]
        try:
            current_audio_stream = self._api.audio.current_stream
        except ResourceUnavailable:
            current_audio_stream = None

        current_video_stream: Optional[VideoStream]
        try:
            current_video_stream = self._api.video.current_stream
        except ResourceUnavailable:
            current_video_stream = None

        for track in track_list:
            track_type = track["type"]
            track_path = track.get("external-filename")

            if (track_type == "video" and current_video_stream
                    and current_video_stream.path.samefile(track_path)):
                vid = track["id"]

            if (track_type == "audio" and current_audio_stream
                    and current_audio_stream.path.samefile(track_path)):
                aid = track["id"]

        if self.mpv.vid != vid:
            self.mpv.vid = vid if vid is not None else "no"
            self._api.log.debug(f"playback: changing vid to {vid}")

        if self.mpv.aid != aid:
            self.mpv.aid = aid if aid is not None else "no"
            self._api.log.debug(f"playback: changing aid to {aid}")

        delay = (current_audio_stream.delay
                 if current_audio_stream else 0) / 1000.0
        if self.mpv.audio_delay != delay:
            self.mpv.audio_delay = delay

        if vid is not None or aid is not None:
            self._api.playback.state = PlaybackFrontendState.READY
        else:
            self._api.playback.state = PlaybackFrontendState.NOT_READY

    def _on_mpv_time_pos_change(self, prop_name: str, new_value: Any) -> None:
        pts = round((new_value or 0) * 1000)
        self._api.playback.receive_current_pts_change.emit(pts)

    def _on_mpv_pause_change(self, prop_name: str, new_value: Any) -> None:
        self._api.playback.pause_changed.disconnect(self._on_pause_change)
        self._api.playback.is_paused = new_value
        self._api.playback.pause_changed.connect(self._on_pause_change)

    def _on_mpv_track_list_change(self, prop_name: str,
                                  new_value: Any) -> None:
        self._on_track_list_ready(new_value)
Ejemplo n.º 4
0
class MpvPlayer(AbstractPlayer):
    """

    player will always play playlist current song. player will listening to
    playlist ``song_changed`` signal and change the current playback.

    TODO: make me singleton
    """
    def __init__(self, audio_device=b'auto', winid=None, *args, **kwargs):
        super(MpvPlayer, self).__init__(*args, **kwargs)
        # https://github.com/cosven/FeelUOwn/issues/246
        locale.setlocale(locale.LC_NUMERIC, 'C')
        mpvkwargs = {}
        if winid is not None:
            mpvkwargs['wid'] = winid
        mpvkwargs['vo'] = 'opengl-cb'
        # set log_handler if you want to debug
        # mpvkwargs['log_handler'] = self.__log_handler
        # mpvkwargs['msg_level'] = 'all=v'
        self._mpv = MPV(ytdl=True,
                        input_default_bindings=True,
                        input_vo_keyboard=True,
                        **mpvkwargs)
        _mpv_set_property_string(self._mpv.handle, b'audio-device', audio_device)

        # TODO: 之后可以考虑将这个属性加入到 AbstractPlayer 中
        self.video_format_changed = Signal()

    def initialize(self):
        self._mpv.observe_property(
            'time-pos',
            lambda name, position: self._on_position_changed(position)
        )
        self._mpv.observe_property(
            'duration',
            lambda name, duration: self._on_duration_changed(duration)
        )
        self._mpv.observe_property(
            'video-format',
            lambda name, vformat: self._on_video_format_changed(vformat)
        )
        # self._mpv.register_event_callback(lambda event: self._on_event(event))
        self._mpv._event_callbacks.append(self._on_event)
        self._playlist.song_changed.connect(self._on_song_changed)
        self.song_finished.connect(self._on_song_finished)
        logger.info('Player initialize finished.')

    def shutdown(self):
        self._mpv.terminate()

    def play(self, url, video=True):
        # NOTE - API DESGIN: we should return None, see
        # QMediaPlayer API reference for more details.

        logger.debug("Player will play: '%s'", url)

        if video:
            # FIXME: for some property, we need to set via setattr, however,
            #  some need to be set via _mpv_set_property_string
            self._mpv.handle.vid = b'auto'
            # it seems that ytdl will auto choose the default format
            #  if we set ytdl-format to ''
            _mpv_set_property_string(self._mpv.handle, b'ytdl-format', b'')
        else:
            # set vid to no and ytdl-format to bestaudio/best
            # see https://mpv.io/manual/stable/#options-vid for more details
            self._mpv.handle.vid = b'no'
            _mpv_set_property_string(self._mpv.handle, b'ytdl-format', b'bestaudio/best')

        # Clear playlist before play next song,
        # otherwise, mpv will seek to the last position and play.
        self._mpv.playlist_clear()
        self._mpv.play(url)
        self._mpv.pause = False
        self.state = State.playing
        self._current_url = url
        self.media_changed.emit(url)

    def play_song(self, song):
        """播放指定歌曲

        如果目标歌曲与当前歌曲不相同,则修改播放列表当前歌曲,
        播放列表会发出 song_changed 信号,player 监听到信号后调用 play 方法,
        到那时才会真正的播放新的歌曲。如果和当前播放歌曲相同,则忽略。

        .. note::

            调用方不应该直接调用 playlist.current_song = song 来切换歌曲
        """
        if song is not None and song == self.current_song:
            logger.warning('The song is already under playing.')
        else:
            self._playlist.current_song = song

    def play_next(self):
        """播放下一首歌曲

        .. note::

            这里不能使用 ``play_song(playlist.next_song)`` 方法来切换歌曲,
            ``play_song`` 和 ``playlist.current_song = song`` 是有区别的。
        """
        self.playlist.current_song = self.playlist.next_song

    def play_previous(self):
        self.playlist.current_song = self.playlist.previous_song

    def replay(self):
        self.playlist.current_song = self.current_song

    def resume(self):
        self._mpv.pause = False
        self.state = State.playing

    def pause(self):
        self._mpv.pause = True
        self.state = State.paused

    def toggle(self):
        self._mpv.pause = not self._mpv.pause
        if self._mpv.pause:
            self.state = State.paused
        else:
            self.state = State.playing

    def stop(self):
        self._mpv.pause = True
        self.state = State.stopped
        self._current_url = None
        self._mpv.playlist_clear()
        logger.info('Player stopped.')

    @property
    def position(self):
        return self._position

    @position.setter
    def position(self, position):
        self._mpv.seek(position, reference='absolute')
        self._position = position

    @AbstractPlayer.volume.setter
    def volume(self, value):
        super(MpvPlayer, MpvPlayer).volume.__set__(self, value)
        self._mpv.volume = self.volume

    @property
    def video_format(self):
        self._video_format

    @video_format.setter
    def video_format(self, vformat):
        self._video_format = vformat
        self.video_format_changed.emit(vformat)

    def _on_position_changed(self, position):
        self._position = position
        self.position_changed.emit(position)

    def _on_duration_changed(self, duration):
        """listening to mpv duration change event"""
        logger.debug('Player receive duration changed signal')
        self.duration = duration

    def _on_video_format_changed(self, vformat):
        self.video_format = vformat

    def _on_song_changed(self, song):
        """播放列表 current_song 发生变化后的回调

        判断变化后的歌曲是否有效的,有效则播放,否则将它标记为无效歌曲。
        如果变化后的歌曲是 None,则停止播放。
        """
        if song is not None:
            if song.url:
                self.play(song.url)
            else:
                self._playlist.mark_as_bad(song)
                self.play_next()
        else:
            self.stop()

    def _on_event(self, event):
        if event['event_id'] == MpvEventID.END_FILE:
            reason = event['event']['reason']
            logger.debug('Current song finished. reason: %d' % reason)
            if self.state != State.stopped and reason != MpvEventEndFile.ABORTED:
                self.song_finished.emit()

    def _on_song_finished(self):
        if self._playlist.playback_mode == PlaybackMode.one_loop:
            self.replay()
        else:
            self.play_next()

    def __log_handler(self, loglevel, component, message):
        print('[{}] {}: {}'.format(loglevel, component, message))
Ejemplo n.º 5
0
class MpvPlayer(AbstractPlayer):
    """

    player will always play playlist current song. player will listening to
    playlist ``song_changed`` signal and change the current playback.

    todo: make me singleton
    """
    def __init__(self, audio_device=b'auto', winid=None, *args, **kwargs):
        super(MpvPlayer, self).__init__(*args, **kwargs)
        # https://github.com/cosven/FeelUOwn/issues/246
        locale.setlocale(locale.LC_NUMERIC, 'C')
        mpvkwargs = {}
        if winid is not None:
            mpvkwargs['wid'] = winid
        mpvkwargs['vo'] = 'opengl-cb'
        # set log_handler if you want to debug
        # mpvkwargs['log_handler'] = self.__log_handler
        # mpvkwargs['msg_level'] = 'all=v'
        logger.info('libmpv version %s', _mpv_client_api_version())
        self._mpv = MPV(ytdl=False,
                        input_default_bindings=True,
                        input_vo_keyboard=True,
                        **mpvkwargs)
        _mpv_set_property_string(self._mpv.handle, b'audio-device', audio_device)
        # old version libmpv(for example: (1, 20)) should set option by using
        # _mpv_set_option_string, while newer version can use _mpv_set_property_string
        _mpv_set_option_string(self._mpv.handle, b'user-agent',
                               b'Mozilla/5.0 (Windows NT 10.0; Win64; x64)')

        #: if video_format changes to None, there is no video available
        self.video_format_changed = Signal()

    def initialize(self):
        self._mpv.observe_property(
            'time-pos',
            lambda name, position: self._on_position_changed(position)
        )
        self._mpv.observe_property(
            'duration',
            lambda name, duration: self._on_duration_changed(duration)
        )
        self._mpv.observe_property(
            'video-format',
            lambda name, vformat: self._on_video_format_changed(vformat)
        )
        # self._mpv.register_event_callback(lambda event: self._on_event(event))
        self._mpv._event_callbacks.append(self._on_event)
        self._playlist.song_changed.connect(self._on_song_changed)
        self.song_finished.connect(self._on_song_finished)
        logger.debug('Player initialize finished.')

    def shutdown(self):
        self._mpv.terminate()

    def play(self, media, video=True):
        logger.debug("Player will play: '%s'", media)
        if isinstance(media, Media):
            media = media
        else:  # media is a url
            media = Media(media)
        self._set_http_headers(media.http_headers)
        url = media.url

        # Clear playlist before play next song,
        # otherwise, mpv will seek to the last position and play.
        self._mpv.playlist_clear()
        self._mpv.play(url)
        self._mpv.pause = False
        self.state = State.playing
        self._current_media = media
        # TODO: we will emit a media object
        self.media_changed.emit(media)

    def prepare_media(self, song, done_cb=None):
        if song.meta.support_multi_quality:
            media, quality = song.select_media('hq<>')
        else:
            media = song.url
        media = Media(media) if media else None
        if done_cb is not None:
            done_cb(media)
        return media

    def play_song(self, song):
        """播放指定歌曲

        如果目标歌曲与当前歌曲不相同,则修改播放列表当前歌曲,
        播放列表会发出 song_changed 信号,player 监听到信号后调用 play 方法,
        到那时才会真正的播放新的歌曲。如果和当前播放歌曲相同,则忽略。

        .. note::

            调用方不应该直接调用 playlist.current_song = song 来切换歌曲
        """
        if song is not None and song == self.current_song:
            logger.warning('The song is already under playing.')
        else:
            self._playlist.current_song = song

    def play_songs(self, songs):
        """(alpha) play list of songs"""
        self.playlist.init_from(songs)
        self.play_next()

    def play_next(self):
        """播放下一首歌曲

        .. note::

            这里不能使用 ``play_song(playlist.next_song)`` 方法来切换歌曲,
            ``play_song`` 和 ``playlist.current_song = song`` 是有区别的。
        """
        warnings.warn('use playlist.next instead, this will be removed on 3.4')
        self.playlist.next()

    def play_previous(self):
        warnings.warn('use playlist.previous instead, this will be removed on 3.4')
        self.playlist.previous()

    def replay(self):
        self.playlist.current_song = self.current_song

    def resume(self):
        self._mpv.pause = False
        self.state = State.playing

    def pause(self):
        self._mpv.pause = True
        self.state = State.paused

    def toggle(self):
        self._mpv.pause = not self._mpv.pause
        if self._mpv.pause:
            self.state = State.paused
        else:
            self.state = State.playing

    def stop(self):
        self._mpv.pause = True
        self.state = State.stopped
        self._current_media = None
        self._mpv.playlist_clear()
        logger.info('Player stopped.')

    @property
    def position(self):
        return self._position

    @position.setter
    def position(self, position):
        if self._current_media:
            self._mpv.seek(position, reference='absolute')
            self._position = position
        else:
            logger.warn("can't set position when current media is empty")

    @AbstractPlayer.volume.setter
    def volume(self, value):
        super(MpvPlayer, MpvPlayer).volume.__set__(self, value)
        self._mpv.volume = self.volume

    @property
    def video_format(self):
        return self._video_format

    @video_format.setter
    def video_format(self, vformat):
        self._video_format = vformat
        self.video_format_changed.emit(vformat)

    def _on_position_changed(self, position):
        self._position = position
        self.position_changed.emit(position)

    def _on_duration_changed(self, duration):
        """listening to mpv duration change event"""
        logger.debug('Player receive duration changed signal')
        self.duration = duration

    def _on_video_format_changed(self, vformat):
        self.video_format = vformat

    def _on_song_changed(self, song):
        """播放列表 current_song 发生变化后的回调

        判断变化后的歌曲是否有效的,有效则播放,否则将它标记为无效歌曲。
        如果变化后的歌曲是 None,则停止播放。
        """
        def prepare_callback(media):
            if media is not None:
                self.play(media)
            else:
                self._playlist.mark_as_bad(song)
                self.play_next()

        if song is not None:
            self.prepare_media(song, done_cb=prepare_callback)
        else:
            self.stop()

    def _on_event(self, event):
        if event['event_id'] == MpvEventID.END_FILE:
            reason = event['event']['reason']
            logger.debug('Current song finished. reason: %d' % reason)
            if self.state != State.stopped and reason != MpvEventEndFile.ABORTED:
                self.song_finished.emit()

    def _on_song_finished(self):
        if self._playlist.playback_mode == PlaybackMode.one_loop:
            self.replay()
        else:
            self.play_next()

    def _set_http_headers(self, http_headers):
        if http_headers:
            headers = []
            for key, value in http_headers.items():
                headers.append("{}: {}".format(key, value))
            headers_text = ','.join(headers)
            headers_bytes = bytes(headers_text, 'utf-8')
            logger.info('play media with headers: %s', headers_text)
            _mpv_set_option_string(self._mpv.handle, b'http-header-fields',
                                   headers_bytes)
        else:
            _mpv_set_option_string(self._mpv.handle, b'http-header-fields',
                                   b'')

    def __log_handler(self, loglevel, component, message):
        print('[{}] {}: {}'.format(loglevel, component, message))
Ejemplo n.º 6
0
class WotabagManager(object):

    def __init__(self, config_file):
        yaml = YAML(typ='safe')
        if isinstance(config_file, IOBase):
            config = yaml.load(config_file)
        else:
            with open(config_file) as f:
                config = yaml.load(f)

        logging.config.dictConfig(config.get('logging', {}))
        self.logger = logging.getLogger('wotabag')

        self.rpc_host = config.get('rpc_host', '127.0.0.1')
        self.rpc_port = config.get('rpc_port', 60715)
        self._load_playlist(config.get('playlist', []))
        volume = int(config.get('volume', 0))
        if volume > 100:
            volume = 100
        elif volume < 0:
            volume = 0
        self.volume = volume
        self.current_track = 0
        self.status = WotabagStatus.IDLE
        self._status_lock = Semaphore()

        # init LED strip
        self.strip = init_strip()
        self.strip.begin()

        # init playback
        self.player = None
        self.song = None
        self._stopped = Event()
        self._stopped.set()
        self._playback_thread = None

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        if self.status == WotabagStatus.PLAYING:
            self._stopped.set()
            if self._playback_thread:
                self._playback_thread.join()
            if self.player:
                self.player.terminate()

    def _mpv_log(self, loglevel, component, message):
        f = {
            'fatal': self.logger.critical,
            'error': self.logger.error,
            'warn': self.logger.warning,
            'info': self.logger.info,
            'verbose': self.logger.debug,
            'debug': self.logger.debug,
            'trace': self.logger.debug,
        }
        if loglevel in f:
            f[loglevel]('[MPV] {}: {}'.format(component, message))

    def _load_file(self, yaml_file):
        yaml = YAML(typ='safe')
        with open(yaml_file) as f:
            song = yaml.load(f)
        self.logger.debug('loaded {} ({})'.format(song['title'], song['filename']))
        self.song = song

    def _load_playlist(self, playlist):
        yaml = YAML(typ='safe')
        self.playlist = []
        for yaml_file in playlist:
            with open(yaml_file) as f:
                song = yaml.load(f)
                self.playlist.append((yaml_file, song['title']))

    def _play(self):
        self._stop()
        self._stopped.clear()

        self._status_lock.acquire()
        self.status = WotabagStatus.PLAYING
        self._status_lock.release()

        self._playback_thread = Thread(target=self._wota_playback)
        self.logger.debug('starting wota playback thread')
        self._playback_thread.start()

    def _stop(self):
        self._stopped.set()
        if self._playback_thread:
            self._playback_thread.join()
            self._playback_thread = None
            self.logger.debug('joined wota playback thread')
        if self.player:
            self.player.terminate()
            self.player = None
        self.song = None

        for i in range(self.strip.numPixels()):
            self.strip.setPixelColor(i, BladeColor.NONE.value)
        self.strip.show()

        self._status_lock.acquire()
        self.status = WotabagStatus.IDLE
        self._status_lock.release()

    def _wota_playback(self):
        while self.current_track < len(self.playlist):
            song, _ = self.playlist[self.current_track]
            self._load_file(song)

            if self.player:
                self.player.terminate()
            self.player = MPV(vid='no', hwdec='mmal', keep_open='yes', volume=self.volume, log_handler=self._mpv_log)
            self.player.play(self.song['filename'])

            # wait for mpv to actually start playing
            playback_lock = Semaphore(value=0)

            def observer(name, val):
                if val is not None:
                    playback_lock.release()

            self.player.observe_property('time-pos', observer)
            playback_lock.acquire()
            self.player.unobserve_property('time-pos', observer)

            start = time.time()
            ticks = 0
            # drift = 0
            bpm = 120
            cur_colors = {
                'left': BladeColor.YOSHIKO,
                'center': BladeColor.YOSHIKO,
                'right': BladeColor.YOSHIKO,
            }

            initial_offset = self.song.get('initial_offset', 0)
            if initial_offset:
                time.sleep((start + initial_offset / 1000) - time.time())

            last_tick = time.perf_counter()
            # play led patterns
            for pattern in self.song['patterns']:
                if 'bpm' in pattern:
                    bpm = pattern['bpm']
                for k in ['left', 'center', 'right']:
                    if k in pattern:
                        if isinstance(pattern[k], list):
                            colors = []
                            for c in pattern[k]:
                                color = c.upper()
                                if color in BladeColor.__members__:
                                    colors.append(BladeColor[color])
                            cur_colors[k] = tuple(colors)
                        else:
                            color = pattern[k].upper()
                            if color in BladeColor.__members__:
                                cur_colors[k] = BladeColor[color]
                kwargs = pattern.get('kwargs', {})
                kwargs.update(cur_colors)
                wota = WOTA_TYPE[pattern['type']](bpm=bpm, strip=self.strip, **kwargs)
                count = pattern.get('count', 1)
                for i in range(count):
                    for _ in range(len(wota)):
                        if self._stopped.is_set():
                            return
                        next_tick = last_tick + wota.tick_s
                        # if i == 0 or i == count - 1:
                        #     loop = False
                        # else:
                        #     loop = True
                        loop = True
                        wota.tick(loop=loop)
                        diff = next_tick - time.perf_counter()
                        last_tick = next_tick
                        # drift = 0
                        if diff > 0:
                            time.sleep(diff)
                        # elif diff < 0:
                        #     drift = diff
                        ticks += 1

            with self.player._playback_cond:
                # wait for mpv to reach the end of the audio file or 5 seconds,
                # whichever comes first
                self.player._playback_cond.wait(5)

            # end of song, setup next track
            self.player.terminate()
            self.player = None
            for i in range(self.strip.numPixels()):
                self.strip.setPixelColor(i, BladeColor.NONE.value)
            self.strip.show()
            self.current_track += 1

        self.current_track = 0
        self._status_lock.acquire()
        self.status = WotabagStatus.IDLE
        self._status_lock.release()

    @public
    def get_playlist(self):
        """Return this wotabag's playlist."""
        self.logger.info('[RPC] wotabag.get_playlist')
        return [title for _, title in self.playlist]

    @public
    def get_status(self):
        """Return current status."""
        self.logger.info('[RPC] wotabag.get_status')
        result = {
            'status': self.status.name,
            'volume': self.volume,
        }
        if self.status == WotabagStatus.IDLE:
            if self.playlist:
                _, title = self.playlist[self.current_track]
                result['next_track'] = title
            else:
                result['next_track'] = None
        elif self.status == WotabagStatus.PLAYING:
            _, title = self.playlist[self.current_track]
            result['current_track'] = title
            next_track = self.current_track + 1
            if next_track < len(self.playlist):
                _, title = self.playlist[next_track]
                result['next_track'] = title
            else:
                result['next_track'] = None
        return result

    @public
    def get_volume(self):
        """Return current volume."""
        self.logger.info('[RPC] wotabag.get_volume')
        return self.volume

    @public
    def set_volume(self, volume):
        """Set volume."""
        self.logger.info('[RPC] wotabag.set_volume {}'.format(volume))
        volume = int(volume)
        if volume > 100:
            volume = 100
        elif volume < 0:
            volume = 0
        self.volume = volume
        if self.player:
            self.player.volume = self.volume
        return self.volume

    @public
    def get_colors(self):
        """Return list of available colors."""
        self.logger.info('[RPC] wotabag.get_colors')
        colors = ['None'] + \
            [x.name.capitalize() for x in aqours] + list(aqours_units.keys()) + ['Aqours Rainbow'] + \
            [x.name.capitalize() for x in saint_snow] + ['Saint Snow'] + \
            [x.name.capitalize() for x in muse] + list(muse_units.keys())
        return colors

    @public
    def set_color(self, color):
        """Set all LEDs to the specified color or color sequence."""
        self.logger.info('[RPC] wotabag.set_color {}'.format(color))
        if color == 'Aqours Rainbow':
            colors = aqours_rainbow
        elif color in aqours_units:
            colors = aqours_units[color]
        elif color == 'Saint Snow':
            colors = saint_snow
        elif color in muse_units:
            colors = muse_units[color]
        elif color.upper() in BladeColor.__members__:
            colors = (BladeColor[color.upper()],)
        else:
            raise BadRequestError('Unknown color')

        strip = self.strip
        if len(colors) == 1:
            for i in range(strip.numPixels()):
                strip.setPixelColor(i, colors[0].value)
            strip.show()
        elif len(colors) <= 3:
            if len(colors) == 2:
                colors = colors + (colors[0],)
            for x, color in (enumerate(colors)):
                for y in range(9):
                    strip.setPixelColor(pixel_index(x, y), color.value)
            strip.show()
        elif len(colors) == 9:
            for x in range(3):
                for y, color in enumerate(colors):
                    strip.setPixelColor(pixel_index(x, y), color.value)
            strip.show()

    @public
    def power_off(self):
        """Power off the device.

        Note:
            If 'shutdown -h' fails, the returncode will be returned.
            If shutdown succeeds, the connection will be dropped immediately
            (this will appear as a returned None to a tinyrpc client).

        """
        self.logger.info('[RPC] wotabag.power_off')
        # clear led's before power off otherwise they will stay turned on until
        # the separate led battery source is manually switched off
        for i in range(self.strip.numPixels()):
            self.strip.setPixelColor(i, BladeColor.NONE.value)
        self.strip.show()
        proc = subprocess.run(['shutdown', '-h', 'now'])
        return proc.returncode

    @public
    def play(self):
        """Start playback of the next song."""
        self.logger.info('[RPC] wotabag.play')
        self._play()

    @public
    def play_index(self, index):
        """Start playback of the specified song."""
        self.logger.info('[RPC] wotabag.play_index {}'.format(index))
        if index >= len(self.playlist) or index < 0:
            raise BadRequestError('Invalid song index')
        self.current_track = index
        self._play()

    @public
    def stop(self):
        """Stop playback."""
        self.logger.info('[RPC] wotabag.stop')
        self._stop()

    @public
    def test_pattern(self):
        """Display test color wipe patterns."""
        self.logger.info('[RPC] wotabag.test_pattern')
        gevent.spawn_later(0, test_wipe, self.strip, clear=True)
Ejemplo n.º 7
0
class MpvPlayer(AbstractPlayer):
    """

    player will always play playlist current song. player will listening to
    playlist ``song_changed`` signal and change the current playback.

    TODO: make me singleton
    """
    def __init__(self, audio_device=b'auto', *args, **kwargs):
        super(MpvPlayer, self).__init__()
        self._mpv = MPV(ytdl=False,
                        input_default_bindings=True,
                        input_vo_keyboard=True)

        _mpv_set_property_string(self._mpv.handle, b'audio-device',
                                 audio_device)

        self._playlist = Playlist()
        self._playlist.song_changed.connect(self._on_song_changed)

    def initialize(self):
        self._mpv.observe_property(
            'time-pos',
            lambda name, position: self._on_position_changed(position))
        self._mpv.observe_property(
            'duration',
            lambda name, duration: self._on_duration_changed(duration))
        # self._mpv.register_event_callback(lambda event: self._on_event(event))
        self._mpv.event_callbacks.append(self._on_event)
        self.song_finished.connect(self.play_next)
        logger.info('Player initialize finished.')

    def shutdown(self):
        del self._mpv

    def play(self, url):
        # NOTE - API DESGIN: we should return None, see
        # QMediaPlayer API reference for more details.

        logger.debug("Player will play: '%s'", url)

        # Clear playlist before play next song,
        # otherwise, mpv will seek to the last position and play.
        self._mpv.playlist_clear()
        self._mpv.play(url)
        self._mpv.pause = False
        self.state = State.playing
        self.media_changed.emit(url)

    def play_song(self, song):
        if self.playlist.current_song is not None and \
                self.playlist.current_song == song:
            logger.warning('the song to be played is same as current song')
            return

        url = song.url
        if url:
            self._playlist.current_song = song
        else:
            logger.warning("Invalid song: song url can't be None or ''")
            self._playlist.mark_as_bad(song)

    def play_next(self):
        self.playlist.current_song = self.playlist.next_song

    def play_previous(self):
        self.playlist.current_song = self.playlist.previous_song

    def resume(self):
        self._mpv.pause = False
        self.state = State.playing

    def pause(self):
        self._mpv.pause = True
        self.state = State.paused

    def toggle(self):
        self._mpv.pause = not self._mpv.pause
        if self._mpv.pause:
            self.state = State.paused
        else:
            self.state = State.playing

    def stop(self):
        logger.info('stop player...')
        self._mpv.pause = True
        self.state = State.stopped
        self._mpv.playlist_clear()

    @property
    def position(self):
        return self._position

    @position.setter
    def position(self, position):
        self._mpv.seek(position, reference='absolute')
        self._position = position

    @AbstractPlayer.volume.setter
    def volume(self, value):
        super(MpvPlayer, MpvPlayer).volume.__set__(self, value)
        self._mpv.volume = self.volume

    def _on_position_changed(self, position):
        self._position = position
        self.position_changed.emit(position)

    def _on_duration_changed(self, duration):
        """listening to mpv duration change event"""
        logger.info('player receive duration changed signal')
        self.duration = duration

    def _on_song_changed(self, song):
        logger.debug('player received song changed signal')
        if song is not None:
            logger.info('Will play song: %s' % self._playlist.current_song)
            self.play(song.url)
        else:
            self.stop()
            logger.info('playlist provide no song anymore.')

    def _on_event(self, event):
        if event['event_id'] == MpvEventID.END_FILE:
            reason = event['event']['reason']
            logger.debug('Current song finished. reason: %d' % reason)
            if self.state != State.stopped and reason != MpvEventEndFile.ABORTED:
                self.song_finished.emit()
Ejemplo n.º 8
0
class MpvPlayer(AbstractPlayer):
    """

    player will always play playlist current song. player will listening to
    playlist ``song_changed`` signal and change the current playback.

    todo: make me singleton
    """
    def __init__(self, audio_device=b'auto', winid=None, *args, **kwargs):
        super(MpvPlayer, self).__init__(*args, **kwargs)
        # https://github.com/cosven/FeelUOwn/issues/246
        locale.setlocale(locale.LC_NUMERIC, 'C')
        mpvkwargs = {}
        if winid is not None:
            mpvkwargs['wid'] = winid
        mpvkwargs['vo'] = 'opengl-cb'
        # set log_handler if you want to debug
        # mpvkwargs['log_handler'] = self.__log_handler
        # mpvkwargs['msg_level'] = 'all=v'
        logger.info('libmpv version %s', _mpv_client_api_version())
        self._mpv = MPV(ytdl=False,
                        input_default_bindings=True,
                        input_vo_keyboard=True,
                        **mpvkwargs)
        _mpv_set_property_string(self._mpv.handle, b'audio-device',
                                 audio_device)
        # old version libmpv(for example: (1, 20)) should set option by using
        # _mpv_set_option_string, while newer version can use _mpv_set_property_string
        _mpv_set_option_string(self._mpv.handle, b'user-agent',
                               b'Mozilla/5.0 (Windows NT 10.0; Win64; x64)')

        #: if video_format changes to None, there is no video available
        self.video_format_changed = Signal()

        self._mpv.observe_property(
            'time-pos',
            lambda name, position: self._on_position_changed(position))
        self._mpv.observe_property(
            'duration',
            lambda name, duration: self._on_duration_changed(duration))
        self._mpv.observe_property(
            'video-format',
            lambda name, vformat: self._on_video_format_changed(vformat))
        # self._mpv.register_event_callback(lambda event: self._on_event(event))
        self._mpv._event_callbacks.append(self._on_event)
        logger.debug('Player initialize finished.')

    def shutdown(self):
        self._mpv.terminate()

    def play(self, media, video=True):
        logger.debug("Player will play: '%s'", media)
        if isinstance(media, Media):
            media = media
        else:  # media is a url
            media = Media(media)
        self._set_http_headers(media.http_headers)
        url = media.url

        # Clear playlist before play next song,
        # otherwise, mpv will seek to the last position and play.
        self.media_about_to_changed.emit(self._current_media, media)
        self._mpv.playlist_clear()
        self._mpv.play(url)
        self._current_media = media
        self.media_changed.emit(media)

    def set_play_range(self, start=None, end=None):
        start = str(start) if start is not None else 'none'
        end = str(end) if end is not None else 'none'
        _mpv_set_option_string(self._mpv.handle, b'start',
                               bytes(start, 'utf-8'))
        _mpv_set_option_string(self._mpv.handle, b'end', bytes(end, 'utf-8'))

    def resume(self):
        self._mpv.pause = False
        self.state = State.playing

    def pause(self):
        self._mpv.pause = True
        self.state = State.paused

    def toggle(self):
        self._mpv.pause = not self._mpv.pause
        if self._mpv.pause:
            self.state = State.paused
        else:
            self.state = State.playing

    def stop(self):
        self._mpv.pause = True
        self.state = State.stopped
        self._current_media = None
        self._mpv.playlist_clear()
        logger.info('Player stopped.')

    @property
    def position(self):
        return self._position

    @position.setter
    def position(self, position):
        if self._current_media:
            self._mpv.seek(position, reference='absolute')
            self._position = position
        else:
            logger.warn("can't set position when current media is empty")

    @AbstractPlayer.volume.setter
    def volume(self, value):
        super(MpvPlayer, MpvPlayer).volume.__set__(self, value)
        self._mpv.volume = self.volume

    @property
    def video_format(self):
        return self._video_format

    @video_format.setter
    def video_format(self, vformat):
        self._video_format = vformat
        self.video_format_changed.emit(vformat)

    def _on_position_changed(self, position):
        self._position = position
        self.position_changed.emit(position)

    def _on_duration_changed(self, duration):
        """listening to mpv duration change event"""
        logger.debug('Player receive duration changed signal')
        self.duration = duration

    def _on_video_format_changed(self, vformat):
        self.video_format = vformat

    def _on_event(self, event):
        if event['event_id'] == MpvEventID.END_FILE:
            reason = event['event']['reason']
            logger.debug('Current song finished. reason: %d' % reason)
            if self.state != State.stopped and reason != MpvEventEndFile.ABORTED:
                self.media_finished.emit()

    def _set_http_headers(self, http_headers):
        if http_headers:
            headers = []
            for key, value in http_headers.items():
                headers.append("{}: {}".format(key, value))
            headers_text = ','.join(headers)
            headers_bytes = bytes(headers_text, 'utf-8')
            logger.info('play media with headers: %s', headers_text)
            _mpv_set_option_string(self._mpv.handle, b'http-header-fields',
                                   headers_bytes)
        else:
            _mpv_set_option_string(self._mpv.handle, b'http-header-fields',
                                   b'')

    def __log_handler(self, loglevel, component, message):
        print('[{}] {}: {}'.format(loglevel, component, message))