winsdk wird seit fast 3 Jahren nicht mehr gepflegt; pywinrt ist der aktive Nachfolger. Alle Paketnamen, Imports und Dependencies aktualisiert, API gegen echte pywinrt-Installation auf Windows verifiziert. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
6.1 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
pywinrt(winrt.windows.media.control) - Factory-Dispatch für
sys.platform == "win32"→SmtcController pywinrt-Pakete alswin32-only Dependencies 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.
"SpotifyAB.SpotifyMusic_zpdnekdrzrea0!Spotify", "msedge.exe").
Das ist das Windows-Äquivalent zum MPRIS-Bus-Namen.
pause()
- Manager lazy via
GlobalSystemMediaTransportControlsSessionManager.request_async()holen. (request_asyncist eine Metaclass-Methode und wird direkt auf der Klasse aufgerufen.) manager.get_sessions()— synchron, gibt alle aktiven Sessions zurück.- Für jede Session:
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:
{s.source_app_user_model_id: s for s in manager.get_sessions()}. - 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. Headless-Session ohne WinRT-Desktop-Kontext),
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 (alle win32-only):
"winrt-Windows.Media.Control>=3.2.1; sys_platform == 'win32'",
"winrt-Windows.Foundation>=3.2.1; sys_platform == 'win32'",
"winrt-Windows.Foundation.Collections>=3.2.1; sys_platform == 'win32'",
winrt-runtime wird automatisch als transitive Dependency von
winrt-Windows.Media.Control mitgezogen.
Warum pywinrt statt winsdk: winsdk (https://github.com/pywinrt/python-winsdk)
wird seit fast drei Jahren nicht mehr weiterentwickelt. pywinrt
(https://github.com/pywinrt/pywinrt) ist der aktive Nachfolger mit demselben
Maintainer, unterstützt Python 3.10–3.14 und liefert async-native WinRT-Bindings.
Die API ist nahezu identisch, der Import-Pfad ändert sich von
winsdk.windows.media.control zu winrt.windows.media.control.
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(aumid, status) — analog zu _make_player() in MPRIS-Tests:
def _make_session(aumid: str, status: int) -> 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
Der status-Wert für PLAYING ist 4 (Integer-Wert von
GlobalSystemMediaTransportControlsSessionPlaybackStatus.PLAYING).
Im Test wird der Enum-Import gemockt, um WinRT-Abhängigkeit zu vermeiden.
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_linuxumbenennen zutest_factory_returns_noop_on_other_platformsund aufdarwinpatchen (bisher war Windows der Testfall, wird jetzt durch eigenen win32-Test ersetzt).
Offene Fragen / bewusst verschoben
- macOS-Implementierung: kein Äquivalent geplant.
winrt-*-Versionsuntergrenzen auf3.2.1gesetzt (verifiziert am 2026-04-15); beim Einbau gegen aktuelle PyPI-Version prüfen.