Merge pull request #7 from antebrl/5-live-chat-implementation
5 live chat implementation
This commit is contained in:
@@ -18,3 +18,15 @@ Before running, make sure you have `ffmpeg` installed on your system.
|
||||
node index.js
|
||||
```
|
||||
Be aware, that this application is designed for Linux systems!
|
||||
|
||||
## Architecture
|
||||
|
||||
### API
|
||||
|
||||
- Endpoints to add a channel, get all channels, get selected channel and set selected channel
|
||||
|
||||
### WebSockets
|
||||
|
||||
- `channel-added` and `channel-selected` events will be send to all connected clients
|
||||
- chat messages: `send-chat-message` and `chat-message`
|
||||
- users: `user-connected` and `user-disconnected`
|
||||
|
||||
@@ -1,66 +1,11 @@
|
||||
const ffmpegService = require('../services/FFmpegService');
|
||||
const storageService = require('../services/StorageService');
|
||||
const Channel = require('../models/Channel');
|
||||
|
||||
let channels = [new Channel('DEFAULT_CHANNEL', process.env.DEFAULT_CHANNEL_URL)];
|
||||
let currentChannel = channels[0];
|
||||
|
||||
function setCurrent(req, res) {
|
||||
const { channelName } = req.body || {};
|
||||
|
||||
if (!channelName) {
|
||||
return res.status(400).json({ status: 'error', message: 'channelName is required' });
|
||||
}
|
||||
|
||||
const nextChannel = channels.find(channel => channel.name === channelName);
|
||||
if (!nextChannel) {
|
||||
res.status(400).json({ status: 'error', message: 'Channel does not exist' });
|
||||
}
|
||||
|
||||
if (currentChannel !== nextChannel) {
|
||||
const segmentNumber = storageService.getNextSegmentNumber();
|
||||
storageService.clearStorage();
|
||||
currentChannel = nextChannel;
|
||||
ffmpegService.startFFmpeg(nextChannel.url, segmentNumber);
|
||||
}
|
||||
res.status(200).json({ status: 'success', channelUrl: currentChannel.url });
|
||||
}
|
||||
|
||||
|
||||
function getCurrent(_, res) {
|
||||
if (currentChannel) {
|
||||
res.status(200).json({ channelName: currentChannel.name, channelUrl: currentChannel.url });
|
||||
} else {
|
||||
res.status(404).json({ status: 'error', message: 'No channel active' });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function getChannels(_, res) {
|
||||
res.status(200).json({ channels });
|
||||
}
|
||||
|
||||
function addChannel(req, res) {
|
||||
const { name, url } = req.body;
|
||||
|
||||
if (!name || !url) {
|
||||
return res.status(400).json({ status: 'error', message: 'Channel name and URL are required' });
|
||||
}
|
||||
|
||||
const channelExists = channels.some(channel => channel.url === url);
|
||||
if (channelExists) {
|
||||
return res.status(409).json({ status: 'error', message: 'Channel already exists' });
|
||||
}
|
||||
|
||||
const newChannel = new Channel(name, url);
|
||||
channels.push(newChannel);
|
||||
res.status(201).json({ status: 'success', message: 'Channel added', channel: newChannel });
|
||||
}
|
||||
|
||||
const ChannelService = require('../services/ChannelService');
|
||||
|
||||
module.exports = {
|
||||
setCurrent,
|
||||
getCurrent,
|
||||
getChannels,
|
||||
addChannel
|
||||
};
|
||||
getChannels(req, res) {
|
||||
res.json(ChannelService.getChannels());
|
||||
},
|
||||
|
||||
getCurrentChannel(req, res) {
|
||||
res.json(ChannelService.getCurrentChannel());
|
||||
},
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
const ffmpegService = require('../services/FFmpegService');
|
||||
const storageService = require('../services/StorageService');
|
||||
const ffmpegService = require('../services/streaming/FFmpegService');
|
||||
const storageService = require('../services/streaming/StorageService');
|
||||
|
||||
function start() {
|
||||
if (!ffmpegService.isFFmpegRunning()) {
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
const express = require('express');
|
||||
const dotenv = require('dotenv');
|
||||
|
||||
const channelController = require('./controllers/ChannelController');
|
||||
const streamController = require('./controllers/StreamController');
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
|
||||
const apiRouter = express.Router();
|
||||
|
||||
apiRouter.post('/current', channelController.setCurrent);
|
||||
apiRouter.get('/current', channelController.getCurrent);
|
||||
|
||||
apiRouter.get('/', channelController.getChannels);
|
||||
apiRouter.post('/add', channelController.addChannel);
|
||||
|
||||
app.use('/channels', apiRouter);
|
||||
|
||||
const PORT = 5000;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server listening on Port ${PORT}`);
|
||||
streamController.start();
|
||||
});
|
||||
@@ -1,7 +1,10 @@
|
||||
class Channel {
|
||||
constructor(name, url) {
|
||||
static nextId = 0;
|
||||
constructor(name, url, avatar) {
|
||||
this.id = Channel.nextId++;
|
||||
this.name = name;
|
||||
this.url = url;
|
||||
this.avatar = avatar;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
9
backend/models/ChatMessage.js
Normal file
9
backend/models/ChatMessage.js
Normal file
@@ -0,0 +1,9 @@
|
||||
class ChatMessage {
|
||||
constructor(userId, message, timestamp) {
|
||||
this.userId = userId;
|
||||
this.message = message;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ChatMessage;
|
||||
240
backend/package-lock.json
generated
240
backend/package-lock.json
generated
@@ -11,7 +11,34 @@
|
||||
"dependencies": {
|
||||
"child_process": "^1.0.2",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.21.1"
|
||||
"express": "^4.21.1",
|
||||
"socket.io": "^4.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="
|
||||
},
|
||||
"node_modules/@types/cookie": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz",
|
||||
"integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
|
||||
},
|
||||
"node_modules/@types/cors": {
|
||||
"version": "2.8.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz",
|
||||
"integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz",
|
||||
"integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.8"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
@@ -31,6 +58,14 @@
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
|
||||
},
|
||||
"node_modules/base64id": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
|
||||
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
|
||||
"engines": {
|
||||
"node": "^4.5.0 || >= 5.9"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.3",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
||||
@@ -117,6 +152,18 @@
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
|
||||
},
|
||||
"node_modules/cors": {
|
||||
"version": "2.8.5",
|
||||
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
|
||||
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
|
||||
"dependencies": {
|
||||
"object-assign": "^4",
|
||||
"vary": "^1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
@@ -182,6 +229,63 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io": {
|
||||
"version": "6.6.2",
|
||||
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz",
|
||||
"integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==",
|
||||
"dependencies": {
|
||||
"@types/cookie": "^0.4.1",
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/node": ">=10.0.0",
|
||||
"accepts": "~1.3.4",
|
||||
"base64id": "2.0.0",
|
||||
"cookie": "~0.7.2",
|
||||
"cors": "~2.8.5",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-parser": "~5.2.1",
|
||||
"ws": "~8.17.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-parser": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
|
||||
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io/node_modules/cookie": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io/node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
|
||||
@@ -475,6 +579,14 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
|
||||
@@ -670,6 +782,107 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io": {
|
||||
"version": "4.8.1",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
|
||||
"integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.4",
|
||||
"base64id": "~2.0.0",
|
||||
"cors": "~2.8.5",
|
||||
"debug": "~4.3.2",
|
||||
"engine.io": "~6.6.0",
|
||||
"socket.io-adapter": "~2.5.2",
|
||||
"socket.io-parser": "~4.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-adapter": {
|
||||
"version": "2.5.5",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz",
|
||||
"integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==",
|
||||
"dependencies": {
|
||||
"debug": "~4.3.4",
|
||||
"ws": "~8.17.1"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-adapter/node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-adapter/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
},
|
||||
"node_modules/socket.io-parser": {
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
|
||||
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-parser/node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-parser/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
},
|
||||
"node_modules/socket.io/node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
@@ -698,6 +911,11 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.19.8",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
@@ -721,6 +939,26 @@
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
"name": "iptv-restream",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
@@ -20,6 +21,7 @@
|
||||
"dependencies": {
|
||||
"child_process": "^1.0.2",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.21.1"
|
||||
"express": "^4.21.1",
|
||||
"socket.io": "^4.8.1"
|
||||
}
|
||||
}
|
||||
|
||||
52
backend/server.js
Normal file
52
backend/server.js
Normal file
@@ -0,0 +1,52 @@
|
||||
const express = require('express');
|
||||
const dotenv = require('dotenv');
|
||||
const { Server } = require('socket.io');
|
||||
|
||||
const ChatSocketHandler = require('./socket/ChatSocketHandler');
|
||||
const ChannelSocketHandler = require('./socket/ChannelSocketHandler');
|
||||
|
||||
const channelController = require('./controllers/ChannelController');
|
||||
const streamController = require('./controllers/StreamController');
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
const apiRouter = express.Router();
|
||||
|
||||
apiRouter.get('/', channelController.getChannels);
|
||||
apiRouter.get('/current', channelController.getCurrentChannel);
|
||||
|
||||
app.use('/channels', apiRouter);
|
||||
|
||||
const PORT = 5000;
|
||||
const server = app.listen(PORT, () => {
|
||||
console.log(`Server listening on Port ${PORT}`);
|
||||
//streamController.start();
|
||||
});
|
||||
|
||||
|
||||
// Web Sockets
|
||||
const io = new Server(server);
|
||||
|
||||
const connectedUsers = {};
|
||||
|
||||
io.on('connection', socket => {
|
||||
console.log('New client connected');
|
||||
|
||||
socket.on('new-user', userId => {
|
||||
connectedUsers[socket.id] = userId;
|
||||
socket.broadcast.emit('user-connected', userId);
|
||||
})
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
socket.broadcast.emit('user-disconnected', connectedUsers[socket.id]);
|
||||
delete connectedUsers[socket.id];
|
||||
})
|
||||
|
||||
ChannelSocketHandler(io, socket);
|
||||
|
||||
ChatSocketHandler(io, socket);
|
||||
|
||||
})
|
||||
45
backend/services/ChannelService.js
Normal file
45
backend/services/ChannelService.js
Normal file
@@ -0,0 +1,45 @@
|
||||
const ffmpegService = require('./streaming/FFmpegService');
|
||||
const storageService = require('./streaming/StorageService');
|
||||
const Channel = require('../models/Channel');
|
||||
|
||||
class ChannelService {
|
||||
constructor() {
|
||||
this.channels = [new Channel('DEFAULT_CHANNEL', process.env.DEFAULT_CHANNEL_URL, "https://images.unsplash.com/photo-1534308143481-c55f00be8bd7?w=64&h=64&fit=crop&crop=faces")];
|
||||
this.currentChannel = this.channels[0];
|
||||
}
|
||||
|
||||
getChannels() {
|
||||
return this.channels;
|
||||
}
|
||||
|
||||
addChannel(name, url, avatar) {
|
||||
const existing = this.channels.some(channel => channel.url === url);
|
||||
if (existing) {
|
||||
throw new Error('Channel already exists');
|
||||
}
|
||||
const newChannel = new Channel(name, url, avatar);
|
||||
this.channels.push(newChannel);
|
||||
|
||||
return newChannel;
|
||||
}
|
||||
|
||||
setCurrentChannel(id) {
|
||||
const nextChannel = this.channels.find(channel => channel.id === id);
|
||||
if (!nextChannel) {
|
||||
throw new Error('Channel does not exist');
|
||||
}
|
||||
if (this.currentChannel !== nextChannel) {
|
||||
const segmentNumber = storageService.getNextSegmentNumber();
|
||||
storageService.clearStorage();
|
||||
this.currentChannel = nextChannel;
|
||||
ffmpegService.startFFmpeg(nextChannel.url, segmentNumber);
|
||||
}
|
||||
return nextChannel;
|
||||
}
|
||||
|
||||
getCurrentChannel() {
|
||||
return this.currentChannel;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new ChannelService();
|
||||
20
backend/services/ChatService.js
Normal file
20
backend/services/ChatService.js
Normal file
@@ -0,0 +1,20 @@
|
||||
const ChatMessage = require('../models/ChatMessage');
|
||||
|
||||
// At the moment, this service is not used! It is only a placeholder for future development for a persistent chat.
|
||||
class ChatService {
|
||||
constructor() {
|
||||
this.messages = [];
|
||||
}
|
||||
|
||||
addMessage(userId, message, timestamp) {
|
||||
const newChatMessage = new ChatMessage(userId, message, timestamp);
|
||||
this.messages.push(newChatMessage);
|
||||
return newChatMessage;
|
||||
}
|
||||
|
||||
getMessages() {
|
||||
return this.messages;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new ChatService();
|
||||
14
backend/services/SocketService.js
Normal file
14
backend/services/SocketService.js
Normal file
@@ -0,0 +1,14 @@
|
||||
let io;
|
||||
|
||||
function setSocketIO(socketIOInstance) {
|
||||
io = socketIOInstance;
|
||||
}
|
||||
|
||||
function getSocketIO() {
|
||||
if (!io) {
|
||||
throw new Error('Socket.IO instance not initialized');
|
||||
}
|
||||
return io;
|
||||
}
|
||||
|
||||
module.exports = { setSocketIO, getSocketIO };
|
||||
24
backend/socket/ChannelSocketHandler.js
Normal file
24
backend/socket/ChannelSocketHandler.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const ChannelService = require('../services/ChannelService');
|
||||
|
||||
module.exports = (io, socket) => {
|
||||
|
||||
socket.on('add-channel', ({ name, url, avatar }) => {
|
||||
try {
|
||||
const newChannel = ChannelService.addChannel(name, url, avatar);
|
||||
io.emit('channel-added', newChannel); // Broadcast to all clients
|
||||
} catch (err) {
|
||||
socket.emit('app-error', { message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
socket.on('set-current-channel', (id) => {
|
||||
try {
|
||||
const nextChannel = ChannelService.setCurrentChannel(id);
|
||||
io.emit('channel-selected', nextChannel); // Broadcast to all clients
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
socket.emit('app-error', { message: err.message });
|
||||
}
|
||||
});
|
||||
};
|
||||
10
backend/socket/ChatSocketHandler.js
Normal file
10
backend/socket/ChatSocketHandler.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const ChatService = require('../services/ChatService');
|
||||
const ChatMessage = require('../models/ChatMessage');
|
||||
|
||||
module.exports = (io, socket) => {
|
||||
socket.on('send-message', ({ userId, message, timestamp }) => {
|
||||
|
||||
const chatMessage = ChatService.addMessage(userId, message, timestamp);
|
||||
socket.broadcast.emit('chat-message', chatMessage) // Broadcast to all clients except sender
|
||||
});
|
||||
};
|
||||
143
frontend/package-lock.json
generated
143
frontend/package-lock.json
generated
@@ -11,7 +11,8 @@
|
||||
"hls.js": "^1.4.0",
|
||||
"lucide-react": "^0.344.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
"react-dom": "^18.3.1",
|
||||
"socket.io-client": "^4.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.9.1",
|
||||
@@ -1209,6 +1210,11 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||
@@ -1894,7 +1900,6 @@
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
@@ -1943,6 +1948,26 @@
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/engine.io-client": {
|
||||
"version": "6.6.2",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.2.tgz",
|
||||
"integrity": "sha512-TAr+NKeoVTjEVW8P3iHguO1LO6RlUz9O5Y8o7EY0fU+gY1NYqas7NN3slpFtbXEsLMHk0h90fJMfKjRkQ0qUIw==",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-parser": "~5.2.1",
|
||||
"ws": "~8.17.1",
|
||||
"xmlhttprequest-ssl": "~2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-parser": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
|
||||
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
||||
@@ -2856,8 +2881,7 @@
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
},
|
||||
"node_modules/mz": {
|
||||
"version": "2.7.0",
|
||||
@@ -3473,6 +3497,32 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-client": {
|
||||
"version": "4.8.1",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
|
||||
"integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.2",
|
||||
"engine.io-client": "~6.6.1",
|
||||
"socket.io-parser": "~4.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-parser": {
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
|
||||
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
@@ -4036,6 +4086,34 @@
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xmlhttprequest-ssl": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
|
||||
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
@@ -4772,6 +4850,11 @@
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@socket.io/component-emitter": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="
|
||||
},
|
||||
"@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||
@@ -5234,7 +5317,6 @@
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ms": "^2.1.3"
|
||||
}
|
||||
@@ -5275,6 +5357,23 @@
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
||||
"dev": true
|
||||
},
|
||||
"engine.io-client": {
|
||||
"version": "6.6.2",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.2.tgz",
|
||||
"integrity": "sha512-TAr+NKeoVTjEVW8P3iHguO1LO6RlUz9O5Y8o7EY0fU+gY1NYqas7NN3slpFtbXEsLMHk0h90fJMfKjRkQ0qUIw==",
|
||||
"requires": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-parser": "~5.2.1",
|
||||
"ws": "~8.17.1",
|
||||
"xmlhttprequest-ssl": "~2.1.1"
|
||||
}
|
||||
},
|
||||
"engine.io-parser": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
|
||||
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="
|
||||
},
|
||||
"esbuild": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
||||
@@ -5938,8 +6037,7 @@
|
||||
"ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
},
|
||||
"mz": {
|
||||
"version": "2.7.0",
|
||||
@@ -6327,6 +6425,26 @@
|
||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||
"dev": true
|
||||
},
|
||||
"socket.io-client": {
|
||||
"version": "4.8.1",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
|
||||
"integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
|
||||
"requires": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.2",
|
||||
"engine.io-client": "~6.6.1",
|
||||
"socket.io-parser": "~4.2.4"
|
||||
}
|
||||
},
|
||||
"socket.io-parser": {
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
|
||||
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
|
||||
"requires": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1"
|
||||
}
|
||||
},
|
||||
"source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
@@ -6682,6 +6800,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ws": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"requires": {}
|
||||
},
|
||||
"xmlhttprequest-ssl": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
|
||||
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ=="
|
||||
},
|
||||
"yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
|
||||
@@ -10,10 +10,11 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"hls.js": "^1.4.0",
|
||||
"lucide-react": "^0.344.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"hls.js": "^1.4.0"
|
||||
"socket.io-client": "^4.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.9.1",
|
||||
@@ -31,4 +32,4 @@
|
||||
"typescript-eslint": "^8.3.0",
|
||||
"vite": "^5.4.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,94 +1,68 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Search, Plus, Settings, Users, Radio, MessageSquare, Tv2 } from 'lucide-react';
|
||||
import VideoPlayer from './components/VideoPlayer';
|
||||
import ChannelList from './components/ChannelList';
|
||||
import Chat from './components/Chat';
|
||||
import Chat from './components/chat/Chat';
|
||||
import AddChannelModal from './components/AddChannelModal';
|
||||
import { Channel } from './types';
|
||||
import socketService from './services/SocketService';
|
||||
|
||||
function App() {
|
||||
const [channels, setChannels] = useState<Channel[]>([
|
||||
{
|
||||
id: 1,
|
||||
id: 100,
|
||||
name: 'Das Erste',
|
||||
url: 'https://mcdn.daserste.de/daserste/de/master1080p5000.m3u8',
|
||||
isLive: true,
|
||||
avatar: 'https://upload.wikimedia.org/wikipedia/commons/thumb/5/56/Das_Erste-Logo_klein.svg/768px-Das_Erste-Logo_klein.svg.png'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
id: 200,
|
||||
name: 'ZDF',
|
||||
url: 'https://mcdn.daserste.de/daserste/de/master1080p5000.m3u8',
|
||||
isLive: true,
|
||||
avatar: 'https://upload.wikimedia.org/wikipedia/commons/thumb/c/c1/ZDF_logo.svg/2560px-ZDF_logo.svg.png'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
id: 300,
|
||||
name: 'Creative Studio',
|
||||
url: 'https://mcdn.daserste.de/daserste/de/master1080p5000.m3u8',
|
||||
isLive: false,
|
||||
avatar: 'https://images.unsplash.com/photo-1534308143481-c55f00be8bd7?w=64&h=64&fit=crop&crop=faces'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
id: 400,
|
||||
name: 'Creative Studio',
|
||||
url: 'https://mcdn.daserste.de/daserste/de/master1080p5000.m3u8',
|
||||
isLive: false,
|
||||
avatar: 'https://images.unsplash.com/photo-1534308143481-c55f00be8bd7?w=64&h=64&fit=crop&crop=faces'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Creative Studio',
|
||||
url: 'https://mcdn.daserste.de/daserste/de/master1080p5000.m3u8',
|
||||
isLive: false,
|
||||
avatar: 'https://images.unsplash.com/photo-1534308143481-c55f00be8bd7?w=64&h=64&fit=crop&crop=faces'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'Creative Studio',
|
||||
url: 'https://mcdn.daserste.de/daserste/de/master1080p5000.m3u8',
|
||||
isLive: false,
|
||||
avatar: 'https://images.unsplash.com/photo-1534308143481-c55f00be8bd7?w=64&h=64&fit=crop&crop=faces'
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: 'Creative Studio',
|
||||
url: 'https://mcdn.daserste.de/daserste/de/master1080p5000.m3u8',
|
||||
isLive: false,
|
||||
avatar: 'https://images.unsplash.com/photo-1534308143481-c55f00be8bd7?w=64&h=64&fit=crop&crop=faces'
|
||||
},
|
||||
|
||||
]);
|
||||
|
||||
const [selectedChannel, setSelectedChannel] = useState<Channel>(channels[0]);
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
const addChannel = (channel: Omit<Channel, 'id' | 'avatar'>) => {
|
||||
const newChannel = {
|
||||
...channel,
|
||||
id: channels.length + 1,
|
||||
avatar: `https://images.unsplash.com/photo-${Math.floor(Math.random() * 1000)}?w=64&h=64&fit=crop&crop=faces`,
|
||||
};
|
||||
setChannels([...channels, newChannel]);
|
||||
};
|
||||
|
||||
const handleChannelChange = (channel: Channel) => {
|
||||
setSelectedChannel(channel);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const systemMessage = {
|
||||
id: Date.now(),
|
||||
user: {
|
||||
name: 'System',
|
||||
avatar: ''
|
||||
},
|
||||
message: `Switched to ${selectedChannel.name}'s stream`,
|
||||
timestamp: new Date().toISOString()
|
||||
|
||||
socketService.connect();
|
||||
|
||||
console.log('Subscribing to events');
|
||||
const channelAddedListener = (channel: Channel) => {
|
||||
setChannels((prevChannels) => [...prevChannels, channel]);
|
||||
};
|
||||
window.dispatchEvent(new CustomEvent('newChatMessage', { detail: systemMessage }));
|
||||
}, [selectedChannel]);
|
||||
|
||||
const channelSelectedListener = (nextChannel: Channel) => {
|
||||
setSelectedChannel(nextChannel);
|
||||
};
|
||||
|
||||
socketService.subscribeToEvent('channel-added', channelAddedListener);
|
||||
socketService.subscribeToEvent('channel-selected', channelSelectedListener);
|
||||
|
||||
return () => {
|
||||
socketService.unsubscribeFromEvent('channel-added', channelAddedListener);
|
||||
socketService.unsubscribeFromEvent('channel-selected', channelSelectedListener);
|
||||
socketService.disconnect();
|
||||
console.log('WebSocket connection closed');
|
||||
};
|
||||
}, []);
|
||||
|
||||
const filteredChannels = channels.filter(channel =>
|
||||
channel.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
@@ -137,7 +111,6 @@ function App() {
|
||||
<ChannelList
|
||||
channels={filteredChannels}
|
||||
selectedChannel={selectedChannel}
|
||||
onSelectChannel={handleChannelChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -153,7 +126,6 @@ function App() {
|
||||
<AddChannelModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
onAdd={addChannel}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import React, { useState } from 'react';
|
||||
import { X } from 'lucide-react';
|
||||
import { Channel } from '../types';
|
||||
import socketService from '../services/SocketService';
|
||||
|
||||
interface AddChannelModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onAdd: (channel: Omit<Channel, 'id'>) => void;
|
||||
}
|
||||
|
||||
function AddChannelModal({ isOpen, onClose, onAdd }: AddChannelModalProps) {
|
||||
function AddChannelModal({ isOpen, onClose}: AddChannelModalProps) {
|
||||
const [name, setName] = useState('');
|
||||
const [url, setUrl] = useState('');
|
||||
|
||||
@@ -16,11 +15,7 @@ function AddChannelModal({ isOpen, onClose, onAdd }: AddChannelModalProps) {
|
||||
e.preventDefault();
|
||||
if (!name.trim() || !url.trim()) return;
|
||||
|
||||
onAdd({
|
||||
name: name.trim(),
|
||||
url: url.trim(),
|
||||
isLive: true,
|
||||
});
|
||||
socketService.addChannel(name.trim(), url.trim(), `https://images.unsplash.com/photo-${Math.floor(Math.random() * 1000)}?w=64&h=64&fit=crop&crop=faces`);
|
||||
|
||||
setName('');
|
||||
setUrl('');
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import React from 'react';
|
||||
import { Channel } from '../types';
|
||||
import socketService from '../services/SocketService';
|
||||
|
||||
interface ChannelListProps {
|
||||
channels: Channel[];
|
||||
selectedChannel: Channel;
|
||||
onSelectChannel: (channel: Channel) => void;
|
||||
}
|
||||
|
||||
function ChannelList({ channels, selectedChannel, onSelectChannel }: ChannelListProps) {
|
||||
function ChannelList({ channels, selectedChannel}: ChannelListProps) {
|
||||
|
||||
const onSelectChannel = (channel: Channel) => {
|
||||
socketService.setCurrentChannel(channel.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex space-x-3 hover:overflow-x-auto overflow-hidden pb-2 px-1 pt-1 scroll-container">
|
||||
@@ -21,21 +25,18 @@ function ChannelList({ channels, selectedChannel, onSelectChannel }: ChannelList
|
||||
: 'hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
<div className="h-20 w-20 mb-2 flex items-center justify-center rounded-lg overflow-hidden mx-auto">
|
||||
<div className="h-20 w-20 mb-2 flex items-center justify-center rounded-lg overflow-hidden mx-auto">
|
||||
<img
|
||||
src={channel.avatar}
|
||||
alt={channel.name}
|
||||
className="w-full h-full object-contain rounded-lg transition-transform group-hover:scale-105"
|
||||
/>
|
||||
|
||||
</div>
|
||||
<p className="text-sm font-medium truncate text-center">
|
||||
{channel.name}
|
||||
</p>
|
||||
<p className="text-sm font-medium truncate text-center">{channel.name}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChannelList;
|
||||
export default ChannelList;
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Send, MessageSquare } from 'lucide-react';
|
||||
import { ChatMessage } from '../types';
|
||||
|
||||
function Chat() {
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([
|
||||
{
|
||||
id: 1,
|
||||
user: {
|
||||
name: 'Alex Thompson',
|
||||
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=64&h=64&fit=crop&crop=faces',
|
||||
},
|
||||
message: 'Amazing stream today! 🔥',
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
user: {
|
||||
name: 'Sarah Chen',
|
||||
avatar: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=64&h=64&fit=crop&crop=faces',
|
||||
},
|
||||
message: 'The quality is incredible!',
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
]);
|
||||
const [newMessage, setNewMessage] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const handleNewMessage = (event: CustomEvent<ChatMessage>) => {
|
||||
setMessages(prev => [...prev, event.detail]);
|
||||
};
|
||||
|
||||
window.addEventListener('newChatMessage', handleNewMessage as EventListener);
|
||||
return () => {
|
||||
window.removeEventListener('newChatMessage', handleNewMessage as EventListener);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleSendMessage = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!newMessage.trim()) return;
|
||||
|
||||
const message: ChatMessage = {
|
||||
id: messages.length + 1,
|
||||
user: {
|
||||
name: 'You',
|
||||
avatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=64&h=64&fit=crop&crop=faces',
|
||||
},
|
||||
message: newMessage,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
setMessages([...messages, message]);
|
||||
setNewMessage('');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-gray-800 rounded-lg h-full">
|
||||
<div className="flex items-center space-x-2 p-4 border-b border-gray-700">
|
||||
<MessageSquare className="w-5 h-5 text-blue-500" />
|
||||
<h2 className="text-xl font-semibold">Live Chat</h2>
|
||||
</div>
|
||||
|
||||
<div className="h-[calc(100vh-13rem)] overflow-y-auto p-4 space-y-4 scroll-container vertical-scroll-container">
|
||||
{messages.map((msg) => (
|
||||
<div key={msg.id} className="flex items-start space-x-3">
|
||||
{msg.user.avatar ? (
|
||||
<img
|
||||
src={msg.user.avatar}
|
||||
alt={msg.user.name}
|
||||
className="w-8 h-8 rounded-full"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-8" /> // Spacer for system messages
|
||||
)}
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className={`font-medium ${msg.user.name === 'System' ? 'text-blue-400' : ''}`}>
|
||||
{msg.user.name}
|
||||
</span>
|
||||
<span className="text-xs text-gray-400">
|
||||
{new Date(msg.timestamp).toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-gray-300">{msg.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSendMessage} className="p-4 border-t border-gray-700">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
value={newMessage}
|
||||
onChange={(e) => setNewMessage(e.target.value)}
|
||||
placeholder="Type a message..."
|
||||
className="w-full bg-gray-700 rounded-lg pl-4 pr-12 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 bg-blue-500 hover:bg-blue-600 p-1.5 rounded-lg transition-colors"
|
||||
>
|
||||
<Send className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Chat;
|
||||
90
frontend/src/components/chat/Chat.tsx
Normal file
90
frontend/src/components/chat/Chat.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Send, MessageSquare } from 'lucide-react';
|
||||
import socketService from '../../services/SocketService';
|
||||
import { Channel, ChatMessage } from '../../types';
|
||||
import { SendMessage } from './SendMessage';
|
||||
import { SystemMessage } from './SystemMessage';
|
||||
import { ReceivedMessage } from './ReceivedMessage';
|
||||
|
||||
function Chat() {
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||
const [newMessage, setNewMessage] = useState('');
|
||||
const [userName] = useState('You');
|
||||
|
||||
useEffect(() => {
|
||||
const messageListener = (message: ChatMessage) => {
|
||||
setMessages((prevMessages) => [...prevMessages, message]);
|
||||
};
|
||||
socketService.subscribeToEvent('chat-message', messageListener);
|
||||
|
||||
const channelSelectedListener = (selectedChannel: Channel) => {
|
||||
const systemMessage = {
|
||||
message: `Switched to ${selectedChannel.name}'s stream`,
|
||||
timestamp: new Date().toISOString(),
|
||||
userId: 'System',
|
||||
};
|
||||
setMessages((prevMessages) => [...prevMessages, systemMessage]);
|
||||
}
|
||||
socketService.subscribeToEvent('channel-selected', channelSelectedListener);
|
||||
|
||||
return () => {
|
||||
socketService.unsubscribeFromEvent('chat-message', messageListener);
|
||||
socketService.unsubscribeFromEvent('channel-selected', channelSelectedListener);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleSendMessage = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!newMessage.trim()) return;
|
||||
|
||||
socketService.sendMessage(userName, newMessage, new Date().toISOString());
|
||||
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{
|
||||
userId: userName,
|
||||
message: newMessage,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
]);
|
||||
setNewMessage('');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-gray-800 rounded-lg h-full">
|
||||
<div className="flex items-center space-x-2 p-4 border-b border-gray-700">
|
||||
<MessageSquare className="w-5 h-5 text-blue-500" />
|
||||
<h2 className="text-xl font-semibold">Live Chat</h2>
|
||||
</div>
|
||||
|
||||
<div className="h-[calc(100vh-13rem)] overflow-y-auto p-4 space-y-4 scroll-container vertical-scroll-container">
|
||||
{messages.map((msg) => {
|
||||
if(msg.userId === userName) {
|
||||
return <SendMessage msg={msg}></SendMessage>;
|
||||
} else if(msg.userId === 'System') {
|
||||
return <SystemMessage msg={msg}></SystemMessage>;
|
||||
} else {
|
||||
return <ReceivedMessage msg={msg}></ReceivedMessage>;
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSendMessage} className="p-4 border-t border-gray-700">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
value={newMessage}
|
||||
onChange={(e) => setNewMessage(e.target.value)}
|
||||
placeholder="Type a message..."
|
||||
className="w-full bg-gray-700 rounded-lg pl-4 pr-12 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
<button type="submit" className="absolute right-2 top-1/2 -translate-y-1/2 bg-blue-500 hover:bg-blue-600 p-1.5 rounded-lg transition-colors">
|
||||
<Send className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Chat;
|
||||
21
frontend/src/components/chat/ReceivedMessage.tsx
Normal file
21
frontend/src/components/chat/ReceivedMessage.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { ChatMessage } from "../../types";
|
||||
|
||||
export function ReceivedMessage({ msg }: {
|
||||
msg: ChatMessage
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-start space-x-3 pr-10">
|
||||
{/* TODO: fetch random images */}
|
||||
<img src={`https://images.unsplash.com/photo-${Math.floor(Math.random() * 1000)}?w=64&h=64&fit=crop&crop=faces`} alt={msg.userId} className="w-8 h-8 rounded-full" />
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="font-medium">{msg.userId}</span>
|
||||
<span className="text-xs text-gray-400">
|
||||
{new Date(msg.timestamp).toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-gray-300">{msg.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
21
frontend/src/components/chat/SendMessage.tsx
Normal file
21
frontend/src/components/chat/SendMessage.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { ChatMessage } from "../../types";
|
||||
|
||||
export function SendMessage({ msg }: {
|
||||
msg: ChatMessage
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-start justify-end space-x-3 pl-10">
|
||||
{/* TODO: fetch random images */}
|
||||
<img src={`https://images.unsplash.com/photo-${Math.floor(Math.random() * 1000)}?w=64&h=64&fit=crop&crop=faces`} alt={msg.userId} className="w-8 h-8 rounded-full" />
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="font-medium">{msg.userId}</span>
|
||||
<span className="text-xs text-gray-400">
|
||||
{new Date(msg.timestamp).toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-gray-300">{msg.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
18
frontend/src/components/chat/SystemMessage.tsx
Normal file
18
frontend/src/components/chat/SystemMessage.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { ChatMessage } from "../../types";
|
||||
|
||||
export function SystemMessage({ msg }: {
|
||||
msg: ChatMessage
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className="ml-11">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-xs text-gray-400">
|
||||
{new Date(msg.timestamp).toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-blue-400">{msg.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,5 @@ import App from './App.tsx'
|
||||
import './index.css'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
<App />,
|
||||
)
|
||||
|
||||
87
frontend/src/services/SocketService.ts
Normal file
87
frontend/src/services/SocketService.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { io, Socket } from 'socket.io-client';
|
||||
import { Channel, ChatMessage } from '../types';
|
||||
|
||||
class SocketService {
|
||||
private socket: Socket | null = null;
|
||||
|
||||
private listeners: Map<string, ((data: any) => void)[]> = new Map();
|
||||
|
||||
// Initialize
|
||||
connect() {
|
||||
if (this.socket?.connected) return;
|
||||
|
||||
console.log('Connecting to WebSocket server');
|
||||
this.socket = io(import.meta.env.BACKEND_WS_URL);
|
||||
|
||||
this.socket.on('connect', () => {
|
||||
console.log('Connected to WebSocket server');
|
||||
});
|
||||
|
||||
this.socket.on('disconnect', () => {
|
||||
console.log('Disconnected from WebSocket server');
|
||||
});
|
||||
|
||||
this.socket.on('app-error', (error) => {
|
||||
console.error('Failed:', error);
|
||||
});
|
||||
|
||||
|
||||
// Listen for incoming custom events
|
||||
this.socket.onAny((event: string, data: any) => {
|
||||
const eventListeners = this.listeners.get(event);
|
||||
if (eventListeners) {
|
||||
eventListeners.forEach((listener) => listener(data));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.socket) {
|
||||
this.socket.disconnect();
|
||||
this.socket = null;
|
||||
}
|
||||
}
|
||||
|
||||
subscribeToEvent<T>(event: string, listener: (data: T) => void) {
|
||||
if (!this.listeners.has(event)) {
|
||||
this.listeners.set(event, []);
|
||||
}
|
||||
this.listeners.get(event)?.push(listener);
|
||||
}
|
||||
|
||||
// Event abbestellen
|
||||
unsubscribeFromEvent<T>(event: string, listener: (data: T) => void) {
|
||||
const eventListeners = this.listeners.get(event);
|
||||
if (eventListeners) {
|
||||
this.listeners.set(
|
||||
event,
|
||||
eventListeners.filter((existingListener) => existingListener !== listener)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Nachricht senden
|
||||
sendMessage(userId, message, timestamp) {
|
||||
if (!this.socket) throw new Error('Socket is not connected.');
|
||||
|
||||
this.socket.emit('send-message', { userId, message, timestamp });
|
||||
}
|
||||
|
||||
// Channel hinzufügen
|
||||
addChannel(name, url, avatar) {
|
||||
if (!this.socket) throw new Error('Socket is not connected.');
|
||||
|
||||
this.socket.emit('add-channel', { name, url, avatar });
|
||||
}
|
||||
|
||||
// Aktuellen Channel setzen
|
||||
setCurrentChannel(id) {
|
||||
if (!this.socket) throw new Error('Socket is not connected.');
|
||||
|
||||
this.socket.emit('set-current-channel', id);
|
||||
}
|
||||
}
|
||||
|
||||
const socketService = new SocketService();
|
||||
export default socketService;
|
||||
@@ -1,3 +1,4 @@
|
||||
// Not used
|
||||
export interface User {
|
||||
name: string;
|
||||
avatar: string;
|
||||
@@ -7,13 +8,11 @@ export interface Channel {
|
||||
id: number;
|
||||
name: string;
|
||||
url: string;
|
||||
isLive: boolean;
|
||||
avatar: string;
|
||||
}
|
||||
|
||||
export interface ChatMessage {
|
||||
id: number;
|
||||
user: User;
|
||||
userId: string;
|
||||
message: string;
|
||||
timestamp: string;
|
||||
}
|
||||
@@ -7,4 +7,7 @@ export default defineConfig({
|
||||
optimizeDeps: {
|
||||
exclude: ['lucide-react'],
|
||||
},
|
||||
server: {
|
||||
port: 8080,
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user