lex-wxl 3 年 前
コミット
09c8a0e14b
37 ファイル変更1836 行追加775 行削除
  1. 0 0
      dist/assets/index-legacy.240acf31.js
  2. 0 0
      dist/assets/index-legacy.4c73a8cc.js
  3. 1 0
      dist/assets/index-legacy.4d72b509.js
  4. 1 0
      dist/assets/index-legacy.745a6ab0.js
  5. 1 0
      dist/assets/index-legacy.8f769e5e.js
  6. 1 0
      dist/assets/index-legacy.ca7ef942.js
  7. 1 0
      dist/assets/index.06d75a83.js
  8. 0 0
      dist/assets/index.39db2823.js
  9. 1 1
      dist/assets/index.99eb2737.js
  10. 0 0
      dist/assets/index.a94d216c.js
  11. 0 0
      dist/assets/index.b94cb86f.css
  12. 1 0
      dist/assets/index.e0102178.js
  13. 0 0
      dist/assets/polyfills-legacy.3fc2007f.js
  14. 0 0
      dist/assets/vendor-legacy.a6333def.js
  15. 0 0
      dist/assets/vendor.8df6bc42.js
  16. 0 0
      dist/assets/vendor.fef4890b.js
  17. 13 0
      dist/assets/wifi.fbaa2f05.svg
  18. 14 0
      dist/assets/wifi1.a1823335.svg
  19. 15 0
      dist/assets/wifi2.80304924.svg
  20. 10 0
      dist/index.html
  21. 11 0
      package-lock.json
  22. 2 0
      package.json
  23. 338 91
      src/components/live-broadcast/action-bar.tsx
  24. 1 0
      src/components/live-broadcast/event.ts
  25. 1 0
      src/components/live-broadcast/header.tsx
  26. 16 0
      src/components/live-broadcast/images/microphone.svg
  27. 234 113
      src/components/live-broadcast/index.tsx
  28. 489 264
      src/components/live-broadcast/runtime.ts
  29. 147 111
      src/components/live-message/model/index.module.less
  30. 250 109
      src/components/live-message/model/join-model.tsx
  31. 137 58
      src/components/live-message/model/look-model.tsx
  32. 6 1
      src/components/svg-icon/index.tsx
  33. 2 0
      src/icons/message/refresh.svg
  34. 101 27
      src/pages/home/header/index.tsx
  35. 13 0
      src/pages/home/header/wifi.svg
  36. 14 0
      src/pages/home/header/wifi1.svg
  37. 15 0
      src/pages/home/header/wifi2.svg

ファイルの差分が大きいため隠しています
+ 0 - 0
dist/assets/index-legacy.240acf31.js


ファイルの差分が大きいため隠しています
+ 0 - 0
dist/assets/index-legacy.4c73a8cc.js


ファイルの差分が大きいため隠しています
+ 1 - 0
dist/assets/index-legacy.4d72b509.js


ファイルの差分が大きいため隠しています
+ 1 - 0
dist/assets/index-legacy.745a6ab0.js


ファイルの差分が大きいため隠しています
+ 1 - 0
dist/assets/index-legacy.8f769e5e.js


ファイルの差分が大きいため隠しています
+ 1 - 0
dist/assets/index-legacy.ca7ef942.js


ファイルの差分が大きいため隠しています
+ 1 - 0
dist/assets/index.06d75a83.js


ファイルの差分が大きいため隠しています
+ 0 - 0
dist/assets/index.39db2823.js


+ 1 - 1
dist/assets/index.0da1cc3f.js → dist/assets/index.99eb2737.js

@@ -1 +1 @@
-import{d as i,a as t,t as s}from"./vendor.8df6bc42.js";const e="_wscnHttp404Container_1v1ex_1",c="_wscnHttp404_1v1ex_1",u="_pic404_1v1ex_13",n="_pic404__parent_1v1ex_19",o="_pic404__child_1v1ex_22",a="_left_1v1ex_25",d="_cloudLeft_1v1ex_1",r="_mid_1v1ex_36",h="_cloudMid_1v1ex_1",p="_right_1v1ex_47",v="_cloudRight_1v1ex_1",b="_bullshit_1v1ex_124",x="_bullshit__oops_1v1ex_132",m="_slideUp_1v1ex_1",g="_bullshit__headline_1v1ex_143",f="_bullshit__info_1v1ex_155",H="_bullshit__returnHome_1v1ex_166";var _={wscnHttp404Container:e,wscnHttp404:c,pic404:u,pic404__parent:n,pic404__child:o,left:a,cloudLeft:d,mid:r,cloudMid:h,right:p,cloudRight:v,bullshit:b,bullshit__oops:x,slideUp:m,bullshit__headline:g,bullshit__info:f,bullshit__returnHome:H},F="./assets/404.538aa4d7.png",l="./assets/404_cloud.98e7ac66.png",w=i({name:"Page404",data(){return{message:"\u5F88\u62B1\u6B49\uFF0C\u4F60\u8BBF\u95EE\u7684\u9875\u9762\u4E0D\u5B58\u5728"}},render(){return t("div",{class:_.wscnHttp404Container},[t("div",{class:_.wscnHttp404},[t("div",{class:_.pic404},[t("img",{class:_.pic404__parent,src:F,alt:"404"},null),t("img",{class:[_.pic404__child,_.left],src:l,alt:"404"},null),t("img",{class:[_.pic404__child,_.mid],src:l,alt:"404"},null),t("img",{class:[_.pic404__child,_.right],src:l,alt:"404"},null)]),t("div",{class:_.bullshit},[t("div",{class:_.bullshit__oops},[s("OOPS!")]),t("div",{class:_.bullshit__headline},[this.message]),t("div",{class:_.bullshit__info},[s("\u8BF7\u68C0\u67E5\u60A8\u8F93\u5165\u7684\u7F51\u5740\u662F\u5426\u6B63\u786E\uFF0C\u6216\u8005\u70B9\u51FB\u94FE\u63A5\u7EE7\u7EED\u6D4F\u89C8")]),t("a",{href:"",class:_.bullshit__returnHome},[s("\u8FD4\u56DE\u9996\u9875")])])])])}});export{w as default};
+import{d as i,a as t,t as s}from"./vendor.fef4890b.js";const e="_wscnHttp404Container_1v1ex_1",c="_wscnHttp404_1v1ex_1",u="_pic404_1v1ex_13",n="_pic404__parent_1v1ex_19",o="_pic404__child_1v1ex_22",a="_left_1v1ex_25",d="_cloudLeft_1v1ex_1",r="_mid_1v1ex_36",h="_cloudMid_1v1ex_1",p="_right_1v1ex_47",v="_cloudRight_1v1ex_1",b="_bullshit_1v1ex_124",x="_bullshit__oops_1v1ex_132",m="_slideUp_1v1ex_1",g="_bullshit__headline_1v1ex_143",f="_bullshit__info_1v1ex_155",H="_bullshit__returnHome_1v1ex_166";var _={wscnHttp404Container:e,wscnHttp404:c,pic404:u,pic404__parent:n,pic404__child:o,left:a,cloudLeft:d,mid:r,cloudMid:h,right:p,cloudRight:v,bullshit:b,bullshit__oops:x,slideUp:m,bullshit__headline:g,bullshit__info:f,bullshit__returnHome:H},F="./assets/404.538aa4d7.png",l="./assets/404_cloud.98e7ac66.png",w=i({name:"Page404",data(){return{message:"\u5F88\u62B1\u6B49\uFF0C\u4F60\u8BBF\u95EE\u7684\u9875\u9762\u4E0D\u5B58\u5728"}},render(){return t("div",{class:_.wscnHttp404Container},[t("div",{class:_.wscnHttp404},[t("div",{class:_.pic404},[t("img",{class:_.pic404__parent,src:F,alt:"404"},null),t("img",{class:[_.pic404__child,_.left],src:l,alt:"404"},null),t("img",{class:[_.pic404__child,_.mid],src:l,alt:"404"},null),t("img",{class:[_.pic404__child,_.right],src:l,alt:"404"},null)]),t("div",{class:_.bullshit},[t("div",{class:_.bullshit__oops},[s("OOPS!")]),t("div",{class:_.bullshit__headline},[this.message]),t("div",{class:_.bullshit__info},[s("\u8BF7\u68C0\u67E5\u60A8\u8F93\u5165\u7684\u7F51\u5740\u662F\u5426\u6B63\u786E\uFF0C\u6216\u8005\u70B9\u51FB\u94FE\u63A5\u7EE7\u7EED\u6D4F\u89C8")]),t("a",{href:"",class:_.bullshit__returnHome},[s("\u8FD4\u56DE\u9996\u9875")])])])])}});export{w as default};

ファイルの差分が大きいため隠しています
+ 0 - 0
dist/assets/index.a94d216c.js


ファイルの差分が大きいため隠しています
+ 0 - 0
dist/assets/index.b94cb86f.css


ファイルの差分が大きいため隠しています
+ 1 - 0
dist/assets/index.e0102178.js


ファイルの差分が大きいため隠しています
+ 0 - 0
dist/assets/polyfills-legacy.3fc2007f.js


ファイルの差分が大きいため隠しています
+ 0 - 0
dist/assets/vendor-legacy.a6333def.js


ファイルの差分が大きいため隠しています
+ 0 - 0
dist/assets/vendor.8df6bc42.js


ファイルの差分が大きいため隠しています
+ 0 - 0
dist/assets/vendor.fef4890b.js


+ 13 - 0
dist/assets/wifi.fbaa2f05.svg

@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="20px" height="14px" viewBox="0 0 20 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>网络信号</title>
+    <g id="后台直播界面" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="图标页面状态" transform="translate(-697.000000, -264.000000)" fill="#00FFF0" fill-rule="nonzero">
+            <g id="编组-14" transform="translate(685.000000, 255.000000)">
+                <g id="网络信号" transform="translate(12.000000, 9.000000)">
+                    <path d="M0,4.0828061 C5.53263112,-1.36093537 14.5108046,-1.36093537 20,4.0828061 L18.1691823,5.89846349 C13.6594636,1.42393416 6.34053643,1.42393416 1.83081768,5.89846349 L0,4.0828061 Z M7.27549137,11.2991289 C8.76533826,9.82377262 11.2346617,9.82377262 12.7234227,11.2991289 L10,14 L7.27549137,11.2991289 L7.27549137,11.2991289 Z M3.65946357,7.66996789 C7.19187751,4.20988058 12.8515583,4.20988058 16.3405364,7.66996789 L14.5531545,9.48562528 C12.0375994,6.9950235 7.9634865,6.9950235 5.44793137,9.48562528 L3.65946357,7.66996789 Z" id="形状"></path>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 14 - 0
dist/assets/wifi1.a1823335.svg

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="20px" height="14px" viewBox="0 0 20 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>网络信号</title>
+    <g id="后台直播界面" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="图标页面状态" transform="translate(-818.000000, -264.000000)" fill-rule="nonzero">
+            <g id="编组-14备份-2" transform="translate(806.000000, 255.000000)">
+                <g id="网络信号" transform="translate(12.000000, 9.000000)">
+                    <path d="M7.27549137,11.2991289 C8.76533826,9.82377262 11.2346617,9.82377262 12.7234227,11.2991289 L10,14 L7.27549137,11.2991289 L7.27549137,11.2991289 Z M3.65946357,7.66996789 C7.19187751,4.20988058 12.8515583,4.20988058 16.3405364,7.66996789 L14.5531545,9.48562528 C12.0375994,6.9950235 7.9634865,6.9950235 5.44793137,9.48562528 L3.65946357,7.66996789 Z" id="形状" fill="#00FFF0"></path>
+                    <path d="M0,4.0828061 C5.53263112,-1.36093537 14.5108046,-1.36093537 20,4.0828061 L18.1691823,5.89846349 C13.6594636,1.42393416 6.34053643,1.42393416 1.83081768,5.89846349 L0,4.0828061 Z" id="路径" fill-opacity="0.5" fill="#FFFFFF"></path>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 15 - 0
dist/assets/wifi2.80304924.svg

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="20px" height="14px" viewBox="0 0 20 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>网络信号</title>
+    <g id="后台直播界面" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="图标页面状态" transform="translate(-939.000000, -264.000000)" fill-rule="nonzero">
+            <g id="编组-14备份" transform="translate(927.000000, 255.000000)">
+                <g id="网络信号" transform="translate(12.000000, 9.000000)">
+                    <path d="M0,4.0828061 C5.53263112,-1.36093537 14.5108046,-1.36093537 20,4.0828061 L18.1691823,5.89846349 C13.6594636,1.42393416 6.34053643,1.42393416 1.83081768,5.89846349 L0,4.0828061 Z" id="路径" fill-opacity="0.5" fill="#FFFFFF"></path>
+                    <path d="M7.27549137,11.2991289 C8.76533826,9.82377262 11.2346617,9.82377262 12.7234227,11.2991289 L10,14 L7.27549137,11.2991289 L7.27549137,11.2991289 Z" id="路径" fill="#FF4E19"></path>
+                    <path d="M3.65946357,7.66996789 C7.19187751,4.20988058 12.8515583,4.20988058 16.3405364,7.66996789 L14.5531545,9.48562528 C12.0375994,6.9950235 7.9634865,6.9950235 5.44793137,9.48562528 L3.65946357,7.66996789 Z" id="路径" fill-opacity="0.5" fill="#FFFFFF"></path>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 10 - 0
dist/index.html

