From 5505983dbdecfc822614eaca16600b2ba6324b06 Mon Sep 17 00:00:00 2001 From: asepharyana Date: Thu, 13 Aug 2026 01:44:40 +0700 Subject: [PATCH] =?UTF-8?q?fix(goLive):=20retry=20VIDEO(op12)/SPEAKING(op5?= =?UTF-8?q?)=20opcodes=20until=20ws=20OPEN=20=E2=80=94=20broken=20shared-s?= =?UTF-8?q?creen=20video?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: BaseMediaConnection.sendOpcode is a silent no-op when ws.readyState !== OPEN. In GoLive, playStream() calls setVideoAttributes(true) + setSpeaking(true) the instant createStream() resolves (right after SELECT_PROTOCOL_ACK), but the StreamConnection WebSocket can still be in CONNECTING for a few ms — so op 12 (VIDEO, activating the video SSRC) was silently DROPPED every session. Empirically verified: 0 ops 12/5 ever logged across the entire journal, yet 10k+ video frames were sent and audio played (audio SSRC is activated via the VoiceConnection handshake, independent of GoLive op 12). Discord's media server thus received video RTP on video_ssrc but was never told to forward it → black/broken shared-screen video with working voice. sendOpcodeWhenOpen retries up to ~2s for ws OPEN instead of dropping. Also emits a=fmtp:101 packetization-mode=1;profile-level-id=42e01f in the answer SDP (H264 FU-A fragments require packetization-mode=1 to reassemble). Also removes pre-existing noNonNullAssertion lint (biome 2.5.8 now errors) that was blocking the deploy CI. --- .../modules/messages/messages.repository.ts | 27 +++++++++---------- .../src/goLive/BaseMediaConnection.ts | 6 +---- .../discord-gateway/src/shared/utils/index.ts | 5 +++- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/services/backend/src/modules/messages/messages.repository.ts b/services/backend/src/modules/messages/messages.repository.ts index a812e33..bdb35a6 100644 --- a/services/backend/src/modules/messages/messages.repository.ts +++ b/services/backend/src/modules/messages/messages.repository.ts @@ -77,12 +77,11 @@ export class MessagesRepository { // Exclude spam threads (NULL-safe: non-thread messages are kept) if (EXCLUDED_THREAD_IDS.length > 0) { - conditions.push( - or( - isNull(pgMessagesTable.thread_id), - notInArray(pgMessagesTable.thread_id, EXCLUDED_THREAD_IDS), - )!, + const excludeThreads = or( + isNull(pgMessagesTable.thread_id), + notInArray(pgMessagesTable.thread_id, EXCLUDED_THREAD_IDS), ); + if (excludeThreads) conditions.push(excludeThreads); } const where = conditions.length > 0 ? and(...conditions) : undefined; @@ -150,12 +149,11 @@ export class MessagesRepository { // Exclude spam threads (NULL-safe) if (EXCLUDED_THREAD_IDS.length > 0) { - conditions.push( - or( - isNull(pgMessagesTable.thread_id), - notInArray(pgMessagesTable.thread_id, EXCLUDED_THREAD_IDS), - )!, + const excludeThreads = or( + isNull(pgMessagesTable.thread_id), + notInArray(pgMessagesTable.thread_id, EXCLUDED_THREAD_IDS), ); + if (excludeThreads) conditions.push(excludeThreads); } const rows = await db @@ -316,12 +314,13 @@ export class MessagesRepository { like(pgAttachmentsTable.type, "image/%"), // Exclude spam threads (NULL-safe for non-thread messages) ...(EXCLUDED_THREAD_IDS.length > 0 - ? [ - or( + ? (() => { + const excludeThreads = or( isNull(pgAttachmentsTable.thread_id), notInArray(pgAttachmentsTable.thread_id, EXCLUDED_THREAD_IDS), - )!, - ] + ); + return excludeThreads ? [excludeThreads] : []; + })() : []), ), ) diff --git a/services/discord-gateway/src/goLive/BaseMediaConnection.ts b/services/discord-gateway/src/goLive/BaseMediaConnection.ts index 7509656..f93615b 100644 --- a/services/discord-gateway/src/goLive/BaseMediaConnection.ts +++ b/services/discord-gateway/src/goLive/BaseMediaConnection.ts @@ -652,11 +652,7 @@ a=ice-lite * CONNECTING) breaks GoLive video while leaving audio intact. We wait for * the open state instead of dropping. */ - private sendOpcodeWhenOpen( - code: number, - data: unknown, - label: string, - ): void { + private sendOpcodeWhenOpen(code: number, data: unknown, label: string): void { const attempt = (triesLeft: number) => { if (this.ws?.readyState === WebSocket.OPEN) { this.sendOpcode(code, data); diff --git a/services/discord-gateway/src/shared/utils/index.ts b/services/discord-gateway/src/shared/utils/index.ts index 83ceba9..12bd541 100644 --- a/services/discord-gateway/src/shared/utils/index.ts +++ b/services/discord-gateway/src/shared/utils/index.ts @@ -108,5 +108,8 @@ export async function retryWithBackoff( }); } } - throw lastError!; + // lastError is always set: the for-loop only exits via break when attempt + // === retries, which only happens in the catch branch that sets lastError. + if (!lastError) throw new Error("Unknown retry error"); + throw lastError; }