Minamp 1.2

- Transcoding: send client identities Plex has a profile for (unknown ones get
  400 "unable to find a matching profile"), with a fresh session id per variant
- Stream proxy: fetch transcodes once into a temp file and serve every player
  request from it, so reconnects no longer restart the transcode (503s, tracks
  cut short); original files still pass Range requests through
- Sign-in: remember the pending link code so it survives Minamp being closed
  while the code is entered elsewhere; add "I've entered it - check now"
- Diagnostics: in-app connection log (DiagLog) with a streaming test and save
  to SD card; tokens are never logged

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xockoandClaude Opus 5.5 committed 2026-10-14 20:38:17 +02:00
1 parent 0a2ac901bd
commit a4b875ef34
12 files changed
+701 -196

No files matched your search

+105 -1
View File
@@ -397,6 +397,7 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
"Library: " + app.sectionTitle(),
"Server: " + (s == null ? "-" : s.name),
"Connection: " + conn,
"Connection log & streaming test",
"Sign out",
};
new AlertDialog.Builder(this)
@@ -424,7 +425,8 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
});
}
break;
case 6: confirmSignOut(); break;
case 6: showDiagnostics(); break;
case 7: confirmSignOut(); break;
}
}
}).show();
@@ -432,6 +434,108 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
protected void onConnectionChanged() {}
// ------------------------------------------------------------------ diagnostics
/** Connection log with "test streaming" and "save to SD card", for reporting problems. */
protected void showDiagnostics() {
final android.widget.ScrollView sv = new android.widget.ScrollView(this);
final TextView tv = new TextView(this);
tv.setTextSize(9);
tv.setTextColor(0xFFDDDDDD);
tv.setTypeface(android.graphics.Typeface.MONOSPACE);
int p = Ui.dp(this, 6);
tv.setPadding(p, p, p, p);
String log = com.minamp.net.DiagLog.dump();
tv.setText(log.length() > 0 ? log : "Nothing logged yet.");
sv.addView(tv);
AlertDialog.Builder b = new AlertDialog.Builder(this)
.setTitle("Connection log")
.setView(sv)
.setNegativeButton("Save to SD card", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int w) { saveDiagnostics(); }
})
.setPositiveButton("Close", null);
if (app.library != null) {
b.setNeutralButton("Test streaming", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int w) { testStreaming(); }
});
}
AlertDialog dlg = b.create();
dlg.show();
sv.post(new Runnable() { public void run() { sv.fullScroll(View.FOCUS_DOWN); } });
}
private void saveDiagnostics() {
try {
if (!android.os.Environment.MEDIA_MOUNTED.equals(android.os.Environment.getExternalStorageState())) {
toast("No SD card available");
return;
}
java.io.File f = new java.io.File(android.os.Environment.getExternalStorageDirectory(), "Minamp-log.txt");
java.io.FileOutputStream o = new java.io.FileOutputStream(f);
String head = "Minamp " + com.minamp.plex.Plex.version + " on " + android.os.Build.MODEL
+ ", Android " + android.os.Build.VERSION.RELEASE + "\n";
o.write((head + com.minamp.net.DiagLog.dump()).getBytes("UTF-8"));
o.close();
toast("Saved as Minamp-log.txt on the SD card");
} catch (Exception e) {
toast("Couldn't save: " + msg(e));
}
}
/** Asks the server about every transcode variant for one track, and logs exactly what it says. */
private void testStreaming() {
final Library lib = lib();
final Item current = PlayQueue.get().current();
toast("Testing\u2026 (about 30 s)");
run(new Task<Object>() {
public Object work() throws Exception {
com.minamp.net.DiagLog.log("=== streaming test ===");
Item t = current;
if (t == null) {
List<Item> r = lib.libraryRadio();
if (r.isEmpty()) throw new Exception("no tracks in the library");
t = r.get(0);
}
lib.ensureMedia(t);
com.minamp.net.DiagLog.log("track: " + t.title + " [" + t.codec + "/" + t.container + " " + t.bitrate + " kbps] via "
+ (lib.server.active() == null ? "?" : Server.kindLabel(lib.server.active().kind)));
int kbps = app.currentMaxKbps();
if (kbps <= 0) kbps = 320;
for (int v = 0; v < Library.VARIANTS; v++) {
com.minamp.net.DiagLog.log("variant " + v + " (" + Library.VARIANT_NAMES[v] + ") decision: " + lib.decision(t, kbps, v));
String session = "minamp-test-" + System.currentTimeMillis() + "-" + v;
String url = lib.transcodeUrl(t, kbps, 0, session, v);
com.minamp.net.Http.Response r = null;
try {
r = com.minamp.net.Http.get(url, null, 30000);
if (r.code < 400) {
byte[] buf = new byte[4096];
int n = r.body.read(buf);
boolean mp3 = n > 3 && ((buf[0] == 'I' && buf[1] == 'D' && buf[2] == '3') || ((buf[0] & 0xFF) == 0xFF && (buf[1] & 0xE0) == 0xE0));
com.minamp.net.DiagLog.log("variant " + v + " start: " + r.code + ", " + r.header("content-type")
+ ", first bytes " + (mp3 ? "look like MP3" : "are NOT MP3"));
} else {
com.minamp.net.DiagLog.log("variant " + v + " start: " + r.code + " " + com.minamp.plex.Plex.snippet(r.readString()));
}
} catch (Exception e) {
com.minamp.net.DiagLog.log("variant " + v + " start failed: " + com.minamp.net.Threads.describe(e));
} finally {
com.minamp.net.Http.abort(r);
lib.stopTranscode(session);
}
}
com.minamp.net.DiagLog.log("=== end of test ===");
return null;
}
public void done(Object o) { showDiagnostics(); }
public void fail(Exception e) {
com.minamp.net.DiagLog.log("streaming test failed: " + msg(e));
showDiagnostics();
}
});
}
private static String qualityLabel(int kbps) {
for (int i = 0; i < App.QUALITIES.length; i++) if (App.QUALITIES[i] == kbps) return App.QUALITY_LABELS[i];
return kbps + " kbps";