import useStore from '@/store' /** * WebRTC服务 */ export default class WebRtcService { private ICE = [ { urls: [ 'stun:caner.top:3478' ] }, { urls: 'turn:caner.top:3478', username: 'admin', credential: '123456' }, { urls: 'stun:stun.l.google.com:19302' } ] public Peer: RTCPeerConnection | null = null private audioTack: MediaStreamTrack | null = null private store = useStore() private lastStats: RTCStatsReport | null = null private sleep = (ms: number) => new Promise((resolve) => { setTimeout(resolve, ms) }) /** * 初始化 * @param DOM HTMLVideoElement */ initRTC(DOM: HTMLVideoElement) { try { if (this.Peer) this.distory() if (!DOM) { this.store.setMqttMessage({ type: 'WebRtcIntError' }); return } console.log('start initRTC') // create Peer this.Peer = new RTCPeerConnection({ iceServers: this.ICE, bundlePolicy: 'max-bundle' }) // listen state this.Peer.onicegatheringstatechange = () => { console.log('GatheringState: ', this.Peer?.iceGatheringState) if (this.Peer?.iceGatheringState === 'complete') { const answer = this.Peer?.localDescription this.store.setMqttMessage({ type: 'answer', data: answer?.toJSON() }) } } // listen track this.Peer.ontrack = (evt) => { console.log('track', evt) // TODO safari 不支持opus ogg 音频编码 DOM.srcObject = evt.streams[0] } // listen changestate· this.Peer.oniceconnectionstatechange = () => { const state = this.Peer?.iceConnectionState console.log('StateChange', state) // P2P 连接失败 if (state === 'failed' || state === 'closed') { console.log('P2P通信失败') this.store.setMqttMessage({ type: 'WebRtcFailed' }) } // ICE连接成功 if (state === 'connected') { this.store.setMqttMessage({ type: 'WebRtcConnected', data: null }) } } // get stats this.getStats() console.log('RTC success') } catch (error) { console.log('RTC 初始化失败', error) this.store.setMqttMessage({ type: 'WebRtcIntError' }) } } /** * 远程静音 * @param mute boolean */ muteRemoteAudio(mute: boolean) { console.log('远程静音', mute) if (!this.audioTack) return this.audioTack!.enabled = mute } /** * 比特率|丢包率 * @returns */ async getStats() { if (this.Peer) { const stats = await this.Peer.getStats() let packetLoss = 0 let bitrate = 0 if (this.lastStats) { stats.forEach((report) => { if (report.type === 'inbound-rtp' && report.kind === 'video') { if (this.lastStats && this.lastStats.has(report.id)) { const lastReport = this.lastStats.get(report.id) const duration = (report.timestamp - lastReport.timestamp) / 1000 bitrate = (report.bytesReceived - lastReport.bytesReceived) / duration / 1000 const lostPackets = (report.packetsLost - lastReport.packetsLost) if ((report.packetsReceived - lastReport.packetsReceived + lostPackets) === 0) { packetLoss = 0 } else { packetLoss = (lostPackets / (report.packetsReceived - lastReport.packetsReceived + lostPackets)) * 100 } } } }) } this.lastStats = stats this.store.setMqttMessage({ type: 'WebRtcStats', data: { packetLoss: Math.floor(packetLoss * 100) / 100, bitrate: Math.floor(bitrate * 100) / 100 } }) } else { this.store.setMqttMessage({ type: 'WebRtcStats', data: { packetLoss: 0, bitrate: 0 } }) } await this.sleep(1000) this.getStats() } distory() { this.Peer?.close() this.Peer = null this.audioTack = null } }