aboutsummaryrefslogtreecommitdiff
path: root/source
diff options
context:
space:
mode:
Diffstat (limited to 'source')
-rw-r--r--source/client/index.ts130
-rw-r--r--source/models.ts11
-rw-r--r--source/server/index.ts108
3 files changed, 249 insertions, 0 deletions
diff --git a/source/client/index.ts b/source/client/index.ts
new file mode 100644
index 0000000..3841c8a
--- /dev/null
+++ b/source/client/index.ts
@@ -0,0 +1,130 @@
+
+export const servers = {
+ 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;
+
+window.onload = async () => {
+
+ 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 {
+
+ }
+
+}
+
+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()
+
+ local_stream.getTracks().forEach(t => pc.addTrack(t, local_stream))
+
+ pc.ontrack = ev => {
+ console.log("peer got remote tracks", ev.streams);
+ ev.streams[0].getTracks().forEach(t => remote_stream.addTrack(t))
+ }
+
+
+ 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
+
+ document.body.append(ls_el, rs_el)
+
+}
+
+interface Offer {
+ sdp: any,
+ type: any
+}
+
+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")
+
+ pc.onicecandidate = ev => {
+ const candidate = ev.candidate?.toJSON()
+ if (!candidate) return
+ ws.send(JSON.stringify({ candidate }))
+ console.log("sent ice candidate", ev.candidate);
+ }
+
+ const offer_description = await pc.createOffer()
+ await pc.setLocalDescription(offer_description);
+
+ const offer: Offer = { sdp: offer_description.sdp, type: offer_description.type };
+
+ ws.send(JSON.stringify({ offer }))
+
+ 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)
+ }
+ }
+
+}
+
+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");
+
+
+ pc.onicecandidate = ev => {
+ const candidate = ev.candidate?.toJSON()
+ if (!candidate) return
+ ws.send(JSON.stringify({ candidate }))
+ console.log("sent ice candidate", 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/models.ts b/source/models.ts
new file mode 100644
index 0000000..45a3825
--- /dev/null
+++ b/source/models.ts
@@ -0,0 +1,11 @@
+
+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
new file mode 100644
index 0000000..bac2ffb
--- /dev/null
+++ b/source/server/index.ts
@@ -0,0 +1,108 @@
+import Express, { static as estatic, json } from "express";
+import { join } from "path";
+import Webpack from "webpack"
+import WebpackDevMiddleware from "webpack-dev-middleware"
+import { existsSync, readFile, readFileSync } from "fs";
+import http from "http"
+import https from "https"
+import expressWs from "express-ws";
+import { CallDocModel } from "../models";
+
+const call_docs: Map<String, CallDocModel> = new Map()
+
+
+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)
+
+ app.disable("x-powered-by");
+ app.use(json());
+
+ app.get("/", (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("/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: () => { },
+ }
+ 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)
+ }
+ }
+ 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)
+ }
+ }
+ 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) => {
+ res.status(404);
+ res.send("This is an error page");
+ });
+
+ 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}`);
+ })
+}
+
+main(); \ No newline at end of file