aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--public/index.html10
-rw-r--r--source/client/index.ts192
-rw-r--r--source/client/room.ts53
-rw-r--r--source/client/types.ts13
-rw-r--r--source/client/user.ts60
-rw-r--r--source/models.ts11
-rw-r--r--source/server/index.ts118
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}`);
})
}