@@ -6,8 +6,13 @@
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
     <title>管乐迷</title>
     
+<<<<<<< HEAD
     <script type="module" crossorigin src="./assets/index.69bf2a50.js"></script>
     <link rel="modulepreload" href="./assets/vendor.8df6bc42.js">
+=======
+    <script type="module" crossorigin src="./assets/index.39db2823.js"></script>
+    <link rel="modulepreload" href="./assets/vendor.fef4890b.js">
+>>>>>>> master
     <link rel="stylesheet" href="./assets/index.cd9189f2.css">
     <script type="module">!function(){try{new Function("m","return import(m)")}catch(o){console.warn("vite: loading legacy build because dynamic import is unsupported, syntax error above should be ignored");var e=document.getElementById("vite-legacy-polyfill"),n=document.createElement("script");n.src=e.src,n.onload=function(){System.import(document.getElementById('vite-legacy-entry').getAttribute('data-src'))},document.body.appendChild(n)}}();</script>
   </head>
@@ -130,7 +135,12 @@
       }
     </script>
     <script nomodule>!function(){var e=document,t=e.createElement("script");if(!("noModule"in t)&&"onbeforeload"in t){var n=!1;e.addEventListener("beforeload",(function(e){if(e.target===t)n=!0;else if(!e.target.hasAttribute("nomodule")||!n)return;e.preventDefault()}),!0),t.type="module",t.src=".",e.head.appendChild(t),t.remove()}}();</script>
+<<<<<<< HEAD
     <script nomodule id="vite-legacy-polyfill" src="./assets/polyfills-legacy.7f3fc881.js"></script>
     <script nomodule id="vite-legacy-entry" data-src="./assets/index-legacy.745a6ab0.js">System.import(document.getElementById('vite-legacy-entry').getAttribute('data-src'))</script>
+=======
+    <script nomodule id="vite-legacy-polyfill" src="./assets/polyfills-legacy.3fc2007f.js"></script>
+    <script nomodule id="vite-legacy-entry" data-src="./assets/index-legacy.4d72b509.js">System.import(document.getElementById('vite-legacy-entry').getAttribute('data-src'))</script>
+>>>>>>> master
   </body>
 </html>

+ 11 - 0
package-lock.json

@@ -1455,6 +1455,12 @@
         "@types/node": "*"
       }
     },
+    "@types/throttle-debounce": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/@types/throttle-debounce/-/throttle-debounce-5.0.0.tgz",
+      "integrity": "sha512-Pb7k35iCGFcGPECoNE4DYp3Oyf2xcTd3FbFQxXUI9hEYKUl6YX+KLf7HrBmgVcD05nl50LIH6i+80js4iYmWbw==",
+      "dev": true
+    },
     "@types/trusted-types": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz",
@@ -5360,6 +5366,11 @@
         }
       }
     },
+    "throttle-debounce": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.0.tgz",
+      "integrity": "sha512-2iQTSgkkc1Zyk0MeVrt/3BvuOXYPl/R8Z0U2xxo9rjwNciaHDG3R+Lm6dh4EeUci49DanvBnuqI6jshoQQRGEg=="
+    },
     "tiny-emitter": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",

+ 2 - 0
package.json

@@ -23,6 +23,7 @@
     "nprogress": "^0.2.0",
     "qrcode": "^1.5.0",
     "query-string": "^7.1.1",
+    "throttle-debounce": "^5.0.0",
     "umi-request": "^1.4.0",
     "vue": "^3.2.25",
     "vue-router": "^4.0.12"
