Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions packages/voice/__tests__/VoiceReceiver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,47 @@ describe('VoiceReceiver', () => {
expect(stream.read()).toEqual(RTP_PACKET.opusFrame);
});

test.each([
['Desktop', RTP_PACKET_DESKTOP, 10_217, 4_157_324_497],
['Chrome', RTP_PACKET_CHROME, 18_143, 660_155_095],
['Android', RTP_PACKET_ANDROID, 14_800, 3_763_991_879],
])('onUdpMessage: RTP header metadata from %s', async (_testName, RTP_PACKET, expectedSeq, expectedTs) => {
receiver['decrypt'] = vitest.fn().mockImplementationOnce(() => RTP_PACKET.decrypted);

const spy = vitest.spyOn(receiver.ssrcMap, 'get');
spy.mockImplementation(() => ({
audioSSRC: RTP_PACKET.ssrc,
userId: '123',
}));

const stream = receiver.subscribe('123');

receiver['onUdpMessage'](RTP_PACKET.packet);
await nextTick();
const packet = stream.read();
expect(packet.sequence).toEqual(expectedSeq);
expect(packet.timestamp).toEqual(expectedTs);
expect(packet.ssrc).toEqual(RTP_PACKET.ssrc);
});

test('onUdpMessage: AudioPacket is backwards compatible', async () => {
receiver['decrypt'] = vitest.fn().mockImplementationOnce(() => RTP_PACKET_DESKTOP.decrypted);

const spy = vitest.spyOn(receiver.ssrcMap, 'get');
spy.mockImplementation(() => ({
audioSSRC: RTP_PACKET_DESKTOP.ssrc,
userId: '123',
}));

const stream = receiver.subscribe('123');

receiver['onUdpMessage'](RTP_PACKET_DESKTOP.packet);
await nextTick();
const packet = stream.read();
expect(Buffer.isBuffer(packet)).toBe(true);
expect(packet).toEqual(RTP_PACKET_DESKTOP.opusFrame);
});

test('onUdpMessage: <8 bytes packet', () => {
expect(() => receiver['onUdpMessage'](Buffer.alloc(4))).not.toThrow();
});
Expand Down
22 changes: 22 additions & 0 deletions packages/voice/src/receive/AudioReceiveStream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,28 @@ export interface AudioReceiveStreamOptions extends ReadableOptions {
end: EndBehavior;
}

/**
* A Buffer containing a decoded Opus packet with RTP header metadata.
*/
export interface AudioPacket extends Buffer {
/**
* The RTP sequence number of this packet (16-bit, wraps at 65535).
*/
readonly sequence: number;

/**
* The RTP timestamp of this packet (32-bit, wraps at 2^32 - 1).
*/
readonly timestamp: number;

/**
* The synchronization source identifier for this packet (32-bit).
* A change in SSRC indicates a new RTP stream, and any stateful
* codec (e.g. Opus) decoder should be reset.
*/
readonly ssrc: number;
}

export function createDefaultAudioReceiveStreamOptions(): AudioReceiveStreamOptions {
return {
end: {
Expand Down
14 changes: 13 additions & 1 deletion packages/voice/src/receive/VoiceReceiver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { methods } from '../util/Secretbox';
import {
AudioReceiveStream,
createDefaultAudioReceiveStreamOptions,
type AudioPacket,
type AudioReceiveStreamOptions,
} from './AudioReceiveStream';
import { SSRCMap } from './SSRCMap';
Expand All @@ -19,6 +20,15 @@ const HEADER_EXTENSION_BYTE = Buffer.from([0xbe, 0xde]);
const UNPADDED_NONCE_LENGTH = 4;
const AUTH_TAG_LENGTH = 16;

function createAudioPacket(buffer: Buffer, sequence: number, timestamp: number, ssrc: number): AudioPacket {
Object.defineProperties(buffer, {
sequence: { value: sequence, writable: false, enumerable: false, configurable: false },
timestamp: { value: timestamp, writable: false, enumerable: false, configurable: false },
ssrc: { value: ssrc, writable: false, enumerable: false, configurable: false },
});
return buffer as AudioPacket;
}

/**
* Attaches to a VoiceConnection, allowing you to receive audio packets from other
* users that are speaking.
Expand Down Expand Up @@ -165,6 +175,8 @@ export class VoiceReceiver {
*/
public onUdpMessage(msg: Buffer) {
if (msg.length <= 8) return;
const sequence = msg.readUInt16BE(2);
const timestamp = msg.readUInt32BE(4);
const ssrc = msg.readUInt32BE(8);

const userData = this.ssrcMap.get(ssrc);
Expand All @@ -184,7 +196,7 @@ export class VoiceReceiver {
this.connectionData.secretKey,
userData.userId,
);
if (packet) stream.push(packet);
if (packet) stream.push(createAudioPacket(packet, sequence, timestamp, ssrc));
} catch (error) {
stream.destroy(error as Error);
}
Expand Down
Loading