shuisheng 2 år sedan
förälder
incheckning
d55b0e7467
3 ändrade filer med 319 tillägg och 5 borttagningar
  1. 1 0
      package.json
  2. 1 0
      src/App.vue
  3. 317 5
      src/network/webRtc.ts

+ 1 - 0
package.json

@@ -34,6 +34,7 @@
     "billd-html-webpack-plugin": "^1.0.0",
     "billd-scss": "^0.0.6",
     "billd-utils": "^0.0.9",
+    "browser-tool": "^1.0.5",
     "flv.js": "^1.6.2",
     "pinia": "^2.0.11",
     "socket.io-client": "^4.6.1",

+ 1 - 0
src/App.vue

@@ -42,6 +42,7 @@ async function handleWebRtc() {
 
 // Handles start button action: creates local MediaStream.
 function startAction() {
+  // WARN navigator.mediaDevices在localhost和https才能用,http://192.168.1.103:8000局域网用不了
   navigator.mediaDevices
     .getUserMedia({ video: true, audio: true })
     .then((event) => {

+ 317 - 5
src/network/webRtc.ts

@@ -1,22 +1,313 @@
+import browserTool from 'browser-tool';
+
+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 {
   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 = {
+    joinRes: 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代表失败
+  };
+
   constructor() {
+    this.browser = browserTool();
     this.createPeerConnection();
+    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 () => {
     if (!this.peerConnection) return;
     try {
       const description = await this.peerConnection.createOffer();
-      console.warn('createOffer成功');
+      this.rtcStatus.createOffer = true;
+      prettierInfo(
+        'createOffer成功',
+        { browser: this.browser.browser },
+        'warn'
+      );
       await this.peerConnection.setLocalDescription(description);
-      console.warn('setLocalDescription成功', description);
+      this.rtcStatus.setLocalDescription = true;
+      prettierInfo(
+        'setLocalDescription成功',
+        { browser: this.browser.browser },
+        'warn'
+      );
       return description;
     } catch (error) {
-      console.error('创建offer失败', error);
+      prettierInfo('创建offer失败', { browser: this.browser.browser }, 'error');
     }
   };
 
@@ -27,18 +318,39 @@ export class WebRTCClass {
       await this.peerConnection.setRemoteDescription(
         new RTCSessionDescription(description)
       );
-      console.warn('设置远端描述成功');
+      this.rtcStatus.setRemoteDescription = true;
+      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;
+    prettierInfo('addStream成功', { browser: this.browser.browser }, 'warn');
+  };
+
   // 创建连接
   createConnect = () => {
     if (!this.peerConnection) return;
     console.warn('createConnect');
     this.peerConnection.addEventListener('icecandidate', (event) => {
-      console.log('icecandidate:', event);
+      this.rtcStatus.icecandidate = true;
+      prettierInfo(
+        'pc收到icecandidate',
+        { browser: this.browser.browser },
+        'warn'
+      );
+      if (event.candidate) {
+        if (this.candidateFlag) return;
+        this.candidateFlag = true;
+        console.log('准备发送_candidate', event.candidate.candidate);
+      }
     });
 
     this.peerConnection.addEventListener('addstream', (event: any) => {