handle application/vnd.apple.mpegurl.

This commit is contained in:
Simon
2026-02-08 15:23:01 +00:00
parent b9f49530e4
commit c67a5cde16
2 changed files with 232 additions and 80 deletions

View File

@@ -4,16 +4,23 @@ const perPage = 12;
const renderedVideoIds = new Set();
let hasNextPage = true;
let isLoading = false;
let hlsPlayer = null;
// 2. Observer Definition (Must be defined before initApp uses it)
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) loadVideos();
}, { threshold: 1.0 });
}, {
threshold: 1.0
});
// 3. Logic Functions
async function InitializeLocalStorage() {
if (!localStorage.getItem('config')) {
localStorage.setItem('config', JSON.stringify({ servers: [{ "https://getfigleaf.com": {} }] }));
localStorage.setItem('config', JSON.stringify({
servers: [{
"https://getfigleaf.com": {}
}]
}));
}
if (!localStorage.getItem('theme')) {
localStorage.setItem('theme', 'dark');
@@ -31,13 +38,20 @@ async function InitializeServerStatus() {
try {
const response = await fetch(`/api/status`, {
method: "POST",
body: JSON.stringify({ server: server }),
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
server: server
}),
headers: {
"Content-Type": "application/json"
},
});
const status = await response.json();
serverObj[server] = status;
} catch (err) {
serverObj[server] = { online: false, channels: [] };
serverObj[server] = {
online: false,
channels: []
};
}
});
@@ -50,7 +64,7 @@ async function InitializeServerStatus() {
if (serverData.channels && serverData.channels.length > 0) {
const channel = serverData.channels[0];
let options = {};
if (channel.options) {
channel.options.forEach(element => {
// Ensure the options structure matches your API expectations
@@ -74,7 +88,7 @@ async function InitializeServerStatus() {
async function loadVideos() {
const session = JSON.parse(localStorage.getItem('session'));
if (!session) return;
if (!session) return;
if (isLoading || !hasNextPage) return;
const searchInput = document.getElementById('search-input');
@@ -92,7 +106,7 @@ async function loadVideos() {
// Correct way to loop through the options object
Object.entries(session.options).forEach(([key, value]) => {
if (Array.isArray(value)) {
body[key] = value.map((entry) => entry.id);
body[key] = value.map((entry) => entry.id).join(", ");
} else if (value && value.id) {
body[key] = value.id;
}
@@ -102,7 +116,9 @@ async function loadVideos() {
isLoading = true;
const response = await fetch('/api/videos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
const videos = await response.json();
@@ -124,7 +140,7 @@ function renderVideos(videos) {
const items = videos && Array.isArray(videos.items) ? videos.items : [];
items.forEach(v => {
if (renderedVideoIds.has(v.id)) return;
const card = document.createElement('div');
card.className = 'video-card';
const durationText = v.duration === 0 ? '' : `${v.duration}s`;
@@ -144,10 +160,10 @@ async function initApp() {
// Clear old data if you want a fresh start every refresh
// localStorage.clear();
await InitializeLocalStorage();
await InitializeLocalStorage();
applyTheme();
renderMenu();
const sentinel = document.getElementById('sentinel');
if (sentinel) {
observer.observe(sentinel);
@@ -163,10 +179,57 @@ function applyTheme() {
if (select) select.value = theme;
}
function openPlayer(url) {
async function openPlayer(url) {
const modal = document.getElementById('video-modal');
const video = document.getElementById('player');
video.src = `/api/stream?url=${encodeURIComponent(url)}`;
// 1. Define isHls (the missing piece!)
const streamUrl = `/api/stream?url=${encodeURIComponent(url)}`;
let isHls = /\.m3u8($|\?)/i.test(url);
// 2. Cleanup existing player instance to prevent aborted bindings
if (hlsPlayer) {
hlsPlayer.stopLoad();
hlsPlayer.detachMedia();
hlsPlayer.destroy();
hlsPlayer = null;
}
// 3. Reset the video element
video.pause();
video.removeAttribute('src');
video.load();
if (!isHls) {
try {
const headResp = await fetch(streamUrl, { method: 'HEAD' });
const contentType = headResp.headers.get('Content-Type') || '';
if (contentType.includes('application/vnd.apple.mpegurl')) {
isHls = true;
}
} catch (err) {
console.warn('Failed to detect stream type', err);
}
}
if (isHls) {
if (window.Hls && window.Hls.isSupported()) {
hlsPlayer = new window.Hls();
hlsPlayer.loadSource(streamUrl);
hlsPlayer.attachMedia(video);
hlsPlayer.on(window.Hls.Events.MANIFEST_PARSED, function() {
video.play();
});
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = streamUrl;
} else {
console.error("HLS not supported in this browser.");
return;
}
} else {
video.src = streamUrl;
}
modal.style.display = 'flex';
document.body.style.overflow = 'hidden';
}
@@ -174,6 +237,10 @@ function openPlayer(url) {
function closePlayer() {
const modal = document.getElementById('video-modal');
const video = document.getElementById('player');
if (hlsPlayer) {
hlsPlayer.destroy();
hlsPlayer = null;
}
video.pause();
video.src = '';
modal.style.display = 'none';
@@ -190,7 +257,9 @@ function handleSearch(value) {
}
function getConfig() {
return JSON.parse(localStorage.getItem('config')) || { servers: [] };
return JSON.parse(localStorage.getItem('config')) || {
servers: []
};
}
function getSession() {
@@ -206,7 +275,10 @@ function getServerEntries() {
if (!config.servers || !Array.isArray(config.servers)) return [];
return config.servers.map((serverObj) => {
const server = Object.keys(serverObj)[0];
return { url: server, data: serverObj[server] || null };
return {
url: server,
data: serverObj[server] || null
};
});
}
@@ -280,9 +352,9 @@ function renderMenu() {
sourceSelect.onchange = () => {
const selectedServerUrl = sourceSelect.value;
const selectedServer = serverEntries.find((entry) => entry.url === selectedServerUrl);
const channels = selectedServer && selectedServer.data && selectedServer.data.channels
? selectedServer.data.channels
: [];
const channels = selectedServer && selectedServer.data && selectedServer.data.channels ?
selectedServer.data.channels :
[];
const nextChannel = channels.length > 0 ? channels[0] : null;
const nextSession = {
server: selectedServerUrl,
@@ -295,9 +367,9 @@ function renderMenu() {
};
const activeServer = serverEntries.find((entry) => entry.url === (session && session.server));
const availableChannels = activeServer && activeServer.data && activeServer.data.channels
? activeServer.data.channels
: [];
const availableChannels = activeServer && activeServer.data && activeServer.data.channels ?
activeServer.data.channels :
[];
channelSelect.innerHTML = "";
availableChannels.forEach((channel) => {
@@ -389,7 +461,9 @@ function renderMenu() {
const exists = (config.servers || []).some((serverObj) => Object.keys(serverObj)[0] === normalized);
if (!exists) {
config.servers = config.servers || [];
config.servers.push({ [normalized]: {} });
config.servers.push({
[normalized]: {}
});
setConfig(config);
sourceInput.value = '';
await refreshServerStatus();
@@ -398,9 +472,9 @@ function renderMenu() {
if (!session || session.server !== normalized) {
const entries = getServerEntries();
const addedEntry = entries.find((entry) => entry.url === normalized);
const nextChannel = addedEntry && addedEntry.data && addedEntry.data.channels
? addedEntry.data.channels[0]
: null;
const nextChannel = addedEntry && addedEntry.data && addedEntry.data.channels ?
addedEntry.data.channels[0] :
null;
setSession({
server: normalized,
channel: nextChannel,