diff --git a/res/values-en/strings.xml b/res/values-en/strings.xml
index b7f4bd7..03051d0 100644
--- a/res/values-en/strings.xml
+++ b/res/values-en/strings.xml
@@ -199,7 +199,7 @@
Most played
Title
Year (newest)
- Playback is stuck. Try pausing and playing again.
+ Playback got stuck: the network isn\'t keeping up.
Starting %1$s…
Stream interrupted
Surprise me
@@ -236,4 +236,6 @@
Removed from favourites
Queue
you
+ Quality over Bluetooth
+ Quality over Bluetooth: %1$s
diff --git a/res/values/strings.xml b/res/values/strings.xml
index f8e136e..a99b0b6 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -199,7 +199,7 @@
Máis escoitados
Título
Ano (máis recentes)
- A reprodución quedou parada. Proba a pausar e reproducir de novo.
+ A reprodución quedou parada: a rede non dá abasto.
Iniciando %1$s…
Interrompeuse a transmisión
Sorpréndeme
@@ -236,4 +236,6 @@
Quitada de favoritas
Cola
ti
+ Calidade por Bluetooth
+ Calidade por Bluetooth: %1$s
diff --git a/src/com/minamp/App.java b/src/com/minamp/App.java
index 6f87f3a..69ec321 100644
--- a/src/com/minamp/App.java
+++ b/src/com/minamp/App.java
@@ -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;
}
diff --git a/src/com/minamp/BaseActivity.java b/src/com/minamp/BaseActivity.java
index 93bc5ef..c864984 100644
--- a/src/com/minamp/BaseActivity.java
+++ b/src/com/minamp/BaseActivity.java
@@ -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();
diff --git a/src/com/minamp/PlayerService.java b/src/com/minamp/PlayerService.java
index 70e9991..1895a84 100644
--- a/src/com/minamp/PlayerService.java
+++ b/src/com/minamp/PlayerService.java
@@ -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);