mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 14:32:22 -04:00
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
5 changed files with 130 additions and 127 deletions
|
@ -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]] = []
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
return ext in (".mp4", ".mov", ".mpg", ".mpeg", ".mkv", ".avi")
|
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:
|
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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
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 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())
|
||||||
|
|
|
@ -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]"],
|
||||||
|
|
Loading…
Reference in a new issue