some hls optimizations

This commit is contained in:
Radek Davidek 2026-03-10 17:18:16 +01:00
parent 691e428240
commit ddf58833da
3 changed files with 164 additions and 9 deletions

View File

@ -9,7 +9,7 @@
<name>xtream-player</name>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

View File

@ -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);

View File

@ -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. <a href="${systemPlayerUrl}" target="_blank" style="color: inherit; text-decoration: underline;">Open stream directly</a>`,
`HLS playback failed${errorDetails}. <a href="${systemPlayerUrl}" target="_blank" style="color: inherit; text-decoration: underline;">Open stream directly</a>`,
"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)}` : "";