diff --git a/src/webrtc/.DS_Store b/src/webrtc/.DS_Store
new file mode 100644
index 00000000..bfb0a416
--- /dev/null
+++ b/src/webrtc/.DS_Store
Binary files differdiff --git a/src/webrtc/Server.ts b/src/webrtc/Server.ts
new file mode 100644
index 00000000..32b795ea
--- /dev/null
+++ b/src/webrtc/Server.ts
@@ -0,0 +1,56 @@
+import { closeDatabase, Config, initDatabase, initEvent } from "@fosscord/util";
+import dotenv from "dotenv";
+import http from "http";
+import ws from "ws";
+import { Connection } from "./events/Connection";
+dotenv.config();
+
+export class Server {
+ public ws: ws.Server;
+ public port: number;
+ public server: http.Server;
+ public production: boolean;
+
+ constructor({ port, server, production }: { port: number; server?: http.Server; production?: boolean }) {
+ this.port = port;
+ this.production = production || false;
+
+ if (server) this.server = server;
+ else {
+ this.server = http.createServer(function (req, res) {
+ res.writeHead(200).end("Online");
+ });
+ }
+
+ // this.server.on("upgrade", (request, socket, head) => {
+ // if (!request.url?.includes("voice")) return;
+ // this.ws.handleUpgrade(request, socket, head, (socket) => {
+ // // @ts-ignore
+ // socket.server = this;
+ // this.ws.emit("connection", socket, request);
+ // });
+ // });
+
+ this.ws = new ws.Server({
+ maxPayload: 1024 * 1024 * 100,
+ server: this.server,
+ });
+ this.ws.on("connection", Connection);
+ this.ws.on("error", console.error);
+ }
+
+ async start(): Promise<void> {
+ await initDatabase();
+ await Config.init();
+ await initEvent();
+ if (!this.server.listening) {
+ this.server.listen(this.port);
+ console.log(`[WebRTC] online on 0.0.0.0:${this.port}`);
+ }
+ }
+
+ async stop() {
+ closeDatabase();
+ this.server.close();
+ }
+}
\ No newline at end of file
diff --git a/src/webrtc/events/Close.ts b/src/webrtc/events/Close.ts
new file mode 100644
index 00000000..1c203653
--- /dev/null
+++ b/src/webrtc/events/Close.ts
@@ -0,0 +1,9 @@
+import { WebSocket } from "@fosscord/gateway";
+import { Session } from "@fosscord/util";
+
+export async function onClose(this: WebSocket, code: number, reason: string) {
+ console.log("[WebRTC] closed", code, reason.toString());
+
+ if (this.session_id) await Session.delete({ session_id: this.session_id });
+ this.removeAllListeners();
+}
\ No newline at end of file
diff --git a/src/webrtc/events/Connection.ts b/src/webrtc/events/Connection.ts
new file mode 100644
index 00000000..bf228d64
--- /dev/null
+++ b/src/webrtc/events/Connection.ts
@@ -0,0 +1,60 @@
+import { CLOSECODES, Send, setHeartbeat, WebSocket } from "@fosscord/gateway";
+import { IncomingMessage } from "http";
+import { URL } from "url";
+import WS from "ws";
+import { VoiceOPCodes } from "../util";
+import { onClose } from "./Close";
+import { onMessage } from "./Message";
+var erlpack: any;
+try {
+ erlpack = require("@yukikaze-bot/erlpack");
+} catch (error) {}
+
+// TODO: check rate limit
+// TODO: specify rate limit in config
+// TODO: check msg max size
+
+export async function Connection(this: WS.Server, socket: WebSocket, request: IncomingMessage) {
+ try {
+ socket.on("close", onClose.bind(socket));
+ socket.on("message", onMessage.bind(socket));
+ console.log("[WebRTC] new connection", request.url);
+
+ if (process.env.WS_LOGEVENTS) {
+ [
+ "close",
+ "error",
+ "upgrade",
+ //"message",
+ "open",
+ "ping",
+ "pong",
+ "unexpected-response"
+ ].forEach((x) => {
+ socket.on(x, (y) => console.log("[WebRTC]", x, y));
+ });
+ }
+
+ const { searchParams } = new URL(`http://localhost${request.url}`);
+
+ socket.encoding = "json";
+ socket.version = Number(searchParams.get("v")) || 5;
+ if (socket.version < 3) return socket.close(CLOSECODES.Unknown_error, "invalid version");
+
+ setHeartbeat(socket);
+
+ socket.readyTimeout = setTimeout(() => {
+ return socket.close(CLOSECODES.Session_timed_out);
+ }, 1000 * 30);
+
+ await Send(socket, {
+ op: VoiceOPCodes.HELLO,
+ d: {
+ heartbeat_interval: 1000 * 30
+ }
+ });
+ } catch (error) {
+ console.error("[WebRTC]", error);
+ return socket.close(CLOSECODES.Unknown_error);
+ }
+}
\ No newline at end of file
diff --git a/src/webrtc/events/Message.ts b/src/webrtc/events/Message.ts
new file mode 100644
index 00000000..8f75a815
--- /dev/null
+++ b/src/webrtc/events/Message.ts
@@ -0,0 +1,38 @@
+import { CLOSECODES, Payload, WebSocket } from "@fosscord/gateway";
+import { Tuple } from "lambert-server";
+import OPCodeHandlers from "../opcodes";
+import { VoiceOPCodes } from "../util";
+
+const PayloadSchema = {
+ op: Number,
+ $d: new Tuple(Object, Number), // or number for heartbeat sequence
+ $s: Number,
+ $t: String
+};
+
+export async function onMessage(this: WebSocket, buffer: Buffer) {
+ try {
+ var data: Payload = JSON.parse(buffer.toString());
+ if (data.op !== VoiceOPCodes.IDENTIFY && !this.user_id) return this.close(CLOSECODES.Not_authenticated);
+
+ // @ts-ignore
+ const OPCodeHandler = OPCodeHandlers[data.op];
+ if (!OPCodeHandler) {
+ // @ts-ignore
+ console.error("[WebRTC] Unkown opcode " + VoiceOPCodes[data.op]);
+ // TODO: if all opcodes are implemented comment this out:
+ // this.close(CloseCodes.Unknown_opcode);
+ return;
+ }
+
+ if (![VoiceOPCodes.HEARTBEAT, VoiceOPCodes.SPEAKING].includes(data.op as VoiceOPCodes)) {
+ // @ts-ignore
+ console.log("[WebRTC] Opcode " + VoiceOPCodes[data.op]);
+ }
+
+ return await OPCodeHandler.call(this, data);
+ } catch (error) {
+ console.error("[WebRTC] error", error);
+ // if (!this.CLOSED && this.CLOSING) return this.close(CloseCodes.Unknown_error);
+ }
+}
\ No newline at end of file
diff --git a/src/webrtc/index.ts b/src/webrtc/index.ts
new file mode 100644
index 00000000..7cecc9b6
--- /dev/null
+++ b/src/webrtc/index.ts
@@ -0,0 +1,2 @@
+export * from "./Server";
+export * from "./util/index";
\ No newline at end of file
diff --git a/src/webrtc/opcodes/BackendVersion.ts b/src/webrtc/opcodes/BackendVersion.ts
new file mode 100644
index 00000000..b4b61c7d
--- /dev/null
+++ b/src/webrtc/opcodes/BackendVersion.ts
@@ -0,0 +1,6 @@
+import { Payload, Send, WebSocket } from "@fosscord/gateway";
+import { VoiceOPCodes } from "../util";
+
+export async function onBackendVersion(this: WebSocket, data: Payload) {
+ await Send(this, { op: VoiceOPCodes.VOICE_BACKEND_VERSION, d: { voice: "0.8.43", rtc_worker: "0.3.26" } });
+}
\ No newline at end of file
diff --git a/src/webrtc/opcodes/Heartbeat.ts b/src/webrtc/opcodes/Heartbeat.ts
new file mode 100644
index 00000000..1b6c5bcd
--- /dev/null
+++ b/src/webrtc/opcodes/Heartbeat.ts
@@ -0,0 +1,9 @@
+import { CLOSECODES, Payload, Send, setHeartbeat, WebSocket } from "@fosscord/gateway";
+import { VoiceOPCodes } from "../util";
+
+export async function onHeartbeat(this: WebSocket, data: Payload) {
+ setHeartbeat(this);
+ if (isNaN(data.d)) return this.close(CLOSECODES.Decode_error);
+
+ await Send(this, { op: VoiceOPCodes.HEARTBEAT_ACK, d: data.d });
+}
\ No newline at end of file
diff --git a/src/webrtc/opcodes/Identify.ts b/src/webrtc/opcodes/Identify.ts
new file mode 100644
index 00000000..19a575ab
--- /dev/null
+++ b/src/webrtc/opcodes/Identify.ts
@@ -0,0 +1,60 @@
+import { CLOSECODES, Payload, Send, WebSocket } from "@fosscord/gateway";
+import { validateSchema, VoiceIdentifySchema, VoiceState } from "@fosscord/util";
+import { endpoint, getClients, VoiceOPCodes, PublicIP } from "@fosscord/webrtc";
+import SemanticSDP from "semantic-sdp";
+const defaultSDP = require("./sdp.json");
+
+export async function onIdentify(this: WebSocket, data: Payload) {
+ clearTimeout(this.readyTimeout);
+ const { server_id, user_id, session_id, token, streams, video } = validateSchema("VoiceIdentifySchema", data.d) as VoiceIdentifySchema;
+
+ const voiceState = await VoiceState.findOne({ where: { guild_id: server_id, user_id, token, session_id } });
+ if (!voiceState) return this.close(CLOSECODES.Authentication_failed);
+
+ this.user_id = user_id;
+ this.session_id = session_id;
+ const sdp = SemanticSDP.SDPInfo.expand(defaultSDP);
+ sdp.setDTLS(SemanticSDP.DTLSInfo.expand({ setup: "actpass", hash: "sha-256", fingerprint: endpoint.getDTLSFingerprint() }));
+
+ this.client = {
+ websocket: this,
+ out: {
+ tracks: new Map()
+ },
+ in: {
+ audio_ssrc: 0,
+ video_ssrc: 0,
+ rtx_ssrc: 0
+ },
+ sdp,
+ channel_id: voiceState.channel_id
+ };
+
+ const clients = getClients(voiceState.channel_id)!;
+ clients.add(this.client);
+
+ this.on("close", () => {
+ clients.delete(this.client!);
+ });
+
+ await Send(this, {
+ op: VoiceOPCodes.READY,
+ d: {
+ streams: [
+ // { type: "video", ssrc: this.ssrc + 1, rtx_ssrc: this.ssrc + 2, rid: "100", quality: 100, active: false }
+ ],
+ ssrc: -1,
+ port: endpoint.getLocalPort(),
+ modes: [
+ "aead_aes256_gcm_rtpsize",
+ "aead_aes256_gcm",
+ "xsalsa20_poly1305_lite_rtpsize",
+ "xsalsa20_poly1305_lite",
+ "xsalsa20_poly1305_suffix",
+ "xsalsa20_poly1305"
+ ],
+ ip: PublicIP,
+ experiments: []
+ }
+ });
+}
\ No newline at end of file
diff --git a/src/webrtc/opcodes/SelectProtocol.ts b/src/webrtc/opcodes/SelectProtocol.ts
new file mode 100644
index 00000000..a3579b34
--- /dev/null
+++ b/src/webrtc/opcodes/SelectProtocol.ts
@@ -0,0 +1,46 @@
+import { Payload, Send, WebSocket } from "@fosscord/gateway";
+import { SelectProtocolSchema, validateSchema } from "@fosscord/util";
+import { endpoint, PublicIP, VoiceOPCodes } from "@fosscord/webrtc";
+import SemanticSDP, { MediaInfo, SDPInfo } from "semantic-sdp";
+
+export async function onSelectProtocol(this: WebSocket, payload: Payload) {
+ if (!this.client) return;
+
+ const data = validateSchema("SelectProtocolSchema", payload.d) as SelectProtocolSchema;
+
+ const offer = SemanticSDP.SDPInfo.parse("m=audio\n" + data.sdp!);
+ this.client.sdp!.setICE(offer.getICE());
+ this.client.sdp!.setDTLS(offer.getDTLS());
+
+ const transport = endpoint.createTransport(this.client.sdp!);
+ this.client.transport = transport;
+ transport.setRemoteProperties(this.client.sdp!);
+ transport.setLocalProperties(this.client.sdp!);
+
+ const dtls = transport.getLocalDTLSInfo();
+ const ice = transport.getLocalICEInfo();
+ const port = endpoint.getLocalPort();
+ const fingerprint = dtls.getHash() + " " + dtls.getFingerprint();
+ const candidates = transport.getLocalCandidates();
+ const candidate = candidates[0];
+
+ const answer =
+ `m=audio ${port} ICE/SDP`
+ + `a=fingerprint:${fingerprint}`
+ + `c=IN IP4 ${PublicIP}`
+ + `a=rtcp:${port}`
+ + `a=ice-ufrag:${ice.getUfrag()}`
+ + `a=ice-pwd:${ice.getPwd()}`
+ + `a=fingerprint:${fingerprint}`
+ + `a=candidate:1 1 ${candidate.getTransport()} ${candidate.getFoundation()} ${candidate.getAddress()} ${candidate.getPort()} typ host`;
+
+ await Send(this, {
+ op: VoiceOPCodes.SELECT_PROTOCOL_ACK,
+ d: {
+ video_codec: "H264",
+ sdp: answer,
+ media_session_id: this.session_id,
+ audio_codec: "opus"
+ }
+ });
+}
\ No newline at end of file
diff --git a/src/webrtc/opcodes/Speaking.ts b/src/webrtc/opcodes/Speaking.ts
new file mode 100644
index 00000000..e2227040
--- /dev/null
+++ b/src/webrtc/opcodes/Speaking.ts
@@ -0,0 +1,22 @@
+import { Payload, Send, WebSocket } from "@fosscord/gateway";
+import { getClients, VoiceOPCodes } from "../util";
+
+// {"speaking":1,"delay":5,"ssrc":2805246727}
+
+export async function onSpeaking(this: WebSocket, data: Payload) {
+ if (!this.client) return;
+
+ getClients(this.client.channel_id).forEach((client) => {
+ if (client === this.client) return;
+ const ssrc = this.client!.out.tracks.get(client.websocket.user_id);
+
+ Send(client.websocket, {
+ op: VoiceOPCodes.SPEAKING,
+ d: {
+ user_id: client.websocket.user_id,
+ speaking: data.d.speaking,
+ ssrc: ssrc?.audio_ssrc || 0
+ }
+ });
+ });
+}
\ No newline at end of file
diff --git a/src/webrtc/opcodes/Video.ts b/src/webrtc/opcodes/Video.ts
new file mode 100644
index 00000000..ff20d5a9
--- /dev/null
+++ b/src/webrtc/opcodes/Video.ts
@@ -0,0 +1,118 @@
+import { Payload, Send, WebSocket } from "@fosscord/gateway";
+import { validateSchema, VoiceVideoSchema } from "@fosscord/util";
+import { channels, getClients, VoiceOPCodes } from "@fosscord/webrtc";
+import { IncomingStreamTrack, SSRCs } from "medooze-media-server";
+import SemanticSDP from "semantic-sdp";
+
+export async function onVideo(this: WebSocket, payload: Payload) {
+ if (!this.client) return;
+ const { transport, channel_id } = this.client;
+ if (!transport) return;
+ const d = validateSchema("VoiceVideoSchema", payload.d) as VoiceVideoSchema;
+
+ await Send(this, { op: VoiceOPCodes.MEDIA_SINK_WANTS, d: { any: 100 } });
+
+ const id = "stream" + this.user_id;
+
+ var stream = this.client.in.stream!;
+ if (!stream) {
+ stream = this.client.transport!.createIncomingStream(
+ // @ts-ignore
+ SemanticSDP.StreamInfo.expand({
+ id,
+ // @ts-ignore
+ tracks: []
+ })
+ );
+ this.client.in.stream = stream;
+
+ const interval = setInterval(() => {
+ for (const track of stream.getTracks()) {
+ for (const layer of Object.values(track.getStats())) {
+ console.log(track.getId(), layer.total);
+ }
+ }
+ }, 5000);
+
+ stream.on("stopped", () => {
+ console.log("stream stopped");
+ clearInterval(interval);
+ });
+ this.on("close", () => {
+ transport!.stop();
+ });
+ const out = transport.createOutgoingStream(
+ // @ts-ignore
+ SemanticSDP.StreamInfo.expand({
+ id: "out" + this.user_id,
+ // @ts-ignore
+ tracks: []
+ })
+ );
+ this.client.out.stream = out;
+
+ const clients = channels.get(channel_id)!;
+
+ clients.forEach((client) => {
+ if (client.websocket.user_id === this.user_id) return;
+ if (!client.in.stream) return;
+
+ client.in.stream?.getTracks().forEach((track) => {
+ attachTrack.call(this, track, client.websocket.user_id);
+ });
+ });
+ }
+
+ if (d.audio_ssrc) {
+ handleSSRC.call(this, "audio", { media: d.audio_ssrc, rtx: d.audio_ssrc + 1 });
+ }
+ if (d.video_ssrc && d.rtx_ssrc) {
+ handleSSRC.call(this, "video", { media: d.video_ssrc, rtx: d.rtx_ssrc });
+ }
+}
+
+function attachTrack(this: WebSocket, track: IncomingStreamTrack, user_id: string) {
+ if (!this.client) return;
+ const outTrack = this.client.transport!.createOutgoingStreamTrack(track.getMedia());
+ outTrack.attachTo(track);
+ this.client.out.stream!.addTrack(outTrack);
+ var ssrcs = this.client.out.tracks.get(user_id)!;
+ if (!ssrcs) ssrcs = this.client.out.tracks.set(user_id, { audio_ssrc: 0, rtx_ssrc: 0, video_ssrc: 0 }).get(user_id)!;
+
+ if (track.getMedia() === "audio") {
+ ssrcs.audio_ssrc = outTrack.getSSRCs().media!;
+ } else if (track.getMedia() === "video") {
+ ssrcs.video_ssrc = outTrack.getSSRCs().media!;
+ ssrcs.rtx_ssrc = outTrack.getSSRCs().rtx!;
+ }
+
+ Send(this, {
+ op: VoiceOPCodes.VIDEO,
+ d: {
+ user_id: user_id,
+ ...ssrcs
+ } as VoiceVideoSchema
+ });
+}
+
+function handleSSRC(this: WebSocket, type: "audio" | "video", ssrcs: SSRCs) {
+ if (!this.client) return;
+ const stream = this.client.in.stream!;
+ const transport = this.client.transport!;
+
+ const id = type + ssrcs.media;
+ var track = stream.getTrack(id);
+ if (!track) {
+ console.log("createIncomingStreamTrack", id);
+ track = transport.createIncomingStreamTrack(type, { id, ssrcs });
+ stream.addTrack(track);
+
+ const clients = getClients(this.client.channel_id)!;
+ clients.forEach((client) => {
+ if (client.websocket.user_id === this.user_id) return;
+ if (!client.out.stream) return;
+
+ attachTrack.call(this, track, client.websocket.user_id);
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/webrtc/opcodes/index.ts b/src/webrtc/opcodes/index.ts
new file mode 100644
index 00000000..8c664cce
--- /dev/null
+++ b/src/webrtc/opcodes/index.ts
@@ -0,0 +1,19 @@
+import { Payload, WebSocket } from "@fosscord/gateway";
+import { VoiceOPCodes } from "../util";
+import { onBackendVersion } from "./BackendVersion";
+import { onHeartbeat } from "./Heartbeat";
+import { onIdentify } from "./Identify";
+import { onSelectProtocol } from "./SelectProtocol";
+import { onSpeaking } from "./Speaking";
+import { onVideo } from "./Video";
+
+export type OPCodeHandler = (this: WebSocket, data: Payload) => any;
+
+export default {
+ [VoiceOPCodes.HEARTBEAT]: onHeartbeat,
+ [VoiceOPCodes.IDENTIFY]: onIdentify,
+ [VoiceOPCodes.VOICE_BACKEND_VERSION]: onBackendVersion,
+ [VoiceOPCodes.VIDEO]: onVideo,
+ [VoiceOPCodes.SPEAKING]: onSpeaking,
+ [VoiceOPCodes.SELECT_PROTOCOL]: onSelectProtocol
+};
\ No newline at end of file
diff --git a/src/webrtc/opcodes/sdp.json b/src/webrtc/opcodes/sdp.json
new file mode 100644
index 00000000..4867b9c7
--- /dev/null
+++ b/src/webrtc/opcodes/sdp.json
@@ -0,0 +1,420 @@
+{
+ "version": 0,
+ "streams": [],
+ "medias": [
+ {
+ "id": "0",
+ "type": "audio",
+ "direction": "sendrecv",
+ "codecs": [
+ {
+ "codec": "opus",
+ "type": 111,
+ "channels": 2,
+ "params": {
+ "minptime": "10",
+ "useinbandfec": "1"
+ },
+ "rtcpfbs": [
+ {
+ "id": "transport-cc"
+ }
+ ]
+ }
+ ],
+ "extensions": {
+ "1": "urn:ietf:params:rtp-hdrext:ssrc-audio-level",
+ "2": "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time",
+ "3": "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01",
+ "4": "urn:ietf:params:rtp-hdrext:sdes:mid"
+ }
+ },
+ {
+ "id": "1",
+ "type": "video",
+ "direction": "sendrecv",
+ "codecs": [
+ {
+ "codec": "VP8",
+ "type": 96,
+ "rtx": 97,
+ "rtcpfbs": [
+ {
+ "id": "goog-remb"
+ },
+ {
+ "id": "transport-cc"
+ },
+ {
+ "id": "ccm",
+ "params": ["fir"]
+ },
+ {
+ "id": "nack"
+ },
+ {
+ "id": "nack",
+ "params": ["pli"]
+ }
+ ]
+ },
+ {
+ "codec": "VP9",
+ "type": 98,
+ "rtx": 99,
+ "params": {
+ "profile-id": "0"
+ },
+ "rtcpfbs": [
+ {
+ "id": "goog-remb"
+ },
+ {
+ "id": "transport-cc"
+ },
+ {
+ "id": "ccm",
+ "params": ["fir"]
+ },
+ {
+ "id": "nack"
+ },
+ {
+ "id": "nack",
+ "params": ["pli"]
+ }
+ ]
+ },
+ {
+ "codec": "VP9",
+ "type": 100,
+ "rtx": 101,
+ "params": {
+ "profile-id": "2"
+ },
+ "rtcpfbs": [
+ {
+ "id": "goog-remb"
+ },
+ {
+ "id": "transport-cc"
+ },
+ {
+ "id": "ccm",
+ "params": ["fir"]
+ },
+ {
+ "id": "nack"
+ },
+ {
+ "id": "nack",
+ "params": ["pli"]
+ }
+ ]
+ },
+ {
+ "codec": "VP9",
+ "type": 102,
+ "rtx": 122,
+ "params": {
+ "profile-id": "1"
+ },
+ "rtcpfbs": [
+ {
+ "id": "goog-remb"
+ },
+ {
+ "id": "transport-cc"
+ },
+ {
+ "id": "ccm",
+ "params": ["fir"]
+ },
+ {
+ "id": "nack"
+ },
+ {
+ "id": "nack",
+ "params": ["pli"]
+ }
+ ]
+ },
+ {
+ "codec": "H264",
+ "type": 127,
+ "rtx": 121,
+ "params": {
+ "level-asymmetry-allowed": "1",
+ "packetization-mode": "1",
+ "profile-level-id": "42001f"
+ },
+ "rtcpfbs": [
+ {
+ "id": "goog-remb"
+ },
+ {
+ "id": "transport-cc"
+ },
+ {
+ "id": "ccm",
+ "params": ["fir"]
+ },
+ {
+ "id": "nack"
+ },
+ {
+ "id": "nack",
+ "params": ["pli"]
+ }
+ ]
+ },
+ {
+ "codec": "H264",
+ "type": 125,
+ "rtx": 107,
+ "params": {
+ "level-asymmetry-allowed": "1",
+ "packetization-mode": "0",
+ "profile-level-id": "42001f"
+ },
+ "rtcpfbs": [
+ {
+ "id": "goog-remb"
+ },
+ {
+ "id": "transport-cc"
+ },
+ {
+ "id": "ccm",
+ "params": ["fir"]
+ },
+ {
+ "id": "nack"
+ },
+ {
+ "id": "nack",
+ "params": ["pli"]
+ }
+ ]
+ },
+ {
+ "codec": "H264",
+ "type": 108,
+ "rtx": 109,
+ "params": {
+ "level-asymmetry-allowed": "1",
+ "packetization-mode": "1",
+ "profile-level-id": "42e01f"
+ },
+ "rtcpfbs": [
+ {
+ "id": "goog-remb"
+ },
+ {
+ "id": "transport-cc"
+ },
+ {
+ "id": "ccm",
+ "params": ["fir"]
+ },
+ {
+ "id": "nack"
+ },
+ {
+ "id": "nack",
+ "params": ["pli"]
+ }
+ ]
+ },
+ {
+ "codec": "H264",
+ "type": 124,
+ "rtx": 120,
+ "params": {
+ "level-asymmetry-allowed": "1",
+ "packetization-mode": "0",
+ "profile-level-id": "42e01f"
+ },
+ "rtcpfbs": [
+ {
+ "id": "goog-remb"
+ },
+ {
+ "id": "transport-cc"
+ },
+ {
+ "id": "ccm",
+ "params": ["fir"]
+ },
+ {
+ "id": "nack"
+ },
+ {
+ "id": "nack",
+ "params": ["pli"]
+ }
+ ]
+ },
+ {
+ "codec": "H264",
+ "type": 123,
+ "rtx": 119,
+ "params": {
+ "level-asymmetry-allowed": "1",
+ "packetization-mode": "1",
+ "profile-level-id": "4d001f"
+ },
+ "rtcpfbs": [
+ {
+ "id": "goog-remb"
+ },
+ {
+ "id": "transport-cc"
+ },
+ {
+ "id": "ccm",
+ "params": ["fir"]
+ },
+ {
+ "id": "nack"
+ },
+ {
+ "id": "nack",
+ "params": ["pli"]
+ }
+ ]
+ },
+ {
+ "codec": "H264",
+ "type": 35,
+ "rtx": 36,
+ "params": {
+ "level-asymmetry-allowed": "1",
+ "packetization-mode": "0",
+ "profile-level-id": "4d001f"
+ },
+ "rtcpfbs": [
+ {
+ "id": "goog-remb"
+ },
+ {
+ "id": "transport-cc"
+ },
+ {
+ "id": "ccm",
+ "params": ["fir"]
+ },
+ {
+ "id": "nack"
+ },
+ {
+ "id": "nack",
+ "params": ["pli"]
+ }
+ ]
+ },
+ {
+ "codec": "H264",
+ "type": 37,
+ "rtx": 38,
+ "params": {
+ "level-asymmetry-allowed": "1",
+ "packetization-mode": "1",
+ "profile-level-id": "f4001f"
+ },
+ "rtcpfbs": [
+ {
+ "id": "goog-remb"
+ },
+ {
+ "id": "transport-cc"
+ },
+ {
+ "id": "ccm",
+ "params": ["fir"]
+ },
+ {
+ "id": "nack"
+ },
+ {
+ "id": "nack",
+ "params": ["pli"]
+ }
+ ]
+ },
+ {
+ "codec": "H264",
+ "type": 39,
+ "rtx": 40,
+ "params": {
+ "level-asymmetry-allowed": "1",
+ "packetization-mode": "0",
+ "profile-level-id": "f4001f"
+ },
+ "rtcpfbs": [
+ {
+ "id": "goog-remb"
+ },
+ {
+ "id": "transport-cc"
+ },
+ {
+ "id": "ccm",
+ "params": ["fir"]
+ },
+ {
+ "id": "nack"
+ },
+ {
+ "id": "nack",
+ "params": ["pli"]
+ }
+ ]
+ },
+ {
+ "codec": "H264",
+ "type": 114,
+ "rtx": 115,
+ "params": {
+ "level-asymmetry-allowed": "1",
+ "packetization-mode": "1",
+ "profile-level-id": "64001f"
+ },
+ "rtcpfbs": [
+ {
+ "id": "goog-remb"
+ },
+ {
+ "id": "transport-cc"
+ },
+ {
+ "id": "ccm",
+ "params": ["fir"]
+ },
+ {
+ "id": "nack"
+ },
+ {
+ "id": "nack",
+ "params": ["pli"]
+ }
+ ]
+ }
+ ],
+ "extensions": {
+ "2": "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time",
+ "3": "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01",
+ "4": "urn:ietf:params:rtp-hdrext:sdes:mid",
+ "5": "http://www.webrtc.org/experiments/rtp-hdrext/playout-delay",
+ "6": "http://www.webrtc.org/experiments/rtp-hdrext/video-content-type",
+ "7": "http://www.webrtc.org/experiments/rtp-hdrext/video-timing",
+ "8": "http://www.webrtc.org/experiments/rtp-hdrext/color-space",
+ "10": "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id",
+ "11": "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id",
+ "13": "urn:3gpp:video-orientation",
+ "14": "urn:ietf:params:rtp-hdrext:toffset"
+ }
+ }
+ ],
+ "candidates": []
+}
\ No newline at end of file
diff --git a/src/webrtc/start.ts b/src/webrtc/start.ts
new file mode 100644
index 00000000..9a5f38ee
--- /dev/null
+++ b/src/webrtc/start.ts
@@ -0,0 +1,13 @@
+process.on("uncaughtException", console.error);
+process.on("unhandledRejection", console.error);
+
+import { config } from "dotenv";
+import { Server } from "./Server";
+config();
+
+const port = Number(process.env.PORT) || 3004;
+
+const server = new Server({
+ port
+});
+server.start();
\ No newline at end of file
diff --git a/src/webrtc/util/Constants.ts b/src/webrtc/util/Constants.ts
new file mode 100644
index 00000000..64d78e22
--- /dev/null
+++ b/src/webrtc/util/Constants.ts
@@ -0,0 +1,26 @@
+export enum VoiceStatus {
+ CONNECTED = 0,
+ CONNECTING = 1,
+ AUTHENTICATING = 2,
+ RECONNECTING = 3,
+ DISCONNECTED = 4
+}
+
+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,
+ VIDEO = 12,
+ CLIENT_DISCONNECT = 13,
+ SESSION_UPDATE = 14,
+ MEDIA_SINK_WANTS = 15,
+ VOICE_BACKEND_VERSION = 16,
+ CHANNEL_OPTIONS_UPDATE = 17
+}
\ No newline at end of file
diff --git a/src/webrtc/util/MediaServer.ts b/src/webrtc/util/MediaServer.ts
new file mode 100644
index 00000000..93230c91
--- /dev/null
+++ b/src/webrtc/util/MediaServer.ts
@@ -0,0 +1,51 @@
+import { WebSocket } from "@fosscord/gateway";
+import MediaServer, { IncomingStream, OutgoingStream, Transport } from "medooze-media-server";
+import SemanticSDP from "semantic-sdp";
+MediaServer.enableLog(true);
+
+export const PublicIP = process.env.PUBLIC_IP || "127.0.0.1";
+
+try {
+ const range = process.env.WEBRTC_PORT_RANGE || "4000";
+ var ports = range.split("-");
+ const min = Number(ports[0]);
+ const max = Number(ports[1]);
+
+ MediaServer.setPortRange(min, max);
+} catch (error) {
+ console.error("Invalid env var: WEBRTC_PORT_RANGE", process.env.WEBRTC_PORT_RANGE, error);
+ process.exit(1);
+}
+
+export const endpoint = MediaServer.createEndpoint(PublicIP);
+
+export const channels = new Map<string, Set<Client>>();
+
+export interface Client {
+ transport?: Transport;
+ websocket: WebSocket;
+ out: {
+ stream?: OutgoingStream;
+ tracks: Map<
+ string,
+ {
+ audio_ssrc: number;
+ video_ssrc: number;
+ rtx_ssrc: number;
+ }
+ >;
+ };
+ in: {
+ stream?: IncomingStream;
+ audio_ssrc: number;
+ video_ssrc: number;
+ rtx_ssrc: number;
+ };
+ sdp: SemanticSDP.SDPInfo;
+ channel_id: string;
+}
+
+export function getClients(channel_id: string) {
+ if (!channels.has(channel_id)) channels.set(channel_id, new Set());
+ return channels.get(channel_id)!;
+}
\ No newline at end of file
diff --git a/src/webrtc/util/index.ts b/src/webrtc/util/index.ts
new file mode 100644
index 00000000..2e09bc48
--- /dev/null
+++ b/src/webrtc/util/index.ts
@@ -0,0 +1,2 @@
+export * from "./Constants";
+export * from "./MediaServer";
\ No newline at end of file
|