Files
whisper-local/docs/superpowers/specs/2026-04-15-media-pause-windows-smtc-design.md
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

6.1 KiB
Raw Permalink Blame History

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

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

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

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