some hls optimizations
This commit is contained in:
parent
691e428240
commit
ddf58833da
2
pom.xml
2
pom.xml
@ -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>
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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)}` : "";
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user