From 0beb8aeb7c983b3ef9af1ff8ce9ada558a6df304 Mon Sep 17 00:00:00 2001 From: Vitali Graf Date: Wed, 15 Apr 2026 19:34:41 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20Spec=20f=C3=BCr=20Windows=20SMTC=20Medi?= =?UTF-8?q?a-Controller?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- ...6-04-15-media-pause-windows-smtc-design.md | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-15-media-pause-windows-smtc-design.md diff --git a/docs/superpowers/specs/2026-04-15-media-pause-windows-smtc-design.md b/docs/superpowers/specs/2026-04-15-media-pause-windows-smtc-design.md new file mode 100644 index 0000000..38ec624 --- /dev/null +++ b/docs/superpowers/specs/2026-04-15-media-pause-windows-smtc-design.md @@ -0,0 +1,141 @@ +# 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.