Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5.0 KiB
Media-Pause Windows: SMTC-Controller
Status: Spec Datum: 2026-04-15 Bezug: 2026-04-14-media-pause-during-recording-design.md
Ziel
Die für Linux via MPRIS implementierte Media-Pause-Funktion auf Windows nachziehen. Auf Windows steuert die Global System Media Transport Controls (GSMTC)-API systemweit alle Mediaplayer (Spotify, Browser-Videos, Groove Music, etc.).
Scope
In Scope:
- Windows-Implementierung via
winsdk.windows.media.control - Factory-Dispatch für
sys.platform == "win32"→SmtcController winsdkalswin32-only Dependency inpyproject.toml- Tests analog zur MPRIS-Testsuite
Out of Scope:
- Änderungen an der Linux-Implementierung
- macOS-Implementierung
- Per-App Allow-/Blocklist
Architektur
Neue Datei whisper_local/media/_smtc.py, analog zu _mpris.py.
Keine strukturellen Änderungen an _noop.py oder der restlichen Pipeline.
whisper_local/media/
__init__.py # +win32-Dispatch-Zweig
_mpris.py # unverändert
_noop.py # unverändert
_smtc.py # neu
Factory-Erweiterung
if sys.platform == "win32":
from whisper_local.media._smtc import SmtcController
return SmtcController()
Der bestehende Fallback-Zweig (NoopController) bleibt für alle anderen Plattformen (macOS, etc.) erhalten.
Kern-Logik (SMTC / Windows)
Session-Identifikation
Sessions werden über ihre source_app_user_model_id (AUMID) identifiziert —
der systemweit eindeutige App-Bezeichner (z. B. "Spotify.exe",
"msedge.exe"). Das ist das Windows-Äquivalent zum MPRIS-Bus-Namen.
pause()
- Manager lazy via
GlobalSystemMediaTransportControlsSessionManager.request_async()holen. manager.get_sessions()— synchron, gibt alle aktiven Sessions zurück.- Für jede Session:
get_playback_info().playback_statusprüfen. - Sessions mit Status
PLAYINGsequenziell pausieren viaawait session.try_pause_async(). - AUMID jeder erfolgreich pausierten Session in
self._paused: list[str]speichern. - Fehler pro Session loggen, andere Sessions nicht blockieren.
resume()
- Aktuelle Sessions vom Manager frisch laden:
dict[aumid, session]. - Für jede AUMID in
self._pauseddie Session suchen. - Gefundene Sessions:
await session.try_play_async(). - Nicht gefundene Sessions (App seit
pause()beendet): still überspringen. - Fehler pro Session loggen und ignorieren.
self._pausedleeren.
State-Isolation
Nur Sessions anfassen, die pause() selbst in den Paused-Zustand versetzt
hat. Eine Session, die vor Aufnahmebeginn bereits pausiert war, bleibt
unangetastet.
Circuit-Breaker
Schlägt request_async() fehl (z. B. kein WinRT-Support, Headless-Session),
wird _broken = True gesetzt. Alle weiteren Aufrufe sind No-Ops. Die Warnung
wird einmalig geloggt — exakt dasselbe Muster wie MprisController._bus_broken.
Dependencies
pyproject.toml:
"winsdk>=1.0.0b10; sys_platform == 'win32'",
winsdk liefert async-native WinRT-Bindings, die direkt mit asyncio
zusammenarbeiten. Keine zusätzliche Systemabhängigkeit nötig.
Tests
tests/test_media_smtc.py
@pytest.mark.skipif(sys.platform != "win32", reason="SMTC is Windows-only")
Alle SMTC-Calls werden mit unittest.mock.AsyncMock / MagicMock gemockt —
kein echter WinRT-Zugriff im Test.
Hilfsfunktion _make_session(status) — analog zu _make_player() in MPRIS-Tests:
def _make_session(aumid: str, status) -> MagicMock:
session = MagicMock()
session.source_app_user_model_id = aumid
info = MagicMock()
info.playback_status = status
session.get_playback_info = MagicMock(return_value=info)
session.try_pause_async = AsyncMock()
session.try_play_async = AsyncMock()
return session
Szenarien:
- Keine Sessions vorhanden →
pause()ist No-Op,_pausedbleibt leer, kein Fehler. - Zwei Sessions im Status
PLAYING→ beide erhaltentry_pause_async(), beide AUMIDs in_paused. - Eine Session
PLAYING, einePAUSED→ nurPLAYINGwird pausiert; nachresume()wird nur diese wieder gestartet. - Session verschwindet zwischen
pause()undresume()(nicht in neuer Session-Liste) → kein Crash, andere Sessions werden korrekt fortgesetzt. try_pause_async()einer Session wirft Exception → Fehler geloggt, andere Sessions werden weiter pausiert.- SMTC nicht erreichbar (
request_async()wirft) →_broken = True, Warnung geloggt, Folgeaufrufe sind No-Ops ohne erneuten Connect-Versuch.
tests/test_media_factory.py — Ergänzungen
sys.platform == "win32"+enabled=True→SmtcController- Bestehenden Test
test_factory_returns_noop_on_non_linuxzutest_factory_returns_noop_on_other_platformsumbennen und aufdarwinpatchen (war Windows-Patch, wird jetzt durch eigenen win32-Test ersetzt).
Offene Fragen / bewusst verschoben
- macOS-Implementierung: kein Äquivalent geplant.
winsdk-Versionsuntergrenze1.0.0b10— sollte beim Einbau gegen aktuelle PyPI-Version geprüft werden.