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() 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: class _BrowserDidChangeRowHook:
_hooks: List[Callable[["aqt.browser.Browser"], None]] = [] _hooks: List[Callable[["aqt.browser.Browser"], None]] = []
@ -496,56 +544,6 @@ class _EditorWillUseFontForFieldFilter:
editor_will_use_font_for_field = _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: class _ProfileDidOpenHook:
_hooks: List[Callable[[], None]] = [] _hooks: List[Callable[[], None]] = []

View file

@ -28,6 +28,7 @@ from anki import hooks
from anki.collection import _Collection from anki.collection import _Collection
from anki.hooks import runHook from anki.hooks import runHook
from anki.lang import _, ngettext from anki.lang import _, ngettext
from anki.sound import AVTag, SoundOrVideoTag
from anki.storage import Collection from anki.storage import Collection
from anki.utils import devMode, ids2str, intTime, isMac, isWin, splitFields from anki.utils import devMode, ids2str, intTime, isMac, isWin, splitFields
from aqt import gui_hooks from aqt import gui_hooks
@ -1169,8 +1170,8 @@ Difference to correct time: %s."""
hooks.notes_will_be_deleted.append(self.onRemNotes) hooks.notes_will_be_deleted.append(self.onRemNotes)
hooks.card_odue_was_invalid.append(self.onOdueInvalid) hooks.card_odue_was_invalid.append(self.onOdueInvalid)
gui_hooks.mpv_will_play.append(self.on_mpv_will_play) gui_hooks.av_player_will_play.append(self.on_av_player_will_play)
gui_hooks.mpv_did_idle.append(self.on_mpv_idle) gui_hooks.av_player_did_play.append(self.on_av_player_did_play)
self._activeWindowOnPlay: Optional[QWidget] = None 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): def _isVideo(self, tag: AVTag) -> bool:
head, ext = os.path.splitext(file.lower()) if isinstance(tag, SoundOrVideoTag):
head, ext = os.path.splitext(tag.filename.lower())
return ext in (".mp4", ".mov", ".mpg", ".mpeg", ".mkv", ".avi") return ext in (".mp4", ".mov", ".mpg", ".mpeg", ".mkv", ".avi")
def on_mpv_will_play(self, file: str) -> None: return False
if not self._isVideo(file):
def on_av_player_will_play(self, tag: AVTag) -> None:
"Record active window to restore after video playing."
if not self._isVideo(tag):
return return
self._activeWindowOnPlay = self.app.activeWindow() or self._activeWindowOnPlay 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 w = self._activeWindowOnPlay
if not self.app.activeWindow() and w and not sip.isdeleted(w) and w.isVisible(): if not self.app.activeWindow() and w and not sip.isdeleted(w) and w.isVisible():
w.activateWindow() w.activateWindow()

View file

@ -73,8 +73,6 @@ class ProfileManager:
# instantiate base folder # instantiate base folder
self._setBaseFolder(base) self._setBaseFolder(base)
aqt.sound.setMpvConfigBase(self.base)
def setupMeta(self) -> LoadMetaResult: def setupMeta(self) -> LoadMetaResult:
# load metadata # load metadata
res = self._loadMeta() res = self._loadMeta()

View file

@ -15,6 +15,7 @@ from typing import Any, Callable, Dict, List, Optional, Tuple, cast
import pyaudio import pyaudio
import anki import anki
import aqt
from anki.lang import _ from anki.lang import _
from anki.sound import AVTag, SoundOrVideoTag from anki.sound import AVTag, SoundOrVideoTag
from anki.utils import isLin, isMac, isWin, tmpdir from anki.utils import isLin, isMac, isWin, tmpdir
@ -99,6 +100,7 @@ class AVPlayer:
def _on_play_finished(self) -> None: def _on_play_finished(self) -> None:
self._current_player = None self._current_player = None
gui_hooks.av_player_did_play()
self._play_next_if_idle() self._play_next_if_idle()
def _play_next_if_idle(self) -> None: def _play_next_if_idle(self) -> None:
@ -113,6 +115,7 @@ class AVPlayer:
for player in self.players: for player in self.players:
if player.can_play(tag): if player.can_play(tag):
self._current_player = player self._current_player = player
gui_hooks.av_player_will_play(tag)
player.play(tag, self._on_play_finished) player.play(tag, self._on_play_finished)
return return
print("no players found for", tag) 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): class SimpleMplayerPlayer(SimpleProcessPlayer):
args, env = _packagedCmd(["mplayer", "-really-quiet", "-noautosub"]) args, env = _packagedCmd(["mplayer", "-really-quiet", "-noautosub"])
@ -232,35 +239,27 @@ def retryWait(proc) -> Any:
########################################################################## ##########################################################################
_player: Optional[Callable[[Any], Any]] class MpvManager(MPV, SoundOrVideoPlayer):
_queueEraser: Optional[Callable[[], Any]]
mpvManager: Optional["MpvManager"] = None
mpvPath, mpvEnv = _packagedCmd(["mpv"])
class MpvManager(MPV):
executable = mpvPath[0]
popenEnv = mpvEnv
if not isLin: if not isLin:
default_argv = MPVBase.default_argv + [ default_argv = MPVBase.default_argv + [
"--input-media-keys=no", "--input-media-keys=no",
] ]
def __init__(self) -> None: def __init__(self, base_path: str) -> None:
super().__init__(window_id=None, debug=False) 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: def play(self, tag: AVTag, on_done: OnDoneCallback) -> None:
gui_hooks.mpv_will_play(file) stag = cast(SoundOrVideoTag, tag)
self._on_done = on_done
path = os.path.join(os.getcwd(), file) path = os.path.join(os.getcwd(), stag.filename)
self.command("loadfile", path, "append-play") self.command("loadfile", path, "append-play")
def clearQueue(self) -> None:
self.command("stop")
def togglePause(self) -> None: def togglePause(self) -> None:
self.set_property("pause", not self.get_property("pause")) self.set_property("pause", not self.get_property("pause"))
@ -268,32 +267,25 @@ class MpvManager(MPV):
self.command("seek", secs, "relative") self.command("seek", secs, "relative")
def on_idle(self) -> None: def on_idle(self) -> None:
gui_hooks.mpv_did_idle() if self._on_done:
self._on_done()
# Legacy, not used
##################################################
def setMpvConfigBase(base) -> None: def queueFile(self, file: str) -> None:
mpvConfPath = os.path.join(base, "mpv.conf") path = os.path.join(os.getcwd(), file)
MpvManager.default_argv += [ self.command("loadfile", path, "append-play")
"--no-config",
"--include=" + mpvConfPath,
]
def clearQueue(self) -> None:
def setupMPV() -> None: self.command("stop")
global mpvManager, _player, _queueEraser
mpvManager = MpvManager()
_player = mpvManager.queueFile
_queueEraser = mpvManager.clearQueue
atexit.register(cleanupMPV)
def cleanupMPV() -> None: def cleanupMPV() -> None:
global mpvManager, _player, _queueEraser global mpvManager
if mpvManager: if mpvManager:
mpvManager.close() mpvManager.close()
mpvManager = None mpvManager = None
_player = None
_queueEraser = None
# Mplayer in slave mode # Mplayer in slave mode
@ -624,35 +616,6 @@ def getAudio(parent, encode=True):
return r.file() 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 # Legacy audio interface
########################################################################## ##########################################################################
# these will be removed in the future # these will be removed in the future
@ -672,10 +635,46 @@ def playFromText(text) -> None:
av_player.extend_from_text(mw.col, text) av_player.extend_from_text(mw.col, text)
# legacy globals
_player = play _player = play
_queueEraser = clearAudioQueue _queueEraser = clearAudioQueue
mpvManager: Optional["MpvManager"] = None
# add everything from this module into anki.sound for backwards compat # add everything from this module into anki.sound for backwards compat
_exports = [i for i in locals().items() if not i[0].startswith("__")] _exports = [i for i in locals().items() if not i[0].startswith("__")]
for (k, v) in _exports: for (k, v) in _exports:
sys.modules["anki.sound"].__dict__[k] = v 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", return_type="str",
legacy_hook="mungeEditingFontName", legacy_hook="mungeEditingFontName",
), ),
# Sound/video
###################
Hook(name="av_player_will_play", args=["tag: anki.sound.AVTag"]),
Hook(name="av_player_did_play"),
# Other # Other
################### ###################
Hook(name="mpv_did_idle"),
Hook(name="mpv_will_play", args=["file: str"], legacy_hook="mpvWillPlay"),
Hook( Hook(
name="current_note_type_did_change", name="current_note_type_did_change",
args=["notetype: Dict[str, Any]"], args=["notetype: Dict[str, Any]"],