Files
video_socket-server/src/class/httphandler.ts
2026-05-16 23:07:08 +08:00

1194 lines
37 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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';
import { onGetAllConnectionIds, onGetOnlineUsers as onGetWsOnlineUsers } from './websockethandler';
import { log, LogLevel } from '../log';
/**
* 断开连接记录类
* 用于记录断开连接的信息
*/
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);
log(LogLevel.log, `deleted sessionId:${sessionId} by timeout.`);
}
}
/**
* 获取会话的连接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.`);
log(LogLevel.warn, err.message);
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 });
}
/**
* 获取在线WebSocket用户列表
* @param req HTTP请求对象
* @param res HTTP响应对象
*/
/**
* @swagger
* /signaling/users:
* get:
* summary: 获取全部在线WebSocket用户列表
* description: 获取所有当前已建立WebSocket连接的用户包括未加入房间的大厅用户支持按 connectionId 过滤指定房间内的用户
* 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
* enum: [host, participant, idle]
* description: 角色
* socketId:
* type: string
* description: WebSocket连接ID
* userId:
* type: string
* description: 用户ID
* name:
* type: string
* description: 用户名称
* avatar:
* type: string
* description: 用户头像URL
* totalCount:
* type: number
* description: 在线WebSocket用户总数
*/
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 });
}
/**
* 导出HTTP处理器函数
*/
export {
reset, // 重置处理器状态
checkSessionId, // 检查会话ID是否有效
getAll, // 获取所有信令消息
getConnection, // 获取连接列表
getOffer, // 获取offer列表
getAnswer, // 获取answer列表
getCandidate, // 获取candidate列表
createSession, // 创建会话
deleteSession, // 删除会话
createConnection, // 创建连接
deleteConnection, // 删除连接
postOffer, // 处理offer信令消息
postAnswer, // 处理answer信令消息
postCandidate, // 处理candidate信令消息
onGetConnections, // 获取房间和用户信息
getAllConnectionIds, // 获取所有连接ID
getOnlineUsers // 获取在线WebSocket用户列表
};