Private
Public Access
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:
1 parent
9aaeb57c87
commit
29ed836b4c
3 files changed
+100
-4
No files matched your search
+2
-2
@@ -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" />
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user