diff --git a/src/main/java/cz/kamma/xtreamplayer/XtreamPlayerApplication.java b/src/main/java/cz/kamma/xtreamplayer/XtreamPlayerApplication.java index d65928e..aae7a90 100644 --- a/src/main/java/cz/kamma/xtreamplayer/XtreamPlayerApplication.java +++ b/src/main/java/cz/kamma/xtreamplayer/XtreamPlayerApplication.java @@ -244,6 +244,11 @@ public final class XtreamPlayerApplication { return; } + if (isLikelyLargeVodFile(target, sourceUrl)) { + proxyLargeMediaStream(exchange, target, sourceUrl); + return; + } + try { List attempts = candidateUris(target); HttpResponse 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 attempts = candidateUris(target); + List 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 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);