@@ -30,6 +31,7 @@
   "devDependencies": {
     "@types/js-cookie": "^3.0.1",
     "@types/node": "^17.0.17",
+    "@types/throttle-debounce": "^5.0.0",
     "@types/webpack-env": "^1.16.3",
     "@vitejs/plugin-legacy": "^1.7.1",
     "@vitejs/plugin-vue": "^2.2.0",

+ 338 - 91
src/components/live-broadcast/action-bar.tsx

@@ -1,8 +1,19 @@
-import { defineComponent, reactive } from 'vue'
-import { ElButton, ElDropdown, ElDropdownMenu, ElDropdownItem, ElSlider, ElDialog, ElIcon } from 'element-plus'
+import { defineComponent, reactive, watch } from 'vue'
+import {
+  ElButton,
+  ElDropdown,
+  ElDropdownMenu,
+  ElDropdownItem,
+  ElSlider,
+  ElDialog,
+  ElIcon,
+} from 'element-plus'
 import runtime, * as RuntimeUtils from './runtime'
+import MicroPhoneIcon from './images/microphone.svg?raw'
 import styles from './action-bar.module.less'
 import Share from './share'
+import { requireMedia } from './helpers'
+import SvgIcon from '../svg-icon'
 
 export const state = reactive({
   volume: 0,
@@ -10,30 +21,128 @@ export const state = reactive({
     camera: false, // 摄像头
     volume: false, // 声音调节
     microphone: false, // 麦克风
+    microphone2: false, // 系统音频
     screen: false, // 共享屏幕
     share: false, // 分享
   },
-  shareVisiable: false
+  shareVisiable: false,
+  binded: false,
 })
 
 export default defineComponent({
   name: 'LiveBroadcast-ActionBar',
   computed: {
     isCameraDisabled() {
-      return state.barStatus.camera && runtime.deviceStatus.camera !== 'denied' && runtime.cameras.length
+      return (
+        state.barStatus.camera &&
+        runtime.deviceStatus.camera !== 'denied' &&
+        runtime.cameras.length
+      )
     },
     isMicrophoneDisabled() {
-      const isDisabled = state.barStatus.microphone && runtime.deviceStatus.microphone !== 'denied' && runtime.microphones.length
+      const isDisabled =
+        state.barStatus.microphone &&
+        runtime.deviceStatus.microphone !== 'denied' &&
+        runtime.microphones.length
       return isDisabled
     },
+    isMicrophone2Disabled() {
+      console.log(runtime.selectedMicrophone2)
+      const isDisabled =
+        state.barStatus.microphone2 &&
+        runtime.deviceStatus.microphone !== 'denied' &&
+        runtime.microphones.length
+      return isDisabled || !runtime.selectedMicrophone2
+    },
     isVolumeDisabled() {
       return state.volume === 0
-    }
+    },
   },
   mounted() {
-    console.log(runtime.cameras, runtime.cameras.length)
+    RuntimeUtils.runtimeEvent.on('microphoneChange', this.listenMicophoneAudioTrack)
+    // RuntimeUtils.runtimeEvent.on('microphoneChange2', this.listenMicophoneAudioTrack)
+    // console.log(runtime.microphones, runtime.microphones.length)
+    // watch(runtime, () => {
+    //   console.log('runtime changed')
+    //   if (state.binded) return
+    // this.listenMicophoneAudioTrack()
+    // })
+  },
+  beforeUnmount() {
+    RuntimeUtils.runtimeEvent.off('microphoneChange', this.listenMicophoneAudioTrack)
+    // RuntimeUtils.runtimeEvent.off('microphoneChange2', this.listenMicophoneAudioTrack)
   },
   methods: {
+    setIconPath(iconName: string, num: number) {
+      const icon = (this.$refs[iconName] as HTMLSpanElement)?.querySelector(
+        '#microphone-content-svg'
+      )
+      if (icon) {
+        const line = 14 - 11 * (num / 100)
+        icon.setAttribute(
+          'd',
+          `M7,${line} L14,${line} L14,10.5 C14,12.4329966 12.4329966,14 10.5,14 C8.56700338,14 7,12.4329966 7,10.5 L7,${line} L7,${line} Z`
+        )
+      }
+    },
+    async listenMicophoneAudioTrack() {
+      let levelChecker: ScriptProcessorNode, levelChecker2: ScriptProcessorNode
+      // console.log('listenMicophoneAudioTrack', runtime.selectedMicrophone)
+      if (runtime.selectedMicrophone && !this.isMicrophoneDisabled) {
+        const mediaStreams = await requireMedia({
+          audio: {
+            deviceId: runtime.selectedMicrophone?.deviceId,
+          },
+          video: false,
+        })
+        levelChecker = RuntimeUtils.listenAudioChecker(mediaStreams, (num, label) => {
+          this.setIconPath('microphoneicon', num)
+        })
+      } else {
+        // @ts-ignore
+        if (levelChecker && levelChecker?.onaudioprocess) {
+          // @ts-ignore
+          // levelChecker?.onaudioprocess = null
+        }
+      }
+      // if (runtime.selectedMicrophone2 && !this.isMicrophone2Disabled) {
+      //   const mediaStreams = await requireMedia({
+      //     audio: {
+      //       deviceId: runtime.selectedMicrophone2?.deviceId,
+      //     },
+      //     video: false,
+      //   })
+      //   levelChecker2 = RuntimeUtils.listenAudioChecker(mediaStreams, (num, label) => {
+      //     console.log(num, label)
+      //     this.setIconPath('microphoneicon2', num)
+      //   })
+      // } else {
+      //   console.log('disconnect')
+      //   // @ts-ignore
+      //   if (levelChecker2 && levelChecker2?.onaudioprocess) {
+      //     // @ts-ignore
+      //     // levelChecker2?.onaudioprocess = null
+      //   }
+      // }
+      // for (const item of runtime.microphones) {
+      //   const mediaStreams = await requireMedia({
+      //     audio: {
+      //       deviceId: item.deviceId,
+      //     },
+      //     video: false,
+      //   })
+      //   const indexof = runtime.microphones.indexOf(item)
+      //   RuntimeUtils.listenAudioChecker(mediaStreams, (num, label) => {
+      //     // this.setIconPath('microphoneicon-' + indexof, num)
+      //     // if (indexof === 0) {
+      //     // this.setIconPath('microphoneicon', num)
+      //     // this.setIconPath('microphoneicon2', num)
+      //     // }
+      //     // console.log(num, label, indexof, this.$refs['microphoneicon-' + indexof])
+      //   })
+      //   state.binded = true
+      // }
+    },
     startShare() {
       console.log('调用')
       state.shareVisiable = true
@@ -41,7 +150,35 @@ export default defineComponent({
     volumeChange(value: number) {
       state.volume = value
       RuntimeUtils.setVolume(value)
-    }
+    },
+    async toggleCamera() {
+      RuntimeUtils.toggleDevice('camera')
+      if (runtime.screenShareStatus) {
+        return
+      }
+      state.barStatus.camera = !state.barStatus.camera
+    },
+    async toggleMicrophone() {
+      const needPublish = runtime.videoStatus === 'liveing'
+      state.barStatus.microphone = !state.barStatus.microphone
+      if (!state.barStatus.microphone) {
+        RuntimeUtils.openDevice('microphone', needPublish)
+      } else {
+        RuntimeUtils.closeDevice('microphone', needPublish)
+      }
+      this.listenMicophoneAudioTrack()
+    },
+    async toggleMicrophone2() {
+      const needPublish = runtime.videoStatus === 'liveing'
+      console.log('toggleMicrophone2', state.barStatus.microphone2)
+      state.barStatus.microphone2 = !state.barStatus.microphone2
+      if (!state.barStatus.microphone2) {
+        RuntimeUtils.openDevice('microphone', needPublish)
+      } else {
+        RuntimeUtils.closeDevice('microphone', needPublish)
+      }
+      this.listenMicophoneAudioTrack()
+    },
   },
   render() {
     return (
@@ -50,42 +187,47 @@ export default defineComponent({
           <div class={styles['bar-btn']}>
             <div class={styles.btnInner}>
               <SvgIcon
-                onClick={() => {
-                  RuntimeUtils.toggleDevice('camera')
-                  if (runtime.screenShareStatus) {
-                    return
-                  }
-                  state.barStatus.camera = !state.barStatus.camera
-                }}
-                name={this.isCameraDisabled ? 'bar-camera-disabled' : 'bar-camera'}
+                onClick={this.toggleCamera}
+                name={
+                  this.isCameraDisabled ? 'bar-camera-disabled' : 'bar-camera'
+                }
                 style={{
                   width: '22px',
-                  cursor: 'pointer'
+                  cursor: 'pointer',
                 }}
               />
-              { runtime.cameras.length === 0 ? null : <ElDropdown
-                placement="top"
-                // @ts-ignore
-                disabled={runtime.cameras.length === 0}
-                onCommand={RuntimeUtils.setSelectCamera}
-                // @ts-ignore
-                vSlots={{
-                  dropdown: () => (
-                    <ElDropdownMenu>
-                      {runtime.cameras.map(item => (<ElDropdownItem disabled={item === runtime.selectedCamera} command={item}>{item.label}</ElDropdownItem>))}
-                    </ElDropdownMenu>
-                  )
-                }}
-              >
-                <div class={styles['bar-btn']} style={{ height: '32px' }}>
-                  <SvgIcon
-                    name="bar-arrow-down"
-                    style={{
-                      width: '18px'
-                    }}
-                  />
-                </div>
-              </ElDropdown> }
+              {runtime.cameras.length === 0 ? null : (
+                <ElDropdown
+                  placement="top"
+                  // @ts-ignore
+                  disabled={runtime.cameras.length === 0}
+                  onCommand={RuntimeUtils.setSelectCamera}
+                  // @ts-ignore
+                  vSlots={{
+                    dropdown: () => (
+                      <ElDropdownMenu>
+                        {runtime.cameras.map((item) => (
+                          <ElDropdownItem
+                            disabled={item === runtime.selectedCamera}
+                            command={item}
+                          >
+                            {item.label}
+                          </ElDropdownItem>
+                        ))}
+                      </ElDropdownMenu>
+                    ),
+                  }}
+                >
+                  <div class={styles['bar-btn']} style={{ height: '32px' }}>
+                    <SvgIcon
+                      name="bar-arrow-down"
+                      style={{
+                        width: '18px',
+                      }}
+                    />
+                  </div>
+                </ElDropdown>
+              )}
             </div>
             <span class={styles['bar-btn-text']}>摄像头</span>
           </div>
@@ -133,18 +275,27 @@ export default defineComponent({
             <span class={styles['bar-btn-text']}>音量调节</span>
           </div> */}
 
-          <div class={styles['bar-btn']} onClick={RuntimeUtils.toggleShareScreenVideo}>
+          {/* <div
+            class={styles['bar-btn']}
+            onClick={RuntimeUtils.toggleShareScreenVideo}
+          >
             <div class={styles.btnInner}>
               <SvgIcon
-                name={runtime.videoStatus === 'liveing' ? 'bar-screen-share' : 'bar-screen-share-disabled2'}
+                name={
+                  runtime.videoStatus === 'liveing'
+                    ? 'bar-screen-share'
+                    : 'bar-screen-share-disabled2'
+                }
                 style={{
                   width: '22px',
-                  cursor: 'pointer'
+                  cursor: 'pointer',
                 }}
               />
             </div>
-            <span class={styles['bar-btn-text']}>{runtime.screenShareStatus ? '取消共享' : '屏幕共享'}</span>
-          </div>
+            <span class={styles['bar-btn-text']}>
+              {runtime.screenShareStatus ? '取消共享' : '屏幕共享'}
+            </span>
+          </div> */}
 
           {/* <div class={styles['bar-btn']} >
             <div class={styles.btnInner}>
@@ -160,72 +311,168 @@ export default defineComponent({
           </div> */}
           <div class={styles['bar-btn']}>
             <div class={styles.btnInner}>
+              {this.isMicrophoneDisabled ? (
+                <SvgIcon
+                  name="bar-mike-disabled"
+                  onClick={this.toggleMicrophone}
+                  style={{
+                    width: '22px',
+                    height: '22px',
+                    cursor: 'pointer',
+                  }}
+                />
+              ) : (
+                <span
+                  ref="microphoneicon"
+                  v-html={MicroPhoneIcon}
+                  onClick={this.toggleMicrophone}
+                  // name={this.isMicrophoneDisabled ? 'bar-mike-disabled' : 'bar-mike'}
+                  style={{
+                    width: '22px',
+                    height: '22px',
+                    cursor: 'pointer',
+                  }}
+                />
+              )}
+              {runtime.microphones.length === 0 ? null : (
+                <ElDropdown
+                  placement="top-start"
+                  // @ts-ignore
+                  disabled={runtime.microphones.length === 0}
+                  popper-options={{
+                    boundariesElement: '#action-bar',
+                    gpuAcceleration: false,
+                  }}
+                  visible-change={(visible: boolean) => {
+                    console.log(visible)
+                  }}
+                  onCommand={RuntimeUtils.setSelectMicrophone}
+                  // @ts-ignore
+                  vSlots={{
+                    dropdown: () => (
+                      <ElDropdownMenu>
+                        {runtime.microphones.map((item, index) => (
+                          <ElDropdownItem
+                            disabled={
+                              item === runtime.selectedMicrophone ||
+                              item === runtime.selectedMicrophone2
+                            }
+                            command={item}
+                          >
+                            {/* <span
+                              ref={'microphoneicon-' + index}
+                              v-html={MicroPhoneIcon}
+                              // name={this.isMicrophoneDisabled ? 'bar-mike-disabled' : 'bar-mike'}
+                              style={{
+                                width: '22px',
+                                cursor: 'pointer',
+                              }}
+                            /> */}
+                            {' ' + item.label}
+                          </ElDropdownItem>
+                        ))}
+                      </ElDropdownMenu>
+                    ),
+                  }}
+                >
+                  <div class={styles['bar-btn']} style={{ height: '32px' }}>
+                    <SvgIcon
+                      name="bar-arrow-down"
+                      style={{
+                        width: '18px',
+                      }}
+                    />
+                  </div>
+                </ElDropdown>
+              )}
+            </div>
+            <span class={styles['bar-btn-text']}>麦克风</span>
+          </div>
+          {/* <div class={styles['bar-btn']}>
+            <div class={styles.btnInner}>
               <SvgIcon
-                onClick={() => {
-                  const needPublish = runtime.videoStatus === 'liveing'
-                  state.barStatus.microphone = !state.barStatus.microphone
-                  if (!state.barStatus.microphone) {
-                    RuntimeUtils.openDevice('microphone', needPublish)
-                  } else {
-                    RuntimeUtils.closeDevice('microphone', needPublish)
-                  }
-                }}
-                name={this.isMicrophoneDisabled ? 'bar-mike-disabled' : 'bar-mike'}
+                onClick={this.toggleMicrophone2}
                 style={{
                   width: '22px',
-                  cursor: 'pointer'
+                  height: '22px',
+                  cursor: 'pointer',
                 }}
+                name={this.isMicrophone2Disabled ? 'bar-volume-disabled' : 'bar-volume'}
               />
-              { runtime.microphones.length === 0 ? null : <ElDropdown
-                placement="top-start"
-                // @ts-ignore
-                disabled={runtime.microphones.length === 0}
-                popper-options={{ boundariesElement: '#action-bar', gpuAcceleration: false }}
-                onCommand={RuntimeUtils.setSelectMicrophone}
-                // @ts-ignore
-                vSlots={{
-                  dropdown: () => (
-                    <ElDropdownMenu>
-                      {runtime.microphones.map(item => (<ElDropdownItem disabled={item === runtime.selectedMicrophone} command={item}>{item.label}</ElDropdownItem>))}
-                    </ElDropdownMenu>
-                  )
-                }}
-              >
-                <div class={styles['bar-btn']} style={{ height: '32px' }}>
-                  <SvgIcon
-                    name="bar-arrow-down"
-                    style={{
-                      width: '18px'
-                    }}
-                  />
-                </div>
-              </ElDropdown>}
+              {runtime.microphones.length === 0 ? null : (
+                <ElDropdown
+                  placement="top-start"
+                  // @ts-ignore
+                  disabled={runtime.microphones.length === 0}
+                  popper-options={{
+                    boundariesElement: '#action-bar',
+                    gpuAcceleration: false,
+                  }}
+                  visible-change={(visible: boolean) => {
+                    console.log(visible)
+                  }}
+                  onCommand={RuntimeUtils.setSelectMicrophone2}
+                  // @ts-ignore
+                  vSlots={{
+                    dropdown: () => (
+                      <ElDropdownMenu>
+                        {runtime.microphones.map((item, index) => (
+                          <ElDropdownItem
+                            disabled={
+                              item === runtime.selectedMicrophone2 ||
+                              item === runtime.selectedMicrophone
+                            }
+                            command={item}
+                          >
+                            {' ' + item.label}
+                          </ElDropdownItem>
+                        ))}
+                      </ElDropdownMenu>
+                    ),
+                  }}
+                >
+                  <div class={styles['bar-btn']} style={{ height: '32px' }}>
+                    <SvgIcon
+                      name="bar-arrow-down"
+                      style={{
+                        width: '18px',
+                      }}
+                    />
+                  </div>
+                </ElDropdown>
+              )}
             </div>
-            <span class={styles['bar-btn-text']}>麦克风</span>
-          </div>
-
+            <span class={styles['bar-btn-text']}>系统音频</span>
+          </div> */}
         </div>
         <div style={{ display: 'flex' }} onClick={this.startShare}>
-          <div class={styles['bar-btn']} >
+          <div class={styles['bar-btn']}>
             <div class={styles.btnInner}>
               <SvgIcon
                 name="bar-share"
                 style={{
                   width: '22px',
-                  cursor: 'pointer'
+                  cursor: 'pointer',
                 }}
               />
             </div>
-            <span class={styles['bar-btn-text']} >分享</span>
+            <span class={styles['bar-btn-text']}>分享</span>
           </div>
         </div>
         {/* <ElButton onClick={RuntimeUtils.shareScreenVideo}>屏幕共享</ElButton> */}
-        <ElDialog width="510px"
-        destroy-on-close
-          append-to-body modelValue={state.shareVisiable} title="分享" before-close={() => { state.shareVisiable = false }}>
-            <Share onClose={()=>state.shareVisiable = false}/>
+        <ElDialog
+          width="510px"
+          destroy-on-close
+          append-to-body
+          modelValue={state.shareVisiable}
+          title="分享"
+          before-close={() => {
+            state.shareVisiable = false
+          }}
+        >
+          <Share onClose={() => (state.shareVisiable = false)} />
         </ElDialog>
       </div>
     )
-  }
+  },
 })

+ 1 - 0
src/components/live-broadcast/event.ts

@@ -16,6 +16,7 @@ export const LIVE_EVENT_MESSAGE = {
   'RC:ForcedOffline': 'ForcedOffline',
   'RC:LookerLoginOut': 'LookerLoginOut',
   'RC:Chatroom:downSeat': 'DownSeat',
+  'RC:Chatroom:MemberCountUp': 'MemberCount',
 }
 
 export default mitt()

+ 1 - 0
src/components/live-broadcast/header.tsx

@@ -37,6 +37,7 @@ export default defineComponent({
         //   }
         // })
         await RuntimeUtils.closeLive()
+        await RuntimeUtils.sendMessage({}, 'PauseLive')
         ElMessage.success('关闭成功')
       } catch (error) {
       }

+ 16 - 0
src/components/live-broadcast/images/microphone.svg

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>麦克风(启动中)</title>
+    <g id="后台直播界面" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="图标页面状态" transform="translate(-67.000000, -244.000000)">
+            <g id="编组-25备份-3" transform="translate(56.000000, 239.000000)">
+                <g id="麦克风(启动中)" transform="translate(11.000000, 5.000000)">
+                    <rect id="矩形" x="0" y="0" width="22" height="22"></rect>
+                    <path d="M19,10.85 C19,15.102 15.786,18.626 11.632,19.177 L11.632,19.88 C11.632,20.496 11.122,21 10.5,21 C9.877,21 9.367,20.496 9.367,19.88 L9.367,19.177 C5.213,18.626 2,15.105 2,10.85 C2,10.334 2.423,9.915 2.945,9.915 C3.467,9.915 3.891,10.334 3.891,10.85 C3.891,14.455 6.856,17.386 10.502,17.386 C14.148,17.386 17.113,14.455 17.113,10.85 C17.113,10.334 17.537,9.915 18.059,9.915 C18.581,9.915 19.002,10.334 19,10.85 Z" id="路径" fill="#FFFFFF"></path>
+                    <path d="M7,7.83993935 L14,7.83993935 L14,10.5000027 C14,12.4329993 12.4329966,14.0000027 10.5,14.0000027 C8.56700338,14.0000027 7,12.4329993 7,10.5000027 L7,7.83993935 L7,7.83993935 Z" id="microphone-content-svg" fill="#2DC7AA"></path>
+                    <path d="M10.554,1 C13.2296842,1.00194737 15.4123767,3.12944737 15.5281717,5.79751675 L15.533,6.015 L15.533,10.339 C15.529,13.108 13.302,15.352 10.554,15.354 C7.87928947,15.3510789 5.6985187,13.2235533 5.58374736,10.5564064 L5.579,10.339 L5.579,6.015 C5.581,3.247 7.807,1.004 10.554,1 Z M10.554,2.999 L10.3822831,3.00534888 C8.87978198,3.09547648 7.67258605,4.31266034 7.58418786,5.83771274 L7.57899948,6.015 L7.57899948,10.3375549 C7.58020489,12.0058476 8.91580953,13.3522097 10.5525444,13.3540005 C12.135134,13.3528487 13.4347128,12.1006072 13.5276824,10.5163484 L13.5330012,10.339 L13.5330012,6.01716685 C13.5312573,4.40761068 12.2878451,3.09780557 10.7272071,3.00522987 L10.554,2.999 Z" id="路径" fill="#FFFFFF" fill-rule="nonzero"></path>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 234 - 113
src/components/live-broadcast/index.tsx

@@ -1,179 +1,300 @@
-import { defineComponent, ref } from 'vue'
-import * as RTC from '@rongcloud/plugin-rtc'
-import Header from './header'
-import ActionBar, { state as ActionBarRuntime} from './action-bar'
-import VideoStatus from './video-status'
-import { state } from '/src/state'
-import event, { LIVE_EVENT_MESSAGE } from './event'
-import runtime, * as RuntimeUtils from './runtime'
-import Chronography from './chronography'
-// import { removeMedia } from './helpers'
-import styles from './index.module.less'
+import { defineComponent, ref } from "vue";
+import * as RTC from "@rongcloud/plugin-rtc";
+import Header from "./header";
+import ActionBar, { state as ActionBarRuntime } from "./action-bar";
+import VideoStatus from "./video-status";
+import { state } from "/src/state";
+import event, { LIVE_EVENT_MESSAGE } from "./event";
+import runtime, * as RuntimeUtils from "./runtime";
+import Chronography from "./chronography";
+import runtimeModel, * as RuntimeModelUtils from "/src/components/live-message/model/runtime";
+import { requireMedia } from "./helpers";
+import styles from "./index.module.less";
+import request from "/src/helpers/request";
+import dayjs from "dayjs";
 
-const videoRef = ref<HTMLVideoElement | null>(null)
+const videoRef = ref<HTMLVideoElement | null>(null);
 
-let microphoneAudioTrack: RTC.RCLocalTrack
-let cameraVideoTrack: RTC.RCLocalTrack
+let microphoneAudioTrack: RTC.RCLocalTrack;
+let cameraVideoTrack: RTC.RCLocalTrack;
 
 export default defineComponent({
-  name: 'LiveBroadcast',
+  name: "LiveBroadcast",
   data() {
     return {
-      headerStatus: false
-    }
+      headerStatus: false,
+      initAudioUsers: [],
+    };
   },
   computed: {
     isLive() {
-      console.log(runtime.videoStatus)
-      if(runtime.videoStatus === 'liveing') {
+      console.log(runtime.videoStatus);
+      if (runtime.videoStatus === "liveing") {
         setTimeout(() => {
-          this.headerStatus = true
+          this.headerStatus = true;
         }, 3000);
       } else {
-        this.headerStatus = false
+        this.headerStatus = false;
       }
-      return runtime.videoStatus === 'liveing'
-    }
+      return runtime.videoStatus === "liveing";
+    },
   },
   async mounted() {
-    this.initializeRoom()
-    RuntimeUtils.loopSyncLike()
-    event.on(LIVE_EVENT_MESSAGE['RC:Chatroom:Like'], this.onLikeMessage)
-    window.onbeforeunload = this.beforeunload
+    this.initializeRoom();
+    RuntimeUtils.loopSyncLike();
+    event.on(LIVE_EVENT_MESSAGE["RC:Chatroom:Like"], this.onLikeMessage);
+    window.onbeforeunload = this.beforeunload;
+    // window.addEventListener('focus', this.visibilitychange)
+    // window.addEventListener('blur', this.visibilitychange)
   },
   beforeUnmount() {
-    event.off(LIVE_EVENT_MESSAGE['RC:Chatroom:Like'], this.onLikeMessage)
-    window.onbeforeunload = null
+    event.off(LIVE_EVENT_MESSAGE["RC:Chatroom:Like"], this.onLikeMessage);
+    window.onbeforeunload = null;
+    // window.removeEventListener('focus', this.visibilitychange)
+    // window.removeEventListener('blur', this.visibilitychange)
   },
   methods: {
+    visibilitychange(evt: FocusEvent) {
+      console.log(evt);
+      if (evt.type === "focus") {
+        document.exitPictureInPicture();
+        document.body.click();
+      }
+      if (evt.type === "blur") {
+        runtime.videoRef?.requestPictureInPicture();
+      }
+    },
     beforeunload() {
-      if (runtime.videoStatus === 'liveing') {
-        return '当前正在直播中是否确认关闭页面?'
+      if (runtime.videoStatus === "liveing") {
+        return "当前正在直播中是否确认关闭页面?";
       }
     },
     onLikeMessage(msg: any) {
-      runtime.likeCount += (msg.counts || 0)
+      runtime.likeCount += msg.counts || 0;
     },
     getDeviceByDeviceType(type: RuntimeUtils.TrackType) {
-      const videoDeviceId = localStorage.getItem(RuntimeUtils.VIDEO_DEVICE_ID)
-      const audioDeviceId = localStorage.getItem(RuntimeUtils.AUDIO_DEVICE_ID)
-      if (type === 'camera') {
+      const videoDeviceId = localStorage.getItem(RuntimeUtils.VIDEO_DEVICE_ID);
+      const audioDeviceId = localStorage.getItem(RuntimeUtils.AUDIO_DEVICE_ID);
+      const audioDeviceId2 = localStorage.getItem(
+        RuntimeUtils.AUDIO_DEVICE_ID2
+      );
+      if (type === "camera") {
         if (videoDeviceId) {
-          return runtime.cameras.find(camera => camera.deviceId === videoDeviceId) || runtime.cameras[0]
+          return (
+            runtime.cameras.find(
+              (camera) => camera.deviceId === videoDeviceId
+            ) || runtime.cameras[0]
+          );
         }
-        return runtime.cameras[0]
+        return runtime.cameras[0];
       }
       if (audioDeviceId) {
-        return runtime.microphones.find(microphone => microphone.deviceId === audioDeviceId) || runtime.microphones[0]
+        return (
+          runtime.microphones.find(
+            (microphone) => microphone.deviceId === audioDeviceId
+          ) || runtime.microphones[0]
+        );
       }
-      return runtime.microphones[0]
+      if (audioDeviceId2) {
+        return (
+          runtime.microphones.find(
+            (microphone) => microphone.deviceId === audioDeviceId2
+          ) || runtime.microphones[0]
+        );
+      }
+      return runtime.microphones[0];
+    },
+    async FetchUserDetails(userIds: string[]) {
+      try {
+        const res = await request.post(
+          "/api-web/imLiveBroadcastRoom/queryBaseUserInfo",
+          {
+            requestType: "json",
+            data: userIds,
+          }
+        );
+        return res.data;
+      } catch (error) {}
+      return [];
     },
-    async initializeRoom () {
-      if (!state.user) throw Error('请先登录')
+    async initializeRoom() {
+      if (!state.user) throw Error("请先登录");
+      let initAudioUsers: any[] = [];
       try {
-        runtime.likeCount = state.user?.likeNum || 0
-        runtime.lookCount = state.user?.lookNum || 0
-        const isLiveing = sessionStorage.getItem(RuntimeUtils.START_LIVE_STATUS) === 'liveing'
+        runtime.likeCount = state.user?.likeNum || 0;
+        runtime.lookCount = state.user?.lookNum || 0;
+        const isLiveing =
+          sessionStorage.getItem(RuntimeUtils.START_LIVE_STATUS) === "liveing";
         // IM连接
-        await RuntimeUtils.connectIM(state.user?.imToken)
-        runtime.videoRef = videoRef.value
+        await RuntimeUtils.connectIM(state.user?.imToken);
+        runtime.videoRef = videoRef.value;
         // 获取设备
-        await RuntimeUtils.getMicrophones()
-        await RuntimeUtils.getCameras()
+        await RuntimeUtils.getMicrophones();
+        await RuntimeUtils.getCameras();
         // 设置播放设备
-        RuntimeUtils.setSelectCamera(this.getDeviceByDeviceType('camera'))
-        RuntimeUtils.setSelectMicrophone(this.getDeviceByDeviceType('microphone'))
-        cameraVideoTrack = await RuntimeUtils.getTrack('camera')
-        runtime.videoRef && cameraVideoTrack.play(runtime.videoRef)
+        RuntimeUtils.setSelectCamera(this.getDeviceByDeviceType("camera"));
+        RuntimeUtils.setSelectMicrophone(
+          this.getDeviceByDeviceType("microphone")
+        );
+        // RuntimeUtils.setSelectMicrophone2(this.getDeviceByDeviceType('microphone2'))
+        cameraVideoTrack = await RuntimeUtils.getTrack("camera");
+        runtime.videoRef && cameraVideoTrack.play(runtime.videoRef);
         // await RuntimeUtils.setTrack([cameraVideoTrack], 'camera', isLiveing)
         // await RuntimeUtils.getTrack('microphone')
-        microphoneAudioTrack = await RuntimeUtils.getTrack('microphone')
+        microphoneAudioTrack = await RuntimeUtils.getTrack("microphone");
         // microphoneAudioTrack.play()
         // console.log(microphoneAudioTrack)
         // console.log(runtime.deviceStatus)
-        runtime.videoStatus = 'stream'
-        const join = await RuntimeUtils.joinRoom(state.user?.roomUid, RTC.RCLivingType.VIDEO, {
-          onMessageReceive(name, content) {
-            console.log(name, content)
-          },
-          onKickOff(byServer: boolean) {
-            console.log(byServer)
-          },
-          async onTrackPublish (tracks: RTC.RCRemoteTrack[]) {
-            const subscribeRes = await join?.room?.subscribe(tracks)
-            console.log(subscribeRes)
-            if (subscribeRes?.code && subscribeRes.code !== RTC.RCRTCCode.SUCCESS) {
-              console.log('资源订阅失败 ->', subscribeRes.code)
-            }
-          },
-          onTrackUnpublish(tracks: RTC.RCRemoteTrack[]) {
-            console.log(tracks)
-            event.emit(LIVE_EVENT_MESSAGE['RM:RTC:TrackUnpublish'], tracks)
-          },
-          onSwitchRole(userId: string, role: RTC.RCRTCLiveRole) {
-            event.emit(LIVE_EVENT_MESSAGE['RM:RTC:SwitchRole'], {
-              userId,
-              role,
-            })
-          },
-          onTrackReady (track: RTC.RCRemoteTrack) {
-            if (track.isAudioTrack()) {
-              // 音轨不需要传递播放控件
-              track.play()
-            }
-          },
-          onUserJoin (userIds: string[]) {
-            console.log('onUserJoin', userIds)
-          },
-          onUserLeave (userIds: string[]) {
-            event.emit(LIVE_EVENT_MESSAGE['RM:RTC:UserLeave'], userIds)
-            console.log('onUserLeave', userIds)
-          },
-        })
-        console.log(join, 'join')
+        runtime.videoStatus = "stream";
+        const _this = this;
+        const join = await RuntimeUtils.joinRoom(
+          state.user?.roomUid,
+          RTC.RCLivingType.VIDEO,
+          {
+            onMessageReceive(name, content) {
+              console.log(name, content);
+            },
+            onKickOff(byServer: boolean) {
+              console.log(byServer);
+            },
+            async onTrackPublish(tracks: RTC.RCRemoteTrack[]) {
+              const subscribeRes = await join?.room?.subscribe(tracks);
+              console.log(subscribeRes, "subscribeRes");
+              if (
+                subscribeRes?.code &&
+                subscribeRes.code !== RTC.RCRTCCode.SUCCESS
+              ) {
+                console.log("资源订阅失败 ->", subscribeRes.code);
+              }
+            },
+            onTrackUnpublish(tracks: RTC.RCRemoteTrack[]) {
+              event.emit(LIVE_EVENT_MESSAGE["RM:RTC:TrackUnpublish"], tracks);
+            },
+            onSwitchRole(userId: string, role: RTC.RCRTCLiveRole) {
+              event.emit(LIVE_EVENT_MESSAGE["RM:RTC:SwitchRole"], {
+                userId,
+                role,
+              });
+            },
+            async onTrackReady(track: RTC.RCRemoteTrack) {
+              if (track.isAudioTrack()) {
+                // 音轨不需要传递播放控件
+                track.play();
+                const userId = track.getUserId();
+
+                // 当流变化时,更新连麦用户列表
+                const joinUser = runtimeModel.joinList[userId];
+                console.log(joinUser, "joinUser");
+                if (!joinUser) {
+                  console.log(runtimeModel.lookList, "runtimeModel.lookList");
+                  const userInfo = runtimeModel.lookList[userId];
+                  const list = userInfo
+                    ? userInfo
+                    : await _this.FetchUserDetails([userId]);
+                  console.log(list, "list");
+                }
+                // if (runtimeModel.joinList || runtimeModel.lookList) {
+
+                // }
+                const trackUser = initAudioUsers.find(
+                  (user: any) => user.userId == Number(userId)
+                );
+                // console.log(trackUser, initAudioUsers, userId)
+                if (trackUser) {
+                  runtimeModel.joinList[userId] = {
+                    ...trackUser,
+                    id: userId,
+                    name: trackUser.userName,
+                    userRoomType: 4,
+                  };
+                  initAudioUsers = initAudioUsers.filter(
+                    (user: any) => user.userId != Number(userId)
+                  );
+                }
+              }
+            },
+            onUserJoin(userIds: string[]) {
+              console.log("onUserJoin", userIds);
+            },
+            onUserLeave(userIds: string[]) {
+              event.emit(LIVE_EVENT_MESSAGE["RM:RTC:UserLeave"], userIds);
+              console.log("onUserLeave", userIds);
+            },
+          }
+        );
+        // RuntimeUtils.sendMessage({}, 'Welcome')
+        const userIds: string[] | undefined = join?.room?.getRemoteUserIds();
+        // console.log('userIds', userIds)
+        if (userIds && userIds.length) {
+          const list = await this.FetchUserDetails(userIds);
+          initAudioUsers.push(...list);
+        }
+        const remoteTracks: RTC.RCRemoteTrack[] | undefined =
+          join?.room?.getRemoteTracks();
+        if (remoteTracks) {
+          await join?.room?.subscribe(remoteTracks);
+        }
         if (join.room && join.code === RTC.RCRTCCode.SUCCESS) {
-          runtime.joinedRoom = join.room
+          runtime.joinedRoom = join.room;
+          join.room.registerReportListener({
+            onStateReport(report) {
+              event.emit("onStateReport", report);
+              // console.log('onStateReport', report)
+            },
+          });
         }
         if (isLiveing) {
-          await RuntimeUtils.startLive(false)
-          runtime.videoStatus = 'liveing'
+          await RuntimeUtils.startLive(false);
+          runtime.videoStatus = "liveing";
         } else {
-          await RuntimeUtils.setTrack([cameraVideoTrack], 'camera', false)
-          await RuntimeUtils.setTrack([microphoneAudioTrack], 'microphone', false)
+          await RuntimeUtils.setTrack([cameraVideoTrack], "camera", false);
+          await RuntimeUtils.setTrack(
+            [microphoneAudioTrack],
+            "microphone",
+            false
+          );
         }
-        const volume =localStorage.getItem(RuntimeUtils.AUDIO_DEVICE_VOLUME)
+        const volume = localStorage.getItem(RuntimeUtils.AUDIO_DEVICE_VOLUME);
         if (volume) {
-          ActionBarRuntime.volume = parseInt(volume)
-          RuntimeUtils.setVolume(parseInt(volume))
+          ActionBarRuntime.volume = parseInt(volume);
+          RuntimeUtils.setVolume(parseInt(volume));
         }
 
         // 同步移动端观看人数
-        await RuntimeUtils.sendMessage({ count: state.user?.lookNum || 0 }, 'MemberCount')
+        await RuntimeUtils.sendMessage(
+          { count: state.user?.lookNum || 0 },
+          "MemberCount"
+        );
       } catch (error) {
-        runtime.videoStatus = 'error'
-        console.log(error)
+        runtime.videoStatus = "error";
+        console.log(error);
       }
     },
     closeLive() {
       // removeMedia(runtime.mediaStreams, runtime.mediaStreamTrack)
-      runtime.videoStatus = 'stream'
-    }
+      runtime.videoStatus = "stream";
+    },
   },
   render() {
     return (
       <div class={styles.main}>
         {this.isLive ? null : null}
         <div class={[styles.headerSection]}>
-          <Header class={[styles.headerContent, this.headerStatus ? styles["header-top"] : null]} />
+          <Header
+            class={[
+              styles.headerContent,
+              this.headerStatus ? styles["header-top"] : null,
+            ]}
+          />
         </div>
         <div class={styles.video}>
           <video ref={videoRef}></video>
-          {!runtime.screenShareStatus ? <VideoStatus/> : null}
-          {runtime.videoStatus === 'liveing' ? <Chronography/> : null}
+          {!runtime.screenShareStatus ? <VideoStatus /> : null}
+          {runtime.videoStatus === "liveing" ? <Chronography /> : null}
         </div>
-        <ActionBar/>
+        <ActionBar />
         {/* <div>video: {runtime.videoStatus}, imStatus: {runtime.imConnectStatus}</div> */}
       </div>
-    )
-  }
-})
+    );
+  },
+});

ファイルの差分が大きいため隠しています
+ 489 - 264
src/components/live-broadcast/runtime.ts


+ 147 - 111
src/components/live-message/model/index.module.less

@@ -1,111 +1,147 @@
-.itemContent {
-  padding: 16px 16px 5px;
-  display: flex;
-  color: var(--live-color);
-  & > img {
-    width: 28px;
-    height: 28px;
-    margin-right: 8px;
-  }
-}
-
-
-.itemInfo {
-  width: 100%;
-}
-
-.itemName {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  padding-top: 3px;
-  .userName {
-    font-size: 15px;
-    line-height: 22px;
-    // display: flex;
-    align-items: center;
-    font-weight: 500;
-  }
-  .name-style {
-    line-height: 24px;
-    color: var(--live-text-color);
-  }
-  .rightTime {
-    font-size: 12px;
-  }
-  :global {
-    .el-tag--default {
-      border-radius: 20px;
-      margin-right: 8px;
-      height: 20px;
-
-      // padding: 0 12px;
-      // line-height: 20px;
-      // color: var(--live-color);
-      // background: var(--message-color);
-      // border-color: var(--message-color);
-    }
-  }
-}
-.itemText {
-  font-size: 15px;
-  line-height: 20px;
-  &.active {
-    color: var(--live-text-color);
-  }
-}
-
-.joinText {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  .join {
-    display: flex;
-    align-items: center;
-  }
-}
-
-.btn {
-  font-size: 13px;
-  line-height: 20px;
-  background-color: var(--live-light-color);
-  border-color: var(--live-light-color);
-  text-align: center;
-  padding: 3px 15px 1px;
-  border-radius: 2px;
-  cursor: pointer;
-  color: var(--live-color);
-  text-decoration: none;
-  &.downBtn {
-    background-color: transparent;
-    border-color: var(--live-light-color);
-    color: var(--live-light-color);
-  }
-}
-
-.loadingStyle {
-  position: absolute;
-  left: 0;
-  right: 0;
-  top: 0;
-  bottom: 0;
-}
-
-
-.slide-top-enter-active {
-  opacity: 0;
-  animation-name: error-num;
-  animation-duration: 0.5s;
-  animation-fill-mode: forwards;
-  animation-delay: 0.1s;
-}
-@keyframes error-num {
-	0% {
-		transform: translateY(30px);
-		opacity: 0;
-	}
-	100% {
-		transform: translateY(0);
-		opacity: 1;
-	}
-}
+.itemContent {
+  padding: 16px 16px 5px;
+  display: flex;
+  color: var(--live-color);
+  & > img {
+    width: 28px;
+    height: 28px;
+    margin-right: 8px;
+  }
+}
+
+.itemInfo {
+  width: 100%;
+}
+
+.itemName {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+  padding-top: 3px;
+  .userName {
+    font-size: 15px;
+    line-height: 22px;
+    // display: flex;
+    align-items: center;
+    font-weight: 500;
+    padding-right: 15px;
+    text-align: justify;
+  }
+  .name-style {
+    line-height: 24px;
+    color: var(--live-text-color);
+  }
+  .rightTime {
+    font-size: 12px;
+    line-height: 22px;
+  }
+  :global {
+    .el-tag--default {
+      border-radius: 20px;
+      margin-right: 8px;
+      height: 20px;
+
+      // padding: 0 12px;
+      // line-height: 20px;
+      // color: var(--live-color);
+      // background: var(--message-color);
+      // border-color: var(--message-color);
+    }
+  }
+}
+.itemText {
+  font-size: 15px;
+  line-height: 20px;
+  word-break: break-all;
+  &.active {
+    color: var(--live-text-color);
+  }
+}
+
+.joinText {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  .join {
+    display: flex;
+    align-items: center;
+  }
+}
+
+.btn {
+  font-size: 13px;
+  line-height: 20px;
+  background-color: var(--live-light-color);
+  border-color: var(--live-light-color);
+  text-align: center;
+  padding: 3px 15px 1px;
+  border-radius: 2px;
+  cursor: pointer;
+  color: var(--live-color);
+  text-decoration: none;
+  &.downBtn {
+    background-color: transparent;
+    border-color: var(--live-light-color);
+    color: var(--live-light-color);
+  }
+}
+
+.loadingStyle {
+  position: absolute;
+  left: 0;
+  right: 0;
+  top: 0;
+  bottom: 0;
+}
+
+.slide-top-enter-active {
+  opacity: 0;
+  animation-name: error-num;
+  animation-duration: 0.5s;
+  animation-fill-mode: forwards;
+  animation-delay: 0.1s;
+}
+@keyframes error-num {
+  0% {
+    transform: translateY(30px);
+    opacity: 0;
+  }
+  100% {
+    transform: translateY(0);
+    opacity: 1;
+  }
+}
+
+.refresh {
+  position: absolute;
+  bottom: 10px;
+  right: 15px;
+  width: 40px;
+  height: 40px;
+  background: #fff;
+  display: flex;
+  border-radius: 50%;
+  align-items: center;
+  justify-content: center;
+}
+
+.refresh-animation {
+  animation: animation 1s linear infinite;
+}
+.refreshStart {
+  animation-play-state: running;
+}
+.refreshStop {
+  animation-play-state: paused;
+}
+@keyframes animation {
+  0% {
+    transform: rotate(0deg);
+  }
+  50% {
+    transform: rotate(180deg);
+  }
+  100% {
+    transform: rotate(360deg);
+  }
+}

+ 250 - 109
src/components/live-message/model/join-model.tsx

@@ -1,12 +1,15 @@
 import { defineComponent } from "vue";
-import styles from './index.module.less'
-import { ElButton } from 'element-plus'
-import event, { LIVE_EVENT_MESSAGE } from '/src/components/live-broadcast/event';
-import { state } from '/src/state'
-import dayjs from 'dayjs';
+import styles from "./index.module.less";
+import { ElButton } from "element-plus";
+import event, {
+  LIVE_EVENT_MESSAGE,
+} from "/src/components/live-broadcast/event";
+import { state } from "/src/state";
+import dayjs from "dayjs";
 import Empty from "/src/components/empty";
-import runtime, * as RuntimeUtils from '/src/components/live-broadcast/runtime'
-import runtimeModel, * as RuntimeModelUtils from '/src/components/live-message/model/runtime'
+import runtime, * as RuntimeUtils from "/src/components/live-broadcast/runtime";
+import runtimeModel, * as RuntimeModelUtils from "/src/components/live-message/model/runtime";
+import request from "/src/helpers/request";
 
 export default defineComponent({
   data() {
@@ -15,23 +18,24 @@ export default defineComponent({
       loadingJoin: false, // 连麦列表状态
       upStatus: false,
       downStatus: false,
-    }
+      refreshStatus: false, // 刷新状态
+    };
   },
   computed: {
     count() {
-      let count = 0
+      let count = 0;
       for (const key in runtimeModel.joinList) {
         if (Object.prototype.hasOwnProperty.call(runtimeModel.joinList, key)) {
           const item = runtimeModel.joinList[key];
           if (item.userRoomType === 4) {
-            count += 1
+            count += 1;
           }
           if (count > 3) {
-            break
+            break;
           }
         }
       }
-      return count
+      return count;
     },
   },
   mounted() {
@@ -40,100 +44,112 @@ export default defineComponent({
     // 2 老师邀请
     // 3 学生申请
     // 4 连麦中
-    event.on(LIVE_EVENT_MESSAGE['RC:Chatroom:SeatApply'], this.onSeatApply);
-    event.on(LIVE_EVENT_MESSAGE['RC:Chatroom:SeatResponse'], this.onSeatApply);
-    event.on(LIVE_EVENT_MESSAGE['RM:RTC:UserLeave'], this.onSeatApply);
-    event.on(LIVE_EVENT_MESSAGE['RC:Chatroom:downSeat'], this.onDownSeat);
-    event.on(LIVE_EVENT_MESSAGE['RM:RTC:SwitchRole'], this.onSwitchRole);
-    event.on(LIVE_EVENT_MESSAGE['RC:Chatroom:Leave'], this.onLeave); // 移动端接收的消息
-    event.on(LIVE_EVENT_MESSAGE['RC:LookerLoginOut'], this.onLeave); // 后台接收的消息
+    event.on(LIVE_EVENT_MESSAGE["RC:Chatroom:SeatApply"], this.onSeatApply);
+    event.on(LIVE_EVENT_MESSAGE["RC:Chatroom:SeatResponse"], this.onSeatApply);
+    event.on(LIVE_EVENT_MESSAGE["RM:RTC:UserLeave"], this.onSeatApply);
+    event.on(LIVE_EVENT_MESSAGE["RC:Chatroom:downSeat"], this.onDownSeat);
+    event.on(LIVE_EVENT_MESSAGE["RM:RTC:SwitchRole"], this.onSwitchRole);
+    // event.on(LIVE_EVENT_MESSAGE["RC:Chatroom:Leave"], this.onLeave); // 移动端接收的消息
+    event.on(LIVE_EVENT_MESSAGE["RC:LookerLoginOut"], this.onLeave); // 后台接收的消息
   },
   unmounted() {
-    event.off(LIVE_EVENT_MESSAGE['RC:Chatroom:SeatApply'], this.onSeatApply);
-    event.off(LIVE_EVENT_MESSAGE['RC:Chatroom:SeatResponse'], this.onSeatApply);
-    event.off(LIVE_EVENT_MESSAGE['RM:RTC:UserLeave'], this.onSeatApply);
-    event.off(LIVE_EVENT_MESSAGE['RM:RTC:SwitchRole'], this.onSwitchRole);
-    event.off(LIVE_EVENT_MESSAGE['RC:Chatroom:Leave'], this.onLeave); // 移动端接收的消息
-    event.off(LIVE_EVENT_MESSAGE['RC:LookerLoginOut'], this.onLeave); // 后台接收的消息
+    event.off(LIVE_EVENT_MESSAGE["RC:Chatroom:SeatApply"], this.onSeatApply);
+    event.off(LIVE_EVENT_MESSAGE["RC:Chatroom:SeatResponse"], this.onSeatApply);
+    event.off(LIVE_EVENT_MESSAGE["RM:RTC:UserLeave"], this.onSeatApply);
+    event.off(LIVE_EVENT_MESSAGE["RM:RTC:SwitchRole"], this.onSwitchRole);
+    // event.off(LIVE_EVENT_MESSAGE["RC:Chatroom:Leave"], this.onLeave); // 移动端接收的消息
+    event.off(LIVE_EVENT_MESSAGE["RC:LookerLoginOut"], this.onLeave); // 后台接收的消息
   },
   methods: {
     async onLeave(value: any) {
       // 学生离开时处理
-      const userId = value.userId || value.fromUserId
-      if(runtimeModel.joinList[userId]) {
-        RuntimeModelUtils.removeJoin(userId)
+      const userId = value.userId || value.fromUserId;
+      if (runtimeModel.joinList[userId]) {
+        RuntimeModelUtils.removeJoin(userId);
       }
-      if(runtimeModel.lookList[userId]) {
-        RuntimeModelUtils.removeLook(userId)
+      if (runtimeModel.lookList[userId]) {
+        RuntimeModelUtils.removeLook(userId);
 
         // 判断是否有学生
-        runtime.lookCount = runtime.lookCount - 1 >= 0 ? runtime.lookCount - 1 : 0
+        // runtime.lookCount =
+        //   runtime.lookCount - 1 >= 0 ? runtime.lookCount - 1 : 0;
       }
       // 同步移动端观看人数
-      await RuntimeUtils.sendMessage({ count: runtime.lookCount }, 'MemberCount')
+      await RuntimeUtils.sendMessage(
+        { count: runtime.lookCount },
+        "MemberCount"
+      );
+
+      // this.onRefresh();
     },
     onSeatApply(evt: any) {
       // console.log(evt, 'onSeatApply joinModel')
       if (Array.isArray(evt)) {
         for (const id of evt) {
-          console.log('onSeatApply', id)
-          RuntimeModelUtils.removeJoin(id)
+          console.log("onSeatApply", id);
+          RuntimeModelUtils.removeJoin(id);
         }
-        return
+        return;
       }
-      const response = evt.$EventMessage.messageType === 'RC:Chatroom:SeatResponse'
-      const userRoomType = response ? 4 : 3
+      const response =
+        evt.$EventMessage.messageType === "RC:Chatroom:SeatResponse";
+      const userRoomType = response ? 4 : 3;
       // if(evt.$EventMessage.messageType === 'RC:Chatroom:SeatResponse')
-      const sendTime = dayjs(evt.$EventMessage.sentTime || new Date()).format('HH:mm:ss')
+      const sendTime = dayjs(evt.$EventMessage.sentTime || new Date()).format(
+        "HH:mm:ss"
+      );
       let tempObj = {
         name: evt.audienceName,
         id: String(evt.audienceId),
         system: 1,
         isSelf: false,
-        content: '',
-        sendTime
-      }
+        content: "",
+        sendTime,
+      };
       // 申请连麦
       if (evt.type === 3) {
-        console.log(evt, '申请连麦')
+        console.log(evt, "申请连麦");
         const params = {
           name: evt.audienceName,
           id: evt.audienceId,
           userRoomType: userRoomType,
           type: evt.type,
-        }
-        RuntimeModelUtils.addJoin(evt.audienceId, params)
-        RuntimeModelUtils.addLook(evt.audienceId, params)
+        };
+        RuntimeModelUtils.addJoin(evt.audienceId, params);
+        RuntimeModelUtils.addLook(evt.audienceId, params);
 
-        tempObj.content = response ? '同意了连麦申请' : '发起了连麦申请'
+        tempObj.content = response ? "同意了连麦申请" : "发起了连麦申请";
         RuntimeModelUtils.addMessage(tempObj);
-        event.emit('MESSAGE:Change')
+        event.emit("MESSAGE:Change");
       }
       // 取消连麦
       if (evt.type === 4) {
-        console.log(evt, '取消连麦')
+        console.log(evt, "取消连麦");
         if (runtimeModel.joinList[evt.audienceId]) {
-          RuntimeModelUtils.removeJoin(evt.audienceId)
+          RuntimeModelUtils.removeJoin(evt.audienceId);
         }
         if (runtimeModel.lookList[evt.audienceId]) {
-          let userLook = runtimeModel.lookList[evt.audienceId]
-          userLook.userRoomType = 1
-          RuntimeModelUtils.addLook(evt.audienceId, userLook)
+          let userLook = runtimeModel.lookList[evt.audienceId];
+          userLook.userRoomType = 1;
+          RuntimeModelUtils.addLook(evt.audienceId, userLook);
         }
 
         //
-        tempObj.content = response ? '拒绝了连麦申请' : '取消了连麦申请'
+        tempObj.content = response ? "拒绝了连麦申请" : "取消了连麦申请";
         RuntimeModelUtils.addMessage(tempObj);
-        event.emit('MESSAGE:Change')
-      }
+        event.emit("MESSAGE:Change");
 
+        setTimeout(() => {
+          this.onRefresh();
+        }, 500);
+      }
     },
     agree(item: any) {
       if (this.count > 3 || this.upStatus) {
-        console.log(true, 2323)
-        return
+        console.log(true, 2323);
+        return;
       }
-      this.upStatus = true
+      this.upStatus = true;
       const data = {
         ...item,
         audienceName: item.name,
@@ -142,20 +158,19 @@ export default defineComponent({
         teacherName: state.user?.speakerName,
         userRoomType: 4,
         type: 1,
-      }
-      RuntimeModelUtils.addJoin(item.id, data)
-      RuntimeModelUtils.addLook(item.id, data)
-      RuntimeUtils.sendMessage(data, 'SeatResponse')
-
+      };
+      RuntimeModelUtils.addJoin(item.id, data);
+      RuntimeModelUtils.addLook(item.id, data);
+      RuntimeUtils.sendMessage(data, "SeatResponse");
       setTimeout(() => {
-        this.upStatus = false
+        this.upStatus = false;
       }, 300);
     },
     refuse(item: any) {
-      if(this.downStatus) {
-        return
+      if (this.downStatus) {
+        return;
       }
-      this.downStatus = true
+      this.downStatus = true;
       const data = {
         ...item,
         audienceName: item.name,
@@ -164,75 +179,201 @@ export default defineComponent({
         teacherName: state.user?.speakerName,
         userRoomType: 4,
         type: 5,
-      }
-      RuntimeModelUtils.addJoin(item.id, data)
-      RuntimeUtils.sendMessage(data, 'SeatApply')
+      };
+      RuntimeModelUtils.addJoin(item.id, data);
+      RuntimeUtils.sendMessage(data, "SeatApply");
       setTimeout(() => {
-        this.downStatus = false
-      }, 300);
+        this.downStatus = false;
+      }, 50000);
     },
     onDownSeat(evt: any) {
-      console.log(evt, 'onDownSeat')
+      this.downStatus = false;
+      console.log(evt, "onDownSeat");
       if (runtimeModel.joinList[evt.audienceId]) {
-        const users = runtimeModel.joinList[evt.audienceId]
-        const sendTime = dayjs(new Date()).format('HH:mm:ss')
-        console.log(evt.$EventMessage.senderUserId, state.user?.speakerId, 'onDownSeat')
-        let message = users.type == 5 ? '被报下麦' : '取消了连麦申请'
+        const users = runtimeModel.joinList[evt.audienceId];
+        const sendTime = dayjs(new Date()).format("HH:mm:ss");
+        console.log(
+          evt.$EventMessage.senderUserId,
+          state.user?.speakerId,
+          "onDownSeat"
+        );
+        let message = users.type == 5 ? "被抱下麦" : "取消了连麦申请";
         let tempObj = {
           name: evt.audienceName,
           id: evt.audienceId,
           system: 1,
           isSelf: false,
           content: message,
-          sendTime
-        }
+          sendTime,
+        };
         RuntimeModelUtils.addMessage(tempObj);
-        event.emit('MESSAGE:Change')
+        event.emit("MESSAGE:Change");
 
-        RuntimeModelUtils.removeJoin(evt.audienceId)
+        RuntimeModelUtils.removeJoin(evt.audienceId);
       }
     },
     onSwitchRole(evt: any) {
-      console.log(evt, 'onSwitchRole')
+      console.log(evt, "onSwitchRole");
       if (runtimeModel.lookList[evt.userId] && evt.role === 2) {
-        let userLook = runtimeModel.lookList[evt.userId]
-        userLook.userRoomType = 1
-        RuntimeModelUtils.addLook(evt.userId, userLook)
+        let userLook = runtimeModel.lookList[evt.userId];
+        userLook.userRoomType = 1;
+        RuntimeModelUtils.addLook(evt.userId, userLook);
       }
-    }
+    },
+    async onRefresh() {
+      // 刷新连麦列表
+      console.log(runtime.joinedRoom?.getRemoteUserIds(), "remoteUserIds");
+      this.refreshStatus = true;
+      const userIds: string[] | undefined =
+        runtime.joinedRoom?.getRemoteUserIds();
+      const noJoinUserIds = userIds?.filter((item: string) => {
+        return !runtimeModel.joinList[item];
+      });
+      const noLookList = noJoinUserIds?.filter((item: string) => {
+        return !runtimeModel.lookList[item];
+      });
+      const inLookList = noJoinUserIds?.filter((item: string) => {
+        return runtimeModel.lookList[item];
+      });
+      const joinUserList = inLookList?.map((item: any) => {
+        const user = runtimeModel.lookList[item];
+        user.userRoomType = 4;
+        return user;
+      });
+      console.log(joinUserList);
+      if (noLookList && noLookList?.length > 0) {
+        const fetchList = await this.FetchUserDetails(noLookList);
+        console.log(fetchList, "fetchList");
+        fetchList.forEach((item: any) => {
+          joinUserList?.push({
+            name: item.userName,
+            id: item.userId,
+            sendTime: dayjs(new Date()).format("HH:mm:ss"),
+            userRoomType: 4,
+            type: 1,
+          });
+        });
+      }
+      // 判断是否在连麦列表中
+      if (joinUserList && joinUserList?.length > 0) {
+        joinUserList?.forEach((item: any) => {
+          RuntimeModelUtils.addJoin(item.id, item);
+        });
+      }
+      setTimeout(() => {
+        this.refreshStatus = false;
+      }, 300);
+    },
+    async FetchUserDetails(userIds: string[]) {
+      try {
+        const res = await request.post(
+          "/api-web/imLiveBroadcastRoom/queryBaseUserInfo",
+          {
+            requestType: "json",
+            data: userIds,
+          }
+        );
+        return res.data;
+      } catch (error) {}
+      return [];
+    },
   },
   render() {
-    const list = Object.values(runtimeModel.joinList)
+    const list = Object.values(runtimeModel.joinList);
     return (
-      <div style={{ minHeight: '100%', position: 'relative' }}>
-        {list.length > 0 ? list.map((item: any) => (
-          <div class={styles.itemContent}>
-            <div class={styles.itemInfo}>
-              <div class={styles.itemName}>
-                <p class={styles.userName}>
-                  <span class={styles['name-style']}>{item.name}</span>
-                  {item.userRoomType !== 4 ? <span style={{ paddingLeft: '10px' }}>申请连麦</span> : <span style={{ paddingLeft: '10px', color: 'var(--live-text-color)' }}>正在连麦</span>}
-                </p>
-                {item.userRoomType !== 4 ? (
+      <div style={{ minHeight: "100%", position: "relative" }}>
+        {list.length > 0 ? (
+          list.map((item: any) => (
+            <div class={styles.itemContent}>
+              <div class={styles.itemInfo}>
+                <div class={styles.itemName}>
+                  <p class={styles.userName}>
+                    <span class={styles["name-style"]}>{item.name}</span>
+                    {item.userRoomType !== 4 ? (
+                      <span style={{ paddingLeft: "10px" }}>申请连麦</span>
+                    ) : (
+                      <span
+                        style={{
+                          paddingLeft: "10px",
+                          color: "var(--live-text-color)",
+                        }}
+                      >
+                        正在连麦
+                      </span>
+                    )}
+                  </p>
+                  {item.userRoomType !== 4 ? (
                     <div class={styles.joinText}>
-                      <div class={styles.join}>
-                        {/* 申请连麦 */}
-                      </div>
-                      <ElButton size="small" type="primary" disabled={this.count > 3} class={styles.btn} onClick={() => this.agree(item)}>上麦</ElButton>
+                      <div class={styles.join}>{/* 申请连麦 */}</div>
+                      <ElButton
+                        size="small"
+                        type="primary"
+                        disabled={this.count > 3}
+                        class={styles.btn}
+                        onClick={() => this.agree(item)}
+                      >
+                        上麦
+                      </ElButton>
                     </div>
                   ) : (
                     <div class={styles.joinText}>
-                      <div class={styles.join}>
-                        {/* 正在连麦 */}
-                      </div>
-                      <ElButton size="small" plain class={[styles.btn, styles.downBtn]} onClick={() => this.refuse(item)}>下麦</ElButton>
+                      <div class={styles.join}>{/* 正在连麦 */}</div>
+                      <ElButton
+                        loading={this.downStatus}
+                        size="small"
+                        plain
+                        class={[styles.btn, styles.downBtn]}
+                        onClick={() => this.refuse(item)}
+                      >
+                        下麦
+                      </ElButton>
                     </div>
                   )}
+                </div>
+              </div>
+            </div>
+          ))
+        ) : this.loadingJoin ? (
+          <div class={styles.loadingStyle}>
+            <div
+              class="el-loading-mask"
+              style="background-color: rgba(0, 0, 0, 0.8);"
+            >
+              <div class="el-loading-spinner">
+                <svg class="circular" viewBox="25 25 50 50">
+                  <circle
+                    class="path"
+                    cx="50"
+                    cy="50"
+                    r="20"
+                    fill="none"
+                  ></circle>
+                </svg>
               </div>
             </div>
           </div>
-        )) : this.loadingJoin ? <div class={styles.loadingStyle}><div class="el-loading-mask" style="background-color: rgba(0, 0, 0, 0.8);"><div class="el-loading-spinner"><svg class="circular" viewBox="25 25 50 50"><circle class="path" cx="50" cy="50" r="20" fill="none"></circle></svg></div></div></div> : <Empty style={{ paddingTop: '120px' }} text="暂无学员发起连麦!" icon="noData-no-join" />}
+        ) : (
+          <Empty
+            style={{ paddingTop: "120px" }}
+            text="暂无学员发起连麦!"
+            icon="noData-no-join"
+          />
+        )}
+        {/* <div
+          class={[
+            styles.refresh,
+            styles["refresh-animation"],
+            this.refreshStatus ? styles.refreshStart : styles.refreshStop,
+          ]}
+          onClick={this.onRefresh}
+        >
+          <SvgIcon
+            name="message-refresh"
+            color={"#01A79E"}
+            style={{ width: "20px", height: "20px" }}
+          />
+        </div> */}
       </div>
-    )
-  }
-})
+    );
+  },
+});

+ 137 - 58
src/components/live-message/model/look-model.tsx

@@ -1,12 +1,14 @@
 import { defineComponent } from "vue";
-import { ElButton } from 'element-plus'
-import runtime, * as RuntimeUtils from '/src/components/live-broadcast/runtime'
-import styles from './index.module.less'
-import event, { LIVE_EVENT_MESSAGE } from '/src/components/live-broadcast/event';
-import { state } from '/src/state'
-import dayjs from 'dayjs';
+import { ElButton } from "element-plus";
+import runtime, * as RuntimeUtils from "/src/components/live-broadcast/runtime";
+import styles from "./index.module.less";
+import event, {
+  LIVE_EVENT_MESSAGE,
+} from "/src/components/live-broadcast/event";
+import { state } from "/src/state";
+import dayjs from "dayjs";
 import Empty from "/src/components/empty";
-import runtimeModel, * as RuntimeModelUtils from '/src/components/live-message/model/runtime'
+import runtimeModel, * as RuntimeModelUtils from "/src/components/live-message/model/runtime";
 import request from "/src/helpers/request";
 
 export default defineComponent({
@@ -14,90 +16,117 @@ export default defineComponent({
     return {
       loadingLook: false, // 观看列表状态
       upStatus: false,
-      downStatus: false
-    }
+      downStatus: false,
+    };
   },
   computed: {
     count() {
-      let count = 0
+      let count = 0;
       for (const key in runtimeModel.lookList) {
         if (Object.prototype.hasOwnProperty.call(runtimeModel.lookList, key)) {
           const item = runtimeModel.lookList[key];
           if (item.userRoomType === 2 || item.userRoomType === 4) {
-            count += 1
+            count += 1;
           }
           if (count > 3) {
-            break
+            break;
           }
         }
       }
-      return count
+      return count;
     },
   },
   async mounted() {
-    await this._init()
-    this.loadingLook = true
+    await this._init();
+    this.loadingLook = true;
     event.on(LIVE_EVENT_MESSAGE["RC:Chatroom:Welcome"], this.onWelcome);
+    event.on(
+      LIVE_EVENT_MESSAGE["RC:Chatroom:MemberCountUp"],
+      this.onMemberCount
+    );
     setTimeout(() => {
       this.loadingLook = false;
-    })
+    });
+  },
+  beforeUnmount() {
+    event.off(LIVE_EVENT_MESSAGE["RC:Chatroom:Welcome"], this.onWelcome);
+    event.off(
+      LIVE_EVENT_MESSAGE["RC:Chatroom:MemberCountUp"],
+      this.onMemberCount
+    );
   },
   methods: {
     async _init() {
       try {
-        const roomUid = sessionStorage.getItem('roomUid')
-        const res = await request.get(`/api-web/imLiveBroadcastRoom/queryRoomUserInfo`, {
-          params: {
-            roomUid: roomUid,
+        const roomUid = sessionStorage.getItem("roomUid");
+        const res = await request.get(
+          `/api-web/imLiveBroadcastRoom/queryRoomUserInfo`,
+          {
+            params: {
+              roomUid: roomUid,
+            },
           }
-        })
-        const resList = res.data
+        );
+        const resList = res.data;
         resList.forEach((item: any) => {
           // 判断是已经,存在学生
-          if(!runtimeModel.lookList[item.userId]) {
+          if (!runtimeModel.lookList[item.userId]) {
             runtimeModel.lookList[item.userId] = {
               id: item.userId,
               name: item.userName,
               type: 3,
               userRoomType: 1,
-              time: dayjs().format('YYYY-MM-DD HH:mm:ss'),
-            }
+              time: dayjs().format("YYYY-MM-DD HH:mm:ss"),
+            };
           }
-        })
+        });
       } catch {
         //
       }
     },
+    /**
+     * 当后端发送人数消息时,更新人数
+     */
+    onMemberCount(evt: any) {
+      runtime.lookCount = evt.content.count || 0;
+      RuntimeUtils.sendMessage({ count: runtime.lookCount }, "MemberCount");
+    },
     async onWelcome(value: any) {
       // console.log(value)
       if (value && value.user) {
-        const sendTime = dayjs(value.$EventMessage.sentTime || new Date()).format('HH:mm:ss')
+        const sendTime = dayjs(
+          value.$EventMessage.sentTime || new Date()
+        ).format("HH:mm:ss");
         let tempObj = {
           name: value.user.name,
           id: value.user.id,
           sendTime,
           userRoomType: 1,
-          type: 3
-        }
+          type: 3,
+        };
         // 判断是否有同一个人
         let isExist = runtimeModel.lookList[tempObj.id] ? true : false;
         if (!isExist) {
           RuntimeModelUtils.addLook(tempObj.id, tempObj);
-          console.log('添加观看人员', tempObj)
-          runtime.lookCount += 1;
+          console.log("添加观看人员", tempObj);
+          // runtime.lookCount += 1;
         }
 
         // 同步移动端观看人数
-        await RuntimeUtils.sendMessage({ count: runtime.lookCount }, 'MemberCount')
+        // await RuntimeUtils.sendMessage({ count: runtime.lookCount }, 'MemberCount')
 
-        this.loadingLook = false
+        this.loadingLook = false;
       }
     },
     async onUpLook(item: any) {
       try {
-        console.log(this.count, runtimeModel.lookList, 'this.count, runtimeModel.lookList')
-        if(this.count > 3) {
-          return
+        console.log(
+          this.count,
+          runtimeModel.lookList,
+          "this.count, runtimeModel.lookList"
+        );
+        if (this.count > 3) {
+          return;
         }
         // RC:Chatroom:SeatResponse type teacherName teacherId audienceName audienceId
         // 操作类型 1 主讲人同意 2 主讲人拒绝 3 观众同意 4 观众拒绝
@@ -115,10 +144,10 @@ export default defineComponent({
           teacherId: String(state.user?.id),
           teacherName: state.user?.speakerName,
           userRoomType: 2,
-          type: 1
-        }
+          type: 1,
+        };
         item.userRoomType = 2;
-        await RuntimeUtils.sendMessage(data, 'SeatApply')
+        await RuntimeUtils.sendMessage(data, "SeatApply");
       } catch {
         //
       }
@@ -132,31 +161,81 @@ export default defineComponent({
           teacherId: String(state.user?.id),
           teacherName: state.user?.speakerName,
           type: 5,
-        }
-        RuntimeModelUtils.addJoin(item.id, data)
-        RuntimeUtils.sendMessage(data, 'SeatApply')
-      } catch {
-      }
+        };
+        RuntimeModelUtils.addJoin(item.id, data);
+        RuntimeUtils.sendMessage(data, "SeatApply");
+      } catch {}
     },
   },
   render() {
-    const list = Object.values(runtimeModel.lookList)
+    const list = Object.values(runtimeModel.lookList);
     return (
       <div>
-        {list.length > 0 ? list.map((item : any) => (
-          <div class={styles.itemContent}>
-            <div class={styles.itemInfo} >
-              <div class={styles.itemName}>
-                <p class={styles.userName}>
-                  <span class={styles['name-style']}>{item.name}</span>
-                </p>
-                {/* 1、最多三个人上麦;2、老师主动邀请;3、没有开始直播 */}
-                {item.userRoomType !== 4 ? <ElButton size="small" type="primary" disabled={this.count > 3 || item.userRoomType === 2 || runtime.videoStatus !== 'liveing'} class={styles.btn} onClick={() => this.onUpLook(item)}>上麦</ElButton> : <ElButton size="small" plain class={[styles.btn, styles.downBtn]} onClick={() => this.onDownLook(item)}>下麦</ElButton>}
+        {list.length > 0 ? (
+          list.map((item: any) => (
+            <div class={styles.itemContent}>
+              <div class={styles.itemInfo}>
+                <div class={styles.itemName}>
+                  <p class={styles.userName}>
+                    <span class={styles["name-style"]}>{item.name}</span>
+                  </p>
+                  {/* 1、最多三个人上麦;2、老师主动邀请;3、没有开始直播 */}
+                  {item.userRoomType !== 4 ? (
+                    <ElButton
+                      size="small"
+                      type="primary"
+                      disabled={
+                        this.count > 3 ||
+                        item.userRoomType === 2 ||
+                        item.userRoomType === 3 ||
+                        runtime.videoStatus !== "liveing"
+                      }
+                      class={styles.btn}
+                      onClick={() => this.onUpLook(item)}
+                    >
+                      上麦
+                    </ElButton>
+                  ) : (
+                    <ElButton
+                      size="small"
+                      plain
+                      class={[styles.btn, styles.downBtn]}
+                      onClick={() => this.onDownLook(item)}
+                    >
+                      下麦
+                    </ElButton>
+                  )}
+                </div>
+              </div>
+            </div>
+          ))
+        ) : this.loadingLook ? (
+          <div class={styles.loadingStyle}>
+            <div
+              class="el-loading-mask"
+              style="background-color: rgba(0, 0, 0, 0.8);"
+            >
+              <div class="el-loading-spinner">
+                <svg class="circular" viewBox="25 25 50 50">
+                  <circle
+                    class="path"
+                    cx="50"
+                    cy="50"
+                    r="20"
+                    fill="none"
+                  ></circle>
+                </svg>
               </div>
             </div>
           </div>
-        )) : this.loadingLook ? <div class={styles.loadingStyle} ><div class="el-loading-mask" style="background-color: rgba(0, 0, 0, 0.8);"><div class="el-loading-spinner"><svg class="circular" viewBox="25 25 50 50"><circle class="path" cx="50" cy="50" r="20" fill="none"></circle></svg></div></div></div> : <Empty style={{ paddingTop: '120px' }} text="暂无学员观看!" icon="noData-no-user" />}
+        ) : (
+          <Empty
+            style={{ paddingTop: "120px" }}
+            text="暂无学员观看!"
+            icon="noData-no-user"
+          />
+        )}
       </div>
-    )
-  }
-})
+    );
+  },
+});

