diff options
author | MetaMuffin <metamuffin@yandex.com> | 2021-08-04 21:00:34 +0200 |
---|---|---|
committer | MetaMuffin <metamuffin@yandex.com> | 2021-08-04 21:00:34 +0200 |
commit | a8d6fa97be777ab08f09252dc4864ba241b17687 (patch) | |
tree | af9955f59049c87100b85669eb90368b607e21d6 | |
parent | 6d4a9c797edf2bbb75cea6afef109cb457c52641 (diff) | |
download | keks-meet-a8d6fa97be777ab08f09252dc4864ba241b17687.tar keks-meet-a8d6fa97be777ab08f09252dc4864ba241b17687.tar.bz2 keks-meet-a8d6fa97be777ab08f09252dc4864ba241b17687.tar.zst |
a
-rw-r--r-- | public/index.html | 10 | ||||
-rw-r--r-- | source/client/index.ts | 192 | ||||
-rw-r--r-- | source/client/room.ts | 53 | ||||
-rw-r--r-- | source/client/types.ts | 13 | ||||
-rw-r--r-- | source/client/user.ts | 60 | ||||
-rw-r--r-- | source/models.ts | 11 | ||||
-rw-r--r-- | source/server/index.ts | 118 |
7 files changed, 277 insertions, 180 deletions
diff --git a/public/index.html b/public/index.html index 92adc49..c6cb1fe 100644 --- a/public/index.html +++ b/public/index.html @@ -6,7 +6,7 @@ <script src="/scripts/bundle.js"></script> <link rel="stylesheet" href="/static/css/master.css" /> - <title>webrtc test</title> + <title>webrtc meeting</title> <style> * { margin: 0px; @@ -16,13 +16,5 @@ </head> <body> - <form action="/" method="get"> - <input type="text" name="offer" placeholder="offer id" /> - <input type="submit" value="offer" /> - </form> - <form action="/" method="get"> - <input type="text" name="answer" placeholder="answer id" /> - <input type="submit" value="answer" /> - </form> </body> </html> diff --git a/source/client/index.ts b/source/client/index.ts index 3841c8a..87421f2 100644 --- a/source/client/index.ts +++ b/source/client/index.ts @@ -1,130 +1,130 @@ +import { Room } from "./room" export const servers = { - iceServers: [ - { - urls: ["stun:stun1.l.google.com:19302", "stun:stun2.l.google.com:19302"] - } - ], + iceServers: [{ urls: ["stun:stun1.l.google.com:19302", "stun:stun2.l.google.com:19302"] }], iceCandidatePoolSize: 10, } -let remote_stream: MediaStream, local_stream: MediaStream; -let pc: RTCPeerConnection; +export interface User { + peer: RTCPeerConnection + stream: MediaStream, +} -window.onload = async () => { +export const users: Map<string, User> = new Map() - if (window.location.search.startsWith("?offer=")) { - await setup_webrtc() - await offer(window.location.search.substr("?offer=".length)) - } else if (window.location.search.startsWith("?answer=")) { - await setup_webrtc() - await answer(window.location.search.substr("?answer=".length)) - } else { +window.onload = async () => { + if (window.location.pathname.startsWith("/room/")) { + const room_name = window.location.pathname.substr("/room/".length) + let room = new Room(room_name) + document.body.append(room.el) + } else { + //TODO show ui for joining rooms } - } -async function setup_webrtc() { - document.body.innerHTML = "" - pc = new RTCPeerConnection(servers) - local_stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }) - remote_stream = new MediaStream() +// async function setup_webrtc() { +// document.body.innerHTML = "" - local_stream.getTracks().forEach(t => pc.addTrack(t, local_stream)) +// pc = new RTCPeerConnection(servers) - pc.ontrack = ev => { - console.log("peer got remote tracks", ev.streams); - ev.streams[0].getTracks().forEach(t => remote_stream.addTrack(t)) - } +// local_stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }) +// remote_stream = new MediaStream() +// local_stream.getTracks().forEach(t => pc.addTrack(t, local_stream)) - const ls_el = document.createElement("video") - const rs_el = document.createElement("video") - ls_el.muted = true - ls_el.autoplay = rs_el.autoplay = true - ls_el.setAttribute("playsinline", "1") - rs_el.setAttribute("playsinline", "1") - ls_el.srcObject = local_stream - rs_el.srcObject = remote_stream +// pc.ontrack = ev => { +// console.log("peer got remote tracks", ev.streams); +// ev.streams[0].getTracks().forEach(t => remote_stream.addTrack(t)) +// } - document.body.append(ls_el, rs_el) -} +// const ls_el = document.createElement("video") +// const rs_el = document.createElement("video") +// ls_el.muted = true +// ls_el.autoplay = rs_el.autoplay = true +// ls_el.setAttribute("playsinline", "1") +// rs_el.setAttribute("playsinline", "1") +// ls_el.srcObject = local_stream +// rs_el.srcObject = remote_stream -interface Offer { - sdp: any, - type: any -} +// document.body.append(ls_el, rs_el) -async function offer(id: string) { - const ws = new WebSocket(`ws://${window.location.host}/offer/${id}`) - ws.onclose = ev => console.log("websocket closed: " + ev.reason); - await new Promise<void>(r => ws.onopen = () => r()) +// } - console.log("websocket opened") +// interface Offer { +// sdp: any, +// type: any +// } - pc.onicecandidate = ev => { - const candidate = ev.candidate?.toJSON() - if (!candidate) return - ws.send(JSON.stringify({ candidate })) - console.log("sent ice candidate", ev.candidate); - } +// async function offer(id: string) { +// const ws = new WebSocket(`ws://${window.location.host}/offer/${id}`) +// ws.onclose = ev => console.log("websocket closed: " + ev.reason); +// await new Promise<void>(r => ws.onopen = () => r()) - const offer_description = await pc.createOffer() - await pc.setLocalDescription(offer_description); +// console.log("websocket opened") - const offer: Offer = { sdp: offer_description.sdp, type: offer_description.type }; +// pc.onicecandidate = ev => { +// const candidate = ev.candidate?.toJSON() +// if (!candidate) return +// ws.send(JSON.stringify({ candidate })) +// console.log("sent ice candidate", ev.candidate); +// } - ws.send(JSON.stringify({ offer })) +// const offer_description = await pc.createOffer() +// await pc.setLocalDescription(offer_description); - ws.onmessage = ev => { - const s = JSON.parse(ev.data) - if (s.answer) { - console.log("got answer", s.answer); - const answer_description = new RTCSessionDescription(s.answer) - pc.setRemoteDescription(answer_description) - } - if (s.candidate) { - console.log("got candidate", s.candidate); - const candidate = new RTCIceCandidate(s.candidate) - pc.addIceCandidate(candidate) - } - } +// const offer: Offer = { sdp: offer_description.sdp, type: offer_description.type }; -} +// ws.send(JSON.stringify({ offer })) -async function answer(id: string) { - const ws = new WebSocket(`ws://${window.location.host}/answer/${id}`) - ws.onclose = ev => console.log("websocket closed: " + ev.reason); - await new Promise<void>(r => ws.onopen = () => r()) - console.log("websocket opened"); +// ws.onmessage = ev => { +// const s = JSON.parse(ev.data) +// if (s.answer) { +// console.log("got answer", s.answer); +// const answer_description = new RTCSessionDescription(s.answer) +// pc.setRemoteDescription(answer_description) +// } +// if (s.candidate) { +// console.log("got candidate", s.candidate); +// const candidate = new RTCIceCandidate(s.candidate) +// pc.addIceCandidate(candidate) +// } +// } +// } - pc.onicecandidate = ev => { - const candidate = ev.candidate?.toJSON() - if (!candidate) return - ws.send(JSON.stringify({ candidate })) - console.log("sent ice candidate", candidate); - } +// async function answer(id: string) { +// const ws = new WebSocket(`ws://${window.location.host}/answer/${id}`) +// ws.onclose = ev => console.log("websocket closed: " + ev.reason); +// await new Promise<void>(r => ws.onopen = () => r()) +// console.log("websocket opened"); - ws.onmessage = async ev => { - const s = JSON.parse(ev.data) - if (s.offer) { - console.log("got offer", s.offer); - await pc.setRemoteDescription(new RTCSessionDescription(s.offer)) - const answer_description = await pc.createAnswer() - await pc.setLocalDescription(answer_description) +// pc.onicecandidate = ev => { +// const candidate = ev.candidate?.toJSON() +// if (!candidate) return +// ws.send(JSON.stringify({ candidate })) +// console.log("sent ice candidate", candidate); +// } - const answer: Offer = { type: answer_description.type, sdp: answer_description.sdp } - ws.send(JSON.stringify({ answer })) - } - if (s.candidate) { - console.log("got candidate", s.candidate); - pc.addIceCandidate(new RTCIceCandidate(s.candidate)) - } - } -} +// ws.onmessage = async ev => { +// const s = JSON.parse(ev.data) +// if (s.offer) { +// console.log("got offer", s.offer); +// await pc.setRemoteDescription(new RTCSessionDescription(s.offer)) + +// const answer_description = await pc.createAnswer() +// await pc.setLocalDescription(answer_description) + +// const answer: Offer = { type: answer_description.type, sdp: answer_description.sdp } +// ws.send(JSON.stringify({ answer })) +// } +// if (s.candidate) { +// console.log("got candidate", s.candidate); +// pc.addIceCandidate(new RTCIceCandidate(s.candidate)) +// } +// } +// } diff --git a/source/client/room.ts b/source/client/room.ts new file mode 100644 index 0000000..100f6cf --- /dev/null +++ b/source/client/room.ts @@ -0,0 +1,53 @@ +import { CSPacket, SCPacket} from "./types"; +import { User } from "./user"; + + +export class Room { + el: HTMLElement + name: string + users: Map<string, User> = new Map() + websocket: WebSocket + local_user: User + + constructor(name: string) { + this.name = name + this.el = document.createElement("div") + + this.websocket = new WebSocket(`ws://${window.location.host}/room/${encodeURIComponent(name)}`) + this.websocket.onclose = () => this.websocket_close() + this.websocket.onopen = () => this.websocket_open() + this.websocket.onmessage = (ev) => { + this.websocket_message(JSON.parse(ev.data)) + } + // const name = prompt() ?? "nameless user" + const uname = Math.random().toString() + this.local_user = new User(this, uname, true) + } + + websocket_send(data: CSPacket) { + this.websocket.send(JSON.stringify(data)) + } + websocket_message(packet: SCPacket) { + console.log("websocket message", packet); + if (packet.join) { + this.users.set(packet.sender, new User(this, packet.sender)) + return + } + const sender = this.users.get(packet.sender) + if (!sender) return console.warn(`unknown sender ${packet.sender}`) + if (packet.leave) { + sender.leave() + this.users.delete(packet.sender) + return + } + + if (packet.data.ice_candiate) sender.add_ice_candidate(packet.data.ice_candiate) + } + websocket_close() { + console.log("websocket closed"); + } + websocket_open() { + console.log("websocket opened"); + this.websocket.send(this.local_user.name) + } +}
\ No newline at end of file diff --git a/source/client/types.ts b/source/client/types.ts new file mode 100644 index 0000000..b235120 --- /dev/null +++ b/source/client/types.ts @@ -0,0 +1,13 @@ + +export interface SCPacket { + sender: string, + data: CSPacket, + join?: boolean, + leave?: boolean +} +export interface CSPacket { + receiver?: string + ice_candiate?: RTCIceCandidateInit +} + + diff --git a/source/client/user.ts b/source/client/user.ts new file mode 100644 index 0000000..fa60012 --- /dev/null +++ b/source/client/user.ts @@ -0,0 +1,60 @@ +import { Room } from "./room" + + + +export class User { + el: HTMLElement + el_video: HTMLVideoElement + + name: string + local: boolean + peer: RTCPeerConnection + room: Room + stream: MediaStream + + constructor(room: Room, name: string, local?: boolean) { + this.name = name + this.room = room + this.local = !!local + this.stream = new MediaStream() + this.el = document.createElement("div") + this.el_video = document.createElement("video") + this.el.append(this.el_video) + this.el_video.autoplay = true + this.el_video.muted = this.local + this.el_video.setAttribute("playsinline", "1") + + this.peer = new RTCPeerConnection() + this.peer.onicecandidate = ev => { + if (!ev.candidate) return + room.websocket_send({ ice_candiate: ev.candidate.toJSON(), receiver: this.name }) + console.log("sent rtc candidate", ev.candidate); + } + this.peer.ontrack = ev => { + console.log("got remote track", ev.streams); + ev.streams[0].getTracks().forEach(t => { + this.stream.addTrack(t) + }) + } + + if (this.local) this.get_local_media().then(stream => { + this.stream = stream + this.el_video.srcObject = stream + }) + + this.room.el.appendChild(this.el) + } + + async get_local_media(): Promise<MediaStream> { + return await navigator.mediaDevices.getUserMedia({ audio: true, video: true }) + } + + add_ice_candidate(candidate: RTCIceCandidateInit) { + this.peer.addIceCandidate(new RTCIceCandidate(candidate)) + } + + + leave() { + this.room.el.removeChild(this.el) + } +}
\ No newline at end of file diff --git a/source/models.ts b/source/models.ts deleted file mode 100644 index 45a3825..0000000 --- a/source/models.ts +++ /dev/null @@ -1,11 +0,0 @@ - -export interface CallDocModel { - offer: SessionModel - offer_candidates: CandidateModel[] - on_answer: (a: SessionModel) => void - on_offer_candidate: (a: CandidateModel) => void - on_answer_candidate: (a: CandidateModel) => void - answered: boolean -} -export type CandidateModel = any -export type SessionModel = any diff --git a/source/server/index.ts b/source/server/index.ts index bac2ffb..8af789b 100644 --- a/source/server/index.ts +++ b/source/server/index.ts @@ -6,21 +6,30 @@ import { existsSync, readFile, readFileSync } from "fs"; import http from "http" import https from "https" import expressWs from "express-ws"; -import { CallDocModel } from "../models"; +import { CSPacket, SCPacket } from "../client/types"; -const call_docs: Map<String, CallDocModel> = new Map() +type Room = Map<string, any> +const rooms: Map<string, Room> = new Map() +function ws_send(ws: any, data: string) { + try { ws.send(data) } + catch (e) { console.warn("i hate express-ws") } +} async function main() { const app_e = Express(); const app = expressWs(app_e).app - const webpackConfig = require('../../webpack.config'); - const compiler = Webpack(webpackConfig) - const devMiddleware = WebpackDevMiddleware(compiler, { - publicPath: webpackConfig.output.publicPath - }) - app.use("/scripts", devMiddleware) + if (process.env.PRODUCTION) { + app.use("/scripts", estatic(join(__dirname, "../../public/dist"))) + } else { + const webpackConfig = require('../../webpack.config'); + const compiler = Webpack(webpackConfig) + const devMiddleware = WebpackDevMiddleware(compiler, { + publicPath: webpackConfig.output.publicPath + }) + app.use("/scripts", devMiddleware) + } app.disable("x-powered-by"); app.use(json()); @@ -28,70 +37,51 @@ async function main() { app.get("/", (req, res) => { res.sendFile(join(__dirname, "../../public/index.html")); }); + app.get("/room/:id", (req, res) => { + res.sendFile(join(__dirname, "../../public/index.html")); + }); app.use("/static", estatic(join(__dirname, "../../public"))); - app.get("/favicon.ico", (req, res) => { - res.sendFile(join(__dirname, "../../public/favicon.ico")); - }); + app.ws("/room/:id", (ws, req) => { + const room_name = req.params.id + const room = rooms.get(req.params.id) ?? new Map() + let initialized = false + let user_name = "" - app.ws("/offer/:id", (ws, req) => { - const id = req.params.id - if (call_docs.get(id)) return ws.close(0, "call already running") - console.log(`[${id}] offer websocket open`); - ws.onclose = () => console.log(`[${id}] offer websocket close`); - const doc: CallDocModel = { - answered: false, - offer_candidates: [], - offer: undefined, - on_answer: () => { }, - on_answer_candidate: () => { }, - on_offer_candidate: () => { }, + const init = (n: string) => { + initialized = true + user_name = n + rooms.set(req.params.id, room) + room.forEach((_, uws) => ws_send(uws, JSON.stringify({ sender: user_name, join: true }))) + room.set(user_name, ws) + console.log(`[${room_name}] ${user_name} joined`) } - ws.onmessage = ev => { - const s = JSON.parse(ev.data.toString()) - if (s.offer) { - console.log(`[${id}] offer`); - doc.offer = s.offer - call_docs.set(id, doc) - } - if (s.candidate) { - console.log(`[${id}] offer candidate`); - if (doc.answered) doc.on_offer_candidate(s.candidate) - else doc.offer_candidates.push(s.candidate) - } + ws.onclose = () => { + room.delete(user_name) + room.forEach((_, uws) => ws_send(uws, JSON.stringify({ sender: user_name, leave: true }))) + if (room.size == 0) rooms.delete(room_name) + console.log(`[${room_name}] ${user_name} left`) } - doc.on_answer = answer => ws.send(JSON.stringify({ answer })) - doc.on_answer_candidate = candidate => ws.send(JSON.stringify({ candidate })) - }) - - app.ws("/answer/:id", (ws, req) => { - const id = req.params.id - console.log(`[${id}] answer websocket open`); - ws.onclose = () => console.log(`[${id}] answer websocket close`); - const doc = call_docs.get(id) - if (!doc) return ws.close(0, "call not found") - if (doc.answered) return ws.close(0, "call already answered") ws.onmessage = ev => { - const s = JSON.parse(ev.data.toString()) - if (s.answer) { - console.log(`[${id}] answer`); - doc.on_answer(s.answer) - } - if (s.candidate) { - console.log(`[${id}] answer candidate`); - doc.on_answer_candidate(s.candidate) + const message = ev.data.toString() + let in_packet: CSPacket; + try { in_packet = JSON.parse(message) } + catch (e) { return } + if (!initialized) return init(message) + + console.log(`[${room_name}] ${user_name} -> ${in_packet.receiver ?? "*"}: ${message}`) + const out_packet: SCPacket = { sender: user_name, data: in_packet } + + if (in_packet.receiver) { + const rws = room.get(in_packet.receiver) + if (rws) ws_send(rws, JSON.stringify(out_packet)) + } else { + room.forEach((uname, uws) => { + if (uname != user_name) ws_send(uws, JSON.stringify(out_packet)) + }) } } - doc.on_offer_candidate = candidate => ws.send(JSON.stringify({ candidate })) - // TODO this is evil - setTimeout(() => { - ws.send(JSON.stringify({ offer: doc.offer })) - for (const candidate of doc.offer_candidates) { - ws.send(JSON.stringify({ candidate })) - } - doc.offer_candidates = [] - }, 100) }) app.use((req, res, next) => { @@ -101,7 +91,7 @@ async function main() { const port = parseInt(process.env.PORT ?? "8080") app.listen(port, process.env.HOST ?? "127.0.0.1", () => { - console.log(`Server listening on 127.0.0.1:${port}`); + console.log(`Server listening on ${process.env.HOST ?? "127.0.0.1"}:${port}`); }) } |