mkv chunking

This commit is contained in:
Radek Davidek 2026-03-04 15:18:26 +01:00
parent 4ae128f35d
commit b8df5d0997

View File

@ -244,6 +244,11 @@ public final class XtreamPlayerApplication {
return;
}
if (isLikelyLargeVodFile(target, sourceUrl)) {
proxyLargeMediaStream(exchange, target, sourceUrl);
return;
}
try {
List<URI> attempts = candidateUris(target);
HttpResponse<byte[]> response = null;
@ -798,10 +803,103 @@ public final class XtreamPlayerApplication {
return out.toString();
}
private static boolean isLikelyLargeVodFile(URI uri, String sourceUrl) {
if (sourceUrl != null && !sourceUrl.isBlank()) {
return false;
}
String path = uri == null || uri.getPath() == null ? "" : uri.getPath().toLowerCase(Locale.ROOT);
return path.endsWith(".mkv") || path.contains(".mkv?")
|| path.endsWith(".mp4") || path.contains(".mp4?");
}
private static void proxyLargeMediaStream(HttpExchange exchange, URI target, String sourceUrl) throws IOException {
List<URI> attempts = candidateUris(target);
List<String> attemptErrors = new ArrayList<>();
for (URI candidate : attempts) {
try {
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(candidate)
.GET()
.header("User-Agent", firstNonBlank(
exchange.getRequestHeaders().getFirst("User-Agent"),
DEFAULT_BROWSER_UA
))
.header("Accept", firstNonBlank(
exchange.getRequestHeaders().getFirst("Accept"),
"*/*"
));
copyRequestHeaderIfPresent(exchange, requestBuilder, "Range");
copyRequestHeaderIfPresent(exchange, requestBuilder, "If-Range");
copyRequestHeaderIfPresent(exchange, requestBuilder, "Accept-Encoding");
copyRequestHeaderIfPresent(exchange, requestBuilder, "Cache-Control");
copyRequestHeaderIfPresent(exchange, requestBuilder, "Pragma");
String referer = resolveRefererForCandidate(exchange, candidate, sourceUrl);
if (!referer.isBlank()) {
requestBuilder.header("Referer", referer);
}
HttpResponse<InputStream> response = HTTP_CLIENT.send(
requestBuilder.build(),
HttpResponse.BodyHandlers.ofInputStream()
);
int status = response.statusCode();
String contentType = response.headers().firstValue("Content-Type").orElse("application/octet-stream");
if (status >= 400) {
attemptErrors.add(maskUri(candidate) + " -> HTTP " + status);
try (InputStream ignored = response.body()) {
// close upstream body
}
continue;
}
copyResponseHeaderIfPresent(response, exchange, "Accept-Ranges");
copyResponseHeaderIfPresent(response, exchange, "Content-Range");
copyResponseHeaderIfPresent(response, exchange, "Cache-Control");
copyResponseHeaderIfPresent(response, exchange, "Expires");
exchange.getResponseHeaders().set("Content-Type", contentType);
response.headers().firstValue("Content-Length")
.ifPresent(value -> exchange.getResponseHeaders().set("Content-Length", value));
long responseLength = parseContentLength(response.headers().firstValue("Content-Length").orElse(""));
exchange.sendResponseHeaders(status, responseLength >= 0 ? responseLength : 0);
long sent = 0;
try (InputStream inputStream = response.body()) {
byte[] buffer = new byte[64 * 1024];
int read;
while ((read = inputStream.read(buffer)) >= 0) {
exchange.getResponseBody().write(buffer, 0, read);
sent += read;
}
} finally {
logApiResponse(exchange, status, (int) Math.min(Integer.MAX_VALUE, sent), contentType);
exchange.close();
}
return;
} catch (InterruptedException interruptedException) {
Thread.currentThread().interrupt();
writeJson(exchange, 500, errorJson("Stream proxy interrupted."));
return;
} catch (Exception exception) {
attemptErrors.add(maskUri(candidate) + " -> " + compactError(exception));
LOGGER.warn("Large media proxy candidate failed uri={} reason={}", maskUri(candidate), compactError(exception));
}
}
writeJson(exchange, 502, errorJson("Unable to proxy large media stream. Attempts: " + String.join(" | ", attemptErrors)));
}
private static String proxyStreamUrl(String absoluteUrl, String sourceUrl) {
return "/api/stream-proxy?url=" + urlEncode(absoluteUrl) + "&src=" + urlEncode(sourceUrl);
}
private static long parseContentLength(String value) {
if (value == null || value.isBlank()) {
return -1L;
}
try {
return Long.parseLong(value.trim());
} catch (NumberFormatException ignored) {
return -1L;
}
}
private static UpstreamResult retrySegmentUsingFreshPlaylist(HttpExchange exchange, URI originalSegmentUri, String sourceUrl)
throws IOException, InterruptedException {
URI sourceUri = URI.create(sourceUrl);