From a6e6ffae0654bf271f7b4dcb33dbef28ded997e3 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Mon, 20 Jan 2020 22:01:38 +1000 Subject: [PATCH] get mpv slave mode working with new API Also move the mpv-specific hooks into AVPlayer --- qt/aqt/gui_hooks.py | 98 +++++++++++++++-------------- qt/aqt/main.py | 22 ++++--- qt/aqt/profiles.py | 2 - qt/aqt/sound.py | 129 +++++++++++++++++++-------------------- qt/tools/genhooks_gui.py | 6 +- 5 files changed, 130 insertions(+), 127 deletions(-) diff --git a/qt/aqt/gui_hooks.py b/qt/aqt/gui_hooks.py index 1a4cb2ce5..4f1db5852 100644 --- a/qt/aqt/gui_hooks.py +++ b/qt/aqt/gui_hooks.py @@ -75,6 +75,54 @@ class _AddCardsWillShowHistoryMenuHook: add_cards_will_show_history_menu = _AddCardsWillShowHistoryMenuHook() +class _AvPlayerDidPlayHook: + _hooks: List[Callable[[], None]] = [] + + def append(self, cb: Callable[[], None]) -> None: + """()""" + self._hooks.append(cb) + + def remove(self, cb: Callable[[], None]) -> None: + if cb in self._hooks: + self._hooks.remove(cb) + + def __call__(self) -> None: + for hook in self._hooks: + try: + hook() + except: + # if the hook fails, remove it + self._hooks.remove(hook) + raise + + +av_player_did_play = _AvPlayerDidPlayHook() + + +class _AvPlayerWillPlayHook: + _hooks: List[Callable[["anki.sound.AVTag"], None]] = [] + + def append(self, cb: Callable[["anki.sound.AVTag"], None]) -> None: + """(tag: anki.sound.AVTag)""" + self._hooks.append(cb) + + def remove(self, cb: Callable[["anki.sound.AVTag"], None]) -> None: + if cb in self._hooks: + self._hooks.remove(cb) + + def __call__(self, tag: anki.sound.AVTag) -> None: + for hook in self._hooks: + try: + hook(tag) + except: + # if the hook fails, remove it + self._hooks.remove(hook) + raise + + +av_player_will_play = _AvPlayerWillPlayHook() + + class _BrowserDidChangeRowHook: _hooks: List[Callable[["aqt.browser.Browser"], None]] = [] @@ -496,56 +544,6 @@ class _EditorWillUseFontForFieldFilter: editor_will_use_font_for_field = _EditorWillUseFontForFieldFilter() -class _MpvDidIdleHook: - _hooks: List[Callable[[], None]] = [] - - def append(self, cb: Callable[[], None]) -> None: - """()""" - self._hooks.append(cb) - - def remove(self, cb: Callable[[], None]) -> None: - if cb in self._hooks: - self._hooks.remove(cb) - - def __call__(self) -> None: - for hook in self._hooks: - try: - hook() - except: - # if the hook fails, remove it - self._hooks.remove(hook) - raise - - -mpv_did_idle = _MpvDidIdleHook() - - -class _MpvWillPlayHook: - _hooks: List[Callable[[str], None]] = [] - - def append(self, cb: Callable[[str], None]) -> None: - """(file: str)""" - self._hooks.append(cb) - - def remove(self, cb: Callable[[str], None]) -> None: - if cb in self._hooks: - self._hooks.remove(cb) - - def __call__(self, file: str) -> None: - for hook in self._hooks: - try: - hook(file) - except: - # if the hook fails, remove it - self._hooks.remove(hook) - raise - # legacy support - runHook("mpvWillPlay", file) - - -mpv_will_play = _MpvWillPlayHook() - - class _ProfileDidOpenHook: _hooks: List[Callable[[], None]] = [] diff --git a/qt/aqt/main.py b/qt/aqt/main.py index 56a62fceb..50932fb53 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -28,6 +28,7 @@ from anki import hooks from anki.collection import _Collection from anki.hooks import runHook from anki.lang import _, ngettext +from anki.sound import AVTag, SoundOrVideoTag from anki.storage import Collection from anki.utils import devMode, ids2str, intTime, isMac, isWin, splitFields from aqt import gui_hooks @@ -1169,8 +1170,8 @@ Difference to correct time: %s.""" hooks.notes_will_be_deleted.append(self.onRemNotes) hooks.card_odue_was_invalid.append(self.onOdueInvalid) - gui_hooks.mpv_will_play.append(self.on_mpv_will_play) - gui_hooks.mpv_did_idle.append(self.on_mpv_idle) + gui_hooks.av_player_will_play.append(self.on_av_player_will_play) + gui_hooks.av_player_did_play.append(self.on_av_player_did_play) self._activeWindowOnPlay: Optional[QWidget] = None @@ -1183,17 +1184,22 @@ and if the problem comes up again, please ask on the support site.""" ) ) - def _isVideo(self, file): - head, ext = os.path.splitext(file.lower()) - return ext in (".mp4", ".mov", ".mpg", ".mpeg", ".mkv", ".avi") + def _isVideo(self, tag: AVTag) -> bool: + if isinstance(tag, SoundOrVideoTag): + head, ext = os.path.splitext(tag.filename.lower()) + return ext in (".mp4", ".mov", ".mpg", ".mpeg", ".mkv", ".avi") - def on_mpv_will_play(self, file: str) -> None: - if not self._isVideo(file): + return False + + def on_av_player_will_play(self, tag: AVTag) -> None: + "Record active window to restore after video playing." + if not self._isVideo(tag): return self._activeWindowOnPlay = self.app.activeWindow() or self._activeWindowOnPlay - def on_mpv_idle(self) -> None: + def on_av_player_did_play(self) -> None: + "Restore window focus after a video was played." w = self._activeWindowOnPlay if not self.app.activeWindow() and w and not sip.isdeleted(w) and w.isVisible(): w.activateWindow() diff --git a/qt/aqt/profiles.py b/qt/aqt/profiles.py index e02fed7fd..092e1cabe 100644 --- a/qt/aqt/profiles.py +++ b/qt/aqt/profiles.py @@ -73,8 +73,6 @@ class ProfileManager: # instantiate base folder self._setBaseFolder(base) - aqt.sound.setMpvConfigBase(self.base) - def setupMeta(self) -> LoadMetaResult: # load metadata res = self._loadMeta() diff --git a/qt/aqt/sound.py b/qt/aqt/sound.py index d53832b72..822967102 100644 --- a/qt/aqt/sound.py +++ b/qt/aqt/sound.py @@ -15,6 +15,7 @@ from typing import Any, Callable, Dict, List, Optional, Tuple, cast import pyaudio import anki +import aqt from anki.lang import _ from anki.sound import AVTag, SoundOrVideoTag from anki.utils import isLin, isMac, isWin, tmpdir @@ -99,6 +100,7 @@ class AVPlayer: def _on_play_finished(self) -> None: self._current_player = None + gui_hooks.av_player_did_play() self._play_next_if_idle() def _play_next_if_idle(self) -> None: @@ -113,6 +115,7 @@ class AVPlayer: for player in self.players: if player.can_play(tag): self._current_player = player + gui_hooks.av_player_will_play(tag) player.play(tag, self._on_play_finished) return print("no players found for", tag) @@ -193,6 +196,10 @@ class SimpleMpvPlayer(SimpleProcessPlayer): ] ) + def __init__(self, base_folder: str) -> None: + conf_path = os.path.join(base_folder, "mpv.conf") + self.args += ["--no-config", "--include=" + conf_path] + class SimpleMplayerPlayer(SimpleProcessPlayer): args, env = _packagedCmd(["mplayer", "-really-quiet", "-noautosub"]) @@ -232,35 +239,27 @@ def retryWait(proc) -> Any: ########################################################################## -_player: Optional[Callable[[Any], Any]] -_queueEraser: Optional[Callable[[], Any]] -mpvManager: Optional["MpvManager"] = None - -mpvPath, mpvEnv = _packagedCmd(["mpv"]) - - -class MpvManager(MPV): - - executable = mpvPath[0] - popenEnv = mpvEnv +class MpvManager(MPV, SoundOrVideoPlayer): if not isLin: default_argv = MPVBase.default_argv + [ "--input-media-keys=no", ] - def __init__(self) -> None: + def __init__(self, base_path: str) -> None: super().__init__(window_id=None, debug=False) + mpvPath, self.popenEnv = _packagedCmd(["mpv"]) + self.executable = mpvPath[0] + self._on_done: Optional[OnDoneCallback] = None + conf_path = os.path.join(base_path, "mpv.conf") + self.default_argv += ["--no-config", "--include=" + conf_path] - def queueFile(self, file: str) -> None: - gui_hooks.mpv_will_play(file) - - path = os.path.join(os.getcwd(), file) + def play(self, tag: AVTag, on_done: OnDoneCallback) -> None: + stag = cast(SoundOrVideoTag, tag) + self._on_done = on_done + path = os.path.join(os.getcwd(), stag.filename) self.command("loadfile", path, "append-play") - def clearQueue(self) -> None: - self.command("stop") - def togglePause(self) -> None: self.set_property("pause", not self.get_property("pause")) @@ -268,32 +267,25 @@ class MpvManager(MPV): self.command("seek", secs, "relative") def on_idle(self) -> None: - gui_hooks.mpv_did_idle() + if self._on_done: + self._on_done() + # Legacy, not used + ################################################## -def setMpvConfigBase(base) -> None: - mpvConfPath = os.path.join(base, "mpv.conf") - MpvManager.default_argv += [ - "--no-config", - "--include=" + mpvConfPath, - ] + def queueFile(self, file: str) -> None: + path = os.path.join(os.getcwd(), file) + self.command("loadfile", path, "append-play") - -def setupMPV() -> None: - global mpvManager, _player, _queueEraser - mpvManager = MpvManager() - _player = mpvManager.queueFile - _queueEraser = mpvManager.clearQueue - atexit.register(cleanupMPV) + def clearQueue(self) -> None: + self.command("stop") def cleanupMPV() -> None: - global mpvManager, _player, _queueEraser + global mpvManager if mpvManager: mpvManager.close() mpvManager = None - _player = None - _queueEraser = None # Mplayer in slave mode @@ -624,35 +616,6 @@ def getAudio(parent, encode=True): return r.file() -# Init defaults -########################################################################## - - -def setup_audio(base_folder: str) -> None: - # if isWin: - # return - # try: - # setupMPV() - # except FileNotFoundError: - # print("mpv not found, reverting to mplayer") - # except aqt.mpv.MPVProcessError: - # print("mpv too old, reverting to mplayer") - # - - if isWin: - mplayer = SimpleMplayerPlayer() - av_player.players.append(mplayer) - else: - mpv = SimpleMpvPlayer() - mpv.args.append("--include=" + base_folder) - av_player.players.append(mpv) - - if isMac: - from aqt.tts import MacTTSPlayer - - av_player.players.append(MacTTSPlayer()) - - # Legacy audio interface ########################################################################## # these will be removed in the future @@ -672,10 +635,46 @@ def playFromText(text) -> None: av_player.extend_from_text(mw.col, text) +# legacy globals _player = play _queueEraser = clearAudioQueue +mpvManager: Optional["MpvManager"] = None # add everything from this module into anki.sound for backwards compat _exports = [i for i in locals().items() if not i[0].startswith("__")] for (k, v) in _exports: sys.modules["anki.sound"].__dict__[k] = v + +# Init defaults +########################################################################## + + +def setup_audio(base_folder: str) -> None: + # legacy global var + global mpvManager + + if not isWin: + try: + mpvManager = MpvManager(base_folder) + except FileNotFoundError: + print("mpv not found, reverting to mplayer") + except aqt.mpv.MPVProcessError: + print("mpv too old, reverting to mplayer") + + if mpvManager is not None: + av_player.players.append(mpvManager) + atexit.register(cleanupMPV) + else: + # fall back on mplayer + mplayer = SimpleMplayerPlayer() + av_player.players.append(mplayer) + + # currently unused + # mpv = SimpleMpvPlayer(base_folder) + # av_player.players.append(mpv) + + # tts support + if isMac: + from aqt.tts import MacTTSPlayer + + av_player.players.append(MacTTSPlayer()) diff --git a/qt/tools/genhooks_gui.py b/qt/tools/genhooks_gui.py index 818c8f427..a2a8196f3 100644 --- a/qt/tools/genhooks_gui.py +++ b/qt/tools/genhooks_gui.py @@ -177,10 +177,12 @@ hooks = [ return_type="str", legacy_hook="mungeEditingFontName", ), + # Sound/video + ################### + Hook(name="av_player_will_play", args=["tag: anki.sound.AVTag"]), + Hook(name="av_player_did_play"), # Other ################### - Hook(name="mpv_did_idle"), - Hook(name="mpv_will_play", args=["file: str"], legacy_hook="mpvWillPlay"), Hook( name="current_note_type_did_change", args=["notetype: Dict[str, Any]"],