feat: add a central proxied stream and playlist
This commit is contained in:
127
backend/controllers/CentralChannelController.js
Normal file
127
backend/controllers/CentralChannelController.js
Normal file
@@ -0,0 +1,127 @@
|
||||
const request = require('request');
|
||||
const ChannelService = require('../services/ChannelService');
|
||||
const ProxyHelperService = require('../services/proxy/ProxyHelperService');
|
||||
const SessionFactory = require('../services/session/SessionFactory');
|
||||
const Path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const STORAGE_PATH = process.env.STORAGE_PATH;
|
||||
|
||||
function fetchM3u8(res, targetUrl, headers) {
|
||||
console.log('Proxy playlist request to:', targetUrl);
|
||||
|
||||
try {
|
||||
request(ProxyHelperService.getRequestOptions(targetUrl, headers), (error, response, body) => {
|
||||
if (error) {
|
||||
console.error('Request error:', error);
|
||||
if (!res.headersSent) {
|
||||
return res.status(500).json({ error: 'Failed to fetch m3u8 file' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const proxyBaseUrl = '/proxy/';
|
||||
const rewrittenBody = ProxyHelperService.rewriteUrls(body, proxyBaseUrl, headers, targetUrl).join('\n');
|
||||
|
||||
if(rewrittenBody.indexOf('channel?url=') !== -1) {
|
||||
const regex = /channel\?url=([^&\s]+)/;
|
||||
const match = rewrittenBody.match(regex);
|
||||
const channelUrl = decodeURIComponent(match[1]);
|
||||
return fetchM3u8(res, channelUrl, headers);
|
||||
}
|
||||
|
||||
const updatedM3u8 = rewrittenBody.replace(/(#EXTINF.*)/, '#EXT-X-DISCONTINUITY\n$1');
|
||||
|
||||
return res.send(updatedM3u8);
|
||||
} catch (e) {
|
||||
console.error('Failed to rewrite URLs:', e);
|
||||
return res.status(500).json({ error: 'Failed to parse m3uo file. Not a valid HLS stream.' });
|
||||
}
|
||||
|
||||
//res.set('Content-Type', 'application/vnd.apple.mpegurl');
|
||||
}).on('error', (err) => {
|
||||
console.error('Unhandled error:', err);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({ error: 'Proxy request failed' });
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to proxy request:', e);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({ error: 'Proxy request failed' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
async currentChannel(req, res) {
|
||||
|
||||
const channel = ChannelService.getCurrentChannel();
|
||||
|
||||
res.set('Access-Control-Allow-Origin', '*');
|
||||
if(channel.restream()) {
|
||||
const path = Path.resolve(`${STORAGE_PATH}${channel.id}/${channel.id}.m3u8`);
|
||||
if (fs.existsSync(path)) {
|
||||
try {
|
||||
const m3u8Data = fs.readFileSync(path, 'utf-8');
|
||||
|
||||
let discontinuityAdded = false;
|
||||
const updatedM3u8 = m3u8Data
|
||||
.split('\n')
|
||||
.map((line, index, lines) => {
|
||||
// Füge #EXT-X-DISCONTINUITY vor der ersten #EXTINF hinzu
|
||||
if (!discontinuityAdded && line.startsWith('#EXTINF')) {
|
||||
discontinuityAdded = true;
|
||||
return `#EXT-X-DISCONTINUITY\n${line}`;
|
||||
}
|
||||
|
||||
// Passe die .ts-Dateipfade an
|
||||
if (line.endsWith('.ts')) {
|
||||
return `${STORAGE_PATH}${channel.id}/${line}`;
|
||||
}
|
||||
|
||||
return line;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
return res.send(updatedM3u8);
|
||||
} catch (err) {
|
||||
console.error('Error loading m3u8 data from fs:', err);
|
||||
res.status(500).json({ error: 'Failed to load m3u8 data from filesystem.' });
|
||||
}
|
||||
}
|
||||
//add platzhalter
|
||||
return res.send('No m3u8 data found.');
|
||||
} else {
|
||||
// Direct/Proxy Mode
|
||||
// -> Fetch the m3u8 file from the channel URL
|
||||
let targetUrl = channel.url;
|
||||
|
||||
const sessionProvider = SessionFactory.getSessionProvider(channel);
|
||||
if(sessionProvider) {
|
||||
await sessionProvider.createSession();
|
||||
targetUrl = channel.sessionUrl;
|
||||
}
|
||||
|
||||
let headers = undefined;
|
||||
if(channel.headers && channel.headers.length > 0) {
|
||||
headers = Buffer.from(JSON.stringify(channel.headers)).toString('base64');
|
||||
}
|
||||
|
||||
fetchM3u8(res, targetUrl, headers);
|
||||
}
|
||||
},
|
||||
|
||||
playlist(req, res) {
|
||||
|
||||
const playlistStr = `#EXTM3U
|
||||
#EXTINF:-1 tvg-name="CURRENT RESTREAM" tvg-logo="https://cdn-icons-png.freepik.com/512/9294/9294560.png" group-title="DE",CURRENT RESTREAM
|
||||
${req.headers['x-forwarded-proto'] ?? 'http'}://${req.get('Host')}:${req.headers['x-forwarded-port'] ?? ''}/proxy/current`;
|
||||
|
||||
//TODO: dynamically add channels from ChannelService (only direct and proxy)
|
||||
|
||||
res.send(playlistStr);
|
||||
}
|
||||
};
|
||||
@@ -32,6 +32,8 @@ module.exports = {
|
||||
|
||||
console.log('Proxy playlist request to:', targetUrl);
|
||||
|
||||
res.set('Access-Control-Allow-Origin', '*');
|
||||
|
||||
try {
|
||||
request(ProxyHelperService.getRequestOptions(targetUrl, headers), (error, response, body) => {
|
||||
if (error) {
|
||||
@@ -76,6 +78,8 @@ module.exports = {
|
||||
|
||||
console.log('Proxy request to:', targetUrl);
|
||||
|
||||
res.set('Access-Control-Allow-Origin', '*');
|
||||
|
||||
req.pipe(
|
||||
request(ProxyHelperService.getRequestOptions(targetUrl, headers))
|
||||
.on('error', (err) => {
|
||||
|
||||
@@ -6,6 +6,7 @@ const ChatSocketHandler = require('./socket/ChatSocketHandler');
|
||||
const ChannelSocketHandler = require('./socket/ChannelSocketHandler');
|
||||
|
||||
const proxyController = require('./controllers/ProxyController');
|
||||
const centralChannelController = require('./controllers/CentralChannelController');
|
||||
const channelController = require('./controllers/ChannelController');
|
||||
const streamController = require('./services/restream/StreamController');
|
||||
const ChannelService = require('./services/ChannelService');
|
||||
@@ -18,24 +19,23 @@ const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
const apiRouter = express.Router();
|
||||
|
||||
apiRouter.get('/', channelController.getChannels);
|
||||
apiRouter.get('/current', channelController.getCurrentChannel);
|
||||
apiRouter.get('/playlist', centralChannelController.playlist);
|
||||
apiRouter.get('/:channelId', channelController.getChannel);
|
||||
apiRouter.delete('/:channelId', channelController.deleteChannel);
|
||||
apiRouter.put('/:channelId', channelController.updateChannel);
|
||||
apiRouter.post('/', channelController.addChannel);
|
||||
|
||||
app.use('/api/channels', apiRouter);
|
||||
|
||||
const proxyRouter = express.Router();
|
||||
|
||||
proxyRouter.get('/channel', proxyController.channel);
|
||||
proxyRouter.get('/segment', proxyController.segment);
|
||||
proxyRouter.get('/key', proxyController.key);
|
||||
|
||||
proxyRouter.get('/current', centralChannelController.currentChannel);
|
||||
app.use('/proxy', proxyRouter);
|
||||
|
||||
|
||||
const PORT = 5000;
|
||||
const server = app.listen(PORT, async () => {
|
||||
console.log(`Server listening on Port ${PORT}`);
|
||||
|
||||
@@ -8,6 +8,16 @@ http {
|
||||
include mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
#Forward header if there is a proxy in front otherwise set the headers
|
||||
map $http_x_forwarded_proto $x_forwarded_proto {
|
||||
default $http_x_forwarded_proto;
|
||||
"" $scheme;
|
||||
}
|
||||
map $http_x_forwarded_port $x_forwarded_port {
|
||||
default $http_x_forwarded_port;
|
||||
"" $server_port;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
@@ -17,6 +27,10 @@ http {
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://iptv_restream_backend:5000;
|
||||
|
||||
proxy_set_header X-Forwarded-Proto $x_forwarded_proto;
|
||||
proxy_set_header X-Forwarded-Port $x_forwarded_port;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
@@ -36,9 +50,9 @@ http {
|
||||
proxy_pass http://iptv_restream_backend:5000;
|
||||
proxy_set_header Host $host;
|
||||
|
||||
add_header 'Access-Control-Allow-Origin' '*';
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
|
||||
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
|
||||
# add_header 'Access-Control-Allow-Origin' '*';
|
||||
# add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
|
||||
# add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
|
||||
|
||||
proxy_hide_header Content-Type;
|
||||
add_header Content-Type 'application/vnd.apple.mpegurl' always;
|
||||
@@ -49,6 +63,7 @@ http {
|
||||
|
||||
location /streams/ {
|
||||
root /;
|
||||
|
||||
add_header 'Access-Control-Allow-Origin' '*';
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS';
|
||||
add_header 'Access-Control-Allow-Headers' 'Range';
|
||||
|
||||
Reference in New Issue
Block a user