Minamp 1.1

- Run network/TLS work on 256 KB-stack threads (net/Threads.java): Android 2.x's
  ~16 KB default stack overflowed during the bundled TLS handshake
- Close pooled TLS connections off the caller thread
- Try several Plex music-transcoder URL variants until the server accepts one,
  and remember the one that worked
- Accept pasted server URLs with paths; default to 443 for https
- Catch fatal errors and save a crash report (shown on next launch and written
  to Minamp-error.txt on the SD card)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xockoandClaude Opus 5.5 committed 2026-10-14 19:59:58 +02:00
1 parent 948d1198ad
commit 0a2ac901bd
16 files changed
+417 -80

No files matched your search

+7 -4
View File
@@ -135,13 +135,16 @@ public final class Http {
/** Drop all pooled connections (e.g. after the network changed). */
public static void evictAll() {
final List<Conn> doomed = new ArrayList<Conn>();
synchronized (pool) {
for (Iterator<List<Conn>> it = pool.values().iterator(); it.hasNext();) {
List<Conn> l = it.next();
for (int i = 0; i < l.size(); i++) l.get(i).close();
}
for (Iterator<List<Conn>> it = pool.values().iterator(); it.hasNext();) doomed.addAll(it.next());
pool.clear();
}
if (doomed.isEmpty()) return;
// closing TLS sends an encrypted alert: keep that off the (small-stack) caller thread
Threads.start(new Runnable() {
public void run() { for (int i = 0; i < doomed.size(); i++) doomed.get(i).close(); }
}, "http-evict");
}
private static Conn connect(final Url u, int timeoutMs) throws IOException {
+59 -20
View File
@@ -26,19 +26,29 @@ public final class StreamProxy implements Runnable {
private Thread thread;
private volatile boolean running;
private int counter;
private final Map<String, String> routes = new HashMap<String, String>();
private final Map<String, Route> routes = new HashMap<String, Route>();
/** One stream, with fallback URLs tried in order until the server accepts one. */
private static final class Route {
final List<String> urls;
int chosen = -1; // index that worked; later requests (seeks) go straight to it
Route(List<String> urls) { this.urls = urls; }
}
private final Map<String, List<Http.Response>> live = new HashMap<String, List<Http.Response>>();
/** Called with how many bytes have arrived for a route (for buffering UI / diagnostics). */
public interface Listener { void onError(String routeId, String message); }
public interface Listener {
/** Every URL was refused (or the proxy failed); code is the last HTTP status, or 0. */
void onError(String routeId, int code, String message);
/** URL #index of the route's list is the one the server accepted. */
void onAccepted(String routeId, int index);
}
public volatile Listener listener;
public synchronized void start() throws IOException {
if (running) return;
server = new ServerSocket(0, 8, InetAddress.getByName("127.0.0.1"));
running = true;
thread = new Thread(this, "stream-proxy");
thread.setDaemon(true);
thread = Threads.create(this, "stream-proxy");
thread.start();
}
@@ -55,8 +65,14 @@ public final class StreamProxy implements Runnable {
/** @return the local URL to give MediaPlayer, ending in a fake extension it may sniff. */
public String register(String id, String remoteUrl, String ext) {
List<String> l = new ArrayList<String>();
l.add(remoteUrl);
return register(id, l, ext);
}
public String register(String id, List<String> remoteUrls, String ext) {
synchronized (routes) {
routes.put(id, remoteUrl);
routes.put(id, new Route(new ArrayList<String>(remoteUrls)));
}
return "http://127.0.0.1:" + port() + "/s/" + id + "/stream." + ext;
}
@@ -89,11 +105,9 @@ public final class StreamProxy implements Runnable {
if (!running) return;
continue;
}
Thread t = new Thread("proxy-conn") {
Threads.start(new Runnable() {
public void run() { handle(s); }
};
t.setDaemon(true);
t.start();
}, "proxy-conn");
}
}
@@ -120,9 +134,9 @@ public final class StreamProxy implements Runnable {
int slash = path.indexOf('/', 3);
id = slash < 0 ? path.substring(3) : path.substring(3, slash);
}
String target;
synchronized (routes) { target = id == null ? null : routes.get(id); }
if (target == null) {
Route route;
synchronized (routes) { route = id == null ? null : routes.get(id); }
if (route == null) {
out.write("HTTP/1.0 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n".getBytes("ISO-8859-1"));
out.flush();
return;
@@ -133,7 +147,31 @@ public final class StreamProxy implements Runnable {
String range = reqHeaders.get("range");
if (range != null) h.put("Range", range);
boolean head = "HEAD".equals(method);
up = Http.request(head ? "HEAD" : "GET", target, h, null, 30000);
int first, last;
synchronized (routes) {
first = route.chosen >= 0 ? route.chosen : 0;
last = route.chosen >= 0 ? route.chosen : route.urls.size() - 1;
}
String refusal = null;
for (int i = first; i <= last; i++) {
up = Http.request(head ? "HEAD" : "GET", route.urls.get(i), h, null, 30000);
if (up.code < 400 || up.code == 416) {
boolean firstTime;
synchronized (routes) { firstTime = route.chosen < 0; route.chosen = i; }
Listener l = listener;
if (firstTime && l != null) l.onAccepted(id, i);
break;
}
// refused: remember why, try the next variant
String body = "";
try { body = head ? "" : new String(up.readAll(), "UTF-8"); } catch (IOException ignored) {}
refusal = "Server refused the stream (" + up.code + ")" + (body.trim().length() > 0 ? ": " + com.minamp.plex.Plex.snippet(body) : "");
if (i < last) { up.close(); up = null; }
}
if (up.code >= 400 && up.code != 416) {
Listener l = listener;
if (l != null) l.onError(id, up.code, refusal);
}
synchronized (routes) {
if (!routes.containsKey(id)) { Http.abort(up); return; }
List<Http.Response> l = live.get(id);
@@ -142,13 +180,15 @@ public final class StreamProxy implements Runnable {
}
StringBuilder resp = new StringBuilder(256);
boolean known = up.contentLength >= 0 && !up.chunked;
boolean refused = up.code >= 400 && up.code != 416;
boolean known = !refused && up.contentLength >= 0 && !up.chunked;
resp.append(known ? "HTTP/1.1 " : "HTTP/1.0 ").append(up.code).append(' ').append(reason(up.code)).append("\r\n");
String ct = up.header("content-type");
if (ct == null || ct.startsWith("application/octet") || ct.startsWith("text/")) ct = guessType(path);
if (up.code >= 400) ct = "text/plain";
resp.append("Content-Type: ").append(ct).append("\r\n");
if (known) resp.append("Content-Length: ").append(up.contentLength).append("\r\n");
if (refused) resp.append("Content-Length: 0\r\n");
String cr = up.header("content-range");
if (cr != null) resp.append("Content-Range: ").append(cr).append("\r\n");
String ar = up.header("accept-ranges");
@@ -156,11 +196,7 @@ public final class StreamProxy implements Runnable {
resp.append("Connection: close\r\n\r\n");
out.write(resp.toString().getBytes("ISO-8859-1"));
out.flush();
if (up.code >= 400) {
Listener l = listener;
if (l != null) l.onError(id, "Server said " + up.code);
}
if (head) { up.close(); return; }
if (head || up.code >= 400) { up.close(); up = null; return; }
byte[] buf = new byte[16384];
int n;
@@ -176,6 +212,9 @@ public final class StreamProxy implements Runnable {
up = null;
} catch (IOException e) {
// player went away, track skipped, or the network dropped: just close
} catch (Throwable t) {
Listener l = listener;
if (l != null && id != null) l.onError(id, 0, Threads.describe(t));
} finally {
if (up != null) Http.abort(up);
if (id != null && up != null) {
+43
View File
@@ -0,0 +1,43 @@
package com.minamp.net;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
/**
* Background threads with big stacks. Android 2.x gives new threads ~16 KB of stack, and the
* bundled TLS handshake + certificate parsing needs far more; overflowing it kills the app.
* 256 KB is the most Dalvik allows.
*/
public final class Threads {
public static final long STACK = 256 * 1024;
private Threads() {}
public static Thread create(Runnable r, String name) {
Thread t = new Thread(null, r, name, STACK);
t.setDaemon(true);
return t;
}
public static void start(Runnable r, String name) {
create(r, name).start();
}
public static ExecutorService pool(final String name, int n) {
ThreadFactory f = new ThreadFactory() {
private int count;
public synchronized Thread newThread(Runnable r) { return create(r, name + "-" + (++count)); }
};
return n == 1 ? Executors.newSingleThreadExecutor(f) : Executors.newFixedThreadPool(n, f);
}
/** Readable one-liner for any failure, including Errors such as StackOverflowError. */
public static String describe(Throwable t) {
String m = t.getMessage();
String cls = t.getClass().getSimpleName();
if (m == null || m.length() == 0) return cls;
if (t instanceof java.io.IOException) return m;
return cls + ": " + m;
}
}