fix(goLive): STREAM_CREATE handshake — self_video voice state + retry + send instrumentation
- signalStream: flip voice state to self_video:true/self_deaf:false before STREAM_CREATE (Discord silently ignores the request while video disabled) - createStream: attach dispatch listeners before first signal (race), clean up listeners on timeout, retry STREAM_CREATE every 3s up to 4 attempts (upstream issue #217/#219 — Discord randomly drops the request) - sendOpcode: direct [goLive:Streamer] log bypassing bootstrap debug filter (proves op 18 is actually broadcast)
This commit is contained in:
@@ -77,6 +77,11 @@ export class Streamer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sendOpcode(code: number, data: unknown): void {
|
sendOpcode(code: number, data: unknown): void {
|
||||||
|
// Direct instrumentation — bypasses the bootstrap debug filter (which
|
||||||
|
// drops messages without [VOICE / [ffmpeg / error / stream).
|
||||||
|
console.log(
|
||||||
|
`[goLive:Streamer] sendOpcode op=${code} d=${JSON.stringify(data)}`,
|
||||||
|
);
|
||||||
this.client.ws.broadcast({ op: code, d: data });
|
this.client.ws.broadcast({ op: code, d: data });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,14 +158,6 @@ export class Streamer {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.signalStream();
|
|
||||||
const streamTimeout = setTimeout(() => {
|
|
||||||
reject(
|
|
||||||
new Error(
|
|
||||||
"Timed out waiting for STREAM_CREATE/STREAM_SERVER_UPDATE from Discord (stream handshake) — voice media session may not be active",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}, 12_000);
|
|
||||||
const {
|
const {
|
||||||
guildId: clientGuildId,
|
guildId: clientGuildId,
|
||||||
channelId: clientChannelId,
|
channelId: clientChannelId,
|
||||||
@@ -175,13 +172,18 @@ export class Streamer {
|
|||||||
clientChannelId,
|
clientChannelId,
|
||||||
(conn) => {
|
(conn) => {
|
||||||
clearTimeout(streamTimeout);
|
clearTimeout(streamTimeout);
|
||||||
|
clearInterval(retryInterval);
|
||||||
resolve(conn);
|
resolve(conn);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
this.voiceConnection.streamConnection = streamConn;
|
this.voiceConnection.streamConnection = streamConn;
|
||||||
this._gatewayEmitter.on(
|
|
||||||
"STREAM_CREATE",
|
// Attach listeners BEFORE the first signal so a fast dispatch can't
|
||||||
(d: { stream_key: string; rtc_server_id: string }) => {
|
// be lost between signalStream() and listener registration.
|
||||||
|
const onStreamCreate = (d: {
|
||||||
|
stream_key: string;
|
||||||
|
rtc_server_id: string;
|
||||||
|
}) => {
|
||||||
const { channelId, guildId, userId } = parseStreamKey(d.stream_key);
|
const { channelId, guildId, userId } = parseStreamKey(d.stream_key);
|
||||||
if (
|
if (
|
||||||
clientGuildId !== guildId ||
|
clientGuildId !== guildId ||
|
||||||
@@ -193,11 +195,12 @@ export class Streamer {
|
|||||||
streamConn.serverId = d.rtc_server_id;
|
streamConn.serverId = d.rtc_server_id;
|
||||||
streamConn.streamKey = d.stream_key;
|
streamConn.streamKey = d.stream_key;
|
||||||
streamConn.setSession(session_id);
|
streamConn.setSession(session_id);
|
||||||
},
|
};
|
||||||
);
|
const onStreamServerUpdate = (d: {
|
||||||
this._gatewayEmitter.on(
|
stream_key: string;
|
||||||
"STREAM_SERVER_UPDATE",
|
endpoint: string;
|
||||||
(d: { stream_key: string; endpoint: string; token: string }) => {
|
token: string;
|
||||||
|
}) => {
|
||||||
const { channelId, guildId, userId } = parseStreamKey(d.stream_key);
|
const { channelId, guildId, userId } = parseStreamKey(d.stream_key);
|
||||||
if (
|
if (
|
||||||
clientGuildId !== guildId ||
|
clientGuildId !== guildId ||
|
||||||
@@ -207,8 +210,46 @@ export class Streamer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
streamConn.setTokens(d.endpoint, d.token);
|
streamConn.setTokens(d.endpoint, d.token);
|
||||||
},
|
};
|
||||||
|
this._gatewayEmitter.on("STREAM_CREATE", onStreamCreate);
|
||||||
|
this._gatewayEmitter.on("STREAM_SERVER_UPDATE", onStreamServerUpdate);
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
clearTimeout(streamTimeout);
|
||||||
|
clearInterval(retryInterval);
|
||||||
|
this._gatewayEmitter.removeListener("STREAM_CREATE", onStreamCreate);
|
||||||
|
this._gatewayEmitter.removeListener(
|
||||||
|
"STREAM_SERVER_UPDATE",
|
||||||
|
onStreamServerUpdate,
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
const streamTimeout = setTimeout(() => {
|
||||||
|
cleanup();
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
"Timed out waiting for STREAM_CREATE/STREAM_SERVER_UPDATE from Discord (stream handshake) — voice media session may not be active",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}, 12_000);
|
||||||
|
|
||||||
|
// Discord sometimes drops the STREAM_CREATE request silently (upstream
|
||||||
|
// issue #217/#219) — resend a few times instead of giving up after one.
|
||||||
|
let attempt = 0;
|
||||||
|
const retryInterval = setInterval(() => {
|
||||||
|
attempt += 1;
|
||||||
|
if (attempt >= 4) {
|
||||||
|
clearInterval(retryInterval);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
`[goLive:Streamer] createStream: retrying STREAM_CREATE (attempt ${attempt + 1}/4)`,
|
||||||
|
);
|
||||||
|
this.signalStream();
|
||||||
|
}, 3_000);
|
||||||
|
console.log(
|
||||||
|
`[goLive:Streamer] createStream: sending STREAM_CREATE (attempt 1/4)`,
|
||||||
|
);
|
||||||
|
this.signalStream();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,6 +302,17 @@ export class Streamer {
|
|||||||
channelId: channel_id,
|
channelId: channel_id,
|
||||||
botId: user_id,
|
botId: user_id,
|
||||||
} = this.voiceConnection;
|
} = this.voiceConnection;
|
||||||
|
// Mimic the real Discord client when starting Go Live: flip the voice
|
||||||
|
// state to video-enabled (and un-deafen) BEFORE requesting the stream.
|
||||||
|
// Discord's gateway silently ignores STREAM_CREATE while the user's
|
||||||
|
// voice state still has self_video: false.
|
||||||
|
this.sendOpcode(GatewayOpCodes.VOICE_STATE_UPDATE, {
|
||||||
|
guild_id,
|
||||||
|
channel_id,
|
||||||
|
self_mute: false,
|
||||||
|
self_deaf: false,
|
||||||
|
self_video: true,
|
||||||
|
});
|
||||||
this.sendOpcode(GatewayOpCodes.STREAM_CREATE, {
|
this.sendOpcode(GatewayOpCodes.STREAM_CREATE, {
|
||||||
type,
|
type,
|
||||||
guild_id,
|
guild_id,
|
||||||
|
|||||||
Reference in New Issue
Block a user