2026-04-29 15:18:30 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* HTTP处理器
|
|
|
|
|
|
* 负责处理HTTP请求,管理会话和连接,处理信令消息
|
|
|
|
|
|
*/
|
|
|
|
|
|
import { Request, Response } from 'express';
|
|
|
|
|
|
import Offer from './offer';
|
|
|
|
|
|
import Answer from './answer';
|
|
|
|
|
|
import Candidate from './candidate';
|
|
|
|
|
|
import { v4 as uuid } from 'uuid';
|
2026-05-16 22:22:34 +08:00
|
|
|
|
import { onGetAllConnectionIds, onGetOnlineUsers as onGetWsOnlineUsers } from './websockethandler';
|
2026-05-06 16:08:00 +08:00
|
|
|
|
import { log, LogLevel } from '../log';
|
2026-04-29 15:18:30 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 断开连接记录类
|
|
|
|
|
|
* 用于记录断开连接的信息
|
|
|
|
|
|
*/
|
|
|
|
|
|
class Disconnection {
|
|
|
|
|
|
id: string; // 连接ID
|
|
|
|
|
|
datetime: number; // 断开连接的时间戳
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 构造函数
|
|
|
|
|
|
* @param id 连接ID
|
|
|
|
|
|
* @param datetime 断开连接的时间戳
|
|
|
|
|
|
*/
|
|
|
|
|
|
constructor(id: string, datetime: number) {
|
|
|
|
|
|
this.id = id;
|
|
|
|
|
|
this.datetime = datetime;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 会话超时时间(毫秒)
|
|
|
|
|
|
const TimeoutRequestedTime = 10000; // 10sec
|
|
|
|
|
|
|
|
|
|
|
|
// 是否为私有模式
|
|
|
|
|
|
let isPrivate: boolean;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 客户端会话映射
|
|
|
|
|
|
* 键: 会话ID
|
|
|
|
|
|
* 值: 该会话的连接ID集合
|
|
|
|
|
|
*/
|
|
|
|
|
|
const clients: Map<string, Set<string>> = new Map<string, Set<string>>();
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 会话最后请求时间映射
|
|
|
|
|
|
* 键: 会话ID
|
|
|
|
|
|
* 值: 最后请求的时间戳
|
|
|
|
|
|
*/
|
|
|
|
|
|
const lastRequestedTime: Map<string, number> = new Map<string, number>();
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 连接对映射
|
|
|
|
|
|
* 键: 连接ID
|
|
|
|
|
|
* 值: [会话ID1, 会话ID2]
|
|
|
|
|
|
*/
|
|
|
|
|
|
const connectionPair: Map<string, [string, string]> = new Map<string, [string, string]>(); // key = connectionId
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 会话的offer映射
|
|
|
|
|
|
* 键: 会话ID
|
|
|
|
|
|
* 值: 该会话的连接ID到Offer对象的映射
|
|
|
|
|
|
*/
|
|
|
|
|
|
const offers: Map<string, Map<string, Offer>> = new Map<string, Map<string, Offer>>(); // key = sessionId
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 会话的answer映射
|
|
|
|
|
|
* 键: 会话ID
|
|
|
|
|
|
* 值: 该会话的连接ID到Answer对象的映射
|
|
|
|
|
|
*/
|
|
|
|
|
|
const answers: Map<string, Map<string, Answer>> = new Map<string, Map<string, Answer>>(); // key = sessionId
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 会话的candidate映射
|
|
|
|
|
|
* 键: 会话ID
|
|
|
|
|
|
* 值: 该会话的连接ID到Candidate数组的映射
|
|
|
|
|
|
*/
|
|
|
|
|
|
const candidates: Map<string, Map<string, Candidate[]>> = new Map<string, Map<string, Candidate[]>>(); // key = sessionId
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 会话的断开连接记录映射
|
|
|
|
|
|
* 键: 会话ID
|
|
|
|
|
|
* 值: 该会话的Disconnection对象数组
|
|
|
|
|
|
*/
|
|
|
|
|
|
const disconnections: Map<string, Disconnection[]> = new Map<string, Disconnection[]>(); // key = sessionId
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取或创建会话的连接ID集合
|
|
|
|
|
|
* @param sessionId 会话ID
|
|
|
|
|
|
* @returns 连接ID的Set集合
|
|
|
|
|
|
*/
|
|
|
|
|
|
function getOrCreateConnectionIds(sessionId: string): Set<string> {
|
|
|
|
|
|
let connectionIds = null;
|
|
|
|
|
|
// 检查会话是否已存在
|
|
|
|
|
|
if (!clients.has(sessionId)) {
|
|
|
|
|
|
// 如果不存在,创建新的连接ID集合
|
|
|
|
|
|
connectionIds = new Set<string>();
|
|
|
|
|
|
// 将新的连接ID集合与会话关联
|
|
|
|
|
|
clients.set(sessionId, connectionIds);
|
|
|
|
|
|
}
|
|
|
|
|
|
// 获取会话的连接ID集合
|
|
|
|
|
|
connectionIds = clients.get(sessionId);
|
|
|
|
|
|
// 返回连接ID集合
|
|
|
|
|
|
return connectionIds;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 重置处理器状态
|
|
|
|
|
|
* @param mode 通信模式(public或private)
|
|
|
|
|
|
*/
|
|
|
|
|
|
function reset(mode: string): void {
|
|
|
|
|
|
// 设置是否为私有模式
|
|
|
|
|
|
isPrivate = mode == "private";
|
|
|
|
|
|
// 清空所有映射
|
|
|
|
|
|
clients.clear();
|
|
|
|
|
|
connectionPair.clear();
|
|
|
|
|
|
offers.clear();
|
|
|
|
|
|
answers.clear();
|
|
|
|
|
|
candidates.clear();
|
|
|
|
|
|
disconnections.clear();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 检查会话ID是否有效
|
|
|
|
|
|
* @param req Express请求对象
|
|
|
|
|
|
* @param res Express响应对象
|
|
|
|
|
|
* @param next 下一个中间件函数
|
|
|
|
|
|
*/
|
|
|
|
|
|
function checkSessionId(req: Request, res: Response, next): void {
|
|
|
|
|
|
// 如果是根路径,直接通过
|
|
|
|
|
|
if (req.url === '/') {
|
|
|
|
|
|
next();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 从请求头获取会话ID
|
|
|
|
|
|
const id: string = req.header('session-id');
|
|
|
|
|
|
// 检查会话是否存在
|
|
|
|
|
|
if (!clients.has(id)) {
|
|
|
|
|
|
res.sendStatus(404);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 更新会话的最后请求时间
|
|
|
|
|
|
lastRequestedTime.set(id, Date.now());
|
|
|
|
|
|
// 继续处理请求
|
|
|
|
|
|
next();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 删除连接
|
|
|
|
|
|
* @param sessionId 会话ID
|
|
|
|
|
|
* @param connectionId 连接ID
|
|
|
|
|
|
* @param datetime 时间戳
|
|
|
|
|
|
*/
|
|
|
|
|
|
function _deleteConnection(sessionId:string, connectionId:string, datetime:number) {
|
|
|
|
|
|
// 从会话的连接ID集合中删除连接ID
|
|
|
|
|
|
clients.get(sessionId).delete(connectionId);
|
|
|
|
|
|
|
|
|
|
|
|
// 处理私有模式
|
|
|
|
|
|
if(isPrivate) {
|
|
|
|
|
|
if (connectionPair.has(connectionId)) {
|
|
|
|
|
|
const pair = connectionPair.get(connectionId);
|
|
|
|
|
|
// 找到另一个会话ID
|
|
|
|
|
|
const otherSessionId = pair[0] == sessionId ? pair[1] : pair[0];
|
|
|
|
|
|
if (otherSessionId) {
|
|
|
|
|
|
if (clients.has(otherSessionId)) {
|
|
|
|
|
|
// 从另一个会话的连接ID集合中删除连接ID
|
|
|
|
|
|
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);
|
|
|
|
|
|
// 从会话的offer映射中删除
|
|
|
|
|
|
offers.get(sessionId).delete(connectionId);
|
|
|
|
|
|
// 从会话的answer映射中删除
|
|
|
|
|
|
answers.get(sessionId).delete(connectionId);
|
|
|
|
|
|
// 从会话的candidate映射中删除
|
|
|
|
|
|
candidates.get(sessionId).delete(connectionId);
|
|
|
|
|
|
|
|
|
|
|
|
// 向当前会话的断开连接记录中添加记录
|
|
|
|
|
|
const array2 = disconnections.get(sessionId);
|
|
|
|
|
|
array2.push(new Disconnection(connectionId, datetime));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 删除会话
|
|
|
|
|
|
* @param sessionId 会话ID
|
|
|
|
|
|
*/
|
|
|
|
|
|
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);
|
2026-05-06 16:08:00 +08:00
|
|
|
|
log(LogLevel.log, `deleted sessionId:${sessionId} by timeout.`);
|
2026-04-29 15:18:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取会话的连接ID列表
|
|
|
|
|
|
* @param sessionId 会话ID
|
|
|
|
|
|
* @returns 连接ID数组
|
|
|
|
|
|
*/
|
|
|
|
|
|
function _getConnection(sessionId: string): string[] {
|
|
|
|
|
|
// 检查超时会话
|
|
|
|
|
|
_checkForTimedOutSessions();
|
|
|
|
|
|
// 返回会话的连接ID集合的数组形式
|
|
|
|
|
|
return Array.from(clients.get(sessionId));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取会话的断开连接记录
|
|
|
|
|
|
* @param sessionId 会话ID
|
|
|
|
|
|
* @param fromTime 起始时间戳
|
|
|
|
|
|
* @returns 断开连接记录数组
|
|
|
|
|
|
*/
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取会话的offer列表
|
|
|
|
|
|
* @param sessionId 会话ID
|
|
|
|
|
|
* @param fromTime 起始时间戳
|
|
|
|
|
|
* @returns [连接ID, Offer]数组
|
|
|
|
|
|
*/
|
|
|
|
|
|
function _getOffer(sessionId: string, fromTime: number): [string, Offer][] {
|
|
|
|
|
|
let arrayOffers: [string, Offer][] = [];
|
|
|
|
|
|
|
|
|
|
|
|
// 如果offer映射不为空
|
|
|
|
|
|
if (offers.size != 0) {
|
|
|
|
|
|
// 处理私有模式
|
|
|
|
|
|
if (isPrivate) {
|
|
|
|
|
|
// 如果会话存在offer记录,获取该会话的offer列表
|
|
|
|
|
|
if (offers.has(sessionId)) {
|
|
|
|
|
|
arrayOffers = Array.from(offers.get(sessionId));
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 公共模式:获取所有其他会话的offer列表
|
|
|
|
|
|
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]])));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果指定了起始时间,过滤出时间戳大于等于起始时间的offer
|
|
|
|
|
|
if (fromTime > 0) {
|
|
|
|
|
|
arrayOffers = arrayOffers.filter((v) => v[1].datetime >= fromTime);
|
|
|
|
|
|
}
|
|
|
|
|
|
return arrayOffers;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取会话的answer列表
|
|
|
|
|
|
* @param sessionId 会话ID
|
|
|
|
|
|
* @param fromTime 起始时间戳
|
|
|
|
|
|
* @returns [连接ID, Answer]数组
|
|
|
|
|
|
*/
|
|
|
|
|
|
function _getAnswer(sessionId: string, fromTime: number): [string, Answer][] {
|
|
|
|
|
|
let arrayAnswers: [string, Answer][] = [];
|
|
|
|
|
|
|
|
|
|
|
|
// 如果answer映射不为空且会话存在answer记录,获取该会话的answer列表
|
|
|
|
|
|
if (answers.size != 0 && answers.has(sessionId)) {
|
|
|
|
|
|
arrayAnswers = Array.from(answers.get(sessionId));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果指定了起始时间,过滤出时间戳大于等于起始时间的answer
|
|
|
|
|
|
if (fromTime > 0) {
|
|
|
|
|
|
arrayAnswers = arrayAnswers.filter((v) => v[1].datetime >= fromTime);
|
|
|
|
|
|
}
|
|
|
|
|
|
return arrayAnswers;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取会话的candidate列表
|
|
|
|
|
|
* @param sessionId 会话ID
|
|
|
|
|
|
* @param fromTime 起始时间戳
|
|
|
|
|
|
* @returns [连接ID, Candidate]数组
|
|
|
|
|
|
*/
|
|
|
|
|
|
function _getCandidate(sessionId: string, fromTime: number): [string, Candidate][] {
|
|
|
|
|
|
// 获取会话的连接ID列表
|
|
|
|
|
|
const connectionIds = Array.from(clients.get(sessionId));
|
|
|
|
|
|
const arr: [string, Candidate][] = [];
|
|
|
|
|
|
// 遍历每个连接ID
|
|
|
|
|
|
for (const connectionId of connectionIds) {
|
|
|
|
|
|
// 获取连接对
|
|
|
|
|
|
const pair = connectionPair.get(connectionId);
|
|
|
|
|
|
if (pair == null) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 找到另一个会话ID
|
|
|
|
|
|
const otherSessionId = sessionId === pair[0] ? pair[1] : pair[0];
|
|
|
|
|
|
// 如果另一个会话不存在candidate记录或该连接ID不存在candidate记录,跳过
|
|
|
|
|
|
if (!candidates.get(otherSessionId) || !candidates.get(otherSessionId).get(connectionId)) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 获取该连接ID的candidate列表,并过滤出时间戳大于等于起始时间的candidate
|
|
|
|
|
|
const arrayCandidates = candidates.get(otherSessionId).get(connectionId)
|
|
|
|
|
|
.filter((v) => v.datetime >= fromTime);
|
|
|
|
|
|
// 如果没有符合条件的candidate,跳过
|
|
|
|
|
|
if (arrayCandidates.length === 0) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 将符合条件的candidate添加到结果数组中
|
|
|
|
|
|
for (const candidate of arrayCandidates) {
|
|
|
|
|
|
arr.push([connectionId, candidate]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return arr;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @swagger
|
|
|
|
|
|
* /signaling/answer:
|
|
|
|
|
|
* get:
|
|
|
|
|
|
* summary: 获取answer列表
|
|
|
|
|
|
* description: 获取当前会话的answer信令消息列表
|
|
|
|
|
|
* security:
|
|
|
|
|
|
* - sessionAuth: []
|
|
|
|
|
|
* parameters:
|
|
|
|
|
|
* - in: query
|
|
|
|
|
|
* name: fromtime
|
|
|
|
|
|
* schema:
|
|
|
|
|
|
* type: number
|
|
|
|
|
|
* description: 起始时间戳,用于过滤消息
|
|
|
|
|
|
* responses:
|
|
|
|
|
|
* 200:
|
|
|
|
|
|
* description: 成功获取answer列表
|
|
|
|
|
|
* content:
|
|
|
|
|
|
* application/json:
|
|
|
|
|
|
* schema:
|
|
|
|
|
|
* type: object
|
|
|
|
|
|
* properties:
|
|
|
|
|
|
* answers:
|
|
|
|
|
|
* type: array
|
|
|
|
|
|
* items:
|
|
|
|
|
|
* type: object
|
|
|
|
|
|
* properties:
|
|
|
|
|
|
* connectionId:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* description: 连接ID
|
|
|
|
|
|
* sdp:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* description: SDP描述
|
|
|
|
|
|
* type:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* description: 消息类型
|
|
|
|
|
|
* datetime:
|
|
|
|
|
|
* type: number
|
|
|
|
|
|
* description: 时间戳
|
|
|
|
|
|
*/
|
|
|
|
|
|
function getAnswer(req: Request, res: Response): void {
|
|
|
|
|
|
// 从请求查询参数中获取`fromtime`参数
|
|
|
|
|
|
const fromTime: number = req.query.fromtime ? Number(req.query.fromtime) : 0;
|
|
|
|
|
|
// 从请求头获取会话ID
|
|
|
|
|
|
const sessionId: string = req.header('session-id');
|
|
|
|
|
|
// 获取会话的answer列表
|
|
|
|
|
|
const answers: [string, Answer][] = _getAnswer(sessionId, fromTime);
|
|
|
|
|
|
// 返回JSON响应,包含answer列表
|
|
|
|
|
|
res.json({ answers: answers.map((v) => ({ connectionId: v[0], sdp: v[1].sdp, type: "answer", datetime: v[1].datetime })) });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @swagger
|
|
|
|
|
|
* /signaling/connection:
|
|
|
|
|
|
* get:
|
|
|
|
|
|
* summary: 获取连接列表
|
|
|
|
|
|
* description: 获取当前会话的连接列表
|
|
|
|
|
|
* security:
|
|
|
|
|
|
* - sessionAuth: []
|
|
|
|
|
|
* responses:
|
|
|
|
|
|
* 200:
|
|
|
|
|
|
* description: 成功获取连接列表
|
|
|
|
|
|
* content:
|
|
|
|
|
|
* application/json:
|
|
|
|
|
|
* schema:
|
|
|
|
|
|
* type: object
|
|
|
|
|
|
* properties:
|
|
|
|
|
|
* connections:
|
|
|
|
|
|
* type: array
|
|
|
|
|
|
* items:
|
|
|
|
|
|
* type: object
|
|
|
|
|
|
* properties:
|
|
|
|
|
|
* connectionId:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* description: 连接ID
|
|
|
|
|
|
* type:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* description: 消息类型
|
|
|
|
|
|
* datetime:
|
|
|
|
|
|
* type: number
|
|
|
|
|
|
* description: 时间戳
|
|
|
|
|
|
*/
|
|
|
|
|
|
function getConnection(req: Request, res: Response): void {
|
|
|
|
|
|
// 从请求头获取会话ID
|
|
|
|
|
|
const sessionId: string = req.header('session-id');
|
|
|
|
|
|
// 获取会话的连接ID列表
|
|
|
|
|
|
const connections = _getConnection(sessionId);
|
|
|
|
|
|
// 返回JSON响应,包含连接列表
|
|
|
|
|
|
res.json({ connections: connections.map((v) => ({ connectionId: v, type: "connect", datetime: Date.now() })) });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @swagger
|
|
|
|
|
|
* /signaling/offer:
|
|
|
|
|
|
* get:
|
|
|
|
|
|
* summary: 获取offer列表
|
|
|
|
|
|
* description: 获取当前会话的offer信令消息列表
|
|
|
|
|
|
* security:
|
|
|
|
|
|
* - sessionAuth: []
|
|
|
|
|
|
* parameters:
|
|
|
|
|
|
* - in: query
|
|
|
|
|
|
* name: fromtime
|
|
|
|
|
|
* schema:
|
|
|
|
|
|
* type: number
|
|
|
|
|
|
* description: 起始时间戳,用于过滤消息
|
|
|
|
|
|
* responses:
|
|
|
|
|
|
* 200:
|
|
|
|
|
|
* description: 成功获取offer列表
|
|
|
|
|
|
* content:
|
|
|
|
|
|
* application/json:
|
|
|
|
|
|
* schema:
|
|
|
|
|
|
* type: object
|
|
|
|
|
|
* properties:
|
|
|
|
|
|
* offers:
|
|
|
|
|
|
* type: array
|
|
|
|
|
|
* items:
|
|
|
|
|
|
* type: object
|
|
|
|
|
|
* properties:
|
|
|
|
|
|
* connectionId:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* description: 连接ID
|
|
|
|
|
|
* sdp:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* description: SDP描述
|
|
|
|
|
|
* polite:
|
|
|
|
|
|
* type: boolean
|
|
|
|
|
|
* description: 是否为polite模式
|
|
|
|
|
|
* type:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* description: 消息类型
|
|
|
|
|
|
* datetime:
|
|
|
|
|
|
* type: number
|
|
|
|
|
|
* description: 时间戳
|
|
|
|
|
|
*/
|
|
|
|
|
|
function getOffer(req: Request, res: Response): void {
|
|
|
|
|
|
// 从请求查询参数中获取`fromtime`参数
|
|
|
|
|
|
const fromTime: number = req.query.fromtime ? Number(req.query.fromtime) : 0;
|
|
|
|
|
|
// 从请求头获取会话ID
|
|
|
|
|
|
const sessionId: string = req.header('session-id');
|
|
|
|
|
|
// 获取会话的offer列表
|
|
|
|
|
|
const offers = _getOffer(sessionId, fromTime);
|
|
|
|
|
|
// 返回JSON响应,包含offer列表
|
|
|
|
|
|
res.json({ offers: offers.map((v) => ({ connectionId: v[0], sdp: v[1].sdp, polite: v[1].polite, type: "offer", datetime: v[1].datetime })) });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @swagger
|
|
|
|
|
|
* /signaling/candidate:
|
|
|
|
|
|
* get:
|
|
|
|
|
|
* summary: 获取candidate列表
|
|
|
|
|
|
* description: 获取当前会话的candidate信令消息列表
|
|
|
|
|
|
* security:
|
|
|
|
|
|
* - sessionAuth: []
|
|
|
|
|
|
* parameters:
|
|
|
|
|
|
* - in: query
|
|
|
|
|
|
* name: fromtime
|
|
|
|
|
|
* schema:
|
|
|
|
|
|
* type: number
|
|
|
|
|
|
* description: 起始时间戳,用于过滤消息
|
|
|
|
|
|
* responses:
|
|
|
|
|
|
* 200:
|
|
|
|
|
|
* description: 成功获取candidate列表
|
|
|
|
|
|
* content:
|
|
|
|
|
|
* application/json:
|
|
|
|
|
|
* schema:
|
|
|
|
|
|
* type: object
|
|
|
|
|
|
* properties:
|
|
|
|
|
|
* candidates:
|
|
|
|
|
|
* type: array
|
|
|
|
|
|
* items:
|
|
|
|
|
|
* type: object
|
|
|
|
|
|
* properties:
|
|
|
|
|
|
* connectionId:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* description: 连接ID
|
|
|
|
|
|
* candidate:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* description: ICE候选者信息
|
|
|
|
|
|
* sdpMLineIndex:
|
|
|
|
|
|
* type: number
|
|
|
|
|
|
* description: SDP媒体行索引
|
|
|
|
|
|
* sdpMid:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* description: SDP媒体ID
|
|
|
|
|
|
* type:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* description: 消息类型
|
|
|
|
|
|
* datetime:
|
|
|
|
|
|
* type: number
|
|
|
|
|
|
* description: 时间戳
|
|
|
|
|
|
*/
|
|
|
|
|
|
function getCandidate(req: Request, res: Response): void {
|
|
|
|
|
|
// 从请求查询参数中获取`fromtime`参数
|
|
|
|
|
|
const fromTime: number = req.query.fromtime ? Number(req.query.fromtime) : 0;
|
|
|
|
|
|
// 从请求头获取会话ID
|
|
|
|
|
|
const sessionId: string = req.header('session-id');
|
|
|
|
|
|
// 获取会话的candidate列表
|
|
|
|
|
|
const candidates = _getCandidate(sessionId, fromTime);
|
|
|
|
|
|
// 返回JSON响应,包含candidate列表
|
|
|
|
|
|
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 })) });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @swagger
|
|
|
|
|
|
* /signaling:
|
|
|
|
|
|
* get:
|
|
|
|
|
|
* summary: 获取所有信令消息
|
|
|
|
|
|
* description: 获取当前会话的所有信令消息,包括连接、断开连接、offer、answer和candidate
|
|
|
|
|
|
* security:
|
|
|
|
|
|
* - sessionAuth: []
|
|
|
|
|
|
* parameters:
|
|
|
|
|
|
* - in: query
|
|
|
|
|
|
* name: fromtime
|
|
|
|
|
|
* schema:
|
|
|
|
|
|
* type: number
|
|
|
|
|
|
* description: 起始时间戳,用于过滤消息
|
|
|
|
|
|
* responses:
|
|
|
|
|
|
* 200:
|
|
|
|
|
|
* description: 成功获取所有信令消息
|
|
|
|
|
|
* content:
|
|
|
|
|
|
* application/json:
|
|
|
|
|
|
* schema:
|
|
|
|
|
|
* type: object
|
|
|
|
|
|
* properties:
|
|
|
|
|
|
* messages:
|
|
|
|
|
|
* type: array
|
|
|
|
|
|
* items:
|
|
|
|
|
|
* type: object
|
|
|
|
|
|
* properties:
|
|
|
|
|
|
* connectionId:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* description: 连接ID
|
|
|
|
|
|
* type:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* description: 消息类型
|
|
|
|
|
|
* datetime:
|
|
|
|
|
|
* type: number
|
|
|
|
|
|
* description: 时间戳
|
|
|
|
|
|
* sdp:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* description: SDP描述(仅offer和answer消息)
|
|
|
|
|
|
* polite:
|
|
|
|
|
|
* type: boolean
|
|
|
|
|
|
* description: 是否为polite模式(仅offer消息)
|
|
|
|
|
|
* candidate:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* description: ICE候选者信息(仅candidate消息)
|
|
|
|
|
|
* sdpMLineIndex:
|
|
|
|
|
|
* type: number
|
|
|
|
|
|
* description: SDP媒体行索引(仅candidate消息)
|
|
|
|
|
|
* sdpMid:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* description: SDP媒体ID(仅candidate消息)
|
|
|
|
|
|
* datetime:
|
|
|
|
|
|
* type: number
|
|
|
|
|
|
* description: 当前时间戳
|
|
|
|
|
|
*/
|
|
|
|
|
|
function getAll(req: Request, res: Response): void {
|
|
|
|
|
|
// 从请求查询参数中获取`fromtime`参数
|
|
|
|
|
|
const fromTime: number = req.query.fromtime ? Number(req.query.fromtime) : 0;
|
|
|
|
|
|
// 从请求头获取会话ID
|
|
|
|
|
|
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);
|
|
|
|
|
|
// 返回JSON响应,包含所有信令消息
|
|
|
|
|
|
res.json({ messages: array, datetime: datetime });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @swagger
|
|
|
|
|
|
* /signaling:
|
|
|
|
|
|
* put:
|
|
|
|
|
|
* summary: 创建会话
|
|
|
|
|
|
* description: 创建一个新的会话,并返回会话ID
|
|
|
|
|
|
* responses:
|
|
|
|
|
|
* 200:
|
|
|
|
|
|
* description: 成功创建会话
|
|
|
|
|
|
* content:
|
|
|
|
|
|
* application/json:
|
|
|
|
|
|
* schema:
|
|
|
|
|
|
* type: object
|
|
|
|
|
|
* properties:
|
|
|
|
|
|
* sessionId:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* description: 新创建的会话ID
|
|
|
|
|
|
*/
|
|
|
|
|
|
function createSession(sessionId: string, res: Response): void;
|
|
|
|
|
|
function createSession(req: Request, res: Response): void;
|
|
|
|
|
|
|
|
|
|
|
|
function createSession(req: string | Request, res: Response): void {
|
|
|
|
|
|
// 确定会话ID
|
|
|
|
|
|
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, []);
|
|
|
|
|
|
// 返回JSON响应,包含会话ID
|
|
|
|
|
|
res.json({ sessionId: sessionId });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @swagger
|
|
|
|
|
|
* /signaling:
|
|
|
|
|
|
* delete:
|
|
|
|
|
|
* summary: 删除会话
|
|
|
|
|
|
* description: 删除当前会话及其所有连接
|
|
|
|
|
|
* security:
|
|
|
|
|
|
* - sessionAuth: []
|
|
|
|
|
|
* responses:
|
|
|
|
|
|
* 200:
|
|
|
|
|
|
* description: 成功删除会话
|
|
|
|
|
|
*/
|
|
|
|
|
|
function deleteSession(req: Request, res: Response): void {
|
|
|
|
|
|
// 从请求头获取会话ID
|
|
|
|
|
|
const id: string = req.header('session-id');
|
|
|
|
|
|
// 删除会话
|
|
|
|
|
|
_deleteSession(id);
|
|
|
|
|
|
// 返回200状态码表示请求处理成功
|
|
|
|
|
|
res.sendStatus(200);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @swagger
|
|
|
|
|
|
* /signaling/connection:
|
|
|
|
|
|
* put:
|
|
|
|
|
|
* summary: 创建连接
|
|
|
|
|
|
* description: 创建一个新的连接,并返回连接信息
|
|
|
|
|
|
* security:
|
|
|
|
|
|
* - sessionAuth: []
|
|
|
|
|
|
* requestBody:
|
|
|
|
|
|
* required: true
|
|
|
|
|
|
* content:
|
|
|
|
|
|
* application/json:
|
|
|
|
|
|
* schema:
|
|
|
|
|
|
* type: object
|
|
|
|
|
|
* properties:
|
|
|
|
|
|
* connectionId:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* description: 连接ID
|
|
|
|
|
|
* responses:
|
|
|
|
|
|
* 200:
|
|
|
|
|
|
* description: 成功创建连接
|
|
|
|
|
|
* content:
|
|
|
|
|
|
* application/json:
|
|
|
|
|
|
* schema:
|
|
|
|
|
|
* type: object
|
|
|
|
|
|
* properties:
|
|
|
|
|
|
* connectionId:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* description: 连接ID
|
|
|
|
|
|
* polite:
|
|
|
|
|
|
* type: boolean
|
|
|
|
|
|
* description: 是否为polite模式
|
|
|
|
|
|
* type:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* description: 消息类型
|
|
|
|
|
|
* datetime:
|
|
|
|
|
|
* type: number
|
|
|
|
|
|
* description: 时间戳
|
|
|
|
|
|
* 400:
|
|
|
|
|
|
* description: 请求参数错误
|
|
|
|
|
|
*/
|
|
|
|
|
|
function createConnection(req: Request, res: Response): void {
|
|
|
|
|
|
// 从请求头获取会话ID
|
|
|
|
|
|
const sessionId: string = req.header('session-id');
|
|
|
|
|
|
// 从请求体获取连接ID
|
|
|
|
|
|
const { connectionId } = req.body;
|
|
|
|
|
|
// 获取会话的最后请求时间
|
|
|
|
|
|
const datetime = lastRequestedTime.get(sessionId);
|
|
|
|
|
|
|
|
|
|
|
|
// 检查连接ID是否存在
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
// 检查连接ID是否已被使用
|
|
|
|
|
|
if (pair[0] != null && pair[1] != null) {
|
|
|
|
|
|
const err = new Error(`${connectionId}: This connection id is already used.`);
|
2026-05-06 16:08:00 +08:00
|
|
|
|
log(LogLevel.warn, err.message);
|
2026-04-29 15:18:30 +08:00
|
|
|
|
res.status(400).send({ error: err });
|
|
|
|
|
|
return;
|
|
|
|
|
|
} else if (pair[0] != null) {
|
|
|
|
|
|
// 找到配对连接
|
|
|
|
|
|
connectionPair.set(connectionId, [pair[0], sessionId]);
|
|
|
|
|
|
// 添加连接ID到另一个会话
|
|
|
|
|
|
const map = getOrCreateConnectionIds(pair[0]);
|
|
|
|
|
|
map.add(connectionId);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 创建新的连接对
|
|
|
|
|
|
connectionPair.set(connectionId, [sessionId, null]);
|
|
|
|
|
|
polite = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 添加连接ID到当前会话
|
|
|
|
|
|
const connectionIds = getOrCreateConnectionIds(sessionId);
|
|
|
|
|
|
connectionIds.add(connectionId);
|
|
|
|
|
|
// 返回JSON响应,包含连接信息
|
|
|
|
|
|
res.json({ connectionId: connectionId, polite: polite, type: "connect", datetime: datetime });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @swagger
|
|
|
|
|
|
* /signaling/connection:
|
|
|
|
|
|
* delete:
|
|
|
|
|
|
* summary: 删除连接
|
|
|
|
|
|
* description: 删除指定的连接
|
|
|
|
|
|
* security:
|
|
|
|
|
|
* - sessionAuth: []
|
|
|
|
|
|
* requestBody:
|
|
|
|
|
|
* required: true
|
|
|
|
|
|
* content:
|
|
|
|
|
|
* application/json:
|
|
|
|
|
|
* schema:
|
|
|
|
|
|
* type: object
|
|
|
|
|
|
* properties:
|
|
|
|
|
|
* connectionId:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* description: 连接ID
|
|
|
|
|
|
* responses:
|
|
|
|
|
|
* 200:
|
|
|
|
|
|
* description: 成功删除连接
|
|
|
|
|
|
* content:
|
|
|
|
|
|
* application/json:
|
|
|
|
|
|
* schema:
|
|
|
|
|
|
* type: object
|
|
|
|
|
|
* properties:
|
|
|
|
|
|
* connectionId:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* description: 连接ID
|
|
|
|
|
|
*/
|
|
|
|
|
|
function deleteConnection(req: Request, res: Response): void {
|
|
|
|
|
|
// 从请求头获取会话ID
|
|
|
|
|
|
const sessionId: string = req.header('session-id');
|
|
|
|
|
|
// 从请求体获取连接ID
|
|
|
|
|
|
const { connectionId } = req.body;
|
|
|
|
|
|
// 获取会话的最后请求时间
|
|
|
|
|
|
const datetime = lastRequestedTime.get(sessionId);
|
|
|
|
|
|
|
|
|
|
|
|
// 删除连接
|
|
|
|
|
|
_deleteConnection(sessionId, connectionId, datetime);
|
|
|
|
|
|
|
|
|
|
|
|
// 返回JSON响应,包含连接ID
|
|
|
|
|
|
res.json({ connectionId: connectionId });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @swagger
|
|
|
|
|
|
* /signaling/offer:
|
|
|
|
|
|
* post:
|
|
|
|
|
|
* summary: 发送offer信令
|
|
|
|
|
|
* description: 发送offer信令消息
|
|
|
|
|
|
* security:
|
|
|
|
|
|
* - sessionAuth: []
|
|
|
|
|
|
* requestBody:
|
|
|
|
|
|
* required: true
|
|
|
|
|
|
* content:
|
|
|
|
|
|
* application/json:
|
|
|
|
|
|
* schema:
|
|
|
|
|
|
* type: object
|
|
|
|
|
|
* properties:
|
|
|
|
|
|
* connectionId:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* description: 连接ID
|
|
|
|
|
|
* sdp:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* description: SDP描述
|
|
|
|
|
|
* responses:
|
|
|
|
|
|
* 200:
|
|
|
|
|
|
* description: 成功发送offer信令
|
|
|
|
|
|
*/
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @swagger
|
|
|
|
|
|
* /signaling/answer:
|
|
|
|
|
|
* post:
|
|
|
|
|
|
* summary: 发送answer信令
|
|
|
|
|
|
* description: 发送answer信令消息
|
|
|
|
|
|
* security:
|
|
|
|
|
|
* - sessionAuth: []
|
|
|
|
|
|
* requestBody:
|
|
|
|
|
|
* required: true
|
|
|
|
|
|
* content:
|
|
|
|
|
|
* application/json:
|
|
|
|
|
|
* schema:
|
|
|
|
|
|
* type: object
|
|
|
|
|
|
* properties:
|
|
|
|
|
|
* connectionId:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* description: 连接ID
|
|
|
|
|
|
* sdp:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* description: SDP描述
|
|
|
|
|
|
* responses:
|
|
|
|
|
|
* 200:
|
|
|
|
|
|
* description: 成功发送answer信令
|
|
|
|
|
|
*/
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @swagger
|
|
|
|
|
|
* /signaling/candidate:
|
|
|
|
|
|
* post:
|
|
|
|
|
|
* summary: 发送candidate信令
|
|
|
|
|
|
* description: 发送candidate信令消息
|
|
|
|
|
|
* security:
|
|
|
|
|
|
* - sessionAuth: []
|
|
|
|
|
|
* requestBody:
|
|
|
|
|
|
* required: true
|
|
|
|
|
|
* content:
|
|
|
|
|
|
* application/json:
|
|
|
|
|
|
* schema:
|
|
|
|
|
|
* type: object
|
|
|
|
|
|
* properties:
|
|
|
|
|
|
* connectionId:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* description: 连接ID
|
|
|
|
|
|
* candidate:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* description: ICE候选者信息
|
|
|
|
|
|
* sdpMLineIndex:
|
|
|
|
|
|
* type: number
|
|
|
|
|
|
* description: SDP媒体行索引
|
|
|
|
|
|
* sdpMid:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* description: SDP媒体ID
|
|
|
|
|
|
* responses:
|
|
|
|
|
|
* 200:
|
|
|
|
|
|
* description: 成功发送candidate信令
|
|
|
|
|
|
*/
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @swagger
|
|
|
|
|
|
* /signaling/rooms:
|
|
|
|
|
|
* get:
|
|
|
|
|
|
* summary: 获取房间和用户信息
|
|
|
|
|
|
* description: 获取所有房间的信息,包括房间ID和链接的用户
|
|
|
|
|
|
* security:
|
|
|
|
|
|
* - sessionAuth: []
|
|
|
|
|
|
* responses:
|
|
|
|
|
|
* 200:
|
|
|
|
|
|
* description: 成功获取房间和用户信息
|
|
|
|
|
|
* content:
|
|
|
|
|
|
* application/json:
|
|
|
|
|
|
* schema:
|
|
|
|
|
|
* type: object
|
|
|
|
|
|
* properties:
|
|
|
|
|
|
* rooms:
|
|
|
|
|
|
* type: array
|
|
|
|
|
|
* items:
|
|
|
|
|
|
* type: object
|
|
|
|
|
|
* properties:
|
|
|
|
|
|
* roomId:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* description: 房间ID
|
|
|
|
|
|
* users:
|
|
|
|
|
|
* type: array
|
|
|
|
|
|
* items:
|
|
|
|
|
|
* type: object
|
|
|
|
|
|
* properties:
|
|
|
|
|
|
* sessionId:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* description: 会话ID
|
|
|
|
|
|
* connected:
|
|
|
|
|
|
* type: boolean
|
|
|
|
|
|
* description: 连接状态
|
|
|
|
|
|
* userCount:
|
|
|
|
|
|
* type: number
|
|
|
|
|
|
* description: 用户数量
|
|
|
|
|
|
* totalRooms:
|
|
|
|
|
|
* type: number
|
|
|
|
|
|
* description: 总房间数
|
|
|
|
|
|
*/
|
|
|
|
|
|
function onGetConnections(req: Request, res: Response): void {
|
|
|
|
|
|
|
|
|
|
|
|
// 收集所有房间ID和链接用户信息
|
|
|
|
|
|
const rooms = [];
|
|
|
|
|
|
|
|
|
|
|
|
// 遍历所有连接对
|
|
|
|
|
|
for (const [connectionId, pair] of Array.from(connectionPair.entries())) {
|
|
|
|
|
|
// 收集房间中的用户信息
|
|
|
|
|
|
const users = [];
|
|
|
|
|
|
|
|
|
|
|
|
// 添加第一个用户
|
|
|
|
|
|
if (pair[0] && clients.has(pair[0])) {
|
|
|
|
|
|
users.push({
|
|
|
|
|
|
sessionId: pair[0],
|
|
|
|
|
|
connected: true
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 添加第二个用户
|
|
|
|
|
|
if (pair[1] && clients.has(pair[1])) {
|
|
|
|
|
|
users.push({
|
|
|
|
|
|
sessionId: pair[1],
|
|
|
|
|
|
connected: true
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 添加房间信息
|
|
|
|
|
|
rooms.push({
|
|
|
|
|
|
roomId: connectionId,
|
|
|
|
|
|
users: users,
|
|
|
|
|
|
userCount: users.length
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
res.json({ rooms: rooms, totalRooms: rooms.length });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @swagger
|
|
|
|
|
|
* /signaling/connection-ids:
|
|
|
|
|
|
* get:
|
|
|
|
|
|
* summary: 获取所有连接ID
|
|
|
|
|
|
* description: 获取所有当前活跃的连接ID
|
|
|
|
|
|
* security:
|
|
|
|
|
|
* - sessionAuth: []
|
|
|
|
|
|
* responses:
|
|
|
|
|
|
* 200:
|
|
|
|
|
|
* description: 成功获取连接ID列表
|
|
|
|
|
|
* content:
|
|
|
|
|
|
* application/json:
|
|
|
|
|
|
* schema:
|
|
|
|
|
|
* type: object
|
|
|
|
|
|
* properties:
|
|
|
|
|
|
* connectionIds:
|
|
|
|
|
|
* type: array
|
|
|
|
|
|
* items:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* description: 连接ID
|
|
|
|
|
|
* totalCount:
|
|
|
|
|
|
* type: number
|
|
|
|
|
|
* description: 总连接数
|
|
|
|
|
|
*/
|
|
|
|
|
|
function getAllConnectionIds(req: Request, res: Response): void {
|
|
|
|
|
|
// 获取所有连接ID
|
|
|
|
|
|
const connectionIds = onGetAllConnectionIds();
|
|
|
|
|
|
// 返回JSON响应,包含连接ID列表和总数量
|
|
|
|
|
|
res.json({ connectionIds: connectionIds, totalCount: connectionIds.length });
|
|
|
|
|
|
}
|
2026-05-16 22:22:34 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取在线WebSocket用户列表
|
|
|
|
|
|
* @param req HTTP请求对象
|
|
|
|
|
|
* @param res HTTP响应对象
|
|
|
|
|
|
*/
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @swagger
|
|
|
|
|
|
* /signaling/users:
|
|
|
|
|
|
* get:
|
2026-05-16 23:07:08 +08:00
|
|
|
|
* summary: 获取全部在线WebSocket用户列表
|
|
|
|
|
|
* description: 获取所有当前已建立WebSocket连接的用户,包括未加入房间的大厅用户;支持按 connectionId 过滤指定房间内的用户
|
2026-05-16 22:22:34 +08:00
|
|
|
|
* parameters:
|
|
|
|
|
|
* - in: query
|
|
|
|
|
|
* name: connectionId
|
|
|
|
|
|
* schema:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* required: false
|
|
|
|
|
|
* description: 连接ID,传入时仅返回该房间内的在线用户
|
|
|
|
|
|
* responses:
|
|
|
|
|
|
* 200:
|
|
|
|
|
|
* description: 成功获取在线用户列表
|
|
|
|
|
|
* content:
|
|
|
|
|
|
* application/json:
|
|
|
|
|
|
* schema:
|
|
|
|
|
|
* type: object
|
|
|
|
|
|
* properties:
|
|
|
|
|
|
* users:
|
|
|
|
|
|
* type: array
|
|
|
|
|
|
* items:
|
|
|
|
|
|
* type: object
|
|
|
|
|
|
* properties:
|
|
|
|
|
|
* connectionId:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* description: 所属连接ID
|
|
|
|
|
|
* participantId:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* description: 参与者ID
|
|
|
|
|
|
* role:
|
|
|
|
|
|
* type: string
|
2026-05-16 23:07:08 +08:00
|
|
|
|
* enum: [host, participant, idle]
|
2026-05-16 22:22:34 +08:00
|
|
|
|
* description: 角色
|
2026-05-16 23:07:08 +08:00
|
|
|
|
* socketId:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* description: WebSocket连接ID
|
2026-05-16 22:22:34 +08:00
|
|
|
|
* userId:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* description: 用户ID
|
|
|
|
|
|
* name:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* description: 用户名称
|
|
|
|
|
|
* avatar:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* description: 用户头像URL
|
|
|
|
|
|
* totalCount:
|
|
|
|
|
|
* type: number
|
2026-05-16 23:07:08 +08:00
|
|
|
|
* description: 在线WebSocket用户总数
|
2026-05-16 22:22:34 +08:00
|
|
|
|
*/
|
|
|
|
|
|
function getOnlineUsers(req: Request, res: Response): void {
|
|
|
|
|
|
const connectionId = typeof req.query.connectionId === 'string' ? req.query.connectionId : undefined;
|
|
|
|
|
|
const users = onGetWsOnlineUsers(connectionId);
|
|
|
|
|
|
res.json({ users: users, totalCount: users.length });
|
|
|
|
|
|
}
|
2026-04-29 15:18:30 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 导出HTTP处理器函数
|
|
|
|
|
|
*/
|
|
|
|
|
|
export {
|
|
|
|
|
|
reset, // 重置处理器状态
|
|
|
|
|
|
checkSessionId, // 检查会话ID是否有效
|
|
|
|
|
|
getAll, // 获取所有信令消息
|
|
|
|
|
|
getConnection, // 获取连接列表
|
|
|
|
|
|
getOffer, // 获取offer列表
|
|
|
|
|
|
getAnswer, // 获取answer列表
|
|
|
|
|
|
getCandidate, // 获取candidate列表
|
|
|
|
|
|
createSession, // 创建会话
|
|
|
|
|
|
deleteSession, // 删除会话
|
|
|
|
|
|
createConnection, // 创建连接
|
|
|
|
|
|
deleteConnection, // 删除连接
|
|
|
|
|
|
postOffer, // 处理offer信令消息
|
|
|
|
|
|
postAnswer, // 处理answer信令消息
|
|
|
|
|
|
postCandidate, // 处理candidate信令消息
|
|
|
|
|
|
onGetConnections, // 获取房间和用户信息
|
2026-05-16 22:22:34 +08:00
|
|
|
|
getAllConnectionIds, // 获取所有连接ID
|
|
|
|
|
|
getOnlineUsers // 获取在线WebSocket用户列表
|
2026-04-29 15:18:30 +08:00
|
|
|
|
};
|