+ 6 - 1
src/components/svg-icon/index.tsx

@@ -15,13 +15,18 @@ export default defineComponent({
       type: String,
       default: '#333',
     },
+    onClick: {
+      type: Function,
+      default: () => {},
+    }
   },
   render () {
     const {name, prefix, color,} = this
     const symbolId = `#${prefix}-${name}`
 
     return (
-      <svg {...this.$attrs} aria-hidden="true" style={{ color: color }}>
+      //@ts-ignore
+      <svg {...this.$attrs} onClick={this.onClick} aria-hidden="true" style={{ color: color }}>
         <use href={symbolId} fill={color} />
       </svg>
     )

+ 2 - 0
src/icons/message/refresh.svg

@@ -0,0 +1,2 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1653536814949" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2842" xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64"><defs><style type="text/css">@font-face { font-family: feedback-iconfont; src: url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.woff2?t=1630033759944") format("woff2"), url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.woff?t=1630033759944") format("woff"), url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.ttf?t=1630033759944") format("truetype"); }
+</style></defs><path d="M87.552 225.645714l59.318857 46.665143c76.434286-113.078857 228.790857-198.217143 364.982857-198.217143 165.668571 0 336.749714 122.075429 401.042286 264.045715-50.249143 18.870857-140.288 95.451429-140.288 95.451428L1023.634286 614.4V511.853714C1023.707429 229.668571 794.038857 0 511.853714 0a511.268571 511.268571 0 0 0-424.301714 225.645714z m31.158857 454.948572C168.96 661.430857 251.026286 590.262857 251.026286 590.262857L0 409.453714v102.4c0 282.185143 229.668571 511.853714 511.853714 511.853715a511.268571 511.268571 0 0 0 424.374857-225.645715l-58.587428-44.032c-76.434286 113.005714-229.449143 197.924571-365.787429 197.924572-165.668571 0-328.850286-129.462857-393.142857-271.36z" p-id="2843"></path></svg>

