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>
|
<name>xtream-player</name>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<maven.compiler.release>17</maven.compiler.release>
|
<maven.compiler.release>21</maven.compiler.release>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
|
|||||||
@ -409,6 +409,9 @@ public final class XtreamPlayerApplication {
|
|||||||
if (isHlsPlaylist(playlistBaseUri, contentType)) {
|
if (isHlsPlaylist(playlistBaseUri, contentType)) {
|
||||||
String rewritten = rewritePlaylistForProxy(playlistBaseUri, body);
|
String rewritten = rewritePlaylistForProxy(playlistBaseUri, body);
|
||||||
exchange.getResponseHeaders().set("Content-Type", "application/vnd.apple.mpegurl; charset=utf-8");
|
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),
|
writeBytes(exchange, response.statusCode(), rewritten.getBytes(StandardCharsets.UTF_8),
|
||||||
"application/vnd.apple.mpegurl; charset=utf-8");
|
"application/vnd.apple.mpegurl; charset=utf-8");
|
||||||
return;
|
return;
|
||||||
@ -419,6 +422,18 @@ public final class XtreamPlayerApplication {
|
|||||||
copyResponseHeaderIfPresent(response, exchange, "Cache-Control");
|
copyResponseHeaderIfPresent(response, exchange, "Cache-Control");
|
||||||
copyResponseHeaderIfPresent(response, exchange, "Expires");
|
copyResponseHeaderIfPresent(response, exchange, "Expires");
|
||||||
exchange.getResponseHeaders().set("Content-Type", contentType);
|
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);
|
writeBytes(exchange, response.statusCode(), body, contentType);
|
||||||
} catch (Exception exception) {
|
} catch (Exception exception) {
|
||||||
LOGGER.error("Stream proxy failed for {}", maskUri(target), exception);
|
LOGGER.error("Stream proxy failed for {}", maskUri(target), exception);
|
||||||
@ -1214,6 +1229,9 @@ public final class XtreamPlayerApplication {
|
|||||||
response.headers().firstValue("Content-Length")
|
response.headers().firstValue("Content-Length")
|
||||||
.ifPresent(value -> exchange.getResponseHeaders().set("Content-Length", value));
|
.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(""));
|
long responseLength = parseContentLength(response.headers().firstValue("Content-Length").orElse(""));
|
||||||
exchange.sendResponseHeaders(status, responseLength >= 0 ? responseLength : 0);
|
exchange.sendResponseHeaders(status, responseLength >= 0 ? responseLength : 0);
|
||||||
long sent = 0;
|
long sent = 0;
|
||||||
@ -1306,11 +1324,18 @@ public final class XtreamPlayerApplication {
|
|||||||
.header("User-Agent", firstNonBlank(
|
.header("User-Agent", firstNonBlank(
|
||||||
exchange.getRequestHeaders().getFirst("User-Agent"),
|
exchange.getRequestHeaders().getFirst("User-Agent"),
|
||||||
DEFAULT_BROWSER_UA
|
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, "Range");
|
||||||
copyRequestHeaderIfPresent(exchange, requestBuilder, "If-Range");
|
copyRequestHeaderIfPresent(exchange, requestBuilder, "If-Range");
|
||||||
copyRequestHeaderIfPresent(exchange, requestBuilder, "Accept-Encoding");
|
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 {
|
private static void writeBytes(HttpExchange exchange, int statusCode, byte[] bytes, String contentType) throws IOException {
|
||||||
addCorsHeaders(exchange);
|
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.sendResponseHeaders(statusCode, bytes.length);
|
||||||
exchange.getResponseBody().write(bytes);
|
exchange.getResponseBody().write(bytes);
|
||||||
logApiResponse(exchange, statusCode, bytes.length, contentType);
|
logApiResponse(exchange, statusCode, bytes.length, contentType);
|
||||||
|
|||||||
@ -2439,9 +2439,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setPlayer(title, url, info = {}) {
|
function setPlayer(title, url, info = {}) {
|
||||||
|
console.log(`Playing stream: ${title}`, {url, info});
|
||||||
el.playerTitle.textContent = title || "Player";
|
el.playerTitle.textContent = title || "Player";
|
||||||
const systemPlayerUrl = buildSystemPlayerHref(title, url, info);
|
const systemPlayerUrl = buildSystemPlayerHref(title, url, info);
|
||||||
const playbackUrl = buildBrowserPlaybackUrl(url);
|
const playbackUrl = buildBrowserPlaybackUrl(url);
|
||||||
|
console.log(`Playback URL: ${playbackUrl}`);
|
||||||
el.openDirect.dataset.url = url;
|
el.openDirect.dataset.url = url;
|
||||||
el.openDirect.disabled = !url;
|
el.openDirect.disabled = !url;
|
||||||
el.openSystemPlayer.dataset.url = systemPlayerUrl;
|
el.openSystemPlayer.dataset.url = systemPlayerUrl;
|
||||||
@ -2473,15 +2475,31 @@
|
|||||||
if (isLikelyHls(playbackUrl) && shouldUseHlsJs()) {
|
if (isLikelyHls(playbackUrl) && shouldUseHlsJs()) {
|
||||||
state.currentStreamInfo.playbackEngine = "hls.js";
|
state.currentStreamInfo.playbackEngine = "hls.js";
|
||||||
renderStreamInfo();
|
renderStreamInfo();
|
||||||
|
console.log("Initializing HLS.js", {playbackUrl});
|
||||||
hlsInstance = new window.Hls({
|
hlsInstance = new window.Hls({
|
||||||
|
debug: false,
|
||||||
enableWorker: true,
|
enableWorker: true,
|
||||||
lowLatencyMode: true
|
lowLatencyMode: false,
|
||||||
|
autoStartLoad: true
|
||||||
});
|
});
|
||||||
hlsInstance.on(window.Hls.Events.MANIFEST_PARSED, () => {
|
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)
|
hlsSubtitleTracks = Array.isArray(hlsInstance?.subtitleTracks)
|
||||||
? hlsInstance.subtitleTracks
|
? hlsInstance.subtitleTracks
|
||||||
: [];
|
: [];
|
||||||
refreshEmbeddedSubtitleTracks();
|
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) => {
|
hlsInstance.on(window.Hls.Events.SUBTITLE_TRACKS_UPDATED, (_event, data) => {
|
||||||
hlsSubtitleTracks = Array.isArray(data?.subtitleTracks)
|
hlsSubtitleTracks = Array.isArray(data?.subtitleTracks)
|
||||||
@ -2489,16 +2507,102 @@
|
|||||||
: (Array.isArray(hlsInstance?.subtitleTracks) ? hlsInstance.subtitleTracks : []);
|
: (Array.isArray(hlsInstance?.subtitleTracks) ? hlsInstance.subtitleTracks : []);
|
||||||
refreshEmbeddedSubtitleTracks();
|
refreshEmbeddedSubtitleTracks();
|
||||||
});
|
});
|
||||||
hlsInstance.loadSource(playbackUrl);
|
|
||||||
hlsInstance.attachMedia(el.player);
|
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) => {
|
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) {
|
if (!data?.fatal) {
|
||||||
|
console.warn("Non-fatal HLS error, continuing playback");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
disposeHls();
|
disposeHls();
|
||||||
const systemPlayerUrl = buildSystemPlayerHref(state.currentStreamInfo?.title, state.currentStreamInfo?.url, state.currentStreamInfo);
|
const systemPlayerUrl = buildSystemPlayerHref(state.currentStreamInfo?.title, state.currentStreamInfo?.url, state.currentStreamInfo);
|
||||||
|
const errorDetails = data?.details ? ` (${data.details})` : "";
|
||||||
|
const errorType = data?.type ? `${data.type}: ` : "";
|
||||||
setSettingsMessageHtml(
|
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"
|
"err"
|
||||||
);
|
);
|
||||||
if (state.currentStreamInfo) {
|
if (state.currentStreamInfo) {
|
||||||
@ -2551,8 +2655,18 @@
|
|||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const pageIsHttps = window.location.protocol === "https:";
|
|
||||||
const target = new URL(url, window.location.href);
|
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:") {
|
if (pageIsHttps && target.protocol === "http:") {
|
||||||
const authToken = getAuthToken();
|
const authToken = getAuthToken();
|
||||||
const tokenParam = authToken ? `&token=${encodeURIComponent(authToken)}` : "";
|
const tokenParam = authToken ? `&token=${encodeURIComponent(authToken)}` : "";
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user