111
This commit is contained in:
8
WebApp/src/class/answer.ts
Normal file
8
WebApp/src/class/answer.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export default class Answer {
|
||||
sdp: string;
|
||||
datetime: number;
|
||||
constructor(sdp: string, datetime: number) {
|
||||
this.sdp = sdp;
|
||||
this.datetime = datetime;
|
||||
}
|
||||
}
|
||||
12
WebApp/src/class/candidate.ts
Normal file
12
WebApp/src/class/candidate.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export default class Candidate {
|
||||
candidate: string;
|
||||
sdpMLineIndex: number;
|
||||
sdpMid: string;
|
||||
datetime: number;
|
||||
constructor(candidate: string, sdpMLineIndex: number, sdpMid: string, datetime: number) {
|
||||
this.candidate = candidate;
|
||||
this.sdpMLineIndex = sdpMLineIndex;
|
||||
this.sdpMid = sdpMid;
|
||||
this.datetime = datetime;
|
||||
}
|
||||
}
|
||||
413
WebApp/src/class/httphandler.ts
Normal file
413
WebApp/src/class/httphandler.ts
Normal file
@@ -0,0 +1,413 @@
|
||||
import { Request, Response } from 'express';
|
||||
import Offer from './offer';
|
||||
import Answer from './answer';
|
||||
import Candidate from './candidate';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
class Disconnection {
|
||||
id: string;
|
||||
datetime: number;
|
||||
constructor(id: string, datetime: number) {
|
||||
this.id = id;
|
||||
this.datetime = datetime;
|
||||
}
|
||||
}
|
||||
|
||||
const TimeoutRequestedTime = 10000; // 10sec
|
||||
|
||||
let isPrivate: boolean;
|
||||
|
||||
// [{sessonId:[connectionId,...]}]
|
||||
const clients: Map<string, Set<string>> = new Map<string, Set<string>>();
|
||||
|
||||
// [{sessonId:Date}]
|
||||
const lastRequestedTime: Map<string, number> = new Map<string, number>();
|
||||
|
||||
// [{connectionId:[sessionId1, sessionId2]}]
|
||||
const connectionPair: Map<string, [string, string]> = new Map<string, [string, string]>(); // key = connectionId
|
||||
|
||||
// [{sessionId:[{connectionId:Offer},...]}]
|
||||
const offers: Map<string, Map<string, Offer>> = new Map<string, Map<string, Offer>>(); // key = sessionId
|
||||
|
||||
// [{sessionId:[{connectionId:Answer},...]}]
|
||||
const answers: Map<string, Map<string, Answer>> = new Map<string, Map<string, Answer>>(); // key = sessionId
|
||||
|
||||
// [{sessionId:[{connectionId:Candidate},...]}]
|
||||
const candidates: Map<string, Map<string, Candidate[]>> = new Map<string, Map<string, Candidate[]>>(); // key = sessionId
|
||||
|
||||
// [{sessionId:[Disconnection,...]}]
|
||||
const disconnections: Map<string, Disconnection[]> = new Map<string, Disconnection[]>(); // key = sessionId
|
||||
|
||||
function getOrCreateConnectionIds(sessionId: string): Set<string> {
|
||||
let connectionIds = null;
|
||||
if (!clients.has(sessionId)) {
|
||||
connectionIds = new Set<string>();
|
||||
clients.set(sessionId, connectionIds);
|
||||
}
|
||||
connectionIds = clients.get(sessionId);
|
||||
return connectionIds;
|
||||
}
|
||||
|
||||
function reset(mode: string): void {
|
||||
isPrivate = mode == "private";
|
||||
clients.clear();
|
||||
connectionPair.clear();
|
||||
offers.clear();
|
||||
answers.clear();
|
||||
candidates.clear();
|
||||
disconnections.clear();
|
||||
}
|
||||
|
||||
function checkSessionId(req: Request, res: Response, next): void {
|
||||
if (req.url === '/') {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
const id: string = req.header('session-id');
|
||||
if (!clients.has(id)) {
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
lastRequestedTime.set(id, Date.now());
|
||||
next();
|
||||
}
|
||||
|
||||
function _deleteConnection(sessionId:string, connectionId:string, datetime:number) {
|
||||
clients.get(sessionId).delete(connectionId);
|
||||
|
||||
if(isPrivate) {
|
||||
if (connectionPair.has(connectionId)) {
|
||||
const pair = connectionPair.get(connectionId);
|
||||
const otherSessionId = pair[0] == sessionId ? pair[1] : pair[0];
|
||||
if (otherSessionId) {
|
||||
if (clients.has(otherSessionId)) {
|
||||
clients.get(otherSessionId).delete(connectionId);
|
||||
const array1 = disconnections.get(otherSessionId);
|
||||
array1.push(new Disconnection(connectionId, datetime));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
disconnections.forEach((array, id) => {
|
||||
if (id == sessionId)
|
||||
return;
|
||||
array.push(new Disconnection(connectionId, datetime));
|
||||
});
|
||||
}
|
||||
|
||||
connectionPair.delete(connectionId);
|
||||
offers.get(sessionId).delete(connectionId);
|
||||
answers.get(sessionId).delete(connectionId);
|
||||
candidates.get(sessionId).delete(connectionId);
|
||||
|
||||
const array2 = disconnections.get(sessionId);
|
||||
array2.push(new Disconnection(connectionId, datetime));
|
||||
}
|
||||
|
||||
function _deleteSession(sessionId: string) {
|
||||
if(clients.has(sessionId)) {
|
||||
for(const connectionId of Array.from(clients.get(sessionId))) {
|
||||
_deleteConnection(sessionId, connectionId, Date.now());
|
||||
}
|
||||
}
|
||||
offers.delete(sessionId);
|
||||
answers.delete(sessionId);
|
||||
candidates.delete(sessionId);
|
||||
clients.delete(sessionId);
|
||||
disconnections.delete(sessionId);
|
||||
}
|
||||
|
||||
function _checkForTimedOutSessions(): void {
|
||||
for (const sessionId of Array.from(clients.keys()))
|
||||
{
|
||||
if(!lastRequestedTime.has(sessionId))
|
||||
continue;
|
||||
if(lastRequestedTime.get(sessionId) > Date.now() - TimeoutRequestedTime)
|
||||
continue;
|
||||
_deleteSession(sessionId);
|
||||
console.log(`deleted sessionId:${sessionId} by timeout.`);
|
||||
}
|
||||
}
|
||||
|
||||
function _getConnection(sessionId: string): string[] {
|
||||
_checkForTimedOutSessions();
|
||||
return Array.from(clients.get(sessionId));
|
||||
}
|
||||
|
||||
function _getDisconnection(sessionId: string, fromTime: number): Disconnection[] {
|
||||
_checkForTimedOutSessions();
|
||||
let arrayDisconnections: Disconnection[] = [];
|
||||
if (disconnections.size != 0 && disconnections.has(sessionId)) {
|
||||
arrayDisconnections = disconnections.get(sessionId);
|
||||
}
|
||||
|
||||
if (fromTime > 0) {
|
||||
arrayDisconnections = arrayDisconnections.filter((v) => v.datetime >= fromTime);
|
||||
}
|
||||
return arrayDisconnections;
|
||||
}
|
||||
|
||||
function _getOffer(sessionId: string, fromTime: number): [string, Offer][] {
|
||||
let arrayOffers: [string, Offer][] = [];
|
||||
|
||||
if (offers.size != 0) {
|
||||
if (isPrivate) {
|
||||
if (offers.has(sessionId)) {
|
||||
arrayOffers = Array.from(offers.get(sessionId));
|
||||
}
|
||||
} else {
|
||||
const otherSessionMap = Array.from(offers).filter(x => x[0] != sessionId);
|
||||
arrayOffers = [].concat(...Array.from(otherSessionMap, x => Array.from(x[1], y => [y[0], y[1]])));
|
||||
}
|
||||
}
|
||||
|
||||
if (fromTime > 0) {
|
||||
arrayOffers = arrayOffers.filter((v) => v[1].datetime >= fromTime);
|
||||
}
|
||||
return arrayOffers;
|
||||
}
|
||||
|
||||
function _getAnswer(sessionId: string, fromTime: number): [string, Answer][] {
|
||||
let arrayAnswers: [string, Answer][] = [];
|
||||
|
||||
if (answers.size != 0 && answers.has(sessionId)) {
|
||||
arrayAnswers = Array.from(answers.get(sessionId));
|
||||
}
|
||||
|
||||
if (fromTime > 0) {
|
||||
arrayAnswers = arrayAnswers.filter((v) => v[1].datetime >= fromTime);
|
||||
}
|
||||
return arrayAnswers;
|
||||
}
|
||||
|
||||
function _getCandidate(sessionId: string, fromTime: number): [string, Candidate][] {
|
||||
const connectionIds = Array.from(clients.get(sessionId));
|
||||
const arr: [string, Candidate][] = [];
|
||||
for (const connectionId of connectionIds) {
|
||||
const pair = connectionPair.get(connectionId);
|
||||
if (pair == null) {
|
||||
continue;
|
||||
}
|
||||
const otherSessionId = sessionId === pair[0] ? pair[1] : pair[0];
|
||||
if (!candidates.get(otherSessionId) || !candidates.get(otherSessionId).get(connectionId)) {
|
||||
continue;
|
||||
}
|
||||
const arrayCandidates = candidates.get(otherSessionId).get(connectionId)
|
||||
.filter((v) => v.datetime >= fromTime);
|
||||
if (arrayCandidates.length === 0) {
|
||||
continue;
|
||||
}
|
||||
for (const candidate of arrayCandidates) {
|
||||
arr.push([connectionId, candidate]);
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
function getAnswer(req: Request, res: Response): void {
|
||||
// get `fromtime` parameter from request query
|
||||
const fromTime: number = req.query.fromtime ? Number(req.query.fromtime) : 0;
|
||||
const sessionId: string = req.header('session-id');
|
||||
const answers: [string, Answer][] = _getAnswer(sessionId, fromTime);
|
||||
res.json({ answers: answers.map((v) => ({ connectionId: v[0], sdp: v[1].sdp, type: "answer", datetime: v[1].datetime })) });
|
||||
}
|
||||
|
||||
function getConnection(req: Request, res: Response): void {
|
||||
// get `fromtime` parameter from request query
|
||||
const sessionId: string = req.header('session-id');
|
||||
const connections = _getConnection(sessionId);
|
||||
res.json({ connections: connections.map((v) => ({ connectionId: v, type: "connect", datetime: Date.now() })) });
|
||||
}
|
||||
|
||||
function getOffer(req: Request, res: Response): void {
|
||||
// get `fromtime` parameter from request query
|
||||
const fromTime: number = req.query.fromtime ? Number(req.query.fromtime) : 0;
|
||||
const sessionId: string = req.header('session-id');
|
||||
const offers = _getOffer(sessionId, fromTime);
|
||||
res.json({ offers: offers.map((v) => ({ connectionId: v[0], sdp: v[1].sdp, polite: v[1].polite, type: "offer", datetime: v[1].datetime })) });
|
||||
}
|
||||
|
||||
function getCandidate(req: Request, res: Response): void {
|
||||
// get `fromtime` parameter from request query
|
||||
const fromTime: number = req.query.fromtime ? Number(req.query.fromtime) : 0;
|
||||
const sessionId: string = req.header('session-id');
|
||||
const candidates = _getCandidate(sessionId, fromTime);
|
||||
res.json({ candidates: candidates.map((v) => ({ connectionId: v[0], candidate: v[1].candidate, sdpMLineIndex: v[1].sdpMLineIndex, sdpMid: v[1].sdpMid, type: "candidate", datetime: v[1].datetime })) });
|
||||
}
|
||||
|
||||
function getAll(req: Request, res: Response): void {
|
||||
const fromTime: number = req.query.fromtime ? Number(req.query.fromtime) : 0;
|
||||
const sessionId: string = req.header('session-id');
|
||||
const connections = _getConnection(sessionId);
|
||||
const offers = _getOffer(sessionId, fromTime);
|
||||
const answers: [string, Answer][] = _getAnswer(sessionId, fromTime);
|
||||
const candidates: [string, Candidate][] = _getCandidate(sessionId, fromTime);
|
||||
const disconnections: Disconnection[] = _getDisconnection(sessionId, fromTime);
|
||||
const datetime = lastRequestedTime.get(sessionId);
|
||||
|
||||
let array: any[] = [];
|
||||
|
||||
array = array.concat(connections.map((v) => ({ connectionId: v, type: "connect", datetime: datetime })));
|
||||
array = array.concat(offers.map((v) => ({ connectionId: v[0], sdp: v[1].sdp, polite: v[1].polite, type: "offer", datetime: v[1].datetime })));
|
||||
array = array.concat(answers.map((v) => ({ connectionId: v[0], sdp: v[1].sdp, type: "answer", datetime: v[1].datetime })));
|
||||
array = array.concat(candidates.map((v) => ({ connectionId: v[0], candidate: v[1].candidate, sdpMLineIndex: v[1].sdpMLineIndex, sdpMid: v[1].sdpMid, type: "candidate", datetime: v[1].datetime })));
|
||||
array = array.concat(disconnections.map((v) => ({ connectionId: v.id, type: "disconnect", datetime: v.datetime })));
|
||||
|
||||
array.sort((a, b) => a.datetime - b.datetime);
|
||||
res.json({ messages: array, datetime: datetime });
|
||||
}
|
||||
|
||||
function createSession(sessionId: string, res: Response): void;
|
||||
function createSession(req: Request, res: Response): void;
|
||||
|
||||
function createSession(req: string | Request, res: Response): void {
|
||||
const sessionId: string = typeof req === "string" ? req : uuid();
|
||||
clients.set(sessionId, new Set<string>());
|
||||
offers.set(sessionId, new Map<string, Offer>());
|
||||
answers.set(sessionId, new Map<string, Answer>());
|
||||
candidates.set(sessionId, new Map<string, Candidate[]>());
|
||||
disconnections.set(sessionId, []);
|
||||
res.json({ sessionId: sessionId });
|
||||
}
|
||||
|
||||
function deleteSession(req: Request, res: Response): void {
|
||||
const id: string = req.header('session-id');
|
||||
_deleteSession(id);
|
||||
res.sendStatus(200);
|
||||
}
|
||||
|
||||
function createConnection(req: Request, res: Response): void {
|
||||
const sessionId: string = req.header('session-id');
|
||||
const { connectionId } = req.body;
|
||||
const datetime = lastRequestedTime.get(sessionId);
|
||||
|
||||
if (connectionId == null) {
|
||||
res.status(400).send({ error: new Error(`connectionId is required`) });
|
||||
return;
|
||||
}
|
||||
let polite = true;
|
||||
if (isPrivate) {
|
||||
if (connectionPair.has(connectionId)) {
|
||||
const pair = connectionPair.get(connectionId);
|
||||
|
||||
if (pair[0] != null && pair[1] != null) {
|
||||
const err = new Error(`${connectionId}: This connection id is already used.`);
|
||||
console.log(err);
|
||||
res.status(400).send({ error: err });
|
||||
return;
|
||||
} else if (pair[0] != null) {
|
||||
connectionPair.set(connectionId, [pair[0], sessionId]);
|
||||
const map = getOrCreateConnectionIds(pair[0]);
|
||||
map.add(connectionId);
|
||||
}
|
||||
} else {
|
||||
connectionPair.set(connectionId, [sessionId, null]);
|
||||
polite = false;
|
||||
}
|
||||
}
|
||||
|
||||
const connectionIds = getOrCreateConnectionIds(sessionId);
|
||||
connectionIds.add(connectionId);
|
||||
res.json({ connectionId: connectionId, polite: polite, type: "connect", datetime: datetime });
|
||||
}
|
||||
|
||||
function deleteConnection(req: Request, res: Response): void {
|
||||
const sessionId: string = req.header('session-id');
|
||||
const { connectionId } = req.body;
|
||||
const datetime = lastRequestedTime.get(sessionId);
|
||||
|
||||
_deleteConnection(sessionId, connectionId, datetime);
|
||||
|
||||
res.json({ connectionId: connectionId });
|
||||
}
|
||||
|
||||
function postOffer(req: Request, res: Response): void {
|
||||
const sessionId: string = req.header('session-id');
|
||||
const { connectionId } = req.body;
|
||||
const datetime = lastRequestedTime.get(sessionId);
|
||||
let keySessionId = null;
|
||||
let polite = false;
|
||||
|
||||
if (isPrivate) {
|
||||
if (connectionPair.has(connectionId)) {
|
||||
const pair = connectionPair.get(connectionId);
|
||||
keySessionId = pair[0] == sessionId ? pair[1] : pair[0];
|
||||
if (keySessionId != null) {
|
||||
polite = true;
|
||||
const map = offers.get(keySessionId);
|
||||
map.set(connectionId, new Offer(req.body.sdp, datetime, polite));
|
||||
}
|
||||
}
|
||||
res.sendStatus(200);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!connectionPair.has(connectionId))
|
||||
{
|
||||
connectionPair.set(connectionId, [sessionId, null]);
|
||||
}
|
||||
|
||||
keySessionId = sessionId;
|
||||
const map = offers.get(keySessionId);
|
||||
map.set(connectionId, new Offer(req.body.sdp, datetime, polite));
|
||||
|
||||
res.sendStatus(200);
|
||||
}
|
||||
|
||||
function postAnswer(req: Request, res: Response): void {
|
||||
const sessionId: string = req.header('session-id');
|
||||
const { connectionId } = req.body;
|
||||
const datetime = lastRequestedTime.get(sessionId);
|
||||
const connectionIds = getOrCreateConnectionIds(sessionId);
|
||||
connectionIds.add(connectionId);
|
||||
|
||||
if (!connectionPair.has(connectionId)) {
|
||||
res.sendStatus(200);
|
||||
return;
|
||||
}
|
||||
|
||||
// add connectionPair
|
||||
const pair = connectionPair.get(connectionId);
|
||||
const otherSessionId = pair[0] == sessionId ? pair[1] : pair[0];
|
||||
if (!clients.has(otherSessionId)) {
|
||||
// already deleted
|
||||
res.sendStatus(200);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isPrivate) {
|
||||
connectionPair.set(connectionId, [otherSessionId, sessionId]);
|
||||
}
|
||||
|
||||
const map = answers.get(otherSessionId);
|
||||
map.set(connectionId, new Answer(req.body.sdp, datetime));
|
||||
|
||||
// update datetime for candidates
|
||||
const mapCandidates = candidates.get(otherSessionId);
|
||||
if (mapCandidates) {
|
||||
const arrayCandidates = mapCandidates.get(connectionId);
|
||||
if (arrayCandidates) {
|
||||
for (const candidate of arrayCandidates) {
|
||||
candidate.datetime = datetime;
|
||||
}
|
||||
}
|
||||
}
|
||||
res.sendStatus(200);
|
||||
}
|
||||
|
||||
function postCandidate(req: Request, res: Response): void {
|
||||
const sessionId: string = req.header('session-id');
|
||||
const { connectionId } = req.body;
|
||||
const datetime = lastRequestedTime.get(sessionId);
|
||||
|
||||
const map = candidates.get(sessionId);
|
||||
if (!map.has(connectionId)) {
|
||||
map.set(connectionId, []);
|
||||
}
|
||||
const arr = map.get(connectionId);
|
||||
const candidate = new Candidate(req.body.candidate, req.body.sdpMLineIndex, req.body.sdpMid, datetime);
|
||||
arr.push(candidate);
|
||||
res.sendStatus(200);
|
||||
}
|
||||
|
||||
export { reset, checkSessionId, getAll, getConnection, getOffer, getAnswer, getCandidate, createSession, deleteSession, createConnection, deleteConnection, postOffer, postAnswer, postCandidate };
|
||||
10
WebApp/src/class/offer.ts
Normal file
10
WebApp/src/class/offer.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export default class Offer {
|
||||
sdp: string;
|
||||
datetime: number;
|
||||
polite: boolean;
|
||||
constructor(sdp: string, datetime: number, polite: boolean) {
|
||||
this.sdp = sdp;
|
||||
this.datetime = datetime;
|
||||
this.polite = polite;
|
||||
}
|
||||
}
|
||||
9
WebApp/src/class/options.ts
Normal file
9
WebApp/src/class/options.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export default interface Options {
|
||||
secure?: boolean;
|
||||
port?: number;
|
||||
keyfile?: string;
|
||||
certfile?: string;
|
||||
type?: string;
|
||||
mode?: string;
|
||||
logging?: string;
|
||||
}
|
||||
153
WebApp/src/class/websockethandler.ts
Normal file
153
WebApp/src/class/websockethandler.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import Offer from './offer';
|
||||
import Answer from './answer';
|
||||
import Candidate from './candidate';
|
||||
|
||||
let isPrivate: boolean;
|
||||
|
||||
// [{sessonId:[connectionId,...]}]
|
||||
const clients: Map<WebSocket, Set<string>> = new Map<WebSocket, Set<string>>();
|
||||
|
||||
// [{connectionId:[sessionId1, sessionId2]}]
|
||||
const connectionPair: Map<string, [WebSocket, WebSocket]> = new Map<string, [WebSocket, WebSocket]>();
|
||||
|
||||
function getOrCreateConnectionIds(session: WebSocket): Set<string> {
|
||||
let connectionIds = null;
|
||||
if (!clients.has(session)) {
|
||||
connectionIds = new Set<string>();
|
||||
clients.set(session, connectionIds);
|
||||
}
|
||||
connectionIds = clients.get(session);
|
||||
return connectionIds;
|
||||
}
|
||||
|
||||
function reset(mode: string): void {
|
||||
isPrivate = mode == "private";
|
||||
}
|
||||
|
||||
function add(ws: WebSocket): void {
|
||||
clients.set(ws, new Set<string>());
|
||||
}
|
||||
|
||||
function remove(ws: WebSocket): void {
|
||||
const connectionIds = clients.get(ws);
|
||||
connectionIds.forEach(connectionId => {
|
||||
const pair = connectionPair.get(connectionId);
|
||||
if (pair) {
|
||||
const otherSessionWs = pair[0] == ws ? pair[1] : pair[0];
|
||||
if (otherSessionWs) {
|
||||
otherSessionWs.send(JSON.stringify({ type: "disconnect", connectionId: connectionId }));
|
||||
}
|
||||
}
|
||||
connectionPair.delete(connectionId);
|
||||
});
|
||||
|
||||
clients.delete(ws);
|
||||
}
|
||||
|
||||
function onConnect(ws: WebSocket, connectionId: string): void {
|
||||
let polite = true;
|
||||
if (isPrivate) {
|
||||
if (connectionPair.has(connectionId)) {
|
||||
const pair = connectionPair.get(connectionId);
|
||||
|
||||
if (pair[0] != null && pair[1] != null) {
|
||||
ws.send(JSON.stringify({ type: "error", message: `${connectionId}: This connection id is already used.` }));
|
||||
return;
|
||||
} else if (pair[0] != null) {
|
||||
connectionPair.set(connectionId, [pair[0], ws]);
|
||||
}
|
||||
} else {
|
||||
connectionPair.set(connectionId, [ws, null]);
|
||||
polite = false;
|
||||
}
|
||||
}
|
||||
|
||||
const connectionIds = getOrCreateConnectionIds(ws);
|
||||
connectionIds.add(connectionId);
|
||||
ws.send(JSON.stringify({ type: "connect", connectionId: connectionId, polite: polite }));
|
||||
}
|
||||
|
||||
function onDisconnect(ws: WebSocket, connectionId: string): void {
|
||||
const connectionIds = clients.get(ws);
|
||||
connectionIds.delete(connectionId);
|
||||
|
||||
if (connectionPair.has(connectionId)) {
|
||||
const pair = connectionPair.get(connectionId);
|
||||
const otherSessionWs = pair[0] == ws ? pair[1] : pair[0];
|
||||
if (otherSessionWs) {
|
||||
otherSessionWs.send(JSON.stringify({ type: "disconnect", connectionId: connectionId }));
|
||||
}
|
||||
}
|
||||
connectionPair.delete(connectionId);
|
||||
ws.send(JSON.stringify({ type: "disconnect", connectionId: connectionId }));
|
||||
}
|
||||
|
||||
function onOffer(ws: WebSocket, message: any): void {
|
||||
const connectionId = message.connectionId as string;
|
||||
const newOffer = new Offer(message.sdp, Date.now(), false);
|
||||
|
||||
if (isPrivate) {
|
||||
if (connectionPair.has(connectionId)) {
|
||||
const pair = connectionPair.get(connectionId);
|
||||
const otherSessionWs = pair[0] == ws ? pair[1] : pair[0];
|
||||
if (otherSessionWs) {
|
||||
newOffer.polite = true;
|
||||
otherSessionWs.send(JSON.stringify({ from: connectionId, to: "", type: "offer", data: newOffer }));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
connectionPair.set(connectionId, [ws, null]);
|
||||
clients.forEach((_v, k) => {
|
||||
if (k == ws) {
|
||||
return;
|
||||
}
|
||||
k.send(JSON.stringify({ from: connectionId, to: "", type: "offer", data: newOffer }));
|
||||
});
|
||||
}
|
||||
|
||||
function onAnswer(ws: WebSocket, message: any): void {
|
||||
const connectionId = message.connectionId as string;
|
||||
const connectionIds = getOrCreateConnectionIds(ws);
|
||||
connectionIds.add(connectionId);
|
||||
const newAnswer = new Answer(message.sdp, Date.now());
|
||||
|
||||
if (!connectionPair.has(connectionId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pair = connectionPair.get(connectionId);
|
||||
const otherSessionWs = pair[0] == ws ? pair[1] : pair[0];
|
||||
|
||||
if (!isPrivate) {
|
||||
connectionPair.set(connectionId, [otherSessionWs, ws]);
|
||||
}
|
||||
|
||||
otherSessionWs.send(JSON.stringify({ from: connectionId, to: "", type: "answer", data: newAnswer }));
|
||||
}
|
||||
|
||||
function onCandidate(ws: WebSocket, message: any): void {
|
||||
const connectionId = message.connectionId;
|
||||
const candidate = new Candidate(message.candidate, message.sdpMLineIndex, message.sdpMid, Date.now());
|
||||
|
||||
if (isPrivate) {
|
||||
if (connectionPair.has(connectionId)) {
|
||||
const pair = connectionPair.get(connectionId);
|
||||
const otherSessionWs = pair[0] == ws ? pair[1] : pair[0];
|
||||
if (otherSessionWs) {
|
||||
otherSessionWs.send(JSON.stringify({ from: connectionId, to: "", type: "candidate", data: candidate }));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
clients.forEach((_v, k) => {
|
||||
if (k === ws) {
|
||||
return;
|
||||
}
|
||||
k.send(JSON.stringify({ from: connectionId, to: "", type: "candidate", data: candidate }));
|
||||
});
|
||||
}
|
||||
|
||||
export { reset, add, remove, onConnect, onDisconnect, onOffer, onAnswer, onCandidate };
|
||||
105
WebApp/src/index.ts
Normal file
105
WebApp/src/index.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { Command } from 'commander';
|
||||
import * as express from 'express';
|
||||
import * as https from 'https';
|
||||
import { Server } from 'http';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import { createServer } from './server';
|
||||
import { AddressInfo } from 'net';
|
||||
import WSSignaling from './websocket';
|
||||
import Options from './class/options';
|
||||
|
||||
export class RenderStreaming {
|
||||
public static run(argv: string[]): RenderStreaming {
|
||||
const program = new Command();
|
||||
const readOptions = (): Options => {
|
||||
if (Array.isArray(argv)) {
|
||||
program
|
||||
.usage('[options] <apps...>')
|
||||
.option('-p, --port <n>', 'Port to start the server on.', process.env.PORT || `80`)
|
||||
.option('-s, --secure', 'Enable HTTPS (you need server.key and server.cert).', process.env.SECURE || false)
|
||||
.option('-k, --keyfile <path>', 'https key file.', process.env.KEYFILE || 'server.key')
|
||||
.option('-c, --certfile <path>', 'https cert file.', process.env.CERTFILE || 'server.cert')
|
||||
.option('-t, --type <type>', 'Type of signaling protocol, Choose websocket or http.', process.env.TYPE || 'websocket')
|
||||
.option('-m, --mode <type>', 'Choose Communication mode public or private.', process.env.MODE || 'public')
|
||||
.option('-l, --logging <type>', 'Choose http logging type combined, dev, short, tiny or none.', process.env.LOGGING || 'dev')
|
||||
.parse(argv);
|
||||
const option = program.opts();
|
||||
return {
|
||||
port: option.port,
|
||||
secure: option.secure == undefined ? false : option.secure,
|
||||
keyfile: option.keyfile,
|
||||
certfile: option.certfile,
|
||||
type: option.type == undefined ? 'websocket' : option.type,
|
||||
mode: option.mode,
|
||||
logging: option.logging,
|
||||
};
|
||||
}
|
||||
};
|
||||
const options = readOptions();
|
||||
return new RenderStreaming(options);
|
||||
}
|
||||
|
||||
public app: express.Application;
|
||||
|
||||
public server?: Server;
|
||||
|
||||
public options: Options;
|
||||
|
||||
constructor(options: Options) {
|
||||
this.options = options;
|
||||
this.app = createServer(this.options);
|
||||
if (this.options.secure) {
|
||||
this.server = https.createServer({
|
||||
key: fs.readFileSync(options.keyfile),
|
||||
cert: fs.readFileSync(options.certfile),
|
||||
}, this.app).listen(this.options.port, () => {
|
||||
const { port } = this.server.address() as AddressInfo;
|
||||
const addresses = this.getIPAddress();
|
||||
for (const address of addresses) {
|
||||
console.log(`https://${address}:${port}`);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.server = this.app.listen(this.options.port, () => {
|
||||
const { port } = this.server.address() as AddressInfo;
|
||||
const addresses = this.getIPAddress();
|
||||
for (const address of addresses) {
|
||||
console.log(`http://${address}:${port}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (this.options.type == 'http') {
|
||||
console.log(`Use http polling for signaling server.`);
|
||||
}
|
||||
else if(this.options.type != 'websocket') {
|
||||
console.log(`signaling type should be set "websocket" or "http". ${this.options.type} is not supported.`);
|
||||
console.log(`Changing signaling type to websocket.`);
|
||||
this.options.type = 'websocket';
|
||||
}
|
||||
if (this.options.type == 'websocket') {
|
||||
console.log(`Use websocket for signaling server ws://${this.getIPAddress()[0]}`);
|
||||
|
||||
//Start Websocket Signaling server
|
||||
new WSSignaling(this.server, this.options.mode);
|
||||
}
|
||||
|
||||
console.log(`start as ${this.options.mode} mode`);
|
||||
}
|
||||
|
||||
getIPAddress(): string[] {
|
||||
const interfaces = os.networkInterfaces();
|
||||
const addresses: string[] = [];
|
||||
for (const k in interfaces) {
|
||||
for (const k2 in interfaces[k]) {
|
||||
const address = interfaces[k][k2];
|
||||
if (address.family === 'IPv4') {
|
||||
addresses.push(address.address);
|
||||
}
|
||||
}
|
||||
}
|
||||
return addresses;
|
||||
}
|
||||
}
|
||||
|
||||
RenderStreaming.run(process.argv);
|
||||
27
WebApp/src/log.ts
Normal file
27
WebApp/src/log.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
const isDebug = true;
|
||||
|
||||
export enum LogLevel {
|
||||
info,
|
||||
log,
|
||||
warn,
|
||||
error,
|
||||
}
|
||||
|
||||
export function log(level: LogLevel, ...args: any[]): void {
|
||||
if (isDebug) {
|
||||
switch (level) {
|
||||
case LogLevel.log:
|
||||
console.log(...args);
|
||||
break;
|
||||
case LogLevel.info:
|
||||
console.info(...args);
|
||||
break;
|
||||
case LogLevel.warn:
|
||||
console.warn(...args);
|
||||
break;
|
||||
case LogLevel.error:
|
||||
console.error(...args);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
111
WebApp/src/room.ts
Normal file
111
WebApp/src/room.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
// server.js - 扩展官方示例
|
||||
const { Signaling } = require('unity-render-streaming');
|
||||
|
||||
class ExtendedSignaling extends Signaling {
|
||||
constructor() {
|
||||
super();
|
||||
this.rooms = new Map(); // 房间信息存储
|
||||
this.connections = new Map(); // 连接信息
|
||||
}
|
||||
|
||||
// 创建房间时记录
|
||||
onCreateConnection(connectionId) {
|
||||
super.onCreateConnection(connectionId);
|
||||
this.connections.set(connectionId, {
|
||||
joinedAt: Date.now(),
|
||||
roomId: null
|
||||
});
|
||||
}
|
||||
|
||||
// 加入房间
|
||||
onJoinRoom(connectionId, roomId) {
|
||||
super.onJoinRoom(connectionId, roomId);
|
||||
|
||||
if (!this.rooms.has(roomId)) {
|
||||
this.rooms.set(roomId, {
|
||||
createdAt: Date.now(),
|
||||
members: new Set()
|
||||
});
|
||||
}
|
||||
this.rooms.get(roomId).members.add(connectionId);
|
||||
this.connections.get(connectionId).roomId = roomId;
|
||||
}
|
||||
|
||||
// 离开房间
|
||||
onLeaveRoom(connectionId, roomId) {
|
||||
super.onLeaveRoom(connectionId, roomId);
|
||||
|
||||
if (this.rooms.has(roomId)) {
|
||||
this.rooms.get(roomId).members.delete(connectionId);
|
||||
if (this.rooms.get(roomId).members.size === 0) {
|
||||
this.rooms.delete(roomId); // 空房间自动清理
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 断开连接
|
||||
onDeleteConnection(connectionId) {
|
||||
const connInfo = this.connections.get(connectionId);
|
||||
if (connInfo && connInfo.roomId) {
|
||||
this.onLeaveRoom(connectionId, connInfo.roomId);
|
||||
}
|
||||
this.connections.delete(connectionId);
|
||||
super.onDeleteConnection(connectionId);
|
||||
}
|
||||
|
||||
// ========== 查询 API ==========
|
||||
|
||||
// 获取所有房间
|
||||
getAllRooms() {
|
||||
return Array.from(this.rooms.entries()).map(([roomId, info]) => ({
|
||||
roomId,
|
||||
memberCount: info.members.size,
|
||||
createdAt: info.createdAt,
|
||||
members: Array.from(info.members)
|
||||
}));
|
||||
}
|
||||
|
||||
// 获取指定房间的成员
|
||||
getRoomMembers(roomId) {
|
||||
const room = this.rooms.get(roomId);
|
||||
if (!room) return null;
|
||||
|
||||
return Array.from(room.members).map(connId => ({
|
||||
connectionId: connId,
|
||||
joinedAt: this.connections.get(connId)?.joinedAt
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// HTTP API 暴露查询接口
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
|
||||
const signaling = new ExtendedSignaling();
|
||||
|
||||
// API: 获取所有房间列表
|
||||
app.get('/api/rooms', (req, res) => {
|
||||
res.json({
|
||||
count: signaling.rooms.size,
|
||||
rooms: signaling.getAllRooms()
|
||||
});
|
||||
});
|
||||
|
||||
// API: 获取指定房间详情
|
||||
app.get('/api/rooms/:roomId', (req, res) => {
|
||||
const { roomId } = req.params;
|
||||
const members = signaling.getRoomMembers(roomId);
|
||||
|
||||
if (!members) {
|
||||
return res.status(404).json({ error: 'Room not found' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
roomId,
|
||||
members,
|
||||
memberCount: members.length
|
||||
});
|
||||
});
|
||||
|
||||
app.listen(3000);
|
||||
signaling.start(80); // WebSocket 信令端口
|
||||
39
WebApp/src/server.ts
Normal file
39
WebApp/src/server.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import * as express from 'express';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as morgan from 'morgan';
|
||||
import signaling from './signaling';
|
||||
import { log, LogLevel } from './log';
|
||||
import Options from './class/options';
|
||||
import { reset as resetHandler }from './class/httphandler';
|
||||
|
||||
const cors = require('cors');
|
||||
|
||||
export const createServer = (config: Options): express.Application => {
|
||||
const app: express.Application = express();
|
||||
resetHandler(config.mode);
|
||||
// logging http access
|
||||
if (config.logging != "none") {
|
||||
app.use(morgan(config.logging));
|
||||
}
|
||||
// const signal = require('./signaling');
|
||||
app.use(cors({origin: '*'}));
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(express.json());
|
||||
app.get('/config', (req, res) => res.json({ useWebSocket: config.type == 'websocket', startupMode: config.mode, logging: config.logging }));
|
||||
app.use('/signaling', signaling);
|
||||
app.use(express.static(path.join(__dirname, '../client/public')));
|
||||
app.use('/module', express.static(path.join(__dirname, '../client/src')));
|
||||
app.get('/', (req, res) => {
|
||||
const indexPagePath: string = path.join(__dirname, '../client/public/index.html');
|
||||
fs.access(indexPagePath, (err) => {
|
||||
if (err) {
|
||||
log(LogLevel.warn, `Can't find file ' ${indexPagePath}`);
|
||||
res.status(404).send(`Can't find file ${indexPagePath}`);
|
||||
} else {
|
||||
res.sendFile(indexPagePath);
|
||||
}
|
||||
});
|
||||
});
|
||||
return app;
|
||||
};
|
||||
19
WebApp/src/signaling.ts
Normal file
19
WebApp/src/signaling.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import * as express from 'express';
|
||||
import * as handler from'./class/httphandler';
|
||||
|
||||
const router: express.Router = express.Router();
|
||||
router.use(handler.checkSessionId);
|
||||
router.get('/connection', handler.getConnection);
|
||||
router.get('/offer', handler.getOffer);
|
||||
router.get('/answer', handler.getAnswer);
|
||||
router.get('/candidate', handler.getCandidate);
|
||||
router.get('', handler.getAll);
|
||||
router.put('', handler.createSession);
|
||||
router.delete('', handler.deleteSession);
|
||||
router.put('/connection', handler.createConnection);
|
||||
router.delete('/connection', handler.deleteConnection);
|
||||
router.post('/offer', handler.postOffer);
|
||||
router.post('/answer', handler.postAnswer);
|
||||
router.post('/candidate', handler.postCandidate);
|
||||
|
||||
export default router;
|
||||
61
WebApp/src/websocket.ts
Normal file
61
WebApp/src/websocket.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import * as websocket from "ws";
|
||||
import { Server } from 'http';
|
||||
import * as handler from "./class/websockethandler";
|
||||
|
||||
export default class WSSignaling {
|
||||
server: Server;
|
||||
wss: websocket.Server;
|
||||
|
||||
constructor(server: Server, mode: string) {
|
||||
this.server = server;
|
||||
this.wss = new websocket.Server({ server });
|
||||
handler.reset(mode);
|
||||
|
||||
this.wss.on('connection', (ws: WebSocket) => {
|
||||
|
||||
handler.add(ws);
|
||||
|
||||
ws.onclose = (): void => {
|
||||
handler.remove(ws);
|
||||
};
|
||||
|
||||
ws.onmessage = (event: MessageEvent): void => {
|
||||
|
||||
// type: connect, disconnect JSON Schema
|
||||
// connectionId: connect or disconnect connectionId
|
||||
|
||||
// type: offer, answer, candidate JSON Schema
|
||||
// from: from connection id
|
||||
// to: to connection id
|
||||
// data: any message data structure
|
||||
|
||||
const msg = JSON.parse(event.data);
|
||||
if (!msg || !this) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(msg);
|
||||
|
||||
switch (msg.type) {
|
||||
case "connect":
|
||||
handler.onConnect(ws, msg.connectionId);
|
||||
break;
|
||||
case "disconnect":
|
||||
handler.onDisconnect(ws, msg.connectionId);
|
||||
break;
|
||||
case "offer":
|
||||
handler.onOffer(ws, msg.data);
|
||||
break;
|
||||
case "answer":
|
||||
handler.onAnswer(ws, msg.data);
|
||||
break;
|
||||
case "candidate":
|
||||
handler.onCandidate(ws, msg.data);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user