# Media-Pause Windows: SMTC-Controller **Status:** Spec **Datum:** 2026-04-15 **Bezug:** [2026-04-14-media-pause-during-recording-design.md](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` - `winsdk` als `win32`-only Dependency in `pyproject.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 ```python 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()` 1. Manager lazy via `GlobalSystemMediaTransportControlsSessionManager.request_async()` holen. 2. `manager.get_sessions()` — synchron, gibt alle aktiven Sessions zurück. 3. Für jede Session: `get_playback_info().playback_status` prüfen. 4. Sessions mit Status `PLAYING` sequenziell pausieren via `await session.try_pause_async()`. 5. AUMID jeder erfolgreich pausierten Session in `self._paused: list[str]` speichern. 6. Fehler pro Session loggen, andere Sessions nicht blockieren. ### `resume()` 1. Aktuelle Sessions vom Manager frisch laden: `dict[aumid, session]`. 2. Für jede AUMID in `self._paused` die Session suchen. 3. Gefundene Sessions: `await session.try_play_async()`. 4. Nicht gefundene Sessions (App seit `pause()` beendet): still überspringen. 5. Fehler pro Session loggen und ignorieren. 6. `self._paused` leeren. ### 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`: ```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: ```python 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:** 1. Keine Sessions vorhanden → `pause()` ist No-Op, `_paused` bleibt leer, kein Fehler. 2. Zwei Sessions im Status `PLAYING` → beide erhalten `try_pause_async()`, beide AUMIDs in `_paused`. 3. Eine Session `PLAYING`, eine `PAUSED` → nur `PLAYING` wird pausiert; nach `resume()` wird nur diese wieder gestartet. 4. Session verschwindet zwischen `pause()` und `resume()` (nicht in neuer Session-Liste) → kein Crash, andere Sessions werden korrekt fortgesetzt. 5. `try_pause_async()` einer Session wirft Exception → Fehler geloggt, andere Sessions werden weiter pausiert. 6. 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_linux` zu `test_factory_returns_noop_on_other_platforms` umbennen und auf `darwin` patchen (war Windows-Patch, wird jetzt durch eigenen win32-Test ersetzt). ## Offene Fragen / bewusst verschoben - macOS-Implementierung: kein Äquivalent geplant. - `winsdk`-Versionsuntergrenze `1.0.0b10` — sollte beim Einbau gegen aktuelle PyPI-Version geprüft werden.