piixishstream 0.0.0-stage → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +124 -2
- package/dist/client/GatewayEvents.d.ts +30 -0
- package/dist/client/GatewayEvents.js +2 -0
- package/dist/client/GatewayEvents.js.map +1 -0
- package/dist/client/GatewayOpCodes.d.ts +40 -0
- package/dist/client/GatewayOpCodes.js +42 -0
- package/dist/client/GatewayOpCodes.js.map +1 -0
- package/dist/client/Streamer.d.ts +30 -0
- package/dist/client/Streamer.js +188 -0
- package/dist/client/Streamer.js.map +1 -0
- package/dist/client/index.d.ts +3 -0
- package/dist/client/index.js +4 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/processing/AnnexBBitstreamReaderWriter.d.ts +23 -0
- package/dist/client/processing/AnnexBBitstreamReaderWriter.js +149 -0
- package/dist/client/processing/AnnexBBitstreamReaderWriter.js.map +1 -0
- package/dist/client/processing/AnnexBHelper.d.ts +93 -0
- package/dist/client/processing/AnnexBHelper.js +127 -0
- package/dist/client/processing/AnnexBHelper.js.map +1 -0
- package/dist/client/processing/SPSVUIRewriter.d.ts +1 -0
- package/dist/client/processing/SPSVUIRewriter.js +297 -0
- package/dist/client/processing/SPSVUIRewriter.js.map +1 -0
- package/dist/client/voice/BaseMediaConnection.d.ts +80 -0
- package/dist/client/voice/BaseMediaConnection.js +552 -0
- package/dist/client/voice/BaseMediaConnection.js.map +1 -0
- package/dist/client/voice/CodecPayloadType.d.ts +59 -0
- package/dist/client/voice/CodecPayloadType.js +60 -0
- package/dist/client/voice/CodecPayloadType.js.map +1 -0
- package/dist/client/voice/StreamConnection.d.ts +11 -0
- package/dist/client/voice/StreamConnection.js +34 -0
- package/dist/client/voice/StreamConnection.js.map +1 -0
- package/dist/client/voice/VoiceConnection.d.ts +8 -0
- package/dist/client/voice/VoiceConnection.js +15 -0
- package/dist/client/voice/VoiceConnection.js.map +1 -0
- package/dist/client/voice/VoiceMessageTypes.d.ts +178 -0
- package/dist/client/voice/VoiceMessageTypes.js +2 -0
- package/dist/client/voice/VoiceMessageTypes.js.map +1 -0
- package/dist/client/voice/VoiceOpCodes.d.ts +35 -0
- package/dist/client/voice/VoiceOpCodes.js +38 -0
- package/dist/client/voice/VoiceOpCodes.js.map +1 -0
- package/dist/client/voice/WebRtcWrapper.d.ts +23 -0
- package/dist/client/voice/WebRtcWrapper.js +159 -0
- package/dist/client/voice/WebRtcWrapper.js.map +1 -0
- package/dist/client/voice/index.d.ts +4 -0
- package/dist/client/voice/index.js +6 -0
- package/dist/client/voice/index.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/media/AudioStream.d.ts +7 -0
- package/dist/media/AudioStream.js +12 -0
- package/dist/media/AudioStream.js.map +1 -0
- package/dist/media/BaseMediaStream.d.ts +31 -0
- package/dist/media/BaseMediaStream.js +164 -0
- package/dist/media/BaseMediaStream.js.map +1 -0
- package/dist/media/LibavCodecId.d.ts +541 -0
- package/dist/media/LibavCodecId.js +553 -0
- package/dist/media/LibavCodecId.js.map +1 -0
- package/dist/media/LibavDecoder.d.ts +5 -0
- package/dist/media/LibavDecoder.js +43 -0
- package/dist/media/LibavDecoder.js.map +1 -0
- package/dist/media/LibavDemuxer.d.ts +44 -0
- package/dist/media/LibavDemuxer.js +231 -0
- package/dist/media/LibavDemuxer.js.map +1 -0
- package/dist/media/VideoStream.d.ts +7 -0
- package/dist/media/VideoStream.js +12 -0
- package/dist/media/VideoStream.js.map +1 -0
- package/dist/media/encoders/index.d.ts +26 -0
- package/dist/media/encoders/index.js +12 -0
- package/dist/media/encoders/index.js.map +1 -0
- package/dist/media/encoders/merge.d.ts +3 -0
- package/dist/media/encoders/merge.js +10 -0
- package/dist/media/encoders/merge.js.map +1 -0
- package/dist/media/encoders/nvenc.d.ts +10 -0
- package/dist/media/encoders/nvenc.js +23 -0
- package/dist/media/encoders/nvenc.js.map +1 -0
- package/dist/media/encoders/software.d.ts +17 -0
- package/dist/media/encoders/software.js +31 -0
- package/dist/media/encoders/software.js.map +1 -0
- package/dist/media/encoders/vaapi.d.ts +6 -0
- package/dist/media/encoders/vaapi.js +22 -0
- package/dist/media/encoders/vaapi.js.map +1 -0
- package/dist/media/index.d.ts +4 -0
- package/dist/media/index.js +5 -0
- package/dist/media/index.js.map +1 -0
- package/dist/media/newApi.d.ts +130 -0
- package/dist/media/newApi.js +409 -0
- package/dist/media/newApi.js.map +1 -0
- package/dist/utils.d.ts +25 -0
- package/dist/utils.js +65 -0
- package/dist/utils.js.map +1 -0
- package/package.json +82 -5
- package/src/client/GatewayEvents.ts +48 -0
- package/src/client/GatewayOpCodes.ts +40 -0
- package/src/client/Streamer.ts +259 -0
- package/src/client/index.ts +3 -0
- package/src/client/processing/AnnexBBitstreamReaderWriter.ts +148 -0
- package/src/client/processing/AnnexBHelper.ts +134 -0
- package/src/client/processing/SPSVUIRewriter.ts +332 -0
- package/src/client/voice/BaseMediaConnection.ts +639 -0
- package/src/client/voice/CodecPayloadType.ts +59 -0
- package/src/client/voice/MediaUdp.ts.bak +171 -0
- package/src/client/voice/StreamConnection.ts +38 -0
- package/src/client/voice/VoiceConnection.ts +19 -0
- package/src/client/voice/VoiceMessageTypes.ts +263 -0
- package/src/client/voice/VoiceOpCodes.ts +36 -0
- package/src/client/voice/WebRtcWrapper.ts +218 -0
- package/src/client/voice/index.ts +5 -0
- package/src/index.ts +3 -0
- package/src/media/AudioStream.ts +18 -0
- package/src/media/BaseMediaStream.ts +198 -0
- package/src/media/LibavCodecId.ts +564 -0
- package/src/media/LibavDecoder.ts +47 -0
- package/src/media/LibavDemuxer.ts +293 -0
- package/src/media/VideoStream.ts +17 -0
- package/src/media/encoders/index.ts +27 -0
- package/src/media/encoders/merge.ts +14 -0
- package/src/media/encoders/nvenc.ts +38 -0
- package/src/media/encoders/software.ts +76 -0
- package/src/media/encoders/vaapi.ts +29 -0
- package/src/media/index.ts +4 -0
- package/src/media/newApi.ts +642 -0
- package/src/utils.ts +101 -0
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
import Davey from "@snazzah/davey";
|
|
2
|
+
import EventEmitter from "node:events";
|
|
3
|
+
import { Log } from "debug-level";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { CodecPayloadType } from "./CodecPayloadType.js";
|
|
6
|
+
import { WebRtcConnWrapper } from "./WebRtcWrapper.js";
|
|
7
|
+
import { VoiceOpCodes, VoiceOpCodesBinary } from "./VoiceOpCodes.js";
|
|
8
|
+
import {
|
|
9
|
+
STREAMS_SIMULCAST,
|
|
10
|
+
type SupportedEncryptionModes,
|
|
11
|
+
} from "../../utils.js";
|
|
12
|
+
import type {
|
|
13
|
+
Message,
|
|
14
|
+
GatewayRequest,
|
|
15
|
+
GatewayResponse,
|
|
16
|
+
} from "./VoiceMessageTypes.js";
|
|
17
|
+
import type { Streamer } from "../Streamer.js";
|
|
18
|
+
|
|
19
|
+
type VoiceConnectionStatus = {
|
|
20
|
+
hasSession: boolean;
|
|
21
|
+
hasToken: boolean;
|
|
22
|
+
started: boolean;
|
|
23
|
+
resuming: boolean;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type WebRtcParameters = {
|
|
27
|
+
address: string;
|
|
28
|
+
port: number;
|
|
29
|
+
audioSsrc: number;
|
|
30
|
+
videoSsrc: number;
|
|
31
|
+
rtxSsrc: number;
|
|
32
|
+
supportedEncryptionModes: SupportedEncryptionModes[];
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type ValueOf<T> = T extends (infer U)[]
|
|
36
|
+
? U
|
|
37
|
+
: T extends Record<string, infer U>
|
|
38
|
+
? U
|
|
39
|
+
: never;
|
|
40
|
+
|
|
41
|
+
export type VideoAttributes = {
|
|
42
|
+
width: number;
|
|
43
|
+
height: number;
|
|
44
|
+
fps: number;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export abstract class BaseMediaConnection extends EventEmitter {
|
|
48
|
+
private interval: NodeJS.Timeout | null = null;
|
|
49
|
+
public guildId: string | null = null;
|
|
50
|
+
public channelId: string;
|
|
51
|
+
public botId: string;
|
|
52
|
+
public ws: WebSocket | null = null;
|
|
53
|
+
public status: VoiceConnectionStatus;
|
|
54
|
+
public server: string | null = null; //websocket url
|
|
55
|
+
public token: string | null = null;
|
|
56
|
+
public session_id: string | null = null;
|
|
57
|
+
|
|
58
|
+
private _webRtcWrapper;
|
|
59
|
+
private _webRtcParams: WebRtcParameters | null = null;
|
|
60
|
+
private _closed = false;
|
|
61
|
+
public ready: (conn: WebRtcConnWrapper) => void;
|
|
62
|
+
|
|
63
|
+
private _streamer: Streamer;
|
|
64
|
+
private _sequenceNumber = -1;
|
|
65
|
+
|
|
66
|
+
private _daveSession: Davey.DaveSession | undefined;
|
|
67
|
+
private _connectedUsers = new Set<string>();
|
|
68
|
+
private _daveProtocolVersion = 0;
|
|
69
|
+
private _davePendingTransitions = new Map<number, number>();
|
|
70
|
+
private _daveDowngraded = false;
|
|
71
|
+
|
|
72
|
+
private _logger = new Log("conn");
|
|
73
|
+
private _loggerDave = new Log("conn:dave");
|
|
74
|
+
constructor(
|
|
75
|
+
streamer: Streamer,
|
|
76
|
+
guildId: string | null,
|
|
77
|
+
botId: string,
|
|
78
|
+
channelId: string,
|
|
79
|
+
callback: (conn: WebRtcConnWrapper) => void,
|
|
80
|
+
) {
|
|
81
|
+
super();
|
|
82
|
+
this._streamer = streamer;
|
|
83
|
+
this.status = {
|
|
84
|
+
hasSession: false,
|
|
85
|
+
hasToken: false,
|
|
86
|
+
started: false,
|
|
87
|
+
resuming: false,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
this.guildId = guildId;
|
|
91
|
+
this.channelId = channelId;
|
|
92
|
+
this.botId = botId;
|
|
93
|
+
this.ready = callback;
|
|
94
|
+
this._webRtcWrapper = new WebRtcConnWrapper(this);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
public abstract get serverId(): string | null;
|
|
98
|
+
|
|
99
|
+
public get type(): "guild" | "call" {
|
|
100
|
+
return this.guildId ? "guild" : "call";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
public get webRtcConn() {
|
|
104
|
+
return this._webRtcWrapper;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
public get webRtcParams() {
|
|
108
|
+
return this._webRtcParams;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
public get streamer() {
|
|
112
|
+
return this._streamer;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
public abstract get daveChannelId(): string;
|
|
116
|
+
|
|
117
|
+
stop(): void {
|
|
118
|
+
this._closed = true;
|
|
119
|
+
this._webRtcWrapper.close();
|
|
120
|
+
this.ws?.close();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
setSession(session_id: string): void {
|
|
124
|
+
this.session_id = session_id;
|
|
125
|
+
|
|
126
|
+
this.status.hasSession = true;
|
|
127
|
+
this.start();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
setTokens(server: string, token: string): void {
|
|
131
|
+
this.token = token;
|
|
132
|
+
this.server = server;
|
|
133
|
+
|
|
134
|
+
this.status.hasToken = true;
|
|
135
|
+
this.start();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
start(): void {
|
|
139
|
+
/*
|
|
140
|
+
** Connection can only start once both
|
|
141
|
+
** session description and tokens have been gathered
|
|
142
|
+
*/
|
|
143
|
+
if (this.status.hasSession && this.status.hasToken) {
|
|
144
|
+
if (this.status.started) return;
|
|
145
|
+
this.status.started = true;
|
|
146
|
+
|
|
147
|
+
this.ws = new WebSocket(`wss://${this.server}/?v=8`);
|
|
148
|
+
this.ws.binaryType = "arraybuffer";
|
|
149
|
+
this.ws.addEventListener("open", () => {
|
|
150
|
+
if (this.status.resuming) {
|
|
151
|
+
this.status.resuming = false;
|
|
152
|
+
this.resume();
|
|
153
|
+
} else {
|
|
154
|
+
this.identify();
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
this.ws.addEventListener("error", (err) => {
|
|
158
|
+
console.error(err);
|
|
159
|
+
});
|
|
160
|
+
this.ws.addEventListener("close", (e) => {
|
|
161
|
+
const wasStarted = this.status.started;
|
|
162
|
+
|
|
163
|
+
this.interval && clearInterval(this.interval);
|
|
164
|
+
this.status.started = false;
|
|
165
|
+
const canResume = e.code === 4_015 || e.code < 4_000;
|
|
166
|
+
|
|
167
|
+
if (canResume && wasStarted) {
|
|
168
|
+
this.status.resuming = true;
|
|
169
|
+
this.start();
|
|
170
|
+
} else {
|
|
171
|
+
this._closed = true;
|
|
172
|
+
this._webRtcWrapper?.close();
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
this.setupEvents();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
handleReady(d: Message.Ready): void {
|
|
180
|
+
// we hardcoded the STREAMS_SIMULCAST, which will always be array of 1
|
|
181
|
+
const stream = d.streams[0];
|
|
182
|
+
this._webRtcParams = {
|
|
183
|
+
address: d.ip,
|
|
184
|
+
port: d.port,
|
|
185
|
+
audioSsrc: d.ssrc,
|
|
186
|
+
videoSsrc: stream.ssrc,
|
|
187
|
+
rtxSsrc: stream.rtx_ssrc,
|
|
188
|
+
supportedEncryptionModes: d.modes,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async handleProtocolAck(d: Message.SelectProtocolAck) {
|
|
193
|
+
if (!("sdp" in d)) throw new Error("Only WebRTC connections are allowed");
|
|
194
|
+
this._daveProtocolVersion = d.dave_protocol_version;
|
|
195
|
+
this.initDave();
|
|
196
|
+
// Discord's SDP is absolute garbage...Generate one ourselves
|
|
197
|
+
let ip = "",
|
|
198
|
+
port = "",
|
|
199
|
+
iceUsername = "",
|
|
200
|
+
icePassword = "",
|
|
201
|
+
fingerprint = "",
|
|
202
|
+
candidate = "";
|
|
203
|
+
for (const line of d.sdp.split("\n")) {
|
|
204
|
+
if (line.startsWith("c=")) ip = line;
|
|
205
|
+
else if (line.startsWith("a=rtcp")) port = line.split(":")[1];
|
|
206
|
+
else if (line.startsWith("a=ice-ufrag")) iceUsername = line;
|
|
207
|
+
else if (line.startsWith("a=ice-pwd")) icePassword = line;
|
|
208
|
+
else if (line.startsWith("a=fingerprint")) fingerprint = line;
|
|
209
|
+
else if (line.startsWith("a=candidate")) candidate = line;
|
|
210
|
+
}
|
|
211
|
+
const audioPayloadType = CodecPayloadType.opus.payload_type;
|
|
212
|
+
const audioSection = `
|
|
213
|
+
m=audio ${port} UDP/TLS/RTP/SAVPF ${audioPayloadType}
|
|
214
|
+
${ip}
|
|
215
|
+
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
|
|
216
|
+
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
|
|
217
|
+
a=setup:passive
|
|
218
|
+
a=mid:0
|
|
219
|
+
a=maxptime:60
|
|
220
|
+
a=inactive
|
|
221
|
+
${iceUsername}
|
|
222
|
+
${icePassword}
|
|
223
|
+
${fingerprint}
|
|
224
|
+
${candidate}
|
|
225
|
+
a=rtcp-mux
|
|
226
|
+
a=rtpmap:${audioPayloadType} opus/48000/2
|
|
227
|
+
a=fmtp:${audioPayloadType} minptime=10;useinbandfec=1;usedtx=1
|
|
228
|
+
a=rtcp-fb:${audioPayloadType} transport-cc
|
|
229
|
+
a=rtcp-fb:${audioPayloadType} nack
|
|
230
|
+
a=ice-lite
|
|
231
|
+
`.trim();
|
|
232
|
+
const videoPayloads = Object.values(CodecPayloadType).filter(
|
|
233
|
+
(el) => el.type === "video",
|
|
234
|
+
);
|
|
235
|
+
const videoPayloadTypes = videoPayloads.flatMap((el) => [
|
|
236
|
+
el.payload_type,
|
|
237
|
+
el.rtx_payload_type,
|
|
238
|
+
]);
|
|
239
|
+
const videoSection = `
|
|
240
|
+
m=video ${port} UDP/TLS/RTP/SAVPF ${videoPayloadTypes.join(" ")}
|
|
241
|
+
${ip}
|
|
242
|
+
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
|
|
243
|
+
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
|
|
244
|
+
a=extmap:14 urn:ietf:params:rtp-hdrext:toffset
|
|
245
|
+
a=extmap:13 urn:3gpp:video-orientation
|
|
246
|
+
a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
|
|
247
|
+
a=setup:passive
|
|
248
|
+
a=mid:1
|
|
249
|
+
a=inactive
|
|
250
|
+
${iceUsername}
|
|
251
|
+
${icePassword}
|
|
252
|
+
${fingerprint}
|
|
253
|
+
${candidate}
|
|
254
|
+
a=rtcp-mux
|
|
255
|
+
a=ice-lite
|
|
256
|
+
`.trim();
|
|
257
|
+
const videoRtpMap = videoPayloads
|
|
258
|
+
.flatMap((el) => [
|
|
259
|
+
`a=rtpmap:${el.payload_type} ${el.name}/90000`,
|
|
260
|
+
`a=rtpmap:${el.rtx_payload_type} rtx/90000`,
|
|
261
|
+
`a=fmtp:${el.rtx_payload_type} apt=${el.payload_type}`,
|
|
262
|
+
`a=rtcp-fb:${el.payload_type} ccm fir`,
|
|
263
|
+
`a=rtcp-fb:${el.payload_type} nack`,
|
|
264
|
+
`a=rtcp-fb:${el.payload_type} nack pli`,
|
|
265
|
+
`a=rtcp-fb:${el.payload_type} goog-remb`,
|
|
266
|
+
`a=rtcp-fb:${el.payload_type} transport-cc`,
|
|
267
|
+
])
|
|
268
|
+
.join("\n");
|
|
269
|
+
this._webRtcWrapper.webRtcConn?.setRemoteDescription(
|
|
270
|
+
[audioSection, videoSection, videoRtpMap].join("\n"),
|
|
271
|
+
"answer",
|
|
272
|
+
);
|
|
273
|
+
this.emit("select_protocol_ack");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
initDave() {
|
|
277
|
+
if (this._daveProtocolVersion) {
|
|
278
|
+
if (this._daveSession) {
|
|
279
|
+
this._daveSession.reinit(
|
|
280
|
+
this._daveProtocolVersion,
|
|
281
|
+
this.botId,
|
|
282
|
+
this.daveChannelId,
|
|
283
|
+
);
|
|
284
|
+
this._loggerDave.debug(`Reinitialized DAVE`, {
|
|
285
|
+
user_id: this.botId,
|
|
286
|
+
channel_id: this.daveChannelId,
|
|
287
|
+
});
|
|
288
|
+
} else {
|
|
289
|
+
this._daveSession = new Davey.DAVESession(
|
|
290
|
+
this._daveProtocolVersion,
|
|
291
|
+
this.botId,
|
|
292
|
+
this.daveChannelId,
|
|
293
|
+
);
|
|
294
|
+
this._loggerDave.debug(`Initialized DAVE`, {
|
|
295
|
+
user_id: this.botId,
|
|
296
|
+
channel_id: this.daveChannelId,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
this.sendOpcodeBinary(
|
|
300
|
+
VoiceOpCodesBinary.MLS_KEY_PACKAGE,
|
|
301
|
+
this._daveSession.getSerializedKeyPackage(),
|
|
302
|
+
);
|
|
303
|
+
} else if (this._daveSession) {
|
|
304
|
+
this._daveSession.reset();
|
|
305
|
+
this._daveSession.setPassthroughMode(true, 10);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
processInvalidCommit(transitionId: number) {
|
|
310
|
+
this._loggerDave.debug("Invalid commit received, reinitializing DAVE", {
|
|
311
|
+
transitionId,
|
|
312
|
+
});
|
|
313
|
+
this.sendOpcode(VoiceOpCodes.MLS_INVALID_COMMIT_WELCOME, {
|
|
314
|
+
transition_id: transitionId,
|
|
315
|
+
});
|
|
316
|
+
this.initDave();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
executePendingTransition(transitionId: number) {
|
|
320
|
+
const newVersion = this._davePendingTransitions.get(transitionId);
|
|
321
|
+
if (newVersion === undefined) {
|
|
322
|
+
this._loggerDave.error("Unrecognized transition ID", { transitionId });
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const oldVersion = this._daveProtocolVersion;
|
|
326
|
+
this._daveProtocolVersion = newVersion;
|
|
327
|
+
|
|
328
|
+
if (oldVersion !== newVersion && newVersion === 0) {
|
|
329
|
+
// Downgraded
|
|
330
|
+
this._daveDowngraded = true;
|
|
331
|
+
this._loggerDave.debug("Downgraded to non-E2E voice call");
|
|
332
|
+
} else if (transitionId > 0 && this._daveDowngraded) {
|
|
333
|
+
this._daveDowngraded = false;
|
|
334
|
+
this._daveSession?.setPassthroughMode(true, 10);
|
|
335
|
+
this._loggerDave.debug("Upgraded to E2E voice call");
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
this._davePendingTransitions.delete(transitionId);
|
|
339
|
+
this._loggerDave.debug(`Pending transition ID ${transitionId} executed`, {
|
|
340
|
+
transitionId,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
setupEvents(): void {
|
|
345
|
+
this.ws?.addEventListener("message", async (e) => {
|
|
346
|
+
if (e.data instanceof ArrayBuffer) {
|
|
347
|
+
this.handleBinaryMessages(Buffer.from(e.data));
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const { op, d, seq } = JSON.parse(e.data as string) as GatewayResponse;
|
|
351
|
+
if (seq) this._sequenceNumber = seq;
|
|
352
|
+
|
|
353
|
+
if (op === VoiceOpCodes.READY) {
|
|
354
|
+
// ready
|
|
355
|
+
this.handleReady(d);
|
|
356
|
+
this.setProtocols().then(() => this.ready(this._webRtcWrapper));
|
|
357
|
+
this.setVideoAttributes(false);
|
|
358
|
+
} else if (op >= 4000) {
|
|
359
|
+
console.error(`Error ${this.constructor.name} connection`, d);
|
|
360
|
+
} else if (op === VoiceOpCodes.HELLO) {
|
|
361
|
+
this.setupHeartbeat(d.heartbeat_interval);
|
|
362
|
+
} else if (op === VoiceOpCodes.SELECT_PROTOCOL_ACK) {
|
|
363
|
+
// session description
|
|
364
|
+
this.handleProtocolAck(d);
|
|
365
|
+
} else if (op === VoiceOpCodes.SPEAKING) {
|
|
366
|
+
// ignore speaking updates
|
|
367
|
+
} else if (op === VoiceOpCodes.HEARTBEAT_ACK) {
|
|
368
|
+
// ignore heartbeat acknowledgements
|
|
369
|
+
} else if (op === VoiceOpCodes.RESUMED) {
|
|
370
|
+
this.status.started = true;
|
|
371
|
+
} else if (op === VoiceOpCodes.CLIENTS_CONNECT) {
|
|
372
|
+
d.user_ids.forEach((id) => {
|
|
373
|
+
this._connectedUsers.add(id);
|
|
374
|
+
});
|
|
375
|
+
} else if (op === VoiceOpCodes.CLIENT_DISCONNECT) {
|
|
376
|
+
this._connectedUsers.delete(d.user_id);
|
|
377
|
+
} else if (op === VoiceOpCodes.DAVE_PREPARE_TRANSITION) {
|
|
378
|
+
this._loggerDave.debug("Preparing for DAVE transition", d);
|
|
379
|
+
this._davePendingTransitions.set(d.transition_id, d.protocol_version);
|
|
380
|
+
if (d.transition_id === 0) {
|
|
381
|
+
this.executePendingTransition(d.transition_id);
|
|
382
|
+
} else {
|
|
383
|
+
if (d.protocol_version === 0)
|
|
384
|
+
this._daveSession?.setPassthroughMode(true, 120);
|
|
385
|
+
this.sendOpcode(VoiceOpCodes.DAVE_TRANSITION_READY, {
|
|
386
|
+
transition_id: d.transition_id,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
} else if (op === VoiceOpCodes.DAVE_EXECUTE_TRANSITION) {
|
|
390
|
+
this.executePendingTransition(d.transition_id);
|
|
391
|
+
} else if (op === VoiceOpCodes.DAVE_PREPARE_EPOCH) {
|
|
392
|
+
this._loggerDave.debug("Preparing for DAVE epoch", d);
|
|
393
|
+
if (d.epoch === 1) {
|
|
394
|
+
this._daveProtocolVersion = d.protocol_version;
|
|
395
|
+
this.initDave();
|
|
396
|
+
}
|
|
397
|
+
} else {
|
|
398
|
+
//console.log("unhandled voice event", {op, d});
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
handleBinaryMessages(msg: Buffer) {
|
|
404
|
+
this._sequenceNumber = msg.readUint16BE(0);
|
|
405
|
+
const op = msg.readUint8(2);
|
|
406
|
+
this._logger.trace(`Handling binary message with op ${op}`, { op });
|
|
407
|
+
switch (op) {
|
|
408
|
+
case VoiceOpCodesBinary.MLS_EXTERNAL_SENDER: {
|
|
409
|
+
this._daveSession?.setExternalSender(msg.subarray(3));
|
|
410
|
+
this._loggerDave.debug("Set MLS external sender");
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
case VoiceOpCodesBinary.MLS_PROPOSALS: {
|
|
414
|
+
const optype = msg.readUint8(3);
|
|
415
|
+
const { commit, welcome } = this._daveSession!.processProposals(
|
|
416
|
+
optype,
|
|
417
|
+
msg.subarray(4),
|
|
418
|
+
[...this._connectedUsers],
|
|
419
|
+
);
|
|
420
|
+
if (commit) {
|
|
421
|
+
this.sendOpcodeBinary(
|
|
422
|
+
VoiceOpCodesBinary.MLS_COMMIT_WELCOME,
|
|
423
|
+
welcome ? Buffer.concat([commit, welcome]) : commit,
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
this._loggerDave.debug("Processed MLS proposal");
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
case VoiceOpCodesBinary.MLS_ANNOUNCE_COMMIT_TRANSITION: {
|
|
430
|
+
const transitionId = msg.readUInt16BE(3);
|
|
431
|
+
try {
|
|
432
|
+
this._daveSession?.processCommit(msg.subarray(5));
|
|
433
|
+
if (transitionId) {
|
|
434
|
+
this._davePendingTransitions.set(
|
|
435
|
+
transitionId,
|
|
436
|
+
this._daveProtocolVersion,
|
|
437
|
+
);
|
|
438
|
+
this.sendOpcode(VoiceOpCodes.DAVE_TRANSITION_READY, {
|
|
439
|
+
transition_id: transitionId,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
this._loggerDave.debug("MLS commit processed", { transitionId });
|
|
443
|
+
} catch (e) {
|
|
444
|
+
this._loggerDave.debug("MLS commit errored", e);
|
|
445
|
+
this.processInvalidCommit(transitionId);
|
|
446
|
+
}
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
case VoiceOpCodesBinary.MLS_WELCOME: {
|
|
450
|
+
const transitionId = msg.readUInt16BE(3);
|
|
451
|
+
try {
|
|
452
|
+
this._daveSession?.processWelcome(msg.subarray(5));
|
|
453
|
+
if (transitionId) {
|
|
454
|
+
this._davePendingTransitions.set(
|
|
455
|
+
transitionId,
|
|
456
|
+
this._daveProtocolVersion,
|
|
457
|
+
);
|
|
458
|
+
this.sendOpcode(VoiceOpCodes.DAVE_TRANSITION_READY, {
|
|
459
|
+
transition_id: transitionId,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
this._loggerDave.debug("MLS welcome processed", { transitionId });
|
|
463
|
+
} catch (e) {
|
|
464
|
+
this._loggerDave.debug("MLS welcome errored", e);
|
|
465
|
+
this.processInvalidCommit(transitionId);
|
|
466
|
+
}
|
|
467
|
+
break;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
public get daveReady() {
|
|
473
|
+
return this._daveProtocolVersion && this._daveSession?.ready;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
public get daveSession() {
|
|
477
|
+
return this._daveSession;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
setupHeartbeat(interval: number): void {
|
|
481
|
+
if (this.interval) {
|
|
482
|
+
clearInterval(this.interval);
|
|
483
|
+
}
|
|
484
|
+
this.interval = setInterval(() => {
|
|
485
|
+
try {
|
|
486
|
+
this.sendOpcode(VoiceOpCodes.HEARTBEAT, {
|
|
487
|
+
t: Date.now(),
|
|
488
|
+
seq_ack: this._sequenceNumber,
|
|
489
|
+
});
|
|
490
|
+
} catch { }
|
|
491
|
+
}, interval);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
sendOpcode<T extends GatewayRequest>(code: T["op"], data: T["d"]): void {
|
|
495
|
+
if (this.ws?.readyState !== WebSocket.OPEN) return;
|
|
496
|
+
this.ws.send(
|
|
497
|
+
JSON.stringify({
|
|
498
|
+
op: code,
|
|
499
|
+
d: data,
|
|
500
|
+
}),
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
sendOpcodeBinary(code: VoiceOpCodesBinary, data: Buffer) {
|
|
504
|
+
if (this.ws?.readyState !== WebSocket.OPEN) return;
|
|
505
|
+
const buf = Buffer.allocUnsafe(data.length + 1);
|
|
506
|
+
buf.writeUInt8(code);
|
|
507
|
+
data.copy(buf, 1);
|
|
508
|
+
this.ws.send(buf);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/*
|
|
512
|
+
** identifies with media server with credentials
|
|
513
|
+
*/
|
|
514
|
+
identify(): void {
|
|
515
|
+
if (!this.serverId) throw new Error("Server ID is null or empty");
|
|
516
|
+
if (!this.session_id) throw new Error("Session ID is null or empty");
|
|
517
|
+
if (!this.token) throw new Error("Token is null or empty");
|
|
518
|
+
this.sendOpcode(VoiceOpCodes.IDENTIFY, {
|
|
519
|
+
server_id: this.serverId,
|
|
520
|
+
user_id: this.botId,
|
|
521
|
+
session_id: this.session_id,
|
|
522
|
+
token: this.token,
|
|
523
|
+
video: true,
|
|
524
|
+
streams: STREAMS_SIMULCAST,
|
|
525
|
+
max_dave_protocol_version: Davey.DAVE_PROTOCOL_VERSION ?? 0,
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
resume(): void {
|
|
530
|
+
if (!this.serverId) throw new Error("Server ID is null or empty");
|
|
531
|
+
if (!this.session_id) throw new Error("Session ID is null or empty");
|
|
532
|
+
if (!this.token) throw new Error("Token is null or empty");
|
|
533
|
+
this.sendOpcode(VoiceOpCodes.RESUME, {
|
|
534
|
+
server_id: this.serverId,
|
|
535
|
+
session_id: this.session_id,
|
|
536
|
+
token: this.token,
|
|
537
|
+
seq_ack: this._sequenceNumber,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/*
|
|
542
|
+
** Sets protocols and ip data used for video and audio.
|
|
543
|
+
** Uses vp8 for video
|
|
544
|
+
** Uses opus for audio
|
|
545
|
+
*/
|
|
546
|
+
public async setProtocols(): Promise<void> {
|
|
547
|
+
if (!this._webRtcParams) throw new Error("WebRTC parameters not set");
|
|
548
|
+
// if (
|
|
549
|
+
// this._webRtcParams.supportedEncryptionModes.includes(SupportedEncryptionModes.AES256) &&
|
|
550
|
+
// !this._streamer.opts.forceChacha20Encryption
|
|
551
|
+
// ) {
|
|
552
|
+
// encryptionMode = SupportedEncryptionModes.AES256
|
|
553
|
+
// } else {
|
|
554
|
+
// encryptionMode = SupportedEncryptionModes.XCHACHA20
|
|
555
|
+
// }
|
|
556
|
+
|
|
557
|
+
const reconnect = () => {
|
|
558
|
+
const webRtcConn = this._webRtcWrapper.initWebRtc();
|
|
559
|
+
webRtcConn.onStateChange((state) => {
|
|
560
|
+
if (state === "closed" && !this._closed) reconnect();
|
|
561
|
+
});
|
|
562
|
+
webRtcConn.onLocalDescription((sdp) => {
|
|
563
|
+
const rtc_connection_id = randomUUID();
|
|
564
|
+
this.sendOpcode(VoiceOpCodes.SELECT_PROTOCOL, {
|
|
565
|
+
protocol: "webrtc",
|
|
566
|
+
codecs: Object.values(CodecPayloadType) as ValueOf<
|
|
567
|
+
typeof CodecPayloadType
|
|
568
|
+
>[],
|
|
569
|
+
data: sdp,
|
|
570
|
+
sdp: sdp,
|
|
571
|
+
rtc_connection_id,
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
webRtcConn.setLocalDescription();
|
|
575
|
+
};
|
|
576
|
+
reconnect();
|
|
577
|
+
return new Promise((resolve) => {
|
|
578
|
+
this.once("select_protocol_ack", () => resolve());
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/*
|
|
583
|
+
* Sets video attributes (width, height, frame rate).
|
|
584
|
+
* enabled -> video on or off
|
|
585
|
+
* attr -> video attributes
|
|
586
|
+
* video and rtx sources are set to ssrc + 1 and ssrc + 2
|
|
587
|
+
*/
|
|
588
|
+
public setVideoAttributes(enabled: false): void;
|
|
589
|
+
public setVideoAttributes(enabled: true, attr: VideoAttributes): void;
|
|
590
|
+
public setVideoAttributes(enabled: boolean, attr?: VideoAttributes): void {
|
|
591
|
+
if (!this._webRtcParams) throw new Error("WebRTC parameters not set");
|
|
592
|
+
const { audioSsrc, videoSsrc, rtxSsrc } = this._webRtcParams;
|
|
593
|
+
if (!enabled) {
|
|
594
|
+
this.sendOpcode(VoiceOpCodes.VIDEO, {
|
|
595
|
+
audio_ssrc: audioSsrc,
|
|
596
|
+
video_ssrc: 0,
|
|
597
|
+
rtx_ssrc: 0,
|
|
598
|
+
streams: [],
|
|
599
|
+
});
|
|
600
|
+
} else {
|
|
601
|
+
if (!attr) throw new Error("Need to specify video attributes");
|
|
602
|
+
this.sendOpcode(VoiceOpCodes.VIDEO, {
|
|
603
|
+
audio_ssrc: audioSsrc,
|
|
604
|
+
video_ssrc: videoSsrc,
|
|
605
|
+
rtx_ssrc: rtxSsrc,
|
|
606
|
+
streams: [
|
|
607
|
+
{
|
|
608
|
+
type: "video",
|
|
609
|
+
rid: "100",
|
|
610
|
+
ssrc: videoSsrc,
|
|
611
|
+
active: true,
|
|
612
|
+
quality: 100,
|
|
613
|
+
rtx_ssrc: rtxSsrc,
|
|
614
|
+
max_bitrate: 10000 * 1000,
|
|
615
|
+
max_framerate: enabled ? attr.fps : 0,
|
|
616
|
+
max_resolution: {
|
|
617
|
+
type: "fixed",
|
|
618
|
+
width: attr.width,
|
|
619
|
+
height: attr.height,
|
|
620
|
+
},
|
|
621
|
+
},
|
|
622
|
+
],
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/*
|
|
628
|
+
** Set speaking status
|
|
629
|
+
** speaking -> speaking status on or off
|
|
630
|
+
*/
|
|
631
|
+
public setSpeaking(speaking: boolean): void {
|
|
632
|
+
if (!this._webRtcParams) throw new Error("WebRTC connection not ready");
|
|
633
|
+
this.sendOpcode(VoiceOpCodes.SPEAKING, {
|
|
634
|
+
delay: 0,
|
|
635
|
+
speaking: speaking ? 1 : 0,
|
|
636
|
+
ssrc: this._webRtcParams.audioSsrc,
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export const CodecPayloadType = {
|
|
2
|
+
opus: {
|
|
3
|
+
name: "opus",
|
|
4
|
+
type: "audio",
|
|
5
|
+
clockRate: 48000,
|
|
6
|
+
priority: 1000,
|
|
7
|
+
payload_type: 120,
|
|
8
|
+
},
|
|
9
|
+
H264: {
|
|
10
|
+
name: "H264",
|
|
11
|
+
type: "video",
|
|
12
|
+
clockRate: 90000,
|
|
13
|
+
priority: 1000,
|
|
14
|
+
payload_type: 101,
|
|
15
|
+
rtx_payload_type: 102,
|
|
16
|
+
encode: true,
|
|
17
|
+
decode: true,
|
|
18
|
+
},
|
|
19
|
+
H265: {
|
|
20
|
+
name: "H265",
|
|
21
|
+
type: "video",
|
|
22
|
+
clockRate: 90000,
|
|
23
|
+
priority: 1000,
|
|
24
|
+
payload_type: 103,
|
|
25
|
+
rtx_payload_type: 104,
|
|
26
|
+
encode: true,
|
|
27
|
+
decode: true,
|
|
28
|
+
},
|
|
29
|
+
VP8: {
|
|
30
|
+
name: "VP8",
|
|
31
|
+
type: "video",
|
|
32
|
+
clockRate: 90000,
|
|
33
|
+
priority: 1000,
|
|
34
|
+
payload_type: 105,
|
|
35
|
+
rtx_payload_type: 106,
|
|
36
|
+
encode: true,
|
|
37
|
+
decode: true,
|
|
38
|
+
},
|
|
39
|
+
VP9: {
|
|
40
|
+
name: "VP9",
|
|
41
|
+
type: "video",
|
|
42
|
+
clockRate: 90000,
|
|
43
|
+
priority: 1000,
|
|
44
|
+
payload_type: 107,
|
|
45
|
+
rtx_payload_type: 108,
|
|
46
|
+
encode: true,
|
|
47
|
+
decode: true,
|
|
48
|
+
},
|
|
49
|
+
AV1: {
|
|
50
|
+
name: "AV1",
|
|
51
|
+
type: "video",
|
|
52
|
+
clockRate: 90000,
|
|
53
|
+
priority: 1000,
|
|
54
|
+
payload_type: 109,
|
|
55
|
+
rtx_payload_type: 110,
|
|
56
|
+
encode: true,
|
|
57
|
+
decode: true,
|
|
58
|
+
},
|
|
59
|
+
} as const;
|