소스 검색

fix: 不能说的秘密

shuisheng 1 년 전
부모
커밋
7436a76349
8개의 변경된 파일263개의 추가작업 그리고 136개의 파일을 삭제
  1. 3 3
      src/hooks/webrtc/srs.ts
  2. 0 16
      src/store/network/index.ts
  3. 54 8
      src/utils/network/webRTC.ts
  4. 1 1
      src/utils/network/webSocket.ts
  5. 3 3
      src/views/h5/room/index.vue
  6. 27 1
      src/views/pull/index.vue
  7. 175 104
      src/views/push/index.vue
  8. 0 0
      test/test.md

+ 3 - 3
src/hooks/webrtc/srs.ts

@@ -39,9 +39,9 @@ export const useWebRtcSrs = () => {
         resolutionRatio: currentResolutionRatio.value,
       });
       return new WebRTCClass({
-        // maxBitrate: currentMaxBitrate.value,
-        // maxFramerate: currentMaxFramerate.value,
-        // resolutionRatio: currentResolutionRatio.value,
+        maxBitrate: currentMaxBitrate.value,
+        maxFramerate: currentMaxFramerate.value,
+        resolutionRatio: currentResolutionRatio.value,
         isSRS: true,
         roomId: roomId.value,
         videoEl: data.videoEl,

+ 0 - 16
src/store/network/index.ts

@@ -16,14 +16,6 @@ export const useNetworkStore = defineStore('network', {
     };
   },
   actions: {
-    updateWsMap(roomId: string, arg) {
-      const val = this.wsMap.get(roomId);
-      if (val) {
-        this.wsMap.set(roomId, { ...val, ...arg });
-      } else {
-        this.wsMap.set(roomId, arg);
-      }
-    },
     removeWs(roomId: string) {
       const old = this.wsMap.get(roomId);
       if (old) {
@@ -31,14 +23,6 @@ export const useNetworkStore = defineStore('network', {
       }
       this.wsMap.delete(roomId);
     },
-    updateRtcMap(socketId: string, arg) {
-      const val = this.rtcMap.get(socketId);
-      if (val) {
-        this.rtcMap.set(socketId, { ...val, ...arg });
-      } else {
-        this.rtcMap.set(socketId, arg);
-      }
-    },
     removeRtc(socketId: string) {
       const old = this.rtcMap.get(socketId);
       if (old) {

+ 54 - 8
src/utils/network/webRTC.ts

@@ -57,6 +57,12 @@ export class WebRTCClass {
 
   isSRS: boolean;
 
+  loss = -1;
+
+  rtt = -1;
+
+  loopGetStatsTimer: any = null;
+
   constructor(data: {
     roomId: string;
     videoEl: HTMLVideoElement;
@@ -83,9 +89,55 @@ export class WebRTCClass {
     }
     this.isSRS = data.isSRS;
     console.warn('new webrtc参数:', data);
+    this.loopGetStats();
     this.createPeerConnection();
   }
 
+  loopGetStats = () => {
+    this.loopGetStatsTimer = setInterval(async () => {
+      if (!this.peerConnection) return;
+      try {
+        const res = await this.peerConnection.getStats();
+        // 总丢包率(音频丢包和视频丢包)
+        let loss = 0;
+        let rtt = 0;
+        res.forEach((report: RTCInboundRtpStreamStats) => {
+          // @ts-ignore
+          const currentRoundTripTime = report?.currentRoundTripTime;
+          const packetsLost = report?.packetsLost;
+          const packetsReceived = report.packetsReceived;
+          if (currentRoundTripTime !== undefined) {
+            rtt = currentRoundTripTime * 1000;
+          }
+          if (report.type === 'inbound-rtp' && report.kind === 'audio') {
+            if (packetsReceived !== undefined && packetsLost !== undefined) {
+              if (packetsLost === 0 || packetsReceived === 0) {
+                loss += 0;
+              } else {
+                loss += packetsLost / packetsReceived;
+              }
+            }
+          }
+          if (report.type === 'inbound-rtp' && report.kind === 'video') {
+            if (packetsReceived !== undefined && packetsLost !== undefined) {
+              if (packetsLost === 0 || packetsReceived === 0) {
+                loss += 0;
+              } else {
+                loss += packetsLost / packetsReceived;
+              }
+            }
+          }
+        });
+        this.loss = loss;
+        this.rtt = rtt;
+        this.update();
+      } catch (error) {
+        console.error('getStats错误');
+        console.log(error);
+      }
+    }, 1000);
+  };
+
   prettierLog = (data: {
     msg: string;
     type?: 'log' | 'warn' | 'error' | 'success';
@@ -233,13 +285,6 @@ export class WebRTCClass {
       const appStore = useAppStore();
       stream.onremovetrack = () => {
         this.prettierLog({ msg: 'onremovetrack事件', type: 'warn' });
-        // const res = appStore.allTrack.filter((info) => {
-        //   if (info.track?.id === event.track.id) {
-        //     return false;
-        //   }
-        //   return true;
-        // });
-        // appStore.setAllTrack(res);
       };
 
       const addTrack: AppRootState['allTrack'] = [];
@@ -561,6 +606,7 @@ export class WebRTCClass {
   close = () => {
     try {
       this.prettierLog({ msg: '手动关闭webrtc连接', type: 'warn' });
+      clearInterval(this.loopGetStatsTimer);
       this.localStream?.getTracks().forEach((track) => {
         track.stop();
       });
@@ -582,6 +628,6 @@ export class WebRTCClass {
   /** 更新store */
   update = () => {
     const networkStore = useNetworkStore();
-    networkStore.updateRtcMap(this.receiver, this);
+    networkStore.rtcMap.set(this.receiver, { ...this });
   };
 }

+ 1 - 1
src/utils/network/webSocket.ts

@@ -84,7 +84,7 @@ export class WebSocketClass {
   // 更新store
   update = () => {
     const networkStore = useNetworkStore();
-    networkStore.updateWsMap(this.roomId, this);
+    networkStore.wsMap.set(this.roomId, { ...this });
   };
 
   // 手动关闭websocket连接

+ 3 - 3
src/views/h5/room/index.vue

@@ -86,9 +86,9 @@
                 class="item"
               >
                 <template v-if="item.msgType === DanmuMsgTypeEnum.danmu">
-                  <span class="time"
-                    >[{{ formatTimeHour(item.send_msg_time) }}]</span
-                  >
+                  <span class="time">
+                    [{{ formatTimeHour(item.send_msg_time) }}]
+                  </span>
                   <span class="name">
                     <span v-if="item.userInfo">
                       <span>{{ item.userInfo.username }}</span>

+ 27 - 1
src/views/pull/index.vue

@@ -42,6 +42,8 @@
                 <div class="f-left">+关注</div>
                 <div class="f-right">666</div>
               </div>
+              <div class="rtt">延迟:{{ rtcRtt || '-' }}</div>
+              <div class="loss">丢包:{{ rtcLoss || '-' }}</div>
             </div>
             <div class="bottom">
               <span>{{ appStore.liveRoomInfo?.desc }}</span>
@@ -401,7 +403,7 @@
 
 <script lang="ts" setup>
 import { getRandomString, openToTarget } from 'billd-utils';
-import { onMounted, onUnmounted, ref, watch } from 'vue';
+import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { useRoute } from 'vue-router';
 
@@ -483,6 +485,22 @@ const {
   anchorInfo,
 } = usePull(roomId.value);
 
+const rtcRtt = computed(() => {
+  const arr: any[] = [];
+  networkStore.rtcMap.forEach((rtc) => {
+    arr.push(`${rtc.rtt}ms`);
+  });
+  return arr.join();
+});
+
+const rtcLoss = computed(() => {
+  const arr: any[] = [];
+  networkStore.rtcMap.forEach((rtc) => {
+    arr.push(`${Number(rtc.loss.toFixed(2))}%`);
+  });
+  return arr.join();
+});
+
 onMounted(() => {
   appStore.videoControls.fps = true;
   appStore.videoControls.fullMode = true;
@@ -933,14 +951,17 @@ function handleScrollTop() {
           .top {
             display: flex;
             margin-bottom: 10px;
+
             .follow {
               display: flex;
               align-items: center;
+              margin-right: 10px;
               margin-left: 10px;
               height: 20px;
               border-radius: 12px;
               background-color: $theme-color-gold;
               font-size: 12px;
+
               .f-left {
                 display: flex;
                 align-items: center;
@@ -957,6 +978,11 @@ function handleScrollTop() {
                 background-color: #e3e5e7;
               }
             }
+            .rtt,
+            .loss {
+              margin-right: 10px;
+              font-size: 14px;
+            }
           }
           .bottom {
             font-size: 14px;

+ 175 - 104
src/views/push/index.vue

@@ -88,21 +88,24 @@
           <div class="detail">
             <div class="top">
               <div class="name">
-                <n-input-group>
-                  <n-input
-                    v-model:value="roomName"
-                    size="small"
-                    placeholder="输入房间名"
-                    :style="{ width: '50%' }"
-                  />
-                  <n-button
-                    size="small"
-                    type="primary"
-                    @click="confirmRoomName"
-                  >
-                    确定
-                  </n-button>
-                </n-input-group>
+                <div class="ipt">
+                  <n-input-group>
+                    <n-input
+                      v-model:value="roomName"
+                      size="small"
+                      placeholder="输入房间名"
+                    />
+                    <n-button
+                      size="small"
+                      type="primary"
+                      @click="confirmRoomName"
+                    >
+                      确定
+                    </n-button>
+                  </n-input-group>
+                </div>
+                <div class="item">延迟:{{ rtcRtt || '-' }}</div>
+                <div class="item">丢包:{{ rtcLoss || '-' }}</div>
               </div>
               <div class="other">
                 <span
@@ -128,57 +131,60 @@
               </div>
             </div>
             <div class="bottom">
-              <div class="rtc">
-                <div class="item">
-                  <div class="txt">码率:</div>
-                  <div class="down small">
-                    <n-select
-                      size="small"
-                      v-model:value="currentMaxBitrate"
-                      :options="maxBitrate"
-                    />
+              <div class="rtc-config">
+                <div class="item-list">
+                  <div class="item">
+                    <div class="txt">码率:</div>
+                    <div class="down small">
+                      <n-select
+                        size="small"
+                        v-model:value="currentMaxBitrate"
+                        :options="maxBitrate"
+                      />
+                    </div>
                   </div>
-                </div>
-                <div class="item">
-                  <div class="txt">帧率:</div>
-                  <div class="down small">
-                    <n-select
-                      size="small"
-                      v-model:value="currentMaxFramerate"
-                      :options="maxFramerate"
-                    />
+                  <div class="item">
+                    <div class="txt">帧率:</div>
+                    <div class="down small">
+                      <n-select
+                        size="small"
+                        v-model:value="currentMaxFramerate"
+                        :options="maxFramerate"
+                      />
+                    </div>
                   </div>
-                </div>
-                <div class="item">
-                  <div class="txt">分辨率:</div>
-                  <div class="down big">
-                    <n-select
-                      size="small"
-                      v-model:value="currentResolutionRatio"
-                      :options="resolutionRatio"
-                    />
+                  <div class="item">
+                    <div class="txt">分辨率:</div>
+                    <div class="down big">
+                      <n-select
+                        size="small"
+                        v-model:value="currentResolutionRatio"
+                        :options="resolutionRatio"
+                      />
+                    </div>
                   </div>
-                </div>
-                <div class="item">
-                  <div class="txt">视频内容:</div>
-                  <div class="down small">
-                    <n-select
-                      size="small"
-                      v-model:value="currentVideoContentHint"
-                      :options="videoContentHint"
-                    />
+                  <div class="item">
+                    <div class="txt">视频内容:</div>
+                    <div class="down small">
+                      <n-select
+                        size="small"
+                        v-model:value="currentVideoContentHint"
+                        :options="videoContentHint"
+                      />
+                    </div>
                   </div>
-                </div>
-                <div class="item">
-                  <div class="txt">音频内容:</div>
-                  <div class="down big">
-                    <n-select
-                      size="small"
-                      v-model:value="currentAudioContentHint"
-                      :options="audioContentHint"
-                    />
+                  <div class="item">
+                    <div class="txt">音频内容:</div>
+                    <div class="down big">
+                      <n-select
+                        size="small"
+                        v-model:value="currentAudioContentHint"
+                        :options="audioContentHint"
+                      />
+                    </div>
                   </div>
                 </div>
+                <div class="rtc-network"></div>
               </div>
               <n-button
                 v-if="!roomLiving"
@@ -305,6 +311,9 @@
               class="item"
             >
               <template v-if="item.msgType === DanmuMsgTypeEnum.danmu">
+                <span class="time">
+                  [{{ formatTimeHour(item.send_msg_time) }}]
+                </span>
                 <span class="name">
                   <span v-if="item.userInfo">
                     {{ item.userInfo.username }}[{{
@@ -399,20 +408,6 @@
             发送
           </div>
         </div>
-        <!-- <div class="send-msg">
-          <input
-            v-model="danmuStr"
-            class="ipt"
-            @keydown="keydownDanmu"
-          />
-          <n-button
-            type="info"
-            size="small"
-            @click="sendDanmu"
-          >
-            发送
-          </n-button>
-        </div> -->
       </div>
     </div>
   </div>
@@ -463,6 +458,7 @@ import { copyToClipBoard, getRandomString } from 'billd-utils';
 import { fabric } from 'fabric';
 import {
   Raw,
+  computed,
   markRaw,
   onMounted,
   onUnmounted,
@@ -502,12 +498,15 @@ import {
   base64ToFile,
   createVideo,
   formatDownTime2,
+  formatTimeHour,
   generateBase64,
   getLiveRoomPageUrl,
   getRandomEnglishString,
   handleUserMedia,
   readFile,
   saveFile,
+  setAudioTrackContentHints,
+  setVideoTrackContentHints,
 } from '@/utils';
 import { NODE_ENV } from 'script/constant';
 
@@ -595,6 +594,22 @@ const recordVideoTimer = ref();
 const recordVideoTime = ref('00:00:00');
 let avRecorder: AVRecorder | null = null;
 
+const rtcRtt = computed(() => {
+  const arr: any[] = [];
+  networkStore.rtcMap.forEach((rtc) => {
+    arr.push(`${rtc.rtt}ms`);
+  });
+  return arr.join();
+});
+
+const rtcLoss = computed(() => {
+  const arr: any[] = [];
+  networkStore.rtcMap.forEach((rtc) => {
+    arr.push(`${Number(rtc.loss.toFixed(2))}%`);
+  });
+  return arr.join();
+});
+
 watch(
   () => roomLiving.value,
   (newval) => {
@@ -623,6 +638,34 @@ watch(
   }
 );
 
+watch(
+  () => currentVideoContentHint.value,
+  (newval) => {
+    console.log('视频内容变了', newval);
+    if (canvasVideoStream.value) {
+      setVideoTrackContentHints(
+        canvasVideoStream.value,
+        // @ts-ignore
+        currentVideoContentHint.value
+      );
+    }
+  }
+);
+
+watch(
+  () => currentAudioContentHint.value,
+  (newval) => {
+    console.log('音频内容变了', newval);
+    if (canvasVideoStream.value) {
+      setAudioTrackContentHints(
+        canvasVideoStream.value,
+        // @ts-ignore
+        currentAudioContentHint.value
+      );
+    }
+  }
+);
+
 watch(
   () => currentMaxFramerate.value,
   (newval) => {
@@ -752,23 +795,23 @@ async function uploadChange() {
 }
 
 function handleMediaRecorderAllType() {
-  // const types = [
-  //   'video/webm',
-  //   'audio/webm',
-  //   'video/mpeg',
-  //   'video/webm;codecs=vp8',
-  //   'video/webm;codecs=vp9',
-  //   'video/webm;codecs=daala',
-  //   'video/webm;codecs=h264',
-  //   'audio/webm;codecs=opus',
-  //   'audio/webm;codecs=aac',
-  //   'audio/webm;codecs=h264,opus',
-  //   'video/webm;codecs=avc1.64001f,opus',
-  //   'video/webm;codecs=avc1.4d002a,opus',
-  // ];
-  // Object.keys(types).forEach((item) => {
-  //   console.log(types[item], MediaRecorder.isTypeSupported(types[item]));
-  // });
+  const types = [
+    'video/webm',
+    'audio/webm',
+    'video/mpeg',
+    'video/webm;codecs=vp8',
+    'video/webm;codecs=vp9',
+    'video/webm;codecs=daala',
+    'video/webm;codecs=h264',
+    'audio/webm;codecs=opus',
+    'audio/webm;codecs=aac',
+    'audio/webm;codecs=h264,opus',
+    'video/webm;codecs=avc1.64001f,opus',
+    'video/webm;codecs=avc1.4d002a,opus',
+  ];
+  Object.keys(types).forEach((item) => {
+    console.log(types[item], MediaRecorder.isTypeSupported(types[item]));
+  });
 }
 
 function handleMsr(stream: MediaStream) {
@@ -2476,7 +2519,15 @@ function handleStartMedia(item: { type: MediaTypeEnum; txt: string }) {
         cursor: pointer;
         transform: translateX(-100%);
       }
-
+      .rtt {
+        position: absolute;
+        top: 5px;
+        left: 5px;
+        z-index: 100;
+        color: red;
+        font-size: 12px;
+        line-height: 1;
+      }
       .add-wrap {
         position: absolute;
         top: 50%;
@@ -2520,6 +2571,17 @@ function handleStartMedia(item: { type: MediaTypeEnum; txt: string }) {
             align-items: center;
             justify-content: space-between;
             color: #18191c;
+            .name {
+              display: flex;
+              align-items: center;
+              .ipt {
+                margin-right: 15px;
+                width: 200px;
+              }
+              .item {
+                padding-right: 10px;
+              }
+            }
             .other {
               .item {
                 margin-right: 10px;
@@ -2534,24 +2596,33 @@ function handleStartMedia(item: { type: MediaTypeEnum; txt: string }) {
             align-items: center;
             flex: 1;
             justify-content: space-between;
-            .rtc {
-              display: flex;
-              align-items: center;
-              flex: 0.95;
-              .item {
+            .rtc-config {
+              .item-list {
                 display: flex;
                 align-items: center;
-                padding-right: 10px;
-                .down {
-                  &.small {
-                    width: 90px;
-                  }
-                  &.big {
-                    width: 110px;
+                flex: 1;
+                .item {
+                  display: flex;
+                  align-items: center;
+                  padding-right: 10px;
+
+                  .down {
+                    &.small {
+                      width: 85px;
+                    }
+                    &.big {
+                      width: 105px;
+                    }
                   }
                 }
               }
             }
+            .rtc-network {
+              display: flex;
+              .item {
+                padding-right: 10px;
+              }
+            }
           }
         }
       }

+ 0 - 0
test/test.md