+ 101 - 27
src/pages/home/header/index.tsx

@@ -1,52 +1,88 @@
 import { defineComponent } from "vue";
-import { ElDropdownMenu, ElDropdown, ElDropdownItem, ElMessage } from "element-plus";
+import {
+  ElDropdownMenu,
+  ElDropdown,
+  ElDropdownItem,
+  ElMessage,
+} from "element-plus";
 import router from "/src/router";
-import styles from './index.module.less';
-import request from '/src/helpers/request';
+import styles from "./index.module.less";
+import request from "/src/helpers/request";
 import runtime, * as RuntimeUtils from "/src/components/live-broadcast/runtime";
 import { removeToken } from "/src/utils/auth";
-import { removeMedia } from '/src/components/live-broadcast/helpers'
-import { state } from '/src/state'
-import userLogo from '/src/assets/home/placehorder-icon.png'
+import { removeMedia } from "/src/components/live-broadcast/helpers";
+import WifiIcon from "./wifi.svg";
+import Wifi1Icon from "./wifi1.svg";
+import Wifi2Icon from "./wifi2.svg";
+import event from "/src/components/live-broadcast/event";
+import * as RTC from "@rongcloud/plugin-rtc";
+import { state } from "/src/state";
+import userLogo from "/src/assets/home/placehorder-icon.png";
 
 export default defineComponent({
+  data() {
+    return {
+      iceCandidatePair: null as RTC.IRCCandidatePairStat | null,
+    };
+  },
   methods: {
     async loginOut() {
       try {
-        await RuntimeUtils.leaveIMRoom()
+        await RuntimeUtils.leaveIMRoom();
         try {
-          await request.post('/api-auth/exit', { data: {} });
+          await request.post("/api-auth/exit", { data: {} });
         } catch {}
-        RuntimeUtils.closeDevice('camera')
-        RuntimeUtils.closeDevice('microphone')
-        state.user = null
-        runtime.syncLikeTimer && clearTimeout(runtime.syncLikeTimer)
-        ElMessage.success('退出成功');
+        RuntimeUtils.closeDevice("camera");
+        RuntimeUtils.closeDevice("microphone");
+        state.user = null;
+        runtime.syncLikeTimer && clearTimeout(runtime.syncLikeTimer);
+        ElMessage.success("退出成功");
         removeToken();
         (this as any).$router.push({
-          path: '/login',
+          path: "/login",
           query: {
-            ...this.$route.query
-          }
+            ...this.$route.query,
+          },
         });
-      } catch(e) {
+      } catch (e) {
         // TODO: handle error
       }
-    }
+    },
+    onStateReport(evt: any) {
+      this.iceCandidatePair = evt.iceCandidatePair;
+    },
+    getWifiICon(bit: number) {
+      if (bit >= 2000) {
+        return WifiIcon;
+      }
+      if (bit >= 1000) {
+        return Wifi1Icon;
+      }
+      return Wifi2Icon;
+    },
+  },
+  mounted() {
+    event.on("onStateReport", this.onStateReport);
+  },
+  beforeUnmount() {
+    event.off("onStateReport", this.onStateReport);
   },
   render() {
+    const iceCandidatePair = this.iceCandidatePair as RTC.IRCCandidatePairStat;
     return (
       <div class={styles.liveHeader}>
         <div class={styles.liveHeaderLeft}>
           <div class={styles.liveHeaderLeftIcon}>
-            <img class={styles.liveLogo} src={state.user?.tenantLogo} alt=""/>
+            <img class={styles.liveLogo} src={state.user?.tenantLogo} alt="" />
             {state.user?.tenantName}
           </div>
           <div class={styles.liveHeaderLeftText}>
             《{state.user?.roomTitle}》
           </div>
         </div>
-        <ElDropdown trigger={'hover'}
+        <ElDropdown
+          trigger={"hover"}
+          placement={"bottom-end"}
           // @ts-ignore
           vSlots={{
             dropdown: () => (
@@ -55,14 +91,52 @@ export default defineComponent({
                   <span>安全退出</span>
                 </ElDropdownItem>
               </ElDropdownMenu>
-            )
-          }}>
+            ),
+          }}
+        >
           <div class={styles.avatarWrapper}>
-            {state.user?.speakerPic ? <img class={styles.userAvatar} src={state.user?.speakerPic} /> : <img class={styles.userAvatar} src={userLogo} />}
-            <span>{ state.user?.speakerName }</span>
+            {runtime.videoStatus === "liveing" && iceCandidatePair ? (
+              <div
+                style={{
+                  background: `#5E626D`,
+                  borderRadius: "6px",
+                  width: "113px",
+                  height: "32px",
+                  lineHeight: "32px",
+                  marginRight: "10px",
+                  textAlign: "center",
+                  display: "flex",
+                  justifyContent: "center",
+                }}
+              >
+                <span
+                  style={{
+                    fontSize: "14px",
+                    color: "#fff",
+                    flex: 1,
+                  }}
+                >
+                  {iceCandidatePair.bitrateSend}kbps
+                </span>
+                <img
+                  style={{
+                    marginLeft: "5px",
+                    marginRight: "10px",
+                    width: "20px",
+                  }}
+                  src={this.getWifiICon(iceCandidatePair.bitrateSend)}
+                />
+              </div>
+            ) : null}
+            {state.user?.speakerPic ? (
+              <img class={styles.userAvatar} src={state.user?.speakerPic} />
+            ) : (
+              <img class={styles.userAvatar} src={userLogo} />
+            )}
+            <span>{state.user?.speakerName}</span>
           </div>
         </ElDropdown>
       </div>
-    )
-  }
-})
+    );
+  },
+});

