@m7mdxzx1/discord-video-strem 0.0.1

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 (89) hide show
  1. package/README.md +294 -0
  2. package/dist/client/GatewayEvents.d.ts +27 -0
  3. package/dist/client/GatewayEvents.js +1 -0
  4. package/dist/client/GatewayOpCodes.d.ts +40 -0
  5. package/dist/client/GatewayOpCodes.js +41 -0
  6. package/dist/client/Streamer.d.ts +41 -0
  7. package/dist/client/Streamer.js +189 -0
  8. package/dist/client/encryptor/TransportEncryptor.d.ts +16 -0
  9. package/dist/client/encryptor/TransportEncryptor.js +38 -0
  10. package/dist/client/index.d.ts +4 -0
  11. package/dist/client/index.js +4 -0
  12. package/dist/client/packet/AudioPacketizer.d.ts +8 -0
  13. package/dist/client/packet/AudioPacketizer.js +22 -0
  14. package/dist/client/packet/BaseMediaPacketizer.d.ts +109 -0
  15. package/dist/client/packet/BaseMediaPacketizer.js +243 -0
  16. package/dist/client/packet/VideoPacketizerAnnexB.d.ts +132 -0
  17. package/dist/client/packet/VideoPacketizerAnnexB.js +231 -0
  18. package/dist/client/packet/VideoPacketizerVP8.d.ts +15 -0
  19. package/dist/client/packet/VideoPacketizerVP8.js +56 -0
  20. package/dist/client/packet/index.d.ts +4 -0
  21. package/dist/client/packet/index.js +4 -0
  22. package/dist/client/processing/AnnexBHelper.d.ts +93 -0
  23. package/dist/client/processing/AnnexBHelper.js +132 -0
  24. package/dist/client/voice/BaseMediaConnection.d.ts +118 -0
  25. package/dist/client/voice/BaseMediaConnection.js +319 -0
  26. package/dist/client/voice/MediaUdp.d.ts +26 -0
  27. package/dist/client/voice/MediaUdp.js +140 -0
  28. package/dist/client/voice/StreamConnection.d.ts +10 -0
  29. package/dist/client/voice/StreamConnection.js +30 -0
  30. package/dist/client/voice/VoiceConnection.d.ts +7 -0
  31. package/dist/client/voice/VoiceConnection.js +10 -0
  32. package/dist/client/voice/VoiceMessageTypes.d.ts +136 -0
  33. package/dist/client/voice/VoiceMessageTypes.js +1 -0
  34. package/dist/client/voice/VoiceOpCodes.d.ts +21 -0
  35. package/dist/client/voice/VoiceOpCodes.js +22 -0
  36. package/dist/client/voice/index.d.ts +5 -0
  37. package/dist/client/voice/index.js +5 -0
  38. package/dist/index.d.ts +5 -0
  39. package/dist/index.js +5 -0
  40. package/dist/media/AudioStream.d.ts +25 -0
  41. package/dist/media/AudioStream.js +63 -0
  42. package/dist/media/BaseMediaStream.d.ts +31 -0
  43. package/dist/media/BaseMediaStream.js +145 -0
  44. package/dist/media/LibavCodecId.d.ts +541 -0
  45. package/dist/media/LibavCodecId.js +552 -0
  46. package/dist/media/LibavDecoder.d.ts +5 -0
  47. package/dist/media/LibavDecoder.js +63 -0
  48. package/dist/media/LibavDemuxer.d.ts +23 -0
  49. package/dist/media/LibavDemuxer.js +295 -0
  50. package/dist/media/VideoStream.d.ts +7 -0
  51. package/dist/media/VideoStream.js +10 -0
  52. package/dist/media/index.d.ts +1 -0
  53. package/dist/media/index.js +1 -0
  54. package/dist/media/newApi.d.ts +126 -0
  55. package/dist/media/newApi.js +387 -0
  56. package/dist/media/utils.d.ts +1 -0
  57. package/dist/media/utils.js +4 -0
  58. package/dist/utils.d.ts +28 -0
  59. package/dist/utils.js +54 -0
  60. package/package.json +69 -0
  61. package/src/client/GatewayEvents.ts +41 -0
  62. package/src/client/GatewayOpCodes.ts +40 -0
  63. package/src/client/Streamer.ts +279 -0
  64. package/src/client/encryptor/TransportEncryptor.ts +62 -0
  65. package/src/client/index.ts +4 -0
  66. package/src/client/packet/AudioPacketizer.ts +28 -0
  67. package/src/client/packet/BaseMediaPacketizer.ts +307 -0
  68. package/src/client/packet/VideoPacketizerAnnexB.ts +263 -0
  69. package/src/client/packet/VideoPacketizerVP8.ts +73 -0
  70. package/src/client/packet/index.ts +4 -0
  71. package/src/client/processing/AnnexBHelper.ts +142 -0
  72. package/src/client/voice/BaseMediaConnection.ts +407 -0
  73. package/src/client/voice/MediaUdp.ts +171 -0
  74. package/src/client/voice/StreamConnection.ts +33 -0
  75. package/src/client/voice/VoiceConnection.ts +15 -0
  76. package/src/client/voice/VoiceMessageTypes.ts +164 -0
  77. package/src/client/voice/VoiceOpCodes.ts +21 -0
  78. package/src/client/voice/index.ts +5 -0
  79. package/src/index.ts +5 -0
  80. package/src/media/AudioStream.ts +81 -0
  81. package/src/media/BaseMediaStream.ts +173 -0
  82. package/src/media/LibavCodecId.ts +566 -0
  83. package/src/media/LibavDecoder.ts +82 -0
  84. package/src/media/LibavDemuxer.ts +348 -0
  85. package/src/media/VideoStream.ts +15 -0
  86. package/src/media/index.ts +1 -0
  87. package/src/media/newApi.ts +618 -0
  88. package/src/media/utils.ts +6 -0
  89. package/src/utils.ts +77 -0
