djs-selfbot-v13 3.7.32 → 3.7.34
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/package.json +8 -1
- package/src/client/Client.js +68 -1
- package/src/client/voice/ClientVoiceManager.js +95 -17
- package/src/client/voice/StreamSession.js +193 -0
- package/src/client/voice/VoiceConnection.js +266 -19
- package/src/client/voice/WebRtcStreamSession.js +191 -0
- package/src/client/voice/dispatcher/AnnexBDispatcher.js +64 -11
- package/src/client/voice/dispatcher/BaseDispatcher.js +13 -9
- package/src/client/voice/dispatcher/VideoDispatcher.js +33 -0
- package/src/client/voice/networking/DAVESession.js +234 -0
- package/src/client/voice/networking/VoiceWebSocket.js +240 -24
- package/src/client/voice/player/MediaPlayer.js +3 -1
- package/src/client/voice/player/processing/AnnexBBitstreamReaderWriter.js +137 -0
- package/src/client/voice/player/processing/SPSVUIRewriter.js +203 -0
- package/src/client/voice/receiver/PacketHandler.js +11 -4
- package/src/errors/Messages.js +8 -1
- package/src/util/Constants.js +12 -0
- package/typings/index.d.ts +36 -0
|
@@ -4,7 +4,8 @@ const EventEmitter = require('events');
|
|
|
4
4
|
const { setTimeout, setInterval } = require('node:timers');
|
|
5
5
|
const WebSocket = require('../../../WebSocket');
|
|
6
6
|
const { Error } = require('../../../errors');
|
|
7
|
-
const { Opcodes, VoiceOpcodes } = require('../../../util/Constants');
|
|
7
|
+
const { Opcodes, VoiceOpcodes, VoiceStatus } = require('../../../util/Constants');
|
|
8
|
+
const { DAVESession, getMaxProtocolVersion } = require('./DAVESession');
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Represents a Voice Connection's WebSocket.
|
|
@@ -29,6 +30,7 @@ class VoiceWebSocket extends EventEmitter {
|
|
|
29
30
|
this._sequenceNumber = -1;
|
|
30
31
|
|
|
31
32
|
this.dead = false;
|
|
33
|
+
this._identified = false;
|
|
32
34
|
this.connection.on('closing', this.shutdown.bind(this));
|
|
33
35
|
}
|
|
34
36
|
|
|
@@ -57,6 +59,7 @@ class VoiceWebSocket extends EventEmitter {
|
|
|
57
59
|
this.ws = null;
|
|
58
60
|
}
|
|
59
61
|
this.clearHeartbeat();
|
|
62
|
+
this._identified = false;
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
/**
|
|
@@ -67,7 +70,7 @@ class VoiceWebSocket extends EventEmitter {
|
|
|
67
70
|
if (this.dead) return;
|
|
68
71
|
if (this.ws) this.reset();
|
|
69
72
|
if (this.attempts >= 5) {
|
|
70
|
-
this.emit('
|
|
73
|
+
this.emit('error', new Error('VOICE_CONNECTION_ATTEMPTS_EXCEEDED', this.attempts));
|
|
71
74
|
return;
|
|
72
75
|
}
|
|
73
76
|
|
|
@@ -87,13 +90,14 @@ class VoiceWebSocket extends EventEmitter {
|
|
|
87
90
|
|
|
88
91
|
/**
|
|
89
92
|
* Sends data to the WebSocket if it is open.
|
|
90
|
-
* @param {string} data The data to send to the WebSocket
|
|
91
|
-
* @returns {Promise<string>}
|
|
93
|
+
* @param {string|Buffer} data The data to send to the WebSocket
|
|
94
|
+
* @returns {Promise<string|Buffer>}
|
|
92
95
|
*/
|
|
93
96
|
send(data) {
|
|
94
|
-
|
|
97
|
+
const preview = typeof data === 'string' ? data : `[bin] ${data.byteLength} bytes`;
|
|
98
|
+
this.emit('debug', `[WS] >> ${preview}`);
|
|
95
99
|
return new Promise((resolve, reject) => {
|
|
96
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) throw new Error('WS_NOT_OPEN',
|
|
100
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) throw new Error('WS_NOT_OPEN', preview);
|
|
97
101
|
this.ws.send(data, null, error => {
|
|
98
102
|
if (error) reject(error);
|
|
99
103
|
else resolve(data);
|
|
@@ -111,23 +115,48 @@ class VoiceWebSocket extends EventEmitter {
|
|
|
111
115
|
return this.send(packet);
|
|
112
116
|
}
|
|
113
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Sends a binary message over the WebSocket.
|
|
120
|
+
* @param {number} opcode The opcode to use
|
|
121
|
+
* @param {Buffer} payload The payload to send
|
|
122
|
+
* @returns {Promise<Buffer>}
|
|
123
|
+
*/
|
|
124
|
+
sendBinaryMessage(opcode, payload) {
|
|
125
|
+
const message = Buffer.concat([Buffer.from([opcode]), payload]);
|
|
126
|
+
return this.send(message);
|
|
127
|
+
}
|
|
128
|
+
|
|
114
129
|
/**
|
|
115
130
|
* Called whenever the WebSocket opens.
|
|
116
131
|
*/
|
|
117
132
|
onOpen() {
|
|
118
133
|
this.emit('debug', `[WS] opened at gateway ${this.connection.authentication.endpoint}`);
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
this.
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
sendIdentify() {
|
|
137
|
+
if (this._identified) return Promise.resolve();
|
|
138
|
+
this._identified = true;
|
|
139
|
+
|
|
140
|
+
const isStream = this.connection.constructor.name === 'StreamConnection';
|
|
141
|
+
const sessionId = this.connection.authentication.sessionId;
|
|
142
|
+
const maxDave = getMaxProtocolVersion();
|
|
143
|
+
const data = {
|
|
144
|
+
server_id: this.connection.serverId || this.connection.channel.guild?.id || this.connection.channel.id,
|
|
145
|
+
user_id: this.client.user.id,
|
|
146
|
+
token: this.connection.authentication.token,
|
|
147
|
+
session_id: sessionId,
|
|
148
|
+
};
|
|
149
|
+
if (maxDave > 0) data.max_dave_protocol_version = maxDave;
|
|
150
|
+
|
|
151
|
+
if (isStream) {
|
|
152
|
+
data.channel_id = this.connection.channel.id;
|
|
153
|
+
data.streams = [{ type: 'screen', rid: '100', quality: 100 }];
|
|
154
|
+
data.video = true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return this.sendPacket({
|
|
158
|
+
op: VoiceOpcodes.IDENTIFY,
|
|
159
|
+
d: data,
|
|
131
160
|
});
|
|
132
161
|
}
|
|
133
162
|
|
|
@@ -138,18 +167,105 @@ class VoiceWebSocket extends EventEmitter {
|
|
|
138
167
|
*/
|
|
139
168
|
onMessage(event) {
|
|
140
169
|
try {
|
|
141
|
-
|
|
170
|
+
const { data } = event;
|
|
171
|
+
if (data instanceof ArrayBuffer || Buffer.isBuffer(data)) {
|
|
172
|
+
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
173
|
+
const seq = buffer.readUInt16BE(0);
|
|
174
|
+
const op = buffer.readUInt8(2);
|
|
175
|
+
const payload = buffer.subarray(3);
|
|
176
|
+
this._sequenceNumber = seq;
|
|
177
|
+
this.emit('debug', `[WS] << [bin] opcode ${op}, seq ${seq}, ${payload.byteLength} bytes`);
|
|
178
|
+
return this.onBinaryMessage({ op, seq, payload });
|
|
179
|
+
}
|
|
180
|
+
return this.onPacket(WebSocket.unpack(data, 'json'));
|
|
142
181
|
} catch (error) {
|
|
143
182
|
return this.onError(error);
|
|
144
183
|
}
|
|
145
184
|
}
|
|
146
185
|
|
|
186
|
+
usesSharedDaveSession() {
|
|
187
|
+
return (
|
|
188
|
+
this.connection.constructor.name === 'StreamConnection' &&
|
|
189
|
+
this.connection.voiceConnection?.dave === this.connection.dave
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
onBinaryMessage(message) {
|
|
194
|
+
const { dave } = this.connection;
|
|
195
|
+
if (!dave) return;
|
|
196
|
+
if (this.usesSharedDaveSession()) {
|
|
197
|
+
this.emit('debug', `[WS] << [bin] opcode ${message.op} ignored (shared MLS session)`);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
switch (message.op) {
|
|
202
|
+
case VoiceOpcodes.DAVE_MLS_EXTERNAL_SENDER:
|
|
203
|
+
dave.setExternalSender(message.payload);
|
|
204
|
+
break;
|
|
205
|
+
case VoiceOpcodes.DAVE_MLS_PROPOSALS: {
|
|
206
|
+
const payload = dave.processProposals(message.payload, this.connection.connectedClients);
|
|
207
|
+
if (payload) this.sendBinaryMessage(VoiceOpcodes.DAVE_MLS_COMMIT_WELCOME, payload);
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
case VoiceOpcodes.DAVE_MLS_ANNOUNCE_COMMIT_TRANSITION: {
|
|
211
|
+
const { transitionId, success } = dave.processCommit(message.payload);
|
|
212
|
+
if (success) {
|
|
213
|
+
if (transitionId === 0) {
|
|
214
|
+
this.emit('transitioned', transitionId);
|
|
215
|
+
} else {
|
|
216
|
+
this.sendPacket({
|
|
217
|
+
op: VoiceOpcodes.DAVE_TRANSITION_READY,
|
|
218
|
+
d: { transition_id: transitionId },
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
case VoiceOpcodes.DAVE_MLS_WELCOME: {
|
|
225
|
+
const { transitionId, success } = dave.processWelcome(message.payload);
|
|
226
|
+
if (success) {
|
|
227
|
+
if (transitionId === 0) {
|
|
228
|
+
this.emit('transitioned', transitionId);
|
|
229
|
+
} else {
|
|
230
|
+
this.sendPacket({
|
|
231
|
+
op: VoiceOpcodes.DAVE_TRANSITION_READY,
|
|
232
|
+
d: { transition_id: transitionId },
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
default:
|
|
239
|
+
this.emit('unknownPacket', message);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
147
243
|
/**
|
|
148
244
|
* Called whenever the connection to the WebSocket server is lost.
|
|
149
245
|
* @param {CloseEvent} event The WebSocket close event
|
|
150
246
|
*/
|
|
151
247
|
onClose(event) {
|
|
152
248
|
this.emit('debug', `[WS] closed with code ${event.code} and reason: ${event.reason}`);
|
|
249
|
+
if (event.code === 4017) {
|
|
250
|
+
this.dead = true;
|
|
251
|
+
this.emit(
|
|
252
|
+
'error',
|
|
253
|
+
new Error('VOICE_DAVE_REQUIRED', 'Discord requires DAVE/E2EE protocol support. Ensure @snazzah/davey is installed.'),
|
|
254
|
+
);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (event.code === 4006) {
|
|
258
|
+
this.dead = true;
|
|
259
|
+
const guildId = this.connection.channel.guild?.id;
|
|
260
|
+
if (guildId) {
|
|
261
|
+
this.connection.channel.client.ws.broadcast({
|
|
262
|
+
op: Opcodes.VOICE_STATE_UPDATE,
|
|
263
|
+
d: { guild_id: guildId, channel_id: null, self_mute: false, self_deaf: false },
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
this.emit('error', new Error('VOICE_SESSION_EXPIRED'));
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
153
269
|
if (!this.dead) setTimeout(this.connect.bind(this), this.attempts * 1000).unref();
|
|
154
270
|
}
|
|
155
271
|
|
|
@@ -162,6 +278,90 @@ class VoiceWebSocket extends EventEmitter {
|
|
|
162
278
|
this.emit('error', error);
|
|
163
279
|
}
|
|
164
280
|
|
|
281
|
+
setupDaveSession(protocolVersion) {
|
|
282
|
+
const isStream = this.connection.constructor.name === 'StreamConnection';
|
|
283
|
+
const parentDave = this.connection.voiceConnection?.dave;
|
|
284
|
+
|
|
285
|
+
if (isStream && parentDave?.session) {
|
|
286
|
+
this.connection.dave = parentDave;
|
|
287
|
+
this.connection.connectedClients.add(this.client.user.id);
|
|
288
|
+
for (const id of this.connection.voiceConnection.connectedClients) {
|
|
289
|
+
this.connection.connectedClients.add(id);
|
|
290
|
+
}
|
|
291
|
+
this.emit('debug', '[DAVE] Reusing parent voice connection MLS session');
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (this.connection.dave) {
|
|
296
|
+
const isSharedDave = isStream && this.connection.dave === parentDave;
|
|
297
|
+
if (!isSharedDave) {
|
|
298
|
+
this.connection.dave.destroy();
|
|
299
|
+
this.connection.dave.removeAllListeners();
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (!protocolVersion || getMaxProtocolVersion() === 0) {
|
|
304
|
+
this.connection.dave = null;
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const session = new DAVESession(
|
|
309
|
+
protocolVersion,
|
|
310
|
+
this.client.user.id,
|
|
311
|
+
this.connection.channel.id,
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
session.on('debug', msg => this.emit('debug', `[DAVE] ${msg}`));
|
|
315
|
+
session.on('keyPackage', keyPackage => {
|
|
316
|
+
this.sendBinaryMessage(VoiceOpcodes.DAVE_MLS_KEY_PACKAGE, keyPackage).catch(e => this.emit('error', e));
|
|
317
|
+
});
|
|
318
|
+
session.on('invalidateTransition', transitionId => {
|
|
319
|
+
this.sendPacket({
|
|
320
|
+
op: VoiceOpcodes.DAVE_MLS_INVALID_COMMIT_WELCOME,
|
|
321
|
+
d: { transition_id: transitionId },
|
|
322
|
+
}).catch(e => this.emit('error', e));
|
|
323
|
+
});
|
|
324
|
+
session.on('error', err => this.emit('error', err));
|
|
325
|
+
|
|
326
|
+
this.connection.dave = session;
|
|
327
|
+
this.connection.connectedClients.add(this.client.user.id);
|
|
328
|
+
if (this.connection.voiceConnection?.connectedClients) {
|
|
329
|
+
for (const id of this.connection.voiceConnection.connectedClients) {
|
|
330
|
+
this.connection.connectedClients.add(id);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
session.reinit();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
handleDavePacket(packet) {
|
|
337
|
+
const { dave } = this.connection;
|
|
338
|
+
if (!dave) return;
|
|
339
|
+
|
|
340
|
+
switch (packet.op) {
|
|
341
|
+
case VoiceOpcodes.DAVE_PREPARE_TRANSITION: {
|
|
342
|
+
const sendReady = dave.prepareTransition(packet.d);
|
|
343
|
+
if (sendReady) {
|
|
344
|
+
this.sendPacket({
|
|
345
|
+
op: VoiceOpcodes.DAVE_TRANSITION_READY,
|
|
346
|
+
d: { transition_id: packet.d.transition_id },
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
if (packet.d.transition_id === 0) this.emit('transitioned', 0);
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
case VoiceOpcodes.DAVE_EXECUTE_TRANSITION: {
|
|
353
|
+
const transitioned = dave.executeTransition(packet.d.transition_id);
|
|
354
|
+
if (transitioned) this.emit('transitioned', packet.d.transition_id);
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
case VoiceOpcodes.DAVE_PREPARE_EPOCH:
|
|
358
|
+
dave.prepareEpoch(packet.d);
|
|
359
|
+
break;
|
|
360
|
+
default:
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
165
365
|
/**
|
|
166
366
|
* Called whenever a valid packet is received from the WebSocket.
|
|
167
367
|
* @param {Object} packet The received packet
|
|
@@ -169,9 +369,24 @@ class VoiceWebSocket extends EventEmitter {
|
|
|
169
369
|
onPacket(packet) {
|
|
170
370
|
this.emit('debug', `[WS] << ${JSON.stringify(packet)}`);
|
|
171
371
|
if (packet.seq) this._sequenceNumber = packet.seq;
|
|
372
|
+
|
|
373
|
+
if (
|
|
374
|
+
[
|
|
375
|
+
VoiceOpcodes.DAVE_PREPARE_TRANSITION,
|
|
376
|
+
VoiceOpcodes.DAVE_EXECUTE_TRANSITION,
|
|
377
|
+
VoiceOpcodes.DAVE_PREPARE_EPOCH,
|
|
378
|
+
].includes(packet.op)
|
|
379
|
+
) {
|
|
380
|
+
if (!this.usesSharedDaveSession()) this.handleDavePacket(packet);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
172
384
|
switch (packet.op) {
|
|
173
385
|
case VoiceOpcodes.HELLO:
|
|
174
386
|
this.setHeartbeat(packet.d.heartbeat_interval);
|
|
387
|
+
this.sendIdentify().catch(() => {
|
|
388
|
+
this.emit('error', new Error('VOICE_JOIN_SOCKET_CLOSED'));
|
|
389
|
+
});
|
|
175
390
|
break;
|
|
176
391
|
case VoiceOpcodes.READY:
|
|
177
392
|
/**
|
|
@@ -180,11 +395,11 @@ class VoiceWebSocket extends EventEmitter {
|
|
|
180
395
|
* @event VoiceWebSocket#ready
|
|
181
396
|
*/
|
|
182
397
|
this.emit('ready', packet.d);
|
|
183
|
-
this.connection.setVideoStatus(false);
|
|
184
398
|
break;
|
|
185
399
|
/* eslint-disable no-case-declarations */
|
|
186
400
|
case VoiceOpcodes.SESSION_DESCRIPTION:
|
|
187
401
|
packet.d.secret_key = new Uint8Array(packet.d.secret_key);
|
|
402
|
+
this.setupDaveSession(packet.d.dave_protocol_version ?? 0);
|
|
188
403
|
/**
|
|
189
404
|
* Emitted once the Voice Websocket receives a description of this voice session.
|
|
190
405
|
* @param {Object} packet The received packet
|
|
@@ -192,6 +407,9 @@ class VoiceWebSocket extends EventEmitter {
|
|
|
192
407
|
*/
|
|
193
408
|
this.emit('sessionDescription', packet.d);
|
|
194
409
|
break;
|
|
410
|
+
case VoiceOpcodes.CLIENTS_CONNECT:
|
|
411
|
+
for (const id of packet.d.user_ids) this.connection.connectedClients.add(id);
|
|
412
|
+
break;
|
|
195
413
|
case VoiceOpcodes.CLIENT_CONNECT:
|
|
196
414
|
this.connection.ssrcMap.set(+packet.d.audio_ssrc, {
|
|
197
415
|
userId: packet.d.user_id,
|
|
@@ -200,6 +418,7 @@ class VoiceWebSocket extends EventEmitter {
|
|
|
200
418
|
});
|
|
201
419
|
break;
|
|
202
420
|
case VoiceOpcodes.CLIENT_DISCONNECT:
|
|
421
|
+
this.connection.connectedClients.delete(packet.d.user_id);
|
|
203
422
|
const streamInfo = this.connection.receiver && this.connection.receiver.packets.streams.get(packet.d.user_id);
|
|
204
423
|
if (streamInfo) {
|
|
205
424
|
this.connection.receiver.packets.streams.delete(packet.d.user_id);
|
|
@@ -258,10 +477,7 @@ class VoiceWebSocket extends EventEmitter {
|
|
|
258
477
|
* Clears a heartbeat interval, if one exists.
|
|
259
478
|
*/
|
|
260
479
|
clearHeartbeat() {
|
|
261
|
-
if (!this.heartbeatInterval)
|
|
262
|
-
this.emit('warn', 'Tried to clear a heartbeat interval that does not exist');
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
480
|
+
if (!this.heartbeatInterval) return;
|
|
265
481
|
clearInterval(this.heartbeatInterval);
|
|
266
482
|
this.heartbeatInterval = null;
|
|
267
483
|
}
|
|
@@ -214,7 +214,9 @@ class MediaPlayer extends EventEmitter {
|
|
|
214
214
|
args.push(...FFMPEG_H265_ARGUMENTS(options));
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
|
|
217
|
+
if (options?.fps) {
|
|
218
|
+
args.push('-g', String(options.fps), '-keyint_min', String(options.fps), '-sc_threshold', '0');
|
|
219
|
+
}
|
|
218
220
|
|
|
219
221
|
if (options?.inputFFmpegArgs) {
|
|
220
222
|
args.unshift(...options.inputFFmpegArgs);
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
class AnnexBBitstreamReader {
|
|
4
|
+
constructor(buffer) {
|
|
5
|
+
this._buffer = buffer;
|
|
6
|
+
this._byteOffset = 0;
|
|
7
|
+
this._bitOffset = 0;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
readBits(count) {
|
|
11
|
+
if (count === 0) return 0;
|
|
12
|
+
let result = 0;
|
|
13
|
+
while (count > 0) {
|
|
14
|
+
if (this._byteOffset >= this._buffer.length) throw new Error('Bad byte offset');
|
|
15
|
+
if (
|
|
16
|
+
this._bitOffset === 0 &&
|
|
17
|
+
this._byteOffset >= 2 &&
|
|
18
|
+
this._buffer[this._byteOffset - 2] === 0 &&
|
|
19
|
+
this._buffer[this._byteOffset - 1] === 0 &&
|
|
20
|
+
this._buffer[this._byteOffset] === 3
|
|
21
|
+
) {
|
|
22
|
+
this._byteOffset++;
|
|
23
|
+
}
|
|
24
|
+
if (this._bitOffset === 0 && count >= 8) {
|
|
25
|
+
result = (result << 8) | this._buffer[this._byteOffset++];
|
|
26
|
+
count -= 8;
|
|
27
|
+
} else {
|
|
28
|
+
const numBitsToRead = Math.min(count, 8 - this._bitOffset);
|
|
29
|
+
const mask = (1 << numBitsToRead) - 1;
|
|
30
|
+
const newBits = (this._buffer[this._byteOffset] >> (8 - this._bitOffset - numBitsToRead)) & mask;
|
|
31
|
+
result = (result << numBitsToRead) | newBits;
|
|
32
|
+
count -= numBitsToRead;
|
|
33
|
+
this._bitOffset += numBitsToRead;
|
|
34
|
+
if (this._bitOffset === 8) {
|
|
35
|
+
this._bitOffset = 0;
|
|
36
|
+
this._byteOffset++;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
readUnsigned(bits) {
|
|
44
|
+
return this.readBits(bits);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
readSigned(bits) {
|
|
48
|
+
const unsigned = this.readUnsigned(bits);
|
|
49
|
+
if (unsigned & (1 << (bits - 1))) return unsigned - (1 << bits);
|
|
50
|
+
return unsigned;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
readUnsignedExpGolomb() {
|
|
54
|
+
let leading0 = 0;
|
|
55
|
+
while (this.readBits(1) === 0) leading0++;
|
|
56
|
+
return (1 << leading0) + this.readBits(leading0) - 1;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
readSignedExpGolomb() {
|
|
60
|
+
const unsigned = this.readUnsignedExpGolomb();
|
|
61
|
+
if (unsigned % 2 === 0) return unsigned / -2;
|
|
62
|
+
return (unsigned + 1) / 2;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
class AnnexBBitstreamWriter {
|
|
67
|
+
constructor() {
|
|
68
|
+
this._arr = [];
|
|
69
|
+
this._pendingByte = 0;
|
|
70
|
+
this._bitOffset = 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
toBuffer() {
|
|
74
|
+
return Buffer.from(this._arr);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
flush() {
|
|
78
|
+
if (this._pendingByte <= 3 && this._arr.at(-1) === 0 && this._arr.at(-2) === 0) this._arr.push(3);
|
|
79
|
+
this._arr.push(this._pendingByte);
|
|
80
|
+
this._pendingByte = 0;
|
|
81
|
+
this._bitOffset = 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
writeBits(bits, count) {
|
|
85
|
+
while (count > 0) {
|
|
86
|
+
if (this._bitOffset === 0) {
|
|
87
|
+
if (count >= 8) {
|
|
88
|
+
this._pendingByte = (bits >> (count - 8)) & 0xff;
|
|
89
|
+
count -= 8;
|
|
90
|
+
this.flush();
|
|
91
|
+
} else {
|
|
92
|
+
const mask = (1 << count) - 1;
|
|
93
|
+
this._pendingByte |= (bits & mask) << (8 - count);
|
|
94
|
+
this._bitOffset = count;
|
|
95
|
+
count = 0;
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
const numBitsToWrite = Math.min(8 - this._bitOffset, count);
|
|
99
|
+
const bitsToWrite = (bits >> (count - numBitsToWrite)) & ((1 << numBitsToWrite) - 1);
|
|
100
|
+
this._pendingByte |= bitsToWrite << (8 - this._bitOffset - numBitsToWrite);
|
|
101
|
+
count -= numBitsToWrite;
|
|
102
|
+
this._bitOffset += numBitsToWrite;
|
|
103
|
+
if (this._bitOffset === 8) {
|
|
104
|
+
this._bitOffset = 0;
|
|
105
|
+
this.flush();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
writeUnsigned(num, count) {
|
|
112
|
+
if (num < 0) throw new Error('Expected a non-negative number');
|
|
113
|
+
this.writeBits(num, count);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
writeSigned(num, count) {
|
|
117
|
+
if (count <= 0) return;
|
|
118
|
+
if (count > 32) throw new Error('writeSigned supports up to 32 bits');
|
|
119
|
+
const mask = count === 32 ? 0xffffffff >>> 0 : (((1 << count) >>> 0) - 1) >>> 0;
|
|
120
|
+
this.writeBits((num & mask) >>> 0, count);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
writeUnsignedExpGolomb(num) {
|
|
124
|
+
if (num < 0) throw new Error('Expected a non-negative number');
|
|
125
|
+
num++;
|
|
126
|
+
const bitCount = 32 - Math.clz32(num >>> 0);
|
|
127
|
+
this.writeBits(0, bitCount - 1);
|
|
128
|
+
this.writeBits(num, bitCount);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
writeSignedExpGolomb(num) {
|
|
132
|
+
if (num < 0) this.writeUnsignedExpGolomb(-2 * num);
|
|
133
|
+
else this.writeUnsignedExpGolomb(2 * num - 1);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = { AnnexBBitstreamReader, AnnexBBitstreamWriter };
|