Files
whisper-local/docs/superpowers/specs/2026-04-15-media-pause-windows-smtc-design.md
T
2026-04-15 19:34:41 +02:00

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
  • 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

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:

"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:

  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=TrueSmtcController
  • 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.