Private
Public Access
Merge pull request 'Minamp 1.9: playback fixes for Bluetooth and USB' (#2) from ios-backports into main
This commit was merged in pull request #2.
This commit is contained in:
commit
9aaeb57c87
8 files changed
+91
-25
No files matched your search
+3
-3
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.minamp"
|
||||
android:versionCode="9"
|
||||
android:versionName="1.8"
|
||||
android:installLocation="auto">
|
||||
android:versionCode="10"
|
||||
android:versionName="1.9"
|
||||
android:installLocation="internalOnly">
|
||||
|
||||
<uses-sdk android:minSdkVersion="8" android:targetSdkVersion="10" />
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@ log stays in English since it's meant for troubleshooting.
|
||||
1. On the phone: **Settings → Applications → Unknown sources** (tick it).
|
||||
2. Copy `Minamp.apk` to the SD card (USB cable, or download it in the browser) and open it
|
||||
with a file manager, or from a computer: `adb install Minamp.apk`.
|
||||
3. The app can live on the SD card (Settings → Applications → Manage → Minamp → Move to SD card),
|
||||
which helps with the Mini's tiny internal storage.
|
||||
3. Minamp installs to internal storage (about 1 MB): an app on the SD card is killed whenever the
|
||||
phone shares its SD card over USB, which stopped the music.
|
||||
|
||||
## Sign in
|
||||
|
||||
@@ -56,11 +56,16 @@ clock has been reset to before 2024 the certificate date check is skipped rather
|
||||
* MP3, AAC (M4A) and Ogg Vorbis play directly. FLAC, ALAC etc. are transcoded to MP3 by the
|
||||
server (Android 2.2/2.3 can't decode FLAC).
|
||||
* **Menu → Settings**: separate quality for home Wi-Fi (default *Original*) and for away/mobile
|
||||
data (default *192 kbps MP3*); 320/256/192/128/96 kbps available.
|
||||
data (default *192 kbps MP3*); 320/256/192/128/96 kbps available. *Quality over Bluetooth*
|
||||
(default 192 kbps) caps both while the sound goes to a Bluetooth speaker: on the Mini, Wi-Fi and
|
||||
Bluetooth share one radio, and a lighter stream keeps the speaker from stuttering.
|
||||
* Seeking in a transcoded track restarts the transcode at the new position.
|
||||
* If a stream fails, Minamp re-picks the connection and resumes; if it fails again it downloads
|
||||
the whole track first and plays from the SD card. "Download tracks before playing" forces that
|
||||
mode (useful on flaky mobile data).
|
||||
mode (useful on flaky mobile data). Playback that stalls ("playing" but the time doesn't move
|
||||
for 10 s) is recovered the same way.
|
||||
* A Bluetooth speaker that drops its link for a moment pauses Minamp, which resumes by itself if the
|
||||
speaker is back within 20 s. Why playback paused or stopped is written to the connection log.
|
||||
* Plays are reported to Plex (Now Playing in the Plex dashboard, play counts, resume position).
|
||||
|
||||
## Features
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -19,6 +19,7 @@ public class MediaButtonReceiver extends BroadcastReceiver {
|
||||
if (ev == null) return;
|
||||
if (isOrderedBroadcast()) abortBroadcast();
|
||||
if (ev.getAction() != KeyEvent.ACTION_DOWN || ev.getRepeatCount() > 0) return;
|
||||
com.minamp.net.DiagLog.log("media button " + ev.getKeyCode() + " (headset / Bluetooth)");
|
||||
final Context app = context.getApplicationContext();
|
||||
switch (ev.getKeyCode()) {
|
||||
case KeyEvent.KEYCODE_HEADSETHOOK:
|
||||
|
||||
@@ -144,6 +144,10 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
|
||||
private boolean hasFocus;
|
||||
private boolean pausedByFocus;
|
||||
private boolean pausedByCall;
|
||||
private boolean pausedByRoute; // the Bluetooth speaker dropped: resume if it's back soon
|
||||
private long routeLostAt;
|
||||
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;
|
||||
@@ -486,6 +490,8 @@ 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)"
|
||||
boolean permanent = refusalCode >= 400 && refusalCode < 500 && refusalCode != 408 && refusalCode != 429;
|
||||
@@ -536,6 +542,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
|
||||
|
||||
private void pause() {
|
||||
pausedByFocus = false;
|
||||
pausedByRoute = false;
|
||||
playWhenReady = false;
|
||||
if (mp != null && prepared) {
|
||||
try { mp.pause(); } catch (Exception ignored) {}
|
||||
@@ -611,6 +618,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
|
||||
|
||||
private final Runnable ticker = new Runnable() {
|
||||
public void run() {
|
||||
if (state == PLAYING) btOn = audio.isBluetoothA2dpOn();
|
||||
App.get().handler.postDelayed(this, 1000);
|
||||
long now = System.currentTimeMillis();
|
||||
if (sleepAt > 0 && now >= sleepAt) {
|
||||
@@ -621,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);
|
||||
@@ -692,10 +704,12 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
|
||||
if (pausedByFocus) { pausedByFocus = false; play(); }
|
||||
break;
|
||||
case AudioManager.AUDIOFOCUS_LOSS:
|
||||
com.minamp.net.DiagLog.log("paused: another app took the audio (focus lost)");
|
||||
hasFocus = false;
|
||||
pause();
|
||||
break;
|
||||
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
|
||||
com.minamp.net.DiagLog.log("paused for a moment: another app needs the audio");
|
||||
if (state == PLAYING) { pause(); pausedByFocus = true; }
|
||||
break;
|
||||
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
|
||||
@@ -716,8 +730,40 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Headphones unplugged, or the Bluetooth speaker went away. Android 2.x sends this for any A2DP dropout too
|
||||
* (Wi-Fi and Bluetooth share one radio on these phones, and cheap speakers drop the link now and then):
|
||||
* after a Bluetooth loss, keep watching for the speaker and resume if it's back within 20 s.
|
||||
*/
|
||||
private final BroadcastReceiver noisy = new BroadcastReceiver() {
|
||||
@Override public void onReceive(Context c, Intent i) { pause(); }
|
||||
@Override public void onReceive(Context c, Intent i) {
|
||||
boolean bt = btOn;
|
||||
com.minamp.net.DiagLog.log("paused: audio output went away (" + (bt ? "Bluetooth speaker dropped" : "headphones unplugged") + ")");
|
||||
pause();
|
||||
if (bt && track != null) {
|
||||
pausedByRoute = true;
|
||||
routeLostAt = System.currentTimeMillis();
|
||||
App.get().handler.removeCallbacks(routeWatch);
|
||||
App.get().handler.postDelayed(routeWatch, 1000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private final Runnable routeWatch = new Runnable() {
|
||||
public void run() {
|
||||
if (!pausedByRoute) return;
|
||||
long gone = System.currentTimeMillis() - routeLostAt;
|
||||
if (audio.isBluetoothA2dpOn()) {
|
||||
com.minamp.net.DiagLog.log("Bluetooth speaker back after " + gone / 1000 + "s: resuming");
|
||||
pausedByRoute = false;
|
||||
play();
|
||||
} else if (gone < 20000) {
|
||||
App.get().handler.postDelayed(this, 1000);
|
||||
} else {
|
||||
com.minamp.net.DiagLog.log("Bluetooth speaker didn't come back: staying paused");
|
||||
pausedByRoute = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private void registerNoisy() {
|
||||
|
||||
Reference in new issue
Block a user