update
This commit is contained in:
@@ -210,6 +210,128 @@ app.get('/epg.xml', async (req, res) => {
|
||||
res.send(xml);
|
||||
});
|
||||
|
||||
// Helper: parse, filter, and verify playability of variants from the master playlist.
|
||||
// Keeps the stream alive even if 1080p (or other qualities) return 403 Forbidden due to cookie issues.
|
||||
const filterMasterPlaylist = async (m3u8Data, m3u8Url) => {
|
||||
try {
|
||||
const lines = m3u8Data.split('\n');
|
||||
const variants = [];
|
||||
let currentHeaders = [];
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
if (!line) continue;
|
||||
|
||||
if (line.startsWith('#')) {
|
||||
if (line.startsWith('#EXT-X-STREAM-INF')) {
|
||||
currentHeaders.push(line);
|
||||
} else if (line.startsWith('#EXTM3U') || line.startsWith('#EXT-X-VERSION') || line.startsWith('#EXT-X-INDEPENDENT-SEGMENTS')) {
|
||||
// Skip global headers as we add them manually
|
||||
} else {
|
||||
currentHeaders.push(line);
|
||||
}
|
||||
} else {
|
||||
let uri = line;
|
||||
if (!uri.startsWith('http')) {
|
||||
uri = new URL(uri, m3u8Url).toString();
|
||||
}
|
||||
variants.push({
|
||||
headers: currentHeaders,
|
||||
uri: uri
|
||||
});
|
||||
currentHeaders = [];
|
||||
}
|
||||
}
|
||||
|
||||
if (variants.length === 0) return m3u8Data;
|
||||
|
||||
// Test playability of each variant in parallel
|
||||
const checkedVariants = await Promise.all(
|
||||
variants.map(async (v) => {
|
||||
try {
|
||||
await axios.get(v.uri, {
|
||||
headers: buildHeaders(),
|
||||
timeout: 1500,
|
||||
responseType: 'stream'
|
||||
});
|
||||
return { ...v, playable: true };
|
||||
} catch (err) {
|
||||
console.log(`[Proxy] Quality variant not playable: ${v.uri} (status: ${err.response?.status || 'error'})`);
|
||||
return { ...v, playable: false };
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const playableVariants = checkedVariants.filter(v => v.playable);
|
||||
const finalVariants = playableVariants.length > 0 ? playableVariants : variants;
|
||||
|
||||
let output = '#EXTM3U\n#EXT-X-VERSION:7\n#EXT-X-INDEPENDENT-SEGMENTS\n';
|
||||
finalVariants.forEach(v => {
|
||||
v.headers.forEach(h => {
|
||||
output += h + '\n';
|
||||
});
|
||||
output += v.uri + '\n';
|
||||
});
|
||||
|
||||
return output;
|
||||
} catch (error) {
|
||||
console.error('Error filtering master playlist:', error.message);
|
||||
return m3u8Data;
|
||||
}
|
||||
};
|
||||
|
||||
// Helper: processes an M3U8 playlist by converting any relative URLs to absolute,
|
||||
// proxying sub-playlists (.m3u8), and proxying decryption keys.
|
||||
const processM3U8 = (m3u8Data, m3u8Url, baseUrl) => {
|
||||
const lines = m3u8Data.split('\n');
|
||||
const processedLines = lines.map(line => {
|
||||
let processed = line.trim();
|
||||
if (!processed) return processed;
|
||||
|
||||
// 1. Rewrite AES Key URIs to use our proxy (making them absolute first if needed)
|
||||
if (processed.startsWith('#EXT-X-KEY:')) {
|
||||
processed = processed.replace(/URI="([^"]+)"/, (match, keyUrl) => {
|
||||
let absoluteKeyUrl = keyUrl;
|
||||
if (!keyUrl.startsWith('http')) {
|
||||
absoluteKeyUrl = new URL(keyUrl, m3u8Url).toString();
|
||||
}
|
||||
return `URI="${baseUrl}/key?url=${encodeURIComponent(absoluteKeyUrl)}"`;
|
||||
});
|
||||
return processed;
|
||||
}
|
||||
|
||||
// 2. Rewrite other tag URIs (like #EXT-X-MEDIA, #EXT-X-MAP) to be absolute
|
||||
if (processed.startsWith('#') && processed.includes('URI=')) {
|
||||
processed = processed.replace(/URI="([^"]+)"/g, (match, uri) => {
|
||||
if (uri && !uri.startsWith('http')) {
|
||||
uri = new URL(uri, m3u8Url).toString();
|
||||
}
|
||||
if (uri.includes('.m3u8') && !uri.includes('key?url=')) {
|
||||
uri = `${baseUrl}/m3u8?url=${encodeURIComponent(uri)}`;
|
||||
}
|
||||
return `URI="${uri}"`;
|
||||
});
|
||||
return processed;
|
||||
}
|
||||
|
||||
// 3. Rewrite main segment/variant playlist URLs to be absolute (and proxy variant playlists)
|
||||
if (!processed.startsWith('#')) {
|
||||
let uri = processed;
|
||||
if (!uri.startsWith('http')) {
|
||||
uri = new URL(uri, m3u8Url).toString();
|
||||
}
|
||||
if (uri.includes('.m3u8')) {
|
||||
uri = `${baseUrl}/m3u8?url=${encodeURIComponent(uri)}`;
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
return processed;
|
||||
});
|
||||
|
||||
return processedLines.join('\n');
|
||||
};
|
||||
|
||||
app.get('/stream/:channelId', async (req, res) => {
|
||||
const { channelId } = req.params;
|
||||
|
||||
@@ -228,8 +350,6 @@ app.get('/stream/:channelId', async (req, res) => {
|
||||
const hlsMedia = media.find(m => m.mediaId === 'HLS' || m.mediaId === 'LLHLS');
|
||||
|
||||
if (hlsMedia && hlsMedia.path) {
|
||||
// Serve HLS master playlist directly (no redirect) so ffmpeg/Jellyfin
|
||||
// doesn't need to follow a redirect chain which can cause code 8 errors
|
||||
const host = req.get('host');
|
||||
const protocol = req.protocol;
|
||||
const baseUrl = `${protocol}://${host}`;
|
||||
@@ -238,34 +358,13 @@ app.get('/stream/:channelId', async (req, res) => {
|
||||
const m3u8Res = await axios.get(m3u8Url);
|
||||
let m3u8Data = m3u8Res.data;
|
||||
|
||||
const lines = m3u8Data.split('\n');
|
||||
const processedLines = lines.map(line => {
|
||||
let processed = line.trim();
|
||||
if (!processed) return processed;
|
||||
// Filter out unplayable variants (e.g. 1080p returning 403) before returning the playlist to Jellyfin
|
||||
m3u8Data = await filterMasterPlaylist(m3u8Data, m3u8Url);
|
||||
|
||||
if (processed.startsWith('#EXT-X-KEY:')) {
|
||||
processed = processed.replace(/URI="(https?:\/\/[^"]+)"/, (match, keyUrl) => {
|
||||
return `URI="${baseUrl}/key?url=${encodeURIComponent(keyUrl)}"`;
|
||||
});
|
||||
return processed;
|
||||
}
|
||||
|
||||
if (!processed.startsWith('#')) {
|
||||
let uri = processed;
|
||||
if (!uri.startsWith('http')) {
|
||||
uri = new URL(uri, m3u8Url).toString();
|
||||
}
|
||||
if (uri.includes('.m3u8')) {
|
||||
uri = `${baseUrl}/m3u8?url=${encodeURIComponent(uri)}`;
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
return processed;
|
||||
});
|
||||
const processedM3U8 = processM3U8(m3u8Data, m3u8Url, baseUrl);
|
||||
|
||||
res.header('Content-Type', 'application/vnd.apple.mpegurl');
|
||||
res.send(processedLines.join('\n'));
|
||||
res.send(processedM3U8);
|
||||
} else {
|
||||
res.status(404).send('HLS stream not found in playback info');
|
||||
}
|
||||
@@ -286,52 +385,10 @@ app.get('/m3u8', async (req, res) => {
|
||||
const protocol = req.protocol;
|
||||
const baseUrl = `${protocol}://${host}`;
|
||||
|
||||
// Process m3u8 lines
|
||||
const lines = m3u8Data.split('\n');
|
||||
const processedLines = lines.map(line => {
|
||||
let processed = line.trim();
|
||||
if (!processed) return processed;
|
||||
|
||||
// Rewrite AES Key URIs to use our proxy
|
||||
if (processed.startsWith('#EXT-X-KEY:')) {
|
||||
processed = processed.replace(/URI="(https?:\/\/[^"]+)"/, (match, keyUrl) => {
|
||||
return `URI="${baseUrl}/key?url=${encodeURIComponent(keyUrl)}"`;
|
||||
});
|
||||
return processed;
|
||||
}
|
||||
|
||||
// If it's a tag with a URI (like #EXT-X-MEDIA:URI="..."), make it absolute and proxy it if it's an m3u8
|
||||
if (processed.startsWith('#EXT-X-MEDIA:') && processed.includes('URI=')) {
|
||||
processed = processed.replace(/URI="(.*?)"/, (match, uri) => {
|
||||
if (uri && !uri.startsWith('http')) {
|
||||
uri = new URL(uri, m3u8Url).toString();
|
||||
}
|
||||
if (uri.includes('.m3u8')) {
|
||||
uri = `${baseUrl}/m3u8?url=${encodeURIComponent(uri)}`;
|
||||
}
|
||||
return `URI="${uri}"`;
|
||||
});
|
||||
return processed;
|
||||
}
|
||||
|
||||
// If it's a URL (variant playlist or segment)
|
||||
if (!processed.startsWith('#')) {
|
||||
let uri = processed;
|
||||
if (!uri.startsWith('http')) {
|
||||
uri = new URL(uri, m3u8Url).toString();
|
||||
}
|
||||
// If it's a variant playlist, proxy it
|
||||
if (uri.includes('.m3u8')) {
|
||||
uri = `${baseUrl}/m3u8?url=${encodeURIComponent(uri)}`;
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
return processed;
|
||||
});
|
||||
const processedM3U8 = processM3U8(m3u8Data, m3u8Url, baseUrl);
|
||||
|
||||
res.header('Content-Type', 'application/vnd.apple.mpegurl');
|
||||
res.send(processedLines.join('\n'));
|
||||
res.send(processedM3U8);
|
||||
} catch (error) {
|
||||
console.error(`Error proxying m3u8: ${error.message}`);
|
||||
res.status(500).send('Error proxying m3u8');
|
||||
|
||||
Reference in New Issue
Block a user