Files
012-kaopeilian/frontend/src/utils/webrtc.ts
yuliang_guo 64f5d567fa
Some checks failed
continuous-integration/drone/push Build is failing
feat: 实现 KPL 系统功能改进计划
1. 课程学习进度追踪
   - 新增 UserCourseProgress 和 UserMaterialProgress 模型
   - 新增 /api/v1/progress/* 进度追踪 API
   - 更新 admin.py 使用真实课程完成率数据

2. 路由权限检查完善
   - 新增前端 permissionChecker.ts 权限检查工具
   - 更新 router/guard.ts 实现团队和课程权限验证
   - 新增后端 permission_service.py

3. AI 陪练音频转文本
   - 新增 speech_recognition.py 语音识别服务
   - 新增 /api/v1/speech/* API
   - 更新 ai-practice-coze.vue 支持语音输入

4. 双人对练报告生成
   - 更新 practice_room_service.py 添加报告生成功能
   - 新增 /rooms/{room_code}/report API
   - 更新 duo-practice-report.vue 调用真实 API

5. 学习提醒推送
   - 新增 notification_service.py 通知服务
   - 新增 scheduler_service.py 定时任务服务
   - 支持钉钉、企微、站内消息推送

6. 智能学习推荐
   - 新增 recommendation_service.py 推荐服务
   - 新增 /api/v1/recommendations/* API
   - 支持错题、能力、进度、热门多维度推荐

7. 安全问题修复
   - DEBUG 默认值改为 False
   - 添加 SECRET_KEY 安全警告
   - 新增 check_security_settings() 检查函数

8. 证书 PDF 生成
   - 更新 certificate_service.py 添加 PDF 生成
   - 添加 weasyprint、Pillow、qrcode 依赖
   - 更新下载 API 支持 PDF 和 PNG 格式
2026-01-30 14:22:35 +08:00

325 lines
8.4 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.
/**
* WebRTC 连接管理模块
*
* 功能:
* - 管理 RTCPeerConnection 生命周期
* - 处理 SDP 交换
* - 处理 ICE 候选收集
* - 音频流管理
*/
export type ConnectionState = 'idle' | 'connecting' | 'connected' | 'disconnected' | 'failed'
export interface WebRTCConfig {
iceServers?: RTCIceServer[]
onLocalStream?: (stream: MediaStream) => void
onRemoteStream?: (stream: MediaStream) => void
onConnectionStateChange?: (state: ConnectionState) => void
onIceCandidate?: (candidate: RTCIceCandidate) => void
onError?: (error: Error) => void
}
// 默认 ICE 服务器配置
const DEFAULT_ICE_SERVERS: RTCIceServer[] = [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' },
{ urls: 'stun:stun2.l.google.com:19302' }
]
export class WebRTCManager {
private peerConnection: RTCPeerConnection | null = null
private localStream: MediaStream | null = null
private remoteStream: MediaStream | null = null
private config: WebRTCConfig
private connectionState: ConnectionState = 'idle'
private pendingIceCandidates: RTCIceCandidate[] = []
constructor(config: WebRTCConfig = {}) {
this.config = {
iceServers: DEFAULT_ICE_SERVERS,
...config
}
}
/**
* 获取当前连接状态
*/
getConnectionState(): ConnectionState {
return this.connectionState
}
/**
* 获取本地音频流
*/
getLocalStream(): MediaStream | null {
return this.localStream
}
/**
* 获取远程音频流
*/
getRemoteStream(): MediaStream | null {
return this.remoteStream
}
/**
* 初始化本地音频流
*/
async initLocalStream(): Promise<MediaStream> {
try {
this.localStream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
},
video: false
})
this.config.onLocalStream?.(this.localStream)
return this.localStream
} catch (error) {
const err = error instanceof Error ? error : new Error('获取麦克风权限失败')
this.config.onError?.(err)
throw err
}
}
/**
* 创建 PeerConnection
*/
private createPeerConnection(): RTCPeerConnection {
const pc = new RTCPeerConnection({
iceServers: this.config.iceServers
})
// 监听 ICE 候选
pc.onicecandidate = (event) => {
if (event.candidate) {
console.log('[WebRTC] ICE candidate:', event.candidate.candidate?.substring(0, 50))
this.config.onIceCandidate?.(event.candidate)
}
}
// 监听连接状态变化
pc.onconnectionstatechange = () => {
console.log('[WebRTC] Connection state:', pc.connectionState)
this.updateConnectionState(pc.connectionState)
}
// 监听 ICE 连接状态
pc.oniceconnectionstatechange = () => {
console.log('[WebRTC] ICE connection state:', pc.iceConnectionState)
if (pc.iceConnectionState === 'failed') {
this.updateConnectionState('failed')
}
}
// 监听远程流
pc.ontrack = (event) => {
console.log('[WebRTC] Remote track received')
if (event.streams && event.streams[0]) {
this.remoteStream = event.streams[0]
this.config.onRemoteStream?.(this.remoteStream)
}
}
return pc
}
/**
* 更新连接状态
*/
private updateConnectionState(state: RTCPeerConnectionState | string) {
const stateMap: Record<string, ConnectionState> = {
'new': 'connecting',
'connecting': 'connecting',
'connected': 'connected',
'disconnected': 'disconnected',
'failed': 'failed',
'closed': 'disconnected'
}
this.connectionState = stateMap[state] || 'idle'
this.config.onConnectionStateChange?.(this.connectionState)
}
/**
* 创建 Offer发起方调用
*/
async createOffer(): Promise<RTCSessionDescriptionInit> {
if (!this.localStream) {
await this.initLocalStream()
}
this.peerConnection = this.createPeerConnection()
this.updateConnectionState('connecting')
// 添加本地音频轨道
this.localStream!.getTracks().forEach(track => {
this.peerConnection!.addTrack(track, this.localStream!)
})
// 创建 Offer
const offer = await this.peerConnection.createOffer()
await this.peerConnection.setLocalDescription(offer)
console.log('[WebRTC] Offer created')
return offer
}
/**
* 处理 Offer接收方调用
*/
async handleOffer(offer: RTCSessionDescriptionInit): Promise<RTCSessionDescriptionInit> {
if (!this.localStream) {
await this.initLocalStream()
}
this.peerConnection = this.createPeerConnection()
this.updateConnectionState('connecting')
// 添加本地音频轨道
this.localStream!.getTracks().forEach(track => {
this.peerConnection!.addTrack(track, this.localStream!)
})
// 设置远程描述
await this.peerConnection.setRemoteDescription(new RTCSessionDescription(offer))
// 处理等待中的 ICE 候选
for (const candidate of this.pendingIceCandidates) {
await this.peerConnection.addIceCandidate(candidate)
}
this.pendingIceCandidates = []
// 创建 Answer
const answer = await this.peerConnection.createAnswer()
await this.peerConnection.setLocalDescription(answer)
console.log('[WebRTC] Answer created')
return answer
}
/**
* 处理 Answer发起方调用
*/
async handleAnswer(answer: RTCSessionDescriptionInit): Promise<void> {
if (!this.peerConnection) {
throw new Error('PeerConnection not initialized')
}
await this.peerConnection.setRemoteDescription(new RTCSessionDescription(answer))
// 处理等待中的 ICE 候选
for (const candidate of this.pendingIceCandidates) {
await this.peerConnection.addIceCandidate(candidate)
}
this.pendingIceCandidates = []
console.log('[WebRTC] Answer handled')
}
/**
* 添加 ICE 候选
*/
async addIceCandidate(candidate: RTCIceCandidateInit): Promise<void> {
const iceCandidate = new RTCIceCandidate(candidate)
if (this.peerConnection && this.peerConnection.remoteDescription) {
await this.peerConnection.addIceCandidate(iceCandidate)
console.log('[WebRTC] ICE candidate added')
} else {
// 如果远程描述还没设置,先缓存候选
this.pendingIceCandidates.push(iceCandidate)
console.log('[WebRTC] ICE candidate queued')
}
}
/**
* 静音/取消静音本地音频
*/
setMuted(muted: boolean): void {
if (this.localStream) {
this.localStream.getAudioTracks().forEach(track => {
track.enabled = !muted
})
}
}
/**
* 检查是否静音
*/
isMuted(): boolean {
if (this.localStream) {
const audioTrack = this.localStream.getAudioTracks()[0]
return audioTrack ? !audioTrack.enabled : true
}
return true
}
/**
* 获取音频音量级别(用于音量指示器)
*/
async getAudioLevel(stream: MediaStream): Promise<number> {
return new Promise((resolve) => {
const audioContext = new AudioContext()
const analyser = audioContext.createAnalyser()
const source = audioContext.createMediaStreamSource(stream)
source.connect(analyser)
analyser.fftSize = 256
const dataArray = new Uint8Array(analyser.frequencyBinCount)
analyser.getByteFrequencyData(dataArray)
// 计算平均音量
const average = dataArray.reduce((a, b) => a + b, 0) / dataArray.length
audioContext.close()
resolve(average / 255) // 归一化到 0-1
})
}
/**
* 关闭连接
*/
close(): void {
console.log('[WebRTC] Closing connection')
// 停止本地流
if (this.localStream) {
this.localStream.getTracks().forEach(track => track.stop())
this.localStream = null
}
// 停止远程流
if (this.remoteStream) {
this.remoteStream.getTracks().forEach(track => track.stop())
this.remoteStream = null
}
// 关闭 PeerConnection
if (this.peerConnection) {
this.peerConnection.close()
this.peerConnection = null
}
this.pendingIceCandidates = []
this.updateConnectionState('disconnected')
}
/**
* 重置管理器
*/
reset(): void {
this.close()
this.connectionState = 'idle'
}
}
// 导出单例工厂函数
export function createWebRTCManager(config?: WebRTCConfig): WebRTCManager {
return new WebRTCManager(config)
}