Player-Seite: FolderPicker, ID3-Cover und Slider-Antifeedback (Teil 21)

Der vollständige Player in EchoPlay erlaubt es, lokale Audiodateien unabhängig von der importierten Bibliothek abzuspielen. Unterstützt werden acht Formate — von MP3 über FLAC bis Opus. Der Nutzer öffnet einen Ordner oder einzelne Dateien, sieht die Playlist und kann Tracks per Klick wechseln. Dabei zeigt die App das Coverbild aus dem ID3-Tag der aktuell spielenden Datei.

FolderPicker in WinUI 3

In WinUI 3 als MSIX-App gilt eine Einschränkung: Der Picker-Dialog braucht das Handle des übergeordneten Fensters. Ohne dieses Handle scheitert der Aufruf. Das Muster dafür heißt InitializeWithWindow:

FolderPicker picker = new()
{
    SuggestedStartLocation = PickerLocationId.MusicLibrary,
    ViewMode               = PickerViewMode.List
};

picker.FileTypeFilter.Add("*");

// WinUI 3: Der Picker braucht das Fenster-Handle für den Dialog
InitializeWithWindow.Initialize(picker, WindowNative.GetWindowHandle(App.MainWindow));

StorageFolder? folder = await picker.PickSingleFolderAsync();

WindowNative.GetWindowHandle() liest das native Win32-Handle aus dem WinUI-3-Fenster. InitializeWithWindow.Initialize() übergibt dieses Handle an den Picker, damit Windows den Dialog korrekt als modalen Dialog des Hauptfensters anzeigt. Damit der FolderPicker beim nächsten Öffnen gleich im richtigen Ordner startet, wird der zuletzt gewählte Pfad in den AppSettings gespeichert.

ID3-Coverbilder mit TagLib#

ID3-Tags sind Metadaten-Blöcke, die in MP3-Dateien eingebettet sind. Sie enthalten Titel, Interpret, Album — und optional ein Coverbild als Byte-Array. Das NuGet-Paket TagLib# liest diese Tags ohne externes Tool direkt vom Dateisystem:

byte[]? imageData = await Task.Run(() =>
{
    using TagLib.File tagFile = TagLib.File.Create(filePath);
    IPicture[] pictures = tagFile.Tag.Pictures;

    return pictures.Length > 0 ? pictures[0].Data.Data : null;
});

Das Lesen wird mit Task.Run() auf einen Hintergrundthread ausgelagert, damit der UI-Thread nicht blockiert. Um das Byte-Array in ein BitmapImage für WinUI 3 umzuwandeln, braucht man einen InMemoryRandomAccessStream:

InMemoryRandomAccessStream randomAccessStream = new();
DataWriter writer = new(randomAccessStream.GetOutputStreamAt(0));
writer.WriteBytes(imageData);
await writer.StoreAsync();

BitmapImage bitmap = new();
await bitmap.SetSourceAsync(randomAccessStream);
CoverImage = bitmap;

Beide Operationen — DataWriter und SetSourceAsync — müssen auf dem UI-Thread laufen, weil BitmapImage und InMemoryRandomAccessStream nicht thread-sicher sind.

Slider-Antifeedback beim Seeking

Ein klassisches Problem bei Audio-Slidern: Der Slider zeigt die aktuelle Position. Wenn der Nutzer am Slider zieht, aktualisiert der PlayerService gleichzeitig die Position — und der Slider springt zurück. Die Lösung ist ein _isSeeking-Flag:

public void BeginSeek()
{
    _isSeeking = true;
}

public void CommitSeek()
{
    _playerService.SeekTo(TimeSpan.FromSeconds(_positionSeconds));
    _isSeeking = false;
}

Im ViewModel wird die PositionSeconds-Property nur aktualisiert, wenn kein Seeking aktiv ist. Der Code-Behind ruft BeginSeek() im PointerPressed-Event auf und CommitSeek() im PointerReleased-Event. Zwischen diesen Ereignissen werden alle eingehenden Positionsupdates ignoriert.

PlaylistItemViewModel und Hervorhebung

Jeder Eintrag in der Playlist ist ein PlaylistItemViewModel. Es enthält Index, Dateiname und den IsCurrentTrack-Status. Wenn ein Track zu spielen beginnt, iteriert das PlayerViewModel über alle Einträge und setzt IsCurrentTrack. Daraus leiten sich zwei Visibility-Properties ab — eine für das Play-Icon, eine für die Zeilennummer. Immer genau eines der beiden Elemente ist sichtbar.

Standalone-Playback mit Guid.Empty

Der PlayerService persistiert nach jeder Wiedergabe den PlaybackState in der Datenbank — dazu braucht er eine episodeId. Der standalone Player-Modus kennt keine Episode, deshalb wird Guid.Empty übergeben. Der PlayerService prüft zu Beginn von SavePlaybackStateAsync(), ob die ID gleich Guid.Empty ist, und überspringt die Persistenz. Das vermeidet eine neue Methode — der bestehende Mechanismus wird um einen definierten Sonderfall erweitert.

Die gezeigten Code-Beispiele dienen zur Veranschaulichung. Nutzung auf eigene Verantwortung. Mehr dazu