Files
whisper-local/docs/superpowers/specs/2026-04-15-media-pause-windows-smtc-design.md
T
info a7b5bd2241 docs: Spec auf pywinrt umgestellt (winsdk veraltet)
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>
2026-04-15 20:02:25 +02:00

159 lines
6.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 `pywinrt` (`winrt.windows.media.control`)
- Factory-Dispatch für `sys.platform == "win32"``SmtcController`
- `pywinrt`-Pakete als `win32`-only Dependencies 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.
`"SpotifyAB.SpotifyMusic_zpdnekdrzrea0!Spotify"`, `"msedge.exe"`).
Das ist das Windows-Äquivalent zum MPRIS-Bus-Namen.
### `pause()`
1. Manager lazy via `GlobalSystemMediaTransportControlsSessionManager.request_async()` holen.
(`request_async` ist eine Metaclass-Methode und wird direkt auf der Klasse aufgerufen.)
2. `manager.get_sessions()` — synchron, gibt alle aktiven Sessions zurück.
3. Für jede Session: `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: `{s.source_app_user_model_id: s for s in manager.get_sessions()}`.
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. 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):
```toml
"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.103.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:
```python
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:**
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` umbenennen zu
`test_factory_returns_noop_on_other_platforms` und auf `darwin` patchen
(bisher war Windows der Testfall, wird jetzt durch eigenen win32-Test ersetzt).
## Offene Fragen / bewusst verschoben
- macOS-Implementierung: kein Äquivalent geplant.
- `winrt-*`-Versionsuntergrenzen auf `3.2.1` gesetzt (verifiziert am 2026-04-15);
beim Einbau gegen aktuelle PyPI-Version prüfen.