-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add cross-pod co-broadcaster transport creation #97
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -223,28 +223,120 @@ const registerBroadcasterHandler = async (socket: Socket) => { | |
| try { | ||
| const room = getRoom(roomId); | ||
| const hostUserId = socket.data.user?.id; | ||
| const router = room.router; | ||
| const socketId = socket.id; | ||
|
|
||
| const roomKey = `room:${roomId}`; | ||
| const redisRoom = await getRedisRoom(roomKey) | ||
| //TODO: Implementation for different pod connections | ||
| if(redisRoom.nodeId !== config.instanceId){ | ||
| logger.error('Different pod') | ||
| throw new ApiError(409,"Room belongs to another node") | ||
| let redisRoom; | ||
| try { | ||
| redisRoom = await getRedisRoom(roomKey) | ||
| } catch (error) { | ||
| logger.error('Error finding redis room',{ | ||
| error: (error as Error).message, | ||
| stack: (error as Error).stack | ||
| }) | ||
|
|
||
| ack({success: false, code: "TRANSPORT_CREATION_FAILED"}) | ||
| return; | ||
| } | ||
|
|
||
|
|
||
| if(redisRoom?.nodeId !== config.instanceId){ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: When the socket connects to a pod that does not own the room, Prompt for AI agents |
||
| const requestId = crypto.randomUUID() | ||
| const date = Date.now(); | ||
| const args = {roomId, socketId}; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Any authenticated socket that knows a room ID can reach this forwarding path without broadcaster authorization. Validate that the caller is an approved broadcaster/co-host on the owning pod before adding broadcaster state or creating the transport. Prompt for AI agents |
||
| const replyTo = `pod:${config.instanceId}:response`; | ||
|
|
||
| const payLoad : PodCommandPayload = { | ||
| requestId, | ||
| type: 'createBroadcasterTransport', | ||
| args, | ||
| replyTo, | ||
| date | ||
| } | ||
|
|
||
| const TIMEOUTMS = 5000; | ||
| const timeOuthandle = setTimeout(() => { | ||
| const entry = podRequestHandleMap.get(requestId); | ||
| if(!entry) return logger.warn(`Entry not found with requestId: ${requestId}`); | ||
| entry.onComplete({}, 'TRANSPORT_CREATION_FAILED'); | ||
| podRequestHandleMap.delete(requestId); | ||
| }, TIMEOUTMS) | ||
|
|
||
| podRequestHandleMap.set(requestId, { | ||
| requestId, | ||
| socketId, | ||
| startDate: date, | ||
| requestType: 'createBroadcasterTransport', | ||
| status: 'pending', | ||
| replyTo, | ||
|
|
||
| onComplete: (result, error) => { | ||
| clearTimeout(timeOuthandle) | ||
|
|
||
| if(error){ | ||
| logger.error('[Transport] pod error', { | ||
| error : error | ||
| }) | ||
|
|
||
| ack({success: false, code: 'TRANSPORT_CREATION_FAILED'}) | ||
| return; | ||
| } | ||
|
|
||
| ack({success: true, data: result}) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift Forward the remaining broadcaster signaling operations. Line 284 returns success to the socket on POD B. Forward both operations to the owning pod, or enforce sticky routing for the socket after transport creation. 🤖 Prompt for AI Agents |
||
|
|
||
| void Broadcaster.findOneAndUpdate( | ||
| { | ||
| broadcasterId: hostUserId, | ||
| roomId, | ||
| }, | ||
| { | ||
| $push: { | ||
| transportIds: result?.id, | ||
| }, | ||
| } | ||
| ).catch((error) => { | ||
| logger.error("Failed to update broadcaster transport", error); | ||
| }); | ||
|
|
||
| } | ||
|
|
||
| }) | ||
|
|
||
| if(!redisRoom.nodeId){ | ||
| logger.error('Redis room nodeId not found'); | ||
| podRequestHandleMap.delete(requestId); | ||
| ack({success: false, code: 'TRANSPORT_CREATION_FAILED'}) | ||
| return; | ||
| } | ||
|
|
||
| let receivers: number; | ||
| try { | ||
| receivers = await publishCommand(payLoad, redisRoom.nodeId) | ||
| } catch (error) { | ||
| clearTimeout(timeOuthandle); | ||
| podRequestHandleMap.delete(requestId); | ||
| throw error; | ||
| } | ||
| if(receivers === 0){ | ||
| logger.error('Cross [POD] connection failed'); | ||
| clearTimeout(timeOuthandle); | ||
| podRequestHandleMap.delete(requestId); | ||
| ack({success: false, code: 'TRANSPORT_CREATION_FAILED'}) | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| const router = room.router; | ||
| const broadcasterTransport = | ||
| await createWebRtcTransport( | ||
| router, | ||
| roomId, | ||
| socket.id, | ||
| socketId, | ||
| "producer" | ||
| ); | ||
|
|
||
| await saveBroadcasterTransport( | ||
| roomId, | ||
| socket.id, | ||
| socketId, | ||
| broadcasterTransport | ||
| ); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -7,7 +7,8 @@ | |||||||||
| import { heartBeat } from "./roomCordinator"; | ||||||||||
| import {consume} from '../handlers/viewer.handler' | ||||||||||
| import { pauseConsumer, resumeConsumer } from "../consumer/consumer.handler"; | ||||||||||
|
|
||||||||||
| import { createWebRtcTransport } from "../mediasoup/transport"; | ||||||||||
| import { addBroadcaster, saveBroadcasterTransport } from "./broadcaster.util"; | ||||||||||
|
|
||||||||||
| export interface PodCommandPayload { | ||||||||||
| type: string; | ||||||||||
|
|
@@ -76,7 +77,33 @@ | |||||||||
| break; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| case(type === ''): {} | ||||||||||
| case(type === 'createBroadcasterTransport'): { | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: When the broadcaster socket is on a different pod from the room, this branch returns a transport created on the owner pod, but the socket’s subsequent connect and produce events still run only on the requesting pod. Forward those operations to the owner pod as well, or keep the transport on the requesting pod; otherwise cross-pod transport creation produces an unusable transport. Prompt for AI agents |
||||||||||
| const {roomId, socketId} = args as unknown as generalArgs; | ||||||||||
| const routerId = roomToRouter.get(roomId); | ||||||||||
| if(!routerId){ | ||||||||||
| error = 'RouterId not found' | ||||||||||
|
Check warning on line 84 in backend/src/utils/podConnection.ts
|
||||||||||
| throw new Error('RouterId not found') | ||||||||||
| } | ||||||||||
| const router = getRouter(routerId); | ||||||||||
|
|
||||||||||
| const broadcasterTransport = await createWebRtcTransport(router,roomId, socketId, 'producer'); | ||||||||||
|
|
||||||||||
| result = { | ||||||||||
| id: broadcasterTransport?.id, | ||||||||||
| iceParameters: broadcasterTransport?.iceParameters, | ||||||||||
| iceCandidates: broadcasterTransport?.iceCandidates, | ||||||||||
| dtlsParameters: broadcasterTransport?.dtlsParameters, | ||||||||||
| } | ||||||||||
| addBroadcaster(roomId, socketId) | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: When the same socket retries Prompt for AI agents
Suggested change
|
||||||||||
| await saveBroadcasterTransport(roomId, socketId, broadcasterTransport) | ||||||||||
|
|
||||||||||
| const payLoad: PodResponsePayload = { | ||||||||||
| requestId, | ||||||||||
| result | ||||||||||
| } | ||||||||||
| await publishResponse(payLoad, replyTo) | ||||||||||
| break; | ||||||||||
| } | ||||||||||
| case(type === ''): {} | ||||||||||
| case(type === ''): {} | ||||||||||
|
|
||||||||||
|
|
@@ -266,15 +293,16 @@ | |||||||||
| podRequestHandleMap.delete(payload.requestId) | ||||||||||
| } | ||||||||||
|
|
||||||||||
| export const publishCommand = async(payload: PodCommandPayload, targetNode:string) => { | ||||||||||
| export const publishCommand = async(payload: PodCommandPayload, targetNode:string):Promise<number> => { | ||||||||||
| const channel = `pod:${targetNode}:cmd` | ||||||||||
| const receivers = await redis.spublish(channel , JSON.stringify(payload)) | ||||||||||
| const receivers = await redis.spublish(channel , JSON.stringify(payload)) as number | ||||||||||
|
|
||||||||||
| if(receivers === 0){ | ||||||||||
| logger.error(`No subscribers for ${channel} — pod may be down`, { requestId: payload.requestId }); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| logger.info("Redis delivered to", receivers, "subscribers"); | ||||||||||
|
|
||||||||||
| return receivers; | ||||||||||
| } | ||||||||||
|
|
||||||||||
|
|
||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: Harxhit/CrowdStream
Length of output: 17189
🏁 Script executed:
Repository: Harxhit/CrowdStream
Length of output: 13783
Move
getRoom(roomId)into the local-owner branch.getRoomreads only localmemoryRoomand throws when the room is absent. A remote room that is not present on the requesting pod therefore returnsTRANSPORT_CREATION_FAILEDbeforepublishCommandruns. Move the lookup to the branch that usesroom.router.🤖 Prompt for AI Agents