@@ -0,0 +1,279 @@
1
+ import { EventEmitter } from "node:events";
2
+ import { VoiceConnection } from "./voice/VoiceConnection.js";
3
+ import { StreamConnection } from "./voice/StreamConnection.js";
4
+ import { GatewayOpCodes } from "./GatewayOpCodes.js";
5
+ import type TypedEmitter from "typed-emitter";
6
+ import type { Client, DMChannel, GroupDMChannel, VoiceBasedChannel } from 'discord.js-selfbot-v13';
7
+ import type { MediaUdp } from "./voice/MediaUdp.js";
8
+ import type { GatewayEvent } from "./GatewayEvents.js";
9
+ import { generateStreamKey, parseStreamKey } from "../utils.js";
10
+
11
+ type EmitterEvents = {
12
+ [K in GatewayEvent["t"]]: (data: Extract<GatewayEvent, { t: K }>["d"]) => void
13
+ }
14
+
15
+ export type StreamerOptions = {
16
+ /**
17
+ * Force the use of ChaCha20 encryption. Faster on CPUs without AES-NI
18
+ */
19
+ forceChacha20Encryption: boolean;
20
+ /**
21
+ * Enable RTCP Sender Report for synchronization
22
+ */
23
+ rtcpSenderReportEnabled: boolean
24
+ }
25
+
26
+ export class Streamer {
27
+ private _voiceConnection?: VoiceConnection;
28
+ private _client: Client;
29
+ private _opts: StreamerOptions;
30
+ private _gatewayEmitter = new EventEmitter() as TypedEmitter.default<EmitterEvents>
31
+
32
+ constructor(client: Client, opts?: Partial<StreamerOptions>) {
33
+ this._client = client;
34
+ this._opts = {
35
+ forceChacha20Encryption: false,
36
+ rtcpSenderReportEnabled: true,
37
+ ...opts
38
+ };
39
+
40
+ //listen for messages
41
+ this.client.on('raw', (packet: GatewayEvent) => {
42
+ // @ts-expect-error I don't know how to make this work with TypeScript, so whatever
43
+ this._gatewayEmitter.emit(packet.t, packet.d);
44
+ });
45
+ }
46
+
47
+ public get client(): Client {
48
+ return this._client;
49
+ }
50
+
51
+ public get opts(): StreamerOptions {
52
+ return this._opts;
53
+ }
54
+
55
+ public get voiceConnection(): VoiceConnection | undefined {
56
+ return this._voiceConnection;
57
+ }
58
+
59
+ public sendOpcode(code: number, data: unknown): void {
60
+ this.client.ws.broadcast({
61
+ op: code,
62
+ d: data,
63
+ });
64
+ }
65
+
66
+ public joinVoiceChannel(channel: DMChannel | GroupDMChannel | VoiceBasedChannel): Promise<MediaUdp> {
67
+ let guildId: string | null = null;
68
+
69
+ if(channel.type === "GUILD_STAGE_VOICE" || channel.type === "GUILD_VOICE") {
70
+ guildId = channel.guildId
71
+ }
72
+
73
+ return this.joinVoice(guildId, channel.id);
74
+ }
75
+
76
+ /**
77
+ * Joins a voice channel and returns a MediaUdp object.
78
+ * @param guild_id the guild id of the voice channel. If null, it will join a DM voice channel.
79
+ * @param channel_id the channel id of the voice channel
80
+ * @returns the MediaUdp object
81
+ * @throws Error if the client is not logged in
82
+ */
83
+ public joinVoice(guild_id: string | null, channel_id: string): Promise<MediaUdp> {
84
+ return new Promise<MediaUdp>((resolve, reject) => {
85
+ if (!this.client.user) {
86
+ reject("Client not logged in");
87
+ return;
88
+ }
89
+ const user_id = this.client.user.id;
90
+ const voiceConn = new VoiceConnection(
91
+ this,
92
+ guild_id,
93
+ user_id,
94
+ channel_id,
95
+ (udp) => {
96
+ resolve(udp)
97
+ }
98
+ );
99
+ this._voiceConnection = voiceConn;
100
+ this._gatewayEmitter.on("VOICE_STATE_UPDATE", (d) => {
101
+ if (user_id !== d.user_id) return;
102
+ voiceConn.setSession(d.session_id);
103
+ });
104
+ this._gatewayEmitter.on("VOICE_SERVER_UPDATE", (d) => {
105
+ if (guild_id !== d.guild_id) return;
106
+
107
+ // channel_id is not set for guild voice calls
108
+ if(d.channel_id && (channel_id !== d.channel_id)) return;
109
+
110
+ voiceConn.setTokens(d.endpoint, d.token);
111
+ })
112
+ this.signalVideo(false);
113
+ });
114
+ }
115
+
116
+ public createStream(): Promise<MediaUdp> {
117
+ return new Promise<MediaUdp>((resolve, reject) => {
118
+ if (!this.client.user) {
119
+ reject("Client not logged in");
120
+ return;
121
+ }
122
+ if (!this.voiceConnection) {
123
+ reject("cannot start stream without first joining voice channel");
124
+ return;
125
+ }
126
+
127
+ this.signalStream();
128
+ const {
129
+ guildId: clientGuildId,
130
+ channelId: clientChannelId,
131
+ session_id
132
+ } = this.voiceConnection;
133
+ const {
134
+ id: clientUserId
135
+ } = this.client.user;
136
+
137
+ if (!session_id)
138
+ throw new Error("Session doesn't exist yet");
139
+ const streamConn = new StreamConnection(
140
+ this,
141
+ clientGuildId,
142
+ clientUserId,
143
+ clientChannelId,
144
+ (udp) => {
145
+ resolve(udp)
146
+ }
147
+ );
148
+ this.voiceConnection.streamConnection = streamConn;
149
+ this._gatewayEmitter.on("STREAM_CREATE", (d) => {
150
+ const { type, channelId, guildId, userId } = parseStreamKey(d.stream_key);
151
+
152
+ if (
153
+ clientGuildId !== guildId ||
154
+ clientChannelId !== channelId ||
155
+ clientUserId !== userId
156
+ )
157
+ return;
158
+
159
+ streamConn.serverId = d.rtc_server_id;
160
+ streamConn.streamKey = d.stream_key;
161
+ streamConn.setSession(session_id);
162
+ });
163
+ this._gatewayEmitter.on("STREAM_SERVER_UPDATE", (d) => {
164
+ const { type, channelId, guildId, userId } = parseStreamKey(d.stream_key);
165
+
166
+ if (
167
+ clientGuildId !== guildId ||
168
+ clientChannelId !== channelId ||
169
+ clientUserId !== userId
170
+ )
171
+ return;
172
+
173
+ streamConn.setTokens(d.endpoint, d.token);
174
+ })
175
+ });
176
+ }
177
+
178
+ public async setStreamPreview(image: Buffer): Promise<void> {
179
+ if (!this.client.token)
180
+ throw new Error("Please login :)");
181
+ if (!this.voiceConnection?.streamConnection?.guildId)
182
+ return;
183
+ const data = `data:image/jpeg;base64,${image.toString("base64")}`;
184
+ const { guildId } = this.voiceConnection.streamConnection;
185
+ const server = await this.client.guilds.fetch(guildId);
186
+ await server.members.me?.voice.postPreview(data);
187
+ }
188
+
189
+ public stopStream(): void {
190
+ const stream = this.voiceConnection?.streamConnection;
191
+
192
+ if (!stream) return;
193
+
194
+ stream.stop();
195
+
196
+ this.signalStopStream();
197
+
198
+ this.voiceConnection.streamConnection = undefined;
199
+ this._gatewayEmitter.removeAllListeners("STREAM_CREATE");
200
+ this._gatewayEmitter.removeAllListeners("STREAM_SERVER_UPDATE");
201
+ }
202
+
203
+ public leaveVoice(): void {
204
+ this.voiceConnection?.stop();
205
+
206
+ this.signalLeaveVoice();
207
+
208
+ this._voiceConnection = undefined;
209
+ this._gatewayEmitter.removeAllListeners("VOICE_STATE_UPDATE");
210
+ this._gatewayEmitter.removeAllListeners("VOICE_SERVER_UPDATE");
211
+ }
212
+
213
+ public signalVideo(video_enabled: boolean): void {
214
+ if (!this.voiceConnection)
215
+ return;
216
+ const {
217
+ guildId: guild_id,
218
+ channelId: channel_id,
219
+ } = this.voiceConnection;
220
+ this.sendOpcode(GatewayOpCodes.VOICE_STATE_UPDATE, {
221
+ guild_id: guild_id,
222
+ channel_id,
223
+ self_mute: false,
224
+ self_deaf: true,
225
+ self_video: video_enabled,
226
+ });
227
+ }
228
+
229
+ public signalStream(): void {
230
+ if (!this.voiceConnection)
231
+ return;
232
+ const {
233
+ type,
234
+ guildId: guild_id,
235
+ channelId: channel_id,
236
+ botId: user_id
237
+ } = this.voiceConnection;
238
+
239
+ const streamKey = generateStreamKey(type, guild_id, channel_id, user_id);
240
+
241
+ this.sendOpcode(GatewayOpCodes.STREAM_CREATE, {
242
+ type,
243
+ guild_id,
244
+ channel_id,
245
+ preferred_region: null,
246
+ });
247
+
248
+ this.sendOpcode(GatewayOpCodes.STREAM_SET_PAUSED, {
249
+ stream_key: streamKey,
250
+ paused: false,
251
+ });
252
+ }
253
+
254
+ public signalStopStream(): void {
255
+ if (!this.voiceConnection)
256
+ return;
257
+ const {
258
+ type,
259
+ guildId: guild_id,
260
+ channelId: channel_id,
261
+ botId: user_id
262
+ } = this.voiceConnection;
263
+
264
+ const streamKey = generateStreamKey(type, guild_id, channel_id, user_id);
265
+ this.sendOpcode(GatewayOpCodes.STREAM_DELETE, {
266
+ stream_key: streamKey
267
+ });
268
+ }
269
+
270
+ public signalLeaveVoice(): void {
271
+ this.sendOpcode(GatewayOpCodes.VOICE_STATE_UPDATE, {
272
+ guild_id: null,
273
+ channel_id: null,
274
+ self_mute: true,
275
+ self_deaf: false,
276
+ self_video: false,
277
+ });
278
+ }
279
+ }
@@ -0,0 +1,62 @@
1
+ import sp from "sodium-plus";
2
+ import { max_int32bit } from "../../utils.js";
3
+ const { SodiumPlus } = sp;
4
+
5
+ export interface TransportEncryptor {
6
+ encrypt(plaintext: Buffer, additionalData: Buffer): Promise<[Buffer, Buffer]>
7
+ }
8
+
9
+ export class AES256TransportEncryptor implements TransportEncryptor
10
+ {
11
+ private _nonce = 0;
12
+ private _secretKey: Promise<CryptoKey>;
13
+ constructor(secretKey: Buffer)
14
+ {
15
+ this._secretKey = crypto.subtle.importKey("raw",
16
+ secretKey,
17
+ {
18
+ name: "AES-GCM",
19
+ length: 32
20
+ },
21
+ false, ["encrypt"]
22
+ );
23
+ }
24
+ async encrypt(plaintext: Buffer, additionalData: Buffer): Promise<[Buffer, Buffer]> {
25
+ const nonceBuffer = Buffer.alloc(12);
26
+ nonceBuffer.writeUInt32BE(this._nonce);
27
+ this._nonce = (this._nonce + 1) % max_int32bit;
28
+
29
+ const ciphertext = Buffer.from(await crypto.subtle.encrypt({
30
+ name: "AES-GCM",
31
+ iv: nonceBuffer,
32
+ additionalData,
33
+ }, await this._secretKey, plaintext));
34
+
35
+ return [ciphertext, nonceBuffer]
36
+ }
37
+ }
38
+
39
+ export class Chacha20TransportEncryptor implements TransportEncryptor
40
+ {
41
+ private static sodium = SodiumPlus.auto();
42
+ private _nonce = 0;
43
+ private _secretKey: sp.CryptographyKey;
44
+ constructor(secretKey: Buffer)
45
+ {
46
+ this._secretKey = new sp.CryptographyKey(secretKey);
47
+ }
48
+ async encrypt(plaintext: Buffer, additionalData: Buffer): Promise<[Buffer, Buffer]> {
49
+ const nonceBuffer = Buffer.alloc(24);
50
+ nonceBuffer.writeUInt32BE(this._nonce);
51
+ this._nonce = (this._nonce + 1) % max_int32bit;
52
+
53
+ const ciphertext = await Chacha20TransportEncryptor.sodium
54
+ .then(s => s.crypto_aead_xchacha20poly1305_ietf_encrypt(
55
+ plaintext, nonceBuffer,
56
+ this._secretKey,
57
+ additionalData
58
+ ));
59
+
60
+ return [ciphertext, nonceBuffer];
61
+ }
62
+ }
@@ -0,0 +1,4 @@
1
+ export * from './packet/index.js';
2
+ export * from './voice/index.js';
3
+ export * from "./GatewayOpCodes.js";
4
+ export * from "./Streamer.js";
@@ -0,0 +1,28 @@
1
+ import type { MediaUdp } from "../voice/MediaUdp.js";
2
+ import { BaseMediaPacketizer } from "./BaseMediaPacketizer.js";
3
+ import { CodecPayloadType } from "../voice/BaseMediaConnection.js";
4
+
5
+ export class AudioPacketizer extends BaseMediaPacketizer {
6
+ constructor(connection: MediaUdp, ssrc: number) {
7
+ super(connection, ssrc, CodecPayloadType.opus.payload_type);
8
+ }
9
+
10
+ public override async sendFrame(frame: Buffer, frametime: number): Promise<void> {
11
+ super.sendFrame(frame, frametime);
12
+ const packet = await this.createPacket(frame);
13
+ this.mediaUdp.sendPacket(packet);
14
+ this.onFrameSent(packet.length, frametime);
15
+ }
16
+
17
+ public async createPacket(chunk: Buffer): Promise<Buffer> {
18
+ const header = this.makeRtpHeader();
19
+
20
+ const [ciphertext, nonceBuffer] = await this.encryptData(chunk, header);
21
+ return Buffer.concat([header, ciphertext, nonceBuffer.subarray(0, 4)]);
22
+ }
23
+
24
+ public override async onFrameSent(bytesSent: number, frametime: number): Promise<void> {
25
+ await super.onFrameSent(1, bytesSent, frametime);
26
+ this.incrementTimestamp(frametime * (48000 / 1000));
27
+ }
28
+ }
@@ -0,0 +1,307 @@
1
+ import sp from "sodium-plus";
2
+ import { Log } from "debug-level";
3
+ import { webcrypto } from 'node:crypto';
4
+ import { max_int16bit, max_int32bit, SupportedEncryptionModes } from "../../utils.js";
5
+ import type { MediaUdp } from "../voice/MediaUdp.js";
6
+
7
+ const { SodiumPlus, CryptographyKey } = sp;
8
+
9
+ const ntpEpoch = new Date("Jan 01 1900 GMT").getTime();
10
+
11
+ let sodium: Promise<sp.SodiumPlus> | undefined;
12
+
13
+ export class BaseMediaPacketizer {
14
+ private _loggerRtcpSr = new Log("packetizer:rtcp-sr");
15
+
16
+ private _ssrc: number;
17
+ private _payloadType: number;
18
+ private _mtu: number;
19
+ private _sequence: number;
20
+ private _timestamp: number;
21
+
22
+ private _totalBytes: number;
23
+ private _totalPackets: number;
24
+ private _lastPacketTime: number;
25
+ private _lastRtcpTime: number;
26
+ private _currentMediaTimestamp: number;
27
+ private _srInterval: number;
28
+
29
+ private _mediaUdp: MediaUdp;
30
+ private _extensionEnabled: boolean;
31
+
32
+ constructor(connection: MediaUdp, ssrc: number, payloadType: number, extensionEnabled = false) {
33
+ this._mediaUdp = connection;
34
+ this._payloadType = payloadType;
35
+ this._ssrc = ssrc;
36
+ this._sequence = 0;
37
+ this._timestamp = 0;
38
+ this._totalBytes = 0;
39
+ this._totalPackets = 0;
40
+ this._lastPacketTime = 0;
41
+ this._lastRtcpTime = 0;
42
+ this._currentMediaTimestamp = 0;
43
+ this._mtu = 1200;
44
+ this._extensionEnabled = extensionEnabled;
45
+
46
+ this._srInterval = 1000;
47
+ }
48
+
49
+ public get ssrc(): number | undefined
50
+ {
51
+ return this._ssrc;
52
+ }
53
+
54
+ public set ssrc(value: number)
55
+ {
56
+ this._ssrc = value;
57
+ this._totalBytes = this._totalPackets = 0;
58
+ }
59
+
60
+ /**
61
+ * The interval between 2 consecutive RTCP Sender Report packets in ms
62
+ */
63
+ public get srInterval(): number
64
+ {
65
+ return this._srInterval;
66
+ }
67
+
68
+ public set srInterval(interval: number)
69
+ {
70
+ this._srInterval = interval;
71
+ }
72
+
73
+ public async sendFrame(frame: Buffer, frametime: number): Promise<void> {
74
+ // override this
75
+ this._lastPacketTime = Date.now();
76
+ }
77
+
78
+ public async onFrameSent(packetsSent: number, bytesSent: number, frametime: number): Promise<void> {
79
+
80
+ if (this._mediaUdp.mediaConnection.streamer.opts.rtcpSenderReportEnabled)
81
+ {
82
+ this._totalPackets = this._totalPackets + packetsSent;
83
+ this._totalBytes = (this._totalBytes + bytesSent) % max_int32bit;
84
+
85
+ /**
86
+ * Not using modulo here, since the timestamp might not be an exact
87
+ * multiple of the interval
88
+ */
89
+ if (Math.floor(this._currentMediaTimestamp / this._srInterval) - Math.floor(this._lastRtcpTime / this._srInterval) > 0)
90
+ {
91
+ const senderReport = await this.makeRtcpSenderReport();
92
+ this._mediaUdp.sendPacket(senderReport);
93
+ this._lastRtcpTime = this._currentMediaTimestamp;
94
+ this._loggerRtcpSr.debug({
95
+ stats: {
96
+ ssrc: this._ssrc,
97
+ timestamp: this._timestamp,
98
+ totalPackets: this._totalPackets,
99
+ totalBytes: this._totalBytes
100
+ }
101
+ }, `Sent RTCP sender report for SSRC ${this._ssrc}`);
102
+ }
103
+ }
104
+ this._currentMediaTimestamp += frametime;
105
+ }
106
+
107
+ /**
108
+ * Partitions a buffer into chunks of length this.mtu
109
+ * @param data buffer to be partitioned
110
+ * @returns array of chunks
111
+ */
112
+ public partitionDataMTUSizedChunks(data: Buffer): Buffer[] {
113
+ let i = 0;
114
+ let len = data.length;
115
+
116
+ const out = [];
117
+
118
+ while (len > 0) {
119
+ const size = Math.min(len, this._mtu);
120
+ out.push(data.subarray(i, i + size));
121
+ len -= size;
122
+ i += size;
123
+ }
124
+
125
+ return out;
126
+ }
127
+
128
+ public getNewSequence(): number {
129
+ this._sequence = (this._sequence + 1) % max_int16bit;
130
+ return this._sequence;
131
+ }
132
+
133
+ public incrementTimestamp(incrementBy: number): void {
134
+ this._timestamp = (this._timestamp + incrementBy) % max_int32bit;
135
+ }
136
+
137
+ public makeRtpHeader(isLastPacket = true): Buffer {
138
+ const packetHeader = Buffer.alloc(12);
139
+
140
+ packetHeader[0] = 2 << 6 | ((this._extensionEnabled ? 1 : 0) << 4); // set version and flags
141
+ packetHeader[1] = this._payloadType; // set packet payload
142
+ if (isLastPacket)
143
+ packetHeader[1] |= 0b10000000; // mark M bit if last frame
144
+
145
+ packetHeader.writeUIntBE(this.getNewSequence(), 2, 2);
146
+ packetHeader.writeUIntBE(this._timestamp, 4, 4);
147
+ packetHeader.writeUIntBE(this._ssrc, 8, 4);
148
+ return packetHeader;
149
+ }
150
+
151
+ public async makeRtcpSenderReport(): Promise<Buffer> {
152
+ const packetHeader = Buffer.allocUnsafe(8);
153
+
154
+ packetHeader[0] = 0x80; // RFC1889 v2, no padding, no reception report count
155
+ packetHeader[1] = 0xc8; // Type: Sender Report (200)
156
+
157
+ // Packet length (always 0x06 for some reason)
158
+ packetHeader[2] = 0x00;
159
+ packetHeader[3] = 0x06;
160
+ packetHeader.writeUInt32BE(this._ssrc, 4);
161
+
162
+ const senderReport = Buffer.allocUnsafe(20);
163
+
164
+ // Convert from floating point to 32.32 fixed point
165
+ // Convert each part separately to reduce precision loss
166
+ const ntpTimestamp = (this._lastPacketTime - ntpEpoch) / 1000;
167
+ const ntpTimestampMsw = Math.floor(ntpTimestamp);
168
+ const ntpTimestampLsw = Math.round((ntpTimestamp - ntpTimestampMsw) * max_int32bit);
169
+
170
+ senderReport.writeUInt32BE(ntpTimestampMsw, 0);
171
+ senderReport.writeUInt32BE(ntpTimestampLsw, 4);
172
+ senderReport.writeUInt32BE(this._timestamp, 8);
173
+ senderReport.writeUInt32BE(this._totalPackets % max_int32bit, 12);
174
+ senderReport.writeUInt32BE(this._totalBytes, 16);
175
+
176
+ const [ciphertext, nonceBuffer] = await this.encryptData(senderReport, packetHeader);
177
+ return Buffer.concat([
178
+ packetHeader, ciphertext,
179
+ nonceBuffer.subarray(0, 4)
180
+ ]);
181
+ }
182
+
183
+ /**
184
+ * Creates a one-byte extension header
185
+ * https://www.rfc-editor.org/rfc/rfc5285#section-4.2
186
+ * @returns extension header
187
+ */
188
+ public createExtensionHeader(extensions: { id: number, len: number, val: number}[]): Buffer {
189
+ /**
190
+ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
191
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
192
+ | defined by profile | length |
193
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
194
+ */
195
+ const profile = Buffer.alloc(4);
196
+ profile[0] = 0xBE;
197
+ profile[1] = 0xDE;
198
+ profile.writeInt16BE(extensions.length, 2); // extension count
199
+
200
+ return profile
201
+ }
202
+
203
+ /**
204
+ * Creates a extension payload in one-byte format according to https://www.rfc-editor.org/rfc/rfc7941.html#section-4.1.1
205
+ * Discord seems to send this extension on every video packet. The extension ids for Discord can be found by connecting
206
+ * to their webrtc gateway using the webclient and the client will send an SDP offer containing it
207
+ * @returns extension payload
208
+ */
209
+ public createExtensionPayload(extensions: { id: number, len: number, val: number}[]): Buffer {
210
+ const extensionsData = [];
211
+ for(const ext of extensions){
212
+ /**
213
+ * EXTENSION DATA - each extension payload is 32 bits
214
+ */
215
+ const data = Buffer.alloc(4);
216
+
217
+ // https://webrtc.googlesource.com/src/+/refs/heads/main/docs/native-code/rtp-hdrext/playout-delay
218
+ if(ext.id === 5) {
219
+ /**
220
+ * 0 1 2 3 4 5 6 7
221
+ +-+-+-+-+-+-+-+-+
222
+ | ID | len |
223
+ +-+-+-+-+-+-+-+-+
224
+
225
+ where len = actual length - 1
226
+ */
227
+ data[0] = (ext.id & 0b00001111) << 4;
228
+ data[0] |= ((ext.len - 1) & 0b00001111);
229
+
230
+ /** Specific to type playout-delay
231
+ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4
232
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
233
+ | MIN delay | MAX delay |
234
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
235
+ */
236
+ data.writeUIntBE(ext.val, 1, 2); // not quite but its 0 anyway
237
+ }
238
+
239
+ extensionsData.push(data);
240
+ }
241
+
242
+ return Buffer.concat(extensionsData)
243
+ }
244
+
245
+ /**
246
+ * Encrypt packet payload. Encrpyed Payload is determined to be
247
+ * according to https://tools.ietf.org/html/rfc3711#section-3.1
248
+ * and https://datatracker.ietf.org/doc/html/rfc7714#section-8.2
249
+ *
250
+ * Associated Data: The version V (2 bits), padding flag P (1 bit),
251
+ extension flag X (1 bit), Contributing Source
252
+ (CSRC) count CC (4 bits), marker M (1 bit),
253
+ Payload Type PT (7 bits), sequence number
254
+ (16 bits), timestamp (32 bits), SSRC (32 bits),
255
+ optional CSRC identifiers (32 bits each), and
256
+ optional RTP extension (variable length).
257
+
258
+ Plaintext: The RTP payload (variable length), RTP padding
259
+ (if used, variable length), and RTP pad count (if
260
+ used, 1 octet).
261
+
262
+ Raw Data: The optional variable-length SRTP Master Key
263
+ Identifier (MKI) and SRTP authentication tag
264
+ (whose use is NOT RECOMMENDED). These fields are
265
+ appended after encryption has been performed.
266
+
267
+ 0 1 2 3
268
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
269
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
270
+ A |V=2|P|X| CC |M| PT | sequence number |
271
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
272
+ A | timestamp |
273
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
274
+ A | synchronization source (SSRC) identifier |
275
+ +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
276
+ A | contributing source (CSRC) identifiers (optional) |
277
+ A | .... |
278
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
279
+ A | RTP extension header (OPTIONAL) |
280
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
281
+ P | payload ... |
282
+ P | +-------------------------------+
283
+ P | | RTP padding | RTP pad count |
284
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
285
+
286
+ P = Plaintext (to be encrypted and authenticated)
287
+ A = Associated Data (to be authenticated only)
288
+ * @param plaintext
289
+ * @param nonceBuffer
290
+ * @param additionalData
291
+ * @returns ciphertext
292
+ */
293
+ public encryptData(plaintext: Buffer, additionalData: Buffer): Promise<[Buffer, Buffer]> {
294
+ const encryptor = this._mediaUdp.mediaConnection.transportEncryptor;
295
+ if (!encryptor)
296
+ throw new Error("Transport encryptor not defined. Did you forget to select protocol?");
297
+ return encryptor.encrypt(plaintext, additionalData);
298
+ }
299
+
300
+ public get mediaUdp(): MediaUdp {
301
+ return this._mediaUdp;
302
+ }
303
+
304
+ public get mtu(): number {
305
+ return this._mtu;
306
+ }
307
+ }