feat!: integrate proxy mode into application

This commit is contained in:
Ante Brähler
2024-12-23 00:16:48 +01:00
parent 7adc220ce3
commit b151a406b3
16 changed files with 131 additions and 103 deletions

View File

@@ -31,7 +31,7 @@ module.exports = {
addChannel(req, res) { addChannel(req, res) {
try { try {
//const { name, url, avatar, restream, headersJson, group, playlist } = req.body; //const { name, url, avatar, mode, headersJson, group, playlist } = req.body;
const newChannel = ChannelService.addChannel(req.body); const newChannel = ChannelService.addChannel(req.body);
res.status(201).json(newChannel); res.status(201).json(newChannel);
} catch (error) { } catch (error) {

View File

@@ -1,15 +1,19 @@
class Channel { class Channel {
static nextId = 0; static nextId = 0;
constructor(name, url, avatar, restream, headers = [], group = null, playlist = null) { constructor(name, url, avatar, mode, headers = [], group = null, playlist = null) {
this.id = Channel.nextId++; this.id = Channel.nextId++;
this.name = name; this.name = name;
this.url = url; this.url = url;
this.avatar = avatar; this.avatar = avatar;
this.restream = restream; this.mode = mode;
this.headers = headers; this.headers = headers;
this.group = group; this.group = group;
this.playlist = playlist; this.playlist = playlist;
} }
restream() {
return this.mode === 'restream';
}
} }
module.exports = Channel; module.exports = Channel;

View File

@@ -37,7 +37,7 @@ app.use('/proxy', proxyRouter);
const PORT = 5000; const PORT = 5000;
const server = app.listen(PORT, () => { const server = app.listen(PORT, () => {
console.log(`Server listening on Port ${PORT}`); console.log(`Server listening on Port ${PORT}`);
if (ChannelService.getCurrentChannel().restream) { if (ChannelService.getCurrentChannel().restream()) {
streamController.start(process.env.DEFAULT_CHANNEL_URL); streamController.start(process.env.DEFAULT_CHANNEL_URL);
} }
}); });

View File

@@ -18,14 +18,14 @@ class ChannelService {
this.channels = [ this.channels = [
//Some Test-channels to get started, remove this when using your own playlist //Some Test-channels to get started, remove this when using your own playlist
new Channel('Das Erste', process.env.DEFAULT_CHANNEL_URL, "https://upload.wikimedia.org/wikipedia/commons/thumb/5/56/Das_Erste-Logo_klein.svg/768px-Das_Erste-Logo_klein.svg.png", false), new Channel('Das Erste', process.env.DEFAULT_CHANNEL_URL, "https://upload.wikimedia.org/wikipedia/commons/thumb/5/56/Das_Erste-Logo_klein.svg/768px-Das_Erste-Logo_klein.svg.png", 'direct'),
new Channel('DAZN 1 DE', "https://xyzdddd.mizhls.ru/lb/premium426/index.m3u8", "https://upload.wikimedia.org/wikipedia/commons/4/49/DAZN_1.svg", true, daddyHeaders), new Channel('DAZN 1 DE', "https://xyzdddd.mizhls.ru/lb/premium426/index.m3u8", "https://upload.wikimedia.org/wikipedia/commons/4/49/DAZN_1.svg", 'restream', daddyHeaders),
new Channel('beIN Sports 1', "https://xyzdddd.mizhls.ru/lb/premium61/index.m3u8", "https://www.thesportsdb.com/images/media/channel/logo/BeIn_Sports_1_Australia.png", true, daddyHeaders), new Channel('beIN Sports 1', "https://xyzdddd.mizhls.ru/lb/premium61/index.m3u8", "https://www.thesportsdb.com/images/media/channel/logo/BeIn_Sports_1_Australia.png", 'restream', daddyHeaders),
new Channel('beIN Sports 2', "https://xyzdddd.mizhls.ru/lb/premium92/index.m3u8", "https://www.thesportsdb.com/images/media/channel/logo/BeIn_Sports_HD_2_France.png", true, daddyHeaders), new Channel('beIN Sports 2', "https://xyzdddd.mizhls.ru/lb/premium92/index.m3u8", "https://www.thesportsdb.com/images/media/channel/logo/BeIn_Sports_HD_2_France.png", 'restream', daddyHeaders),
new Channel('Sky Sport Football', "https://xyzdddd.mizhls.ru/lb/premium35/index.m3u8", "https://raw.githubusercontent.com/tv-logo/tv-logos/main/countries/united-kingdom/sky-sports-football-uk.png", true, daddyHeaders), new Channel('Sky Sport Football', "https://xyzdddd.mizhls.ru/lb/premium35/index.m3u8", "https://raw.githubusercontent.com/tv-logo/tv-logos/main/countries/united-kingdom/sky-sports-football-uk.png", 'restream', daddyHeaders),
new Channel('Sky Sports Premier League', "https://xyzdddd.mizhls.ru/lb/premium130/index.m3u8", "https://github.com/tv-logo/tv-logos/blob/main/countries/united-kingdom/sky-sports-premier-league-uk.png?raw=true", true, daddyHeaders), new Channel('Sky Sports Premier League', "https://xyzdddd.mizhls.ru/lb/premium130/index.m3u8", "https://github.com/tv-logo/tv-logos/blob/main/countries/united-kingdom/sky-sports-premier-league-uk.png?raw=true", 'restream', daddyHeaders),
new Channel('SuperSport Premier League', 'https://xyzdddd.mizhls.ru/lb/premium414/index.m3u8', "https://github.com/tv-logo/tv-logos/blob/8d25ddd79ca2f9cd033b808c45cccd2b3da563ee/countries/south-africa/supersport-premier-league-za.png?raw=true", true, daddyHeaders), new Channel('SuperSport Premier League', 'https://xyzdddd.mizhls.ru/lb/premium414/index.m3u8', "https://github.com/tv-logo/tv-logos/blob/8d25ddd79ca2f9cd033b808c45cccd2b3da563ee/countries/south-africa/supersport-premier-league-za.png?raw=true", 'restream', daddyHeaders),
new Channel('NBA', "https://v14.thetvapp.to/hls/NBA28/index.m3u8?token=bFFITmZCbllna21WRUJra0xjN0JPN0w1VlBmSkNUcTl4Zml3a2tQSg==", "https://raw.githubusercontent.com/tv-logo/tv-logos/635e715cb2f2c6d28e9691861d3d331dd040285b/countries/united-states/nba-tv-icon-us.png", false), new Channel('NBA', "https://v14.thetvapp.to/hls/NBA28/index.m3u8?token=bFFITmZCbllna21WRUJra0xjN0JPN0w1VlBmSkNUcTl4Zml3a2tQSg==", "https://raw.githubusercontent.com/tv-logo/tv-logos/635e715cb2f2c6d28e9691861d3d331dd040285b/countries/united-states/nba-tv-icon-us.png", 'direct'),
]; ];
this.currentChannel = this.channels[0]; this.currentChannel = this.channels[0];
} }
@@ -34,7 +34,7 @@ class ChannelService {
return this.channels; return this.channels;
} }
addChannel({ name, url, avatar, restream, headersJson, group = false, playlist = false }) { addChannel({ name, url, avatar, mode, headersJson, group = false, playlist = false }) {
const existing = this.channels.find(channel => channel.url === url); const existing = this.channels.find(channel => channel.url === url);
if (existing) { if (existing) {
@@ -42,7 +42,7 @@ class ChannelService {
} }
const headers = JSON.parse(headersJson); const headers = JSON.parse(headersJson);
const newChannel = new Channel(name, url, avatar, restream, headers, group, playlist); const newChannel = new Channel(name, url, avatar, mode, headers, group, playlist);
this.channels.push(newChannel); this.channels.push(newChannel);
return newChannel; return newChannel;
@@ -55,7 +55,7 @@ class ChannelService {
} }
if (this.currentChannel !== nextChannel) { if (this.currentChannel !== nextChannel) {
if (nextChannel.restream) { if (nextChannel.restream()) {
streamController.stop(this.currentChannel.id); streamController.stop(this.currentChannel.id);
streamController.stop(nextChannel.id); streamController.stop(nextChannel.id);
streamController.start(nextChannel); streamController.start(nextChannel);
@@ -84,12 +84,12 @@ class ChannelService {
const [deletedChannel] = this.channels.splice(channelIndex, 1); const [deletedChannel] = this.channels.splice(channelIndex, 1);
if (this.currentChannel.id === id) { if (this.currentChannel.id === id) {
if (deletedChannel.restream) { if (deletedChannel.restream()) {
streamController.stop(deletedChannel.id); streamController.stop(deletedChannel.id);
} }
this.currentChannel = this.channels.length > 0 ? this.channels[0] : null; this.currentChannel = this.channels.length > 0 ? this.channels[0] : null;
if (this.currentChannel?.restream) { if (this.currentChannel?.restream()) {
streamController.start(this.currentChannel); streamController.start(this.currentChannel);
} }
} }
@@ -106,7 +106,7 @@ class ChannelService {
const streamChanged = updatedAttributes.url != this.currentChannel.url || const streamChanged = updatedAttributes.url != this.currentChannel.url ||
JSON.stringify(updatedAttributes.headers) != JSON.stringify(this.currentChannel.headers) || JSON.stringify(updatedAttributes.headers) != JSON.stringify(this.currentChannel.headers) ||
updatedAttributes.restream != this.currentChannel.restream; updatedAttributes.mode != this.currentChannel.mode;
const channel = this.channels[channelIndex]; const channel = this.channels[channelIndex];
Object.assign(channel, updatedAttributes); Object.assign(channel, updatedAttributes);
@@ -114,7 +114,7 @@ class ChannelService {
if (this.currentChannel.id == id) { if (this.currentChannel.id == id) {
if (streamChanged) { if (streamChanged) {
streamController.stop(channel.id); streamController.stop(channel.id);
if (channel.restream) { if (channel.restream()) {
streamController.start(channel); streamController.start(channel);
} }
} }

View File

@@ -3,7 +3,7 @@ const ChannelService = require('./ChannelService');
class PlaylistService { class PlaylistService {
async addPlaylist(playlistUrl, restream, headersJson) { async addPlaylist(playlistUrl, mode, headersJson) {
const response = await fetch(playlistUrl); const response = await fetch(playlistUrl);
const content = await response.text(); const content = await response.text();
@@ -18,7 +18,7 @@ class PlaylistService {
name: channel.name, name: channel.name,
url: channel.url, url: channel.url,
avatar: channel.tvg.logo, avatar: channel.tvg.logo,
restream: restream, mode: mode,
headersJson: headersJson, headersJson: headersJson,
group: channel.group.title, group: channel.group.title,
playlist: playlistUrl playlist: playlistUrl

View File

@@ -2,9 +2,9 @@ const ChannelService = require('../services/ChannelService');
module.exports = (io, socket) => { module.exports = (io, socket) => {
socket.on('add-channel', ({ name, url, avatar, restream, headersJson }) => { socket.on('add-channel', ({ name, url, avatar, mode, headersJson }) => {
try { try {
const newChannel = ChannelService.addChannel({ name: name, url: url, avatar: avatar, restream: restream, headersJson: headersJson }); const newChannel = ChannelService.addChannel({ name: name, url: url, avatar: avatar, mode: mode, headersJson: headersJson });
io.emit('channel-added', newChannel); // Broadcast to all clients io.emit('channel-added', newChannel); // Broadcast to all clients
} catch (err) { } catch (err) {
socket.emit('app-error', { message: err.message }); socket.emit('app-error', { message: err.message });

View File

@@ -4,9 +4,9 @@ const Channel = require('../models/Channel');
module.exports = (io, socket) => { module.exports = (io, socket) => {
socket.on('add-playlist', async ({ playlist, restream, headersJson }) => { socket.on('add-playlist', async ({ playlist, mode, headersJson }) => {
try { try {
const channels = await PlaylistService.addPlaylist(playlist, restream, headersJson); const channels = await PlaylistService.addPlaylist(playlist, mode, headersJson);
if (channels) { if (channels) {
channels.forEach(channel => { channels.forEach(channel => {
io.emit('channel-added', channel); io.emit('channel-added', channel);

View File

@@ -7,7 +7,6 @@ services:
args: args:
# Set this to the server ip/domain, if your backend is deployed on a different server # Set this to the server ip/domain, if your backend is deployed on a different server
#VITE_BACKEND_URL: http://123.123.123.123:5000 #VITE_BACKEND_URL: http://123.123.123.123:5000
VITE_BACKEND_STREAMS_PATH: /streams/
VITE_STREAM_DELAY: 18 VITE_STREAM_DELAY: 18
# Optional settings for synchronization # Optional settings for synchronization
#VITE_SYNCHRONIZATION_TOLERANCE: 0.8 #VITE_SYNCHRONIZATION_TOLERANCE: 0.8

View File

@@ -7,7 +7,6 @@ services:
args: args:
# Set this to the server ip/domain, if your backend is deployed on a different server # Set this to the server ip/domain, if your backend is deployed on a different server
#VITE_BACKEND_URL: http://123.123.123.123:5000 #VITE_BACKEND_URL: http://123.123.123.123:5000
VITE_BACKEND_STREAMS_PATH: /streams/
VITE_STREAM_DELAY: 18 VITE_STREAM_DELAY: 18
# Optional settings for synchronization # Optional settings for synchronization
#VITE_SYNCHRONIZATION_TOLERANCE: 0.8 #VITE_SYNCHRONIZATION_TOLERANCE: 0.8

View File

@@ -4,7 +4,6 @@ FROM node:20 AS builder
WORKDIR /app WORKDIR /app
ARG VITE_BACKEND_URL ARG VITE_BACKEND_URL
ARG VITE_BACKEND_STREAMS_PATH
ARG VITE_STREAM_DELAY ARG VITE_STREAM_DELAY
ARG VITE_SYNCHRONIZATION_TOLERANCE ARG VITE_SYNCHRONIZATION_TOLERANCE
ARG VITE_SYNCHRONIZATION_MAX_DEVIATION ARG VITE_SYNCHRONIZATION_MAX_DEVIATION
@@ -19,7 +18,6 @@ RUN npm install
COPY . . COPY . .
ENV VITE_BACKEND_URL=$VITE_BACKEND_URL ENV VITE_BACKEND_URL=$VITE_BACKEND_URL
ENV VITE_BACKEND_STREAMS_PATH=$VITE_BACKEND_STREAMS_PATH
ENV VITE_STREAM_DELAY=$VITE_STREAM_DELAY ENV VITE_STREAM_DELAY=$VITE_STREAM_DELAY
ENV VITE_SYNCHRONIZATION_TOLERANCE=$VITE_SYNCHRONIZATION_TOLERANCE ENV VITE_SYNCHRONIZATION_TOLERANCE=$VITE_SYNCHRONIZATION_TOLERANCE
ENV VITE_SYNCHRONIZATION_MAX_DEVIATION=$VITE_SYNCHRONIZATION_MAX_DEVIATION ENV VITE_SYNCHRONIZATION_MAX_DEVIATION=$VITE_SYNCHRONIZATION_MAX_DEVIATION

View File

@@ -5,7 +5,6 @@ Setup a `.env` file or
equivalent environment variables: equivalent environment variables:
```env ```env
#VITE_BACKEND_URL: http://123.123.123.123:5000 #VITE_BACKEND_URL: http://123.123.123.123:5000
VITE_BACKEND_STREAMS_PATH: /streams/
VITE_STREAM_DELAY: 18 VITE_STREAM_DELAY: 18
``` ```

View File

@@ -54,7 +54,7 @@ function App() {
if(selectedChannel?.id === updatedChannel.id) { if(selectedChannel?.id === updatedChannel.id) {
// Reload stream if the stream attributes (url, headers) have changed // Reload stream if the stream attributes (url, headers) have changed
if((selectedChannel?.url != updatedChannel.url || JSON.stringify(selectedChannel?.headers) != JSON.stringify(updatedChannel.headers)) && selectedChannel?.restream == updatedChannel.restream){ if((selectedChannel?.url != updatedChannel.url || JSON.stringify(selectedChannel?.headers) != JSON.stringify(updatedChannel.headers)) && selectedChannel?.mode === 'restream'){
//TODO: find a better solution instead of reloading (problem is m3u8 needs time to refresh server-side) //TODO: find a better solution instead of reloading (problem is m3u8 needs time to refresh server-side)
setTimeout(() => { setTimeout(() => {
window.location.reload(); window.location.reload();

View File

@@ -1,6 +1,6 @@
import React, { useContext, useEffect, useRef } from 'react'; import React, { useContext, useEffect, useRef } from 'react';
import Hls from 'hls.js'; import Hls from 'hls.js';
import { Channel } from '../types'; import { Channel, ChannelMode } from '../types';
import { ToastContext } from './notifications/ToastContext'; import { ToastContext } from './notifications/ToastContext';
interface VideoPlayerProps { interface VideoPlayerProps {
@@ -49,15 +49,15 @@ function VideoPlayer({ channel, syncEnabled }: VideoPlayerProps) {
}, },
}, },
}); });
const sourceLinks: Record<ChannelMode, string> = {
direct: channel.url,
proxy: import.meta.env.VITE_BACKEND_URL + '/proxy/channel', //TODO: needs update for multi-channel streaming
restream: import.meta.env.VITE_BACKEND_URL + '/streams/' + channel.id + "/" + channel.id + ".m3u8", //e.g. http://backend:3000/streams/1/1.m3u8
};
hlsRef.current = hls; hlsRef.current = hls;
hls.loadSource( hls.loadSource(sourceLinks[channel.mode]);
channel.restream ?
//e.g. http://backend:3000/streams/1/1.m3u8
import.meta.env.VITE_BACKEND_URL + import.meta.env.VITE_BACKEND_STREAMS_PATH + channel.id + "/" + channel.id + ".m3u8"
: channel.url
);
hls.attachMedia(video); hls.attachMedia(video);
if(!syncEnabled) return; if(!syncEnabled) return;
@@ -66,7 +66,7 @@ function VideoPlayer({ channel, syncEnabled }: VideoPlayerProps) {
let toastStartId = null; let toastStartId = null;
toastStartId = addToast({ toastStartId = addToast({
type: 'loading', type: 'loading',
title: channel.restream ? 'Starting Restream': 'Starting Stream', title: 'Starting Stream',
message: 'This might take a few moments...', message: 'This might take a few moments...',
duration: 0, duration: 0,
}); });
@@ -76,7 +76,7 @@ function VideoPlayer({ channel, syncEnabled }: VideoPlayerProps) {
let toastDurationSet = false; let toastDurationSet = false;
hls.on(Hls.Events.MANIFEST_PARSED, (_event, _data) => { hls.on(Hls.Events.MANIFEST_PARSED, (_event, _data) => {
if (channel.restream) { if (channel.mode === 'restream') {
const now = new Date().getTime(); const now = new Date().getTime();
const fragments = hls.levels[0]?.details?.fragments; const fragments = hls.levels[0]?.details?.fragments;
@@ -111,7 +111,7 @@ function VideoPlayer({ channel, syncEnabled }: VideoPlayerProps) {
// Reload manifest // Reload manifest
setTimeout(() => { setTimeout(() => {
hls.loadSource(import.meta.env.VITE_BACKEND_URL + import.meta.env.VITE_BACKEND_STREAMS_PATH + channel.id + "/" + channel.id + ".m3u8"); hls.loadSource(import.meta.env.VITE_BACKEND_URL + '/streams/' + channel.id + "/" + channel.id + ".m3u8");
}, 1000); }, 1000);
} }
} else { } else {
@@ -163,13 +163,17 @@ function VideoPlayer({ channel, syncEnabled }: VideoPlayerProps) {
if (toastStartId) { if (toastStartId) {
removeToast(toastStartId); removeToast(toastStartId);
} }
const messages: Record<ChannelMode, string> = {
direct: 'The stream is not working. Try with proxy/restream option enabled for this channel.',
proxy: 'The stream is not working. Try with restream option enabled for this channel.',
restream: `The stream is not working. Check the source. ${data.response?.text}`,
};
addToast({ addToast({
type: 'error', type: 'error',
title: 'Stream Error', title: 'Stream Error',
message: !channel.restream message: messages[channel.mode],
? 'The stream is not working. Try with restream option enabled for this channel.'
: `The stream is not working. Check the source. ${data.response?.text}`,
duration: 5000, duration: 5000,
}); });
return; return;
@@ -183,7 +187,7 @@ function VideoPlayer({ channel, syncEnabled }: VideoPlayerProps) {
hlsRef.current.destroy(); hlsRef.current.destroy();
} }
}; };
}, [channel?.url, channel?.restream, syncEnabled]); }, [channel?.url, channel?.mode, syncEnabled]);
const handleVideoClick = (event: React.MouseEvent<HTMLVideoElement>) => { const handleVideoClick = (event: React.MouseEvent<HTMLVideoElement>) => {
if (videoRef.current?.muted) { if (videoRef.current?.muted) {

View File

@@ -1,7 +1,7 @@
import React, { useState, useEffect, useContext } from 'react'; import React, { useState, useEffect, useContext } from 'react';
import { Plus, Trash2, X } from 'lucide-react'; import { Plus, Trash2, X } from 'lucide-react';
import socketService from '../../services/SocketService'; import socketService from '../../services/SocketService';
import { CustomHeader, Channel } from '../../types'; import { CustomHeader, Channel, ChannelMode } from '../../types';
import CustomHeaderInput from './CustomHeaderInput'; import CustomHeaderInput from './CustomHeaderInput';
import { ToastContext } from '../notifications/ToastContext'; import { ToastContext } from '../notifications/ToastContext';
@@ -12,13 +12,13 @@ interface ChannelModalProps {
} }
function ChannelModal({ isOpen, onClose, channel }: ChannelModalProps) { function ChannelModal({ isOpen, onClose, channel }: ChannelModalProps) {
const [mode, setMode] = useState<'channel' | 'playlist'>('channel'); const [type, setType] = useState<'channel' | 'playlist'>('channel');
const [isEditMode, setIsEditMode] = useState(false); const [isEditMode, setIsEditMode] = useState(false);
const [name, setName] = useState(''); const [name, setName] = useState('');
const [url, setUrl] = useState(''); const [url, setUrl] = useState('');
const [avatar, setAvatar] = useState(''); const [avatar, setAvatar] = useState('');
const [restream, setRestream] = useState(true); const [mode, setMode] = useState<ChannelMode>('proxy');
const [headers, setHeaders] = useState<CustomHeader[]>([]); const [headers, setHeaders] = useState<CustomHeader[]>([]);
const [playlistUrl, setPlaylistUrl] = useState(''); const [playlistUrl, setPlaylistUrl] = useState('');
@@ -30,20 +30,20 @@ function ChannelModal({ isOpen, onClose, channel }: ChannelModalProps) {
setName(channel.name); setName(channel.name);
setUrl(channel.url); setUrl(channel.url);
setAvatar(channel.avatar); setAvatar(channel.avatar);
setRestream(channel.restream); setMode(channel.mode);
setHeaders(channel.headers); setHeaders(channel.headers);
setPlaylistUrl(channel.playlist); setPlaylistUrl(channel.playlist);
setIsEditMode(true); setIsEditMode(true);
setMode('channel'); // Default to "channel" if a channel object exists setType('channel'); // Default to "channel" if a channel object exists
} else { } else {
setName(''); setName('');
setUrl(''); setUrl('');
setAvatar(''); setAvatar('');
setRestream(false); setMode('proxy');
setHeaders([]); setHeaders([]);
setPlaylistUrl(''); setPlaylistUrl('');
setIsEditMode(false); setIsEditMode(false);
setMode('channel'); // Default to "channel" if a channel object exists setType('channel'); // Default to "channel" if a channel object exists
} }
}, [channel]); }, [channel]);
@@ -69,23 +69,23 @@ function ChannelModal({ isOpen, onClose, channel }: ChannelModalProps) {
return; return;
} }
if (mode === 'channel') { if (type === 'channel') {
if (!name.trim() || !url.trim()) return; if (!name.trim() || !url.trim()) return;
socketService.addChannel( socketService.addChannel(
name.trim(), name.trim(),
url.trim(), url.trim(),
avatar.trim() || 'https://via.placeholder.com/64', avatar.trim() || 'https://via.placeholder.com/64',
restream, mode,
JSON.stringify(headers) JSON.stringify(headers)
); );
} else if (mode === 'playlist') { } else if (type === 'playlist') {
if (!playlistUrl.trim()) return; if (!playlistUrl.trim()) return;
socketService.addPlaylist(playlistUrl.trim(), restream, JSON.stringify(headers)); socketService.addPlaylist(playlistUrl.trim(), mode, JSON.stringify(headers));
} }
addToast({ addToast({
type: 'success', type: 'success',
title: `${mode} added`, title: `${type} added`,
duration: 3000, duration: 3000,
}); });
@@ -93,24 +93,24 @@ function ChannelModal({ isOpen, onClose, channel }: ChannelModalProps) {
}; };
const handleUpdate = (id: number) => { const handleUpdate = (id: number) => {
if (mode === 'channel') { if (type === 'channel') {
socketService.updateChannel(id, { socketService.updateChannel(id, {
name: name.trim(), name: name.trim(),
url: url.trim(), url: url.trim(),
avatar: avatar.trim() || 'https://via.placeholder.com/64', avatar: avatar.trim() || 'https://via.placeholder.com/64',
restream, mode: mode,
headers: headers, headers: headers,
}); });
} else if (mode === 'playlist') { } else if (type === 'playlist') {
if(channel!.playlist !== playlistUrl.trim()) { if(channel!.playlist !== playlistUrl.trim()) {
// If the playlist URL has changed, we need to reload the playlist (delete old channels and fetch again) // If the playlist URL has changed, we need to reload the playlist (delete old channels and fetch again)
socketService.deletePlaylist(channel!.playlist); socketService.deletePlaylist(channel!.playlist);
socketService.addPlaylist(playlistUrl.trim(), restream, JSON.stringify(headers)); socketService.addPlaylist(playlistUrl.trim(), mode, JSON.stringify(headers));
} else { } else {
socketService.updatePlaylist(playlistUrl.trim(), { socketService.updatePlaylist(playlistUrl.trim(), {
playlist: playlistUrl.trim(), playlist: playlistUrl.trim(),
restream, mode: mode,
headers: headers, headers: headers,
}); });
} }
@@ -118,7 +118,7 @@ function ChannelModal({ isOpen, onClose, channel }: ChannelModalProps) {
addToast({ addToast({
type: 'success', type: 'success',
title: `${mode} updated`, title: `${type} updated`,
duration: 3000, duration: 3000,
}); });
@@ -127,15 +127,15 @@ function ChannelModal({ isOpen, onClose, channel }: ChannelModalProps) {
const handleDelete = () => { const handleDelete = () => {
if (channel) { if (channel) {
if (mode === 'channel') { if (type === 'channel') {
socketService.deleteChannel(channel.id); socketService.deleteChannel(channel.id);
} else if (mode === 'playlist') { } else if (type === 'playlist') {
socketService.deletePlaylist(channel.playlist); socketService.deletePlaylist(channel.playlist);
} }
} }
addToast({ addToast({
type: 'error', type: 'error',
title: `${mode} deleted`, title: `${type} deleted`,
duration: 3000, duration: 3000,
}); });
onClose(); onClose();
@@ -149,7 +149,7 @@ function ChannelModal({ isOpen, onClose, channel }: ChannelModalProps) {
{/* Header mit Slider */} {/* Header mit Slider */}
<div className="flex items-center justify-between p-4 border-b border-gray-700"> <div className="flex items-center justify-between p-4 border-b border-gray-700">
<h2 className="text-xl font-semibold"> <h2 className="text-xl font-semibold">
{isEditMode ? (mode === 'channel' ? 'Edit Channel' : 'Edit Playlist') : mode === 'channel' ? 'Add New Channel' : 'Add New Playlist'} {isEditMode ? (type === 'channel' ? 'Edit Channel' : 'Edit Playlist') : type === 'channel' ? 'Add New Channel' : 'Add New Playlist'}
</h2> </h2>
<button <button
onClick={onClose} onClick={onClose}
@@ -164,15 +164,15 @@ function ChannelModal({ isOpen, onClose, channel }: ChannelModalProps) {
<div className="p-4 pb-0"> <div className="p-4 pb-0">
<div className="flex space-x-4 justify-center"> <div className="flex space-x-4 justify-center">
<button <button
onClick={() => setMode('channel')} onClick={() => setType('channel')}
className={`px-4 py-2 rounded-lg border-2 ${mode === 'channel' ? 'border-blue-600' : 'border-transparent' className={`px-4 py-2 rounded-lg border-2 ${type === 'channel' ? 'border-blue-600' : 'border-transparent'
} hover:border-blue-600 transition-colors`} } hover:border-blue-600 transition-colors`}
> >
Channel Channel
</button> </button>
<button <button
onClick={() => setMode('playlist')} onClick={() => setType('playlist')}
className={`px-4 py-2 rounded-lg border-2 ${mode === 'playlist' ? 'border-blue-600' : 'border-transparent' className={`px-4 py-2 rounded-lg border-2 ${type === 'playlist' ? 'border-blue-600' : 'border-transparent'
} hover:border-blue-600 transition-colors`} } hover:border-blue-600 transition-colors`}
> >
Playlist Playlist
@@ -183,7 +183,7 @@ function ChannelModal({ isOpen, onClose, channel }: ChannelModalProps) {
{/* Form */} {/* Form */}
<form onSubmit={handleSubmit} className="p-4 space-y-4"> <form onSubmit={handleSubmit} className="p-4 space-y-4">
{mode === 'channel' && ( {type === 'channel' && (
<> <>
{/* Channel fields */} {/* Channel fields */}
<div> <div>
@@ -228,36 +228,47 @@ function ChannelModal({ isOpen, onClose, channel }: ChannelModalProps) {
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium mb-1">Restream through backend</label> <label className="block text-sm font-medium mb-1">Channel Mode</label>
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<label className="flex items-center"> <label className="flex items-center">
<input <input
type="radio" type="radio"
name="restream" name="mode"
value="yes" value="direct"
checked={restream} checked={mode === 'direct'}
className="form-radio text-blue-600" className="form-radio text-blue-600"
onChange={() => setRestream(true)} onChange={() => setMode('direct')}
/> />
<span className="ml-2">Yes</span> <span className="ml-2">Direct</span>
</label> </label>
<label className="flex items-center"> <label className="flex items-center">
<input <input
type="radio" type="radio"
name="restream" name="mode"
value="no" value="proxy"
className="form-radio text-blue-600" className="form-radio text-blue-600"
checked={!restream} checked={mode === 'proxy'}
onChange={() => setRestream(false)} onChange={() => setMode('proxy')}
/> />
<span className="ml-2">No</span> <span className="ml-2">Proxy</span>
</label>
<label className="flex items-center">
<input
type="radio"
name="mode"
value="restream"
className="form-radio text-blue-600"
checked={mode === 'restream'}
onChange={() => setMode('restream')}
/>
<span className="ml-2">Restream</span>
</label> </label>
</div> </div>
</div> </div>
</> </>
)} )}
{mode === 'playlist' && ( {type === 'playlist' && (
<> <>
{/* Playlist fields */} {/* Playlist fields */}
<div> <div>
@@ -275,29 +286,40 @@ function ChannelModal({ isOpen, onClose, channel }: ChannelModalProps) {
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium mb-1">Restream through backend</label> <label className="block text-sm font-medium mb-1">Channel Mode</label>
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<label className="flex items-center"> <label className="flex items-center">
<input <input
type="radio" type="radio"
name="restream" name="mode"
value="yes" value="direct"
checked={restream} checked={mode === 'direct'}
className="form-radio text-blue-600" className="form-radio text-blue-600"
onChange={() => setRestream(true)} onChange={() => setMode('direct')}
/> />
<span className="ml-2">Yes</span> <span className="ml-2">Direct</span>
</label> </label>
<label className="flex items-center"> <label className="flex items-center">
<input <input
type="radio" type="radio"
name="restream" name="mode"
value="no" value="proxy"
className="form-radio text-blue-600" className="form-radio text-blue-600"
checked={!restream} checked={mode === 'proxy'}
onChange={() => setRestream(false)} onChange={() => setMode('proxy')}
/> />
<span className="ml-2">No</span> <span className="ml-2">Proxy</span>
</label>
<label className="flex items-center">
<input
type="radio"
name="mode"
value="restream"
className="form-radio text-blue-600"
checked={mode === 'restream'}
onChange={() => setMode('restream')}
/>
<span className="ml-2">Restream</span>
</label> </label>
</div> </div>
</div> </div>
@@ -305,7 +327,7 @@ function ChannelModal({ isOpen, onClose, channel }: ChannelModalProps) {
)} )}
{/* Custom Headers */} {/* Custom Headers */}
{restream && ( {mode !== 'direct' && (
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<label className="block text-sm font-medium"> <label className="block text-sm font-medium">

View File

@@ -1,4 +1,5 @@
import { io, Socket } from 'socket.io-client'; import { io, Socket } from 'socket.io-client';
import { ChannelMode } from '../types';
class SocketService { class SocketService {
private socket: Socket | null = null; private socket: Socket | null = null;
@@ -69,10 +70,10 @@ class SocketService {
} }
// Add channel // Add channel
addChannel(name: string, url: string, avatar: string, restream: boolean, headersJson: string) { addChannel(name: string, url: string, avatar: string, mode: ChannelMode, headersJson: string) {
if (!this.socket) throw new Error('Socket is not connected.'); if (!this.socket) throw new Error('Socket is not connected.');
this.socket.emit('add-channel', { name, url, avatar, restream, headersJson }); this.socket.emit('add-channel', { name, url, avatar, mode, headersJson });
} }
// Set current channel // Set current channel
@@ -97,10 +98,10 @@ class SocketService {
} }
// Add playlist // Add playlist
addPlaylist(playlist: string, restream: boolean, headersJson: string) { addPlaylist(playlist: string, mode: ChannelMode, headersJson: string) {
if (!this.socket) throw new Error('Socket is not connected.'); if (!this.socket) throw new Error('Socket is not connected.');
this.socket.emit('add-playlist', { playlist, restream, headersJson }); this.socket.emit('add-playlist', { playlist, mode, headersJson });
} }
// Update playlist // Update playlist

View File

@@ -18,12 +18,14 @@ export interface RandomUser {
}[]; }[];
}; };
export type ChannelMode = 'direct' | 'proxy' | 'restream';
export interface Channel { export interface Channel {
id: number; id: number;
name: string; name: string;
url: string; url: string;
avatar: string; avatar: string;
restream: boolean; mode: ChannelMode;
headers: CustomHeader[]; headers: CustomHeader[];
group: string; group: string;
playlist: string; playlist: string;