diff options
author | Madeline <46743919+MaddyUnderStars@users.noreply.github.com> | 2022-09-25 18:24:21 +1000 |
---|---|---|
committer | Madeline <46743919+MaddyUnderStars@users.noreply.github.com> | 2022-09-25 23:35:18 +1000 |
commit | f44f5d7ac2d24ff836c2e1d4b2fa58da04b13052 (patch) | |
tree | a6655c41bb3db79c30fd876b06ee60fe9cf70c9b /src/webrtc | |
parent | Allow edited_timestamp to passthrough in handleMessage (diff) | |
download | server-f44f5d7ac2d24ff836c2e1d4b2fa58da04b13052.tar.xz |
Refactor to mono-repo + upgrade packages
Diffstat (limited to 'src/webrtc')
-rw-r--r-- | src/webrtc/.DS_Store | bin | 0 -> 6148 bytes | |||
-rw-r--r-- | src/webrtc/Server.ts | 56 | ||||
-rw-r--r-- | src/webrtc/events/Close.ts | 9 | ||||
-rw-r--r-- | src/webrtc/events/Connection.ts | 60 | ||||
-rw-r--r-- | src/webrtc/events/Message.ts | 38 | ||||
-rw-r--r-- | src/webrtc/index.ts | 2 | ||||
-rw-r--r-- | src/webrtc/opcodes/BackendVersion.ts | 6 | ||||
-rw-r--r-- | src/webrtc/opcodes/Heartbeat.ts | 9 | ||||
-rw-r--r-- | src/webrtc/opcodes/Identify.ts | 60 | ||||
-rw-r--r-- | src/webrtc/opcodes/SelectProtocol.ts | 46 | ||||
-rw-r--r-- | src/webrtc/opcodes/Speaking.ts | 22 | ||||
-rw-r--r-- | src/webrtc/opcodes/Video.ts | 118 | ||||
-rw-r--r-- | src/webrtc/opcodes/index.ts | 19 | ||||
-rw-r--r-- | src/webrtc/opcodes/sdp.json | 420 | ||||
-rw-r--r-- | src/webrtc/start.ts | 13 | ||||
-rw-r--r-- | src/webrtc/util/Constants.ts | 26 | ||||
-rw-r--r-- | src/webrtc/util/MediaServer.ts | 51 | ||||
-rw-r--r-- | src/webrtc/util/index.ts | 2 |
18 files changed, 957 insertions, 0 deletions
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 |