fix(goLive): retry VIDEO(op12)/SPEAKING(op5) opcodes until ws OPEN — broken shared-screen video
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.
This commit is contained in:
@@ -77,12 +77,11 @@ export class MessagesRepository {
|
|||||||
|
|
||||||
// Exclude spam threads (NULL-safe: non-thread messages are kept)
|
// Exclude spam threads (NULL-safe: non-thread messages are kept)
|
||||||
if (EXCLUDED_THREAD_IDS.length > 0) {
|
if (EXCLUDED_THREAD_IDS.length > 0) {
|
||||||
conditions.push(
|
const excludeThreads = or(
|
||||||
or(
|
isNull(pgMessagesTable.thread_id),
|
||||||
isNull(pgMessagesTable.thread_id),
|
notInArray(pgMessagesTable.thread_id, EXCLUDED_THREAD_IDS),
|
||||||
notInArray(pgMessagesTable.thread_id, EXCLUDED_THREAD_IDS),
|
|
||||||
)!,
|
|
||||||
);
|
);
|
||||||
|
if (excludeThreads) conditions.push(excludeThreads);
|
||||||
}
|
}
|
||||||
|
|
||||||
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
||||||
@@ -150,12 +149,11 @@ export class MessagesRepository {
|
|||||||
|
|
||||||
// Exclude spam threads (NULL-safe)
|
// Exclude spam threads (NULL-safe)
|
||||||
if (EXCLUDED_THREAD_IDS.length > 0) {
|
if (EXCLUDED_THREAD_IDS.length > 0) {
|
||||||
conditions.push(
|
const excludeThreads = or(
|
||||||
or(
|
isNull(pgMessagesTable.thread_id),
|
||||||
isNull(pgMessagesTable.thread_id),
|
notInArray(pgMessagesTable.thread_id, EXCLUDED_THREAD_IDS),
|
||||||
notInArray(pgMessagesTable.thread_id, EXCLUDED_THREAD_IDS),
|
|
||||||
)!,
|
|
||||||
);
|
);
|
||||||
|
if (excludeThreads) conditions.push(excludeThreads);
|
||||||
}
|
}
|
||||||
|
|
||||||
const rows = await db
|
const rows = await db
|
||||||
@@ -316,12 +314,13 @@ export class MessagesRepository {
|
|||||||
like(pgAttachmentsTable.type, "image/%"),
|
like(pgAttachmentsTable.type, "image/%"),
|
||||||
// Exclude spam threads (NULL-safe for non-thread messages)
|
// Exclude spam threads (NULL-safe for non-thread messages)
|
||||||
...(EXCLUDED_THREAD_IDS.length > 0
|
...(EXCLUDED_THREAD_IDS.length > 0
|
||||||
? [
|
? (() => {
|
||||||
or(
|
const excludeThreads = or(
|
||||||
isNull(pgAttachmentsTable.thread_id),
|
isNull(pgAttachmentsTable.thread_id),
|
||||||
notInArray(pgAttachmentsTable.thread_id, EXCLUDED_THREAD_IDS),
|
notInArray(pgAttachmentsTable.thread_id, EXCLUDED_THREAD_IDS),
|
||||||
)!,
|
);
|
||||||
]
|
return excludeThreads ? [excludeThreads] : [];
|
||||||
|
})()
|
||||||
: []),
|
: []),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -652,11 +652,7 @@ a=ice-lite
|
|||||||
* CONNECTING) breaks GoLive video while leaving audio intact. We wait for
|
* CONNECTING) breaks GoLive video while leaving audio intact. We wait for
|
||||||
* the open state instead of dropping.
|
* the open state instead of dropping.
|
||||||
*/
|
*/
|
||||||
private sendOpcodeWhenOpen(
|
private sendOpcodeWhenOpen(code: number, data: unknown, label: string): void {
|
||||||
code: number,
|
|
||||||
data: unknown,
|
|
||||||
label: string,
|
|
||||||
): void {
|
|
||||||
const attempt = (triesLeft: number) => {
|
const attempt = (triesLeft: number) => {
|
||||||
if (this.ws?.readyState === WebSocket.OPEN) {
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
||||||
this.sendOpcode(code, data);
|
this.sendOpcode(code, data);
|
||||||
|
|||||||
@@ -108,5 +108,8 @@ export async function retryWithBackoff<T>(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user