import { AndroidKeycode } from "../const"; import VideoController from "video-controller-next"; import { PlayerEvents, internalEmitter, externalEmitter } from "../emitter"; import SignalSimple from "../signal/signal-simple"; import WebrtcMediaStreamSimple from "../signal/webrtc-stream"; import WebRtcTextSimple from "../signal/webrtc-text"; import type { ICtrlMsg, IRealtimePlayer, PeerType } from "../type"; import { throttle } from "lodash"; export default class RealtimePlayer { private options: IRealtimePlayer; private videoElement: HTMLVideoElement; private imageElement?: HTMLImageElement; private videoController: VideoController; private wsAddress: string; private videoMode: string; private signal: SignalSimple|undefined; private streamer: WebrtcMediaStreamSimple|undefined; private getMessageType: (msg: string) => { type: string, payload: any, toPeerType: string }; private hooksOnSignalMessage: (parsed: { type: string; payload: Record }) => void; private initPeerCase: (parsed: { type: string; payload: Record }) => boolean; private answerType; private candidateType; protected textHelperElement: HTMLInputElement; private hasInitPeer: boolean; private peerType: PeerType[]; private textPeer?: WebRtcTextSimple; private _retryCount = 0; private _maxRetryTime = 3; private _isPC = false; private muted = false; public retryFailed = false; constructor(options: IRealtimePlayer) { if (!window.RTCPeerConnection || !window.RTCSessionDescription || !window.RTCIceCandidate) { alert('Sorry, Browser not support WebRTC. Plase update or change browser'); } this.options = options; this.imageElement = this.options?.imageElement; this.videoElement = options.videoElement; this.videoMode = options.videoMode; this.textHelperElement = options.textHelperElement; this.wsAddress = options.wsAddress; this.getMessageType = options.getMessageType; this.hooksOnSignalMessage = options.hooksOnSignalMessage; this.peerType = options.peerType ?? [] this.hasInitPeer = false; this.initPeerCase = options?.initPeerCase ?? ((parsed) => parsed.type === 'roomStatus'); this.answerType = options?.answerType ?? 'answer'; this.candidateType = options?.candidateType ?? 'candidate'; this._isPC = options?.isPC ?? false; this.videoController = new VideoController({ videoMode: this.videoMode, imageElement: this.imageElement, sendCtrl: this._sendControl, videoElement: this.videoElement, textHelperElement: this.textHelperElement, isPc: this._isPC }); this._initComponent(); internalEmitter.on('reset', () => { this.replay(); externalEmitter.emit(PlayerEvents.RESET); this.streamer?.cleanMediaRecorder(); this.streamer?.clearSetStatusTimer(); }) } get controller() { return this.videoController } get isWebrtc() { return this.videoMode === 'webrtc'; } emitter() { return externalEmitter; } // 发送JoinRoomer消息 private sendJoinRoomer() { this.signal && this.signal?.sendMsg(this.options.buildJoinRoomMessage()); } private _initComponent(replay: boolean = false) { if (this.isWebrtc) { this.streamer = new WebrtcMediaStreamSimple({ buildMediaCandidateMessage: this.options.buildMediaCandidateMessage, buildJoinRoomMessage: this.options.buildJoinRoomMessage, buildMediaOfferMessage: this.options.buildMediaOfferMessage, videoElement: this.videoElement, keyFrame: this.options?.keyFrame, iceConfig: this.options.iceConfig }) } if (this.peerType.includes('text')) { this.textPeer = new WebRtcTextSimple({ buildTextCandidateMessage: this.options.buildTextCandidateMessage, buildTextOfferMessage: this.options.buildTextOfferMessage, iceConfig: this.options.iceConfig, imageElement: this.options.imageElement }); } this.signal = new SignalSimple({ socketClientAddr: this.wsAddress, heartBeatMsg: this.options.heartBeatMsg || { type: "heartbeat" }, heartBeatInterval: this.options.heartBeatInterval || 10000, hooksOnOpen: () => { this.retryFailed = false; this.streamer?.setSignal(this.signal!); this.textPeer && this.textPeer?.setSignal(this.signal!); this.sendJoinRoomer(); this.options.hooksOnSignalOpen && this.options.hooksOnSignalOpen.apply(this); }, hookOnClose: (ev) => { // if (this._retryCount > this._maxRetryTime) { // return; // } internalEmitter.emit('reset') }, hookOnError: (err) => { }, hookOnMessage: (ev: MessageEvent) => { const messageObj = this.getMessageType(ev.data); if (!messageObj.type || !messageObj) { console.warn(`RealtimePlayer: failed to get messageType from ${ev.data}`) return; } if (this.initPeerCase(messageObj) && !this.hasInitPeer) { this._retryCount = 0; this.streamer?.initPeer(); this.textPeer && this.textPeer?.initPeer(); this.hasInitPeer = true; if (replay) { externalEmitter.emit(PlayerEvents.RETRY_SUCCESS) } if (this.videoMode === 'image') { this.textPeer && this.textPeer?.requestImageChannel(); } } switch (messageObj.type) { case this.answerType: if (messageObj.toPeerType === 'media') { this.streamer?.handleReceiveAnswer(messageObj.payload.description); } if (messageObj.toPeerType === 'text') { this.textPeer && this.textPeer?.handleReceiveAnswer(messageObj.payload.description); } break; case this.candidateType: if (messageObj.toPeerType === 'media') { this.streamer?.handleReceiveCandidate(messageObj.payload.candidate); } if (messageObj.toPeerType === 'text') { this.textPeer && this.textPeer?.handleReceiveCandidate(messageObj.payload.candidate); } break; default: break; } this.hooksOnSignalMessage(messageObj) } }) } replay = throttle((force: boolean = false) => { if (force) { this._replay(); } else { this._retryCount++; if (this._retryCount > this._maxRetryTime) { this.retryFailed = true; console.error(`RealtimePlayer: 允许最大尝试次数 ${this._maxRetryTime}, 当前尝试次数 ${this._retryCount}, 重试失败`) externalEmitter.emit(PlayerEvents.RETRY_FAILED) this.streamer?.clearSetStatusTimer(); return; } else { console.warn(`RealtimePlayer: 允许最大尝试次数 ${this._maxRetryTime}, 当前尝试次数 ${this._retryCount}`) } this._replay(); } }, 1000) private _replay() { this.destroy(); this._initComponent(true); } streamerInstance = () => { return this.streamer; } textInstance = () => { return this.textPeer; } signalInstance = () => { return this.signal; } private _sendControl = (msg: ICtrlMsg) => { if (this.isWebrtc) { if (msg.position && this.videoElement) { msg.position.height = this.videoElement.videoHeight; msg.position.width = this.videoElement.videoWidth; } this.streamer && this.streamer.sendControlChannelMessage(msg); } else { if (msg.position && this.imageElement) { msg.position.height = this.imageElement.height; msg.position.width = this.imageElement.width; } this.textPeer && this.textPeer.imageStreamChannel && this.textPeer.imageStreamChannel.send(JSON.stringify(msg)); } } public switchMuted = () => { if (this.videoElement) { this.videoElement.muted = !this.videoElement.muted; this.videoElement.volume = 1; this.muted = this.videoElement.muted; return this.videoElement.muted; } return true; } public keyPress = (keyCode: AndroidKeycode) => { this.videoController.keyPress(keyCode); } public rotate = () => { this.videoController.rotate(); } public backOrScreenOn = () => { this.videoController.backOrScreenOn(); } public startRecord = () => { if (!this.isWebrtc) { console.error('RealtimePlayer: Only webrtc mode support start/stop record.'); return; } this.streamer?.mediaRecorder?.start(); } public stopRecord = () => { if (!this.isWebrtc) { console.error('RealtimePlayer: Only webrtc mode support start/stop record.'); return; } this.streamer?.mediaRecorder?.stop(); } public destroy = () => { this.streamer?.peerConnection()?.close(); this.streamer?.cleanMediaRecorder(); this.streamer?.clearSetStatusTimer(); this.streamer = undefined; this.signal?.close(); this.signal = undefined; this.textPeer?.peerConnection()?.close(); this.textPeer = undefined; this.hasInitPeer = false; this.videoController.unmount(); } }