@@ -8,12 +8,26 @@ import { fileURLToPath } from 'url';
88import { dirname , join } from 'path' ;
99import * as AuthControllers from './src/controllers/AuthControllers.js' ;
1010import { auth , authorizeRole } from './src/middleware/auth.js' ;
11+ import { createServer } from 'http' ;
12+ import { Server } from 'socket.io' ;
1113
1214const app = express ( ) ;
1315app . use ( morgan ( 'dev' ) ) ;
1416const PORT = process . env . PORT || 3000 ;
1517const MONGODB_URI = process . env . MONGODB_URI ;
1618
19+ // Create HTTP server and attach Socket.IO
20+ const httpServer = createServer ( app ) ;
21+ const io = new Server ( httpServer , {
22+ cors : {
23+ origin : process . env . CLIENT_URL
24+ ? [ process . env . CLIENT_URL ]
25+ : [ 'http://localhost:5173' , 'http://localhost:3000' ] ,
26+ methods : [ 'GET' , 'POST' ] ,
27+ credentials : true ,
28+ } ,
29+ } ) ;
30+
1731// Recreate __dirname since it is not available in ES Modules by default
1832const __filename = fileURLToPath ( import . meta. url ) ;
1933const __dirname = dirname ( __filename ) ;
@@ -132,16 +146,153 @@ app.get('/health', (req,res)=>{
132146 } ) ;
133147} ) ;
134148
149+ // ============== WEBRTC SIGNALING SERVER ==============
150+ // Track active streams: { roomId: { broadcaster: socketId, viewers: [socketId] } }
151+ const activeStreams = new Map ( ) ;
152+
153+ // API: Get list of active live streams
154+ app . get ( '/api/live-streams' , ( req , res ) => {
155+ const streams = [ ] ;
156+ activeStreams . forEach ( ( value , key ) => {
157+ streams . push ( { roomId : key , viewerCount : value . viewers . length } ) ;
158+ } ) ;
159+ res . json ( streams ) ;
160+ } ) ;
161+
162+ /**
163+ * Validate that a roomId exists in activeStreams and that the socket
164+ * has joined the corresponding Socket.IO room.
165+ * Returns the stream object on success, or null after emitting an error.
166+ */
167+ function validateRoom ( socket , roomId , eventName ) {
168+ if ( ! roomId || ! activeStreams . has ( roomId ) ) {
169+ socket . emit ( 'error' , {
170+ event : eventName ,
171+ message : `Room "${ roomId } " does not exist in active streams` ,
172+ } ) ;
173+ return null ;
174+ }
175+
176+ if ( ! socket . rooms . has ( roomId ) ) {
177+ socket . emit ( 'error' , {
178+ event : eventName ,
179+ message : `Socket is not a member of room "${ roomId } "` ,
180+ } ) ;
181+ return null ;
182+ }
183+
184+ return activeStreams . get ( roomId ) ;
185+ }
186+
187+ io . on ( 'connection' , ( socket ) => {
188+ console . log ( `[SOCKET] User connected: ${ socket . id } ` ) ;
189+
190+ // Broadcaster starts a stream
191+ socket . on ( 'start-stream' , ( roomId ) => {
192+ if ( activeStreams . has ( roomId ) ) {
193+ socket . emit ( 'error' , { message : 'Room already exists' } ) ;
194+ return ;
195+ }
196+ socket . join ( roomId ) ;
197+ activeStreams . set ( roomId , { broadcaster : socket . id , viewers : [ ] } ) ;
198+ console . log ( `[LIVE] Stream started: ${ roomId } by ${ socket . id } ` ) ;
199+ socket . emit ( 'stream-started' , { roomId } ) ;
200+ } ) ;
201+
202+ // Viewer joins a stream
203+ socket . on ( 'join-stream' , ( roomId ) => {
204+ const stream = activeStreams . get ( roomId ) ;
205+ if ( ! stream ) {
206+ socket . emit ( 'error' , { message : 'Stream not found' } ) ;
207+ return ;
208+ }
209+ socket . join ( roomId ) ;
210+ stream . viewers . push ( socket . id ) ;
211+ console . log ( `[VIEWER] ${ socket . id } joined ${ roomId } ` ) ;
212+
213+ // Notify broadcaster that a new viewer joined
214+ io . to ( stream . broadcaster ) . emit ( 'viewer-joined' , {
215+ viewerId : socket . id ,
216+ } ) ;
217+ } ) ;
218+
219+ // WebRTC signaling: Offer (broadcaster -> viewer)
220+ socket . on ( 'offer' , ( { roomId, offer, viewerId } ) => {
221+ const stream = validateRoom ( socket , roomId , 'offer' ) ;
222+ if ( ! stream ) return ;
223+
224+ io . to ( viewerId ) . emit ( 'offer' , { offer, broadcasterId : socket . id } ) ;
225+ } ) ;
226+
227+ // WebRTC signaling: Answer (viewer -> broadcaster)
228+ socket . on ( 'answer' , ( { roomId, answer, broadcasterId } ) => {
229+ const stream = validateRoom ( socket , roomId , 'answer' ) ;
230+ if ( ! stream ) return ;
231+
232+ io . to ( broadcasterId ) . emit ( 'answer' , { answer, viewerId : socket . id } ) ;
233+ } ) ;
234+
235+ // WebRTC signaling: ICE Candidate
236+ socket . on ( 'ice-candidate' , ( { roomId, candidate, targetId } ) => {
237+ const stream = validateRoom ( socket , roomId , 'ice-candidate' ) ;
238+ if ( ! stream ) return ;
239+
240+ io . to ( targetId ) . emit ( 'ice-candidate' , {
241+ candidate,
242+ senderId : socket . id ,
243+ } ) ;
244+ } ) ;
245+
246+ // Stop stream
247+ socket . on ( 'stop-stream' , ( roomId ) => {
248+ if ( activeStreams . has ( roomId ) ) {
249+ io . to ( roomId ) . emit ( 'stream-ended' ) ;
250+ activeStreams . delete ( roomId ) ;
251+ console . log ( `[ENDED] Stream ended: ${ roomId } ` ) ;
252+ }
253+ } ) ;
254+
255+ // Disconnect handling
256+ socket . on ( 'disconnect' , ( ) => {
257+ console . log ( `[SOCKET] User disconnected: ${ socket . id } ` ) ;
258+
259+ // Check all streams for this socket
260+ activeStreams . forEach ( ( stream , roomId ) => {
261+ if ( stream . broadcaster === socket . id ) {
262+ // Broadcaster disconnected - end the stream for all viewers
263+ io . to ( roomId ) . emit ( 'stream-ended' , {
264+ reason : 'Broadcaster disconnected' ,
265+ } ) ;
266+ activeStreams . delete ( roomId ) ;
267+ console . log (
268+ `[ENDED] Stream ${ roomId } ended (broadcaster disconnected)`
269+ ) ;
270+ } else if ( stream . viewers . includes ( socket . id ) ) {
271+ // Viewer disconnected - remove from list and notify broadcaster
272+ stream . viewers = stream . viewers . filter ( ( id ) => id !== socket . id ) ;
273+ io . to ( stream . broadcaster ) . emit ( 'viewer-left' , {
274+ viewerId : socket . id ,
275+ viewerCount : stream . viewers . length ,
276+ } ) ;
277+ console . log (
278+ `[VIEWER] ${ socket . id } left ${ roomId } . Viewers remaining: ${ stream . viewers . length } `
279+ ) ;
280+ }
281+ } ) ;
282+ } ) ;
283+ } ) ;
284+
135285// 404 Not Found handler (must be after all routes)
136286app . use ( ( req , res ) => {
137287 res . status ( 404 ) . json ( {
138288 error : 'Route not found' ,
139289 } ) ;
140290} ) ;
141291
142- app . listen ( PORT , ( ) => {
292+ httpServer . listen ( PORT , ( ) => {
143293 console . log ( `\nServer successfully started!` ) ;
144294 console . log ( `Home: http://localhost:${ PORT } ` ) ;
145295 console . log ( '\n✅ Server successfully started!' ) ;
146296 console . log ( `🏠 Home: http://localhost:${ PORT } ` ) ;
297+ console . log ( `WebRTC Signaling: ws://localhost:${ PORT } ` ) ;
147298} ) ;
0 commit comments