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);