+ 13 - 0
src/pages/home/header/wifi.svg

@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="20px" height="14px" viewBox="0 0 20 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>网络信号</title>
+    <g id="后台直播界面" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="图标页面状态" transform="translate(-697.000000, -264.000000)" fill="#00FFF0" fill-rule="nonzero">
+            <g id="编组-14" transform="translate(685.000000, 255.000000)">
+                <g id="网络信号" transform="translate(12.000000, 9.000000)">
+                    <path d="M0,4.0828061 C5.53263112,-1.36093537 14.5108046,-1.36093537 20,4.0828061 L18.1691823,5.89846349 C13.6594636,1.42393416 6.34053643,1.42393416 1.83081768,5.89846349 L0,4.0828061 Z M7.27549137,11.2991289 C8.76533826,9.82377262 11.2346617,9.82377262 12.7234227,11.2991289 L10,14 L7.27549137,11.2991289 L7.27549137,11.2991289 Z M3.65946357,7.66996789 C7.19187751,4.20988058 12.8515583,4.20988058 16.3405364,7.66996789 L14.5531545,9.48562528 C12.0375994,6.9950235 7.9634865,6.9950235 5.44793137,9.48562528 L3.65946357,7.66996789 Z" id="形状"></path>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 14 - 0
src/pages/home/header/wifi1.svg

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="20px" height="14px" viewBox="0 0 20 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>网络信号</title>
+    <g id="后台直播界面" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="图标页面状态" transform="translate(-818.000000, -264.000000)" fill-rule="nonzero">
+            <g id="编组-14备份-2" transform="translate(806.000000, 255.000000)">
+                <g id="网络信号" transform="translate(12.000000, 9.000000)">
+                    <path d="M7.27549137,11.2991289 C8.76533826,9.82377262 11.2346617,9.82377262 12.7234227,11.2991289 L10,14 L7.27549137,11.2991289 L7.27549137,11.2991289 Z M3.65946357,7.66996789 C7.19187751,4.20988058 12.8515583,4.20988058 16.3405364,7.66996789 L14.5531545,9.48562528 C12.0375994,6.9950235 7.9634865,6.9950235 5.44793137,9.48562528 L3.65946357,7.66996789 Z" id="形状" fill="#00FFF0"></path>
+                    <path d="M0,4.0828061 C5.53263112,-1.36093537 14.5108046,-1.36093537 20,4.0828061 L18.1691823,5.89846349 C13.6594636,1.42393416 6.34053643,1.42393416 1.83081768,5.89846349 L0,4.0828061 Z" id="路径" fill-opacity="0.5" fill="#FFFFFF"></path>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 15 - 0
src/pages/home/header/wifi2.svg

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="20px" height="14px" viewBox="0 0 20 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>网络信号</title>
+    <g id="后台直播界面" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="图标页面状态" transform="translate(-939.000000, -264.000000)" fill-rule="nonzero">
+            <g id="编组-14备份" transform="translate(927.000000, 255.000000)">
+                <g id="网络信号" transform="translate(12.000000, 9.000000)">
+                    <path d="M0,4.0828061 C5.53263112,-1.36093537 14.5108046,-1.36093537 20,4.0828061 L18.1691823,5.89846349 C13.6594636,1.42393416 6.34053643,1.42393416 1.83081768,5.89846349 L0,4.0828061 Z" id="路径" fill-opacity="0.5" fill="#FFFFFF"></path>
+                    <path d="M7.27549137,11.2991289 C8.76533826,9.82377262 11.2346617,9.82377262 12.7234227,11.2991289 L10,14 L7.27549137,11.2991289 L7.27549137,11.2991289 Z" id="路径" fill="#FF4E19"></path>
+                    <path d="M3.65946357,7.66996789 C7.19187751,4.20988058 12.8515583,4.20988058 16.3405364,7.66996789 L14.5531545,9.48562528 C12.0375994,6.9950235 7.9634865,6.9950235 5.44793137,9.48562528 L3.65946357,7.66996789 Z" id="路径" fill-opacity="0.5" fill="#FFFFFF"></path>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません