get mpv slave mode working with new API
Also move the mpv-specific hooks into AVPlayer
This commit is contained in:
parent
d9c240afa2
commit
a6e6ffae06
@ -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]] = []
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
129
qt/aqt/sound.py
129
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())
|
||||
|
@ -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]"],
|
||||
|
Loading…
Reference in New Issue
Block a user