2424import android .content .Intent ;
2525import android .content .SharedPreferences ;
2626import android .graphics .Bitmap ;
27+ import android .graphics .BitmapFactory ;
2728import android .net .Uri ;
2829import android .os .Handler ;
2930import android .os .IBinder ;
3233import android .support .v4 .media .session .MediaSessionCompat ;
3334import android .widget .Toast ;
3435
36+ import androidx .annotation .Nullable ;
3537import androidx .media3 .common .MediaItem ;
3638import androidx .media3 .common .Player ;
39+ import androidx .media3 .common .util .BitmapLoader ;
3740import androidx .media3 .common .util .UnstableApi ;
3841import androidx .media3 .session .MediaSession ;
3942import androidx .media3 .session .SessionCommand ;
4043import androidx .media3 .session .SessionCommands ;
4144import androidx .media3 .ui .PlayerNotificationManager ;
4245
46+ import com .google .common .util .concurrent .Futures ;
47+ import com .google .common .util .concurrent .ListenableFuture ;
48+ import com .google .common .util .concurrent .SettableFuture ;
49+
4350import org .fourthline .cling .model .action .ActionInvocation ;
4451import org .fourthline .cling .model .message .UpnpResponse ;
4552import org .fourthline .cling .model .meta .Device ;
8996import de .yaacc .upnp .server .http .YaaccUpnpServerContentHttpHandler ;
9097import de .yaacc .util .InterfaceResolutionHelper ;
9198import de .yaacc .util .YaaccLogger ;
99+ import de .yaacc .util .image .IconDownloadCacheHandler ;
92100import de .yaacc .util .image .ImageDownloader ;
93101
94102/**
@@ -113,7 +121,7 @@ public class AVTransportPlayer extends AbstractPlayer {
113121 private int consecutivePositionFailures = 0 ;
114122
115123 // Retry tracking for critical commands
116- private static final int MAX_RETRIES = 3 ;
124+ private static final int MAX_RETRIES = 30 ;
117125 private final Map <String , Integer > commandRetries = new HashMap <>();
118126
119127
@@ -150,10 +158,62 @@ public AVTransportPlayer(UpnpClient upnpClient) {
150158
151159 // Initialize Media3 Player wrapper
152160 playerWrapper = new AVTransportPlayerWrapper (this , null );
161+ BitmapLoader bitmapLoader = new BitmapLoader () {
162+ @ Override
163+ public ListenableFuture <Bitmap > decodeBitmap (byte [] data ) {
164+ SettableFuture <Bitmap > future = SettableFuture .create ();
165+ try {
166+ Bitmap bitmap = android .graphics .BitmapFactory .decodeByteArray (data , 0 , data .length );
167+ future .set (bitmap );
168+ } catch (Exception e ) {
169+ future .setException (e );
170+ }
171+ return future ;
172+ }
173+
174+ @ Override
175+ public ListenableFuture <Bitmap > loadBitmap (Uri uri ) {
176+ return loadBitmap (uri , null );
177+ }
178+
179+ @ Override
180+ public ListenableFuture <Bitmap > loadBitmap (Uri uri , @ Nullable BitmapFactory .Options options ) {
181+ YaaccLogger .e (getClass ().getName (), "BitmapLoader.loadBitmap called with uri: " + uri );
182+
183+ // Check cache first
184+ IconDownloadCacheHandler cache = IconDownloadCacheHandler .getInstance ();
185+ Bitmap cachedBitmap = cache .getBitmap (uri , 512 , 512 );
186+ if (cachedBitmap != null ) {
187+ YaaccLogger .e (getClass ().getName (), "Returning cached bitmap: " + cachedBitmap .getWidth () + "x" + cachedBitmap .getHeight ());
188+ return Futures .immediateFuture (cachedBitmap );
189+ }
190+
191+ SettableFuture <Bitmap > future = SettableFuture .create ();
192+ // Load bitmap in background using ImageDownloader
193+ ((Yaacc ) getContext ().getApplicationContext ()).getContentLoadExecutor ().execute (() -> {
194+ try {
195+ YaaccLogger .e (getClass ().getName (), "Loading bitmap from: " + uri );
196+ Bitmap bitmap = new ImageDownloader ().retrieveImageWithCertainSize (uri , 512 , 512 );
197+ if (bitmap != null ) {
198+ cache .addBitmap (uri , 512 , 512 , bitmap );
199+ }
200+ YaaccLogger .e (getClass ().getName (), "Bitmap loaded: " + (bitmap != null ? bitmap .getWidth () + "x" + bitmap .getHeight () : "null" ));
201+ future .set (bitmap );
202+ YaaccLogger .e (getClass ().getName (), "Future.set() called" );
203+ } catch (Exception e ) {
204+ YaaccLogger .e (getClass ().getName (), "Failed to load bitmap" , e );
205+ future .setException (e );
206+ }
207+ });
208+ return future ;
209+ }
210+
153211
212+ };
154213 // Create Media3 MediaSession for the wrapper
155214 media3Session = new MediaSession .Builder (getContext (), playerWrapper )
156215 .setId ("avtransport_" + id )
216+ // Don't set BitmapLoader - let notification manager handle it via getCurrentLargeIcon()
157217 .setCallback (new MediaSession .Callback () {
158218 @ Override
159219 public MediaSession .ConnectionResult onConnect (MediaSession session ,
@@ -210,6 +270,40 @@ public CharSequence getCurrentContentText(Player player) {
210270 @ Override
211271 public Bitmap getCurrentLargeIcon (Player player ,
212272 PlayerNotificationManager .BitmapCallback callback ) {
273+ // Get album art URI from AVTransportPlayer (includes cover.jpg fallback)
274+ URI albumArtJavaUri = getAlbumArt ();
275+ YaaccLogger .e (getClass ().getName (), "getCurrentLargeIcon called, albumArtUri: " + albumArtJavaUri );
276+
277+ if (albumArtJavaUri != null ) {
278+ android .net .Uri artworkUri = android .net .Uri .parse (albumArtJavaUri .toString ());
279+
280+ // Check cache first - return immediately if available
281+ IconDownloadCacheHandler cache = IconDownloadCacheHandler .getInstance ();
282+ Bitmap cachedBitmap = cache .getBitmap (artworkUri , 512 , 512 );
283+ if (cachedBitmap != null ) {
284+ YaaccLogger .e (getClass ().getName (), "Returning cached bitmap synchronously: " + cachedBitmap .getWidth () + "x" + cachedBitmap .getHeight ());
285+ return cachedBitmap ;
286+ }
287+
288+ // Load bitmap in background thread and use callback
289+ ((Yaacc ) getContext ().getApplicationContext ()).getContentLoadExecutor ().execute (() -> {
290+ try {
291+ YaaccLogger .e (getClass ().getName (), "Loading bitmap from: " + artworkUri );
292+ Bitmap bitmap = new ImageDownloader ().retrieveImageWithCertainSize (artworkUri , 512 , 512 );
293+ if (bitmap != null ) {
294+ cache .addBitmap (artworkUri , 512 , 512 , bitmap );
295+ YaaccLogger .e (getClass ().getName (), "Bitmap loaded, calling callback: " + bitmap .getWidth () + "x" + bitmap .getHeight ());
296+ callback .onBitmap (bitmap );
297+ } else {
298+ YaaccLogger .e (getClass ().getName (), "Bitmap is null" );
299+ }
300+ } catch (Exception e ) {
301+ YaaccLogger .e (getClass ().getName (), "Failed to load album art" , e );
302+ }
303+ });
304+ } else {
305+ YaaccLogger .e (getClass ().getName (), "albumArtUri is null" );
306+ }
213307 return null ;
214308 }
215309 })
@@ -631,6 +725,11 @@ private void proceedWithSetURI(PlayableItem playableItem, Service<?, ?> service)
631725 }
632726 DIDLObject .Property <URI > albumArtUriProperty = playableItem .getItem () == null ? null : playableItem .getItem ().getFirstProperty (DIDLObject .Property .UPNP .ALBUM_ART_URI .class );
633727 albumArtUri = (albumArtUriProperty == null ) ? null : albumArtUriProperty .getValue ();
728+
729+ // Trigger notification update with new album art
730+ if (albumArtUri != null ) {
731+ updateMetadataInternal ();
732+ }
634733
635734 InternalSetAVTransportURI setAVTransportURI = new InternalSetAVTransportURI (
636735 service , modifyProxyUrlWithDeviceId (playableItem .getUri ().toString ()), actionState , metadata ,
@@ -910,6 +1009,35 @@ public void success(ActionInvocation actioninvocation) {
9101009 executorService .execute (actionCallback );
9111010 }
9121011
1012+ @ Override
1013+ public Bitmap getIcon () {
1014+ // Try to get album art from cache only (don't block on download)
1015+ if (albumArtUri != null ) {
1016+ IconDownloadCacheHandler cache = IconDownloadCacheHandler .getInstance ();
1017+ Bitmap albumArt = cache .getBitmap (android .net .Uri .parse (albumArtUri .toString ()), 512 , 512 );
1018+ if (albumArt != null ) {
1019+ return albumArt ;
1020+ }
1021+
1022+ // Trigger async download for next notification update
1023+ android .net .Uri artworkUri = android .net .Uri .parse (albumArtUri .toString ());
1024+ ((Yaacc ) getContext ().getApplicationContext ()).getContentLoadExecutor ().execute (() -> {
1025+ try {
1026+ Bitmap bitmap = new ImageDownloader ().retrieveImageWithCertainSize (artworkUri , 512 , 512 );
1027+ if (bitmap != null ) {
1028+ cache .addBitmap (artworkUri , 512 , 512 , bitmap );
1029+ // Trigger notification update by updating metadata
1030+ updateMetadataInternal ();
1031+ }
1032+ } catch (Exception e ) {
1033+ YaaccLogger .w (getClass ().getName (), "Failed to load album art" , e );
1034+ }
1035+ });
1036+ }
1037+ // Fall back to device icon
1038+ return super .getIcon ();
1039+ }
1040+
9131041 @ Override
9141042 protected void doResume () {
9151043 // For UPnP, just send Play command to resume from current position
@@ -1018,8 +1146,10 @@ public void received(ActionInvocation actioninvocation, TransportInfo info) {
10181146 return ;
10191147 }
10201148
1021- // If not playing and we haven't exceeded retry limit, try Play command again
1022- if (info .getCurrentTransportState () != TransportState .PLAYING && playRetryCount < MAX_PLAY_RETRIES ) {
1149+ // Only retry Play if we think we should be playing (not paused by user)
1150+ if (info .getCurrentTransportState () != TransportState .PLAYING &&
1151+ isPlaying () &&
1152+ playRetryCount < MAX_RETRIES ) {
10231153 playRetryCount ++;
10241154 YaaccLogger .d (getClass ().getName (), "Renderer not playing, sending Play command again (attempt " + playRetryCount + ")" );
10251155 executeCommand (new TimerTask () {
@@ -1088,7 +1218,7 @@ protected void getPositionInfo() {
10881218
10891219 // Track device-not-found as position failure
10901220 consecutivePositionFailures ++;
1091- if (consecutivePositionFailures >= 3 && isPlaying ()) {
1221+ if (consecutivePositionFailures >= MAX_RETRIES && isPlaying ()) {
10921222 YaaccLogger .w (getClass ().getName (), "Device lost, stopping playback" );
10931223 consecutivePositionFailures = 0 ;
10941224 stop ();
@@ -1122,7 +1252,7 @@ public void failure(ActionInvocation actioninvocation,
11221252 YaaccLogger .w (getClass ().getName (), "Position query failed " + consecutivePositionFailures + " times" );
11231253
11241254 // After 3 consecutive failures, check device state to see if track ended
1125- if (consecutivePositionFailures >= 3 && isPlaying ()) {
1255+ if (consecutivePositionFailures >= MAX_RETRIES && isPlaying ()) {
11261256 YaaccLogger .w (getClass ().getName (), "Position query failed 3 times, checking transport state" );
11271257 consecutivePositionFailures = 0 ;
11281258 getTransportInfo ();
0 commit comments