refactor: vendor content as regular files (remove git submodules) so CI builds don't need GitHub auth
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
type GatewayEventGeneric<Type extends string = string, Data = unknown> = {
|
||||
t: Type;
|
||||
d: Data;
|
||||
};
|
||||
|
||||
export namespace GatewayEvent {
|
||||
export type VoiceStateUpdate = GatewayEventGeneric<
|
||||
"VOICE_STATE_UPDATE",
|
||||
{
|
||||
user_id: string;
|
||||
session_id: string;
|
||||
}
|
||||
>;
|
||||
export type VoiceServerUpdate = GatewayEventGeneric<
|
||||
"VOICE_SERVER_UPDATE",
|
||||
{
|
||||
guild_id: string;
|
||||
channel_id?: string;
|
||||
endpoint: string;
|
||||
token: string;
|
||||
}
|
||||
>;
|
||||
export type StreamCreate = GatewayEventGeneric<
|
||||
"STREAM_CREATE",
|
||||
{
|
||||
stream_key: string;
|
||||
rtc_server_id: string;
|
||||
}
|
||||
>;
|
||||
export type StreamServerUpdate = GatewayEventGeneric<
|
||||
"STREAM_SERVER_UPDATE",
|
||||
{
|
||||
stream_key: string;
|
||||
endpoint: string;
|
||||
token: string;
|
||||
}
|
||||
>;
|
||||
}
|
||||
|
||||
export type GatewayEvent =
|
||||
| GatewayEvent.VoiceStateUpdate
|
||||
| GatewayEvent.VoiceServerUpdate
|
||||
| GatewayEvent.StreamCreate
|
||||
| GatewayEvent.StreamServerUpdate;
|
||||
|
||||
export type GatewayEventMap = {
|
||||
[E in GatewayEvent as E["t"]]: [E["d"]];
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
export enum GatewayOpCodes {
|
||||
DISPATCH = 0,
|
||||
HEARTBEAT = 1,
|
||||
IDENTIFY = 2,
|
||||
PRESENCE_UPDATE = 3,
|
||||
VOICE_STATE_UPDATE = 4,
|
||||
VOICE_SERVER_PING = 5,
|
||||
RESUME = 6,
|
||||
RECONNECT = 7,
|
||||
REQUEST_GUILD_MEMBERS = 8,
|
||||
INVALID_SESSION = 9,
|
||||
HELLO = 10,
|
||||
HEARTBEAT_ACK = 11,
|
||||
CALL_CONNECT = 13,
|
||||
GUILD_SUBSCRIPTIONS = 14,
|
||||
LOBBY_CONNECT = 15,
|
||||
LOBBY_DISCONNECT = 16,
|
||||
LOBBY_VOICE_STATES_UPDATE = 17,
|
||||
STREAM_CREATE = 18,
|
||||
STREAM_DELETE = 19,
|
||||
STREAM_WATCH = 20,
|
||||
STREAM_PING = 21,
|
||||
STREAM_SET_PAUSED = 22,
|
||||
REQUEST_GUILD_APPLICATION_COMMANDS = 24,
|
||||
EMBEDDED_ACTIVITY_LAUNCH = 25,
|
||||
EMBEDDED_ACTIVITY_CLOSE = 26,
|
||||
EMBEDDED_ACTIVITY_UPDATE = 27,
|
||||
REQUEST_FORUM_UNREADS = 28,
|
||||
REMOTE_COMMAND = 29,
|
||||
GET_DELETED_ENTITY_IDS_NOT_MATCHING_HASH = 30,
|
||||
REQUEST_SOUNDBOARD_SOUNDS = 31,
|
||||
SPEED_TEST_CREATE = 32,
|
||||
SPEED_TEST_DELETE = 33,
|
||||
REQUEST_LAST_MESSAGES = 34,
|
||||
SEARCH_RECENT_MEMBERS = 35,
|
||||
REQUEST_CHANNEL_STATUSES = 36,
|
||||
GUILD_SUBSCRIPTIONS_BULK = 37,
|
||||
GUILD_CHANNELS_RESYNC = 38,
|
||||
REQUEST_CHANNEL_MEMBER_COUNT = 39,
|
||||
}
|
||||
+259
@@ -0,0 +1,259 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import { VoiceConnection } from "./voice/VoiceConnection.js";
|
||||
import { StreamConnection } from "./voice/StreamConnection.js";
|
||||
import { GatewayOpCodes } from "./GatewayOpCodes.js";
|
||||
import type {
|
||||
Client,
|
||||
DMChannel,
|
||||
GroupDMChannel,
|
||||
VoiceBasedChannel,
|
||||
} from "discord.js-selfbot-v13";
|
||||
import type { GatewayEvent, GatewayEventMap } from "./GatewayEvents.js";
|
||||
import type { WebRtcConnWrapper } from "./voice/WebRtcWrapper.js";
|
||||
import { generateStreamKey, parseStreamKey } from "../utils.js";
|
||||
|
||||
export class Streamer {
|
||||
private _voiceConnection?: VoiceConnection;
|
||||
private _client: Client;
|
||||
private _gatewayEmitter = new EventEmitter<GatewayEventMap>();
|
||||
|
||||
constructor(client: Client) {
|
||||
this._client = client;
|
||||
|
||||
//listen for messages
|
||||
this.client.on("raw", (packet: GatewayEvent) => {
|
||||
// @ts-expect-error I don't know how to make this work with TypeScript, so whatever
|
||||
this._gatewayEmitter.emit(packet.t, packet.d);
|
||||
});
|
||||
}
|
||||
|
||||
public get client(): Client {
|
||||
return this._client;
|
||||
}
|
||||
|
||||
public get opts() {
|
||||
return {};
|
||||
}
|
||||
|
||||
public get voiceConnection(): VoiceConnection | undefined {
|
||||
return this._voiceConnection;
|
||||
}
|
||||
|
||||
public sendOpcode(code: number, data: unknown): void {
|
||||
this.client.ws.broadcast({
|
||||
op: code,
|
||||
d: data,
|
||||
});
|
||||
}
|
||||
|
||||
public joinVoiceChannel(
|
||||
channel: DMChannel | GroupDMChannel | VoiceBasedChannel,
|
||||
): Promise<WebRtcConnWrapper> {
|
||||
let guildId: string | null = null;
|
||||
|
||||
if (
|
||||
channel.type === "GUILD_STAGE_VOICE" ||
|
||||
channel.type === "GUILD_VOICE"
|
||||
) {
|
||||
guildId = channel.guildId;
|
||||
}
|
||||
|
||||
return this.joinVoice(guildId, channel.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins a voice channel and returns a WebRtcConnWrapper object.
|
||||
* @param guild_id the guild id of the voice channel. If null, it will join a DM voice channel.
|
||||
* @param channel_id the channel id of the voice channel
|
||||
* @returns the WebRtcConnWrapper object
|
||||
* @throws Error if the client is not logged in
|
||||
*/
|
||||
public joinVoice(
|
||||
guild_id: string | null,
|
||||
channel_id: string,
|
||||
): Promise<WebRtcConnWrapper> {
|
||||
return new Promise<WebRtcConnWrapper>((resolve, reject) => {
|
||||
if (!this.client.user) {
|
||||
reject("Client not logged in");
|
||||
return;
|
||||
}
|
||||
const user_id = this.client.user.id;
|
||||
const voiceConn = new VoiceConnection(
|
||||
this,
|
||||
guild_id,
|
||||
user_id,
|
||||
channel_id,
|
||||
(conn) => {
|
||||
resolve(conn);
|
||||
},
|
||||
);
|
||||
this._voiceConnection = voiceConn;
|
||||
this._gatewayEmitter.on("VOICE_STATE_UPDATE", (d) => {
|
||||
if (user_id !== d.user_id) return;
|
||||
voiceConn.setSession(d.session_id);
|
||||
});
|
||||
this._gatewayEmitter.on("VOICE_SERVER_UPDATE", (d) => {
|
||||
if (guild_id !== d.guild_id) return;
|
||||
|
||||
// channel_id is not set for guild voice calls
|
||||
if (d.channel_id && channel_id !== d.channel_id) return;
|
||||
|
||||
voiceConn.setTokens(d.endpoint, d.token);
|
||||
});
|
||||
this.signalVideo(false);
|
||||
});
|
||||
}
|
||||
|
||||
public createStream(): Promise<WebRtcConnWrapper> {
|
||||
return new Promise<WebRtcConnWrapper>((resolve, reject) => {
|
||||
if (!this.client.user) {
|
||||
reject("Client not logged in");
|
||||
return;
|
||||
}
|
||||
if (!this.voiceConnection) {
|
||||
reject("cannot start stream without first joining voice channel");
|
||||
return;
|
||||
}
|
||||
|
||||
this.signalStream();
|
||||
const {
|
||||
guildId: clientGuildId,
|
||||
channelId: clientChannelId,
|
||||
session_id,
|
||||
} = this.voiceConnection;
|
||||
const { id: clientUserId } = this.client.user;
|
||||
|
||||
if (!session_id) throw new Error("Session doesn't exist yet");
|
||||
const streamConn = new StreamConnection(
|
||||
this,
|
||||
clientGuildId,
|
||||
clientUserId,
|
||||
clientChannelId,
|
||||
(conn) => {
|
||||
resolve(conn);
|
||||
},
|
||||
);
|
||||
this.voiceConnection.streamConnection = streamConn;
|
||||
this._gatewayEmitter.on("STREAM_CREATE", (d) => {
|
||||
const { channelId, guildId, userId } = parseStreamKey(d.stream_key);
|
||||
|
||||
if (
|
||||
clientGuildId !== guildId ||
|
||||
clientChannelId !== channelId ||
|
||||
clientUserId !== userId
|
||||
)
|
||||
return;
|
||||
|
||||
streamConn.serverId = d.rtc_server_id;
|
||||
streamConn.streamKey = d.stream_key;
|
||||
streamConn.setSession(session_id);
|
||||
});
|
||||
this._gatewayEmitter.on("STREAM_SERVER_UPDATE", (d) => {
|
||||
const { channelId, guildId, userId } = parseStreamKey(d.stream_key);
|
||||
|
||||
if (
|
||||
clientGuildId !== guildId ||
|
||||
clientChannelId !== channelId ||
|
||||
clientUserId !== userId
|
||||
)
|
||||
return;
|
||||
|
||||
streamConn.setTokens(d.endpoint, d.token);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async setStreamPreview(image: Buffer): Promise<void> {
|
||||
if (!this.client.token) throw new Error("Please login :)");
|
||||
if (!this.voiceConnection?.streamConnection?.guildId) return;
|
||||
const data = `data:image/jpeg;base64,${image.toString("base64")}`;
|
||||
const { guildId } = this.voiceConnection.streamConnection;
|
||||
const server = await this.client.guilds.fetch(guildId);
|
||||
await server.members.me?.voice.postPreview(data);
|
||||
}
|
||||
|
||||
public stopStream(): void {
|
||||
const stream = this.voiceConnection?.streamConnection;
|
||||
|
||||
if (!stream) return;
|
||||
|
||||
stream.stop();
|
||||
|
||||
this.signalStopStream();
|
||||
|
||||
this.voiceConnection.streamConnection = undefined;
|
||||
this._gatewayEmitter.removeAllListeners("STREAM_CREATE");
|
||||
this._gatewayEmitter.removeAllListeners("STREAM_SERVER_UPDATE");
|
||||
}
|
||||
|
||||
public leaveVoice(): void {
|
||||
this.voiceConnection?.stop();
|
||||
|
||||
this.signalLeaveVoice();
|
||||
|
||||
this._voiceConnection = undefined;
|
||||
this._gatewayEmitter.removeAllListeners("VOICE_STATE_UPDATE");
|
||||
this._gatewayEmitter.removeAllListeners("VOICE_SERVER_UPDATE");
|
||||
}
|
||||
|
||||
public signalVideo(video_enabled: boolean): void {
|
||||
if (!this.voiceConnection) return;
|
||||
const { guildId: guild_id, channelId: channel_id } = this.voiceConnection;
|
||||
this.sendOpcode(GatewayOpCodes.VOICE_STATE_UPDATE, {
|
||||
guild_id: guild_id,
|
||||
channel_id,
|
||||
self_mute: false,
|
||||
self_deaf: true,
|
||||
self_video: video_enabled,
|
||||
});
|
||||
}
|
||||
|
||||
public signalStream(): void {
|
||||
if (!this.voiceConnection) return;
|
||||
const {
|
||||
type,
|
||||
guildId: guild_id,
|
||||
channelId: channel_id,
|
||||
botId: user_id,
|
||||
} = this.voiceConnection;
|
||||
|
||||
const streamKey = generateStreamKey(type, guild_id, channel_id, user_id);
|
||||
|
||||
this.sendOpcode(GatewayOpCodes.STREAM_CREATE, {
|
||||
type,
|
||||
guild_id,
|
||||
channel_id,
|
||||
preferred_region: null,
|
||||
});
|
||||
|
||||
this.sendOpcode(GatewayOpCodes.STREAM_SET_PAUSED, {
|
||||
stream_key: streamKey,
|
||||
paused: false,
|
||||
});
|
||||
}
|
||||
|
||||
public signalStopStream(): void {
|
||||
if (!this.voiceConnection) return;
|
||||
const {
|
||||
type,
|
||||
guildId: guild_id,
|
||||
channelId: channel_id,
|
||||
botId: user_id,
|
||||
} = this.voiceConnection;
|
||||
|
||||
const streamKey = generateStreamKey(type, guild_id, channel_id, user_id);
|
||||
this.sendOpcode(GatewayOpCodes.STREAM_DELETE, {
|
||||
stream_key: streamKey,
|
||||
});
|
||||
}
|
||||
|
||||
public signalLeaveVoice(): void {
|
||||
this.sendOpcode(GatewayOpCodes.VOICE_STATE_UPDATE, {
|
||||
guild_id: null,
|
||||
channel_id: null,
|
||||
self_mute: true,
|
||||
self_deaf: false,
|
||||
self_video: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./voice/index.js";
|
||||
export * from "./GatewayOpCodes.js";
|
||||
export * from "./Streamer.js";
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
export class AnnexBBitstreamReader {
|
||||
private _buffer: Buffer;
|
||||
private _byteOffset = 0;
|
||||
private _bitOffset = 0;
|
||||
|
||||
constructor(buffer: Buffer) {
|
||||
this._buffer = buffer;
|
||||
}
|
||||
public readBits(count: number) {
|
||||
if (count === 0) return 0;
|
||||
let result = 0;
|
||||
while (count > 0) {
|
||||
if (this._byteOffset >= this._buffer.length)
|
||||
throw new Error("Bad byte offset");
|
||||
if (
|
||||
this._bitOffset === 0 &&
|
||||
this._byteOffset >= 2 &&
|
||||
this._buffer[this._byteOffset - 2] === 0 &&
|
||||
this._buffer[this._byteOffset - 1] === 0 &&
|
||||
this._buffer[this._byteOffset] === 3
|
||||
) {
|
||||
// Skip over emulation prevention
|
||||
this._byteOffset++;
|
||||
}
|
||||
if (this._bitOffset === 0 && count >= 8) {
|
||||
// We're byte aligned, read whole bytes and push in
|
||||
result = (result << 8) | this._buffer[this._byteOffset++];
|
||||
count -= 8;
|
||||
} else {
|
||||
// Read just enough to get us to the next byte
|
||||
const numBitsToRead = Math.min(count, 8 - this._bitOffset);
|
||||
const mask = (1 << numBitsToRead) - 1;
|
||||
const newBits =
|
||||
(this._buffer[this._byteOffset] >>
|
||||
(8 - this._bitOffset - numBitsToRead)) &
|
||||
mask;
|
||||
result = (result << numBitsToRead) | newBits;
|
||||
count -= numBitsToRead;
|
||||
this._bitOffset += numBitsToRead;
|
||||
if (this._bitOffset === 8) {
|
||||
this._bitOffset = 0;
|
||||
this._byteOffset++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
public readUnsigned(bits: number) {
|
||||
return this.readBits(bits);
|
||||
}
|
||||
public readSigned(bits: number) {
|
||||
const unsigned = this.readUnsigned(bits);
|
||||
if (unsigned & (1 << (bits - 1))) return unsigned - (1 << bits);
|
||||
return unsigned;
|
||||
}
|
||||
public readUnsignedExpGolomb() {
|
||||
let leading0 = 0;
|
||||
while (this.readBits(1) === 0) leading0++;
|
||||
|
||||
return (1 << leading0) + this.readBits(leading0) - 1;
|
||||
}
|
||||
public readSignedExpGolomb() {
|
||||
// Mapping: x <= 0 => -2x, x > 0 => 2x - 1
|
||||
const unsigned = this.readUnsignedExpGolomb();
|
||||
if (unsigned % 2 === 0) return unsigned / -2;
|
||||
return (unsigned + 1) / 2;
|
||||
}
|
||||
}
|
||||
|
||||
export class AnnexBBitstreamWriter {
|
||||
private _arr: number[] = [];
|
||||
private _pendingByte = 0;
|
||||
private _bitOffset = 0;
|
||||
|
||||
public toBuffer() {
|
||||
return Buffer.from(this._arr);
|
||||
}
|
||||
public flush() {
|
||||
// Write the pending byte into the array and reset, taking care of emulation prevention
|
||||
if (
|
||||
this._pendingByte <= 3 &&
|
||||
this._arr.at(-1) === 0 &&
|
||||
this._arr.at(-2) === 0
|
||||
)
|
||||
this._arr.push(3);
|
||||
this._arr.push(this._pendingByte);
|
||||
this._pendingByte = 0;
|
||||
this._bitOffset = 0;
|
||||
}
|
||||
public writeBits(bits: number, count: number) {
|
||||
while (count > 0) {
|
||||
if (this._bitOffset === 0) {
|
||||
if (count >= 8) {
|
||||
// We're byte aligned and has more than 1 byte left to write, write a whole byte
|
||||
this._pendingByte = (bits >> (count - 8)) & 0xff;
|
||||
count -= 8;
|
||||
this.flush();
|
||||
} else {
|
||||
// We have less than 1 byte, write the rest in
|
||||
const mask = (1 << count) - 1;
|
||||
this._pendingByte |= (bits & mask) << (8 - count);
|
||||
this._bitOffset = count;
|
||||
count = 0;
|
||||
}
|
||||
} else {
|
||||
// Write the minimum number of bits to get us byte aligned again
|
||||
const numBitsToWrite = Math.min(8 - this._bitOffset, count);
|
||||
const bitsToWrite =
|
||||
(bits >> (count - numBitsToWrite)) & ((1 << numBitsToWrite) - 1);
|
||||
this._pendingByte |=
|
||||
bitsToWrite << (8 - this._bitOffset - numBitsToWrite);
|
||||
count -= numBitsToWrite;
|
||||
this._bitOffset += numBitsToWrite;
|
||||
if (this._bitOffset === 8) {
|
||||
this._bitOffset = 0;
|
||||
this.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
public writeUnsigned(num: number, count: number) {
|
||||
if (num < 0) throw new Error("Expected a non-negative number");
|
||||
this.writeBits(num, count);
|
||||
}
|
||||
public writeSigned(num: number, count: number) {
|
||||
if (count <= 0) return;
|
||||
if (count > 32) throw new Error("writeSigned supports up to 32 bits");
|
||||
|
||||
// Build mask for `count` bits. Handle 32-bit as a special case.
|
||||
const mask =
|
||||
count === 32 ? 0xffffffff >>> 0 : (((1 << count) >>> 0) - 1) >>> 0;
|
||||
|
||||
// Convert to two's-complement unsigned representation and write
|
||||
const unsigned = (num & mask) >>> 0;
|
||||
this.writeBits(unsigned, count);
|
||||
}
|
||||
public writeUnsignedExpGolomb(num: number) {
|
||||
if (num < 0) throw new Error("Expected a non-negative number");
|
||||
num++;
|
||||
const bitCount = 32 - Math.clz32(num >>> 0);
|
||||
this.writeBits(0, bitCount - 1);
|
||||
this.writeBits(num, bitCount);
|
||||
}
|
||||
public writeSignedExpGolomb(num: number) {
|
||||
if (num < 0) this.writeUnsignedExpGolomb(-2 * num);
|
||||
else this.writeUnsignedExpGolomb(2 * num - 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
export enum H264NalUnitTypes {
|
||||
Unspecified = 0,
|
||||
CodedSliceNonIDR = 1,
|
||||
CodedSlicePartitionA = 2,
|
||||
CodedSlicePartitionB = 3,
|
||||
CodedSlicePartitionC = 4,
|
||||
CodedSliceIdr = 5,
|
||||
SEI = 6,
|
||||
SPS = 7,
|
||||
PPS = 8,
|
||||
AccessUnitDelimiter = 9,
|
||||
EndOfSequence = 10,
|
||||
EndOfStream = 11,
|
||||
FillerData = 12,
|
||||
SEIExtenstion = 13,
|
||||
PrefixNalUnit = 14,
|
||||
SubsetSPS = 15,
|
||||
}
|
||||
|
||||
export enum H265NalUnitTypes {
|
||||
TRAIL_N = 0,
|
||||
TRAIL_R = 1,
|
||||
TSA_N = 2,
|
||||
TSA_R = 3,
|
||||
STSA_N = 4,
|
||||
STSA_R = 5,
|
||||
RADL_N = 6,
|
||||
RADL_R = 7,
|
||||
RASL_N = 8,
|
||||
RASL_R = 9,
|
||||
RSV_VCL_N10 = 10,
|
||||
RSV_VCL_R11 = 11,
|
||||
RSV_VCL_N12 = 12,
|
||||
RSV_VCL_R13 = 13,
|
||||
RSV_VCL_N14 = 14,
|
||||
RSV_VCL_R15 = 15,
|
||||
BLA_W_LP = 16,
|
||||
BLA_W_RADL = 17,
|
||||
BLA_N_LP = 18,
|
||||
IDR_W_RADL = 19,
|
||||
IDR_N_LP = 20,
|
||||
CRA_NUT = 21,
|
||||
RSV_IRAP_VCL22 = 22,
|
||||
RSV_IRAP_VCL23 = 23,
|
||||
RSV_VCL24 = 24,
|
||||
RSV_VCL25 = 25,
|
||||
RSV_VCL26 = 26,
|
||||
RSV_VCL27 = 27,
|
||||
RSV_VCL28 = 28,
|
||||
RSV_VCL29 = 29,
|
||||
RSV_VCL30 = 30,
|
||||
RSV_VCL31 = 31,
|
||||
VPS_NUT = 32,
|
||||
SPS_NUT = 33,
|
||||
PPS_NUT = 34,
|
||||
AUD_NUT = 35,
|
||||
EOS_NUT = 36,
|
||||
EOB_NUT = 37,
|
||||
FD_NUT = 38,
|
||||
PREFIX_SEI_NUT = 39,
|
||||
SUFFIX_SEI_NUT = 40,
|
||||
RSV_NVCL41 = 41,
|
||||
RSV_NVCL42 = 42,
|
||||
RSV_NVCL43 = 43,
|
||||
RSV_NVCL44 = 44,
|
||||
RSV_NVCL45 = 45,
|
||||
RSV_NVCL46 = 46,
|
||||
RSV_NVCL47 = 47,
|
||||
UNSPEC48 = 48,
|
||||
UNSPEC49 = 49,
|
||||
UNSPEC50 = 50,
|
||||
UNSPEC51 = 51,
|
||||
UNSPEC52 = 52,
|
||||
UNSPEC53 = 53,
|
||||
UNSPEC54 = 54,
|
||||
UNSPEC55 = 55,
|
||||
UNSPEC56 = 56,
|
||||
UNSPEC57 = 57,
|
||||
UNSPEC58 = 58,
|
||||
UNSPEC59 = 59,
|
||||
UNSPEC60 = 60,
|
||||
UNSPEC61 = 61,
|
||||
UNSPEC62 = 62,
|
||||
UNSPEC63 = 63,
|
||||
}
|
||||
|
||||
export interface AnnexBHelpers {
|
||||
getUnitType(frame: Buffer): number;
|
||||
splitHeader(frame: Buffer): [Buffer, Buffer];
|
||||
isAUD(unitType: number): boolean;
|
||||
}
|
||||
|
||||
export const H264Helpers: AnnexBHelpers = {
|
||||
getUnitType(frame) {
|
||||
return frame[0] & 0x1f;
|
||||
},
|
||||
splitHeader(frame) {
|
||||
return [frame.subarray(0, 1), frame.subarray(1)];
|
||||
},
|
||||
isAUD(unitType) {
|
||||
return unitType === H264NalUnitTypes.AccessUnitDelimiter;
|
||||
},
|
||||
};
|
||||
|
||||
export const H265Helpers: AnnexBHelpers = {
|
||||
getUnitType(frame) {
|
||||
return (frame[0] >> 1) & 0x3f;
|
||||
},
|
||||
splitHeader(frame) {
|
||||
return [frame.subarray(0, 2), frame.subarray(2)];
|
||||
},
|
||||
isAUD(unitType) {
|
||||
return unitType === H265NalUnitTypes.AUD_NUT;
|
||||
},
|
||||
};
|
||||
|
||||
export const startCode3 = Buffer.from([0, 0, 1]);
|
||||
|
||||
export function splitNalu(buf: Buffer) {
|
||||
let temp: Buffer | null = buf;
|
||||
const nalus: Buffer[] = [];
|
||||
while (temp?.byteLength) {
|
||||
let pos: number = temp.indexOf(startCode3);
|
||||
let length = 3;
|
||||
if (pos > 0 && temp[pos - 1] === 0) {
|
||||
pos--;
|
||||
length++;
|
||||
}
|
||||
const nalu = pos === -1 ? temp : temp.subarray(0, pos);
|
||||
temp = pos === -1 ? null : temp.subarray(pos + length);
|
||||
if (nalu.byteLength) nalus.push(nalu);
|
||||
}
|
||||
return nalus;
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
import {
|
||||
AnnexBBitstreamReader,
|
||||
AnnexBBitstreamWriter,
|
||||
} from "./AnnexBBitstreamReaderWriter.js";
|
||||
|
||||
export function rewriteSPSVUI(buffer: Buffer) {
|
||||
const reader = new AnnexBBitstreamReader(buffer.subarray(1));
|
||||
const writer = new AnnexBBitstreamWriter();
|
||||
|
||||
const readBit = (n = 1) => reader.readBits(n);
|
||||
const writeBit = (v: number, n = 1) => writer.writeBits(v, n);
|
||||
const readU = (n: number) => reader.readUnsigned(n);
|
||||
const writeU = (v: number, n: number) => writer.writeUnsigned(v, n);
|
||||
const readUE = () => reader.readUnsignedExpGolomb();
|
||||
const writeUE = (v: number) => writer.writeUnsignedExpGolomb(v);
|
||||
const readSE = () => reader.readSignedExpGolomb();
|
||||
const writeSE = (v: number) => writer.writeSignedExpGolomb(v);
|
||||
|
||||
// Rewrite the NAL header
|
||||
writeU(buffer[0], 8);
|
||||
|
||||
const profile_idc = readU(8);
|
||||
writeU(profile_idc, 8);
|
||||
|
||||
const constraint_flags = readU(8);
|
||||
writeU(constraint_flags, 8);
|
||||
|
||||
const level_idc = readU(8);
|
||||
writeU(level_idc, 8);
|
||||
|
||||
const seq_parameter_set_id = readUE();
|
||||
writeUE(seq_parameter_set_id);
|
||||
|
||||
// If profile in high profiles, additional fields
|
||||
const highProfiles = new Set([
|
||||
100, 110, 122, 244, 44, 83, 86, 118, 128, 138, 144,
|
||||
]);
|
||||
if (highProfiles.has(profile_idc)) {
|
||||
const chroma_format_idc = readUE();
|
||||
writeUE(chroma_format_idc);
|
||||
|
||||
if (chroma_format_idc === 3) {
|
||||
const separate_colour_plane_flag = readBit(1);
|
||||
writeBit(separate_colour_plane_flag, 1);
|
||||
}
|
||||
|
||||
const bit_depth_luma_minus8 = readUE();
|
||||
writeUE(bit_depth_luma_minus8);
|
||||
const bit_depth_chroma_minus8 = readUE();
|
||||
writeUE(bit_depth_chroma_minus8);
|
||||
|
||||
const qpprime_y_zero_transform_bypass_flag = readBit(1);
|
||||
writeBit(qpprime_y_zero_transform_bypass_flag, 1);
|
||||
|
||||
const seq_scaling_matrix_present_flag = readBit(1);
|
||||
writeBit(seq_scaling_matrix_present_flag, 1);
|
||||
if (seq_scaling_matrix_present_flag) {
|
||||
const scalingCount = chroma_format_idc !== 3 ? 8 : 12;
|
||||
for (let i = 0; i < scalingCount; i++) {
|
||||
const seq_scaling_list_present_flag = readBit(1);
|
||||
writeBit(seq_scaling_list_present_flag, 1);
|
||||
if (seq_scaling_list_present_flag) {
|
||||
const size = i < 6 ? 16 : 64;
|
||||
// scaling_list(size)
|
||||
let lastScale = 8;
|
||||
let nextScale = 8;
|
||||
for (let j = 0; j < size; j++) {
|
||||
const delta = readSE();
|
||||
writeSE(delta);
|
||||
nextScale = (lastScale + delta + 256) % 256;
|
||||
if (nextScale !== 0) lastScale = nextScale;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const log2_max_frame_num_minus4 = readUE();
|
||||
writeUE(log2_max_frame_num_minus4);
|
||||
|
||||
const pic_order_cnt_type = readUE();
|
||||
writeUE(pic_order_cnt_type);
|
||||
if (pic_order_cnt_type === 0) {
|
||||
const log2_max_pic_order_cnt_lsb_minus4 = readUE();
|
||||
writeUE(log2_max_pic_order_cnt_lsb_minus4);
|
||||
} else if (pic_order_cnt_type === 1) {
|
||||
const delta_pic_order_always_zero_flag = readBit(1);
|
||||
writeBit(delta_pic_order_always_zero_flag, 1);
|
||||
const offset_for_non_ref_pic = readSE();
|
||||
writeSE(offset_for_non_ref_pic);
|
||||
const offset_for_top_to_bottom_field = readSE();
|
||||
writeSE(offset_for_top_to_bottom_field);
|
||||
const num_ref_frames_in_pic_order_cnt_cycle = readUE();
|
||||
writeUE(num_ref_frames_in_pic_order_cnt_cycle);
|
||||
for (let i = 0; i < num_ref_frames_in_pic_order_cnt_cycle; i++) {
|
||||
const offset_for_ref_frame = readSE();
|
||||
writeSE(offset_for_ref_frame);
|
||||
}
|
||||
}
|
||||
|
||||
const max_num_ref_frames = readUE();
|
||||
writeUE(max_num_ref_frames);
|
||||
|
||||
const gaps_in_frame_num_value_allowed_flag = readBit(1);
|
||||
writeBit(gaps_in_frame_num_value_allowed_flag, 1);
|
||||
|
||||
const pic_width_in_mbs_minus1 = readUE();
|
||||
writeUE(pic_width_in_mbs_minus1);
|
||||
|
||||
const pic_height_in_map_units_minus1 = readUE();
|
||||
writeUE(pic_height_in_map_units_minus1);
|
||||
|
||||
const frame_mbs_only_flag = readBit(1);
|
||||
writeBit(frame_mbs_only_flag, 1);
|
||||
if (frame_mbs_only_flag === 0) {
|
||||
const mb_adaptive_frame_field_flag = readBit(1);
|
||||
writeBit(mb_adaptive_frame_field_flag, 1);
|
||||
}
|
||||
|
||||
const direct_8x8_inference_flag = readBit(1);
|
||||
writeBit(direct_8x8_inference_flag, 1);
|
||||
|
||||
const frame_cropping_flag = readBit(1);
|
||||
writeBit(frame_cropping_flag, 1);
|
||||
if (frame_cropping_flag) {
|
||||
const frame_crop_left_offset = readUE();
|
||||
writeUE(frame_crop_left_offset);
|
||||
const frame_crop_right_offset = readUE();
|
||||
writeUE(frame_crop_right_offset);
|
||||
const frame_crop_top_offset = readUE();
|
||||
writeUE(frame_crop_top_offset);
|
||||
const frame_crop_bottom_offset = readUE();
|
||||
writeUE(frame_crop_bottom_offset);
|
||||
}
|
||||
|
||||
// https://webrtc.googlesource.com/src/+/5f2c9278f35e47ff72eb191669d473b7400c9f3e/common_video/h264/sps_vui_rewriter.cc#283
|
||||
function addBitstreamRestriction() {
|
||||
// motion_vectors_over_pic_boundaries_flag: u(1)
|
||||
// Default is 1 when not present.
|
||||
writeBit(1, 1);
|
||||
// max_bytes_per_pic_denom: ue(v)
|
||||
// Default is 2 when not present.
|
||||
writeUE(2);
|
||||
// max_bits_per_mb_denom: ue(v)
|
||||
// Default is 1 when not present.
|
||||
writeUE(1);
|
||||
// log2_max_mv_length_horizontal: ue(v)
|
||||
// log2_max_mv_length_vertical: ue(v)
|
||||
// Both default to 16 when not present.
|
||||
writeUE(16);
|
||||
writeUE(16);
|
||||
// ********* IMPORTANT! **********
|
||||
// max_num_reorder_frames: ue(v)
|
||||
writeUE(0);
|
||||
// max_dec_frame_buffering: ue(v)
|
||||
writeUE(max_num_ref_frames);
|
||||
}
|
||||
|
||||
const vui_parameters_present_flag = readBit(1);
|
||||
writeBit(1, 1);
|
||||
// If no VUI exists, write one
|
||||
if (!vui_parameters_present_flag) {
|
||||
// aspect_ratio_info_present_flag, overscan_info_present_flag. Both u(1).
|
||||
writeBit(0, 2);
|
||||
|
||||
// video_signal_type_present_flag, u(1).
|
||||
// Just write 0 here because I'm not gonna bother myself with color space and whatnot
|
||||
writeBit(0, 1);
|
||||
|
||||
// chroma_loc_info_present_flag, timing_info_present_flag,
|
||||
// nal_hrd_parameters_present_flag, vcl_hrd_parameters_present_flag,
|
||||
// pic_struct_present_flag, All u(1)
|
||||
writeBit(0, 5);
|
||||
|
||||
// bitstream_restriction_flag: u(1)
|
||||
writeBit(1, 1);
|
||||
|
||||
addBitstreamRestriction();
|
||||
} else {
|
||||
// VUI parsing and copying
|
||||
const aspect_ratio_info_present_flag = readBit(1);
|
||||
writeBit(aspect_ratio_info_present_flag, 1);
|
||||
if (aspect_ratio_info_present_flag) {
|
||||
const aspect_ratio_idc = readU(8);
|
||||
writeU(aspect_ratio_idc, 8);
|
||||
if (aspect_ratio_idc === 255) {
|
||||
// Extended_SAR
|
||||
const sar_width = readU(16);
|
||||
writeU(sar_width, 16);
|
||||
const sar_height = readU(16);
|
||||
writeU(sar_height, 16);
|
||||
}
|
||||
}
|
||||
|
||||
const overscan_info_present_flag = readBit(1);
|
||||
writeBit(overscan_info_present_flag, 1);
|
||||
if (overscan_info_present_flag) {
|
||||
const overscan_appropriate_flag = readBit(1);
|
||||
writeBit(overscan_appropriate_flag, 1);
|
||||
}
|
||||
|
||||
// Read the video signal type, but don't copy it
|
||||
const video_signal_type_present_flag = readBit(1);
|
||||
writeBit(0, 1);
|
||||
if (video_signal_type_present_flag) {
|
||||
const _video_format = readBit(3);
|
||||
// writeBit(video_format, 3);
|
||||
const _video_full_range_flag = readBit(1);
|
||||
// writeBit(video_full_range_flag, 1);
|
||||
const colour_description_present_flag = readBit(1);
|
||||
// writeBit(colour_description_present_flag, 1);
|
||||
if (colour_description_present_flag) {
|
||||
const _colour_primaries = readU(8);
|
||||
// writeU(colour_primaries, 8);
|
||||
const _transfer_characteristics = readU(8);
|
||||
// writeU(transfer_characteristics, 8);
|
||||
const _matrix_coeffs = readU(8);
|
||||
// writeU(matrix_coeffs, 8);
|
||||
}
|
||||
}
|
||||
|
||||
const chroma_loc_info_present_flag = readBit(1);
|
||||
writeBit(chroma_loc_info_present_flag, 1);
|
||||
if (chroma_loc_info_present_flag) {
|
||||
const chroma_sample_loc_type_top_field = readUE();
|
||||
writeUE(chroma_sample_loc_type_top_field);
|
||||
const chroma_sample_loc_type_bottom_field = readUE();
|
||||
writeUE(chroma_sample_loc_type_bottom_field);
|
||||
}
|
||||
|
||||
const timing_info_present_flag = readBit(1);
|
||||
writeBit(timing_info_present_flag, 1);
|
||||
if (timing_info_present_flag) {
|
||||
const num_units_in_tick = readU(32);
|
||||
writeU(num_units_in_tick, 32);
|
||||
const time_scale = readU(32);
|
||||
writeU(time_scale, 32);
|
||||
const fixed_frame_rate_flag = readBit(1);
|
||||
writeBit(fixed_frame_rate_flag, 1);
|
||||
}
|
||||
|
||||
const nal_hrd_parameters_present_flag = readBit(1);
|
||||
writeBit(nal_hrd_parameters_present_flag, 1);
|
||||
if (nal_hrd_parameters_present_flag) {
|
||||
// hrd_parameters()
|
||||
const cpb_cnt_minus1 = readUE();
|
||||
writeUE(cpb_cnt_minus1);
|
||||
const bit_rate_scale = readBit(4);
|
||||
writeBit(bit_rate_scale, 4);
|
||||
const cpb_size_scale = readBit(4);
|
||||
writeBit(cpb_size_scale, 4);
|
||||
for (let i = 0; i <= cpb_cnt_minus1; i++) {
|
||||
const bit_rate_value_minus1 = readUE();
|
||||
writeUE(bit_rate_value_minus1);
|
||||
const cpb_size_value_minus1 = readUE();
|
||||
writeUE(cpb_size_value_minus1);
|
||||
const cbr_flag = readBit(1);
|
||||
writeBit(cbr_flag, 1);
|
||||
}
|
||||
const initial_cpb_removal_delay_length_minus1 = readBit(5);
|
||||
writeBit(initial_cpb_removal_delay_length_minus1, 5);
|
||||
const cpb_removal_delay_length_minus1 = readBit(5);
|
||||
writeBit(cpb_removal_delay_length_minus1, 5);
|
||||
const dpb_output_delay_length_minus1 = readBit(5);
|
||||
writeBit(dpb_output_delay_length_minus1, 5);
|
||||
const time_offset_length = readBit(5);
|
||||
writeBit(time_offset_length, 5);
|
||||
}
|
||||
|
||||
const vcl_hrd_parameters_present_flag = readBit(1);
|
||||
writeBit(vcl_hrd_parameters_present_flag, 1);
|
||||
if (vcl_hrd_parameters_present_flag) {
|
||||
// hrd_parameters()
|
||||
const cpb_cnt_minus1 = readUE();
|
||||
writeUE(cpb_cnt_minus1);
|
||||
const bit_rate_scale = readBit(4);
|
||||
writeBit(bit_rate_scale, 4);
|
||||
const cpb_size_scale = readBit(4);
|
||||
writeBit(cpb_size_scale, 4);
|
||||
for (let i = 0; i <= cpb_cnt_minus1; i++) {
|
||||
const bit_rate_value_minus1 = readUE();
|
||||
writeUE(bit_rate_value_minus1);
|
||||
const cpb_size_value_minus1 = readUE();
|
||||
writeUE(cpb_size_value_minus1);
|
||||
const cbr_flag = readBit(1);
|
||||
writeBit(cbr_flag, 1);
|
||||
}
|
||||
const initial_cpb_removal_delay_length_minus1 = readBit(5);
|
||||
writeBit(initial_cpb_removal_delay_length_minus1, 5);
|
||||
const cpb_removal_delay_length_minus1 = readBit(5);
|
||||
writeBit(cpb_removal_delay_length_minus1, 5);
|
||||
const dpb_output_delay_length_minus1 = readBit(5);
|
||||
writeBit(dpb_output_delay_length_minus1, 5);
|
||||
const time_offset_length = readBit(5);
|
||||
writeBit(time_offset_length, 5);
|
||||
}
|
||||
|
||||
if (nal_hrd_parameters_present_flag || vcl_hrd_parameters_present_flag) {
|
||||
const low_delay_hrd_flag = readBit(1);
|
||||
writeBit(low_delay_hrd_flag, 1);
|
||||
}
|
||||
|
||||
const pic_struct_present_flag = readBit(1);
|
||||
writeBit(pic_struct_present_flag, 1);
|
||||
|
||||
const bitstream_restriction_flag = readBit(1);
|
||||
writeBit(1, 1);
|
||||
if (!bitstream_restriction_flag) {
|
||||
addBitstreamRestriction();
|
||||
} else {
|
||||
const motion_vectors_over_pic_boundaries_flag = readBit(1);
|
||||
writeBit(motion_vectors_over_pic_boundaries_flag, 1);
|
||||
const max_bytes_per_pic_denom = readUE();
|
||||
writeUE(max_bytes_per_pic_denom);
|
||||
const max_bits_per_mb_denom = readUE();
|
||||
writeUE(max_bits_per_mb_denom);
|
||||
const log2_max_mv_length_horizontal = readUE();
|
||||
writeUE(log2_max_mv_length_horizontal);
|
||||
const log2_max_mv_length_vertical = readUE();
|
||||
writeUE(log2_max_mv_length_vertical);
|
||||
const _num_reorder_frames = readUE();
|
||||
writeUE(0);
|
||||
const _max_dec_frame_buffering = readUE();
|
||||
writeUE(max_num_ref_frames);
|
||||
}
|
||||
}
|
||||
|
||||
writeBit(1, 1); // rbsp_stop_one_bit
|
||||
writer.flush();
|
||||
// return the rewritten RBSP as a buffer
|
||||
return writer.toBuffer();
|
||||
}
|
||||
@@ -0,0 +1,640 @@
|
||||
import Davey from "@snazzah/davey";
|
||||
import EventEmitter from "node:events";
|
||||
import { Log } from "debug-level";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { CodecPayloadType } from "./CodecPayloadType.js";
|
||||
import { WebRtcConnWrapper } from "./WebRtcWrapper.js";
|
||||
import { VoiceOpCodes, VoiceOpCodesBinary } from "./VoiceOpCodes.js";
|
||||
import {
|
||||
STREAMS_SIMULCAST,
|
||||
type SupportedEncryptionModes,
|
||||
} from "../../utils.js";
|
||||
import type {
|
||||
Message,
|
||||
GatewayRequest,
|
||||
GatewayResponse,
|
||||
} from "./VoiceMessageTypes.js";
|
||||
import type { Streamer } from "../Streamer.js";
|
||||
|
||||
type VoiceConnectionStatus = {
|
||||
hasSession: boolean;
|
||||
hasToken: boolean;
|
||||
started: boolean;
|
||||
resuming: boolean;
|
||||
};
|
||||
|
||||
type WebRtcParameters = {
|
||||
address: string;
|
||||
port: number;
|
||||
audioSsrc: number;
|
||||
videoSsrc: number;
|
||||
rtxSsrc: number;
|
||||
supportedEncryptionModes: SupportedEncryptionModes[];
|
||||
};
|
||||
|
||||
type ValueOf<T> = T extends (infer U)[]
|
||||
? U
|
||||
: T extends Record<string, infer U>
|
||||
? U
|
||||
: never;
|
||||
|
||||
export type VideoAttributes = {
|
||||
width: number;
|
||||
height: number;
|
||||
fps: number;
|
||||
};
|
||||
|
||||
export abstract class BaseMediaConnection extends EventEmitter {
|
||||
private interval: NodeJS.Timeout | null = null;
|
||||
public guildId: string | null = null;
|
||||
public channelId: string;
|
||||
public botId: string;
|
||||
public ws: WebSocket | null = null;
|
||||
public status: VoiceConnectionStatus;
|
||||
public server: string | null = null; //websocket url
|
||||
public token: string | null = null;
|
||||
public session_id: string | null = null;
|
||||
|
||||
private _webRtcWrapper;
|
||||
private _webRtcParams: WebRtcParameters | null = null;
|
||||
private _closed = false;
|
||||
public ready: (conn: WebRtcConnWrapper) => void;
|
||||
|
||||
private _streamer: Streamer;
|
||||
private _sequenceNumber = -1;
|
||||
|
||||
private _daveSession: Davey.DaveSession | undefined;
|
||||
private _connectedUsers = new Set<string>();
|
||||
private _daveProtocolVersion = 0;
|
||||
private _davePendingTransitions = new Map<number, number>();
|
||||
private _daveDowngraded = false;
|
||||
|
||||
private _logger = new Log("conn");
|
||||
private _loggerDave = new Log("conn:dave");
|
||||
constructor(
|
||||
streamer: Streamer,
|
||||
guildId: string | null,
|
||||
botId: string,
|
||||
channelId: string,
|
||||
callback: (conn: WebRtcConnWrapper) => void,
|
||||
) {
|
||||
super();
|
||||
this._streamer = streamer;
|
||||
this.status = {
|
||||
hasSession: false,
|
||||
hasToken: false,
|
||||
started: false,
|
||||
resuming: false,
|
||||
};
|
||||
|
||||
this.guildId = guildId;
|
||||
this.channelId = channelId;
|
||||
this.botId = botId;
|
||||
this.ready = callback;
|
||||
this._webRtcWrapper = new WebRtcConnWrapper(this);
|
||||
}
|
||||
|
||||
public abstract get serverId(): string | null;
|
||||
|
||||
public get type(): "guild" | "call" {
|
||||
return this.guildId ? "guild" : "call";
|
||||
}
|
||||
|
||||
public get webRtcConn() {
|
||||
return this._webRtcWrapper;
|
||||
}
|
||||
|
||||
public get webRtcParams() {
|
||||
return this._webRtcParams;
|
||||
}
|
||||
|
||||
public get streamer() {
|
||||
return this._streamer;
|
||||
}
|
||||
|
||||
public abstract get daveChannelId(): string;
|
||||
|
||||
stop(): void {
|
||||
this._closed = true;
|
||||
this._webRtcWrapper.close();
|
||||
this.ws?.close();
|
||||
}
|
||||
|
||||
setSession(session_id: string): void {
|
||||
this.session_id = session_id;
|
||||
|
||||
this.status.hasSession = true;
|
||||
this.start();
|
||||
}
|
||||
|
||||
setTokens(server: string, token: string): void {
|
||||
this.token = token;
|
||||
this.server = server;
|
||||
|
||||
this.status.hasToken = true;
|
||||
this.start();
|
||||
}
|
||||
|
||||
start(): void {
|
||||
/*
|
||||
** Connection can only start once both
|
||||
** session description and tokens have been gathered
|
||||
*/
|
||||
if (this.status.hasSession && this.status.hasToken) {
|
||||
if (this.status.started) return;
|
||||
this.status.started = true;
|
||||
|
||||
this.ws = new WebSocket(`wss://${this.server}/?v=8`);
|
||||
this.ws.binaryType = "arraybuffer";
|
||||
this.ws.addEventListener("open", () => {
|
||||
if (this.status.resuming) {
|
||||
this.status.resuming = false;
|
||||
this.resume();
|
||||
} else {
|
||||
this.identify();
|
||||
}
|
||||
});
|
||||
this.ws.addEventListener("error", (err) => {
|
||||
console.error(err);
|
||||
});
|
||||
this.ws.addEventListener("close", (e) => {
|
||||
const wasStarted = this.status.started;
|
||||
|
||||
this.interval && clearInterval(this.interval);
|
||||
this.status.started = false;
|
||||
const canResume = e.code === 4_015 || e.code < 4_000;
|
||||
|
||||
if (canResume && wasStarted) {
|
||||
this.status.resuming = true;
|
||||
this.start();
|
||||
} else {
|
||||
this._closed = true;
|
||||
this._webRtcWrapper?.close();
|
||||
}
|
||||
});
|
||||
this.setupEvents();
|
||||
}
|
||||
}
|
||||
|
||||
handleReady(d: Message.Ready): void {
|
||||
// we hardcoded the STREAMS_SIMULCAST, which will always be array of 1
|
||||
const stream = d.streams[0];
|
||||
this._webRtcParams = {
|
||||
address: d.ip,
|
||||
port: d.port,
|
||||
audioSsrc: d.ssrc,
|
||||
videoSsrc: stream.ssrc,
|
||||
rtxSsrc: stream.rtx_ssrc,
|
||||
supportedEncryptionModes: d.modes,
|
||||
};
|
||||
}
|
||||
|
||||
async handleProtocolAck(d: Message.SelectProtocolAck) {
|
||||
if (!("sdp" in d)) throw new Error("Only WebRTC connections are allowed");
|
||||
this._daveProtocolVersion = d.dave_protocol_version;
|
||||
this.initDave();
|
||||
// Discord's SDP is absolute garbage...Generate one ourselves
|
||||
let ip = "",
|
||||
port = "",
|
||||
iceUsername = "",
|
||||
icePassword = "",
|
||||
fingerprint = "",
|
||||
candidate = "";
|
||||
for (const line of d.sdp.split("\n")) {
|
||||
if (line.startsWith("c=")) ip = line;
|
||||
else if (line.startsWith("a=rtcp")) port = line.split(":")[1];
|
||||
else if (line.startsWith("a=ice-ufrag")) iceUsername = line;
|
||||
else if (line.startsWith("a=ice-pwd")) icePassword = line;
|
||||
else if (line.startsWith("a=fingerprint")) fingerprint = line;
|
||||
else if (line.startsWith("a=candidate")) candidate = line;
|
||||
}
|
||||
const audioPayloadType = CodecPayloadType.opus.payload_type;
|
||||
const audioSection = `
|
||||
m=audio ${port} UDP/TLS/RTP/SAVPF ${audioPayloadType}
|
||||
${ip}
|
||||
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
|
||||
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
|
||||
a=setup:passive
|
||||
a=mid:0
|
||||
a=maxptime:60
|
||||
a=inactive
|
||||
${iceUsername}
|
||||
${icePassword}
|
||||
${fingerprint}
|
||||
${candidate}
|
||||
a=rtcp-mux
|
||||
a=rtpmap:${audioPayloadType} opus/48000/2
|
||||
a=fmtp:${audioPayloadType} minptime=10;useinbandfec=1;usedtx=1
|
||||
a=rtcp-fb:${audioPayloadType} transport-cc
|
||||
a=rtcp-fb:${audioPayloadType} nack
|
||||
a=ice-lite
|
||||
`.trim();
|
||||
const videoPayloads = Object.values(CodecPayloadType).filter(
|
||||
(el) => el.type === "video",
|
||||
);
|
||||
const videoPayloadTypes = videoPayloads.flatMap((el) => [
|
||||
el.payload_type,
|
||||
el.rtx_payload_type,
|
||||
]);
|
||||
const videoSection = `
|
||||
m=video ${port} UDP/TLS/RTP/SAVPF ${videoPayloadTypes.join(" ")}
|
||||
${ip}
|
||||
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
|
||||
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
|
||||
a=extmap:14 urn:ietf:params:rtp-hdrext:toffset
|
||||
a=extmap:13 urn:3gpp:video-orientation
|
||||
a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
|
||||
a=setup:passive
|
||||
a=mid:1
|
||||
a=inactive
|
||||
${iceUsername}
|
||||
${icePassword}
|
||||
${fingerprint}
|
||||
${candidate}
|
||||
a=rtcp-mux
|
||||
a=ice-lite
|
||||
`.trim();
|
||||
const videoRtpMap = videoPayloads
|
||||
.flatMap((el) => [
|
||||
`a=rtpmap:${el.payload_type} ${el.name}/90000`,
|
||||
`a=rtpmap:${el.rtx_payload_type} rtx/90000`,
|
||||
`a=fmtp:${el.rtx_payload_type} apt=${el.payload_type}`,
|
||||
`a=rtcp-fb:${el.payload_type} ccm fir`,
|
||||
`a=rtcp-fb:${el.payload_type} nack`,
|
||||
`a=rtcp-fb:${el.payload_type} nack pli`,
|
||||
`a=rtcp-fb:${el.payload_type} goog-remb`,
|
||||
`a=rtcp-fb:${el.payload_type} transport-cc`,
|
||||
])
|
||||
.join("\n");
|
||||
this._webRtcWrapper.webRtcConn?.setRemoteDescription(
|
||||
[audioSection, videoSection, videoRtpMap].join("\n"),
|
||||
"answer",
|
||||
);
|
||||
this.emit("select_protocol_ack");
|
||||
}
|
||||
|
||||
initDave() {
|
||||
if (this._daveProtocolVersion) {
|
||||
if (this._daveSession) {
|
||||
this._daveSession.reinit(
|
||||
this._daveProtocolVersion,
|
||||
this.botId,
|
||||
this.daveChannelId,
|
||||
);
|
||||
this._loggerDave.debug(`Reinitialized DAVE`, {
|
||||
user_id: this.botId,
|
||||
channel_id: this.daveChannelId,
|
||||
});
|
||||
} else {
|
||||
this._daveSession = new Davey.DAVESession(
|
||||
this._daveProtocolVersion,
|
||||
this.botId,
|
||||
this.daveChannelId,
|
||||
);
|
||||
this._loggerDave.debug(`Initialized DAVE`, {
|
||||
user_id: this.botId,
|
||||
channel_id: this.daveChannelId,
|
||||
});
|
||||
}
|
||||
this.sendOpcodeBinary(
|
||||
VoiceOpCodesBinary.MLS_KEY_PACKAGE,
|
||||
this._daveSession.getSerializedKeyPackage(),
|
||||
);
|
||||
} else if (this._daveSession) {
|
||||
this._daveSession.reset();
|
||||
this._daveSession.setPassthroughMode(true, 10);
|
||||
}
|
||||
}
|
||||
|
||||
processInvalidCommit(transitionId: number) {
|
||||
this._loggerDave.debug("Invalid commit received, reinitializing DAVE", {
|
||||
transitionId,
|
||||
});
|
||||
this.sendOpcode(VoiceOpCodes.MLS_INVALID_COMMIT_WELCOME, {
|
||||
transition_id: transitionId,
|
||||
});
|
||||
this.initDave();
|
||||
}
|
||||
|
||||
executePendingTransition(transitionId: number) {
|
||||
const newVersion = this._davePendingTransitions.get(transitionId);
|
||||
if (newVersion === undefined) {
|
||||
this._loggerDave.error("Unrecognized transition ID", { transitionId });
|
||||
return;
|
||||
}
|
||||
const oldVersion = this._daveProtocolVersion;
|
||||
this._daveProtocolVersion = newVersion;
|
||||
|
||||
if (oldVersion !== newVersion && newVersion === 0) {
|
||||
// Downgraded
|
||||
this._daveDowngraded = true;
|
||||
this._loggerDave.debug("Downgraded to non-E2E voice call");
|
||||
} else if (transitionId > 0 && this._daveDowngraded) {
|
||||
this._daveDowngraded = false;
|
||||
this._daveSession?.setPassthroughMode(true, 10);
|
||||
this._loggerDave.debug("Upgraded to E2E voice call");
|
||||
}
|
||||
|
||||
this._davePendingTransitions.delete(transitionId);
|
||||
this._loggerDave.debug(`Pending transition ID ${transitionId} executed`, {
|
||||
transitionId,
|
||||
});
|
||||
}
|
||||
|
||||
setupEvents(): void {
|
||||
this.ws?.addEventListener("message", async (e) => {
|
||||
if (e.data instanceof ArrayBuffer) {
|
||||
this.handleBinaryMessages(Buffer.from(e.data));
|
||||
return;
|
||||
}
|
||||
const { op, d, seq } = JSON.parse(e.data as string) as GatewayResponse;
|
||||
if (seq) this._sequenceNumber = seq;
|
||||
|
||||
if (op === VoiceOpCodes.READY) {
|
||||
// ready
|
||||
this.handleReady(d);
|
||||
this.setProtocols().then(() => this.ready(this._webRtcWrapper));
|
||||
this.setVideoAttributes(false);
|
||||
} else if (op >= 4000) {
|
||||
console.error(`Error ${this.constructor.name} connection`, d);
|
||||
} else if (op === VoiceOpCodes.HELLO) {
|
||||
this.setupHeartbeat(d.heartbeat_interval);
|
||||
} else if (op === VoiceOpCodes.SELECT_PROTOCOL_ACK) {
|
||||
// session description
|
||||
this.handleProtocolAck(d);
|
||||
} else if (op === VoiceOpCodes.SPEAKING) {
|
||||
// ignore speaking updates
|
||||
} else if (op === VoiceOpCodes.HEARTBEAT_ACK) {
|
||||
// ignore heartbeat acknowledgements
|
||||
} else if (op === VoiceOpCodes.RESUMED) {
|
||||
this.status.started = true;
|
||||
} else if (op === VoiceOpCodes.CLIENTS_CONNECT) {
|
||||
d.user_ids.forEach((id) => {
|
||||
this._connectedUsers.add(id);
|
||||
});
|
||||
} else if (op === VoiceOpCodes.CLIENT_DISCONNECT) {
|
||||
this._connectedUsers.delete(d.user_id);
|
||||
} else if (op === VoiceOpCodes.DAVE_PREPARE_TRANSITION) {
|
||||
this._loggerDave.debug("Preparing for DAVE transition", d);
|
||||
this._davePendingTransitions.set(d.transition_id, d.protocol_version);
|
||||
if (d.transition_id === 0) {
|
||||
this.executePendingTransition(d.transition_id);
|
||||
} else {
|
||||
if (d.protocol_version === 0)
|
||||
this._daveSession?.setPassthroughMode(true, 120);
|
||||
this.sendOpcode(VoiceOpCodes.DAVE_TRANSITION_READY, {
|
||||
transition_id: d.transition_id,
|
||||
});
|
||||
}
|
||||
} else if (op === VoiceOpCodes.DAVE_EXECUTE_TRANSITION) {
|
||||
this.executePendingTransition(d.transition_id);
|
||||
} else if (op === VoiceOpCodes.DAVE_PREPARE_EPOCH) {
|
||||
this._loggerDave.debug("Preparing for DAVE epoch", d);
|
||||
if (d.epoch === 1) {
|
||||
this._daveProtocolVersion = d.protocol_version;
|
||||
this.initDave();
|
||||
}
|
||||
} else {
|
||||
//console.log("unhandled voice event", {op, d});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleBinaryMessages(msg: Buffer) {
|
||||
this._sequenceNumber = msg.readUint16BE(0);
|
||||
const op = msg.readUint8(2);
|
||||
this._logger.trace(`Handling binary message with op ${op}`, { op });
|
||||
switch (op) {
|
||||
case VoiceOpCodesBinary.MLS_EXTERNAL_SENDER: {
|
||||
this._daveSession?.setExternalSender(msg.subarray(3));
|
||||
this._loggerDave.debug("Set MLS external sender");
|
||||
break;
|
||||
}
|
||||
case VoiceOpCodesBinary.MLS_PROPOSALS: {
|
||||
const optype = msg.readUint8(3);
|
||||
const { commit, welcome } = this._daveSession!.processProposals(
|
||||
optype,
|
||||
msg.subarray(4),
|
||||
[...this._connectedUsers],
|
||||
);
|
||||
if (commit) {
|
||||
this.sendOpcodeBinary(
|
||||
VoiceOpCodesBinary.MLS_COMMIT_WELCOME,
|
||||
welcome ? Buffer.concat([commit, welcome]) : commit,
|
||||
);
|
||||
}
|
||||
this._loggerDave.debug("Processed MLS proposal");
|
||||
break;
|
||||
}
|
||||
case VoiceOpCodesBinary.MLS_ANNOUNCE_COMMIT_TRANSITION: {
|
||||
const transitionId = msg.readUInt16BE(3);
|
||||
try {
|
||||
this._daveSession?.processCommit(msg.subarray(5));
|
||||
if (transitionId) {
|
||||
this._davePendingTransitions.set(
|
||||
transitionId,
|
||||
this._daveProtocolVersion,
|
||||
);
|
||||
this.sendOpcode(VoiceOpCodes.DAVE_TRANSITION_READY, {
|
||||
transition_id: transitionId,
|
||||
});
|
||||
}
|
||||
this._loggerDave.debug("MLS commit processed", { transitionId });
|
||||
} catch (e) {
|
||||
this._loggerDave.debug("MLS commit errored", e);
|
||||
this.processInvalidCommit(transitionId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case VoiceOpCodesBinary.MLS_WELCOME: {
|
||||
const transitionId = msg.readUInt16BE(3);
|
||||
try {
|
||||
this._daveSession?.processWelcome(msg.subarray(5));
|
||||
if (transitionId) {
|
||||
this._davePendingTransitions.set(
|
||||
transitionId,
|
||||
this._daveProtocolVersion,
|
||||
);
|
||||
this.sendOpcode(VoiceOpCodes.DAVE_TRANSITION_READY, {
|
||||
transition_id: transitionId,
|
||||
});
|
||||
}
|
||||
this._loggerDave.debug("MLS welcome processed", { transitionId });
|
||||
} catch (e) {
|
||||
this._loggerDave.debug("MLS welcome errored", e);
|
||||
this.processInvalidCommit(transitionId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public get daveReady() {
|
||||
return this._daveProtocolVersion && this._daveSession?.ready;
|
||||
}
|
||||
|
||||
public get daveSession() {
|
||||
return this._daveSession;
|
||||
}
|
||||
|
||||
setupHeartbeat(interval: number): void {
|
||||
if (this.interval) {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
this.interval = setInterval(() => {
|
||||
try {
|
||||
this.sendOpcode(VoiceOpCodes.HEARTBEAT, {
|
||||
t: Date.now(),
|
||||
seq_ack: this._sequenceNumber,
|
||||
});
|
||||
} catch {}
|
||||
}, interval);
|
||||
}
|
||||
|
||||
sendOpcode<T extends GatewayRequest>(code: T["op"], data: T["d"]): void {
|
||||
if (this.ws?.readyState !== WebSocket.OPEN) return;
|
||||
this.ws.send(
|
||||
JSON.stringify({
|
||||
op: code,
|
||||
d: data,
|
||||
}),
|
||||
);
|
||||
}
|
||||
sendOpcodeBinary(code: VoiceOpCodesBinary, data: Buffer) {
|
||||
if (this.ws?.readyState !== WebSocket.OPEN) return;
|
||||
const buf = Buffer.allocUnsafe(data.length + 1);
|
||||
buf.writeUInt8(code);
|
||||
data.copy(buf, 1);
|
||||
this.ws.send(buf);
|
||||
}
|
||||
|
||||
/*
|
||||
** identifies with media server with credentials
|
||||
*/
|
||||
identify(): void {
|
||||
if (!this.serverId) throw new Error("Server ID is null or empty");
|
||||
if (!this.session_id) throw new Error("Session ID is null or empty");
|
||||
if (!this.token) throw new Error("Token is null or empty");
|
||||
this.sendOpcode(VoiceOpCodes.IDENTIFY, {
|
||||
server_id: this.serverId,
|
||||
user_id: this.botId,
|
||||
session_id: this.session_id,
|
||||
token: this.token,
|
||||
video: true,
|
||||
streams: STREAMS_SIMULCAST,
|
||||
max_dave_protocol_version: Davey.DAVE_PROTOCOL_VERSION ?? 0,
|
||||
});
|
||||
}
|
||||
|
||||
resume(): void {
|
||||
if (!this.serverId) throw new Error("Server ID is null or empty");
|
||||
if (!this.session_id) throw new Error("Session ID is null or empty");
|
||||
if (!this.token) throw new Error("Token is null or empty");
|
||||
this.sendOpcode(VoiceOpCodes.RESUME, {
|
||||
server_id: this.serverId,
|
||||
session_id: this.session_id,
|
||||
token: this.token,
|
||||
seq_ack: this._sequenceNumber,
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
** Sets protocols and ip data used for video and audio.
|
||||
** Uses vp8 for video
|
||||
** Uses opus for audio
|
||||
*/
|
||||
public async setProtocols(): Promise<void> {
|
||||
if (!this._webRtcParams) throw new Error("WebRTC parameters not set");
|
||||
// if (
|
||||
// this._webRtcParams.supportedEncryptionModes.includes(SupportedEncryptionModes.AES256) &&
|
||||
// !this._streamer.opts.forceChacha20Encryption
|
||||
// ) {
|
||||
// encryptionMode = SupportedEncryptionModes.AES256
|
||||
// } else {
|
||||
// encryptionMode = SupportedEncryptionModes.XCHACHA20
|
||||
// }
|
||||
|
||||
const reconnect = () => {
|
||||
const webRtcConn = this._webRtcWrapper.initWebRtc();
|
||||
webRtcConn.onStateChange((state) => {
|
||||
if (state === "closed" && !this._closed) reconnect();
|
||||
});
|
||||
webRtcConn.onLocalDescription((sdp) => {
|
||||
const rtc_connection_id = randomUUID();
|
||||
this.sendOpcode(VoiceOpCodes.SELECT_PROTOCOL, {
|
||||
protocol: "webrtc",
|
||||
codecs: Object.values(CodecPayloadType) as ValueOf<
|
||||
typeof CodecPayloadType
|
||||
>[],
|
||||
data: sdp,
|
||||
sdp: sdp,
|
||||
rtc_connection_id,
|
||||
});
|
||||
});
|
||||
webRtcConn.setLocalDescription();
|
||||
};
|
||||
reconnect();
|
||||
return new Promise((resolve) => {
|
||||
this.once("select_protocol_ack", () => resolve());
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Sets video attributes (width, height, frame rate).
|
||||
* enabled -> video on or off
|
||||
* attr -> video attributes
|
||||
* video and rtx sources are set to ssrc + 1 and ssrc + 2
|
||||
*/
|
||||
public setVideoAttributes(enabled: false): void;
|
||||
public setVideoAttributes(enabled: true, attr: VideoAttributes): void;
|
||||
public setVideoAttributes(enabled: boolean, attr?: VideoAttributes): void {
|
||||
if (!this._webRtcParams) throw new Error("WebRTC parameters not set");
|
||||
const { audioSsrc, videoSsrc, rtxSsrc } = this._webRtcParams;
|
||||
if (!enabled) {
|
||||
this.sendOpcode(VoiceOpCodes.VIDEO, {
|
||||
audio_ssrc: audioSsrc,
|
||||
video_ssrc: 0,
|
||||
rtx_ssrc: 0,
|
||||
streams: [],
|
||||
});
|
||||
} else {
|
||||
if (!attr) throw new Error("Need to specify video attributes");
|
||||
this.sendOpcode(VoiceOpCodes.VIDEO, {
|
||||
audio_ssrc: audioSsrc,
|
||||
video_ssrc: videoSsrc,
|
||||
rtx_ssrc: rtxSsrc,
|
||||
streams: [
|
||||
{
|
||||
type: "video",
|
||||
rid: "100",
|
||||
ssrc: videoSsrc,
|
||||
active: true,
|
||||
quality: 100,
|
||||
rtx_ssrc: rtxSsrc,
|
||||
// hardcode the max bitrate because we don't really know anyway
|
||||
max_bitrate: 10000 * 1000,
|
||||
max_framerate: enabled ? attr.fps : 0,
|
||||
max_resolution: {
|
||||
type: "fixed",
|
||||
width: attr.width,
|
||||
height: attr.height,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
** Set speaking status
|
||||
** speaking -> speaking status on or off
|
||||
*/
|
||||
public setSpeaking(speaking: boolean): void {
|
||||
if (!this._webRtcParams) throw new Error("WebRTC connection not ready");
|
||||
this.sendOpcode(VoiceOpCodes.SPEAKING, {
|
||||
delay: 0,
|
||||
speaking: speaking ? 1 : 0,
|
||||
ssrc: this._webRtcParams.audioSsrc,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
export const CodecPayloadType = {
|
||||
opus: {
|
||||
name: "opus",
|
||||
type: "audio",
|
||||
clockRate: 48000,
|
||||
priority: 1000,
|
||||
payload_type: 120,
|
||||
},
|
||||
H264: {
|
||||
name: "H264",
|
||||
type: "video",
|
||||
clockRate: 90000,
|
||||
priority: 1000,
|
||||
payload_type: 101,
|
||||
rtx_payload_type: 102,
|
||||
encode: true,
|
||||
decode: true,
|
||||
},
|
||||
H265: {
|
||||
name: "H265",
|
||||
type: "video",
|
||||
clockRate: 90000,
|
||||
priority: 1000,
|
||||
payload_type: 103,
|
||||
rtx_payload_type: 104,
|
||||
encode: true,
|
||||
decode: true,
|
||||
},
|
||||
VP8: {
|
||||
name: "VP8",
|
||||
type: "video",
|
||||
clockRate: 90000,
|
||||
priority: 1000,
|
||||
payload_type: 105,
|
||||
rtx_payload_type: 106,
|
||||
encode: true,
|
||||
decode: true,
|
||||
},
|
||||
VP9: {
|
||||
name: "VP9",
|
||||
type: "video",
|
||||
clockRate: 90000,
|
||||
priority: 1000,
|
||||
payload_type: 107,
|
||||
rtx_payload_type: 108,
|
||||
encode: true,
|
||||
decode: true,
|
||||
},
|
||||
AV1: {
|
||||
name: "AV1",
|
||||
type: "video",
|
||||
clockRate: 90000,
|
||||
priority: 1000,
|
||||
payload_type: 109,
|
||||
rtx_payload_type: 110,
|
||||
encode: true,
|
||||
decode: true,
|
||||
},
|
||||
} as const;
|
||||
@@ -0,0 +1,171 @@
|
||||
import udpCon from 'node:dgram';
|
||||
import { isIP } from 'node:net';
|
||||
import { AudioPacketizer } from '../packet/AudioPacketizer.js';
|
||||
import {
|
||||
VideoPacketizerH264,
|
||||
VideoPacketizerH265
|
||||
} from '../packet/VideoPacketizerAnnexB.js';
|
||||
import { VideoPacketizerVP8 } from '../packet/VideoPacketizerVP8.js';
|
||||
import { normalizeVideoCodec } from '../../utils.js';
|
||||
import type { BaseMediaPacketizer } from '../packet/BaseMediaPacketizer.js';
|
||||
import type { BaseMediaConnection } from './BaseMediaConnection.js';
|
||||
|
||||
// credit to discord.js
|
||||
function parseLocalPacket(message: Buffer) {
|
||||
const packet = Buffer.from(message);
|
||||
|
||||
const ip = packet.subarray(8, packet.indexOf(0, 8)).toString('utf8');
|
||||
|
||||
if (!isIP(ip)) {
|
||||
throw new Error('Malformed IP address');
|
||||
}
|
||||
|
||||
const port = packet.readUInt16BE(packet.length - 2);
|
||||
|
||||
return { ip, port };
|
||||
}
|
||||
|
||||
export class MediaUdp {
|
||||
private _mediaConnection: BaseMediaConnection;
|
||||
private _socket: udpCon.Socket | null = null;
|
||||
private _ready = false;
|
||||
private _audioPacketizer?: BaseMediaPacketizer;
|
||||
private _videoPacketizer?: BaseMediaPacketizer;
|
||||
private _ip?: string;
|
||||
private _port?: number;
|
||||
|
||||
constructor(voiceConnection: BaseMediaConnection) {
|
||||
this._mediaConnection = voiceConnection;
|
||||
}
|
||||
|
||||
public get audioPacketizer(): BaseMediaPacketizer | undefined {
|
||||
return this._audioPacketizer;
|
||||
}
|
||||
|
||||
public get videoPacketizer(): BaseMediaPacketizer | undefined {
|
||||
// This will never be undefined anyway, so it's safe
|
||||
return this._videoPacketizer;
|
||||
}
|
||||
|
||||
public get mediaConnection(): BaseMediaConnection {
|
||||
return this._mediaConnection;
|
||||
}
|
||||
|
||||
public get ip()
|
||||
{
|
||||
return this._ip;
|
||||
}
|
||||
|
||||
public get port()
|
||||
{
|
||||
return this._port;
|
||||
}
|
||||
|
||||
public async sendAudioFrame(frame: Buffer, frametime: number): Promise<void> {
|
||||
if(!this.ready) return;
|
||||
await this.audioPacketizer?.sendFrame(frame, frametime);
|
||||
}
|
||||
|
||||
public async sendVideoFrame(frame: Buffer, frametime: number): Promise<void> {
|
||||
if(!this.ready) return;
|
||||
await this.videoPacketizer?.sendFrame(frame, frametime);
|
||||
}
|
||||
|
||||
public setPacketizer(videoCodec: string): void {
|
||||
if (!this.mediaConnection.webRtcParams)
|
||||
throw new Error("WebRTC connection not ready");
|
||||
const { audioSsrc, videoSsrc } = this.mediaConnection.webRtcParams;
|
||||
this._audioPacketizer = new AudioPacketizer(this, audioSsrc);
|
||||
switch (normalizeVideoCodec(videoCodec))
|
||||
{
|
||||
case "H264":
|
||||
this._videoPacketizer = new VideoPacketizerH264(this, videoSsrc);
|
||||
break;
|
||||
case "H265":
|
||||
this._videoPacketizer = new VideoPacketizerH265(this, videoSsrc);
|
||||
break;
|
||||
case "VP8":
|
||||
this._videoPacketizer = new VideoPacketizerVP8(this, videoSsrc);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Packetizer not implemented for ${videoCodec}`)
|
||||
}
|
||||
}
|
||||
|
||||
public sendPacket(packet: Buffer): Promise<void> {
|
||||
if (!this.mediaConnection.webRtcParams)
|
||||
throw new Error("WebRTC connection not ready");
|
||||
const { address, port } = this.mediaConnection.webRtcParams;
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
try {
|
||||
this._socket?.send(packet, 0, packet.length, port, address, (error, bytes) => {
|
||||
if (error) {
|
||||
console.log("ERROR", error);
|
||||
reject(error);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
} catch(e) {reject(e)}
|
||||
});
|
||||
}
|
||||
|
||||
handleIncoming(buf: unknown): void {
|
||||
//console.log("RECEIVED PACKET", buf);
|
||||
}
|
||||
|
||||
public get ready(): boolean {
|
||||
return this._ready;
|
||||
}
|
||||
|
||||
public set ready(val: boolean) {
|
||||
this._ready = val;
|
||||
}
|
||||
|
||||
public stop(): void {
|
||||
try {
|
||||
this.ready = false;
|
||||
this._socket?.disconnect();
|
||||
}catch(e) {}
|
||||
}
|
||||
|
||||
public createUdp(): Promise<void> {
|
||||
if (!this.mediaConnection.webRtcParams)
|
||||
throw new Error("WebRTC connection not ready");
|
||||
const { audioSsrc, address, port } = this.mediaConnection.webRtcParams;
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
this._socket = udpCon.createSocket('udp4');
|
||||
|
||||
this._socket.on('error', (error: Error) => {
|
||||
console.error("Error connecting to media udp server", error);
|
||||
reject(error);
|
||||
});
|
||||
|
||||
this._socket.once('message', (message) => {
|
||||
if (message.readUInt16BE(0) !== 2) {
|
||||
reject('wrong handshake packet for udp')
|
||||
}
|
||||
try {
|
||||
const packet = parseLocalPacket(message);
|
||||
this._ip = packet.ip;
|
||||
this._port = packet.port;
|
||||
this._ready = true;
|
||||
} catch(e) { reject(e) }
|
||||
|
||||
resolve();
|
||||
this._socket?.on('message', this.handleIncoming);
|
||||
});
|
||||
|
||||
const blank = Buffer.alloc(74);
|
||||
|
||||
blank.writeUInt16BE(1, 0);
|
||||
blank.writeUInt16BE(70, 2);
|
||||
blank.writeUInt32BE(audioSsrc, 4);
|
||||
|
||||
this._socket.send(blank, 0, blank.length, port, address, (error, bytes) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { VoiceOpCodes } from "../voice/VoiceOpCodes.js";
|
||||
import { BaseMediaConnection } from "./BaseMediaConnection.js";
|
||||
|
||||
export class StreamConnection extends BaseMediaConnection {
|
||||
private _streamKey: string | null = null;
|
||||
private _serverId: string | null = null;
|
||||
|
||||
public override setSpeaking(speaking: boolean): void {
|
||||
if (!this.webRtcParams) throw new Error("WebRTC connection not ready");
|
||||
this.sendOpcode(VoiceOpCodes.SPEAKING, {
|
||||
delay: 0,
|
||||
speaking: speaking ? 2 : 0,
|
||||
ssrc: this.webRtcParams.audioSsrc,
|
||||
});
|
||||
}
|
||||
|
||||
public override get daveChannelId() {
|
||||
if (this._serverId === null)
|
||||
throw new Error("Server ID not set (this shouldn't happen)");
|
||||
const channelId = BigInt(this._serverId) - 1n;
|
||||
return channelId.toString();
|
||||
}
|
||||
|
||||
public override get serverId(): string | null {
|
||||
return this._serverId;
|
||||
}
|
||||
|
||||
public set serverId(id: string) {
|
||||
this._serverId = id;
|
||||
}
|
||||
|
||||
public get streamKey(): string | null {
|
||||
return this._streamKey;
|
||||
}
|
||||
public set streamKey(value: string) {
|
||||
this._streamKey = value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { BaseMediaConnection } from "./BaseMediaConnection.js";
|
||||
import type { StreamConnection } from "./StreamConnection.js";
|
||||
|
||||
export class VoiceConnection extends BaseMediaConnection {
|
||||
public streamConnection?: StreamConnection;
|
||||
|
||||
public override get daveChannelId() {
|
||||
return this.channelId;
|
||||
}
|
||||
|
||||
public override get serverId(): string {
|
||||
return this.guildId ?? this.channelId; // for guild vc it is the guild id, for dm voice it is the channel id
|
||||
}
|
||||
|
||||
public override stop(): void {
|
||||
super.stop();
|
||||
this.streamConnection?.stop();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
import type { VoiceOpCodes } from "./VoiceOpCodes.js";
|
||||
import type { SupportedEncryptionModes } from "../../utils.js";
|
||||
|
||||
type StreamInfo = {
|
||||
active: boolean;
|
||||
quality: number;
|
||||
rid: string;
|
||||
ssrc: number;
|
||||
rtx_ssrc: number;
|
||||
/**
|
||||
* always "video" from what I observed
|
||||
*/
|
||||
type: string;
|
||||
};
|
||||
|
||||
type SimulcastInfo = {
|
||||
type: string;
|
||||
rid: string;
|
||||
quality: number;
|
||||
};
|
||||
|
||||
type CodecPayloadType =
|
||||
| {
|
||||
name: string;
|
||||
type: "audio";
|
||||
priority: number;
|
||||
payload_type: number;
|
||||
}
|
||||
| {
|
||||
name: string;
|
||||
type: "video";
|
||||
priority: number;
|
||||
payload_type: number;
|
||||
rtx_payload_type: number;
|
||||
encode: boolean;
|
||||
decode: boolean;
|
||||
};
|
||||
|
||||
export namespace Message {
|
||||
// Request messages
|
||||
export type Identify = {
|
||||
server_id: string;
|
||||
user_id: string;
|
||||
session_id: string;
|
||||
token: string;
|
||||
video: boolean;
|
||||
streams: SimulcastInfo[];
|
||||
max_dave_protocol_version?: number;
|
||||
};
|
||||
|
||||
export type Resume = {
|
||||
server_id: string;
|
||||
session_id: string;
|
||||
token: string;
|
||||
seq_ack: number;
|
||||
};
|
||||
|
||||
export type Heartbeat = {
|
||||
t: number;
|
||||
seq_ack?: number;
|
||||
};
|
||||
|
||||
export type SelectProtocol =
|
||||
| {
|
||||
protocol: "udp";
|
||||
codecs: CodecPayloadType[];
|
||||
data: {
|
||||
address: string;
|
||||
port: number;
|
||||
mode: SupportedEncryptionModes;
|
||||
};
|
||||
}
|
||||
| {
|
||||
protocol: "webrtc";
|
||||
codecs: CodecPayloadType[];
|
||||
data: string;
|
||||
sdp: string;
|
||||
rtc_connection_id: string;
|
||||
};
|
||||
|
||||
export type Video = {
|
||||
audio_ssrc: number;
|
||||
video_ssrc: number;
|
||||
rtx_ssrc: number;
|
||||
streams: {
|
||||
type: "video";
|
||||
rid: string;
|
||||
ssrc: number;
|
||||
active: boolean;
|
||||
quality: number;
|
||||
rtx_ssrc: number;
|
||||
max_bitrate: number;
|
||||
max_framerate: number;
|
||||
max_resolution: {
|
||||
type: "fixed";
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
}[];
|
||||
};
|
||||
|
||||
// Response messages
|
||||
export type Hello = {
|
||||
heartbeat_interval: number;
|
||||
};
|
||||
|
||||
export type Ready = {
|
||||
ssrc: number;
|
||||
ip: string;
|
||||
port: number;
|
||||
modes: SupportedEncryptionModes[];
|
||||
experiments: string[];
|
||||
streams: StreamInfo[];
|
||||
};
|
||||
|
||||
export type Speaking = {
|
||||
speaking: 0 | 1 | 2;
|
||||
delay: number;
|
||||
ssrc: number;
|
||||
};
|
||||
|
||||
export type SelectProtocolAck = {
|
||||
audio_codec: string;
|
||||
video_codec: string;
|
||||
dave_protocol_version: number;
|
||||
} & (
|
||||
| {
|
||||
secret_key: number[];
|
||||
mode: string;
|
||||
}
|
||||
| {
|
||||
media_session_id: number;
|
||||
sdp: string;
|
||||
}
|
||||
);
|
||||
|
||||
export type HeartbeatAck = {
|
||||
t: number;
|
||||
};
|
||||
|
||||
export type ClientsConnect = {
|
||||
user_ids: string[];
|
||||
};
|
||||
|
||||
export type ClientDisconnect = {
|
||||
user_id: string;
|
||||
};
|
||||
|
||||
export type DavePrepareTransition = {
|
||||
transition_id: number;
|
||||
protocol_version: number;
|
||||
};
|
||||
|
||||
export type DaveExecuteTransition = {
|
||||
transition_id: number;
|
||||
};
|
||||
|
||||
export type DaveTransitionReady = {
|
||||
transition_id: number;
|
||||
};
|
||||
|
||||
export type DavePrepareEpoch = {
|
||||
epoch: number;
|
||||
protocol_version: number;
|
||||
};
|
||||
|
||||
export type MlsInvalidCommitWelcome = {
|
||||
transition_id: number;
|
||||
};
|
||||
}
|
||||
|
||||
export namespace GatewayResponse {
|
||||
type Generic<
|
||||
Op extends VoiceOpCodes,
|
||||
T extends Record<string, unknown> | null,
|
||||
> = {
|
||||
op: Op;
|
||||
d: T;
|
||||
seq?: number;
|
||||
};
|
||||
export type Hello = Generic<VoiceOpCodes.HELLO, Message.Hello>;
|
||||
export type Ready = Generic<VoiceOpCodes.READY, Message.Ready>;
|
||||
export type Resumed = Generic<VoiceOpCodes.RESUMED, null>;
|
||||
export type Speaking = Generic<VoiceOpCodes.SPEAKING, Message.Speaking>;
|
||||
export type SelectProtocolAck = Generic<
|
||||
VoiceOpCodes.SELECT_PROTOCOL_ACK,
|
||||
Message.SelectProtocolAck
|
||||
>;
|
||||
export type HeartbeatAck = Generic<
|
||||
VoiceOpCodes.HEARTBEAT_ACK,
|
||||
Message.HeartbeatAck
|
||||
>;
|
||||
export type ClientsConnect = Generic<
|
||||
VoiceOpCodes.CLIENTS_CONNECT,
|
||||
Message.ClientsConnect
|
||||
>;
|
||||
export type ClientDisconnect = Generic<
|
||||
VoiceOpCodes.CLIENT_DISCONNECT,
|
||||
Message.ClientDisconnect
|
||||
>;
|
||||
export type DavePrepareTransition = Generic<
|
||||
VoiceOpCodes.DAVE_PREPARE_TRANSITION,
|
||||
Message.DavePrepareTransition
|
||||
>;
|
||||
export type DaveExecuteTransition = Generic<
|
||||
VoiceOpCodes.DAVE_EXECUTE_TRANSITION,
|
||||
Message.DaveExecuteTransition
|
||||
>;
|
||||
export type DavePrepareEpoch = Generic<
|
||||
VoiceOpCodes.DAVE_PREPARE_EPOCH,
|
||||
Message.DavePrepareEpoch
|
||||
>;
|
||||
}
|
||||
|
||||
export type GatewayResponse =
|
||||
| GatewayResponse.Hello
|
||||
| GatewayResponse.Ready
|
||||
| GatewayResponse.Resumed
|
||||
| GatewayResponse.Speaking
|
||||
| GatewayResponse.SelectProtocolAck
|
||||
| GatewayResponse.HeartbeatAck
|
||||
| GatewayResponse.ClientsConnect
|
||||
| GatewayResponse.ClientDisconnect
|
||||
| GatewayResponse.DavePrepareTransition
|
||||
| GatewayResponse.DaveExecuteTransition
|
||||
| GatewayResponse.DavePrepareEpoch;
|
||||
|
||||
export namespace GatewayRequest {
|
||||
type Generic<
|
||||
Op extends VoiceOpCodes,
|
||||
T extends Record<string, unknown> | null,
|
||||
> = {
|
||||
op: Op;
|
||||
d: T;
|
||||
};
|
||||
export type Identify = Generic<VoiceOpCodes.IDENTIFY, Message.Identify>;
|
||||
export type Resume = Generic<VoiceOpCodes.RESUME, Message.Resume>;
|
||||
export type Heartbeat = Generic<VoiceOpCodes.HEARTBEAT, Message.Heartbeat>;
|
||||
export type SelectProtocol = Generic<
|
||||
VoiceOpCodes.SELECT_PROTOCOL,
|
||||
Message.SelectProtocol
|
||||
>;
|
||||
export type Video = Generic<VoiceOpCodes.VIDEO, Message.Video>;
|
||||
export type Speaking = Generic<VoiceOpCodes.SPEAKING, Message.Speaking>;
|
||||
export type DaveTransitionReady = Generic<
|
||||
VoiceOpCodes.DAVE_TRANSITION_READY,
|
||||
Message.DaveTransitionReady
|
||||
>;
|
||||
export type MlsInvalidCommitWelcome = Generic<
|
||||
VoiceOpCodes.MLS_INVALID_COMMIT_WELCOME,
|
||||
Message.MlsInvalidCommitWelcome
|
||||
>;
|
||||
}
|
||||
|
||||
export type GatewayRequest =
|
||||
| GatewayRequest.Identify
|
||||
| GatewayRequest.Resume
|
||||
| GatewayRequest.Heartbeat
|
||||
| GatewayRequest.SelectProtocol
|
||||
| GatewayRequest.Video
|
||||
| GatewayRequest.Speaking
|
||||
| GatewayRequest.DaveTransitionReady
|
||||
| GatewayRequest.MlsInvalidCommitWelcome;
|
||||
@@ -0,0 +1,36 @@
|
||||
export enum VoiceOpCodes {
|
||||
IDENTIFY = 0,
|
||||
SELECT_PROTOCOL = 1,
|
||||
READY = 2,
|
||||
HEARTBEAT = 3,
|
||||
SELECT_PROTOCOL_ACK = 4,
|
||||
SPEAKING = 5,
|
||||
HEARTBEAT_ACK = 6,
|
||||
RESUME = 7,
|
||||
HELLO = 8,
|
||||
RESUMED = 9,
|
||||
CLIENTS_CONNECT = 11,
|
||||
VIDEO = 12,
|
||||
CLIENT_DISCONNECT = 13,
|
||||
SESSION_UPDATE = 14,
|
||||
MEDIA_SINK_WANTS = 15,
|
||||
VOICE_BACKEND_VERSION = 16,
|
||||
CHANNEL_OPTIONS_UPDATE = 17,
|
||||
FLAGS = 18,
|
||||
SPEED_TEST = 19,
|
||||
PLATFORM = 20,
|
||||
DAVE_PREPARE_TRANSITION = 21,
|
||||
DAVE_EXECUTE_TRANSITION = 22,
|
||||
DAVE_TRANSITION_READY = 23,
|
||||
DAVE_PREPARE_EPOCH = 24,
|
||||
MLS_INVALID_COMMIT_WELCOME = 31,
|
||||
}
|
||||
|
||||
export enum VoiceOpCodesBinary {
|
||||
MLS_EXTERNAL_SENDER = 25,
|
||||
MLS_KEY_PACKAGE = 26,
|
||||
MLS_PROPOSALS = 27,
|
||||
MLS_COMMIT_WELCOME = 28,
|
||||
MLS_ANNOUNCE_COMMIT_TRANSITION = 29,
|
||||
MLS_WELCOME = 30,
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
import {
|
||||
PeerConnection,
|
||||
Audio,
|
||||
Video,
|
||||
PacingHandler,
|
||||
RtpPacketizer,
|
||||
H264RtpPacketizer,
|
||||
H265RtpPacketizer,
|
||||
AV1RtpPacketizer,
|
||||
RtpPacketizationConfig,
|
||||
RtcpNackResponder,
|
||||
RtcpSrReporter,
|
||||
type Track,
|
||||
} from "@lng2004/node-datachannel";
|
||||
import { Codec, MediaType } from "@snazzah/davey";
|
||||
import { CodecPayloadType } from "./CodecPayloadType.js";
|
||||
import { normalizeVideoCodec, type SupportedVideoCodec } from "../../utils.js";
|
||||
import {
|
||||
splitNalu,
|
||||
H264Helpers,
|
||||
H264NalUnitTypes,
|
||||
startCode3,
|
||||
} from "../processing/AnnexBHelper.js";
|
||||
import { rewriteSPSVUI } from "../processing/SPSVUIRewriter.js";
|
||||
import type { BaseMediaConnection } from "./BaseMediaConnection.js";
|
||||
|
||||
export class WebRtcConnWrapper {
|
||||
private _mediaConn: BaseMediaConnection;
|
||||
|
||||
private _webRtcConn?: PeerConnection;
|
||||
private _audioDef: Audio;
|
||||
private _videoDef: Video;
|
||||
private _audioTrack?: Track;
|
||||
private _videoTrack?: Track;
|
||||
private _audioPacketizer?: RtpPacketizer;
|
||||
private _videoPacketizer?: RtpPacketizer;
|
||||
private _videoCodec?: SupportedVideoCodec;
|
||||
|
||||
constructor(mediaConn: BaseMediaConnection) {
|
||||
this._mediaConn = mediaConn;
|
||||
this._audioDef = new Audio("0", "SendRecv");
|
||||
this._videoDef = new Video("1", "SendRecv");
|
||||
this._audioDef.addOpusCodec(CodecPayloadType.opus.payload_type);
|
||||
for (const {
|
||||
name,
|
||||
payload_type,
|
||||
rtx_payload_type,
|
||||
clockRate,
|
||||
} of Object.values(CodecPayloadType).filter((el) => el.type === "video")) {
|
||||
switch (name) {
|
||||
case "H264":
|
||||
this._videoDef.addH264Codec(payload_type);
|
||||
break;
|
||||
case "H265":
|
||||
this._videoDef.addH265Codec(payload_type);
|
||||
break;
|
||||
case "VP8":
|
||||
this._videoDef.addVP8Codec(payload_type);
|
||||
break;
|
||||
case "VP9":
|
||||
this._videoDef.addVP9Codec(payload_type);
|
||||
break;
|
||||
case "AV1":
|
||||
this._videoDef.addAV1Codec(payload_type);
|
||||
break;
|
||||
}
|
||||
this._videoDef.addRTXCodec(rtx_payload_type, payload_type, clockRate);
|
||||
}
|
||||
}
|
||||
|
||||
public initWebRtc() {
|
||||
this._webRtcConn = new PeerConnection("", {
|
||||
iceServers: ["stun:stun.l.google.com:19302"],
|
||||
});
|
||||
this._audioTrack = this._webRtcConn.addTrack(this._audioDef);
|
||||
this._videoTrack = this._webRtcConn.addTrack(this._videoDef);
|
||||
this._setMediaHandler();
|
||||
return this._webRtcConn;
|
||||
}
|
||||
|
||||
private _setMediaHandler() {
|
||||
if (this._audioPacketizer)
|
||||
this._audioTrack?.setMediaHandler(this._audioPacketizer);
|
||||
if (this._videoPacketizer)
|
||||
this._videoTrack?.setMediaHandler(this._videoPacketizer);
|
||||
}
|
||||
|
||||
public close() {
|
||||
this._webRtcConn?.close();
|
||||
}
|
||||
|
||||
public get webRtcConn() {
|
||||
return this._webRtcConn;
|
||||
}
|
||||
|
||||
public get ready() {
|
||||
return this._webRtcConn?.state() === "connected";
|
||||
}
|
||||
|
||||
public get mediaConnection() {
|
||||
return this._mediaConn;
|
||||
}
|
||||
|
||||
public sendAudioFrame(frame: Buffer, frametime: number) {
|
||||
if (!this.ready) return;
|
||||
if (!this._audioPacketizer) return;
|
||||
const { rtpConfig } = this._audioPacketizer;
|
||||
const { clockRate } = rtpConfig;
|
||||
if (this.mediaConnection.daveReady)
|
||||
frame = this.mediaConnection.daveSession!.encryptOpus(frame);
|
||||
this._audioTrack?.sendMessageBinary(frame);
|
||||
rtpConfig.timestamp += Math.round((frametime * clockRate) / 1000);
|
||||
}
|
||||
|
||||
public sendVideoFrame(frame: Buffer, frametime: number) {
|
||||
if (!this.ready) return;
|
||||
if (!this._videoPacketizer) return;
|
||||
const { rtpConfig } = this._videoPacketizer;
|
||||
const { clockRate } = rtpConfig;
|
||||
if (this._videoCodec === "H264") {
|
||||
let spsRewritten = false;
|
||||
const nalus = splitNalu(frame).map((el) => {
|
||||
if (H264Helpers.getUnitType(el) === H264NalUnitTypes.SPS) {
|
||||
spsRewritten = true;
|
||||
return rewriteSPSVUI(el);
|
||||
}
|
||||
return el;
|
||||
});
|
||||
if (spsRewritten)
|
||||
frame = Buffer.concat(nalus.flatMap((el) => [startCode3, el]));
|
||||
}
|
||||
if (this.mediaConnection.daveReady) {
|
||||
let daveCodec = Codec.UNKNOWN;
|
||||
switch (this._videoCodec) {
|
||||
case "H264":
|
||||
daveCodec = Codec.H264;
|
||||
break;
|
||||
case "H265":
|
||||
daveCodec = Codec.H265;
|
||||
break;
|
||||
case "VP8":
|
||||
daveCodec = Codec.VP8;
|
||||
break;
|
||||
case "VP9":
|
||||
daveCodec = Codec.VP9;
|
||||
break;
|
||||
case "AV1":
|
||||
daveCodec = Codec.AV1;
|
||||
break;
|
||||
}
|
||||
frame = this.mediaConnection.daveSession!.encrypt(
|
||||
MediaType.VIDEO,
|
||||
daveCodec,
|
||||
frame,
|
||||
);
|
||||
}
|
||||
this._videoTrack?.sendMessageBinary(frame);
|
||||
rtpConfig.timestamp += Math.round((frametime * clockRate) / 1000);
|
||||
}
|
||||
|
||||
public setPacketizer(videoCodec: string): void {
|
||||
if (!this.mediaConnection.webRtcParams)
|
||||
throw new Error("WebRTC connection not ready");
|
||||
const { audioSsrc, videoSsrc } = this.mediaConnection.webRtcParams;
|
||||
const rtpConfigAudio = new RtpPacketizationConfig(
|
||||
audioSsrc,
|
||||
"",
|
||||
CodecPayloadType.opus.payload_type,
|
||||
CodecPayloadType.opus.clockRate,
|
||||
);
|
||||
rtpConfigAudio.playoutDelayId = 5;
|
||||
rtpConfigAudio.playoutDelayMin = 0;
|
||||
rtpConfigAudio.playoutDelayMax = 1;
|
||||
this._audioPacketizer = new RtpPacketizer(rtpConfigAudio);
|
||||
this._audioPacketizer.addToChain(new RtcpSrReporter(rtpConfigAudio));
|
||||
this._audioPacketizer.addToChain(new RtcpNackResponder());
|
||||
|
||||
this._videoCodec = normalizeVideoCodec(videoCodec);
|
||||
const rtpConfigVideo = new RtpPacketizationConfig(
|
||||
videoSsrc,
|
||||
"",
|
||||
CodecPayloadType[this._videoCodec].payload_type,
|
||||
CodecPayloadType[this._videoCodec].clockRate,
|
||||
);
|
||||
rtpConfigVideo.playoutDelayId = 5;
|
||||
rtpConfigVideo.playoutDelayMin = 0;
|
||||
rtpConfigVideo.playoutDelayMax = 10;
|
||||
switch (this._videoCodec) {
|
||||
case "H264":
|
||||
this._videoPacketizer = new H264RtpPacketizer(
|
||||
"StartSequence",
|
||||
rtpConfigVideo,
|
||||
);
|
||||
break;
|
||||
case "H265":
|
||||
this._videoPacketizer = new H265RtpPacketizer(
|
||||
"StartSequence",
|
||||
rtpConfigVideo,
|
||||
);
|
||||
break;
|
||||
case "AV1":
|
||||
this._videoPacketizer = new AV1RtpPacketizer("Obu", rtpConfigVideo);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Packetizer not implemented for ${this._videoCodec}`);
|
||||
}
|
||||
this._videoPacketizer.addToChain(new RtcpSrReporter(rtpConfigVideo));
|
||||
this._videoPacketizer.addToChain(new RtcpNackResponder());
|
||||
this._videoPacketizer.addToChain(new PacingHandler(25 * 1000 * 1000, 1));
|
||||
|
||||
this._setMediaHandler();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from "./VoiceConnection.js";
|
||||
export * from "./VoiceOpCodes.js";
|
||||
// export * from './MediaUdp.js';
|
||||
export * from "./StreamConnection.js";
|
||||
export * from "./BaseMediaConnection.js";
|
||||
Reference in New Issue
Block a user