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)}` : "";