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:
xocko committed 2026-10-18 12:49:56 +00:00
commit 9aaeb57c87
8 files changed
+91 -25

No files matched your search

+3 -3
View File
@@ -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" />
+9 -4
View File
@@ -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
+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();
+1
View File
@@ -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:
+51 -5
View File
@@ -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() {