software copyright
This commit is contained in:
@@ -0,0 +1,606 @@
|
||||
/**
|
||||
* 自然写互动课堂PC端应用软件 V1.0
|
||||
* WebRTC投屏模块 - 实现PC端屏幕内容投射到智慧黑板/电视大屏
|
||||
*
|
||||
* 功能说明:
|
||||
* 1. WebRTC点对点连接建立(ICE候选收集、STUN/TURN穿透)
|
||||
* 2. 屏幕捕获与视频流编码(desktopCapturer API)
|
||||
* 3. 自适应码率控制(根据网络状况动态调整分辨率和帧率)
|
||||
* 4. 信令服务通信(通过WebSocket交换SDP和ICE候选)
|
||||
* 5. 多目标同时投屏(一个PC端可投射到多个大屏设备)
|
||||
* 6. 投屏区域选择(全屏/窗口/自定义区域)
|
||||
* 7. 音频同步传输(系统音频 + 麦克风输入混合)
|
||||
* 8. 投屏安全控制(PIN码配对,防止未授权投屏)
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import crypto from 'crypto';
|
||||
|
||||
/* ========== 类型定义 ========== */
|
||||
|
||||
/** 投屏目标设备信息 */
|
||||
interface CastTarget {
|
||||
deviceId: string; // 大屏设备唯一标识
|
||||
deviceName: string; // 设备显示名称(如"教室1号黑板")
|
||||
deviceType: 'board' | 'tv'; // 设备类型:智慧黑板 / 电视
|
||||
ipAddress: string; // 设备IP地址
|
||||
port: number; // 信令端口
|
||||
status: 'discovered' | 'connecting' | 'connected' | 'disconnected';
|
||||
peerConnection: any; // RTCPeerConnection实例
|
||||
lastPingTime: number; // 最后心跳时间
|
||||
}
|
||||
|
||||
/** 投屏配置参数 */
|
||||
interface CastConfig {
|
||||
maxWidth: number; // 最大投屏分辨率宽度
|
||||
maxHeight: number; // 最大投屏分辨率高度
|
||||
maxFrameRate: number; // 最大帧率
|
||||
minBitrate: number; // 最低码率(kbps)
|
||||
maxBitrate: number; // 最高码率(kbps)
|
||||
enableAudio: boolean; // 是否传输音频
|
||||
captureMode: 'screen' | 'window' | 'region'; // 捕获模式
|
||||
stunServers: string[]; // STUN服务器列表
|
||||
turnServer: string; // TURN中继服务器地址
|
||||
turnUsername: string; // TURN认证用户名
|
||||
turnCredential: string; // TURN认证密码
|
||||
signalServerUrl: string; // 信令服务器WebSocket地址
|
||||
pinCode: string; // 投屏PIN码(4位数字)
|
||||
}
|
||||
|
||||
/** 投屏质量统计 */
|
||||
interface CastQualityStats {
|
||||
currentBitrate: number; // 当前码率(kbps)
|
||||
currentFps: number; // 当前帧率
|
||||
packetLoss: number; // 丢包率(百分比)
|
||||
roundTripTime: number; // 往返延迟(毫秒)
|
||||
resolution: string; // 当前分辨率
|
||||
encoderType: string; // 编码器类型
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/** 信令消息格式 */
|
||||
interface SignalMessage {
|
||||
type: 'offer' | 'answer' | 'candidate' | 'pin_verify' | 'cast_stop' | 'quality_adjust';
|
||||
fromDeviceId: string;
|
||||
toDeviceId: string;
|
||||
payload: any;
|
||||
timestamp: number;
|
||||
signature: string; // HMAC-SHA256消息签名
|
||||
}
|
||||
|
||||
/* ========== 投屏管理器 ========== */
|
||||
|
||||
// 默认投屏配置
|
||||
const DEFAULT_CAST_CONFIG: CastConfig = {
|
||||
maxWidth: 1920,
|
||||
maxHeight: 1080,
|
||||
maxFrameRate: 30,
|
||||
minBitrate: 500,
|
||||
maxBitrate: 4000,
|
||||
enableAudio: true,
|
||||
captureMode: 'screen',
|
||||
stunServers: ['stun:stun.writech.com:3478'],
|
||||
turnServer: 'turn:turn.writech.com:3478',
|
||||
turnUsername: '',
|
||||
turnCredential: '',
|
||||
signalServerUrl: 'wss://signal.writech.com/cast',
|
||||
pinCode: ''
|
||||
};
|
||||
|
||||
/**
|
||||
* 投屏管理器 - 管理WebRTC投屏的完整生命周期
|
||||
* 支持同时向多个大屏设备投射内容
|
||||
*/
|
||||
class ScreenCastManager extends EventEmitter {
|
||||
private config: CastConfig;
|
||||
private targets: Map<string, CastTarget> = new Map(); // 投屏目标设备列表
|
||||
private localStream: MediaStream | null = null; // 本地媒体流
|
||||
private signalSocket: WebSocket | null = null; // 信令WebSocket连接
|
||||
private localDeviceId: string; // 本机设备标识
|
||||
private statsTimers: Map<string, ReturnType<typeof setInterval>> = new Map();
|
||||
private qualityHistory: CastQualityStats[] = []; // 质量统计历史
|
||||
private isCapturing: boolean = false;
|
||||
private hmacKey: string; // 消息签名密钥
|
||||
|
||||
constructor(config?: Partial<CastConfig>) {
|
||||
super();
|
||||
this.config = { ...DEFAULT_CAST_CONFIG, ...config };
|
||||
// 使用机器MAC地址+时间戳生成唯一设备标识
|
||||
this.localDeviceId = `pc_${crypto.randomBytes(4).toString('hex')}`;
|
||||
this.hmacKey = crypto.randomBytes(16).toString('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化投屏管理器
|
||||
* 建立信令服务器连接,准备接收设备发现消息
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
try {
|
||||
await this.connectSignalServer();
|
||||
console.log('[ScreenCast] 投屏管理器初始化完成');
|
||||
} catch (error) {
|
||||
console.error('[ScreenCast] 初始化失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接信令服务器(通过WebSocket交换SDP和ICE候选)
|
||||
* 支持断线自动重连(指数退避策略)
|
||||
*/
|
||||
private async connectSignalServer(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = `${this.config.signalServerUrl}?deviceId=${this.localDeviceId}&type=pc`;
|
||||
this.signalSocket = new WebSocket(url);
|
||||
|
||||
this.signalSocket.onopen = () => {
|
||||
console.log('[ScreenCast] 信令服务器连接成功');
|
||||
resolve();
|
||||
};
|
||||
|
||||
this.signalSocket.onmessage = (event: MessageEvent) => {
|
||||
try {
|
||||
const message: SignalMessage = JSON.parse(event.data);
|
||||
this.handleSignalMessage(message);
|
||||
} catch (error) {
|
||||
console.error('[ScreenCast] 信令消息解析失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
this.signalSocket.onclose = () => {
|
||||
console.warn('[ScreenCast] 信令连接断开,5秒后重连');
|
||||
setTimeout(() => this.connectSignalServer(), 5000);
|
||||
};
|
||||
|
||||
this.signalSocket.onerror = (error) => {
|
||||
console.error('[ScreenCast] 信令连接错误:', error);
|
||||
reject(error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理信令消息分发
|
||||
* 根据消息类型执行不同的操作(SDP交换/ICE候选/PIN验证等)
|
||||
*/
|
||||
private handleSignalMessage(message: SignalMessage): void {
|
||||
// 验证消息签名(防止篡改)
|
||||
if (message.signature && !this.verifyMessageSignature(message)) {
|
||||
console.warn('[ScreenCast] 消息签名验证失败,丢弃:', message.type);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (message.type) {
|
||||
case 'answer':
|
||||
this.handleRemoteAnswer(message.fromDeviceId, message.payload);
|
||||
break;
|
||||
case 'candidate':
|
||||
this.handleRemoteCandidate(message.fromDeviceId, message.payload);
|
||||
break;
|
||||
case 'pin_verify':
|
||||
this.handlePinVerifyResult(message.fromDeviceId, message.payload);
|
||||
break;
|
||||
case 'quality_adjust':
|
||||
this.handleQualityAdjust(message.fromDeviceId, message.payload);
|
||||
break;
|
||||
case 'cast_stop':
|
||||
this.handleRemoteStop(message.fromDeviceId);
|
||||
break;
|
||||
default:
|
||||
console.warn('[ScreenCast] 未知信令类型:', message.type);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始屏幕捕获 - 使用Electron desktopCapturer API获取屏幕视频流
|
||||
* 支持全屏、窗口、自定义区域三种捕获模式
|
||||
*/
|
||||
async startCapture(sourceId?: string): Promise<void> {
|
||||
if (this.isCapturing) {
|
||||
console.warn('[ScreenCast] 已在投屏中,请先停止当前投屏');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 通过Electron desktopCapturer获取可用的屏幕/窗口源
|
||||
const { desktopCapturer } = require('electron');
|
||||
const sources = await desktopCapturer.getSources({
|
||||
types: this.config.captureMode === 'window' ? ['window'] : ['screen'],
|
||||
thumbnailSize: { width: 320, height: 180 }
|
||||
});
|
||||
|
||||
if (sources.length === 0) {
|
||||
throw new Error('未找到可用的屏幕源');
|
||||
}
|
||||
|
||||
// 选择屏幕源(默认使用第一个或指定的源)
|
||||
const selectedSource = sourceId
|
||||
? sources.find((s: any) => s.id === sourceId) || sources[0]
|
||||
: sources[0];
|
||||
|
||||
// 配置视频约束参数
|
||||
const videoConstraints: any = {
|
||||
mandatory: {
|
||||
chromeMediaSource: 'desktop',
|
||||
chromeMediaSourceId: selectedSource.id,
|
||||
maxWidth: this.config.maxWidth,
|
||||
maxHeight: this.config.maxHeight,
|
||||
maxFrameRate: this.config.maxFrameRate,
|
||||
minFrameRate: 15
|
||||
}
|
||||
};
|
||||
|
||||
// 获取媒体流(视频 + 可选音频)
|
||||
const stream = await (navigator.mediaDevices as any).getUserMedia({
|
||||
video: videoConstraints,
|
||||
audio: this.config.enableAudio ? {
|
||||
mandatory: { chromeMediaSource: 'desktop' }
|
||||
} : false
|
||||
});
|
||||
|
||||
this.localStream = stream;
|
||||
this.isCapturing = true;
|
||||
this.emit('captureStarted', { sourceId: selectedSource.id, name: selectedSource.name });
|
||||
console.log('[ScreenCast] 屏幕捕获已启动:', selectedSource.name);
|
||||
} catch (error) {
|
||||
console.error('[ScreenCast] 屏幕捕获失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 向指定大屏设备发起投屏连接
|
||||
* 创建RTCPeerConnection,添加本地流,发送SDP Offer
|
||||
*/
|
||||
async castToDevice(deviceId: string, deviceName: string, ipAddress: string, port: number): Promise<void> {
|
||||
if (!this.localStream) {
|
||||
throw new Error('请先启动屏幕捕获');
|
||||
}
|
||||
|
||||
// 创建投屏目标记录
|
||||
const target: CastTarget = {
|
||||
deviceId, deviceName,
|
||||
deviceType: 'board',
|
||||
ipAddress, port,
|
||||
status: 'connecting',
|
||||
peerConnection: null,
|
||||
lastPingTime: Date.now()
|
||||
};
|
||||
|
||||
// 配置ICE服务器(STUN + TURN)
|
||||
const iceConfig: RTCConfiguration = {
|
||||
iceServers: [
|
||||
{ urls: this.config.stunServers },
|
||||
{
|
||||
urls: this.config.turnServer,
|
||||
username: this.config.turnUsername,
|
||||
credential: this.config.turnCredential
|
||||
}
|
||||
],
|
||||
iceCandidatePoolSize: 10
|
||||
};
|
||||
|
||||
// 创建RTCPeerConnection
|
||||
const pc = new RTCPeerConnection(iceConfig);
|
||||
target.peerConnection = pc;
|
||||
|
||||
// 添加本地媒体流的所有轨道
|
||||
this.localStream.getTracks().forEach(track => {
|
||||
pc.addTrack(track, this.localStream!);
|
||||
});
|
||||
|
||||
// 配置视频编码参数(优先使用H.264 High Profile)
|
||||
const sender = pc.getSenders().find(s => s.track?.kind === 'video');
|
||||
if (sender) {
|
||||
const params = sender.getParameters();
|
||||
if (params.encodings && params.encodings.length > 0) {
|
||||
params.encodings[0].maxBitrate = this.config.maxBitrate * 1000;
|
||||
params.encodings[0].maxFramerate = this.config.maxFrameRate;
|
||||
await sender.setParameters(params);
|
||||
}
|
||||
}
|
||||
|
||||
// 监听ICE候选事件,发送给对端
|
||||
pc.onicecandidate = (event) => {
|
||||
if (event.candidate) {
|
||||
this.sendSignalMessage({
|
||||
type: 'candidate',
|
||||
fromDeviceId: this.localDeviceId,
|
||||
toDeviceId: deviceId,
|
||||
payload: event.candidate.toJSON(),
|
||||
timestamp: Date.now(),
|
||||
signature: ''
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 监听连接状态变化
|
||||
pc.onconnectionstatechange = () => {
|
||||
console.log(`[ScreenCast] 连接状态[${deviceName}]:`, pc.connectionState);
|
||||
switch (pc.connectionState) {
|
||||
case 'connected':
|
||||
target.status = 'connected';
|
||||
this.startQualityMonitor(deviceId);
|
||||
this.emit('deviceConnected', { deviceId, deviceName });
|
||||
break;
|
||||
case 'disconnected':
|
||||
case 'failed':
|
||||
target.status = 'disconnected';
|
||||
this.stopQualityMonitor(deviceId);
|
||||
this.emit('deviceDisconnected', { deviceId, deviceName });
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// 创建并发送SDP Offer
|
||||
const offer = await pc.createOffer({
|
||||
offerToReceiveAudio: false,
|
||||
offerToReceiveVideo: false
|
||||
});
|
||||
await pc.setLocalDescription(offer);
|
||||
|
||||
// 通过信令服务器发送Offer给大屏设备
|
||||
this.sendSignalMessage({
|
||||
type: 'offer',
|
||||
fromDeviceId: this.localDeviceId,
|
||||
toDeviceId: deviceId,
|
||||
payload: { sdp: offer.sdp, type: offer.type, pinCode: this.config.pinCode },
|
||||
timestamp: Date.now(),
|
||||
signature: ''
|
||||
});
|
||||
|
||||
this.targets.set(deviceId, target);
|
||||
console.log(`[ScreenCast] 已向 ${deviceName} 发起投屏请求`);
|
||||
}
|
||||
|
||||
/** 处理远端设备的SDP Answer */
|
||||
private async handleRemoteAnswer(deviceId: string, payload: any): Promise<void> {
|
||||
const target = this.targets.get(deviceId);
|
||||
if (!target || !target.peerConnection) return;
|
||||
|
||||
try {
|
||||
const answer = new RTCSessionDescription(payload);
|
||||
await target.peerConnection.setRemoteDescription(answer);
|
||||
console.log(`[ScreenCast] 收到 ${target.deviceName} 的Answer`);
|
||||
} catch (error) {
|
||||
console.error(`[ScreenCast] 设置RemoteDescription失败:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/** 处理远端ICE候选 */
|
||||
private async handleRemoteCandidate(deviceId: string, payload: any): Promise<void> {
|
||||
const target = this.targets.get(deviceId);
|
||||
if (!target || !target.peerConnection) return;
|
||||
|
||||
try {
|
||||
const candidate = new RTCIceCandidate(payload);
|
||||
await target.peerConnection.addIceCandidate(candidate);
|
||||
} catch (error) {
|
||||
console.error('[ScreenCast] 添加ICE候选失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/** 处理PIN码验证结果 */
|
||||
private handlePinVerifyResult(deviceId: string, payload: { verified: boolean }): void {
|
||||
if (!payload.verified) {
|
||||
console.warn(`[ScreenCast] 设备 ${deviceId} PIN码验证失败`);
|
||||
this.disconnectDevice(deviceId);
|
||||
this.emit('pinVerifyFailed', { deviceId });
|
||||
}
|
||||
}
|
||||
|
||||
/** 处理远端质量调整请求(大屏端网络差时要求降低码率) */
|
||||
private handleQualityAdjust(deviceId: string, payload: { maxBitrate?: number; maxFps?: number }): void {
|
||||
const target = this.targets.get(deviceId);
|
||||
if (!target || !target.peerConnection) return;
|
||||
|
||||
const sender = target.peerConnection.getSenders().find((s: any) => s.track?.kind === 'video');
|
||||
if (sender) {
|
||||
const params = sender.getParameters();
|
||||
if (params.encodings && params.encodings.length > 0) {
|
||||
if (payload.maxBitrate) {
|
||||
params.encodings[0].maxBitrate = payload.maxBitrate * 1000;
|
||||
}
|
||||
if (payload.maxFps) {
|
||||
params.encodings[0].maxFramerate = payload.maxFps;
|
||||
}
|
||||
sender.setParameters(params);
|
||||
console.log(`[ScreenCast] 已调整投屏质量: 码率=${payload.maxBitrate}kbps, 帧率=${payload.maxFps}fps`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 处理远端停止投屏请求 */
|
||||
private handleRemoteStop(deviceId: string): void {
|
||||
console.log(`[ScreenCast] 收到远端停止请求: ${deviceId}`);
|
||||
this.disconnectDevice(deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动投屏质量监控
|
||||
* 每3秒采集一次WebRTC连接统计信息
|
||||
*/
|
||||
private startQualityMonitor(deviceId: string): void {
|
||||
const timer = setInterval(async () => {
|
||||
const target = this.targets.get(deviceId);
|
||||
if (!target || !target.peerConnection) {
|
||||
this.stopQualityMonitor(deviceId);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const stats = await target.peerConnection.getStats();
|
||||
let qualityStats: CastQualityStats = {
|
||||
currentBitrate: 0, currentFps: 0,
|
||||
packetLoss: 0, roundTripTime: 0,
|
||||
resolution: '', encoderType: '',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
stats.forEach((report: any) => {
|
||||
if (report.type === 'outbound-rtp' && report.kind === 'video') {
|
||||
qualityStats.currentBitrate = Math.round((report.bytesSent * 8) / 1000);
|
||||
qualityStats.currentFps = report.framesPerSecond || 0;
|
||||
qualityStats.resolution = `${report.frameWidth}x${report.frameHeight}`;
|
||||
qualityStats.encoderType = report.encoderImplementation || 'unknown';
|
||||
}
|
||||
if (report.type === 'candidate-pair' && report.state === 'succeeded') {
|
||||
qualityStats.roundTripTime = report.currentRoundTripTime * 1000;
|
||||
}
|
||||
if (report.type === 'remote-inbound-rtp') {
|
||||
qualityStats.packetLoss = report.fractionLost * 100;
|
||||
}
|
||||
});
|
||||
|
||||
// 保存统计历史(最多保留1000条)
|
||||
this.qualityHistory.push(qualityStats);
|
||||
if (this.qualityHistory.length > 1000) {
|
||||
this.qualityHistory.splice(0, this.qualityHistory.length - 1000);
|
||||
}
|
||||
|
||||
// 自适应码率控制:丢包率过高时自动降低码率
|
||||
if (qualityStats.packetLoss > 5) {
|
||||
const reducedBitrate = Math.max(
|
||||
this.config.minBitrate,
|
||||
qualityStats.currentBitrate * 0.7
|
||||
);
|
||||
this.adjustBitrate(deviceId, reducedBitrate);
|
||||
} else if (qualityStats.packetLoss < 1 && qualityStats.currentBitrate < this.config.maxBitrate) {
|
||||
// 网络状况良好时逐步提高码率
|
||||
const increasedBitrate = Math.min(
|
||||
this.config.maxBitrate,
|
||||
qualityStats.currentBitrate * 1.1
|
||||
);
|
||||
this.adjustBitrate(deviceId, increasedBitrate);
|
||||
}
|
||||
|
||||
this.emit('qualityUpdate', { deviceId, stats: qualityStats });
|
||||
} catch (error) {
|
||||
console.error('[ScreenCast] 质量监控统计失败:', error);
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
this.statsTimers.set(deviceId, timer);
|
||||
}
|
||||
|
||||
/** 停止质量监控 */
|
||||
private stopQualityMonitor(deviceId: string): void {
|
||||
const timer = this.statsTimers.get(deviceId);
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
this.statsTimers.delete(deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
/** 动态调整视频码率 */
|
||||
private adjustBitrate(deviceId: string, targetBitrate: number): void {
|
||||
const target = this.targets.get(deviceId);
|
||||
if (!target || !target.peerConnection) return;
|
||||
|
||||
const sender = target.peerConnection.getSenders().find((s: any) => s.track?.kind === 'video');
|
||||
if (sender) {
|
||||
const params = sender.getParameters();
|
||||
if (params.encodings && params.encodings.length > 0) {
|
||||
params.encodings[0].maxBitrate = Math.round(targetBitrate * 1000);
|
||||
sender.setParameters(params).catch((e: Error) => {
|
||||
console.error('[ScreenCast] 码率调整失败:', e.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 断开指定设备的投屏连接 */
|
||||
disconnectDevice(deviceId: string): void {
|
||||
const target = this.targets.get(deviceId);
|
||||
if (!target) return;
|
||||
|
||||
// 关闭PeerConnection
|
||||
if (target.peerConnection) {
|
||||
target.peerConnection.close();
|
||||
}
|
||||
|
||||
// 停止质量监控
|
||||
this.stopQualityMonitor(deviceId);
|
||||
|
||||
// 通知对端
|
||||
this.sendSignalMessage({
|
||||
type: 'cast_stop',
|
||||
fromDeviceId: this.localDeviceId,
|
||||
toDeviceId: deviceId,
|
||||
payload: {},
|
||||
timestamp: Date.now(),
|
||||
signature: ''
|
||||
});
|
||||
|
||||
this.targets.delete(deviceId);
|
||||
this.emit('deviceDisconnected', { deviceId, deviceName: target.deviceName });
|
||||
console.log(`[ScreenCast] 已断开投屏: ${target.deviceName}`);
|
||||
}
|
||||
|
||||
/** 停止所有投屏并释放资源 */
|
||||
stopAllCasting(): void {
|
||||
// 断开所有投屏目标
|
||||
for (const deviceId of this.targets.keys()) {
|
||||
this.disconnectDevice(deviceId);
|
||||
}
|
||||
|
||||
// 停止屏幕捕获
|
||||
if (this.localStream) {
|
||||
this.localStream.getTracks().forEach(track => track.stop());
|
||||
this.localStream = null;
|
||||
}
|
||||
this.isCapturing = false;
|
||||
|
||||
this.emit('allCastingStopped');
|
||||
console.log('[ScreenCast] 所有投屏已停止');
|
||||
}
|
||||
|
||||
/** 发送信令消息(附加HMAC-SHA256签名) */
|
||||
private sendSignalMessage(message: SignalMessage): void {
|
||||
// 生成消息签名,防止信令被篡改
|
||||
const content = `${message.type}:${message.fromDeviceId}:${message.toDeviceId}:${message.timestamp}`;
|
||||
message.signature = crypto.createHmac('sha256', this.hmacKey).update(content).digest('hex');
|
||||
|
||||
if (this.signalSocket && this.signalSocket.readyState === WebSocket.OPEN) {
|
||||
this.signalSocket.send(JSON.stringify(message));
|
||||
} else {
|
||||
console.warn('[ScreenCast] 信令连接不可用,消息发送失败');
|
||||
}
|
||||
}
|
||||
|
||||
/** 验证收到的信令消息签名 */
|
||||
private verifyMessageSignature(message: SignalMessage): boolean {
|
||||
const content = `${message.type}:${message.fromDeviceId}:${message.toDeviceId}:${message.timestamp}`;
|
||||
const expected = crypto.createHmac('sha256', this.hmacKey).update(content).digest('hex');
|
||||
return message.signature === expected;
|
||||
}
|
||||
|
||||
/** 获取当前投屏状态汇总 */
|
||||
getStatus(): { isCapturing: boolean; connectedDevices: number; targets: any[] } {
|
||||
const targetList = Array.from(this.targets.values()).map(t => ({
|
||||
deviceId: t.deviceId,
|
||||
deviceName: t.deviceName,
|
||||
status: t.status,
|
||||
deviceType: t.deviceType
|
||||
}));
|
||||
return {
|
||||
isCapturing: this.isCapturing,
|
||||
connectedDevices: targetList.filter(t => t.status === 'connected').length,
|
||||
targets: targetList
|
||||
};
|
||||
}
|
||||
|
||||
/** 销毁投屏管理器,释放所有资源 */
|
||||
destroy(): void {
|
||||
this.stopAllCasting();
|
||||
if (this.signalSocket) {
|
||||
this.signalSocket.close();
|
||||
this.signalSocket = null;
|
||||
}
|
||||
this.qualityHistory = [];
|
||||
this.removeAllListeners();
|
||||
console.log('[ScreenCast] 投屏管理器已销毁');
|
||||
}
|
||||
}
|
||||
|
||||
export default ScreenCastManager;
|
||||
Reference in New Issue
Block a user