Recover from playback stalls; cap quality over Bluetooth

On the Galaxy Mini, Wi-Fi and Bluetooth audio share one radio: streams starve
and the media player then sits at the same position forever while showing
playing. After 10 s without progress, recover like a network failure (reconnect
and resume, then download first, then skip). New setting: quality over
Bluetooth (default 192 kbps) to leave the radio airtime for the speaker.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
XoséandClaude Opus 5.5 committed 2026-10-18 14:20:22 +02:00
1 parent a610b090f0
commit 9440536fa1
5 files changed
+38 -18

No files matched your search

+3 -1
View File
@@ -199,7 +199,7 @@
<string name="sort_played">Most played</string>
<string name="sort_title">Title</string>
<string name="sort_year">Year (newest)</string>
<string name="stalled">Playback is stuck. Try pausing and playing again.</string>
<string name="stalled">Playback got stuck: the network isn\'t keeping up.</string>
<string name="starting_x">Starting %1$s…</string>
<string name="stream_interrupted">Stream interrupted</string>
<string name="surprise_me">Surprise me</string>
@@ -236,4 +236,6 @@
<string name="unloved">Removed from favourites</string>
<string name="up_next">Queue</string>
<string name="you">you</string>
<string name="quality_bt_title">Quality over Bluetooth</string>
<string name="set_quality_bt">Quality over Bluetooth: %1$s</string>
</resources>
+3 -1
View File
@@ -199,7 +199,7 @@
<string name="sort_played">Máis escoitados</string>
<string name="sort_title">Título</string>
<string name="sort_year">Ano (máis recentes)</string>
<string name="stalled">A reprodución quedou parada. Proba a pausar e reproducir de novo.</string>
<string name="stalled">A reprodución quedou parada: a rede non dá abasto.</string>
<string name="starting_x">Iniciando %1$s…</string>
<string name="stream_interrupted">Interrompeuse a transmisión</string>
<string name="surprise_me">Sorpréndeme</string>
@@ -236,4 +236,6 @@
<string name="unloved">Quitada de favoritas</string>
<string name="up_next">Cola</string>
<string name="you">ti</string>
<string name="quality_bt_title">Calidade por Bluetooth</string>
<string name="set_quality_bt">Calidade por Bluetooth: %1$s</string>
</resources>
+8
View File
@@ -296,12 +296,20 @@ public class App extends Application {
public int qualityHome() { return prefs.getInt("qHome", 0); }
public int qualityAway() { return prefs.getInt("qAway", 192); }
/** Cap while the sound goes to a Bluetooth speaker (0 = none). Wi-Fi and Bluetooth share this phone's radio:
* less to download leaves the Bluetooth link the airtime it needs, so it doesn't stutter. */
public int qualityBluetooth() { return prefs.getInt("qBt", 192); }
/** Max kbps for the current situation; 0 = original. */
public int currentMaxKbps() {
boolean away = server == null || !server.isLocal() || onMobileData();
int q = away ? qualityAway() : qualityHome();
if (server != null && server.isRelay() && (q == 0 || q > 192)) q = 192; // relay is bandwidth-capped
int bt = qualityBluetooth();
if (bt > 0 && (q == 0 || q > bt)) {
android.media.AudioManager am = (android.media.AudioManager) getSystemService(AUDIO_SERVICE);
if (am != null && am.isBluetoothA2dpOn()) q = bt;
}
return q;
}
+13 -11
View File
@@ -435,6 +435,7 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
final String[] rows = {
str(R.string.set_quality_home, qualityLabel(app.qualityHome())),
str(R.string.set_quality_away, qualityLabel(app.qualityAway())),
str(R.string.set_quality_bt, qualityLabel(app.qualityBluetooth())),
str(R.string.set_dl_quality, dlQualityLabel(Downloads.get().quality())),
str(R.string.set_delete_downloads, Downloads.size(Downloads.get().bytesOnDisk())),
str(R.string.set_download_first, str(app.downloadFirst() ? R.string.on : R.string.off)),
@@ -454,21 +455,22 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
switch (which) {
case 0: pickQuality("qHome", app.qualityHome(), str(R.string.quality_home_title)); break;
case 1: pickQuality("qAway", app.qualityAway(), str(R.string.quality_away_title)); break;
case 2: pickDownloadQuality(); break;
case 3: confirmDeleteDownloads(); break;
case 4:
case 2: pickQuality("qBt", app.qualityBluetooth(), str(R.string.quality_bt_title)); break;
case 3: pickDownloadQuality(); break;
case 4: confirmDeleteDownloads(); break;
case 5:
app.prefs.edit().putBoolean("downloadFirst", !app.downloadFirst()).commit();
toast(str(R.string.download_first_toast, str(app.downloadFirst() ? R.string.on : R.string.off)));
break;
case 5:
case 6:
Server.preferSecureAtHome = !Server.preferSecureAtHome;
app.prefs.edit().putBoolean("secureHome", Server.preferSecureAtHome).commit();
if (app.server != null) app.server.invalidate();
toast(str(Server.preferSecureAtHome ? R.string.https_home_on : R.string.https_home_off));
break;
case 6: startActivity(new Intent(BaseActivity.this, SetupActivity.class).putExtra(SetupActivity.EXTRA_MODE, SetupActivity.MODE_LIBRARY)); break;
case 7: startActivity(new Intent(BaseActivity.this, SetupActivity.class).putExtra(SetupActivity.EXTRA_MODE, SetupActivity.MODE_SERVER)); break;
case 8:
case 7: startActivity(new Intent(BaseActivity.this, SetupActivity.class).putExtra(SetupActivity.EXTRA_MODE, SetupActivity.MODE_LIBRARY)); break;
case 8: startActivity(new Intent(BaseActivity.this, SetupActivity.class).putExtra(SetupActivity.EXTRA_MODE, SetupActivity.MODE_SERVER)); break;
case 9:
if (app.server != null) {
app.server.invalidate();
toast(str(R.string.reconnecting));
@@ -480,10 +482,10 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
});
}
break;
case 9: showDiagnostics(); break;
case 10: pickLanguage(); break;
case 11: pickSize(); break;
case 12: confirmSignOut(); break;
case 10: showDiagnostics(); break;
case 11: pickLanguage(); break;
case 12: pickSize(); break;
case 13: confirmSignOut(); break;
}
}
}).show();
+11 -5
View File
@@ -146,7 +146,8 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
private boolean pausedByCall;
private boolean pausedByRoute; // the Bluetooth speaker dropped: resume if it's back soon
private long routeLostAt;
private boolean btOn; // audio was going to Bluetooth (A2DP), as last seen while playing
private boolean btOn;
private long failedAtPos = -1; // where the last failure/stall was recovered from // audio was going to Bluetooth (A2DP), as last seen while playing
private WifiManager.WifiLock wifiLock;
private boolean noisyRegistered;
private boolean foreground;
@@ -489,6 +490,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
}
private void trackFailed(String msg) {
failedAtPos = Math.max(currentPosition(), lastPos);
com.minamp.net.DiagLog.log("track failed: " + msg + (refusal != null ? " / " + refusal : "") + " (retry " + (retries + 1) + ")");
long pos = Math.max(currentPosition(), lastPos);
if (refusal != null) msg = refusal; // the server's own reason beats "Playback error (1/-1004)"
@@ -627,15 +629,19 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
if (state == PLAYING && track != null) {
long pos = currentPosition();
lastPos = pos;
// "playing" but not moving for 12 s: say so instead of staying silent
// "playing" but not moving: the stream starved (Wi-Fi stalled, often because Bluetooth audio
// shares the radio) and the media player waits forever. Recover like a network error:
// reconnect and resume here; then download the track first; then move on.
if (pos != stallPos) {
stallPos = pos;
stallSince = now;
stallWarned = false;
} else if (!stallWarned && now - stallSince > 12000) {
// playing smoothly again for a while: a later stall starts the recovery steps afresh
if (retries > 0 && failedAtPos >= 0 && pos - failedAtPos > 30000) { retries = 0; failedAtPos = -1; }
} else if (!stallWarned && now - stallSince > 10000) {
stallWarned = true;
com.minamp.net.DiagLog.log("playback stalled at " + pos / 1000 + "s");
toast(getString(R.string.stalled));
com.minamp.net.DiagLog.log("playback stalled at " + pos / 1000 + "s" + (btOn ? " (Bluetooth)" : "") + ": recovering");
trackFailed(getString(R.string.stalled));
}
if (!scrobbled && track.duration > 0 && pos > track.duration * 9 / 10) scrobble(track);
if (now - lastTimeline > 10000) report(false);