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.
Files changed (123) hide show
  1. package/README.md +124 -2
  2. package/dist/client/GatewayEvents.d.ts +30 -0
  3. package/dist/client/GatewayEvents.js +2 -0
  4. package/dist/client/GatewayEvents.js.map +1 -0
  5. package/dist/client/GatewayOpCodes.d.ts +40 -0
  6. package/dist/client/GatewayOpCodes.js +42 -0
  7. package/dist/client/GatewayOpCodes.js.map +1 -0
  8. package/dist/client/Streamer.d.ts +30 -0
  9. package/dist/client/Streamer.js +188 -0
  10. package/dist/client/Streamer.js.map +1 -0
  11. package/dist/client/index.d.ts +3 -0
  12. package/dist/client/index.js +4 -0
  13. package/dist/client/index.js.map +1 -0
  14. package/dist/client/processing/AnnexBBitstreamReaderWriter.d.ts +23 -0
  15. package/dist/client/processing/AnnexBBitstreamReaderWriter.js +149 -0
  16. package/dist/client/processing/AnnexBBitstreamReaderWriter.js.map +1 -0
  17. package/dist/client/processing/AnnexBHelper.d.ts +93 -0
  18. package/dist/client/processing/AnnexBHelper.js +127 -0
  19. package/dist/client/processing/AnnexBHelper.js.map +1 -0
  20. package/dist/client/processing/SPSVUIRewriter.d.ts +1 -0
  21. package/dist/client/processing/SPSVUIRewriter.js +297 -0
  22. package/dist/client/processing/SPSVUIRewriter.js.map +1 -0
  23. package/dist/client/voice/BaseMediaConnection.d.ts +80 -0
  24. package/dist/client/voice/BaseMediaConnection.js +552 -0
  25. package/dist/client/voice/BaseMediaConnection.js.map +1 -0
  26. package/dist/client/voice/CodecPayloadType.d.ts +59 -0
  27. package/dist/client/voice/CodecPayloadType.js +60 -0
  28. package/dist/client/voice/CodecPayloadType.js.map +1 -0
  29. package/dist/client/voice/StreamConnection.d.ts +11 -0
  30. package/dist/client/voice/StreamConnection.js +34 -0
  31. package/dist/client/voice/StreamConnection.js.map +1 -0
  32. package/dist/client/voice/VoiceConnection.d.ts +8 -0
  33. package/dist/client/voice/VoiceConnection.js +15 -0
  34. package/dist/client/voice/VoiceConnection.js.map +1 -0
  35. package/dist/client/voice/VoiceMessageTypes.d.ts +178 -0
  36. package/dist/client/voice/VoiceMessageTypes.js +2 -0
  37. package/dist/client/voice/VoiceMessageTypes.js.map +1 -0
  38. package/dist/client/voice/VoiceOpCodes.d.ts +35 -0
  39. package/dist/client/voice/VoiceOpCodes.js +38 -0
  40. package/dist/client/voice/VoiceOpCodes.js.map +1 -0
  41. package/dist/client/voice/WebRtcWrapper.d.ts +23 -0
  42. package/dist/client/voice/WebRtcWrapper.js +159 -0
  43. package/dist/client/voice/WebRtcWrapper.js.map +1 -0
  44. package/dist/client/voice/index.d.ts +4 -0
  45. package/dist/client/voice/index.js +6 -0
  46. package/dist/client/voice/index.js.map +1 -0
  47. package/dist/index.d.ts +3 -0
  48. package/dist/index.js +4 -0
  49. package/dist/index.js.map +1 -0
  50. package/dist/media/AudioStream.d.ts +7 -0
  51. package/dist/media/AudioStream.js +12 -0
  52. package/dist/media/AudioStream.js.map +1 -0
  53. package/dist/media/BaseMediaStream.d.ts +31 -0
  54. package/dist/media/BaseMediaStream.js +164 -0
  55. package/dist/media/BaseMediaStream.js.map +1 -0
  56. package/dist/media/LibavCodecId.d.ts +541 -0
  57. package/dist/media/LibavCodecId.js +553 -0
  58. package/dist/media/LibavCodecId.js.map +1 -0
  59. package/dist/media/LibavDecoder.d.ts +5 -0
  60. package/dist/media/LibavDecoder.js +43 -0
  61. package/dist/media/LibavDecoder.js.map +1 -0
  62. package/dist/media/LibavDemuxer.d.ts +44 -0
  63. package/dist/media/LibavDemuxer.js +231 -0
  64. package/dist/media/LibavDemuxer.js.map +1 -0
  65. package/dist/media/VideoStream.d.ts +7 -0
  66. package/dist/media/VideoStream.js +12 -0
  67. package/dist/media/VideoStream.js.map +1 -0
  68. package/dist/media/encoders/index.d.ts +26 -0
  69. package/dist/media/encoders/index.js +12 -0
  70. package/dist/media/encoders/index.js.map +1 -0
  71. package/dist/media/encoders/merge.d.ts +3 -0
  72. package/dist/media/encoders/merge.js +10 -0
  73. package/dist/media/encoders/merge.js.map +1 -0
  74. package/dist/media/encoders/nvenc.d.ts +10 -0
  75. package/dist/media/encoders/nvenc.js +23 -0
  76. package/dist/media/encoders/nvenc.js.map +1 -0
  77. package/dist/media/encoders/software.d.ts +17 -0
  78. package/dist/media/encoders/software.js +31 -0
  79. package/dist/media/encoders/software.js.map +1 -0
  80. package/dist/media/encoders/vaapi.d.ts +6 -0
  81. package/dist/media/encoders/vaapi.js +22 -0
  82. package/dist/media/encoders/vaapi.js.map +1 -0
  83. package/dist/media/index.d.ts +4 -0
  84. package/dist/media/index.js +5 -0
  85. package/dist/media/index.js.map +1 -0
  86. package/dist/media/newApi.d.ts +130 -0
  87. package/dist/media/newApi.js +409 -0
  88. package/dist/media/newApi.js.map +1 -0
  89. package/dist/utils.d.ts +25 -0
  90. package/dist/utils.js +65 -0
  91. package/dist/utils.js.map +1 -0
  92. package/package.json +82 -5
  93. package/src/client/GatewayEvents.ts +48 -0
  94. package/src/client/GatewayOpCodes.ts +40 -0
  95. package/src/client/Streamer.ts +259 -0
  96. package/src/client/index.ts +3 -0
  97. package/src/client/processing/AnnexBBitstreamReaderWriter.ts +148 -0
  98. package/src/client/processing/AnnexBHelper.ts +134 -0
  99. package/src/client/processing/SPSVUIRewriter.ts +332 -0
  100. package/src/client/voice/BaseMediaConnection.ts +639 -0
  101. package/src/client/voice/CodecPayloadType.ts +59 -0
  102. package/src/client/voice/MediaUdp.ts.bak +171 -0
  103. package/src/client/voice/StreamConnection.ts +38 -0
  104. package/src/client/voice/VoiceConnection.ts +19 -0
  105. package/src/client/voice/VoiceMessageTypes.ts +263 -0
  106. package/src/client/voice/VoiceOpCodes.ts +36 -0
  107. package/src/client/voice/WebRtcWrapper.ts +218 -0
  108. package/src/client/voice/index.ts +5 -0
  109. package/src/index.ts +3 -0
  110. package/src/media/AudioStream.ts +18 -0
  111. package/src/media/BaseMediaStream.ts +198 -0
  112. package/src/media/LibavCodecId.ts +564 -0
  113. package/src/media/LibavDecoder.ts +47 -0
  114. package/src/media/LibavDemuxer.ts +293 -0
  115. package/src/media/VideoStream.ts +17 -0
  116. package/src/media/encoders/index.ts +27 -0
  117. package/src/media/encoders/merge.ts +14 -0
  118. package/src/media/encoders/nvenc.ts +38 -0
  119. package/src/media/encoders/software.ts +76 -0
  120. package/src/media/encoders/vaapi.ts +29 -0
  121. package/src/media/index.ts +4 -0
  122. package/src/media/newApi.ts +642 -0
  123. 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;