diff --git a/index.js b/index.js index f2c59ef..82495d5 100644 --- a/index.js +++ b/index.js @@ -325,7 +325,8 @@ const filterMasterPlaylist = async (m3u8Data, m3u8Url) => { }; // Helper: processes an M3U8 playlist by converting any relative URLs to absolute, -// proxying sub-playlists (.m3u8), and proxying decryption keys. +// proxying sub-playlists (.m3u8), proxying decryption keys, and proxying segment files (.m4s, .m4v) +// to bypass FFmpeg's strict extension security checks. const processM3U8 = (m3u8Data, m3u8Url, baseUrl) => { const lines = m3u8Data.split('\n'); const processedLines = lines.map(line => { @@ -352,13 +353,17 @@ const processM3U8 = (m3u8Data, m3u8Url, baseUrl) => { } if (uri.includes('.m3u8') && !uri.includes('key?url=')) { uri = `${baseUrl}/m3u8?url=${encodeURIComponent(uri)}`; + } else if (uri.includes('.m4s') || uri.includes('.m4v')) { + // Proxy fragmented MP4 files to bypass FFmpeg's strict extension checks + const filename = uri.split('/').pop().split('?')[0].replace(/\.(m4s|m4v)$/, '.mp4'); + uri = `${baseUrl}/segment/${filename}?url=${encodeURIComponent(uri)}`; } return `URI="${uri}"`; }); return processed; } - // 3. Rewrite main segment/variant playlist URLs to be absolute (and proxy variant playlists) + // 3. Rewrite main segment/variant playlist URLs to be absolute (and proxy variant playlists/segments) if (!processed.startsWith('#')) { let uri = processed; if (!uri.startsWith('http')) { @@ -366,6 +371,10 @@ const processM3U8 = (m3u8Data, m3u8Url, baseUrl) => { } if (uri.includes('.m3u8')) { uri = `${baseUrl}/m3u8?url=${encodeURIComponent(uri)}`; + } else if (uri.includes('.m4s') || uri.includes('.m4v')) { + // Proxy fragmented MP4 files to bypass FFmpeg's strict extension checks + const filename = uri.split('/').pop().split('?')[0].replace(/\.(m4s|m4v)$/, '.mp4'); + uri = `${baseUrl}/segment/${filename}?url=${encodeURIComponent(uri)}`; } return uri; } @@ -433,6 +442,41 @@ app.get('/m3u8', async (req, res) => { } }); +app.get('/segment/:filename', async (req, res) => { + const segmentUrl = req.query.url; + if (!segmentUrl) return res.status(400).send('Missing url parameter'); + + try { + const response = await axios.get(segmentUrl, { + headers: buildHeaders(), + responseType: 'stream', + timeout: 10000 + }); + + if (response.headers['content-type']) { + res.header('Content-Type', response.headers['content-type']); + } else { + res.header('Content-Type', 'video/mp4'); + } + if (response.headers['content-length']) { + res.header('Content-Length', response.headers['content-length']); + } + + response.data.pipe(res); + + req.on('close', () => { + if (response.data && typeof response.data.destroy === 'function') { + response.data.destroy(); + } + }); + } catch (error) { + console.error(`Error proxying segment: ${error.message}`); + if (!res.headersSent) { + res.status(500).send('Error proxying segment'); + } + } +}); + app.get('/key', async (req, res) => { const keyUrl = req.query.url; if (!keyUrl) return res.status(400).send('Missing url parameter');