From ddf58833dacbc90d8a6cf1b37cbc4ec06ca370c0 Mon Sep 17 00:00:00 2001 From: Radek Davidek Date: Tue, 10 Mar 2026 17:18:16 +0100 Subject: [PATCH] some hls optimizations --- pom.xml | 2 +- .../xtreamplayer/XtreamPlayerApplication.java | 49 ++++++- src/main/resources/web/assets/app.js | 122 +++++++++++++++++- 3 files changed, 164 insertions(+), 9 deletions(-) diff --git a/pom.xml b/pom.xml index c83b67f..f83a97e 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ xtream-player - 17 + 21 UTF-8 diff --git a/src/main/java/cz/kamma/xtreamplayer/XtreamPlayerApplication.java b/src/main/java/cz/kamma/xtreamplayer/XtreamPlayerApplication.java index bd91d2d..16d87bf 100644 --- a/src/main/java/cz/kamma/xtreamplayer/XtreamPlayerApplication.java +++ b/src/main/java/cz/kamma/xtreamplayer/XtreamPlayerApplication.java @@ -409,6 +409,9 @@ public final class XtreamPlayerApplication { if (isHlsPlaylist(playlistBaseUri, contentType)) { String rewritten = rewritePlaylistForProxy(playlistBaseUri, body); exchange.getResponseHeaders().set("Content-Type", "application/vnd.apple.mpegurl; charset=utf-8"); + exchange.getResponseHeaders().set("Cache-Control", "no-cache, no-store, must-revalidate"); + exchange.getResponseHeaders().set("Pragma", "no-cache"); + exchange.getResponseHeaders().set("Expires", "0"); writeBytes(exchange, response.statusCode(), rewritten.getBytes(StandardCharsets.UTF_8), "application/vnd.apple.mpegurl; charset=utf-8"); return; @@ -419,6 +422,18 @@ public final class XtreamPlayerApplication { copyResponseHeaderIfPresent(response, exchange, "Cache-Control"); copyResponseHeaderIfPresent(response, exchange, "Expires"); exchange.getResponseHeaders().set("Content-Type", contentType); + + // Debug logging for segments + boolean isSegment = rawUrl.contains(".ts") || rawUrl.contains(".m4s"); + if (isSegment) { + LOGGER.info( + "Proxying segment: {} bytes, statusCode={}, contentType={}", + body.length, + response.statusCode(), + contentType + ); + } + writeBytes(exchange, response.statusCode(), body, contentType); } catch (Exception exception) { LOGGER.error("Stream proxy failed for {}", maskUri(target), exception); @@ -1214,6 +1229,9 @@ public final class XtreamPlayerApplication { response.headers().firstValue("Content-Length") .ifPresent(value -> exchange.getResponseHeaders().set("Content-Length", value)); + // Add CORS headers for media segments + addCorsHeaders(exchange); + long responseLength = parseContentLength(response.headers().firstValue("Content-Length").orElse("")); exchange.sendResponseHeaders(status, responseLength >= 0 ? responseLength : 0); long sent = 0; @@ -1306,11 +1324,18 @@ public final class XtreamPlayerApplication { .header("User-Agent", firstNonBlank( exchange.getRequestHeaders().getFirst("User-Agent"), DEFAULT_BROWSER_UA - )) - .header("Accept", firstNonBlank( - exchange.getRequestHeaders().getFirst("Accept"), - "*/*" )); + + // Auto-detect Accept header based on URL + String acceptHeader = exchange.getRequestHeaders().getFirst("Accept"); + if (acceptHeader == null || acceptHeader.equals("*/*")) { + String path = candidate.getPath() == null ? "" : candidate.getPath().toLowerCase(Locale.ROOT); + if (path.endsWith(".m3u8") || path.contains("m3u8?")) { + acceptHeader = "application/vnd.apple.mpegurl,application/x-mpegurl,*/*"; + } + } + requestBuilder.header("Accept", firstNonBlank(acceptHeader, "*/*")); + copyRequestHeaderIfPresent(exchange, requestBuilder, "Range"); copyRequestHeaderIfPresent(exchange, requestBuilder, "If-Range"); copyRequestHeaderIfPresent(exchange, requestBuilder, "Accept-Encoding"); @@ -1513,6 +1538,22 @@ public final class XtreamPlayerApplication { private static void writeBytes(HttpExchange exchange, int statusCode, byte[] bytes, String contentType) throws IOException { addCorsHeaders(exchange); + + // Debug CORS headers + String corsOrigin = exchange.getResponseHeaders().getFirst("Access-Control-Allow-Origin"); + String requestMethod = exchange.getRequestMethod(); + String requestPath = exchange.getRequestURI().getPath(); + if (requestPath.contains(".ts") || requestPath.contains(".m4s") || requestPath.contains(".m3u8")) { + LOGGER.info( + "writeBytes response: method={} path={} statusCode={} contentType={} CORS-Origin={}", + requestMethod, + requestPath, + statusCode, + contentType, + corsOrigin + ); + } + exchange.sendResponseHeaders(statusCode, bytes.length); exchange.getResponseBody().write(bytes); logApiResponse(exchange, statusCode, bytes.length, contentType); diff --git a/src/main/resources/web/assets/app.js b/src/main/resources/web/assets/app.js index 8d1af0c..86a9ab5 100644 --- a/src/main/resources/web/assets/app.js +++ b/src/main/resources/web/assets/app.js @@ -2439,9 +2439,11 @@ } function setPlayer(title, url, info = {}) { + console.log(`Playing stream: ${title}`, {url, info}); el.playerTitle.textContent = title || "Player"; const systemPlayerUrl = buildSystemPlayerHref(title, url, info); const playbackUrl = buildBrowserPlaybackUrl(url); + console.log(`Playback URL: ${playbackUrl}`); el.openDirect.dataset.url = url; el.openDirect.disabled = !url; el.openSystemPlayer.dataset.url = systemPlayerUrl; @@ -2473,15 +2475,31 @@ if (isLikelyHls(playbackUrl) && shouldUseHlsJs()) { state.currentStreamInfo.playbackEngine = "hls.js"; renderStreamInfo(); + console.log("Initializing HLS.js", {playbackUrl}); hlsInstance = new window.Hls({ + debug: false, enableWorker: true, - lowLatencyMode: true + lowLatencyMode: false, + autoStartLoad: true }); hlsInstance.on(window.Hls.Events.MANIFEST_PARSED, () => { + console.log("Manifest parsed", { + autoStartLoad: hlsInstance.autostart, + levels: hlsInstance.levels, + audioTracks: hlsInstance.audioTracks, + subtitleTracks: hlsInstance.subtitleTracks + }); hlsSubtitleTracks = Array.isArray(hlsInstance?.subtitleTracks) ? hlsInstance.subtitleTracks : []; refreshEmbeddedSubtitleTracks(); + + // Start segment loading + console.log("Starting HLS segment loading..."); + hlsInstance.startLoad(); + + // Wait for canplay event to have enough data before playing + console.log("Waiting for canplay event..."); }); hlsInstance.on(window.Hls.Events.SUBTITLE_TRACKS_UPDATED, (_event, data) => { hlsSubtitleTracks = Array.isArray(data?.subtitleTracks) @@ -2489,16 +2507,102 @@ : (Array.isArray(hlsInstance?.subtitleTracks) ? hlsInstance.subtitleTracks : []); refreshEmbeddedSubtitleTracks(); }); - hlsInstance.loadSource(playbackUrl); hlsInstance.attachMedia(el.player); + console.log("HLS.js attached to video element", { + videoReady: el.player.readyState, + isInDOM: document.contains(el.player), + tagName: el.player.tagName, + id: el.player.id, + src: el.player.src, + srcObject: !!el.player.srcObject + }); + + // Add event listeners for debugging + hlsInstance.on(window.Hls.Events.LEVEL_SWITCHING, (_event, data) => { + console.log("Level switching:", data); + }); + hlsInstance.on(window.Hls.Events.FRAG_LOADING, (_event, data) => { + console.log("Fragment loading:", data?.frag?.url?.substring(0, 60)); + }); + hlsInstance.on(window.Hls.Events.FRAG_LOADED, (_event, data) => { + console.log("Fragment loaded, size:", data?.frag?.stats?.total, "bytes"); + }); + hlsInstance.on(window.Hls.Events.FRAG_BUFFERED, (_event, data) => { + console.log("Fragment BUFFERED:", { + start: data?.frag?.start, + duration: data?.frag?.duration, + bufferEnd: data?.stats?.bufferEnd + }); + }); + hlsInstance.on(window.Hls.Events.BUFFER_APPENDING, (_event, data) => { + console.log("Buffer appending"); + }); + hlsInstance.on(window.Hls.Events.BUFFER_APPENDED, (_event, data) => { + console.log("Buffer APPENDED, length:", el.player.buffered?.length, { + timeRange0: el.player.buffered?.length > 0 ? {start: el.player.buffered.start(0), end: el.player.buffered.end(0)} : null, + readyState: el.player.readyState, + networkState: el.player.networkState, + paused: el.player.paused + }); + }); + + hlsInstance.loadSource(playbackUrl); + console.log("HLS.js loadSource called"); + + // Video element event listeners for debugging + let playAttempted = false; + el.player.addEventListener('play', () => console.log("Video: play event")); + el.player.addEventListener('playing', () => console.log("🎬 Video: PLAYING event - video is now playing!")); + el.player.addEventListener('pause', () => console.log("Video: pause event")); + el.player.addEventListener('seeking', () => console.log("Video: seeking event")); + el.player.addEventListener('seeked', () => console.log("Video: seeked event")); + el.player.addEventListener('loadstart', () => console.log("Video: loadstart event")); + el.player.addEventListener('progress', () => console.log("Video: progress event, buffered:", el.player.buffered.length)); + el.player.addEventListener('durationchange', () => console.log("Video: durationchange, duration:", el.player.duration)); + el.player.addEventListener('loadedmetadata', () => console.log("Video: loadedmetadata event")); + el.player.addEventListener('canplay', () => { + console.log("✓ Video: canplay event - enough data to play"); + // Try to play when we have enough data + if (!playAttempted) { + playAttempted = true; + console.log("Attempting to play on canplay event..."); + const playPromise = el.player.play(); + if (playPromise !== undefined) { + playPromise.then(() => { + console.log("✓ Playback started on canplay!"); + }).catch((err) => { + console.error("Play failed on canplay:", err.name, err.message); + }); + } + } + }); + el.player.addEventListener('canplaythrough', () => console.log("✓✓ Video: canplaythrough event - enough data to play through")); + el.player.addEventListener('error', (e) => { + const error = el.player.error; + console.error("Video: error event", { + code: error?.code, + message: error?.message, + type: ['MEDIA_ERR_ABORTED', 'MEDIA_ERR_NETWORK', 'MEDIA_ERR_DECODE', 'MEDIA_ERR_SRC_NOT_SUPPORTED'][error?.code - 1] + }); + }); hlsInstance.on(window.Hls.Events.ERROR, (_event, data) => { + console.error("HLS.js error:", { + type: data?.type, + details: data?.details, + fatal: data?.fatal, + error: data?.error?.message, + fullData: data + }); if (!data?.fatal) { + console.warn("Non-fatal HLS error, continuing playback"); return; } disposeHls(); const systemPlayerUrl = buildSystemPlayerHref(state.currentStreamInfo?.title, state.currentStreamInfo?.url, state.currentStreamInfo); + const errorDetails = data?.details ? ` (${data.details})` : ""; + const errorType = data?.type ? `${data.type}: ` : ""; setSettingsMessageHtml( - `HLS playback failed in embedded player. Open stream directly`, + `HLS playback failed${errorDetails}. Open stream directly`, "err" ); if (state.currentStreamInfo) { @@ -2551,8 +2655,18 @@ return url; } try { - const pageIsHttps = window.location.protocol === "https:"; const target = new URL(url, window.location.href); + const targetPath = target.pathname.toLowerCase(); + + // Always proxy HLS playlists through our backend for proper headers + if (targetPath.endsWith(".m3u8") || url.includes("m3u8?") || url.includes("type=m3u8")) { + const authToken = getAuthToken(); + const tokenParam = authToken ? `&token=${encodeURIComponent(authToken)}` : ""; + return `/api/stream-proxy?url=${encodeURIComponent(target.toString())}${tokenParam}`; + } + + // Proxy HTTP->HTTPS mixed content for security + const pageIsHttps = window.location.protocol === "https:"; if (pageIsHttps && target.protocol === "http:") { const authToken = getAuthToken(); const tokenParam = authToken ? `&token=${encodeURIComponent(authToken)}` : "";