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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="com.minamp"
|
package="com.minamp"
|
||||||
android:versionCode="10"
|
android:versionCode="11"
|
||||||
android:versionName="1.9"
|
android:versionName="1.10"
|
||||||
android:installLocation="internalOnly">
|
android:installLocation="internalOnly">
|
||||||
|
|
||||||
<uses-sdk android:minSdkVersion="8" android:targetSdkVersion="10" />
|
<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 boolean pausedByRoute; // the Bluetooth speaker dropped: resume if it's back soon
|
||||||
private long routeLostAt;
|
private long routeLostAt;
|
||||||
private boolean btOn;
|
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 WifiManager.WifiLock wifiLock;
|
||||||
private boolean noisyRegistered;
|
private boolean noisyRegistered;
|
||||||
private boolean foreground;
|
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);
|
begin(new java.util.ArrayList<String>(), "mp3", saved, 0, posMs);
|
||||||
return;
|
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();
|
final boolean download = forceDownload || app.downloadFirst();
|
||||||
worker.execute(new Runnable() {
|
worker.execute(new Runnable() {
|
||||||
public void run() {
|
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) {
|
private static String extFor(Item it) {
|
||||||
String c = it.container.toLowerCase();
|
String c = it.container.toLowerCase();
|
||||||
if ("mp4".equals(c) || "m4a".equals(c) || "m4b".equals(c)) return "m4a";
|
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() {
|
private final Runnable ticker = new Runnable() {
|
||||||
public void run() {
|
public void run() {
|
||||||
if (state == PLAYING) btOn = audio.isBluetoothA2dpOn();
|
if (state == PLAYING) {
|
||||||
|
btOn = audio.isBluetoothA2dpOn();
|
||||||
|
maybePrefetch();
|
||||||
|
}
|
||||||
App.get().handler.postDelayed(this, 1000);
|
App.get().handler.postDelayed(this, 1000);
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
if (sleepAt > 0 && now >= sleepAt) {
|
if (sleepAt > 0 && now >= sleepAt) {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ public final class DiagLog {
|
|||||||
private DiagLog() {}
|
private DiagLog() {}
|
||||||
|
|
||||||
public static void log(String msg) {
|
public static void log(String msg) {
|
||||||
|
android.util.Log.i("Minamp", msg); // also in logcat: readable over USB after the fact
|
||||||
String line;
|
String line;
|
||||||
synchronized (fmt) { line = fmt.format(new Date()) + " " + msg; }
|
synchronized (fmt) { line = fmt.format(new Date()) + " " + msg; }
|
||||||
synchronized (lines) {
|
synchronized (lines) {
|
||||||
|
|||||||
Reference in new issue
Block a user