| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548 |
- import browserTool from 'browser-tool';
- import { useNetworkStore } from '@/store/network';
- import { wsMsgType } from './webSocket';
- function prettierInfo(
- str: string,
- data: {
- browser: string;
- },
- type?: 'log' | 'warn' | 'error',
- ...args
- ) {
- console[type || 'log'](
- `${new Date().toLocaleString()},${data.browser}浏览器,${str}`,
- ...args
- );
- }
- export const frontendErrorCode = {
- rtcStatusErr: {
- // rtcStatus没有100%
- code: 1001,
- msg: '连接错误,是否尝试刷新页面(不影响云手机内应用运行)',
- refresh: true,
- },
- streamStop: {
- // 视频流卡主
- code: 1002,
- msg: '网络似乎不稳定,建议更换网络或刷新页面(不影响云手机内应用运行)',
- refresh: true,
- },
- blackScreen: {
- // 视频流黑屏
- code: 1003,
- msg: '当前浏览器似乎不兼容,建议使用Google Chrome浏览器',
- refresh: false,
- },
- connectionStateFailed: {
- code: 1004,
- msg: '连接错误,是否尝试刷新页面(不影响云手机内应用运行)',
- refresh: true,
- },
- iceConnectionStateDisconnected: {
- code: 1005,
- msg: '连接错误,是否尝试刷新页面(不影响云手机内应用运行)',
- refresh: true,
- },
- getStatsErr: {
- code: 1006,
- msg: '连接错误,是否尝试刷新页面(不影响云手机内应用运行)',
- refresh: true,
- },
- connectLongTime: {
- // 5秒内没有收到loadedmetadata回调
- code: 1007,
- msg: '等待太久,是否刷新页面重试?',
- refresh: true,
- },
- };
- export class WebRTCClass {
- roomId = '-1';
- peerConnection: RTCPeerConnection | null = null;
- dataChannel: RTCDataChannel | null = null;
- candidateFlag = false;
- getStatsSetIntervalDelay = 1000;
- getStatsSetIntervalTimer;
- // getStatsSetIntervalDelay是1秒的话,forceINumsMax是3,就代表一直卡了3秒。
- forceINums = 0; // 发送forceI次数
- forceINumsMax = 3; // 最多发送几次forceI
- preFramesDecoded = -1; // 上一帧
- browser: {
- device: string;
- language: string;
- engine: string;
- browser: string;
- system: string;
- systemVersion: string;
- platform: string;
- isWebview: boolean;
- isBot: boolean;
- version: string;
- };
- rtcStatus = {
- joined: false, // true代表成功,false代表失败
- icecandidate: false, // true代表成功,false代表失败
- createOffer: false, // true代表成功,false代表失败
- setLocalDescription: false, // true代表成功,false代表失败
- answer: false, // true代表成功,false代表失败
- setRemoteDescription: false, // true代表成功,false代表失败
- addStream: false, // true代表成功,false代表失败
- loadstart: false, // true代表成功,false代表失败
- loadedmetadata: false, // true代表成功,false代表失败
- };
- localDescription: any;
- stream: any;
- constructor({ roomId }) {
- this.roomId = roomId;
- this.browser = browserTool();
- this.createPeerConnection();
- this.update();
- // this.handleWebRtcError();
- }
- myAddTrack = (track, stream) => {
- console.warn('myAddTrackmyAddTrack', track, stream);
- this.peerConnection?.addTrack(track, stream);
- };
- handleWebRtcError = () => {
- this.getStatsSetIntervalTimer = setInterval(() => {
- this.peerConnection
- ?.getStats()
- .then((res) => {
- let isBlack = false;
- let currFramesDecoded = -1;
- res.forEach((report: RTCInboundRtpStreamStats) => {
- // 不能结构report的值,不然如果卡主之后,report的值就一直都是
- // const { type, kind, framesDecoded } = report;
- const data = {
- type: report.type,
- kind: report.kind,
- framesDecoded: report.framesDecoded,
- decoderImplementation: report.decoderImplementation,
- isChrome: false,
- isSafari: false,
- other: '',
- };
- if (this.browser.browser === 'safari') {
- data.isSafari = true;
- } else if (this.browser.browser === 'chrome') {
- data.isChrome = true;
- } else {
- data.other = this.browser.browser;
- }
- const isStopFlag = this.getCurrentFramesDecoded(data);
- const isBlackFlag = this.isBlackScreen(data);
- if (isStopFlag !== false) {
- currFramesDecoded = isStopFlag;
- }
- if (isBlackFlag !== false) {
- isBlack = isBlackFlag;
- }
- });
- /** 处理视频流卡主 */
- const handleStreamStop = () => {
- // console.error(
- // `上一帧:${this.preFramesDecoded},当前帧:${currFramesDecoded},forceINums:${this.forceINums}`
- // );
- if (this.preFramesDecoded === currFramesDecoded) {
- if (this.forceINums >= this.forceINumsMax) {
- prettierInfo(
- '超过forceI次数,提示更换网络',
- { browser: this.browser.browser },
- 'warn'
- );
- this.forceINums = 0;
- } else {
- this.forceINums += 1;
- prettierInfo(
- `当前视频流卡主了,主动刷新云手机(${this.forceINums}/${this.forceINumsMax})`,
- { browser: this.browser.browser },
- 'warn'
- );
- }
- } else {
- this.forceINums = 0;
- // console.warn('视频流没有卡主');
- }
- this.preFramesDecoded = currFramesDecoded;
- };
- /** 处理黑屏 */
- const handleBlackScreen = () => {
- if (isBlack) {
- prettierInfo(
- '黑屏了',
- { browser: this.browser.browser },
- 'error'
- );
- }
- // else {
- // console.warn('没有黑屏');
- // }
- };
- /** 处理rtcStatus */
- const handleRtcStatus = () => {
- const res = this.rtcStatusIsOk();
- const length = Object.keys(res).length;
- if (length) {
- prettierInfo(
- `rtcStatus错误:${Object.keys(res).join()}`,
- { browser: this.browser.browser },
- 'error'
- );
- }
- // else {
- // console.warn('rtcStatus正常');
- // }
- };
- handleStreamStop();
- handleBlackScreen();
- handleRtcStatus();
- // if (!networkStore.errorCode.length) {
- // networkStore.setShowErrorModal(false);
- // }
- })
- .catch((err) => {
- console.error(new Date().toLocaleString(), 'getStatsErr', err);
- // networkStore.setErrorCode(frontendErrorCode.getStatsErr);
- // networkStore.setShowErrorModal(true);
- });
- }, this.getStatsSetIntervalDelay);
- };
- /** rtcStatus是否都是true了 */
- rtcStatusIsOk = () => {
- const res = {};
- const status = this.rtcStatus;
- Object.keys(status).forEach((key) => {
- if (!status[key]) {
- res[key] = false;
- }
- });
- return res;
- };
- /** 当前是否黑屏,true代表黑屏了,false代表没有黑屏 */
- isBlackScreen = ({
- type,
- kind,
- framesDecoded,
- decoderImplementation,
- isSafari = false,
- isChrome = false,
- }) => {
- // https://blog.csdn.net/weixin_44523653/article/details/127414387
- // console.warn(
- // // eslint-disable-next-line
- // `type:${type},kind:${kind},framesDecoded:${framesDecoded},decoderImplementation:${decoderImplementation}`
- // );
- if (isSafari) {
- if (
- type === 'inbound-rtp' &&
- kind === 'video' &&
- // framesDecoded等于0代表黑屏
- framesDecoded === 0
- ) {
- return true;
- }
- } else if (isChrome) {
- if (
- (type === 'track' || type === 'inbound-rtp') &&
- kind === 'video' &&
- // framesDecoded等于0代表黑屏
- framesDecoded === 0
- ) {
- return true;
- }
- } else {
- if (
- (type === 'track' || type === 'inbound-rtp') &&
- kind === 'video' &&
- // framesDecoded等于0代表黑屏
- framesDecoded === 0
- ) {
- // 安卓的qq浏览器适用这个黑屏判断
- return true;
- }
- }
- return false;
- };
- /** 获取当前帧 */
- getCurrentFramesDecoded = ({
- type,
- kind,
- framesDecoded = this.preFramesDecoded,
- isSafari = false,
- isChrome = false,
- }) => {
- if (isSafari) {
- if (type === 'inbound-rtp' && kind === 'video') {
- return framesDecoded;
- }
- } else if (isChrome) {
- if ((type === 'track' || type === 'inbound-rtp') && kind === 'video') {
- return framesDecoded;
- }
- } else {
- if ((type === 'track' || type === 'inbound-rtp') && kind === 'video') {
- // 安卓的qq浏览器适用这个卡屏判断
- return framesDecoded;
- }
- }
- return false;
- };
- // 创建offer
- createOffer = async () => {
- console.log('开始createOffer');
- if (!this.peerConnection) return;
- if (this.rtcStatus.createOffer) return this.localDescription;
- try {
- const description = await this.peerConnection.createOffer({
- iceRestart: true,
- offerToReceiveAudio: true,
- offerToReceiveVideo: true,
- });
- this.rtcStatus.createOffer = true;
- this.update();
- prettierInfo(
- 'createOffer成功',
- { browser: this.browser.browser },
- 'warn'
- );
- console.log('开始设置本地描述', description);
- await this.peerConnection.setLocalDescription(description);
- this.localDescription = description;
- this.rtcStatus.setLocalDescription = true;
- this.update();
- prettierInfo(
- 'setLocalDescription成功',
- { browser: this.browser.browser },
- 'warn'
- );
- return description;
- } catch (error) {
- prettierInfo(
- 'createOffer失败',
- { browser: this.browser.browser },
- 'error'
- );
- console.log(error);
- }
- };
- // 创建answer
- createAnswer = async () => {
- console.log('开始createAnswer');
- if (!this.peerConnection) return;
- try {
- const description = await this.peerConnection.createAnswer();
- this.update();
- prettierInfo(
- 'createAnswer成功',
- { browser: this.browser.browser },
- 'warn'
- );
- console.log('开始设置本地描述', description);
- await this.peerConnection.setLocalDescription(description);
- this.localDescription = description;
- this.rtcStatus.setLocalDescription = true;
- this.update();
- prettierInfo(
- 'setLocalDescription成功',
- { browser: this.browser.browser },
- 'warn'
- );
- return description;
- } catch (error) {
- prettierInfo(
- 'createAnswer失败',
- { browser: this.browser.browser },
- 'error'
- );
- console.log(error);
- }
- };
- // 设置远端描述
- setRemoteDescription = async (description) => {
- console.log('开始设置远端描述', description);
- if (!this.peerConnection) return;
- if (this.rtcStatus.setRemoteDescription) return;
- try {
- await this.peerConnection.setRemoteDescription(
- new RTCSessionDescription(description)
- );
- this.rtcStatus.setRemoteDescription = true;
- this.update();
- prettierInfo(
- 'setRemoteDescription成功',
- { browser: this.browser.browser },
- 'warn'
- );
- } catch (error) {
- console.error('设置远端描述错误', error);
- }
- };
- addStream = (stream) => {
- if (!this.peerConnection || this.rtcStatus.addStream) return;
- this.rtcStatus.addStream = true;
- this.stream = stream;
- console.log(stream, 22222);
- document.querySelector<HTMLVideoElement>('#localVideo')!.srcObject = stream;
- prettierInfo('addStream成功', { browser: this.browser.browser }, 'warn');
- this.update();
- };
- // 创建连接
- createConnect = () => {
- if (!this.peerConnection) return;
- console.warn('createConnect');
- this.peerConnection.addEventListener('icecandidate', (event) => {
- this.rtcStatus.icecandidate = true;
- this.update();
- prettierInfo(
- 'pc收到icecandidate',
- { browser: this.browser.browser },
- 'warn'
- );
- if (event.candidate) {
- // if (this.candidateFlag) return;
- const networkStore = useNetworkStore();
- this.candidateFlag = true;
- console.log('准备发送candidate', event.candidate.candidate);
- const data = {
- socketId: networkStore.wsMap.get(this.roomId)?.socketIo?.id,
- roomId: this.roomId,
- candidate: event.candidate.candidate,
- sdpMid: event.candidate.sdpMid,
- sdpMLineIndex: event.candidate.sdpMLineIndex,
- };
- networkStore.wsMap
- .get(this.roomId)
- ?.socketIo?.emit(wsMsgType.candidate, data);
- this.update();
- }
- });
- console.warn('开始监听addstream');
- this.peerConnection.addEventListener('addstream', (event: any) => {
- console.log('pc收到addstream事件', event.stream);
- // document.querySelector<HTMLVideoElement>('#localVideo')!.srcObject =
- // event.stream;
- // this.addStream(event.stream);
- });
- console.warn('开始监听ontrack');
- this.peerConnection.addEventListener('ontrack', (event: any) => {
- console.log('pc收到ontrack事件', event.stream);
- });
- console.warn('开始监听addtrack');
- this.peerConnection.addEventListener('addtrack', (event: any) => {
- console.log('pc收到addtrack事件', event.stream);
- });
- console.warn('开始监听track');
- this.peerConnection.addEventListener('track', (event: any) => {
- console.log('pc收到track事件', event);
- document.querySelector<HTMLVideoElement>('#localVideo')!.srcObject =
- event.streams[0];
- });
- // connectionstatechange
- this.peerConnection.addEventListener(
- 'connectionstatechange',
- (event: any) => {
- console.log('connectionstatechange', event);
- const connectionState = event.currentTarget.connectionState;
- const iceConnectionState = event.currentTarget.iceConnectionState;
- console.log(
- // eslint-disable-next-line
- `connectionState:${connectionState}, iceConnectionState:${iceConnectionState}`
- );
- if (connectionState === 'connected') {
- console.warn('connectionState:connected');
- }
- if (connectionState === 'failed') {
- // 失败
- console.error('connectionState:failed', event);
- }
- if (iceConnectionState === 'disconnected') {
- // 已断开,请重新连接
- console.error('iceConnectionState:disconnected', event);
- }
- }
- );
- };
- // 创建对等连接
- createPeerConnection() {
- if (!window.RTCPeerConnection) {
- console.error('当前环境不支持RTCPeerConnection!');
- alert('当前环境不支持RTCPeerConnection!');
- return;
- }
- if (!this.peerConnection) {
- this.peerConnection = new RTCPeerConnection({
- iceServers: [
- // {
- // urls: 'stun:stun.l.google.com:19302',
- // },
- {
- urls: 'turn:hsslive.cn:3478',
- username: 'hss',
- credential: '123456',
- },
- ],
- });
- // this.dataChannel =
- // this.peerConnection.createDataChannel('MessageChannel');
- // this.dataChannel.onopen = (event) => {
- // console.warn('dataChannel---onopen', event);
- // };
- // this.dataChannel.onerror = (event) => {
- // console.warn('dataChannel---onerror', event);
- // };
- // this.dataChannel.onmessage = (event) => {
- // console.log('dataChannel---onmessage', event);
- // };
- // this.peerConnection.addTransceiver('video', { direction: 'recvonly' });
- // this.peerConnection.addTransceiver('audio', { direction: 'recvonly' });
- this.createConnect();
- this.update();
- }
- }
- // 手动关闭webrtc连接
- close() {
- console.warn(`${new Date().toLocaleString()},手动关闭webrtc连接`);
- this.peerConnection?.close();
- this.dataChannel?.close();
- this.peerConnection = null;
- this.dataChannel = null;
- this.update();
- }
- // 更新store
- update = () => {
- const networkStore = useNetworkStore();
- networkStore.updateRtcMap(this.roomId, this);
- };
- }
|