get mpv slave mode working with new API

Also move the mpv-specific hooks into AVPlayer
This commit is contained in:
Damien Elmes 2020-01-20 22:01:38 +10:00
parent d9c240afa2
commit a6e6ffae06
5 changed files with 130 additions and 127 deletions

View File

@ -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]] = []

View File

@ -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())
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()

View File

@ -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()

View File

@ -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())

View File

@ -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]"],