diff options
Diffstat (limited to 'source')
-rw-r--r-- | source/client/index.ts | 130 | ||||
-rw-r--r-- | source/models.ts | 11 | ||||
-rw-r--r-- | source/server/index.ts | 108 |
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 |