feat!: integrate proxy mode into application
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user