Minamp 1.10: read the next track ahead; log to logcat too

On weak Wi-Fi shared with Bluetooth audio, Android 2.x's player (a few seconds
of stream buffer) ran dry and went silent for 6-7 s at a time; the speaker then
sometimes hung up. While a track plays, the next one is fetched whole (Wi-Fi
only) and played from the file. DiagLog lines also go to logcat (tag Minamp).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
XoséandClaude Opus 5.5 committed 2026-10-18 15:25:54 +02:00
1 parent 9aaeb57c87
commit 29ed836b4c
3 files changed
+100 -4

No files matched your search

+2 -2
View File
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.minamp"
android:versionCode="10"
android:versionName="1.9"
android:versionCode="11"
android:versionName="1.10"
android:installLocation="internalOnly">
<uses-sdk android:minSdkVersion="8" android:targetSdkVersion="10" />
+97 -2
View File
@@ -147,7 +147,17 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
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 long failedAtPos = -1;
// Read-ahead: the next track is fetched whole while this one plays, then played from the file.
// Android 2.x's player keeps only seconds of stream buffered; on weak Wi-Fi shared with Bluetooth
// audio it runs dry and goes silent for 6-7 s at a time (and some speakers then hang up).
private volatile boolean prefetchBusy;
private String prefetchKey; // track in prefetchFile (complete), main thread
private File prefetchFile;
private String prefetchExt;
private int prefetchKbps;
private File playingPrefetch; // the prefetched file being played: not to be overwritten // 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;
@@ -285,6 +295,18 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
begin(new java.util.ArrayList<String>(), "mp3", saved, 0, posMs);
return;
}
if (prefetchKey != null && prefetchKey.equals(it.ratingKey) && prefetchFile != null && prefetchFile.exists()) {
com.minamp.net.DiagLog.log("playing \"" + it.title + "\" from the read-ahead file");
transcoding = false;
session = null;
variantOrder = null;
kbps = prefetchKbps;
playingPrefetch = prefetchFile;
prefetchKey = null;
begin(new java.util.ArrayList<String>(), prefetchExt, playingPrefetch, 0, posMs);
return;
}
playingPrefetch = null;
final boolean download = forceDownload || app.downloadFirst();
worker.execute(new Runnable() {
public void run() {
@@ -331,6 +353,76 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
});
}
/** Fetches the next track whole in the background (Wi-Fi only), if it isn't here yet. Main thread. */
private void maybePrefetch() {
final App app = App.get();
if (prefetchBusy || app.library == null || app.onMobileData()) return;
PlayQueue q = PlayQueue.get();
final Item next = q.peekNext();
if (next == null || next == track || next.ratingKey.equals(prefetchKey) || Downloads.get().fileFor(next) != null) return;
// two slots: never overwrite the file that's playing
File base = null;
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) base = getExternalCacheDir();
if (base == null) base = getCacheDir();
File a = new File(base, "ahead-a.tmp"), b = new File(base, "ahead-b.tmp");
final File target = a.equals(playingPrefetch) ? b : a;
prefetchBusy = true;
prefetchKey = null;
com.minamp.net.Threads.start(new Runnable() {
public void run() {
String ext = "mp3";
int k = 0;
boolean ok = false;
long t0 = System.currentTimeMillis();
try {
Library lib = app.library;
lib.ensureMedia(next);
lib.server.base();
int max = app.currentMaxKbps();
boolean direct = Library.canDirectPlay(next, max);
java.util.List<String> urls;
if (direct) { urls = new java.util.ArrayList<String>(); urls.add(lib.directUrl(next)); ext = extFor(next); }
else urls = lib.transcodeUrls(next, max, 0, "minamp-ahead-" + Long.toString(System.currentTimeMillis(), 36));
k = direct ? 0 : (max <= 0 ? 320 : max);
for (String u : urls) {
Http.Response r = Http.get(u, lib.server.mediaHeaders(), 30000);
if (r.code != 200) { r.close(); continue; }
FileOutputStream out = new FileOutputStream(target);
long got = 0;
try {
byte[] buf = new byte[16384];
int n;
while ((n = r.body.read(buf)) > 0) { out.write(buf, 0, n); got += n; }
} finally {
out.close();
r.close();
}
// a stream cut short leaves a truncated file: only a plausible length counts
ok = got > 0 && (r.contentLength <= 0 || got >= r.contentLength);
com.minamp.net.DiagLog.log("read ahead \"" + next.title + "\": " + got / 1024 + " KB in "
+ (System.currentTimeMillis() - t0) / 1000 + " s" + (ok ? "" : " (incomplete)"));
break;
}
} catch (Throwable e) {
com.minamp.net.DiagLog.log("read ahead failed: " + com.minamp.net.Threads.describe(e));
}
final boolean done = ok;
final String fext = ext;
final int fk = k;
post(new Runnable() {
public void run() {
prefetchBusy = false;
if (!done) return;
prefetchKey = next.ratingKey;
prefetchFile = target;
prefetchExt = fext;
prefetchKbps = fk;
}
});
}
}, "ahead");
}
private static String extFor(Item it) {
String c = it.container.toLowerCase();
if ("mp4".equals(c) || "m4a".equals(c) || "m4b".equals(c)) return "m4a";
@@ -618,7 +710,10 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
private final Runnable ticker = new Runnable() {
public void run() {
if (state == PLAYING) btOn = audio.isBluetoothA2dpOn();
if (state == PLAYING) {
btOn = audio.isBluetoothA2dpOn();
maybePrefetch();
}
App.get().handler.postDelayed(this, 1000);
long now = System.currentTimeMillis();
if (sleepAt > 0 && now >= sleepAt) {
+1
View File
@@ -19,6 +19,7 @@ public final class DiagLog {
private DiagLog() {}
public static void log(String msg) {
android.util.Log.i("Minamp", msg); // also in logcat: readable over USB after the fact
String line;
synchronized (fmt) { line = fmt.format(new Date()) + " " + msg; }
synchronized (lines) {