Private
Public Access
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:
1 parent
948d1198ad
commit
0a2ac901bd
16 files changed
+417
-80
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user