Private
Public Access
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:
1 parent
0a2ac901bd
commit
a4b875ef34
12 files changed
+701
-196
No files matched your search
@@ -173,11 +173,14 @@ public final class Library {
|
||||
}
|
||||
|
||||
/**
|
||||
* Plex servers differ in which form of the music transcoder request they accept (some answer
|
||||
* 400 to one and stream fine with another), so there are several known variants. The proxy
|
||||
* tries them in order; the one that worked is remembered and tried first next time.
|
||||
* Plex only transcodes for a client identity it has a profile for: an unknown platform or
|
||||
* device gets a bare 400 ("unable to find a matching profile"). These are request shapes
|
||||
* known to work against real servers, tried in order; the one that works is remembered.
|
||||
* Every variant gets its own session id: re-using one tears down the previous transcode
|
||||
* (503s, tracks cut short).
|
||||
*/
|
||||
public static final int VARIANTS = 4;
|
||||
public static final String[] VARIANT_NAMES = {"web player", "web player + MP3 profile", "speaker profile", "generic audio"};
|
||||
public static volatile int preferredVariant = 0;
|
||||
|
||||
public static int[] variantOrder() {
|
||||
@@ -187,54 +190,83 @@ public final class Library {
|
||||
return order;
|
||||
}
|
||||
|
||||
public static String variantSession(String session, int variant) { return session + "-v" + variant; }
|
||||
|
||||
/** MP3 transcode URLs over plain progressive HTTP starting at offsetMs, in {@link #variantOrder()} order. */
|
||||
public java.util.List<String> transcodeUrls(Item t, int kbps, long offsetMs, String session) throws IOException {
|
||||
int[] order = variantOrder();
|
||||
java.util.List<String> out = new ArrayList<String>();
|
||||
for (int i = 0; i < order.length; i++) out.add(transcodeUrl(t, kbps, offsetMs, session, order[i]));
|
||||
for (int i = 0; i < order.length; i++) out.add(transcodeUrl(t, kbps, offsetMs, variantSession(session, order[i]), order[i]));
|
||||
return out;
|
||||
}
|
||||
|
||||
String transcodeUrl(Item t, int kbps, long offsetMs, String session, int variant) throws IOException {
|
||||
public String transcodeUrl(Item t, int kbps, long offsetMs, String session, int variant) throws IOException {
|
||||
if (kbps <= 0) kbps = 320;
|
||||
String target = "add-transcode-target(type=musicProfile&context=streaming&protocol=http&container=mp3&audioCodec=mp3)";
|
||||
String profile;
|
||||
String platform;
|
||||
String base = "/music/:/transcode/universal/start.mp3";
|
||||
String mp3Target = "add-transcode-target(replace=true&type=musicProfile&context=streaming&protocol=http&container=mp3&audioCodec=mp3)";
|
||||
String bitrateLimit = "+add-limitation(scope=musicCodec&scopeName=mp3&type=upperBound&name=audio.bitrate&value=" + kbps
|
||||
+ "&onlyTranscodes=true&replace=true)";
|
||||
StringBuilder q = new StringBuilder();
|
||||
String base;
|
||||
q.append("path=").append(Plex.enc("/library/metadata/" + t.ratingKey));
|
||||
switch (variant) {
|
||||
case 0: // what Plex's own web player identifies as, with an explicit MP3 target
|
||||
platform = "Chrome";
|
||||
profile = target + "+add-limitation(scope=musicCodec&scopeName=*&type=upperBound&name=audio.bitrate&value=" + kbps + ")";
|
||||
case 0:
|
||||
// Plex Web's own identity and the minimal parameter set it uses for a progressive MP3
|
||||
base = "/music/:/transcode/universal/start.mp3";
|
||||
q.append("&protocol=http&mediaIndex=0&partIndex=0&audioCodec=mp3")
|
||||
.append("&directPlay=0&directStream=1&fastSeek=1&musicBitrate=").append(kbps);
|
||||
break;
|
||||
case 1: // generic client profile
|
||||
platform = "Generic";
|
||||
profile = target + "+add-limitation(scope=musicCodec&scopeName=mp3&type=upperBound&name=audio.bitrate&value=" + kbps + ")";
|
||||
case 1:
|
||||
// same identity, forcing a real MP3 encode at our bitrate via a declared target
|
||||
base = "/music/:/transcode/universal/start.mp3";
|
||||
q.append("&protocol=http&mediaIndex=0&partIndex=0")
|
||||
.append("&directPlay=0&directStream=0&fastSeek=1&musicBitrate=").append(kbps)
|
||||
.append("&X-Plex-Client-Profile-Extra=").append(Plex.enc(mp3Target + bitrateLimit));
|
||||
break;
|
||||
case 2: // older servers' audio endpoint
|
||||
platform = "Chrome";
|
||||
profile = target;
|
||||
base = "/audio/:/transcode/universal/start.mp3";
|
||||
case 2:
|
||||
// a networked-speaker identity Plex ships a profile for; output decided by the target
|
||||
base = "/audio/:/transcode/universal/start";
|
||||
q.append("&directPlay=0&directStream=0&musicBitrate=").append(kbps)
|
||||
.append("&X-Plex-Client-Profile-Extra=").append(Plex.enc(mp3Target + bitrateLimit
|
||||
+ "+add-limitation(scope=musicCodec&scopeName=mp3&type=upperBound&name=audio.samplingRate&value=48000&onlyTranscodes=true&replace=true)"));
|
||||
break;
|
||||
default: // no profile tweaks at all; bitrate from musicBitrate only
|
||||
platform = "Chrome";
|
||||
profile = null;
|
||||
default:
|
||||
base = "/audio/:/transcode/universal/start";
|
||||
q.append("&protocol=http&directPlay=0&directStream=0&musicBitrate=").append(kbps)
|
||||
.append("&X-Plex-Client-Profile-Extra=").append(Plex.enc(mp3Target));
|
||||
}
|
||||
if (offsetMs >= 1000) q.append("&offset=").append(offsetMs / 1000);
|
||||
q.append("&session=").append(Plex.enc(session))
|
||||
.append("&X-Plex-Session-Identifier=").append(Plex.enc(session))
|
||||
.append("&X-Plex-Client-Identifier=").append(Plex.enc(Plex.clientId))
|
||||
.append("&X-Plex-Product=").append(Plex.enc(Plex.product))
|
||||
.append("&X-Plex-Version=").append(Plex.enc(Plex.version));
|
||||
if (variant == 2) {
|
||||
q.append("&X-Plex-Platform=Linux&X-Plex-Platform-Version=1.0")
|
||||
.append("&X-Plex-Device=Sonos&X-Plex-Device-Name=").append(Plex.enc(Plex.deviceName))
|
||||
.append("&X-Plex-Model=sonos");
|
||||
} else {
|
||||
// no X-Plex-Device: a phone model name matches no server profile and gets a 400
|
||||
q.append("&X-Plex-Platform=Chrome");
|
||||
}
|
||||
return server.url(base + "?" + q);
|
||||
}
|
||||
|
||||
/** Ask the server what it would do with a transcode request (for the diagnostics screen). */
|
||||
public String decision(Item t, int kbps, int variant) {
|
||||
try {
|
||||
String url = transcodeUrl(t, kbps, 0, "minamp-check-" + System.currentTimeMillis(), variant);
|
||||
url = url.replace("/start.mp3?", "/decision?").replace("/universal/start?", "/universal/decision?");
|
||||
com.minamp.net.Http.Response r = com.minamp.net.Http.get(url, Plex.headers(null), 20000);
|
||||
String body = r.readString();
|
||||
if (r.code != 200) return "HTTP " + r.code + (Plex.snippet(body).length() > 0 ? ": " + Plex.snippet(body) : "");
|
||||
Json.Obj mc = Json.parseObj(body).optObj("MediaContainer");
|
||||
if (mc == null) return "200 (no details)";
|
||||
return "general " + mc.optString("generalDecisionCode", "?") + " " + mc.optString("generalDecisionText", "")
|
||||
+ " | transcode " + mc.optString("transcodeDecisionCode", "?") + " " + mc.optString("transcodeDecisionText", "")
|
||||
+ " | direct play " + mc.optString("directPlayDecisionCode", "?") + " " + mc.optString("directPlayDecisionText", "");
|
||||
} catch (Exception e) {
|
||||
return com.minamp.net.Threads.describe(e);
|
||||
}
|
||||
String path = base
|
||||
+ "?path=" + Plex.enc("/library/metadata/" + t.ratingKey)
|
||||
+ "&mediaIndex=0&partIndex=0&protocol=http&hasMDE=1&fastSeek=1©ts=1"
|
||||
+ "&directPlay=0&directStream=0&directStreamAudio=0"
|
||||
+ "&musicBitrate=" + kbps + "&maxAudioBitrate=" + kbps
|
||||
+ "&offset=" + (offsetMs / 1000)
|
||||
+ "&session=" + Plex.enc(session)
|
||||
+ "&location=" + (server.isLocal() ? "lan" : "wan")
|
||||
+ "&X-Plex-Session-Identifier=" + Plex.enc(session)
|
||||
+ "&X-Plex-Client-Identifier=" + Plex.enc(Plex.clientId)
|
||||
+ "&X-Plex-Product=" + Plex.enc(Plex.product)
|
||||
+ "&X-Plex-Version=" + Plex.enc(Plex.version)
|
||||
+ "&X-Plex-Platform=" + platform
|
||||
+ "&X-Plex-Device=" + Plex.enc(Plex.device)
|
||||
+ (profile != null ? "&X-Plex-Client-Profile-Extra=" + Plex.enc(profile) : "");
|
||||
return server.url(path);
|
||||
}
|
||||
|
||||
public void stopTranscode(String session) {
|
||||
|
||||
Reference in new issue
Block a user