feat(flutter/player): set MediaItem.artUri from local cover cache
Audio handler accepts an AlbumCoverCache via configure() and uses it to populate MediaItem.artUri with a file:// URI to a locally cached album cover. Lock screen, Bluetooth car displays, Wear OS, and CarPlay now show the album cover for the currently playing track instead of the system's generic music icon. Flow: - _toMediaItem stashes album_id in MediaItem.extras - After setQueueFromTracks pushes the queue + initial mediaItem, fires _loadArtForCurrentItem async (doesn't block playback) - Subscribes to _player.currentIndexStream so track advances trigger the same loader for the new current item - _loadArtForCurrentItem early-returns if cache is null, no current item, no album_id, or artUri already set; otherwise calls cache.getOrFetch and pushes an updated MediaItem with artUri set - Race guard: if the user skipped to another track while the fetch was in flight, the result is discarded Closes the visible "generic music icon on lock screen" gap operator flagged when first-testing on a real device. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,23 +1,34 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
|
||||
import '../models/track.dart';
|
||||
import 'album_cover_cache.dart';
|
||||
|
||||
class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler {
|
||||
MinstrelAudioHandler() {
|
||||
_player.playbackEventStream.listen(_broadcastState);
|
||||
_player.currentIndexStream.listen(_onCurrentIndexChanged);
|
||||
}
|
||||
|
||||
final AudioPlayer _player = AudioPlayer();
|
||||
String _baseUrl = '';
|
||||
String? _token;
|
||||
AlbumCoverCache? _coverCache;
|
||||
|
||||
void configure({required String baseUrl, required String? token}) {
|
||||
void configure({
|
||||
required String baseUrl,
|
||||
required String? token,
|
||||
AlbumCoverCache? coverCache,
|
||||
}) {
|
||||
_baseUrl = baseUrl;
|
||||
_token = token;
|
||||
if (coverCache != null) _coverCache = coverCache;
|
||||
debugPrint('audio_handler.configure: baseUrl="$baseUrl" '
|
||||
'tokenPresent=${token != null && token.isNotEmpty}');
|
||||
'tokenPresent=${token != null && token.isNotEmpty} '
|
||||
'cachePresent=${_coverCache != null}');
|
||||
}
|
||||
|
||||
Future<void> setQueueFromTracks(List<TrackRef> tracks, {int initialIndex = 0}) async {
|
||||
@@ -54,6 +65,11 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl
|
||||
sources,
|
||||
initialIndex: initialIndex,
|
||||
);
|
||||
|
||||
// Kick the cover fetch for the initial item — async, doesn't block
|
||||
// playback. Subsequent track changes are handled by the
|
||||
// currentIndexStream listener.
|
||||
unawaited(_loadArtForCurrentItem());
|
||||
}
|
||||
|
||||
String _resolveStreamUrl(TrackRef t) {
|
||||
@@ -73,8 +89,35 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl
|
||||
artist: t.artistName,
|
||||
album: t.albumTitle,
|
||||
duration: Duration(seconds: t.durationSec),
|
||||
// Stash album_id in extras so _loadArtForCurrentItem can pull it
|
||||
// back without re-walking the track list.
|
||||
extras: t.albumId.isEmpty ? null : {'album_id': t.albumId},
|
||||
);
|
||||
|
||||
void _onCurrentIndexChanged(int? idx) {
|
||||
if (idx == null) return;
|
||||
unawaited(_loadArtForCurrentItem());
|
||||
}
|
||||
|
||||
/// Async-fetches the cover for whichever item is currently active and
|
||||
/// pushes a MediaItem update with artUri set. No-op if no cache is
|
||||
/// configured, no current item, the item has no album_id in extras,
|
||||
/// or the fetch returns null.
|
||||
Future<void> _loadArtForCurrentItem() async {
|
||||
final cache = _coverCache;
|
||||
if (cache == null) return;
|
||||
final current = mediaItem.value;
|
||||
if (current == null) return;
|
||||
final albumId = current.extras?['album_id'] as String?;
|
||||
if (albumId == null || albumId.isEmpty) return;
|
||||
if (current.artUri != null) return; // already set
|
||||
final path = await cache.getOrFetch(albumId);
|
||||
if (path == null) return;
|
||||
// Discard if the user advanced to another track while we waited.
|
||||
if (mediaItem.value?.id != current.id) return;
|
||||
mediaItem.add(current.copyWith(artUri: Uri.file(path)));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> play() => _player.play();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user