ソースを参照

feat: 整理代码

shuisheng 2 年 前
コミット
4ce9b76726
47 ファイル変更4103 行追加9315 行削除
  1. 5 1
      package.json
  2. 244 12
      pnpm-lock.yaml
  3. 25 21
      public/index.html
  4. 0 5208
      public/jsmpeg.min.js
  5. 11 3
      script/config/webpack.common.ts
  6. 12 0
      src/App.vue
  7. 3 3
      src/api/order.ts
  8. 10 0
      src/api/qqUser.ts
  9. 8 0
      src/api/user.ts
  10. 18 0
      src/components/Message/content/index.vue
  11. 21 0
      src/components/Message/index.vue
  12. 59 0
      src/components/VideoControls/index.vue
  13. 15 0
      src/constant.ts
  14. 61 0
      src/hooks/use-login.ts
  15. 405 0
      src/hooks/use-pull.ts
  16. 583 0
      src/hooks/use-push.ts
  17. 92 4
      src/interface.ts
  18. 19 0
      src/layout/footer/index.vue
  19. 174 117
      src/layout/head/index.vue
  20. 2 0
      src/layout/index.vue
  21. 6 0
      src/main.ts
  22. 11 0
      src/network/webSocket.ts
  23. 70 253
      src/old/srs-flv-pull/index.vue
  24. 418 0
      src/old/srs-webrtc-pull/index.vue
  25. 413 0
      src/old/srs-webrtc-push/index.vue
  26. 4 5
      src/old/webrtc-pull/index.vue
  27. 415 0
      src/old/webrtc-push/index.vue
  28. 20 43
      src/router/index.ts
  29. 16 0
      src/shims-vue.d.ts
  30. 5 0
      src/store/app/index.ts
  31. 52 9
      src/store/user/index.ts
  32. 3 0
      src/utils/cache.ts
  33. 1 0
      src/utils/cookie/index.ts
  34. 17 0
      src/utils/cookie/loginEnv.ts
  35. 2 22
      src/utils/index.ts
  36. 13 1
      src/utils/request.ts
  37. 0 22
      src/views/aliPay/index.vue
  38. 0 903
      src/views/bilibiliPush/index.vue
  39. 75 37
      src/views/home/index.vue
  40. 87 0
      src/views/oauth/index.vue
  41. 74 213
      src/views/pull/index.vue
  42. 415 0
      src/views/push/index.vue
  43. 218 0
      src/views/rank/index.vue
  44. 1 1
      src/views/sponsors/index.vue
  45. 0 741
      src/views/srs-webrtc-push/index.vue
  46. 0 854
      src/views/test1/index.vue
  47. 0 842
      src/views/webrtc-push/index.vue

+ 5 - 1
package.json

@@ -32,6 +32,7 @@
     }
   },
   "dependencies": {
+    "@vicons/ionicons5": "^0.12.0",
     "axios": "^1.2.1",
     "billd-deploy": "^1.0.16",
     "billd-html-webpack-plugin": "^1.0.1",
@@ -39,11 +40,14 @@
     "billd-utils": "^0.0.9",
     "browser-tool": "^1.0.5",
     "flv.js": "^1.6.2",
+    "js-cookie": "^3.0.5",
     "mediasoup-client": "^3.6.84",
     "msr": "^1.3.4",
-    "pinia": "^2.0.11",
+    "naive-ui": "^2.34.3",
+    "pinia": "^2.0.33",
     "qrcode": "^1.5.3",
     "socket.io-client": "^4.6.1",
+    "unplugin-vue-components": "^0.24.1",
     "vconsole": "^3.15.0",
     "vue": "^3.2.31",
     "vue-demi": "^0.13.11",

+ 244 - 12
pnpm-lock.yaml

@@ -9,6 +9,7 @@ specifiers:
   '@soda/friendly-errors-webpack-plugin': ^1.8.1
   '@types/node': ^18.11.9
   '@typescript-eslint/parser': ^5.8.1
+  '@vicons/ionicons5': ^0.12.0
   '@vue/compiler-sfc': ^3.2.31
   '@vue/eslint-config-prettier': ^7.0.0
   '@vue/eslint-config-typescript': ^10.0.0
@@ -40,12 +41,14 @@ specifiers:
   html-webpack-plugin: ^5.5.0
   html-webpack-tags-plugin: ^3.0.1
   husky: ^7.0.0
+  js-cookie: ^3.0.5
   lint-staged: ^12.1.4
   mediasoup-client: ^3.6.84
   mini-css-extract-plugin: ^2.6.0
   msr: ^1.3.4
+  naive-ui: ^2.34.3
   node-emoji: ^1.11.0
-  pinia: ^2.0.11
+  pinia: ^2.0.33
   portfinder: ^1.0.28
   postcss: ^8.4.8
   postcss-loader: ^6.2.1
@@ -62,6 +65,7 @@ specifiers:
   ts-loader: ^9.2.7
   ts-node: ^10.9.1
   typescript: ^4.6.2
+  unplugin-vue-components: ^0.24.1
   vconsole: ^3.15.0
   vue: ^3.2.31
   vue-demi: ^0.13.11
@@ -78,6 +82,7 @@ specifiers:
   windicss-webpack-plugin: ^1.7.7
 
 dependencies:
+  '@vicons/ionicons5': 0.12.0
   axios: 1.3.4
   billd-deploy: 1.0.16_imakxv3mh5kp5z5uouwrjmnj5q
   billd-html-webpack-plugin: 1.0.1_imakxv3mh5kp5z5uouwrjmnj5q
@@ -85,11 +90,14 @@ dependencies:
   billd-utils: 0.0.9_typescript@4.9.5
   browser-tool: 1.0.5
   flv.js: 1.6.2
+  js-cookie: 3.0.5
   mediasoup-client: 3.6.84
   msr: 1.3.4
+  naive-ui: 2.34.3_vue@3.2.47
   pinia: 2.0.33_hmuptsblhheur2tugfgucj7gc4
   qrcode: 1.5.3
   socket.io-client: 4.6.1
+  unplugin-vue-components: 0.24.1_vue@3.2.47
   vconsole: 3.15.0
   vue: 3.2.47
   vue-demi: 0.13.11_vue@3.2.47
@@ -167,7 +175,6 @@ packages:
 
   /@antfu/utils/0.7.2:
     resolution: {integrity: sha512-vy9fM3pIxZmX07dL+VX1aZe7ynZ+YyB0jY+jE6r3hOK6GNY2t6W8rzpFC4tgpbXUYABkFQwgJq2XYXlxbXAI0g==}
-    dev: true
 
   /@babel/code-frame/7.18.6:
     resolution: {integrity: sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==}
@@ -1465,6 +1472,22 @@ packages:
       '@jridgewell/trace-mapping': 0.3.9
     dev: true
 
+  /@css-render/plugin-bem/0.15.12_css-render@0.15.12:
+    resolution: {integrity: sha512-Lq2jSOZn+wYQtsyaFj6QRz2EzAnd3iW5fZeHO1WSXQdVYwvwGX0ZiH3X2JQgtgYLT1yeGtrwrqJdNdMEUD2xTw==}
+    peerDependencies:
+      css-render: ~0.15.12
+    dependencies:
+      css-render: 0.15.12
+    dev: false
+
+  /@css-render/vue3-ssr/0.15.12_vue@3.2.47:
+    resolution: {integrity: sha512-AQLGhhaE0F+rwybRCkKUdzBdTEM/5PZBYy+fSYe1T9z9+yxMuV/k7ZRqa4M69X+EI1W8pa4kc9Iq2VjQkZx4rg==}
+    peerDependencies:
+      vue: ^3.0.11
+    dependencies:
+      vue: 3.2.47
+    dev: false
+
   /@csstools/postcss-cascade-layers/1.1.1_postcss@8.4.21:
     resolution: {integrity: sha512-+KdYrpKC5TgomQr2DlZF4lDEpHcoxnj5IGddYYfBWJAKfj1JtuHUIqMa+E1pJJ+z3kvDViWMqyqPlG4Ja7amQA==}
     engines: {node: ^12 || ^14 || >=16}
@@ -1624,6 +1647,10 @@ packages:
     resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==}
     engines: {node: '>=10.0.0'}
 
+  /@emotion/hash/0.8.0:
+    resolution: {integrity: sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==}
+    dev: false
+
   /@esbuild/android-arm/0.15.18:
     resolution: {integrity: sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==}
     engines: {node: '>=12'}
@@ -1947,6 +1974,10 @@ packages:
       '@jridgewell/sourcemap-codec': 1.4.14
     dev: true
 
+  /@juggle/resize-observer/3.4.0:
+    resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==}
+    dev: false
+
   /@leichtgewicht/ip-codec/2.0.4:
     resolution: {integrity: sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==}
 
@@ -1956,12 +1987,10 @@ packages:
     dependencies:
       '@nodelib/fs.stat': 2.0.5
       run-parallel: 1.2.0
-    dev: true
 
   /@nodelib/fs.stat/2.0.5:
     resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
     engines: {node: '>= 8'}
-    dev: true
 
   /@nodelib/fs.walk/1.2.8:
     resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
@@ -1969,7 +1998,6 @@ packages:
     dependencies:
       '@nodelib/fs.scandir': 2.1.5
       fastq: 1.15.0
-    dev: true
 
   /@polka/url/1.0.0-next.21:
     resolution: {integrity: sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==}
@@ -2068,6 +2096,20 @@ packages:
       picomatch: 2.3.1
     dev: false
 
+  /@rollup/pluginutils/5.0.2:
+    resolution: {integrity: sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==}
+    engines: {node: '>=14.0.0'}
+    peerDependencies:
+      rollup: ^1.20.0||^2.0.0||^3.0.0
+    peerDependenciesMeta:
+      rollup:
+        optional: true
+    dependencies:
+      '@types/estree': 1.0.0
+      estree-walker: 2.0.2
+      picomatch: 2.3.1
+    dev: false
+
   /@rushstack/eslint-patch/1.2.0:
     resolution: {integrity: sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==}
     dev: true
@@ -2147,7 +2189,7 @@ packages:
     resolution: {integrity: sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==}
     dependencies:
       '@types/eslint': 8.21.2
-      '@types/estree': 0.0.51
+      '@types/estree': 1.0.0
 
   /@types/eslint/8.21.2:
     resolution: {integrity: sha512-EMpxUyystd3uZVByZap1DACsMXvb82ypQnGn89e1Y0a+LYu3JJscUd/gqhRsVFDkaD2MIiWo0MT8EfXr3DGRKw==}
@@ -2196,6 +2238,20 @@ packages:
     resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
     dev: true
 
+  /@types/katex/0.14.0:
+    resolution: {integrity: sha512-+2FW2CcT0K3P+JMR8YG846bmDwplKUTsWgT2ENwdQ1UdVfRk3GQrh6Mi4sTopy30gI8Uau5CEqHTDZ6YvWIUPA==}
+    dev: false
+
+  /@types/lodash-es/4.17.7:
+    resolution: {integrity: sha512-z0ptr6UI10VlU6l5MYhGwS4mC8DZyYer2mCoyysZtSF7p26zOX8UpbrV0YpNYLGS8K4PUFIyEr62IMFFjveSiQ==}
+    dependencies:
+      '@types/lodash': 4.14.194
+    dev: false
+
+  /@types/lodash/4.14.194:
+    resolution: {integrity: sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g==}
+    dev: false
+
   /@types/mime/3.0.1:
     resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==}
 
@@ -2388,6 +2444,10 @@ packages:
       eslint-visitor-keys: 3.3.0
     dev: true
 
+  /@vicons/ionicons5/0.12.0:
+    resolution: {integrity: sha512-Iy1EUVRpX0WWxeu1VIReR1zsZLMc4fqpt223czR+Rpnrwu7pt46nbnC2ycO7ItI/uqDLJxnbcMC7FujKs9IfFA==}
+    dev: false
+
   /@vue/compiler-core/3.2.47:
     resolution: {integrity: sha512-p4D7FDnQb7+YJmO2iPEv0SQNeNzcbHdGByJDsT4lynf63AFkOTFN07HsiRSvjGo0QrxR/o3d0hUyNCUnBU2Tig==}
     dependencies:
@@ -2960,6 +3020,10 @@ packages:
     engines: {node: '>=8'}
     dev: true
 
+  /async-validator/4.2.5:
+    resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==}
+    dev: false
+
   /async/2.6.4:
     resolution: {integrity: sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==}
     dependencies:
@@ -3223,6 +3287,12 @@ packages:
       balanced-match: 1.0.2
       concat-map: 0.0.1
 
+  /brace-expansion/2.0.1:
+    resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
+    dependencies:
+      balanced-match: 1.0.2
+    dev: false
+
   /braces/3.0.2:
     resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==}
     engines: {node: '>=8'}
@@ -3998,6 +4068,13 @@ packages:
       postcss: 8.4.21
     dev: true
 
+  /css-render/0.15.12:
+    resolution: {integrity: sha512-eWzS66patiGkTTik+ipO9qNGZ+uNuGyTmnz6/+EJIiFg8+3yZRpnMwgFo8YdXhQRsiePzehnusrxVvugNjXzbw==}
+    dependencies:
+      '@emotion/hash': 0.8.0
+      csstype: 3.0.11
+    dev: false
+
   /css-select/4.3.0:
     resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==}
     dependencies:
@@ -4100,6 +4177,10 @@ packages:
   /csstype/2.6.21:
     resolution: {integrity: sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==}
 
+  /csstype/3.0.11:
+    resolution: {integrity: sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==}
+    dev: false
+
   /cz-conventional-changelog/3.3.0:
     resolution: {integrity: sha512-U466fIzU5U22eES5lTNiNbZ+d8dfcHcssH4o7QsdWaCcRs/feIPCxKYSWkYBNs5mny7MvEfwpTLWjvbm94hecw==}
     engines: {node: '>= 10'}
@@ -4139,6 +4220,21 @@ packages:
     engines: {node: '>= 6'}
     dev: false
 
+  /date-fns-tz/1.3.8_date-fns@2.30.0:
+    resolution: {integrity: sha512-qwNXUFtMHTTU6CFSFjoJ80W8Fzzp24LntbjFFBgL/faqds4e5mo9mftoRLgr3Vi1trISsg4awSpYVsOQCRnapQ==}
+    peerDependencies:
+      date-fns: '>=2.0.0'
+    dependencies:
+      date-fns: 2.30.0
+    dev: false
+
+  /date-fns/2.30.0:
+    resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==}
+    engines: {node: '>=0.11'}
+    dependencies:
+      '@babel/runtime': 7.21.0
+    dev: false
+
   /date-format/4.0.14:
     resolution: {integrity: sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==}
     engines: {node: '>=4.0'}
@@ -5118,6 +5214,10 @@ packages:
     resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
     engines: {node: '>=0.8.x'}
 
+  /evtd/0.2.4:
+    resolution: {integrity: sha512-qaeGN5bx63s/AXgQo8gj6fBkxge+OoLddLniox5qtLAEY5HSnuSlISXVPxnSae1dWblvTh4/HoMIB+mbMsvZzw==}
+    dev: false
+
   /execa/5.1.1:
     resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
     engines: {node: '>=10'}
@@ -5215,7 +5315,6 @@ packages:
       glob-parent: 5.1.2
       merge2: 1.4.1
       micromatch: 4.0.5
-    dev: true
 
   /fast-json-stable-stringify/2.1.0:
     resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
@@ -5231,7 +5330,6 @@ packages:
     resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==}
     dependencies:
       reusify: 1.0.4
-    dev: true
 
   /faye-websocket/0.11.4:
     resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==}
@@ -5795,6 +5893,11 @@ packages:
     hasBin: true
     dev: true
 
+  /highlight.js/11.8.0:
+    resolution: {integrity: sha512-MedQhoqVdr0U6SSnWPzfiadUcDHfN/Wzq25AkXiQv9oiOO/sG0S7XkvpFIqWBl9Yq1UYyYOOVORs5UW2XlPyzg==}
+    engines: {node: '>=12.0.0'}
+    dev: false
+
   /homedir-polyfill/1.0.3:
     resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==}
     engines: {node: '>=0.10.0'}
@@ -6391,6 +6494,11 @@ packages:
     resolution: {integrity: sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==}
     dev: false
 
+  /js-cookie/3.0.5:
+    resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
+    engines: {node: '>=14'}
+    dev: false
+
   /js-sdsl/4.3.0:
     resolution: {integrity: sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==}
     dev: true
@@ -6598,6 +6706,11 @@ packages:
       json5: 2.2.3
     dev: true
 
+  /local-pkg/0.4.3:
+    resolution: {integrity: sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==}
+    engines: {node: '>=14'}
+    dev: false
+
   /locate-path/2.0.0:
     resolution: {integrity: sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==}
     engines: {node: '>=4'}
@@ -6627,6 +6740,10 @@ packages:
       p-locate: 5.0.0
     dev: true
 
+  /lodash-es/4.17.21:
+    resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
+    dev: false
+
   /lodash.debounce/4.0.8:
     resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
 
@@ -6734,6 +6851,13 @@ packages:
       '@jridgewell/sourcemap-codec': 1.4.14
     dev: true
 
+  /magic-string/0.30.0:
+    resolution: {integrity: sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==}
+    engines: {node: '>=12'}
+    dependencies:
+      '@jridgewell/sourcemap-codec': 1.4.14
+    dev: false
+
   /make-dir/3.1.0:
     resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
     engines: {node: '>=8'}
@@ -6814,7 +6938,6 @@ packages:
   /merge2/1.4.1:
     resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
     engines: {node: '>= 8'}
-    dev: true
 
   /methods/1.1.2:
     resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==}
@@ -6880,6 +7003,13 @@ packages:
     dependencies:
       brace-expansion: 1.1.11
 
+  /minimatch/7.4.6:
+    resolution: {integrity: sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==}
+    engines: {node: '>=10'}
+    dependencies:
+      brace-expansion: 2.0.1
+    dev: false
+
   /minimist-options/4.1.0:
     resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==}
     engines: {node: '>= 6'}
@@ -6965,6 +7095,32 @@ packages:
       thenify-all: 1.6.0
     dev: false
 
+  /naive-ui/2.34.3_vue@3.2.47:
+    resolution: {integrity: sha512-fUMr0dzb/iGsOTWgoblPVobY5X5dihQ1eam5dA+H74oyLYAvgX4pL96xQFPBLIYqvyRFBAsN85kHN5pLqdtpxA==}
+    peerDependencies:
+      vue: ^3.0.0
+    dependencies:
+      '@css-render/plugin-bem': 0.15.12_css-render@0.15.12
+      '@css-render/vue3-ssr': 0.15.12_vue@3.2.47
+      '@types/katex': 0.14.0
+      '@types/lodash': 4.14.194
+      '@types/lodash-es': 4.17.7
+      async-validator: 4.2.5
+      css-render: 0.15.12
+      date-fns: 2.30.0
+      date-fns-tz: 1.3.8_date-fns@2.30.0
+      evtd: 0.2.4
+      highlight.js: 11.8.0
+      lodash: 4.17.21
+      lodash-es: 4.17.21
+      seemly: 0.3.6
+      treemate: 0.3.11
+      vdirs: 0.1.8_vue@3.2.47
+      vooks: 0.2.12_vue@3.2.47
+      vue: 3.2.47
+      vueuc: 0.4.51_vue@3.2.47
+    dev: false
+
   /nan/2.17.0:
     resolution: {integrity: sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==}
     dev: false
@@ -8534,7 +8690,6 @@ packages:
   /reusify/1.0.4:
     resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
     engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
-    dev: true
 
   /rfdc/1.3.0:
     resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==}
@@ -8643,7 +8798,6 @@ packages:
     resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
     dependencies:
       queue-microtask: 1.2.3
-    dev: true
 
   /rxjs/6.6.7:
     resolution: {integrity: sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==}
@@ -8765,6 +8919,10 @@ packages:
     resolution: {integrity: sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw==}
     dev: false
 
+  /seemly/0.3.6:
+    resolution: {integrity: sha512-lEV5VB8BUKTo/AfktXJcy+JeXns26ylbMkIUco8CYREsQijuz4mrXres2Q+vMLdwkuLxJdIPQ8IlCIxLYm71Yw==}
+    dev: false
+
   /select-hose/2.0.0:
     resolution: {integrity: sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==}
 
@@ -9458,6 +9616,10 @@ packages:
     resolution: {integrity: sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==}
     engines: {node: '>=6'}
 
+  /treemate/0.3.11:
+    resolution: {integrity: sha512-M8RGFoKtZ8dF+iwJfAJTOH/SM4KluKOKRJpjCMhI8bG3qB74zrFoArKZ62ll0Fr3mqkMJiQOmWYkdYgDeITYQg==}
+    dev: false
+
   /trim-newlines/3.0.1:
     resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==}
     engines: {node: '>=8'}
@@ -9663,6 +9825,44 @@ packages:
     resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
     engines: {node: '>= 0.8'}
 
+  /unplugin-vue-components/0.24.1_vue@3.2.47:
+    resolution: {integrity: sha512-T3A8HkZoIE1Cja95xNqolwza0yD5IVlgZZ1PVAGvVCx8xthmjsv38xWRCtHtwl+rvZyL9uif42SRkDGw9aCfMA==}
+    engines: {node: '>=14'}
+    peerDependencies:
+      '@babel/parser': ^7.15.8
+      '@nuxt/kit': ^3.2.2
+      vue: 2 || 3
+    peerDependenciesMeta:
+      '@babel/parser':
+        optional: true
+      '@nuxt/kit':
+        optional: true
+    dependencies:
+      '@antfu/utils': 0.7.2
+      '@rollup/pluginutils': 5.0.2
+      chokidar: 3.5.3
+      debug: 4.3.4
+      fast-glob: 3.2.12
+      local-pkg: 0.4.3
+      magic-string: 0.30.0
+      minimatch: 7.4.6
+      resolve: 1.22.1
+      unplugin: 1.3.1
+      vue: 3.2.47
+    transitivePeerDependencies:
+      - rollup
+      - supports-color
+    dev: false
+
+  /unplugin/1.3.1:
+    resolution: {integrity: sha512-h4uUTIvFBQRxUKS2Wjys6ivoeofGhxzTe2sRWlooyjHXVttcVfV/JiavNd3d4+jty0SVV0dxGw9AkY9MwiaCEw==}
+    dependencies:
+      acorn: 8.8.2
+      chokidar: 3.5.3
+      webpack-sources: 3.2.3
+      webpack-virtual-modules: 0.5.0
+    dev: false
+
   /update-browserslist-db/1.0.10_browserslist@4.21.5:
     resolution: {integrity: sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==}
     hasBin: true
@@ -9758,6 +9958,15 @@ packages:
       mutation-observer: 1.0.3
     dev: false
 
+  /vdirs/0.1.8_vue@3.2.47:
+    resolution: {integrity: sha512-H9V1zGRLQZg9b+GdMk8MXDN2Lva0zx72MPahDKc30v+DtwKjfyOSXWRIX4t2mhDubM1H09gPhWeth/BJWPHGUw==}
+    peerDependencies:
+      vue: ^3.0.11
+    dependencies:
+      evtd: 0.2.4
+      vue: 3.2.47
+    dev: false
+
   /vite/4.2.0_34kcdhufoak4xbjfji44lxubke:
     resolution: {integrity: sha512-AbDTyzzwuKoRtMIRLGNxhLRuv1FpRgdIw+1y6AQG73Q5+vtecmvzKo/yk8X/vrHDpETRTx01ABijqUHIzBXi0g==}
     engines: {node: ^14.18.0 || >=16.0.0}
@@ -9803,6 +10012,15 @@ packages:
       acorn-walk: 8.2.0
     dev: false
 
+  /vooks/0.2.12_vue@3.2.47:
+    resolution: {integrity: sha512-iox0I3RZzxtKlcgYaStQYKEzWWGAduMmq+jS7OrNdQo1FgGfPMubGL3uGHOU9n97NIvfFDBGnpSvkWyb/NSn/Q==}
+    peerDependencies:
+      vue: ^3.0.0
+    dependencies:
+      evtd: 0.2.4
+      vue: 3.2.47
+    dev: false
+
   /vue-demi/0.13.11_vue@3.2.47:
     resolution: {integrity: sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==}
     engines: {node: '>=12'}
@@ -9881,6 +10099,21 @@ packages:
       '@vue/server-renderer': 3.2.47_vue@3.2.47
       '@vue/shared': 3.2.47
 
+  /vueuc/0.4.51_vue@3.2.47:
+    resolution: {integrity: sha512-pLiMChM4f+W8czlIClGvGBYo656lc2Y0/mXFSCydcSmnCR1izlKPGMgiYBGjbY9FDkFG8a2HEVz7t0DNzBWbDw==}
+    peerDependencies:
+      vue: ^3.0.11
+    dependencies:
+      '@css-render/vue3-ssr': 0.15.12_vue@3.2.47
+      '@juggle/resize-observer': 3.4.0
+      css-render: 0.15.12
+      evtd: 0.2.4
+      seemly: 0.3.6
+      vdirs: 0.1.8_vue@3.2.47
+      vooks: 0.2.12_vue@3.2.47
+      vue: 3.2.47
+    dev: false
+
   /watchpack/2.4.0:
     resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==}
     engines: {node: '>=10.13.0'}
@@ -10030,7 +10263,6 @@ packages:
 
   /webpack-virtual-modules/0.5.0:
     resolution: {integrity: sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==}
-    dev: true
 
   /webpack/5.76.2_webpack-cli@4.10.0:
     resolution: {integrity: sha512-Th05ggRm23rVzEOlX8y67NkYCHa9nTNcwHPBhdg+lKG+mtiW7XgggjAeeLnADAe7mLjJ6LUNfgHAuRRh+Z6J7w==}

+ 25 - 21
public/index.html

@@ -1,25 +1,29 @@
 <!DOCTYPE html>
 <html lang="">
+  <head>
+    <meta charset="utf-8" />
+    <meta
+      http-equiv="X-UA-Compatible"
+      content="IE=edge"
+    />
+    <meta
+      name="viewport"
+      content="width=device-width,initial-scale=1.0"
+    />
+    <link
+      rel="icon"
+      href="<%= BASE_URL %>favicon.ico"
+    />
+    <title><%= htmlWebpackPlugin.options.title %></title>
+  </head>
 
-<head>
-  <meta charset="utf-8" />
-  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
-  <meta name="viewport" content="width=device-width,initial-scale=1.0" />
-  <link rel="icon" href="<%= BASE_URL %>favicon.ico" />
-  <script src="<%= BASE_URL %>jsmpeg.min.js"></script>
-  <title>
-    <%= htmlWebpackPlugin.options.title %>
-  </title>
-</head>
-
-<body>
-  <noscript>
-    <strong>
-      We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work
+  <body>
+    <noscript>
+      <strong>
+        We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work
         properly without JavaScript enabled. Please enable it to continue.
-    </strong>
-  </noscript>
-  <div id="app"></div>
-</body>
-
-</html>
+      </strong>
+    </noscript>
+    <div id="app"></div>
+  </body>
+</html>

ファイルの差分が大きいため隠しています
+ 0 - 5208
public/jsmpeg.min.js


+ 11 - 3
script/config/webpack.common.ts

@@ -1,9 +1,11 @@
 import FriendlyErrorsWebpackPlugin from '@soda/friendly-errors-webpack-plugin';
-import BilldHtmlWebpackPlugin from 'billd-html-webpack-plugin';
+import BilldHtmlWebpackPlugin, { logData } from 'billd-html-webpack-plugin';
 import CopyWebpackPlugin from 'copy-webpack-plugin';
 import ESLintPlugin from 'eslint-webpack-plugin';
 import HtmlWebpackPlugin from 'html-webpack-plugin';
 import MiniCssExtractPlugin from 'mini-css-extract-plugin';
+import { NaiveUiResolver } from 'unplugin-vue-components/resolvers';
+import ComponentsPlugin from 'unplugin-vue-components/webpack';
 import { VueLoaderPlugin } from 'vue-loader';
 import { Configuration, DefinePlugin } from 'webpack';
 import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
@@ -313,6 +315,11 @@ const commonConfig = (isProduction) => {
       new FriendlyErrorsWebpackPlugin(),
       // 解析vue
       new VueLoaderPlugin(),
+      // eslint-disable-next-line
+      ComponentsPlugin({
+        // eslint-disable-next-line
+        resolvers: [NaiveUiResolver()],
+      }),
       // windicss
       windicssEnable && new WindiCSSWebpackPlugin(),
       // 该插件将为您生成一个HTML5文件,其中包含使用脚本标签的所有Webpack捆绑包
@@ -362,6 +369,7 @@ const commonConfig = (isProduction) => {
       new DefinePlugin({
         BASE_URL: `${JSON.stringify(outputStaticUrl(isProduction))}`, // public下的index.html里面的favicon.ico的路径
         'process.env': {
+          BilldHtmlWebpackPlugin: JSON.stringify(logData()),
           NODE_ENV: JSON.stringify(isProduction ? 'production' : 'development'),
           PUBLIC_PATH: JSON.stringify(outputStaticUrl(isProduction)),
           VUE_APP_RELEASE_PROJECT_NAME: JSON.stringify(
@@ -371,8 +379,8 @@ const commonConfig = (isProduction) => {
             process.env.VUE_APP_RELEASE_PROJECT_ENV
           ),
         },
-        __VUE_OPTIONS_API__: 'true',
-        __VUE_PROD_DEVTOOLS__: 'false',
+        __VUE_OPTIONS_API__: false,
+        __VUE_PROD_DEVTOOLS__: false,
       }),
       // bundle分析
       analyzerEnable &&

+ 12 - 0
src/App.vue

@@ -5,7 +5,19 @@
 <script lang="ts" setup>
 import { onMounted } from 'vue';
 
+import { loginMessage } from '@/hooks/use-login';
+import { useUserStore } from '@/store/user';
+import cache from '@/utils/cache';
+
+const userStore = useUserStore();
+
 onMounted(() => {
+  loginMessage();
+  const token = cache.getStorageExp('token');
+  if (token) {
+    userStore.setToken(token);
+    userStore.getUserInfo();
+  }
   // 启用vconsole
   // import('vconsole')
   //   .then((VConsole) => {

+ 3 - 3
src/api/aliPay.ts → src/api/order.ts

@@ -13,7 +13,7 @@ export function fetchAliPay(data: {
   body: string;
 }) {
   return request.instance({
-    url: '/api/alipay/pay',
+    url: '/api/order/pay',
     method: 'post',
     data,
   });
@@ -26,7 +26,7 @@ export function fetchAliPay(data: {
  */
 export function fetchAliPayStatus(params: { out_trade_no: string }) {
   return request.instance({
-    url: '/api/alipay/pay_status',
+    url: '/api/order/pay_status',
     method: 'get',
     params,
   });
@@ -38,7 +38,7 @@ export function fetchAliPayStatus(params: { out_trade_no: string }) {
  */
 export function fetchAliPayList() {
   return request.instance({
-    url: '/api/alipay/pay_list',
+    url: '/api/order/order_list',
     method: 'get',
   });
 }

+ 10 - 0
src/api/qqUser.ts

@@ -0,0 +1,10 @@
+import request from '@/utils/request';
+
+// qq登录
+export function fetchQQLogin(code: any) {
+  return request.instance({
+    url: `/api/qq_user/login`,
+    method: 'post',
+    data: { code },
+  });
+}

+ 8 - 0
src/api/user.ts

@@ -0,0 +1,8 @@
+import request from '@/utils/request';
+
+export function fetchUserInfo() {
+  return request.instance({
+    url: '/api/user/get_user_info',
+    method: 'get',
+  });
+}

+ 18 - 0
src/components/Message/content/index.vue

@@ -0,0 +1,18 @@
+<template>
+  <div></div>
+</template>
+
+<script lang="ts">
+import { useMessage } from 'naive-ui';
+import { defineComponent } from 'vue';
+
+export default defineComponent({
+  components: {},
+  setup() {
+    // @ts-ignore
+    window.$message = useMessage();
+  },
+});
+</script>
+
+<style lang="scss" scoped></style>

+ 21 - 0
src/components/Message/index.vue

@@ -0,0 +1,21 @@
+<template>
+  <div>
+    <NMessageProvider>
+      <ContentCpt></ContentCpt>
+    </NMessageProvider>
+  </div>
+</template>
+
+<script lang="ts">
+import { NMessageProvider } from 'naive-ui';
+import { defineComponent } from 'vue';
+
+import ContentCpt from './content/index.vue';
+
+export default defineComponent({
+  components: { NMessageProvider, ContentCpt },
+  setup() {},
+});
+</script>
+
+<style lang="scss" scoped></style>

+ 59 - 0
src/components/VideoControls/index.vue

@@ -0,0 +1,59 @@
+<template>
+  <div class="video-controls-wrap">
+    <div
+      v-if="!appStore.muted"
+      class="item"
+      @click="appStore.setMuted(!appStore.muted)"
+    >
+      <n-icon
+        size="25"
+        color="white"
+      >
+        <VolumeHighOutline></VolumeHighOutline>
+      </n-icon>
+    </div>
+    <div
+      v-else
+      class="item"
+      @click="appStore.setMuted(!appStore.muted)"
+    >
+      <n-icon
+        size="25"
+        color="white"
+      >
+        <VolumeMuteOutline></VolumeMuteOutline>
+      </n-icon>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { VolumeHighOutline, VolumeMuteOutline } from '@vicons/ionicons5';
+
+import { useAppStore } from '@/store/app';
+
+const appStore = useAppStore();
+</script>
+
+<style lang="scss" scoped>
+.video-controls-wrap {
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  display: flex;
+  align-items: center;
+  box-sizing: border-box;
+  padding-left: 10px;
+  width: 100%;
+  height: 40px;
+  background-image: linear-gradient(
+    -180deg,
+    rgba(0, 0, 0, 0),
+    rgba(0, 0, 0, 0.6)
+  );
+  text-align: initial;
+  .item {
+    cursor: pointer;
+  }
+}
+</style>

+ 15 - 0
src/constant.ts

@@ -0,0 +1,15 @@
+export const QQ_CLIENT_ID = '101958191';
+export const QQ_OAUTH_URL = 'https://graph.qq.com/oauth2.0';
+export const QQ_REDIRECT_URI = 'https://live.hsslive.cn/oauth/qq_login';
+
+export const LIVE_CLIENT_URL = 'https://www.hsslive.cn';
+
+// 全局的cookie的key
+export const COOKIE_KEY = {
+  loginInfo: 'loginInfo',
+};
+
+// 全局的localStorage的key
+export const LOCALSTORAGE_KEY = {
+  verion: '0.0.1',
+};

+ 61 - 0
src/hooks/use-login.ts

@@ -0,0 +1,61 @@
+import { hrefToTarget, isMobile } from 'billd-utils';
+
+import { fetchQQLogin } from '@/api/qqUser';
+import { QQ_CLIENT_ID, QQ_OAUTH_URL, QQ_REDIRECT_URI } from '@/constant';
+import { PlatformEnum } from '@/interface';
+import { useUserStore } from '@/store/user';
+import { clearLoginInfo, setLoginInfo } from '@/utils/cookie';
+
+const POSTMESSAGE_TYPE = [PlatformEnum.qqLogin];
+
+export const handleLogin = async (e) => {
+  const { type, data } = e.data;
+  if (!POSTMESSAGE_TYPE.includes(type)) return;
+  console.log('收到消息', type, data);
+  const userStore = useUserStore();
+
+  try {
+    switch (type) {
+      case PlatformEnum.qqLogin: {
+        const res = await fetchQQLogin(data);
+        if (res.code === 200) {
+          window.$message.success('登录成功!');
+        }
+        userStore.setToken(res.data);
+        userStore.getUserInfo();
+        break;
+      }
+    }
+  } catch (error) {
+    console.log(error);
+  } finally {
+    clearLoginInfo();
+  }
+};
+
+export const loginMessage = () => {
+  window.addEventListener('message', handleLogin);
+};
+
+export const useQQLogin = () => {
+  const url = (state: string) =>
+    `${QQ_OAUTH_URL}/authorize?response_type=code&client_id=${QQ_CLIENT_ID}&redirect_uri=${QQ_REDIRECT_URI}&scope=get_user_info,get_vip_info,get_vip_rich_info&state=${state}`;
+  let loginInfo = JSON.stringify({
+    isMobile: false,
+    createTime: +new Date(),
+    env: 'qq',
+    dev: process.env.NODE_ENV === 'development',
+  });
+  if (isMobile()) {
+    loginInfo = JSON.stringify({ ...JSON.parse(loginInfo), isMobile: true });
+    setLoginInfo(loginInfo);
+    hrefToTarget(url(window.btoa(loginInfo)));
+  } else {
+    setLoginInfo(loginInfo);
+    window.open(
+      url(window.btoa(loginInfo)),
+      'qq_login_window',
+      'toolbar=yes,location=no,directories=no,status=no,menubar=no,scrollbars=no,titlebar=no,toolbar=no,resizable=no,copyhistory=yes, width=918, height=609,top=250,left=400'
+    );
+  }
+};

+ 405 - 0
src/hooks/use-pull.ts

@@ -0,0 +1,405 @@
+import { getRandomString } from 'billd-utils';
+import { reactive, ref, watch } from 'vue';
+import { useRoute } from 'vue-router';
+
+import { fetchRtcV1Play } from '@/api/srs';
+import { useFlvPlay } from '@/hooks/use-play';
+import {
+  DanmuMsgTypeEnum,
+  IAdminIn,
+  ICandidate,
+  IDanmu,
+  ILiveUser,
+  IOffer,
+} from '@/interface';
+import { SRSWebRTCClass } from '@/network/srsWebRtc';
+import { WebRTCClass } from '@/network/webRtc';
+import {
+  WebSocketClass,
+  WsConnectStatusEnum,
+  WsMsgTypeEnum,
+} from '@/network/webSocket';
+import { useNetworkStore } from '@/store/network';
+import { useUserStore } from '@/store/user';
+
+export function usePull({
+  localVideoRef,
+  isSRS,
+  isFlv,
+}: {
+  localVideoRef;
+  isSRS?: boolean;
+  isFlv?: boolean;
+}) {
+  const route = useRoute();
+  const userStore = useUserStore();
+  const networkStore = useNetworkStore();
+
+  const heartbeatTimer = ref();
+  const roomId = ref(route.params.roomId as string);
+  const roomName = ref('');
+  const streamurl = ref('');
+  const flvurl = ref('');
+  const danmuStr = ref('');
+  const damuList = ref<IDanmu[]>([]);
+  const liveUserList = ref<ILiveUser[]>([]);
+  const roomNoLive = ref(false);
+  const track = reactive({
+    audio: true,
+    video: true,
+  });
+  const giftList = ref([
+    { name: '鲜花', ico: '', price: '免费' },
+    { name: '肥宅水', ico: '', price: '2元' },
+    { name: '小鸡腿', ico: '', price: '3元' },
+    { name: '大鸡腿', ico: '', price: '5元' },
+    { name: '一杯咖啡', ico: '', price: '10元' },
+  ]);
+
+  watch(
+    [
+      () => userStore.userInfo,
+      () => networkStore.wsMap.get(roomId.value)?.socketIo?.connected,
+    ],
+    ([userInfo, connected]) => {
+      if (userInfo && connected) {
+        const instance = networkStore.wsMap.get(roomId.value);
+        if (!instance) return;
+        instance.send({
+          msgType: WsMsgTypeEnum.updateJoinInfo,
+          data: {
+            userInfo: userStore.userInfo,
+          },
+        });
+      }
+    }
+  );
+
+  function initPull() {
+    console.warn('开始new WebSocketClass');
+    const ws = new WebSocketClass({
+      roomId: roomId.value,
+      url:
+        process.env.NODE_ENV === 'development'
+          ? 'ws://localhost:4300'
+          : 'wss://live.hsslive.cn',
+      isAdmin: false,
+    });
+    ws.update();
+    initReceive();
+    localVideoRef.value.addEventListener('loadstart', () => {
+      console.warn('视频流-loadstart');
+      const rtc = networkStore.getRtcMap(roomId.value);
+      if (!rtc) return;
+      rtc.rtcStatus.loadstart = true;
+      rtc.update();
+    });
+
+    localVideoRef.value.addEventListener('loadedmetadata', () => {
+      console.warn('视频流-loadedmetadata');
+      const rtc = networkStore.getRtcMap(roomId.value);
+      if (!rtc) return;
+      rtc.rtcStatus.loadedmetadata = true;
+      rtc.update();
+    });
+  }
+
+  function handleHeartbeat() {
+    heartbeatTimer.value = setInterval(() => {
+      const instance = networkStore.wsMap.get(roomId.value);
+      if (!instance) return;
+      instance.send({
+        msgType: WsMsgTypeEnum.heartbeat,
+      });
+    }, 1000 * 30);
+  }
+
+  function closeWs() {
+    const instance = networkStore.wsMap.get(roomId.value);
+    instance?.close();
+  }
+
+  function closeRtc() {
+    networkStore.rtcMap.forEach((rtc) => {
+      rtc.close();
+    });
+  }
+
+  function getSocketId() {
+    return networkStore.wsMap.get(roomId.value!)?.socketIo?.id || '-1';
+  }
+
+  function sendJoin() {
+    const instance = networkStore.wsMap.get(roomId.value);
+    if (!instance) return;
+    instance.send({
+      msgType: WsMsgTypeEnum.join,
+      data: { userInfo: userStore.userInfo },
+    });
+  }
+
+  /** 原生的webrtc时,receiver必传 */
+  async function startNewWebRtc(receiver?: string) {
+    if (isSRS) {
+      console.warn('开始new SRSWebRTCClass', getSocketId());
+      const rtc = new SRSWebRTCClass({
+        roomId: `${roomId.value}___${getSocketId()}`,
+      });
+      rtc.rtcStatus.joined = true;
+      rtc.update();
+      if (track.video) {
+        rtc.peerConnection?.addTransceiver('video', { direction: 'recvonly' });
+      }
+      if (track.audio) {
+        rtc.peerConnection?.addTransceiver('audio', { direction: 'recvonly' });
+      }
+      try {
+        const offer = await rtc.createOffer();
+        if (!offer) return;
+        await rtc.setLocalDescription(offer);
+        const res: any = await fetchRtcV1Play({
+          api: `${
+            process.env.NODE_ENV === 'development'
+              ? 'http://localhost:1985'
+              : 'https://live.hsslive.cn/srs'
+          }/rtc/v1/play/`,
+          clientip: null,
+          sdp: offer.sdp!,
+          streamurl: streamurl.value,
+          tid: getRandomString(10),
+        });
+        await rtc.setRemoteDescription(
+          new RTCSessionDescription({ type: 'answer', sdp: res.sdp })
+        );
+      } catch (error) {
+        console.log(error);
+      }
+    } else {
+      console.warn('开始new WebRTCClass');
+      const rtc = new WebRTCClass({ roomId: `${roomId.value}___${receiver!}` });
+      return rtc;
+    }
+  }
+
+  function keydownDanmu(event: KeyboardEvent) {
+    const key = event.key.toLowerCase();
+    if (key === 'enter') {
+      event.preventDefault();
+      sendDanmu();
+    }
+  }
+
+  function sendDanmu() {
+    if (!danmuStr.value.trim().length) {
+      window.$message.warning('请输入弹幕内容!');
+      return;
+    }
+    const instance = networkStore.wsMap.get(roomId.value);
+    if (!instance) return;
+    const danmu: IDanmu = {
+      socketId: getSocketId(),
+      userInfo: userStore.userInfo,
+      msgType: DanmuMsgTypeEnum.danmu,
+      msg: danmuStr.value,
+    };
+    instance.send({
+      msgType: WsMsgTypeEnum.message,
+      data: danmu,
+    });
+    damuList.value.push(danmu);
+    danmuStr.value = '';
+  }
+
+  function initReceive() {
+    const instance = networkStore.wsMap.get(roomId.value);
+    if (!instance?.socketIo) return;
+    // websocket连接成功
+    instance.socketIo.on(WsConnectStatusEnum.connect, () => {
+      console.log('【websocket】websocket连接成功');
+      if (!instance) return;
+      instance.status = WsConnectStatusEnum.connect;
+      instance.update();
+      sendJoin();
+      handleHeartbeat();
+    });
+
+    // websocket连接断开
+    instance.socketIo.on(WsConnectStatusEnum.disconnect, () => {
+      console.log('【websocket】websocket连接断开');
+      if (!instance) return;
+      instance.status = WsConnectStatusEnum.disconnect;
+      instance.update();
+    });
+
+    // 收到offer
+    instance.socketIo.on(WsMsgTypeEnum.offer, async (data: IOffer) => {
+      console.warn('【websocket】收到offer', data);
+      if (isSRS) return;
+      if (!instance) return;
+      if (data.data.receiver === getSocketId()) {
+        console.log('收到offer,这个offer是发给我的');
+        const rtc = await startNewWebRtc(data.data.sender);
+        if (rtc) {
+          await rtc.setRemoteDescription(data.data.sdp);
+          const sdp = await rtc.createAnswer();
+          await rtc.setLocalDescription(sdp);
+          instance.send({
+            msgType: WsMsgTypeEnum.answer,
+            data: { sdp, sender: getSocketId(), receiver: data.data.sender },
+          });
+        }
+      } else {
+        console.log('收到offer,但是这个offer不是发给我的');
+      }
+    });
+
+    // 收到answer
+    instance.socketIo.on(WsMsgTypeEnum.answer, async (data: IOffer) => {
+      console.warn('【websocket】收到answer', data);
+      if (isSRS) return;
+      if (!instance) return;
+      const rtc = networkStore.getRtcMap(`${roomId.value}___${data.socketId}`);
+      if (!rtc) return;
+      rtc.rtcStatus.answer = true;
+      rtc.update();
+      if (data.data.receiver === getSocketId()) {
+        console.log('收到answer,这个answer是发给我的');
+        await rtc.setRemoteDescription(data.data.sdp);
+      } else {
+        console.log('收到answer,但这个answer不是发给我的');
+      }
+    });
+
+    // 收到candidate
+    instance.socketIo.on(WsMsgTypeEnum.candidate, (data: ICandidate) => {
+      console.warn('【websocket】收到candidate', data);
+      if (isSRS) return;
+      if (!instance) return;
+      const rtc = networkStore.getRtcMap(`${roomId.value}___${data.socketId}`);
+      if (!rtc) return;
+      if (data.socketId !== getSocketId()) {
+        console.log('不是我发的candidate');
+        const candidate = new RTCIceCandidate({
+          sdpMid: data.data.sdpMid,
+          sdpMLineIndex: data.data.sdpMLineIndex,
+          candidate: data.data.candidate,
+        });
+        rtc.peerConnection
+          ?.addIceCandidate(candidate)
+          .then(() => {
+            console.log('candidate成功');
+          })
+          .catch((err) => {
+            console.error('candidate失败', err);
+          });
+      } else {
+        console.log('是我发的candidate');
+      }
+    });
+
+    // 当前所有在线用户
+    instance.socketIo.on(WsMsgTypeEnum.roomLiveing, (data: IAdminIn) => {
+      console.log('【websocket】收到管理员正在直播', data);
+      if (isSRS && !isFlv) {
+        startNewWebRtc();
+      }
+    });
+
+    // 当前所有在线用户
+    instance.socketIo.on(WsMsgTypeEnum.roomNoLive, (data: IAdminIn) => {
+      console.log('【websocket】收到管理员不在直播', data);
+      roomNoLive.value = true;
+      closeRtc();
+    });
+
+    // 当前所有在线用户
+    instance.socketIo.on(WsMsgTypeEnum.liveUser, (data) => {
+      console.log('【websocket】当前所有在线用户', data);
+      if (!instance) return;
+      liveUserList.value = data.map((item) => ({
+        avatar: 'red',
+        socketId: item.id,
+      }));
+    });
+
+    // 收到用户发送消息
+    instance.socketIo.on(WsMsgTypeEnum.message, (data) => {
+      console.log('【websocket】收到用户发送消息', data);
+      if (!instance) return;
+      const danmu: IDanmu = {
+        msgType: DanmuMsgTypeEnum.danmu,
+        socketId: data.socketId,
+        userInfo: data.data.userInfo,
+        msg: data.data.msg,
+      };
+      damuList.value.push(danmu);
+    });
+
+    // 用户加入房间
+    instance.socketIo.on(WsMsgTypeEnum.joined, (data) => {
+      console.log('【websocket】用户加入房间完成', data);
+      roomName.value = data.data.roomName;
+      track.audio = data.data.track_audio;
+      track.video = data.data.track_video;
+      streamurl.value = data.data.streamurl;
+      flvurl.value = data.data.flvurl;
+      if (isFlv) {
+        useFlvPlay(flvurl.value, localVideoRef.value!);
+      }
+      instance.send({ msgType: WsMsgTypeEnum.getLiveUser });
+    });
+
+    // 其他用户加入房间
+    instance.socketIo.on(WsMsgTypeEnum.otherJoin, (data) => {
+      console.log('【websocket】其他用户加入房间', data);
+      const danmu: IDanmu = {
+        msgType: DanmuMsgTypeEnum.otherJoin,
+        socketId: data.data.socketId,
+        userInfo: data.data.userInfo,
+        msg: '',
+      };
+      damuList.value.push(danmu);
+    });
+
+    // 用户离开房间
+    instance.socketIo.on(WsMsgTypeEnum.leave, (data) => {
+      console.log('【websocket】用户离开房间', data);
+      if (!instance) return;
+      instance.socketIo?.emit(WsMsgTypeEnum.leave, {
+        roomId: instance.roomId,
+      });
+    });
+
+    // 用户离开房间完成
+    instance.socketIo.on(WsMsgTypeEnum.leaved, (data) => {
+      console.log('【websocket】用户离开房间完成', data);
+      if (!instance) return;
+      const res = liveUserList.value.filter(
+        (item) => item.socketId !== data.socketId
+      );
+      liveUserList.value = res;
+      const danmu: IDanmu = {
+        msgType: DanmuMsgTypeEnum.userLeaved,
+        socketId: data.socketId,
+        userInfo: data.data.userInfo,
+        msg: '',
+      };
+      damuList.value.push(danmu);
+    });
+  }
+
+  return {
+    initPull,
+    closeWs,
+    closeRtc,
+    getSocketId,
+    keydownDanmu,
+    sendDanmu,
+    roomName,
+    roomNoLive,
+    damuList,
+    giftList,
+    liveUserList,
+    danmuStr,
+  };
+}

+ 583 - 0
src/hooks/use-push.ts

@@ -0,0 +1,583 @@
+import { getRandomString } from 'billd-utils';
+import { reactive, ref } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+
+import { fetchRtcV1Publish } from '@/api/srs';
+import {
+  DanmuMsgTypeEnum,
+  IAdminIn,
+  ICandidate,
+  IDanmu,
+  ILiveUser,
+  IOffer,
+  MediaTypeEnum,
+} from '@/interface';
+import { SRSWebRTCClass } from '@/network/srsWebRtc';
+import { WebRTCClass } from '@/network/webRtc';
+import {
+  WebSocketClass,
+  WsConnectStatusEnum,
+  WsMsgTypeEnum,
+} from '@/network/webSocket';
+import { useNetworkStore } from '@/store/network';
+import { useUserStore } from '@/store/user';
+
+export function usePush({
+  localVideoRef,
+  isSRS,
+}: {
+  localVideoRef;
+  isSRS?: boolean;
+}) {
+  const route = useRoute();
+  const router = useRouter();
+  const userStore = useUserStore();
+  const networkStore = useNetworkStore();
+
+  const roomId = ref<string>(getRandomString(15));
+  const danmuStr = ref('');
+  const roomName = ref('');
+  const isDone = ref(false);
+  const joined = ref(false);
+  const disabled = ref(false);
+  const localStream = ref();
+  const offerSended = ref(new Set());
+
+  const track = reactive({
+    audio: true,
+    video: true,
+  });
+  const streamurl = ref(
+    `webrtc://${
+      process.env.NODE_ENV === 'development' ? 'localhost' : 'live.hsslive.cn'
+    }/live/livestream/${roomId.value}`
+  );
+  const flvurl = ref(
+    `${
+      process.env.NODE_ENV === 'development'
+        ? 'http://localhost:5001'
+        : 'https://live.hsslive.cn/srsflv'
+    }/live/livestream/${roomId.value}.flv`
+  );
+
+  const damuList = ref<IDanmu[]>([]);
+  const liveUserList = ref<ILiveUser[]>([]);
+
+  const allMediaTypeList = {
+    [MediaTypeEnum.camera]: {
+      type: MediaTypeEnum.camera,
+      txt: '摄像头',
+    },
+    [MediaTypeEnum.screen]: {
+      type: MediaTypeEnum.screen,
+      txt: '窗口',
+    },
+  };
+  const currMediaTypeList = ref<
+    {
+      type: MediaTypeEnum;
+      txt: string;
+    }[]
+  >([]);
+  const currMediaType = ref<{
+    type: MediaTypeEnum;
+    txt: string;
+  }>();
+
+  function startLive() {
+    if (!roomNameIsOk()) return;
+    if (currMediaTypeList.value.length <= 0) {
+      window.$message.warning('请选择一个素材!');
+      return;
+    }
+    disabled.value = true;
+
+    const ws = new WebSocketClass({
+      roomId: roomId.value,
+      url:
+        process.env.NODE_ENV === 'development'
+          ? 'ws://localhost:4300'
+          : 'wss://live.hsslive.cn',
+      isAdmin: true,
+    });
+    ws.update();
+    initReceive();
+    if (isSRS) {
+      sendJoin();
+    }
+  }
+
+  /** 原生的webrtc时,receiver必传 */
+  async function startNewWebRtc(receiver?: string) {
+    if (isSRS) {
+      console.warn('开始new SRSWebRTCClass');
+      const rtc = new SRSWebRTCClass({
+        roomId: `${roomId.value}___${getSocketId()}`,
+      });
+      localStream.value.getTracks().forEach((track) => {
+        rtc.addTrack({
+          track,
+          stream: localStream.value,
+          direction: 'sendonly',
+        });
+      });
+      try {
+        const offer = await rtc.createOffer();
+        if (!offer) return;
+        await rtc.setLocalDescription(offer);
+        const res: any = await fetchRtcV1Publish({
+          api: `${
+            process.env.NODE_ENV === 'development'
+              ? 'http://localhost:1985'
+              : 'https://live.hsslive.cn/srs'
+          }/rtc/v1/publish/`,
+          clientip: null,
+          sdp: offer.sdp!,
+          streamurl: streamurl.value,
+          tid: getRandomString(10),
+        });
+        await rtc.setRemoteDescription(
+          new RTCSessionDescription({ type: 'answer', sdp: res.sdp })
+        );
+      } catch (error) {
+        console.log(error);
+      }
+    } else {
+      console.warn('开始new WebRTCClass');
+      const rtc = new WebRTCClass({ roomId: `${roomId.value}___${receiver!}` });
+      return rtc;
+    }
+  }
+
+  function handleCoverImg() {
+    const canvas = document.createElement('canvas');
+    const { width, height } = localVideoRef.value!.getBoundingClientRect();
+    const rate = width / height;
+    const coverWidth = width * 0.5;
+    const coverHeight = coverWidth / rate;
+    canvas.width = coverWidth;
+    canvas.height = coverHeight;
+    canvas
+      .getContext('2d')!
+      .drawImage(localVideoRef.value!, 0, 0, coverWidth, coverHeight);
+    // webp比png的体积小非常多!因此coverWidth就可以不用压缩太夸张
+    const dataURL = canvas.toDataURL('image/webp');
+    return dataURL;
+  }
+
+  function closeWs() {
+    const instance = networkStore.wsMap.get(roomId.value);
+    instance?.close();
+  }
+
+  function closeRtc() {
+    networkStore.rtcMap.forEach((rtc) => {
+      rtc.close();
+    });
+  }
+
+  function addTrack() {
+    if (!localStream.value) return;
+    liveUserList.value.forEach((item) => {
+      if (item.socketId !== getSocketId()) {
+        localStream.value.getTracks().forEach((track) => {
+          const rtc = networkStore.getRtcMap(
+            `${roomId.value}___${item.socketId}`
+          );
+          rtc?.addTrack(track, localStream.value);
+        });
+      }
+    });
+  }
+
+  function sendJoin() {
+    const instance = networkStore.wsMap.get(roomId.value);
+    if (!instance) return;
+    instance.send({
+      msgType: WsMsgTypeEnum.join,
+      data: {
+        roomName: roomName.value,
+        coverImg: handleCoverImg(),
+        srs: isSRS
+          ? {
+              streamurl: streamurl.value,
+              flvurl: flvurl.value,
+            }
+          : undefined,
+        track,
+        userInfo: userStore.userInfo,
+      },
+    });
+  }
+
+  async function sendOffer({
+    sender,
+    receiver,
+  }: {
+    sender: string;
+    receiver: string;
+  }) {
+    if (isDone.value) return;
+    const instance = networkStore.wsMap.get(roomId.value);
+    if (!instance) return;
+    const rtc = networkStore.getRtcMap(`${roomId.value}___${receiver}`);
+    if (!rtc) return;
+    const sdp = await rtc.createOffer();
+    await rtc.setLocalDescription(sdp);
+    instance.send({
+      msgType: WsMsgTypeEnum.offer,
+      data: { sdp, sender, receiver },
+    });
+  }
+
+  function batchSendOffer() {
+    liveUserList.value.forEach(async (item) => {
+      if (
+        !offerSended.value.has(item.socketId) &&
+        item.socketId !== getSocketId()
+      ) {
+        await startNewWebRtc(item.socketId);
+        await addTrack();
+        console.warn('new WebRTCClass完成');
+        console.log('执行sendOffer', {
+          sender: getSocketId(),
+          receiver: item.socketId,
+        });
+        sendOffer({ sender: getSocketId(), receiver: item.socketId });
+        offerSended.value.add(item.socketId);
+      }
+    });
+  }
+
+  function getSocketId() {
+    return networkStore.wsMap.get(roomId.value!)?.socketIo?.id || '-1';
+  }
+
+  function initReceive() {
+    const instance = networkStore.wsMap.get(roomId.value);
+    if (!instance?.socketIo) return;
+    // websocket连接成功
+    instance.socketIo.on(WsConnectStatusEnum.connect, () => {
+      console.log('【websocket】websocket连接成功', instance.socketIo?.id);
+      if (!instance) return;
+      instance.status = WsConnectStatusEnum.connect;
+      instance.update();
+      if (!isSRS) {
+        sendJoin();
+      }
+    });
+
+    // websocket连接断开
+    instance.socketIo.on(WsConnectStatusEnum.disconnect, () => {
+      console.log('【websocket】websocket连接断开', instance);
+      if (!instance) return;
+      instance.status = WsConnectStatusEnum.disconnect;
+      instance.update();
+    });
+
+    // 收到offer
+    instance.socketIo.on(WsMsgTypeEnum.offer, async (data: IOffer) => {
+      console.warn('【websocket】收到offer', data);
+      if (isSRS) return;
+      if (!instance) return;
+      if (data.data.receiver === getSocketId()) {
+        console.log('收到offer,这个offer是发给我的');
+        const rtc = await startNewWebRtc(data.data.sender);
+        if (rtc) {
+          await rtc.setRemoteDescription(data.data.sdp);
+          const sdp = await rtc.createAnswer();
+          await rtc.setLocalDescription(sdp);
+          instance.send({
+            msgType: WsMsgTypeEnum.answer,
+            data: { sdp, sender: getSocketId(), receiver: data.data.sender },
+          });
+        }
+      } else {
+        console.log('收到offer,但是这个offer不是发给我的');
+      }
+    });
+
+    // 收到answer
+    instance.socketIo.on(WsMsgTypeEnum.answer, async (data: IOffer) => {
+      console.warn('【websocket】收到answer', data);
+      if (isSRS) return;
+      if (isDone.value) return;
+      if (!instance) return;
+      const rtc = networkStore.getRtcMap(`${roomId.value}___${data.socketId}`);
+      if (!rtc) return;
+      rtc.rtcStatus.answer = true;
+      rtc.update();
+      if (data.data.receiver === getSocketId()) {
+        console.log('收到answer,这个answer是发给我的');
+        await rtc.setRemoteDescription(data.data.sdp);
+      } else {
+        console.log('收到answer,但这个answer不是发给我的');
+      }
+    });
+
+    // 收到candidate
+    instance.socketIo.on(WsMsgTypeEnum.candidate, (data: ICandidate) => {
+      console.warn('【websocket】收到candidate', data);
+      if (isSRS) return;
+      if (isDone.value) return;
+      if (!instance) return;
+      const rtc = networkStore.getRtcMap(`${roomId.value}___${data.socketId}`);
+      if (!rtc) return;
+      if (data.socketId !== getSocketId()) {
+        console.log('不是我发的candidate');
+        const candidate = new RTCIceCandidate({
+          sdpMid: data.data.sdpMid,
+          sdpMLineIndex: data.data.sdpMLineIndex,
+          candidate: data.data.candidate,
+        });
+        rtc.peerConnection
+          ?.addIceCandidate(candidate)
+          .then(() => {
+            console.log('candidate成功');
+          })
+          .catch((err) => {
+            console.error('candidate失败', err);
+          });
+      } else {
+        console.log('是我发的candidate');
+      }
+    });
+
+    // 当前所有在线用户
+    instance.socketIo.on(WsMsgTypeEnum.roomLiveing, (data: IAdminIn) => {
+      console.log('【websocket】收到管理员正在直播', data);
+    });
+
+    // 当前所有在线用户
+    instance.socketIo.on(WsMsgTypeEnum.liveUser, () => {
+      console.log('【websocket】当前所有在线用户');
+      if (!instance) return;
+    });
+
+    // 收到用户发送消息
+    instance.socketIo.on(WsMsgTypeEnum.message, (data) => {
+      console.log('【websocket】收到用户发送消息', data);
+      if (!instance) return;
+      damuList.value.push({
+        socketId: data.socketId,
+        msgType: DanmuMsgTypeEnum.danmu,
+        msg: data.data.msg,
+      });
+    });
+
+    // 用户加入房间完成
+    instance.socketIo.on(WsMsgTypeEnum.joined, (data) => {
+      console.log('【websocket】用户加入房间完成', data);
+      joined.value = true;
+      liveUserList.value.push({
+        socketId: `${getSocketId()}`,
+      });
+      if (isSRS) {
+        startNewWebRtc();
+      } else {
+        batchSendOffer();
+      }
+    });
+
+    // 其他用户加入房间
+    instance.socketIo.on(WsMsgTypeEnum.otherJoin, (data) => {
+      console.log('【websocket】其他用户加入房间', data);
+      liveUserList.value.push({
+        socketId: data.data.socketId,
+      });
+      damuList.value.push({
+        socketId: data.data.socketId,
+        userInfo: data.data.userInfo,
+        msgType: DanmuMsgTypeEnum.otherJoin,
+        msg: '',
+      });
+      if (isSRS) return;
+      if (joined.value) {
+        batchSendOffer();
+      }
+    });
+
+    // 用户离开房间
+    instance.socketIo.on(WsMsgTypeEnum.leave, (data) => {
+      console.log('【websocket】用户离开房间', data);
+      if (!instance) return;
+      instance.socketIo?.emit(WsMsgTypeEnum.leave, {
+        roomId: instance.roomId,
+      });
+    });
+
+    // 用户离开房间完成
+    instance.socketIo.on(WsMsgTypeEnum.leaved, (data) => {
+      console.log('【websocket】用户离开房间完成', data);
+      const res = liveUserList.value.filter(
+        (item) => item.socketId !== data.socketId
+      );
+      liveUserList.value = res;
+      damuList.value.push({
+        socketId: data.socketId,
+        msgType: DanmuMsgTypeEnum.userLeaved,
+        msg: '',
+      });
+    });
+  }
+
+  function roomNameIsOk() {
+    if (!roomName.value.length) {
+      window.$message.warning('请输入房间名!');
+      return false;
+    }
+    if (roomName.value.length < 3 || roomName.value.length > 10) {
+      window.$message.warning('房间名要求3-10个字符!');
+      return false;
+    }
+    return true;
+  }
+
+  /** 摄像头 */
+  async function startGetUserMedia() {
+    if (!localStream.value) {
+      // WARN navigator.mediaDevices在localhost和https才能用,http://192.168.1.103:8000局域网用不了
+      const event = await navigator.mediaDevices.getUserMedia({
+        video: true,
+        audio: true,
+      });
+      console.log('getUserMedia成功', event);
+      currMediaType.value = allMediaTypeList[MediaTypeEnum.camera];
+      currMediaTypeList.value.push(allMediaTypeList[MediaTypeEnum.camera]);
+      if (!localVideoRef.value) return;
+      localVideoRef.value.srcObject = event;
+      localStream.value = event;
+    }
+  }
+
+  /** 窗口 */
+  async function startGetDisplayMedia() {
+    if (!localStream.value) {
+      // WARN navigator.mediaDevices.getDisplayMedia在localhost和https才能用,http://192.168.1.103:8000局域网用不了
+      const event = await navigator.mediaDevices.getDisplayMedia({
+        video: true,
+        audio: true,
+      });
+      const audio = event.getAudioTracks();
+      const video = event.getVideoTracks();
+      track.audio = !!audio.length;
+      track.video = !!video.length;
+      console.log('getDisplayMedia成功', event);
+      currMediaType.value = allMediaTypeList[MediaTypeEnum.screen];
+      currMediaTypeList.value.push(allMediaTypeList[MediaTypeEnum.screen]);
+      if (!localVideoRef.value) return;
+      localVideoRef.value.srcObject = event;
+      localStream.value = event;
+    }
+  }
+  function keydownDanmu(event: KeyboardEvent) {
+    const key = event.key.toLowerCase();
+    if (key === 'enter') {
+      event.preventDefault();
+      sendDanmu();
+    }
+  }
+
+  function confirmRoomName() {
+    if (!roomNameIsOk()) return;
+    disabled.value = true;
+  }
+
+  function sendDanmu() {
+    if (!danmuStr.value.length) {
+      window.$message.warning('请输入弹幕内容!');
+      return;
+    }
+    const instance = networkStore.wsMap.get(roomId.value);
+    if (!instance) {
+      window.$message.error('还没开播,不能发送弹幕');
+      return;
+    }
+    instance.send({
+      msgType: WsMsgTypeEnum.message,
+      data: { msg: danmuStr.value },
+    });
+    damuList.value.push({
+      socketId: getSocketId(),
+      msgType: DanmuMsgTypeEnum.danmu,
+      msg: danmuStr.value,
+    });
+    danmuStr.value = '';
+  }
+
+  /** 结束直播 */
+  function endLive() {
+    disabled.value = false;
+    closeRtc();
+    currMediaTypeList.value = [];
+    localStream.value = null;
+    localVideoRef.value!.srcObject = null;
+    const instance = networkStore.wsMap.get(roomId.value);
+    if (!instance) return;
+    instance.send({
+      msgType: WsMsgTypeEnum.roomNoLive,
+      data: {},
+    });
+    setTimeout(() => {
+      instance.close();
+    }, 500);
+  }
+  async function getAllMediaDevices() {
+    const res = await navigator.mediaDevices.enumerateDevices();
+    // const audioInput = res.filter(
+    //   (item) => item.kind === 'audioinput' && item.deviceId !== 'default'
+    // );
+    // const videoInput = res.filter(
+    //   (item) => item.kind === 'videoinput' && item.deviceId !== 'default'
+    // );
+    return res;
+  }
+
+  async function initPush() {
+    router.push({ query: { ...route.query, roomId: roomId.value } });
+    const all = await getAllMediaDevices();
+    allMediaTypeList[MediaTypeEnum.camera] = {
+      txt: all.find((item) => item.kind === 'videoinput')?.label || '摄像头',
+      type: MediaTypeEnum.camera,
+    };
+    localVideoRef.value.addEventListener('loadstart', () => {
+      console.warn('视频流-loadstart');
+      const rtc = networkStore.getRtcMap(roomId.value);
+      if (!rtc) return;
+      rtc.rtcStatus.loadstart = true;
+      rtc.update();
+    });
+
+    localVideoRef.value.addEventListener('loadedmetadata', () => {
+      console.warn('视频流-loadedmetadata');
+      const rtc = networkStore.getRtcMap(roomId.value);
+      if (!rtc) return;
+      rtc.rtcStatus.loadedmetadata = true;
+      rtc.update();
+      if (isSRS) return;
+      if (joined.value) {
+        batchSendOffer();
+      }
+    });
+  }
+
+  return {
+    initPush,
+    confirmRoomName,
+    getSocketId,
+    startGetDisplayMedia,
+    startGetUserMedia,
+    startLive,
+    endLive,
+    closeWs,
+    closeRtc,
+    sendDanmu,
+    keydownDanmu,
+    disabled,
+    danmuStr,
+    roomName,
+    damuList,
+    liveUserList,
+    currMediaTypeList,
+  };
+}

+ 92 - 4
src/interface.ts

@@ -1,4 +1,92 @@
 // 这里放项目里面的类型
+
+export enum liveTypeEnum {
+  webrtcPull = 'webrtcPull',
+  srsWebrtcPull = 'srsWebrtcPull',
+  srsFlvPull = 'srsFlvPull',
+  srsPush = 'srsPush',
+  webrtcPush = 'webrtcPush',
+}
+
+export interface BilldHtmlWebpackPluginLog {
+  pkgName: string;
+  pkgVersion: string;
+  pkgRepository: string;
+  commitSubject: string;
+  commitBranch: string;
+  committerDate: string;
+  commitHash: string;
+  committerName: string;
+  committerEmail: string;
+  lastBuildDate: string;
+}
+
+export enum PlatformEnum {
+  qqLogin = 'qq_login',
+}
+
+export interface IAuth {
+  id?: number;
+  auth_name?: string;
+  auth_value?: string;
+  type?: number;
+  priority?: number | string;
+  p_id?: number | null;
+  created_at?: string;
+  updated_at?: string;
+  deleted_at?: null;
+  c_auths?: number[];
+}
+
+export interface IRole {
+  id?: number;
+  role_name?: string;
+  role_value?: string;
+  type?: number;
+  priority?: number | string;
+  p_id?: number | null;
+  created_at?: string;
+  updated_at?: string;
+  deleted_at?: null;
+  role_auths?: number[];
+  c_roles?: number[];
+}
+export interface IUser {
+  id?: number;
+  username?: string;
+  password?: string;
+  status?: number;
+  avatar?: string;
+  desc?: string;
+  token?: string;
+  user_roles?: number[];
+  created_at?: string;
+  updated_at?: string;
+  deleted_at?: string;
+  qq_users?: IQqUser[];
+}
+
+export interface IQqUser {
+  id?: number;
+  client_id?: number;
+  openid?: string;
+  unionid?: string;
+  username?: string;
+  figureurl?: string;
+  figureurl_1?: string;
+  figureurl_2?: string;
+  figureurl_qq_1?: string;
+  figureurl_qq_2?: string;
+  constellation?: string;
+  gender?: string;
+  city?: string;
+  province?: string;
+  year?: string;
+  created_at?: string;
+  updated_at?: string;
+  deleted_at?: any;
+}
+
 export interface ILive {
   id?: number;
   system?: number;
@@ -15,7 +103,7 @@ export interface ILive {
   deleted_at?: string;
 }
 
-export enum LiveTypeEnum {
+export enum MediaTypeEnum {
   camera,
   screen,
 }
@@ -28,14 +116,14 @@ export enum DanmuMsgTypeEnum {
 
 export interface ILiveUser {
   socketId: string;
-  avatar: string;
-  expr: number;
+  userInfo?: IUser;
 }
 
 export interface IDanmu {
-  socketId: string;
   msgType: DanmuMsgTypeEnum;
   msg: string;
+  socketId: string;
+  userInfo?: IUser;
 }
 
 export interface IAdminIn {

+ 19 - 0
src/layout/footer/index.vue

@@ -0,0 +1,19 @@
+<template>
+  <p class="footer-wrap">最近更新:{{ billd.lastBuildDate }}</p>
+</template>
+
+<script lang="ts" setup>
+import { BilldHtmlWebpackPluginLog } from '@/interface';
+
+// @ts-ignore
+const billd: BilldHtmlWebpackPluginLog = process.env.BilldHtmlWebpackPlugin;
+</script>
+
+<style lang="scss" scoped>
+.footer-wrap {
+  position: fixed;
+  bottom: 0;
+  left: 50%;
+  transform: translateX(-50%);
+}
+</style>

+ 174 - 117
src/layout/head/index.vue

@@ -8,14 +8,24 @@
         Billd直播
       </div>
       <div class="nav">
+        <a
+          :class="{
+            item: 1,
+            active: router.currentRoute.value.name === routerName.rank,
+          }"
+          href="/rank"
+          @click.prevent="router.push({ name: routerName.rank })"
+        >
+          排行榜
+        </a>
         <div
-          v-for="(item, index) in pushList.filter(
-            (item) => router.currentRoute.value.name === item.routerName
+          v-for="(item, index) in navLeftList.filter(
+            (item) => router.currentRoute.value.query.liveType === item.liveType
           )"
           :key="index"
           :class="{
             item: 1,
-            active: router.currentRoute.value.name === item.routerName,
+            active: router.currentRoute.value.query.liveType === item.liveType,
           }"
           @click="goPushPage(item.routerName)"
         >
@@ -23,71 +33,69 @@
         </div>
         <div
           v-for="(item, index) in pullList.filter(
-            (item) => router.currentRoute.value.name === item.routerName
+            (item) => router.currentRoute.value.query.liveType === item.liveType
           )"
           :key="index"
           :class="{
             item: 1,
-            active: router.currentRoute.value.name === item.routerName,
+            active: router.currentRoute.value.query.liveType === item.liveType,
           }"
         >
           {{ item.title }}
         </div>
       </div>
     </div>
-    <!-- <div class="search">
-      <input
-        class="ipt"
-        type="text"
-        placeholder="搜索"
-      />
-    </div> -->
     <div class="right">
+      <div
+        v-if="!userStore.userInfo"
+        class="qqlogin"
+        @click="useQQLogin()"
+      >
+        登录
+      </div>
+      <n-dropdown
+        v-else
+        trigger="hover"
+        :options="userOptions"
+        @select="handleUserSelect"
+      >
+        <div
+          class="qqlogin"
+          :style="{ backgroundImage: `url(${userStore.userInfo.avatar})` }"
+          @click="useQQLogin()"
+        ></div>
+      </n-dropdown>
+
       <a
-        class="github-btn"
+        class="sponsors"
+        href="/sponsors"
+        @click.prevent="router.push({ name: routerName.sponsors })"
+      >
+        赞助
+      </a>
+      <a
+        class="bilibili"
+        target="_blank"
+        href="https://space.bilibili.com/381307133/channel/seriesdetail?sid=3285689"
+      >
+        b站视频
+      </a>
+      <a
+        class="github"
         target="_blank"
         href="https://github.com/galaxy-s10/billd-live"
       >
-        <img
-          :src="githubStar"
-          alt=""
-        />
+        github
       </a>
 
-      <!-- <iframe
-        src="https://ghbtns.com/github-btn.html?user=galaxy-s10&repo=billd-live&type=fork&count=true&v=2"
-        frameborder="0"
-        scrolling="0"
-        width="105px"
-        height="21px"
-      ></iframe> -->
-
-      <div
-        v-if="router.currentRoute.value.name !== routerName.sponsors"
-        class="sponsors"
-        @click="router.push({ name: routerName.sponsors })"
-      >
-        赞助支持
-      </div>
-      <div
-        v-if="![routerName.webrtcPush].includes(router.currentRoute.value.name as string)"
-        class="btn ani"
-        @click="goPushPage(routerName.webrtcPush)"
-      >
-        webrtc开播
-      </div>
-      <div
-        v-if="![routerName.srsWebRtcPush].includes(router.currentRoute.value.name as string)"
-        class="btn ani"
-        @click="goPushPage(routerName.srsWebRtcPush)"
+      <n-dropdown
+        v-if="router.currentRoute.value.name !== routerName.push"
+        trigger="hover"
+        :options="options"
+        @select="handlePushSelect"
       >
-        srs-webrtc开播
-      </div>
-      <!-- <div
-        v-if="![routerName.srsWebRtcPush].includes(router.currentRoute.value.name as string)"
-        class="btn qqlogin"
-        @click="goPushPage(routerName.srsWebRtcPush)"
-      ></div> -->
+        <div class="start-live">我要开播</div>
+      </n-dropdown>
     </div>
   </div>
 </template>
@@ -97,24 +105,76 @@ import { openToTarget } from 'billd-utils';
 import { onMounted, ref } from 'vue';
 import { useRouter } from 'vue-router';
 
+import { useQQLogin } from '@/hooks/use-login';
+import { liveTypeEnum } from '@/interface';
 import { routerName } from '@/router';
+import { useUserStore } from '@/store/user';
 
 const router = useRouter();
-const githubStar = ref('');
-const pushList = ref([
-  { title: 'Webrtc Push', routerName: routerName.webrtcPush },
-  { title: 'SRS WebRTC Push', routerName: routerName.srsWebRtcPush },
+const userStore = useUserStore();
+const navLeftList = ref([
+  {
+    title: 'Webrtc Push',
+    routerName: routerName.push,
+    liveType: liveTypeEnum.webrtcPush,
+  },
+  {
+    title: 'SRS WebRTC Push',
+    routerName: routerName.push,
+    liveType: liveTypeEnum.srsPush,
+  },
 ]);
 
 const pullList = ref([
-  { title: 'Webrtc Pull', routerName: routerName.webrtcPull },
-  { title: 'SRS WebRTC Pull', routerName: routerName.srsWebRtcPull },
+  {
+    title: 'Webrtc Pull',
+    routerName: routerName.pull,
+    liveType: liveTypeEnum.webrtcPull,
+  },
+  {
+    title: 'SRS WebRTC Pull Flv',
+    routerName: routerName.pull,
+    liveType: liveTypeEnum.srsFlvPull,
+  },
+  {
+    title: 'SRS WebRTC Pull',
+    routerName: routerName.pull,
+    liveType: liveTypeEnum.srsWebrtcPull,
+  },
 ]);
 
-onMounted(() => {
-  githubStar.value =
-    'https://img.shields.io/github/stars/galaxy-s10/billd-live?label=Star&logo=GitHub&labelColor=white&logoColor=black&style=social&cacheSeconds=3600';
-});
+const userOptions = ref([
+  {
+    label: '退出',
+    key: '1',
+  },
+]);
+
+const options = ref([
+  {
+    label: 'webrtc开播',
+    key: liveTypeEnum.webrtcPush,
+  },
+  {
+    label: 'srs-webrtc开播',
+    key: liveTypeEnum.srsPush,
+  },
+]);
+
+function handleUserSelect(key) {
+  if (key === '1') {
+    userStore.logout();
+  }
+}
+function handlePushSelect(key) {
+  const url = router.resolve({
+    name: routerName.push,
+    query: { liveType: key },
+  });
+  openToTarget(url.href);
+}
+
+onMounted(() => {});
 
 function goPushPage(routerName: string) {
   const url = router.resolve({ name: routerName });
@@ -150,13 +210,16 @@ function goPushPage(routerName: string) {
       .item {
         position: relative;
         padding: 0 10px;
+        color: black;
+        text-decoration: none;
         cursor: pointer;
+
         &.active {
           &::after {
             position: absolute;
             bottom: -6px;
             left: 50%;
-            width: 50%;
+            width: 40% !important;
             height: 2px;
             background-color: red;
             content: '';
@@ -165,71 +228,75 @@ function goPushPage(routerName: string) {
           }
         }
         &::after {
-          position: absolute;
-          bottom: -6px;
-          left: 50%;
-          width: 0px;
-          height: 2px;
-          background-color: red;
-          content: '';
-          transition: all 0.1s ease;
-          transform: translateX(-50%);
+          width: 0px !important;
+
+          @extend .active;
         }
         &:hover {
           &::after {
-            width: 50%;
+            width: 40% !important;
           }
         }
       }
     }
   }
-  // .search {
-  //   flex: 1;
-
-  //   .ipt {
-  //     display: block;
-  //     box-sizing: border-box;
-  //     margin: 0 auto;
-  //     padding: 10px 20px;
-  //     min-width: 200px;
-  //     outline: none;
-  //     border: 1px solid hsla(0, 0%, 60%, 0.2);
-  //     border-radius: 8px;
-  //     border-radius: 10px;
-  //     background-color: #f1f2f3;
-  //     font-size: 14px;
-  //   }
-  // }
   .right {
     display: flex;
     align-items: center;
     margin-right: 20px;
 
-    @keyframes big-small {
-      0%,
-      100% {
-        transform: scale(1);
-      }
-      50% {
-        transform: scale(0.9);
-      }
-    }
-
-    .github-btn {
-      margin-right: 10px;
-      img {
-        display: block;
-      }
+    .qqlogin {
+      box-sizing: border-box;
+      margin-right: 15px;
+      width: 35px;
+      height: 35px;
+      border-radius: 50%;
+      background-color: skyblue;
+      background-position: center;
+      background-size: cover;
+      background-repeat: no-repeat;
+      color: white;
+      text-align: center;
+      font-size: 13px;
+      line-height: 35px;
+      cursor: pointer;
     }
 
-    .sponsors {
-      margin-right: 10px;
-      padding: 5px 10px;
+    .sponsors,
+    .bilibili,
+    .github {
+      position: relative;
+      margin-right: 15px;
       border-radius: 6px;
+      color: black;
+      text-decoration: none;
       font-size: 14px;
       cursor: pointer;
+      &.active {
+        &::after {
+          position: absolute;
+          bottom: -6px;
+          left: 50%;
+          width: 40%;
+          height: 2px;
+          background-color: red;
+          content: '';
+          transition: all 0.1s ease;
+          transform: translateX(-50%);
+        }
+      }
+      &::after {
+        width: 0px !important;
+
+        @extend .active;
+      }
+      &:hover {
+        &::after {
+          width: 40% !important;
+        }
+      }
     }
-    .btn {
+    .start-live {
       margin-right: 10px;
       padding: 5px 10px;
       border-radius: 6px;
@@ -237,16 +304,6 @@ function goPushPage(routerName: string) {
       color: white;
       font-size: 14px;
       cursor: pointer;
-      &.ani {
-        animation: big-small 1s ease infinite;
-      }
-      &.qqlogin {
-        width: 76px;
-        height: 24px;
-        background-color: none;
-
-        @include setBackground('@/assets/img/qq_login.png');
-      }
     }
   }
 }

+ 2 - 0
src/layout/index.vue

@@ -4,10 +4,12 @@
     <router-view v-slot="{ Component }">
       <component :is="Component"></component>
     </router-view>
+    <FooterCpt></FooterCpt>
   </div>
 </template>
 
 <script lang="ts" setup>
+import FooterCpt from './footer/index.vue';
 import HeadCpt from './head/index.vue';
 </script>
 

+ 6 - 0
src/main.ts

@@ -5,6 +5,7 @@ import './showBilldVersion';
 import { createApp } from 'vue';
 import adapter from 'webrtc-adapter';
 
+import Message from '@/components/Message/index.vue';
 import router from '@/router/index';
 import store from '@/store/index';
 
@@ -17,4 +18,9 @@ const app = createApp(App);
 app.use(store);
 app.use(router);
 
+const message = createApp(Message);
+const messageEle = document.createElement('div');
+const appEl = document.getElementById('app');
+appEl?.appendChild(messageEle);
+message.mount(messageEle);
 app.mount('#app');

+ 11 - 0
src/network/webSocket.ts

@@ -44,6 +44,10 @@ export enum WsMsgTypeEnum {
   sendBlob = 'sendBlob',
   /** getLiveUser */
   getLiveUser = 'getLiveUser',
+  /** updateJoinInfo */
+  updateJoinInfo = 'updateJoinInfo',
+  /** heartbeat */
+  heartbeat = 'heartbeat',
   offer = 'offer',
   answer = 'answer',
   candidate = 'candidate',
@@ -73,6 +77,13 @@ export class WebSocketClass {
   // 发送websocket消息
   send = ({ msgType, data }: { msgType: WsMsgTypeEnum; data?: any }) => {
     console.log('【websocket】发送websocket消息', msgType, data);
+    if (!this.socketIo?.connected) {
+      console.error(
+        '【websocket】未连接成功,不发送websocket消息!',
+        msgType,
+        data
+      );
+    }
     this.socketIo?.emit(msgType, {
       roomId: this.roomId,
       socketId: this.socketIo.id,

+ 70 - 253
src/views/srs-webrtc-pull/index.vue → src/old/srs-flv-pull/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="srt-webrtc-pull-wrap">
+  <div class="srs-flv-pull-wrap">
     <template v-if="roomNoLive">当前房间没在直播~</template>
     <template v-else>
       <div class="left">
@@ -28,9 +28,11 @@
             x5-video-player-type="h5"
             x5-video-player-fullscreen="true"
             x5-video-orientation="portraint"
-            muted
-            controls
+            :muted="appStore.muted"
           ></video>
+          <div class="controls">
+            <VideoControls></VideoControls>
+          </div>
         </div>
         <div
           ref="bottomRef"
@@ -51,19 +53,33 @@
         <div class="tab">
           <span>在线用户</span>
           <span> | </span>
-          <span>大航海</span>
+          <span>排行榜</span>
         </div>
         <div class="user-list">
           <div
-            v-for="(item, index) in liveUserList"
+            v-for="(item, index) in liveUserList.filter((item) =>
+              userStore.userInfo ? item.socketId !== getSocketId() : true
+            )"
             :key="index"
             class="item"
           >
             <div class="info">
               <div class="avatar"></div>
-              <div class="nickname">{{ item.socketId }}</div>
+              <div class="username">{{ item.socketId }}</div>
+            </div>
+          </div>
+          <div
+            v-if="userStore.userInfo"
+            class="item"
+          >
+            <div class="info">
+              <img
+                :src="userStore.userInfo.avatar"
+                class="avatar"
+                alt=""
+              />
+              <div class="username">{{ userStore.userInfo.username }}</div>
             </div>
-            <div class="expr">{{ item.expr }}</div>
           </div>
         </div>
         <div class="danmu-list">
@@ -73,16 +89,22 @@
             class="item"
           >
             <template v-if="item.msgType === DanmuMsgTypeEnum.danmu">
-              <span class="name">{{ item.socketId }}:</span>
+              <span class="name">
+                {{ item.userInfo?.username || item.socketId }}:
+              </span>
               <span class="msg">{{ item.msg }}</span>
             </template>
             <template v-else-if="item.msgType === DanmuMsgTypeEnum.otherJoin">
               <span class="name system">系统通知:</span>
-              <span class="msg">{{ item.socketId }}进入直播!</span>
+              <span class="msg">
+                {{ item.userInfo?.username || item.socketId }}进入直播!
+              </span>
             </template>
             <template v-else-if="item.msgType === DanmuMsgTypeEnum.userLeaved">
               <span class="name system">系统通知:</span>
-              <span class="msg">{{ item.socketId }}离开直播!</span>
+              <span class="msg">
+                {{ item.userInfo?.username || item.socketId }}离开直播!
+              </span>
             </template>
           </div>
         </div>
@@ -90,6 +112,7 @@
           <textarea
             v-model="danmuStr"
             class="ipt"
+            @keydown="keydownDanmu"
           ></textarea>
           <div
             class="btn"
@@ -104,224 +127,38 @@
 </template>
 
 <script lang="ts" setup>
-import { getRandomString } from 'billd-utils';
-import { onMounted, onUnmounted, reactive, ref } from 'vue';
-import { useRoute } from 'vue-router';
+import { onMounted, onUnmounted, ref } from 'vue';
 
-import { fetchRtcV1Play } from '@/api/srs';
-import { DanmuMsgTypeEnum, IAdminIn, IDanmu, ILiveUser } from '@/interface';
-import { SRSWebRTCClass } from '@/network/srsWebRtc';
-import {
-  WebSocketClass,
-  WsConnectStatusEnum,
-  WsMsgTypeEnum,
-} from '@/network/webSocket';
-import { useNetworkStore } from '@/store/network';
+import { usePull } from '@/hooks/use-pull';
+import { DanmuMsgTypeEnum } from '@/interface';
+import { useAppStore } from '@/store/app';
+import { useUserStore } from '@/store/user';
 
-const networkStore = useNetworkStore();
-const route = useRoute();
+const userStore = useUserStore();
+const appStore = useAppStore();
 
 const topRef = ref<HTMLDivElement>();
 const bottomRef = ref<HTMLDivElement>();
 const localVideoRef = ref<HTMLVideoElement>();
-const track = reactive({
-  audio: true,
-  video: true,
-});
-const streamurl = ref();
-const roomNoLive = ref(false);
-const roomId = ref('');
-const roomName = ref('');
-const danmuStr = ref('');
-const websocketInstant = ref<WebSocketClass>();
-const damuList = ref<IDanmu[]>([]);
-const liveUserList = ref<ILiveUser[]>([]);
-const giftList = ref([
-  { name: '鲜花', ico: '', price: '免费' },
-  { name: '肥宅水', ico: '', price: '2元' },
-  { name: '小鸡腿', ico: '', price: '3元' },
-  { name: '大鸡腿', ico: '', price: '5元' },
-  { name: '一杯咖啡', ico: '', price: '10元' },
-]);
-
-function closeWs() {
-  const instance = networkStore.wsMap.get(roomId.value);
-  if (!instance) return;
-  instance.close();
-}
-
-function closeRtc() {
-  networkStore.rtcMap.forEach((rtc) => {
-    rtc.close();
-  });
-}
-
-function getSocketId() {
-  return networkStore.wsMap.get(roomId.value!)?.socketIo?.id || '-1';
-}
-
-function sendJoin() {
-  const instance = networkStore.wsMap.get(roomId.value);
-  if (!instance) return;
-  instance.send({ msgType: WsMsgTypeEnum.join, data: {} });
-}
-
-function sendDanmu() {
-  if (!danmuStr.value.length) {
-    alert('请输入弹幕内容!');
-  }
-  if (!websocketInstant.value) return;
-  websocketInstant.value.send({
-    msgType: WsMsgTypeEnum.message,
-    data: { msg: danmuStr.value },
-  });
-  damuList.value.push({
-    socketId: getSocketId(),
-    msgType: DanmuMsgTypeEnum.danmu,
-    msg: danmuStr.value,
-  });
-  danmuStr.value = '';
-}
-
-function startNewWebRtc(receiver: string) {
-  console.warn('开始new SRSWebRTCClass', receiver);
-  const rtc = new SRSWebRTCClass({ roomId: `${roomId.value}___${receiver}` });
-  rtc.rtcStatus.joined = true;
-  rtc.update();
-  return rtc;
-}
 
-async function handleSrsPlay() {
-  const rtc = startNewWebRtc(getSocketId());
-  if (track.video) {
-    rtc.peerConnection?.addTransceiver('video', { direction: 'recvonly' });
-  }
-  if (track.audio) {
-    rtc.peerConnection?.addTransceiver('audio', { direction: 'recvonly' });
-  }
-  try {
-    const offer = await rtc.createOffer();
-    if (!offer) return;
-    await rtc.setLocalDescription(offer);
-    const res: any = await fetchRtcV1Play({
-      api: `${
-        process.env.NODE_ENV === 'development'
-          ? 'http://localhost:1985'
-          : 'https://live.hsslive.cn/srs'
-      }/rtc/v1/play/`,
-      clientip: null,
-      sdp: offer.sdp!,
-      streamurl: streamurl.value,
-      tid: getRandomString(10),
-    });
-    await rtc.setRemoteDescription(
-      new RTCSessionDescription({ type: 'answer', sdp: res.sdp })
-    );
-  } catch (error) {
-    console.log(error);
-  }
-}
-
-function initReceive() {
-  const instance = websocketInstant.value;
-  if (!instance?.socketIo) return;
-  // websocket连接成功
-  instance.socketIo.on(WsConnectStatusEnum.connect, () => {
-    console.log('【websocket】websocket连接成功', instance.socketIo?.id);
-    if (!instance) return;
-    instance.status = WsConnectStatusEnum.connect;
-    instance.update();
-  });
-
-  // websocket连接断开
-  instance.socketIo.on(WsConnectStatusEnum.disconnect, () => {
-    console.log('【websocket】websocket连接断开', instance);
-    if (!instance) return;
-    instance.status = WsConnectStatusEnum.disconnect;
-    instance.update();
-  });
-
-  // 当前所有在线用户
-  instance.socketIo.on(WsMsgTypeEnum.roomLiveing, (data: IAdminIn) => {
-    console.log('【websocket】收到管理员正在直播', data);
-    handleSrsPlay();
-  });
-
-  // 当前所有在线用户
-  instance.socketIo.on(WsMsgTypeEnum.roomNoLive, (data: IAdminIn) => {
-    console.log('【websocket】收到管理员不在直播', data);
-    roomNoLive.value = true;
-    closeRtc();
-  });
-
-  // 当前所有在线用户
-  instance.socketIo.on(WsMsgTypeEnum.liveUser, (data) => {
-    console.log('【websocket】当前所有在线用户', data);
-    if (!instance) return;
-    liveUserList.value = data.map((item) => ({
-      avatar: 'red',
-      socketId: item.id,
-      expr: 1,
-    }));
-  });
-
-  // 收到用户发送消息
-  instance.socketIo.on(WsMsgTypeEnum.message, (data) => {
-    console.log('【websocket】收到用户发送消息', data);
-    if (!instance) return;
-    damuList.value.push({
-      socketId: data.socketId,
-      msgType: DanmuMsgTypeEnum.danmu,
-      msg: data.data.msg,
-    });
-  });
-
-  // 用户加入房间
-  instance.socketIo.on(WsMsgTypeEnum.joined, (data) => {
-    console.log('【websocket】用户加入房间完成', data);
-    roomName.value = data.roomName;
-    track.audio = data.track_audio;
-    track.video = data.track_video;
-    streamurl.value = data.streamurl;
-  });
-
-  // 其他用户加入房间
-  instance.socketIo.on(WsMsgTypeEnum.otherJoin, (data) => {
-    console.log('【websocket】其他用户加入房间', data);
-    damuList.value.push({
-      socketId: data.socketId,
-      msgType: DanmuMsgTypeEnum.otherJoin,
-      msg: '',
-    });
-  });
-
-  // 用户离开房间
-  instance.socketIo.on(WsMsgTypeEnum.leave, (data) => {
-    console.log('【websocket】用户离开房间', data);
-    if (!instance) return;
-    instance.socketIo?.emit(WsMsgTypeEnum.leave, {
-      roomId: instance.roomId,
-    });
-  });
-
-  // 用户离开房间完成
-  instance.socketIo.on(WsMsgTypeEnum.leaved, (data) => {
-    console.log('【websocket】用户离开房间完成', data);
-    if (!instance) return;
-    const res = liveUserList.value.filter(
-      (item) => item.socketId !== data.socketId
-    );
-    liveUserList.value = res;
-    damuList.value.push({
-      socketId: data.socketId,
-      msgType: DanmuMsgTypeEnum.userLeaved,
-      msg: '',
-    });
-  });
-}
+const {
+  initPull,
+  closeWs,
+  closeRtc,
+  getSocketId,
+  keydownDanmu,
+  sendDanmu,
+  roomName,
+  roomNoLive,
+  damuList,
+  giftList,
+  liveUserList,
+  danmuStr,
+} = usePull({ localVideoRef });
 
 onUnmounted(() => {
   closeWs();
+  closeRtc();
 });
 
 onMounted(() => {
@@ -332,40 +169,12 @@ onMounted(() => {
         topRef.value.getBoundingClientRect().height);
     localVideoRef.value.style.height = `${res}px`;
   }
-  roomId.value = route.params.roomId as string;
-  console.warn('开始new WebSocketClass');
-  websocketInstant.value = new WebSocketClass({
-    roomId: roomId.value,
-    url:
-      process.env.NODE_ENV === 'development'
-        ? 'ws://localhost:4300'
-        : 'wss://live.hsslive.cn',
-    isAdmin: false,
-  });
-  websocketInstant.value.update();
-  initReceive();
-  sendJoin();
-
-  localVideoRef.value?.addEventListener('loadstart', () => {
-    console.warn('视频流-loadstart');
-    const rtc = networkStore.getRtcMap(roomId.value);
-    if (!rtc) return;
-    rtc.rtcStatus.loadstart = true;
-    rtc.update();
-  });
-
-  localVideoRef.value?.addEventListener('loadedmetadata', () => {
-    console.warn('视频流-loadedmetadata');
-    const rtc = networkStore.getRtcMap(roomId.value);
-    if (!rtc) return;
-    rtc.rtcStatus.loadedmetadata = true;
-    rtc.update();
-  });
+  initPull();
 });
 </script>
 
 <style lang="scss" scoped>
-.srt-webrtc-pull-wrap {
+.srs-flv-pull-wrap {
   margin: 20px auto 0;
   min-width: $large-width;
   height: 700px;
@@ -376,10 +185,11 @@ onMounted(() => {
     box-sizing: border-box;
     width: $large-left-width;
     height: 100%;
-    border-radius: 10px;
+    border-radius: 6px;
     background-color: white;
     color: #9499a0;
     vertical-align: top;
+    overflow: hidden;
     .head {
       display: flex;
       justify-content: space-between;
@@ -445,13 +255,20 @@ onMounted(() => {
       }
     }
     .video-wrap {
-      // height: 100px;
-      // height: 550px;
+      position: relative;
       background-color: #18191c;
       #localVideo {
         max-width: 100%;
         max-height: 100%;
       }
+      .controls {
+        display: none;
+      }
+      &:hover {
+        .controls {
+          display: block;
+        }
+      }
     }
     .gift {
       position: absolute;
@@ -521,7 +338,7 @@ onMounted(() => {
             border-radius: 50%;
             background-color: skyblue;
           }
-          .nickname {
+          .username {
             color: black;
           }
         }
@@ -584,7 +401,7 @@ onMounted(() => {
 
 // 屏幕宽度小于$large-width的时候
 @media screen and (max-width: $large-width) {
-  .srt-webrtc-pull-wrap {
+  .srs-flv-pull-wrap {
     .left {
       width: $medium-left-width;
     }

+ 418 - 0
src/old/srs-webrtc-pull/index.vue

@@ -0,0 +1,418 @@
+<template>
+  <div class="srs-webrtc-pull-wrap">
+    <template v-if="roomNoLive">当前房间没在直播~</template>
+    <template v-else>
+      <div class="left">
+        <div
+          ref="topRef"
+          class="head"
+        >
+          <div class="info">
+            <div class="avatar"></div>
+            <div class="detail">
+              <div class="top">房间名:{{ roomName }}</div>
+              <div class="bottom">
+                <span>你的socketId:{{ getSocketId() }}</span>
+              </div>
+            </div>
+          </div>
+        </div>
+        <div class="video-wrap">
+          <video
+            id="localVideo"
+            ref="localVideoRef"
+            autoplay
+            webkit-playsinline="true"
+            playsinline
+            x-webkit-airplay="allow"
+            x5-video-player-type="h5"
+            x5-video-player-fullscreen="true"
+            x5-video-orientation="portraint"
+            :muted="appStore.muted"
+          ></video>
+          <div class="controls">
+            <VideoControls></VideoControls>
+          </div>
+        </div>
+        <div
+          ref="bottomRef"
+          class="gift"
+        >
+          <div
+            v-for="(item, index) in giftList"
+            :key="index"
+            class="item"
+          >
+            <div class="ico"></div>
+            <div class="name">{{ item.name }}</div>
+            <div class="price">{{ item.price }}</div>
+          </div>
+        </div>
+      </div>
+      <div class="right">
+        <div class="tab">
+          <span>在线用户</span>
+          <span> | </span>
+          <span>排行榜</span>
+        </div>
+        <div class="user-list">
+          <div
+            v-for="(item, index) in liveUserList.filter((item) =>
+              userStore.userInfo ? item.socketId !== getSocketId() : true
+            )"
+            :key="index"
+            class="item"
+          >
+            <div class="info">
+              <div class="avatar"></div>
+              <div class="username">{{ item.socketId }}</div>
+            </div>
+          </div>
+          <div
+            v-if="userStore.userInfo"
+            class="item"
+          >
+            <div class="info">
+              <img
+                :src="userStore.userInfo.avatar"
+                class="avatar"
+                alt=""
+              />
+              <div class="username">{{ userStore.userInfo.username }}</div>
+            </div>
+          </div>
+        </div>
+        <div class="danmu-list">
+          <div
+            v-for="(item, index) in damuList"
+            :key="index"
+            class="item"
+          >
+            <template v-if="item.msgType === DanmuMsgTypeEnum.danmu">
+              <span class="name">
+                {{ item.userInfo?.username || item.socketId }}:
+              </span>
+              <span class="msg">{{ item.msg }}</span>
+            </template>
+            <template v-else-if="item.msgType === DanmuMsgTypeEnum.otherJoin">
+              <span class="name system">系统通知:</span>
+              <span class="msg">
+                {{ item.userInfo?.username || item.socketId }}进入直播!
+              </span>
+            </template>
+            <template v-else-if="item.msgType === DanmuMsgTypeEnum.userLeaved">
+              <span class="name system">系统通知:</span>
+              <span class="msg">
+                {{ item.userInfo?.username || item.socketId }}离开直播!
+              </span>
+            </template>
+          </div>
+        </div>
+        <div class="send-msg">
+          <textarea
+            v-model="danmuStr"
+            class="ipt"
+            @keydown="keydownDanmu"
+          ></textarea>
+          <div
+            class="btn"
+            @click="sendDanmu"
+          >
+            发送
+          </div>
+        </div>
+      </div>
+    </template>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { onMounted, onUnmounted, ref } from 'vue';
+
+import { usePull } from '@/hooks/use-pull';
+import { DanmuMsgTypeEnum } from '@/interface';
+import { useAppStore } from '@/store/app';
+import { useUserStore } from '@/store/user';
+
+const userStore = useUserStore();
+const appStore = useAppStore();
+
+const topRef = ref<HTMLDivElement>();
+const bottomRef = ref<HTMLDivElement>();
+const localVideoRef = ref<HTMLVideoElement>();
+
+const {
+  initPull,
+  closeWs,
+  closeRtc,
+  getSocketId,
+  keydownDanmu,
+  sendDanmu,
+  roomName,
+  roomNoLive,
+  damuList,
+  giftList,
+  liveUserList,
+  danmuStr,
+} = usePull({ localVideoRef, isFlv: false, isSRS: true });
+
+onUnmounted(() => {
+  closeWs();
+  closeRtc();
+});
+
+onMounted(() => {
+  if (topRef.value && bottomRef.value && localVideoRef.value) {
+    const res =
+      bottomRef.value.getBoundingClientRect().top -
+      (topRef.value.getBoundingClientRect().top +
+        topRef.value.getBoundingClientRect().height);
+    localVideoRef.value.style.height = `${res}px`;
+  }
+  initPull();
+});
+</script>
+
+<style lang="scss" scoped>
+.srs-webrtc-pull-wrap {
+  margin: 20px auto 0;
+  min-width: $large-width;
+  height: 700px;
+  text-align: center;
+  .left {
+    position: relative;
+    display: inline-block;
+    box-sizing: border-box;
+    width: $large-left-width;
+    height: 100%;
+    border-radius: 6px;
+    background-color: white;
+    color: #9499a0;
+    vertical-align: top;
+    overflow: hidden;
+    .head {
+      display: flex;
+      justify-content: space-between;
+      padding: 20px;
+      background-color: papayawhip;
+      .tag {
+        display: inline-block;
+        margin-right: 5px;
+        padding: 1px 4px;
+        border: 1px solid;
+        border-radius: 2px;
+        color: #9499a0;
+        font-size: 12px;
+      }
+
+      .info {
+        display: flex;
+        align-items: center;
+        text-align: initial;
+
+        .avatar {
+          margin-right: 20px;
+          width: 64px;
+          height: 64px;
+          border-radius: 50%;
+          background-color: skyblue;
+        }
+        .detail {
+          .top {
+            margin-bottom: 10px;
+            color: #18191c;
+          }
+          .bottom {
+            font-size: 14px;
+          }
+        }
+      }
+      .other {
+        display: flex;
+        flex-direction: column;
+        justify-content: center;
+        font-size: 12px;
+        .top {
+          display: flex;
+          align-items: center;
+          .item {
+            display: flex;
+            align-items: center;
+            margin-right: 20px;
+            .ico {
+              display: inline-block;
+              margin-right: 4px;
+              width: 10px;
+              height: 10px;
+              border-radius: 50%;
+              background-color: skyblue;
+            }
+          }
+        }
+        .bottom {
+          margin-top: 10px;
+        }
+      }
+    }
+    .video-wrap {
+      position: relative;
+      background-color: #18191c;
+      #localVideo {
+        max-width: 100%;
+        max-height: 100%;
+      }
+      .controls {
+        display: none;
+      }
+      &:hover {
+        .controls {
+          display: block;
+        }
+      }
+    }
+    .gift {
+      position: absolute;
+      right: 0;
+      bottom: 0;
+      left: 0;
+      display: flex;
+      align-items: center;
+      justify-content: space-around;
+      height: 100px;
+      background-color: papayawhip;
+      .item {
+        margin-right: 10px;
+        text-align: center;
+
+        .ico {
+          width: 50px;
+          height: 50px;
+          background-color: skyblue;
+        }
+        .name {
+          color: #18191c;
+          font-size: 12px;
+        }
+        .price {
+          color: #9499a0;
+          font-size: 12px;
+        }
+      }
+    }
+  }
+  .right {
+    position: relative;
+    display: inline-block;
+    box-sizing: border-box;
+    margin-left: 10px;
+    min-width: 300px;
+    height: 100%;
+    border-radius: 6px;
+    background-color: papayawhip;
+    color: #9499a0;
+    .tab {
+      display: flex;
+      align-items: center;
+      justify-content: space-evenly;
+      padding: 5px 0;
+      font-size: 12px;
+    }
+    .user-list {
+      overflow-y: scroll;
+      padding: 0 15px;
+      height: 100px;
+      background-color: papayawhip;
+      .item {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        margin-bottom: 10px;
+        font-size: 12px;
+        .info {
+          display: flex;
+          align-items: center;
+          .avatar {
+            margin-right: 5px;
+            width: 25px;
+            height: 25px;
+            border-radius: 50%;
+            background-color: skyblue;
+          }
+          .username {
+            color: black;
+          }
+        }
+      }
+    }
+    .danmu-list {
+      overflow-y: scroll;
+      padding: 0 15px;
+      height: 450px;
+      text-align: initial;
+      .item {
+        margin-bottom: 10px;
+        font-size: 12px;
+        .name {
+          color: #9499a0;
+          &.system {
+            color: red;
+          }
+        }
+        .msg {
+          color: #61666d;
+        }
+      }
+    }
+    .send-msg {
+      position: absolute;
+      bottom: 15px;
+      box-sizing: border-box;
+      padding: 0 10px;
+      width: 100%;
+      .ipt {
+        display: block;
+        box-sizing: border-box;
+        margin: 0 auto;
+        padding: 10px;
+        width: 100%;
+        height: 60px;
+        outline: none;
+        border: 1px solid hsla(0, 0%, 60%, 0.2);
+        border-radius: 4px;
+        background-color: #f1f2f3;
+        font-size: 14px;
+      }
+      .btn {
+        box-sizing: border-box;
+        margin-top: 10px;
+        margin-left: auto;
+        padding: 5px;
+        width: 80px;
+        border-radius: 4px;
+        background-color: skyblue;
+        color: white;
+        text-align: center;
+        font-size: 12px;
+        cursor: pointer;
+      }
+    }
+  }
+}
+
+// 屏幕宽度小于$large-width的时候
+@media screen and (max-width: $large-width) {
+  .srs-webrtc-pull-wrap {
+    .left {
+      width: $medium-left-width;
+    }
+    .right {
+      .list {
+        .item {
+          width: 150px;
+          height: 80px;
+        }
+      }
+    }
+  }
+}
+</style>

+ 413 - 0
src/old/srs-webrtc-push/index.vue

@@ -0,0 +1,413 @@
+<template>
+  <div class="srs-webrtc-push-wrap">
+    <div
+      ref="topRef"
+      class="left"
+    >
+      <div class="video-wrap">
+        <video
+          id="localVideo"
+          ref="localVideoRef"
+          autoplay
+          webkit-playsinline="true"
+          playsinline
+          x-webkit-airplay="allow"
+          x5-video-player-type="h5"
+          x5-video-player-fullscreen="true"
+          x5-video-orientation="portraint"
+          muted
+        ></video>
+        <div
+          v-if="!currMediaTypeList || currMediaTypeList.length <= 0"
+          class="add-wrap"
+        >
+          <n-space>
+            <n-button
+              class="item"
+              @click="startGetUserMedia"
+            >
+              摄像头
+            </n-button>
+            <n-button
+              class="item"
+              @click="startGetDisplayMedia"
+            >
+              窗口
+            </n-button>
+          </n-space>
+        </div>
+      </div>
+      <div
+        ref="bottomRef"
+        class="control"
+      >
+        <div class="info">
+          <div
+            class="avatar"
+            :style="{ backgroundImage: `url(${userStore.userInfo?.avatar})` }"
+          ></div>
+          <div class="detail">
+            <div class="top">
+              <n-input-group>
+                <n-input
+                  v-model:value="roomName"
+                  size="small"
+                  placeholder="输入房间名"
+                  :style="{ width: '50%' }"
+                  :disabled="disabled"
+                />
+                <n-button
+                  size="small"
+                  type="primary"
+                  :disabled="disabled"
+                  @click="confirmRoomName"
+                >
+                  确定
+                </n-button>
+              </n-input-group>
+            </div>
+            <div class="bottom">
+              <span>socketId:{{ getSocketId() }}</span>
+            </div>
+          </div>
+        </div>
+        <div class="other">
+          <div class="top">
+            <span class="item">
+              <i class="ico"></i>
+              <span>正在观看人数:{{ liveUserList.length }}</span>
+            </span>
+          </div>
+          <div class="bottom">
+            <n-space>
+              <n-button
+                type="info"
+                size="small"
+                @click="startLive"
+              >
+                开始直播
+              </n-button>
+              <n-button
+                type="info"
+                size="small"
+                @click="endLive"
+              >
+                结束直播
+              </n-button>
+            </n-space>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="right">
+      <div class="resource-card">
+        <div class="title">素材列表</div>
+        <div class="list">
+          <div
+            v-for="(item, index) in currMediaTypeList"
+            :key="index"
+            class="item"
+          >
+            <span class="name">{{ item.txt }}</span>
+          </div>
+        </div>
+      </div>
+      <div class="danmu-card">
+        <div class="title">弹幕互动</div>
+        <div class="list-wrap">
+          <div class="list">
+            <div
+              v-for="(item, index) in damuList"
+              :key="index"
+              class="item"
+            >
+              <template v-if="item.msgType === DanmuMsgTypeEnum.danmu">
+                <span class="name">{{ item.socketId }}:</span>
+                <span class="msg">{{ item.msg }}</span>
+              </template>
+              <template v-else-if="item.msgType === DanmuMsgTypeEnum.otherJoin">
+                <span class="name system">系统通知:</span>
+                <span class="msg">{{ item.socketId }}进入直播!</span>
+              </template>
+              <template
+                v-else-if="item.msgType === DanmuMsgTypeEnum.userLeaved"
+              >
+                <span class="name system">系统通知:</span>
+                <span class="msg">{{ item.socketId }}离开直播!</span>
+              </template>
+            </div>
+          </div>
+        </div>
+        <div class="send-msg">
+          <input
+            v-model="danmuStr"
+            class="ipt"
+            @keydown="keydownDanmu"
+          />
+          <n-button
+            type="info"
+            size="small"
+            @click="sendDanmu"
+          >
+            发送
+          </n-button>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { onMounted, onUnmounted, ref } from 'vue';
+
+import { usePush } from '@/hooks/use-push';
+import { DanmuMsgTypeEnum } from '@/interface';
+import { useUserStore } from '@/store/user';
+
+const userStore = useUserStore();
+
+const topRef = ref<HTMLDivElement>();
+const bottomRef = ref<HTMLDivElement>();
+const localVideoRef = ref<HTMLVideoElement>();
+
+const {
+  initPush,
+  confirmRoomName,
+  getSocketId,
+  startGetDisplayMedia,
+  startGetUserMedia,
+  startLive,
+  endLive,
+  closeWs,
+  closeRtc,
+  sendDanmu,
+  keydownDanmu,
+  disabled,
+  danmuStr,
+  roomName,
+  damuList,
+  liveUserList,
+  currMediaTypeList,
+} = usePush({
+  localVideoRef,
+  isSRS: true,
+});
+
+onUnmounted(() => {
+  closeWs();
+  closeRtc();
+});
+
+onMounted(() => {
+  initPush();
+  if (topRef.value && bottomRef.value && localVideoRef.value) {
+    const res =
+      bottomRef.value.getBoundingClientRect().top -
+      topRef.value.getBoundingClientRect().top;
+    localVideoRef.value.style.height = `100px`;
+    localVideoRef.value.style.height = `${res}px`;
+  }
+});
+</script>
+
+<style lang="scss" scoped>
+.srs-webrtc-push-wrap {
+  margin: 20px auto 0;
+  min-width: $large-width;
+  height: 700px;
+  text-align: center;
+  .left {
+    position: relative;
+    display: inline-block;
+    box-sizing: border-box;
+    width: $large-left-width;
+    height: 100%;
+    border-radius: 6px;
+    overflow: hidden;
+    background-color: white;
+    color: #9499a0;
+    vertical-align: top;
+
+    .video-wrap {
+      position: relative;
+      background-color: #18191c;
+      #localVideo {
+        max-width: 100%;
+        max-height: 100%;
+      }
+      .add-wrap {
+        position: absolute;
+        top: 50%;
+        left: 50%;
+        display: flex;
+        align-items: center;
+        justify-content: space-around;
+        padding: 0 20px;
+        height: 50px;
+        border-radius: 5px;
+        background-color: white;
+        transform: translate(-50%, -50%);
+      }
+    }
+    .control {
+      position: absolute;
+      right: 0;
+      bottom: 0;
+      left: 0;
+      display: flex;
+      justify-content: space-between;
+      padding: 20px;
+      background-color: papayawhip;
+
+      .info {
+        display: flex;
+        align-items: center;
+
+        .avatar {
+          margin-right: 20px;
+          width: 64px;
+          height: 64px;
+          border-radius: 50%;
+          background-color: skyblue;
+          background-position: center center;
+          background-size: cover;
+          background-repeat: no-repeat;
+        }
+        .detail {
+          display: flex;
+          flex-direction: column;
+          text-align: initial;
+          .top {
+            margin-bottom: 10px;
+            color: #18191c;
+            .btn {
+              margin-left: 10px;
+            }
+          }
+          .bottom {
+            font-size: 14px;
+          }
+        }
+      }
+      .other {
+        display: flex;
+        flex-direction: column;
+        justify-content: center;
+        font-size: 12px;
+        .top {
+          display: flex;
+          align-items: center;
+          .item {
+            display: flex;
+            align-items: center;
+            margin-right: 20px;
+            .ico {
+              display: inline-block;
+              margin-right: 4px;
+              width: 10px;
+              height: 10px;
+              border-radius: 50%;
+              background-color: skyblue;
+            }
+          }
+        }
+        .bottom {
+          margin-top: 10px;
+        }
+      }
+    }
+  }
+  .right {
+    position: relative;
+    display: inline-block;
+    box-sizing: border-box;
+    margin-left: 10px;
+    width: 240px;
+    height: 100%;
+    border-radius: 6px;
+    background-color: white;
+    color: #9499a0;
+
+    .resource-card {
+      box-sizing: border-box;
+      margin-bottom: 5%;
+      margin-bottom: 10px;
+      padding: 10px;
+      width: 100%;
+      height: 290px;
+      border-radius: 6px;
+      background-color: papayawhip;
+      .title {
+        text-align: initial;
+      }
+      .item {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        margin: 5px 0;
+        font-size: 12px;
+      }
+    }
+    .danmu-card {
+      box-sizing: border-box;
+      padding: 10px;
+      width: 100%;
+      height: 400px;
+      border-radius: 6px;
+      background-color: papayawhip;
+      text-align: initial;
+      .title {
+        margin-bottom: 10px;
+      }
+      .list {
+        margin-bottom: 10px;
+        height: 300px;
+        .item {
+          margin-bottom: 10px;
+          font-size: 12px;
+          .name {
+            color: #9499a0;
+          }
+          .msg {
+            color: #61666d;
+          }
+        }
+      }
+
+      .send-msg {
+        display: flex;
+        align-items: center;
+        box-sizing: border-box;
+        .ipt {
+          display: block;
+          box-sizing: border-box;
+          margin: 0 auto;
+          margin-right: 10px;
+          padding: 10px;
+          width: 80%;
+          height: 30px;
+          outline: none;
+          border: 1px solid hsla(0, 0%, 60%, 0.2);
+          border-radius: 4px;
+          background-color: #f1f2f3;
+          font-size: 14px;
+        }
+      }
+    }
+  }
+}
+// 屏幕宽度小于$large-width的时候
+@media screen and (max-width: $large-width) {
+  .srs-webrtc-push-wrap {
+    .left {
+      width: $medium-left-width;
+    }
+    .right {
+      .list {
+        .item {
+        }
+      }
+    }
+  }
+}
+</style>

+ 4 - 5
src/views/webrtc-pull/index.vue → src/old/webrtc-pull/index.vue

@@ -29,7 +29,6 @@
             x5-video-player-fullscreen="true"
             x5-video-orientation="portraint"
             muted
-            controls
           ></video>
         </div>
         <div
@@ -51,7 +50,7 @@
         <div class="tab">
           <span>在线用户</span>
           <span> | </span>
-          <span>大航海</span>
+          <span>排行榜</span>
         </div>
         <div class="user-list">
           <div
@@ -61,7 +60,7 @@
           >
             <div class="info">
               <div class="avatar"></div>
-              <div class="nickname">{{ item.socketId }}</div>
+              <div class="username">{{ item.socketId }}</div>
             </div>
             <div class="expr">{{ item.expr }}</div>
           </div>
@@ -318,7 +317,7 @@ function initReceive() {
   instance.socketIo.on(WsMsgTypeEnum.otherJoin, (data) => {
     console.log('【websocket】其他用户加入房间', data);
     damuList.value.push({
-      socketId: data.socketId,
+      socketId: data.data.socketId,
       msgType: DanmuMsgTypeEnum.otherJoin,
       msg: '',
     });
@@ -542,7 +541,7 @@ onMounted(() => {
             border-radius: 50%;
             background-color: skyblue;
           }
-          .nickname {
+          .username {
             color: black;
           }
         }

+ 415 - 0
src/old/webrtc-push/index.vue

@@ -0,0 +1,415 @@
+<template>
+  <div class="webrtc-push-wrap">
+    <div
+      ref="topRef"
+      class="left"
+    >
+      <div class="video-wrap">
+        <video
+          id="localVideo"
+          ref="localVideoRef"
+          autoplay
+          webkit-playsinline="true"
+          playsinline
+          x-webkit-airplay="allow"
+          x5-video-player-type="h5"
+          x5-video-player-fullscreen="true"
+          x5-video-orientation="portraint"
+          muted
+        ></video>
+        <div
+          v-if="!currMediaTypeList || currMediaTypeList.length <= 0"
+          class="add-wrap"
+        >
+          <n-space>
+            <n-button
+              class="item"
+              @click="startGetUserMedia"
+            >
+              摄像头
+            </n-button>
+            <n-button
+              class="item"
+              @click="startGetDisplayMedia"
+            >
+              窗口
+            </n-button>
+          </n-space>
+        </div>
+      </div>
+      <div
+        ref="bottomRef"
+        class="control"
+      >
+        <div class="info">
+          <div
+            class="avatar"
+            :style="{ backgroundImage: `url(${userStore.userInfo?.avatar})` }"
+          ></div>
+          <div class="detail">
+            <div class="top">
+              <n-input-group>
+                <n-input
+                  v-model:value="roomName"
+                  size="small"
+                  placeholder="输入房间名"
+                  :style="{ width: '50%' }"
+                  :disabled="disabled"
+                />
+                <n-button
+                  size="small"
+                  type="primary"
+                  :disabled="disabled"
+                  @click="confirmRoomName"
+                >
+                  确定
+                </n-button>
+              </n-input-group>
+            </div>
+            <div class="bottom">
+              <span>socketId:{{ getSocketId() }}</span>
+            </div>
+          </div>
+        </div>
+        <div class="other">
+          <div class="top">
+            <span class="item">
+              <i class="ico"></i>
+              <span>正在观看人数:{{ liveUserList.length }}</span>
+            </span>
+          </div>
+          <div class="bottom">
+            <n-space>
+              <n-button
+                type="info"
+                size="small"
+                @click="startLive"
+              >
+                开始直播
+              </n-button>
+              <n-button
+                type="info"
+                size="small"
+                @click="endLive"
+              >
+                结束直播
+              </n-button>
+            </n-space>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="right">
+      <div class="resource-card">
+        <div class="title">素材列表</div>
+        <div class="list">
+          <div
+            v-for="(item, index) in currMediaTypeList"
+            :key="index"
+            class="item"
+          >
+            <span class="name">{{ item.txt }}</span>
+          </div>
+        </div>
+      </div>
+      <div class="danmu-card">
+        <div class="title">弹幕互动</div>
+        <div class="list-wrap">
+          <div class="list">
+            <div
+              v-for="(item, index) in damuList"
+              :key="index"
+              class="item"
+            >
+              <template v-if="item.msgType === DanmuMsgTypeEnum.danmu">
+                <span class="name">{{ item.socketId }}:</span>
+                <span class="msg">{{ item.msg }}</span>
+              </template>
+              <template v-else-if="item.msgType === DanmuMsgTypeEnum.otherJoin">
+                <span class="name system">系统通知:</span>
+                <span class="msg">{{ item.socketId }}进入直播!</span>
+              </template>
+              <template
+                v-else-if="item.msgType === DanmuMsgTypeEnum.userLeaved"
+              >
+                <span class="name system">系统通知:</span>
+                <span class="msg">{{ item.socketId }}离开直播!</span>
+              </template>
+            </div>
+          </div>
+        </div>
+        <div class="send-msg">
+          <input
+            v-model="danmuStr"
+            class="ipt"
+            @keydown="keydownDanmu"
+          />
+          <n-button
+            type="info"
+            size="small"
+            @click="sendDanmu"
+          >
+            发送
+          </n-button>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { onMounted, onUnmounted, ref } from 'vue';
+import { useRoute } from 'vue-router';
+
+import { usePush } from '@/hooks/use-push';
+import { DanmuMsgTypeEnum, liveTypeEnum } from '@/interface';
+import { useUserStore } from '@/store/user';
+
+const route = useRoute();
+const userStore = useUserStore();
+
+const topRef = ref<HTMLDivElement>();
+const bottomRef = ref<HTMLDivElement>();
+const localVideoRef = ref<HTMLVideoElement>();
+const liveType = route.query.liveType;
+
+const {
+  initPush,
+  confirmRoomName,
+  getSocketId,
+  startGetDisplayMedia,
+  startGetUserMedia,
+  startLive,
+  endLive,
+  closeWs,
+  closeRtc,
+  sendDanmu,
+  keydownDanmu,
+  disabled,
+  danmuStr,
+  roomName,
+  damuList,
+  liveUserList,
+  currMediaTypeList,
+} = usePush({
+  localVideoRef,
+  isSRS: liveType === liveTypeEnum.srsPush,
+});
+
+onUnmounted(() => {
+  closeWs();
+  closeRtc();
+});
+
+onMounted(() => {
+  initPush();
+  if (topRef.value && bottomRef.value && localVideoRef.value) {
+    const res =
+      bottomRef.value.getBoundingClientRect().top -
+      topRef.value.getBoundingClientRect().top;
+    localVideoRef.value.style.height = `${res}px`;
+  }
+});
+</script>
+
+<style lang="scss" scoped>
+.webrtc-push-wrap {
+  margin: 20px auto 0;
+  min-width: $large-width;
+  height: 700px;
+  text-align: center;
+  .left {
+    position: relative;
+    display: inline-block;
+    box-sizing: border-box;
+    width: $large-left-width;
+    height: 100%;
+    border-radius: 6px;
+    overflow: hidden;
+    background-color: white;
+    color: #9499a0;
+    vertical-align: top;
+
+    .video-wrap {
+      position: relative;
+      background-color: #18191c;
+      #localVideo {
+        max-width: 100%;
+        max-height: 100%;
+      }
+      .add-wrap {
+        position: absolute;
+        top: 50%;
+        left: 50%;
+        display: flex;
+        align-items: center;
+        justify-content: space-around;
+        padding: 0 20px;
+        height: 50px;
+        border-radius: 5px;
+        background-color: white;
+        transform: translate(-50%, -50%);
+      }
+    }
+    .control {
+      position: absolute;
+      right: 0;
+      bottom: 0;
+      left: 0;
+      display: flex;
+      justify-content: space-between;
+      padding: 20px;
+      background-color: papayawhip;
+
+      .info {
+        display: flex;
+        align-items: center;
+
+        .avatar {
+          margin-right: 20px;
+          width: 64px;
+          height: 64px;
+          border-radius: 50%;
+          background-color: skyblue;
+          background-position: center center;
+          background-size: cover;
+          background-repeat: no-repeat;
+        }
+        .detail {
+          display: flex;
+          flex-direction: column;
+          text-align: initial;
+          .top {
+            margin-bottom: 10px;
+            color: #18191c;
+            .btn {
+              margin-left: 10px;
+            }
+          }
+          .bottom {
+            font-size: 14px;
+          }
+        }
+      }
+      .other {
+        display: flex;
+        flex-direction: column;
+        justify-content: center;
+        font-size: 12px;
+        .top {
+          display: flex;
+          align-items: center;
+          .item {
+            display: flex;
+            align-items: center;
+            margin-right: 20px;
+            .ico {
+              display: inline-block;
+              margin-right: 4px;
+              width: 10px;
+              height: 10px;
+              border-radius: 50%;
+              background-color: skyblue;
+            }
+          }
+        }
+        .bottom {
+          margin-top: 10px;
+        }
+      }
+    }
+  }
+  .right {
+    position: relative;
+    display: inline-block;
+    box-sizing: border-box;
+    margin-left: 10px;
+    width: 240px;
+    height: 100%;
+    border-radius: 6px;
+    background-color: white;
+    color: #9499a0;
+
+    .resource-card {
+      box-sizing: border-box;
+      margin-bottom: 5%;
+      margin-bottom: 10px;
+      padding: 10px;
+      width: 100%;
+      height: 290px;
+      border-radius: 6px;
+      background-color: papayawhip;
+      .title {
+        text-align: initial;
+      }
+      .item {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        margin: 5px 0;
+        font-size: 12px;
+      }
+    }
+    .danmu-card {
+      box-sizing: border-box;
+      padding: 10px;
+      width: 100%;
+      height: 400px;
+      border-radius: 6px;
+      background-color: papayawhip;
+      text-align: initial;
+      .title {
+        margin-bottom: 10px;
+      }
+      .list {
+        margin-bottom: 10px;
+        height: 300px;
+        .item {
+          margin-bottom: 10px;
+          font-size: 12px;
+          .name {
+            color: #9499a0;
+          }
+          .msg {
+            color: #61666d;
+          }
+        }
+      }
+
+      .send-msg {
+        display: flex;
+        align-items: center;
+        box-sizing: border-box;
+        .ipt {
+          display: block;
+          box-sizing: border-box;
+          margin: 0 auto;
+          margin-right: 10px;
+          padding: 10px;
+          width: 80%;
+          height: 30px;
+          outline: none;
+          border: 1px solid hsla(0, 0%, 60%, 0.2);
+          border-radius: 4px;
+          background-color: #f1f2f3;
+          font-size: 14px;
+        }
+      }
+    }
+  }
+}
+// 屏幕宽度小于$large-width的时候
+@media screen and (max-width: $large-width) {
+  .webrtc-push-wrap {
+    .left {
+      width: $medium-left-width;
+    }
+    .right {
+      .list {
+        .item {
+        }
+      }
+    }
+  }
+}
+</style>

+ 20 - 43
src/router/index.ts

@@ -5,17 +5,14 @@ import Layout from '@/layout/index.vue';
 import type { RouteRecordRaw } from 'vue-router';
 
 export const routerName = {
-  aliPay: 'aliPay',
-  sponsors: 'sponsors',
   home: 'home',
+  rank: 'rank',
+  sponsors: 'sponsors',
+  oauth: 'oauth',
   notFound: 'notFound',
-  bilibiliPush: 'bilibiliPush',
-  test1: 'test1',
-  webrtcPush: 'webrtcPush',
-  webrtcPull: 'webrtcPull',
-  srsWebRtcPush: 'srsWebRtcPush',
-  srsWebRtcPull: 'srsWebRtcPull',
-  srsFlvPull: 'srsFlvPull',
+
+  pull: 'pull',
+  push: 'push',
 };
 
 // 默认路由
@@ -29,52 +26,32 @@ export const defaultRoutes: RouteRecordRaw[] = [
         path: '/',
         component: () => import('@/views/home/index.vue'),
       },
+      {
+        name: routerName.rank,
+        path: '/rank',
+        component: () => import('@/views/rank/index.vue'),
+      },
       {
         name: routerName.sponsors,
         path: '/sponsors',
         component: () => import('@/views/sponsors/index.vue'),
       },
       {
-        name: routerName.bilibiliPush,
-        path: '/bilibiliPush',
-        component: () => import('@/views/bilibiliPush/index.vue'),
-      },
-      {
-        name: routerName.test1,
-        path: '/test1',
-        component: () => import('@/views/test1/index.vue'),
-      },
-      {
-        name: routerName.webrtcPush,
-        path: '/webrtc-push',
-        component: () => import('@/views/webrtc-push/index.vue'),
-      },
-      {
-        name: routerName.webrtcPull,
-        path: '/webrtc-pull/:roomId',
-        component: () => import('@/views/webrtc-pull/index.vue'),
-      },
-      {
-        name: routerName.srsWebRtcPush,
-        path: '/srs-webrtc-push',
-        component: () => import('@/views/srs-webrtc-push/index.vue'),
-      },
-      {
-        name: routerName.srsWebRtcPull,
-        path: '/srs-webrtc-pull/:roomId',
-        component: () => import('@/views/srs-webrtc-pull/index.vue'),
+        name: routerName.pull,
+        path: '/pull/:roomId',
+        component: () => import('@/views/pull/index.vue'),
       },
       {
-        name: routerName.srsFlvPull,
-        path: '/srs-flv-pull/:roomId',
-        component: () => import('@/views/srs-flv-pull/index.vue'),
+        name: routerName.push,
+        path: '/push',
+        component: () => import('@/views/push/index.vue'),
       },
     ],
   },
   {
-    name: routerName.aliPay,
-    path: '/ali-pay',
-    component: () => import('@/views/aliPay/index.vue'),
+    name: routerName.oauth,
+    path: '/oauth/:platform',
+    component: () => import('@/views/oauth/index.vue'),
   },
 ];
 const router = createRouter({

+ 16 - 0
src/shims-vue.d.ts

@@ -0,0 +1,16 @@
+declare module '*.vue' {
+  /* eslint-disable */
+  import type { DefineComponent } from 'vue';
+  const component: DefineComponent<{}, {}, any>;
+  export default component;
+}
+interface Window {
+  $message: {
+    info: any;
+    success: any;
+    warning: any;
+    error: any;
+    loading: any;
+    default: any;
+  };
+}

+ 5 - 0
src/store/app/index.ts

@@ -2,17 +2,22 @@ import { defineStore } from 'pinia';
 
 type AppRootState = {
   liveStatus: boolean;
+  muted: boolean;
 };
 
 export const useAppStore = defineStore('app', {
   state: (): AppRootState => {
     return {
       liveStatus: false,
+      muted: true,
     };
   },
   actions: {
     setLiveStatus(res: AppRootState['liveStatus']) {
       this.liveStatus = res;
     },
+    setMuted(res: AppRootState['muted']) {
+      this.muted = res;
+    },
   },
 });

+ 52 - 9
src/store/user/index.ts

@@ -1,23 +1,66 @@
-import { mockAjax } from 'billd-utils';
 import { defineStore } from 'pinia';
 
-type UserRootState = {
-  detail: any;
+import { fetchUserInfo } from '@/api/user';
+import { IRole } from '@/interface';
+import cache from '@/utils/cache';
+
+type RootState = {
+  userInfo?: {
+    id: number;
+    username: string;
+    status: number;
+    avatar: string;
+    title: string;
+    created_at: string;
+    updated_at: string;
+    deleted_at: any;
+    send_comments_total: number;
+    receive_comments_total: number;
+    send_stars_total: number;
+    receive_stars_total: number;
+    articles_total: number;
+    qq_users: any[];
+    github_users: any[];
+    email_users: any[];
+    roles: IRole[];
+  };
+  token?: string;
+  roles?: IRole[];
 };
 
 export const useUserStore = defineStore('user', {
-  state: (): UserRootState => {
+  state: (): RootState => {
     return {
-      detail: null,
+      userInfo: undefined,
+      token: undefined,
+      roles: [],
     };
   },
   actions: {
-    async setDetail(payload: number) {
+    setUserInfo(res) {
+      this.userInfo = res;
+    },
+    setToken(res) {
+      cache.setStorageExp('token', res, 24);
+      this.token = res;
+    },
+    setRoles(res) {
+      this.roles = res;
+    },
+    logout() {
+      cache.clearStorage('token');
+      this.token = undefined;
+      this.userInfo = undefined;
+      this.roles = [];
+    },
+    async getUserInfo() {
       try {
-        const data = await mockAjax({ flag: payload === 1 });
-        this.detail = data;
+        const { code, data }: any = await fetchUserInfo();
+        this.setUserInfo(data);
+        this.setRoles(data.roles);
+        return { code, data };
       } catch (error) {
-        console.log(error);
+        return error;
       }
     },
   },

+ 3 - 0
src/utils/cache.ts

@@ -0,0 +1,3 @@
+import { CacheModel } from 'billd-utils';
+
+export default new CacheModel();

+ 1 - 0
src/utils/cookie/index.ts

@@ -0,0 +1 @@
+export * from './loginEnv';

+ 17 - 0
src/utils/cookie/loginEnv.ts

@@ -0,0 +1,17 @@
+import cookies from 'js-cookie';
+
+import { COOKIE_KEY } from '@/constant';
+
+export const getLoginInfo = () => {
+  return cookies.get(COOKIE_KEY.loginInfo);
+};
+
+export const setLoginInfo = (val) => {
+  cookies.set(COOKIE_KEY.loginInfo, val, {
+    domain: process.env.NODE_ENV === 'development' ? undefined : '.hsslive.cn',
+  });
+};
+
+export const clearLoginInfo = () => {
+  cookies.remove(COOKIE_KEY.loginInfo);
+};

+ 2 - 22
src/utils/index.ts

@@ -1,23 +1,3 @@
-/** 模拟ajax请求 */
-export const mockAjax = ({ flag = false, delay = 500 }) => {
-  return new Promise<{ code: Number; data: { id: number }; msg: string }>(
-    (resolve, rejected) => {
-      setTimeout(() => {
-        if (flag) {
-          resolve({
-            code: 200,
-            data: {
-              id: 1,
-            },
-            msg: '请求成功',
-          });
-        } else {
-          rejected({
-            code: 400,
-            msg: '请求失败',
-          });
-        }
-      }, delay);
-    }
-  );
+export const sum = (a, b) => {
+  return a + b;
 };

+ 13 - 1
src/utils/request.ts

@@ -1,5 +1,8 @@
 import axios, { Axios, AxiosRequestConfig } from 'axios';
 
+import { useUserStore } from '@/store/user';
+import cache from '@/utils/cache';
+
 export interface MyAxiosPromise<T = any>
   extends Promise<{
     code: number;
@@ -26,6 +29,11 @@ class MyAxios {
     // 请求拦截器
     this.instance.interceptors.request.use(
       (cfg) => {
+        const token = cache.getStorageExp('token');
+        if (token) {
+          // eslint-disable-next-line
+          cfg.headers.Authorization = `Bearer ${token}`;
+        }
         return cfg;
       },
       (error) => {
@@ -45,7 +53,9 @@ class MyAxios {
         console.log('响应拦截到错误', error);
         if (error.message.indexOf('timeout') !== -1) {
           console.error(error.message);
-          return;
+          window.$message.error('请求超时,请重试');
+        } else {
+          window.$message.error('网络错误,请重试');
         }
         const statusCode = error.response.status as number;
         const errorResponseData = error.response.data;
@@ -69,6 +79,8 @@ class MyAxios {
           }
           if (statusCode === 401) {
             console.error(errorResponseData.message);
+            const userStore = useUserStore();
+            userStore.logout();
             return Promise.reject(errorResponseData);
           }
           if (statusCode === 403) {

+ 0 - 22
src/views/aliPay/index.vue

@@ -1,22 +0,0 @@
-<template>
-  <div></div>
-</template>
-
-<script lang="ts" setup>
-import { hrefToTarget } from 'billd-utils';
-
-import { fetchAliPay } from '@/api/aliPay';
-
-async function startPay() {
-  try {
-    const res = await fetchAliPay();
-    console.log(res);
-    hrefToTarget(res.data);
-  } catch (error) {
-    console.log(error);
-  }
-}
-startPay();
-</script>
-
-<style lang="scss" scoped></style>

+ 0 - 903
src/views/bilibiliPush/index.vue

@@ -1,903 +0,0 @@
-<template>
-  <div class="push-wrap">
-    <div
-      ref="topRef"
-      class="left"
-    >
-      <div class="video-wrap">
-        <video
-          id="localVideo"
-          ref="localVideoRef"
-          autoplay
-          webkit-playsinline="true"
-          playsinline
-          x-webkit-airplay="allow"
-          x5-video-player-type="h5"
-          x5-video-player-fullscreen="true"
-          x5-video-orientation="portraint"
-          muted
-          controls
-        ></video>
-        <div
-          v-if="currMediaTypeList.length <= 0"
-          class="add-wrap"
-        >
-          <div
-            class="item"
-            @click="startMediaDevices"
-          >
-            摄像头
-          </div>
-          <div
-            class="item"
-            @click="startGetDisplayMedia"
-          >
-            窗口
-          </div>
-        </div>
-      </div>
-      <div
-        ref="bottomRef"
-        class="control"
-      >
-        <div class="info">
-          <div class="avatar"></div>
-          <div class="detail">
-            <div class="top">
-              <input
-                ref="roomNameRef"
-                v-model="roomName"
-                type="text"
-                placeholder="输入房间名"
-              />
-              <button
-                class="btn"
-                @click="confirmRoomName"
-              >
-                确定
-              </button>
-              <!-- 房东的猫livehouse/音乐节 -->
-            </div>
-            <div class="bottom">
-              <span>socketId:{{ getSocketId() }}</span>
-            </div>
-          </div>
-        </div>
-        <div class="other">
-          <div class="top">
-            <span class="item">
-              <i class="ico"></i>
-              <span>正在观看人数:{{ liveUserList.length }}</span>
-            </span>
-          </div>
-          <div class="bottom">
-            <button @click="startLive">开始直播</button>
-            <button @click="endLive">结束直播</button>
-          </div>
-        </div>
-      </div>
-    </div>
-    <div class="right">
-      <div class="resource-card">
-        <div class="title">素材列表</div>
-        <div class="list">
-          <div
-            v-for="(item, index) in currMediaTypeList"
-            :key="index"
-            class="item"
-          >
-            <span class="name">{{ item }}</span>
-          </div>
-        </div>
-      </div>
-      <div class="danmu-card">
-        <div class="title">弹幕互动</div>
-        <div class="list">
-          <div
-            v-for="(item, index) in damuList"
-            :key="index"
-            class="item"
-          >
-            <span class="name">{{ item.socketId }}:</span>
-            <span class="msg">{{ item.msg }}</span>
-          </div>
-        </div>
-
-        <div class="send-msg">
-          <input
-            v-model="danmuStr"
-            class="ipt"
-          />
-          <div
-            class="btn"
-            @click="sendDanmu"
-          >
-            发送
-          </div>
-        </div>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script lang="ts" setup>
-import { getRandomString } from 'billd-utils';
-import MediaStreamRecorder from 'msr';
-import { onMounted, onUnmounted, ref } from 'vue';
-
-import { IAdminIn, ICandidate, IOffer, LiveTypeEnum } from '@/interface';
-import { WebRTCClass } from '@/network/webRtc';
-import {
-  WebSocketClass,
-  WsConnectStatusEnum,
-  WsMsgTypeEnum,
-} from '@/network/webSocket';
-import { useNetworkStore } from '@/store/network';
-
-const networkStore = useNetworkStore();
-
-const topRef = ref<HTMLDivElement>();
-const bottomRef = ref<HTMLDivElement>();
-const roomIdRef = ref<HTMLInputElement>();
-const joinRef = ref<HTMLButtonElement>();
-const leaveRef = ref<HTMLButtonElement>();
-const defaultRoomId = getRandomString(15);
-const roomId = ref<string>(defaultRoomId);
-const danmuStr = ref('');
-const roomName = ref('');
-const roomNameRef = ref<HTMLInputElement>();
-const websocketInstant = ref<WebSocketClass>();
-const isDone = ref(false);
-const localVideoRef = ref<HTMLVideoElement>();
-const localStream = ref();
-const currMediaTypeList = ref<LiveTypeEnum[]>([]);
-const currMediaType = ref<LiveTypeEnum>();
-const joined = ref(false);
-const isAdmin = ref(true);
-const offerSended = ref(new Set());
-const damuList = ref<
-  {
-    socketId: string;
-    msgType: number;
-    msg: string;
-  }[]
->([]);
-
-const liveUserList = ref<
-  {
-    socketId: string;
-    avatar: string;
-    expr: number;
-  }[]
->([]);
-
-function closeWs() {
-  const instance = networkStore.wsMap.get(roomId.value);
-  if (!instance) return;
-  instance.close();
-}
-
-function closeRtc() {
-  networkStore.rtcMap.forEach((rtc) => {
-    rtc.close();
-  });
-}
-
-function sendDanmu() {
-  if (!danmuStr.value.length) {
-    alert('请输入弹幕内容!');
-  }
-  if (!websocketInstant.value) return;
-  websocketInstant.value.send({
-    msgType: WsMsgTypeEnum.message,
-    data: { msg: danmuStr.value },
-  });
-  damuList.value.push({
-    socketId: getSocketId(),
-    msgType: 1,
-    msg: danmuStr.value,
-  });
-  danmuStr.value = '';
-}
-
-onUnmounted(() => {
-  closeWs();
-  closeRtc();
-});
-let flvPlayer;
-
-function startRtmp() {
-  try {
-    flvPlayer.play();
-  } catch (error) {
-    console.log(error);
-  }
-}
-
-onMounted(() => {
-  // if (flvJs.isSupported()) {
-  //   flvPlayer = flvJs.createPlayer({
-  //     type: 'flv',
-  //     url: 'http://localhost:8080/live/show.flv',
-  //     // url: 'http://42.193.157.44:9000/live/fddm_2.flv',
-  //     // url: 'https://www.hsslive.cn/stream/live/fddm_2.flv',
-  //     // url: 'https://www.hsslive.cn/bilibilistream/live-bvc/173676/live_381307133_59434826.flv?expires=1680798150&len=0&oi=1900220676&pt=web&qn=0&trid=10002eb4d1a458684cc9a93d888d7b580cac&sigparams=cdn,expires,len,oi,pt,qn,trid&cdn=cn-gotcha15&sign=1724a76684e5e1f8d68346613a32f8c0&sk=3e2a893893799632504afbdef4a9e3d7&p2p_type=1&sl=1&free_type=0&mid=381307133&sche=ban&score=1&pp=rtmp&freeze=1&source=onetier&trace=10&site=12e2a543d23e1c612a7145eb5a5cfac4&order=1',
-  //     // url: 'https://xy117x149x235x203xy.mcdn.bilivideo.cn/live-bvc/173676/live_381307133_59434826.flv?expires=1680798150&len=0&oi=1900220676&pt=web&qn=0&trid=10002eb4d1a458684cc9a93d888d7b580cac&sigparams=cdn,expires,len,oi,pt,qn,trid&cdn=cn-gotcha15&sign=1724a76684e5e1f8d68346613a32f8c0&sk=3e2a893893799632504afbdef4a9e3d7&p2p_type=1&sl=1&free_type=0&mid=381307133&sche=ban&score=1&pp=rtmp&freeze=1&source=onetier&trace=10&site=12e2a543d23e1c612a7145eb5a5cfac4&order=1',
-  //   });
-  //   // @ts-ignore
-  //   flvPlayer.attachMediaElement(
-  //     document.querySelector<HTMLVideoElement>('#blobVideo')
-  //   );
-  //   flvPlayer.load();
-  // }
-  if (topRef.value && bottomRef.value && localVideoRef.value) {
-    const res =
-      bottomRef.value.getBoundingClientRect().top -
-      topRef.value.getBoundingClientRect().top;
-    localVideoRef.value.style.height = `100px`;
-    // localVideoRef.value.style.height = `${res}px`;
-  }
-  localVideoRef.value?.addEventListener('loadstart', () => {
-    console.warn('视频流-loadstart');
-    const rtc = networkStore.getRtcMap(roomId.value);
-    if (!rtc) return;
-    rtc.rtcStatus.loadstart = true;
-    rtc.update();
-  });
-
-  localVideoRef.value?.addEventListener('loadedmetadata', () => {
-    console.warn('视频流-loadedmetadata');
-    const rtc = networkStore.getRtcMap(roomId.value);
-    if (!rtc) return;
-    rtc.rtcStatus.loadedmetadata = true;
-    rtc.update();
-    if (isAdmin.value) {
-      batchSendOffer();
-    }
-  });
-});
-
-function getSocketId() {
-  return networkStore.wsMap.get(roomId.value!)?.socketIo?.id || '-1';
-}
-
-function sendJoin() {
-  const instance = networkStore.wsMap.get(roomId.value);
-  if (!instance) return;
-  instance.send({
-    msgType: WsMsgTypeEnum.join,
-    data: {
-      roomName: roomName.value,
-    },
-  });
-}
-
-function batchSendOffer() {
-  liveUserList.value.forEach(async (item) => {
-    if (
-      !offerSended.value.has(item.socketId) &&
-      item.socketId !== getSocketId()
-    ) {
-      await startNewWebRtc(item.socketId);
-      await addTrack();
-      console.warn('new WebRTCClass完成');
-      console.log('执行sendOffer', {
-        sender: getSocketId(),
-        receiver: item.socketId,
-      });
-      sendOffer({ sender: getSocketId(), receiver: item.socketId });
-      offerSended.value.add(item.socketId);
-    }
-  });
-}
-
-function initReceive() {
-  const instance = websocketInstant.value;
-  if (!instance?.socketIo) return;
-  // websocket连接成功
-  instance.socketIo.on(WsConnectStatusEnum.connect, () => {
-    console.log('【websocket】websocket连接成功', instance.socketIo?.id);
-    if (!instance) return;
-    instance.status = WsConnectStatusEnum.connect;
-    instance.update();
-  });
-
-  // websocket连接断开
-  instance.socketIo.on(WsConnectStatusEnum.disconnect, () => {
-    console.log('【websocket】websocket连接断开', instance);
-    if (!instance) return;
-    instance.status = WsConnectStatusEnum.disconnect;
-    instance.update();
-  });
-
-  // 当前所有在线用户
-  instance.socketIo.on(WsMsgTypeEnum.roomLiveing, (data: IAdminIn) => {
-    console.log('【websocket】收到管理员正在直播', data);
-  });
-
-  // 当前所有在线用户
-  instance.socketIo.on(WsMsgTypeEnum.liveUser, () => {
-    console.log('【websocket】当前所有在线用户');
-    if (!instance) return;
-  });
-
-  // 收到offer
-  instance.socketIo.on(WsMsgTypeEnum.offer, async (data: IOffer) => {
-    console.warn('【websocket】收到offer', data);
-    if (!instance) return;
-    if (data.data.receiver === getSocketId()) {
-      console.log('收到offer,这个offer是发给我的');
-      const rtc = startNewWebRtc(data.data.sender);
-      await rtc.setRemoteDescription(data.data.sdp);
-      const sdp = await rtc.createAnswer();
-      await rtc.setLocalDescription(sdp);
-      websocketInstant.value?.send({
-        msgType: WsMsgTypeEnum.answer,
-        data: { sdp, sender: getSocketId(), receiver: data.data.sender },
-      });
-    } else {
-      console.log('收到offer,但是这个offer不是发给我的');
-    }
-  });
-
-  // 收到answer
-  instance.socketIo.on(WsMsgTypeEnum.answer, async (data: IOffer) => {
-    console.warn('【websocket】收到answer', data);
-    if (isDone.value) return;
-    if (!instance) return;
-    const rtc = networkStore.getRtcMap(`${roomId.value}___${data.socketId}`);
-    console.log(rtc, '收到answer收到answer');
-    if (!rtc) return;
-    rtc.rtcStatus.answer = true;
-    rtc.update();
-    if (data.data.receiver === getSocketId()) {
-      console.log('收到answer,这个answer是发给我的');
-      await rtc.setRemoteDescription(data.data.sdp);
-    } else {
-      console.log('收到answer,但这个answer不是发给我的');
-    }
-  });
-
-  // 收到candidate
-  instance.socketIo.on(WsMsgTypeEnum.candidate, (data: ICandidate) => {
-    console.warn('【websocket】收到candidate', data);
-    if (isDone.value) return;
-    if (!instance) return;
-    const rtc =
-      networkStore.getRtcMap(`${roomId.value}___${data.socketId}`) ||
-      networkStore.getRtcMap(roomId.value);
-    if (!rtc) return;
-    if (data.socketId !== getSocketId()) {
-      console.log('不是我发的candidate');
-      const candidate = new RTCIceCandidate({
-        sdpMid: data.data.sdpMid,
-        sdpMLineIndex: data.data.sdpMLineIndex,
-        candidate: data.data.candidate,
-      });
-      rtc.peerConnection
-        ?.addIceCandidate(candidate)
-        .then(() => {
-          console.log('candidate成功');
-          // rtc.handleStream();
-        })
-        .catch((err) => {
-          console.error('candidate失败', err);
-        });
-    } else {
-      console.log('是我发的candidate');
-    }
-  });
-
-  // 收到用户发送消息
-  instance.socketIo.on(WsMsgTypeEnum.message, (data) => {
-    console.log('【websocket】收到用户发送消息', data);
-    if (!instance) return;
-    damuList.value.push({
-      socketId: data.socketId,
-      msgType: 1,
-      msg: data.data.msg,
-    });
-  });
-
-  // 用户加入房间
-  instance.socketIo.on(WsMsgTypeEnum.join, (data) => {
-    console.log('【websocket】用户加入房间', data);
-    if (!instance) return;
-  });
-
-  // 用户加入房间
-  instance.socketIo.on(WsMsgTypeEnum.joined, (data) => {
-    console.log('【websocket】用户加入房间完成', data);
-    joined.value = true;
-    liveUserList.value.push({
-      avatar: 'red',
-      socketId: `${getSocketId()}`,
-      expr: 1,
-    });
-    batchSendOffer();
-  });
-
-  // 其他用户加入房间
-  instance.socketIo.on(WsMsgTypeEnum.otherJoin, (data) => {
-    console.log('【websocket】其他用户加入房间', data);
-    liveUserList.value.push({
-      avatar: 'red',
-      socketId: data.socketId,
-      expr: 1,
-    });
-    console.log('当前所有在线用户', JSON.stringify(liveUserList.value));
-    console.log(isAdmin.value, joined.value);
-    if (isAdmin.value && joined.value) {
-      batchSendOffer();
-    }
-  });
-
-  // 用户离开房间
-  instance.socketIo.on(WsMsgTypeEnum.leave, (data) => {
-    console.log('【websocket】用户离开房间', data);
-    if (!instance) return;
-    instance.socketIo?.emit(WsMsgTypeEnum.leave, {
-      roomId: instance.roomId,
-    });
-  });
-
-  // 用户离开房间完成
-  instance.socketIo.on(WsMsgTypeEnum.leaved, (data) => {
-    console.log('【websocket】用户离开房间完成', data);
-    if (!instance) return;
-    const res = liveUserList.value.filter(
-      (item) => item.socketId !== data.socketId
-    );
-    console.log('当前所有在线用户', JSON.stringify(res));
-    liveUserList.value = res;
-  });
-}
-
-function roomNameIsOk() {
-  if (!roomName.value.length) {
-    alert('请输入房间名!');
-    return false;
-  }
-  if (roomName.value.length < 3 || roomName.value.length > 10) {
-    alert('房间名要求3-10个字符!');
-    return false;
-  }
-  return true;
-}
-
-function confirmRoomName() {
-  if (!roomNameIsOk()) return;
-  if (!roomNameRef.value) return;
-  roomNameRef.value.disabled = true;
-}
-
-function endLive() {
-  console.log('endLive');
-  closeRtc();
-  currMediaTypeList.value = [];
-  localStream.value = null;
-  if (localVideoRef.value) {
-    localVideoRef.value.srcObject = null;
-  }
-  if (!websocketInstant.value) return;
-  websocketInstant.value.close();
-  // websocketInstant.value.send({
-  //   msgType: WsMsgTypeEnum.roomNoLive,
-  // });
-}
-
-function startLive() {
-  websocketInstant.value = new WebSocketClass({
-    roomId: roomId.value,
-    url:
-      process.env.NODE_ENV === 'development'
-        ? 'ws://localhost:4300'
-        : 'wss://live.hsslive.cn',
-    isAdmin: isAdmin.value,
-  });
-  websocketInstant.value.update();
-  setTimeout(() => {
-    websocketInstant.value!.socketIo?.emit(WsMsgTypeEnum.message, {
-      data: { debug: 1 },
-    });
-  }, 1000);
-
-  // initReceive();
-  // sendJoin();
-}
-
-const blobArr = ref<Blob[]>([]);
-/** 摄像头 */
-async function startMediaDevices() {
-  if (!localStream.value) {
-    // WARN navigator.mediaDevices在localhost和https才能用,http://192.168.1.103:8000局域网用不了
-    const event = await navigator.mediaDevices.getUserMedia({
-      video: true,
-      audio: false,
-    });
-    console.log('getUserMedia成功', event);
-    currMediaType.value = LiveTypeEnum.camera;
-    currMediaTypeList.value.push(LiveTypeEnum.camera);
-    if (!localVideoRef.value) return;
-    localVideoRef.value.srcObject = event;
-    // const rec = new MediaRecorder(event, { mimeType: 'image/png' });
-    // const rec = new MediaRecorder(event);
-    const rec = new MediaRecorder(event, {
-      // mimeType: 'video/webm;codecs=avc1.64001f,opus',
-      mimeType: 'video/webm',
-    });
-    rec.addEventListener('dataavailable', (e) => {
-      console.log(new Date().toLocaleString(), 'dataavailable');
-      if (e.data.size > 0) {
-        blobArr.value.push(e.data);
-      }
-      console.log(e.data.stream());
-      // document.querySelector<HTMLVideoElement>('#blobVideo')!.srcObject =
-      //   e.data.stream();
-      // const recordedBlob = new Blob([e.data], { type: 'video/webm' });
-      const recordedBlob = new Blob([e.data]);
-      console.log(recordedBlob);
-      // const url = window.URL.createObjectURL(recordedBlob);
-      // const a = document.createElement('a');
-      // a.style.display = 'none';
-      // a.href = url;
-      // a.download = 'test.webm';
-      // document.body.appendChild(a);
-      // a.click();
-      // setTimeout(() => {
-      //   document.body.removeChild(a);
-      //   window.URL.revokeObjectURL(url);
-      // }, 100);
-      if (!websocketInstant.value) return;
-      // websocketInstant.value.socketIo?.emit(WsMsgTypeEnum.sendBlob, e.data);
-      // websocketInstant.value.socketIo?.emit('aaa', { aa: recordedBlob });
-      websocketInstant.value.send({
-        msgType: WsMsgTypeEnum.sendBlob,
-        data: { blob: recordedBlob, timestamp: new Date().getTime() },
-      });
-    });
-    rec.start(500);
-
-    localStream.value = event;
-  }
-}
-/** 窗口 */
-async function startGetDisplayMedia() {
-  if (!localStream.value) {
-    // WARN navigator.mediaDevices.getDisplayMedia在localhost和https才能用,http://192.168.1.103:8000局域网用不了
-    const event = await navigator.mediaDevices.getDisplayMedia({
-      video: true,
-      audio: true,
-    });
-    console.log('getDisplayMedia成功', event);
-    currMediaType.value = LiveTypeEnum.screen;
-    currMediaTypeList.value.push(LiveTypeEnum.screen);
-    if (!localVideoRef.value) return;
-    localVideoRef.value.srcObject = event;
-    localStream.value = event;
-    const rec = new MediaStreamRecorder(event);
-    // const rec = new MediaRecorder(event, {
-    //   mimeType: 'video/webm',
-    // });
-    rec.ondataavailable = (blob) => {
-      if (!websocketInstant.value) return;
-      // websocketInstant.value.socketIo?.emit(WsMsgTypeEnum.sendBlob, e.data);
-      // websocketInstant.value.socketIo?.emit('aaa', { aa: recordedBlob });
-      websocketInstant.value.send({
-        msgType: WsMsgTypeEnum.sendBlob,
-        data: { blob, timestamp: new Date().getTime() },
-      });
-    };
-    rec.start(500);
-
-    // rec.addEventListener('dataavailable', (e) => {
-    //   console.log(new Date().toLocaleString(), 'dataavailable');
-    //   if (e.data.size <= 0) {
-    //     return;
-    //   }
-    //   blobArr.value.push(e.data);
-
-    //   console.log(e.data.stream());
-    //   // document.querySelector<HTMLVideoElement>('#blobVideo')!.srcObject =
-    //   //   e.data.stream();
-    //   // const recordedBlob = new Blob([e.data], { type: 'video/webm' });
-    //   // const res1 = blobArr.value.pop();
-    //   // const res2 = res1 ? [res1] : [];
-    //   // const recordedBlob = new Blob(res2, { type: 'video/webm' });
-    //   const recordedBlob = new Blob(blobArr.value, { type: 'video/webm' });
-    //   // const recordedBlob = new Blob([e.data]);
-    //   console.log(recordedBlob, blobArr.value.length, 222);
-    //   // const url = window.URL.createObjectURL(recordedBlob);
-    //   // const a = document.createElement('a');
-    //   // a.style.display = 'none';
-    //   // a.href = url;
-    //   // a.download = 'test.webm';
-    //   // document.body.appendChild(a);
-    //   // a.click();
-    //   // setTimeout(() => {
-    //   //   document.body.removeChild(a);
-    //   //   window.URL.revokeObjectURL(url);
-    //   // }, 100);
-    //   if (!websocketInstant.value) return;
-    //   // websocketInstant.value.socketIo?.emit(WsMsgTypeEnum.sendBlob, e.data);
-    //   // websocketInstant.value.socketIo?.emit('aaa', { aa: recordedBlob });
-    //   websocketInstant.value.send({
-    //     msgType: WsMsgTypeEnum.sendBlob,
-    //     data: { blob: recordedBlob, timestamp: new Date().getTime() },
-    //   });
-    //   // blobArr.value = [];
-    // });
-    // rec.start(500);
-  }
-}
-function addTrack() {
-  if (!localStream.value) return;
-  liveUserList.value.forEach((item) => {
-    if (item.socketId !== getSocketId()) {
-      localStream.value.getTracks().forEach((track) => {
-        const rtc = networkStore.getRtcMap(
-          `${roomId.value}___${item.socketId}`
-        );
-        rtc?.addTrack(track, localStream.value);
-      });
-    }
-  });
-}
-
-async function sendOffer({
-  sender,
-  receiver,
-}: {
-  sender: string;
-  receiver: string;
-}) {
-  if (isDone.value) return;
-  if (!websocketInstant.value) return;
-  const rtc = networkStore.getRtcMap(`${roomId.value}___${receiver}`);
-  if (!rtc) return;
-  const sdp = await rtc.createOffer();
-  await rtc.setLocalDescription(sdp);
-  websocketInstant.value.send({
-    msgType: WsMsgTypeEnum.offer,
-    data: { sdp, sender, receiver },
-  });
-}
-
-function startNewWebRtc(receiver: string) {
-  console.warn('开始new WebRTCClass', receiver);
-  const rtc = new WebRTCClass({ roomId: `${roomId.value}___${receiver}` });
-  rtc.rtcStatus.joined = true;
-  rtc.update();
-  return rtc;
-}
-
-function leave() {
-  if (joinRef.value && leaveRef.value && roomIdRef.value) {
-    roomIdRef.value.disabled = false;
-    joinRef.value.disabled = false;
-    leaveRef.value.disabled = true;
-  }
-  if (!websocketInstant.value) return;
-  websocketInstant.value.socketIo?.emit(WsMsgTypeEnum.leave, {
-    roomId: websocketInstant.value.roomId,
-  });
-}
-</script>
-
-<style lang="scss" scoped>
-.push-wrap {
-  margin: 20px auto 0;
-  min-width: $large-width;
-  height: 700px;
-  text-align: center;
-  .left {
-    position: relative;
-    display: inline-block;
-    box-sizing: border-box;
-    width: $large-left-width;
-    height: 100%;
-    border-radius: 10px;
-    background-color: white;
-    color: #9499a0;
-    vertical-align: top;
-
-    .video-wrap {
-      position: relative;
-      background-color: #18191c;
-      #localVideo {
-        max-width: 100%;
-        max-height: 100%;
-      }
-      .add-wrap {
-        position: absolute;
-        top: 50%;
-        left: 50%;
-        display: flex;
-        align-items: center;
-        justify-content: space-around;
-        width: 200px;
-        height: 50px;
-        background-color: #fff;
-        transform: translate(-50%, -50%);
-        .item {
-          width: 60px;
-          height: 30px;
-          border-radius: 4px;
-          background-color: rebeccapurple;
-          color: white;
-          font-size: 14px;
-          line-height: 30px;
-          cursor: pointer;
-        }
-      }
-    }
-    .control {
-      position: absolute;
-      right: 0;
-      bottom: 0;
-      left: 0;
-      display: flex;
-      justify-content: space-between;
-      padding: 20px;
-      background-color: papayawhip;
-
-      .info {
-        display: flex;
-        align-items: center;
-
-        .avatar {
-          margin-right: 20px;
-          width: 64px;
-          height: 64px;
-          border-radius: 50%;
-          background-color: yellow;
-        }
-        .detail {
-          display: flex;
-          flex-direction: column;
-          text-align: initial;
-          .top {
-            margin-bottom: 10px;
-            color: #18191c;
-            .btn {
-              margin-left: 10px;
-            }
-          }
-          .bottom {
-            font-size: 14px;
-          }
-        }
-      }
-      .other {
-        display: flex;
-        flex-direction: column;
-        justify-content: center;
-        font-size: 12px;
-        .top {
-          display: flex;
-          align-items: center;
-          .item {
-            display: flex;
-            align-items: center;
-            margin-right: 20px;
-            .ico {
-              display: inline-block;
-              margin-right: 4px;
-              width: 10px;
-              height: 10px;
-              border-radius: 50%;
-              background-color: skyblue;
-            }
-          }
-        }
-        .bottom {
-          margin-top: 10px;
-        }
-      }
-    }
-  }
-  .right {
-    position: relative;
-    display: inline-block;
-    box-sizing: border-box;
-    margin-left: 10px;
-    width: 240px;
-    height: 100%;
-    border-radius: 10px;
-    background-color: white;
-    color: #9499a0;
-
-    .resource-card {
-      margin-bottom: 5%;
-      margin-bottom: 10px;
-      width: 100%;
-      height: 290px;
-      border-radius: 4px;
-      background-color: papayawhip;
-      .title {
-        padding: 10px;
-        text-align: initial;
-      }
-      .item {
-        display: flex;
-        align-items: center;
-        justify-content: space-between;
-        margin-bottom: 10px;
-        font-size: 12px;
-      }
-    }
-    .danmu-card {
-      box-sizing: border-box;
-      padding: 10px;
-      width: 100%;
-      height: 400px;
-      border-radius: 4px;
-      background-color: papayawhip;
-      text-align: initial;
-      .title {
-        margin-bottom: 10px;
-      }
-      .list {
-        margin-bottom: 10px;
-        height: 300px;
-        .item {
-          margin-bottom: 10px;
-          font-size: 12px;
-          .name {
-            color: #9499a0;
-          }
-          .msg {
-            color: #61666d;
-          }
-        }
-      }
-
-      .send-msg {
-        display: flex;
-        align-items: center;
-        box-sizing: border-box;
-        .ipt {
-          display: block;
-          box-sizing: border-box;
-          margin: 0 auto;
-          margin-right: 10px;
-          padding: 10px;
-          width: 80%;
-          height: 30px;
-          outline: none;
-          border: 1px solid hsla(0, 0%, 60%, 0.2);
-          border-radius: 4px;
-          background-color: #f1f2f3;
-          font-size: 14px;
-        }
-        .btn {
-          box-sizing: border-box;
-          width: 80px;
-          height: 30px;
-          border-radius: 4px;
-          background-color: #23ade5;
-          color: white;
-          text-align: center;
-          font-size: 12px;
-          line-height: 30px;
-          cursor: pointer;
-        }
-      }
-    }
-  }
-}
-// 屏幕宽度小于$large-width的时候
-@media screen and (max-width: $large-width) {
-  .push-wrap {
-    .left {
-      width: $medium-left-width;
-    }
-    .right {
-      .list {
-        .item {
-        }
-      }
-    }
-  }
-}
-</style>

+ 75 - 37
src/views/home/index.vue

@@ -1,42 +1,49 @@
 <template>
   <div class="home-wrap">
-    <div
-      class="left"
-      :style="{ backgroundImage: `url(${currentLiveRoom?.coverImg})` }"
-    >
+    <div class="left">
+      <div
+        class="cover"
+        :style="{ backgroundImage: `url(${currentLiveRoom?.coverImg})` }"
+      ></div>
+      <!-- x-webkit-airplay这个属性应该是使此视频支持ios的AirPlay功能 -->
+      <!-- playsinline、 webkit-playsinline IOS微信浏览器支持小窗内播放 -->
+      <!-- x5-video-player-type 启用H5播放器,是wechat安卓版特性 -->
+      <!-- x5-video-player-fullscreen 全屏设置 -->
+      <!-- x5-video-orientation 声明播放器支持的方向,可选值landscape横屏,portraint竖屏。默认值portraint。 -->
       <video
         v-if="currentLiveRoom?.flvurl"
         id="localVideo"
         ref="localVideoRef"
         autoplay
-        webkit-playsinline="true"
         playsinline
+        webkit-playsinline="true"
         x-webkit-airplay="allow"
         x5-video-player-type="h5"
         x5-video-player-fullscreen="true"
         x5-video-orientation="portraint"
-        muted
-        controls
+        :muted="appStore.muted"
       ></video>
-      <div
-        v-if="currentLiveRoom"
-        class="btn-wrap"
-      >
-        <div
-          v-if="currentLiveRoom.system === 2"
-          class="btn webrtc"
-          @click="joinRoom()"
-        >
-          进入直播(webrtc)
+      <template v-if="currentLiveRoom">
+        <div class="controls">
+          <VideoControls></VideoControls>
         </div>
-        <div
-          v-if="currentLiveRoom?.flvurl"
-          class="btn flv"
-          @click="joinFlvRoom()"
-        >
-          进入直播(flv)
+        <div class="join-btn">
+          <div
+            v-if="currentLiveRoom.system === 2"
+            class="btn webrtc"
+            @click="joinRoom()"
+          >
+            进入直播(webrtc)
+          </div>
+          <div
+            v-if="currentLiveRoom?.flvurl"
+            class="btn flv"
+            @click="joinFlvRoom()"
+          >
+            进入直播(flv)
+          </div>
         </div>
-      </div>
+      </template>
     </div>
     <div class="right">
       <div
@@ -79,9 +86,11 @@ import { useRouter } from 'vue-router';
 
 import { fetchLiveList } from '@/api/live';
 import { useFlvPlay } from '@/hooks/use-play';
-import { ILive } from '@/interface';
+import { ILive, liveTypeEnum } from '@/interface';
 import { routerName } from '@/router';
+import { useAppStore } from '@/store/app';
 
+const appStore = useAppStore();
 const router = useRouter();
 const liveRoomList = ref<ILive[]>([]);
 const currentLiveRoom = ref<ILive>();
@@ -116,21 +125,34 @@ onMounted(() => {
 function joinRoom() {
   if (currentLiveRoom.value?.streamurl) {
     router.push({
-      name: routerName.srsWebRtcPull,
-      params: { roomId: currentLiveRoom.value.roomId },
+      name: routerName.pull,
+      params: {
+        roomId: currentLiveRoom.value.roomId,
+      },
+      query: {
+        liveType: liveTypeEnum.srsWebrtcPull,
+      },
     });
   } else {
     router.push({
-      name: routerName.webrtcPull,
-      params: { roomId: currentLiveRoom.value?.roomId },
+      name: routerName.pull,
+      params: {
+        roomId: currentLiveRoom.value?.roomId,
+      },
+      query: {
+        liveType: liveTypeEnum.webrtcPull,
+      },
     });
   }
 }
 
 function joinFlvRoom() {
   router.push({
-    name: routerName.srsFlvPull,
+    name: routerName.pull,
     params: { roomId: currentLiveRoom.value?.roomId },
+    query: {
+      liveType: liveTypeEnum.srsFlvPull,
+    },
   });
 }
 </script>
@@ -146,26 +168,43 @@ function joinFlvRoom() {
   .left {
     position: relative;
     display: inline-block;
+    overflow: hidden;
     box-sizing: border-box;
     width: $large-left-width;
     height: 610px;
     border-radius: 4px;
-    background-color: papayawhip;
+    background-color: rgba($color: #000000, $alpha: 0.3);
     vertical-align: top;
 
+    @extend %coverBg;
+
+    .cover {
+      position: absolute;
+      background-position: center center;
+      background-size: cover;
+      filter: blur(30px);
+
+      inset: 0;
+    }
+
     #localVideo {
+      position: relative;
       width: 100%;
       height: 100%;
     }
-
-    @extend %coverBg;
+    .controls {
+      display: none;
+    }
 
     &:hover {
-      .btn-wrap {
+      .join-btn {
         display: inline-flex;
       }
+      .controls {
+        display: block;
+      }
     }
-    .btn-wrap {
+    .join-btn {
       position: absolute;
       top: 50%;
       left: 50%;
@@ -174,14 +213,13 @@ function joinFlvRoom() {
       transform: translate(-50%, -50%);
 
       .btn {
-        cursor: pointer;
-
         padding: 14px 26px;
         border: 2px solid rgba($color: skyblue, $alpha: 0.5);
         border-radius: 6px;
         background-color: rgba(0, 0, 0, 0.3);
         color: skyblue;
         font-size: 16px;
+        cursor: pointer;
         &:hover {
           background-color: rgba($color: skyblue, $alpha: 0.5);
         }

+ 87 - 0
src/views/oauth/index.vue

@@ -0,0 +1,87 @@
+<template>
+  <div v-if="!errMsg.length">{{ currentOauth }}登录...</div>
+  <div v-else>非法登录!{{ errMsg }}</div>
+</template>
+
+<script lang="ts" setup>
+import { onMounted, ref } from 'vue';
+import { useRoute } from 'vue-router';
+
+import { handleLogin } from '@/hooks/use-login';
+import { PlatformEnum } from '@/interface';
+import { clearLoginInfo, getLoginInfo } from '@/utils/cookie';
+
+const route = useRoute();
+
+const errMsg = ref('');
+const currentOauth = ref('');
+
+onMounted(async () => {
+  const { platform } = route.params;
+  const { code, state } = route.query;
+  if (!code) {
+    errMsg.value = '地址栏缺少code';
+    return;
+  }
+  if (!state) {
+    errMsg.value = '地址栏缺少state';
+    return;
+  }
+
+  const atobStateRes = window.atob(window.decodeURIComponent(state as string));
+  let loginInfo = '';
+
+  try {
+    const res = JSON.parse(atobStateRes);
+    if (!res.dev) {
+      // 在第三方登录的时候,都会往cookie里记录环境,因此这里直接读取
+      loginInfo = getLoginInfo();
+      if (!loginInfo) {
+        errMsg.value = 'cookie缺少登录信息';
+        return;
+      }
+
+      if (state !== window.btoa(window.decodeURIComponent(loginInfo))) {
+        errMsg.value = 'state非法';
+        return;
+      }
+    } else {
+      loginInfo = atobStateRes;
+    }
+  } catch (error) {
+    errMsg.value = 'state非法';
+    return;
+  }
+
+  switch (platform) {
+    case PlatformEnum.qqLogin:
+      currentOauth.value = 'QQ';
+      break;
+  }
+
+  try {
+    const { isMobile, env } = JSON.parse(loginInfo);
+    const info = { type: PlatformEnum.qqLogin, data: code };
+
+    if (env === 'qq') {
+      if (isMobile) {
+        try {
+          await handleLogin({
+            data: info,
+          });
+        } catch (error) {
+          console.log(error);
+        }
+      } else {
+        window.opener.postMessage(info, '*');
+        window.close();
+      }
+    }
+  } catch (error) {
+    console.log(error);
+    clearLoginInfo();
+  }
+});
+</script>
+
+<style lang="scss" scoped></style>

+ 74 - 213
src/views/srs-flv-pull/index.vue → src/views/pull/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="srt-webrtc-pull-wrap">
+  <div class="srs-webrtc-pull-wrap">
     <template v-if="roomNoLive">当前房间没在直播~</template>
     <template v-else>
       <div class="left">
@@ -28,9 +28,11 @@
             x5-video-player-type="h5"
             x5-video-player-fullscreen="true"
             x5-video-orientation="portraint"
-            muted
-            controls
+            :muted="appStore.muted"
           ></video>
+          <div class="controls">
+            <VideoControls></VideoControls>
+          </div>
         </div>
         <div
           ref="bottomRef"
@@ -51,19 +53,33 @@
         <div class="tab">
           <span>在线用户</span>
           <span> | </span>
-          <span>大航海</span>
+          <span>排行榜</span>
         </div>
         <div class="user-list">
           <div
-            v-for="(item, index) in liveUserList"
+            v-for="(item, index) in liveUserList.filter((item) =>
+              userStore.userInfo ? item.socketId !== getSocketId() : true
+            )"
             :key="index"
             class="item"
           >
             <div class="info">
               <div class="avatar"></div>
-              <div class="nickname">{{ item.socketId }}</div>
+              <div class="username">{{ item.socketId }}</div>
+            </div>
+          </div>
+          <div
+            v-if="userStore.userInfo"
+            class="item"
+          >
+            <div class="info">
+              <img
+                :src="userStore.userInfo.avatar"
+                class="avatar"
+                alt=""
+              />
+              <div class="username">{{ userStore.userInfo.username }}</div>
             </div>
-            <div class="expr">{{ item.expr }}</div>
           </div>
         </div>
         <div class="danmu-list">
@@ -73,16 +89,22 @@
             class="item"
           >
             <template v-if="item.msgType === DanmuMsgTypeEnum.danmu">
-              <span class="name">{{ item.socketId }}:</span>
+              <span class="name">
+                {{ item.userInfo?.username || item.socketId }}:
+              </span>
               <span class="msg">{{ item.msg }}</span>
             </template>
             <template v-else-if="item.msgType === DanmuMsgTypeEnum.otherJoin">
               <span class="name system">系统通知:</span>
-              <span class="msg">{{ item.socketId }}进入直播!</span>
+              <span class="msg">
+                {{ item.userInfo?.username || item.socketId }}进入直播!
+              </span>
             </template>
             <template v-else-if="item.msgType === DanmuMsgTypeEnum.userLeaved">
               <span class="name system">系统通知:</span>
-              <span class="msg">{{ item.socketId }}离开直播!</span>
+              <span class="msg">
+                {{ item.userInfo?.username || item.socketId }}离开直播!
+              </span>
             </template>
           </div>
         </div>
@@ -90,6 +112,7 @@
           <textarea
             v-model="danmuStr"
             class="ipt"
+            @keydown="keydownDanmu"
           ></textarea>
           <div
             class="btn"
@@ -104,186 +127,44 @@
 </template>
 
 <script lang="ts" setup>
-import { onMounted, onUnmounted, reactive, ref } from 'vue';
+import { onMounted, onUnmounted, ref } from 'vue';
 import { useRoute } from 'vue-router';
 
-import { useFlvPlay } from '@/hooks/use-play';
-import { DanmuMsgTypeEnum, IAdminIn, IDanmu, ILiveUser } from '@/interface';
-import {
-  WebSocketClass,
-  WsConnectStatusEnum,
-  WsMsgTypeEnum,
-} from '@/network/webSocket';
-import { useNetworkStore } from '@/store/network';
+import { usePull } from '@/hooks/use-pull';
+import { DanmuMsgTypeEnum, liveTypeEnum } from '@/interface';
+import { useAppStore } from '@/store/app';
+import { useUserStore } from '@/store/user';
 
-const networkStore = useNetworkStore();
 const route = useRoute();
+const userStore = useUserStore();
+const appStore = useAppStore();
 
 const topRef = ref<HTMLDivElement>();
 const bottomRef = ref<HTMLDivElement>();
 const localVideoRef = ref<HTMLVideoElement>();
-const track = reactive({
-  audio: true,
-  video: true,
-});
-const streamurl = ref();
-const flvurl = ref();
-const roomNoLive = ref(false);
-const roomId = ref('');
-const roomName = ref('');
-const danmuStr = ref('');
-const websocketInstant = ref<WebSocketClass>();
-const damuList = ref<IDanmu[]>([]);
-const liveUserList = ref<ILiveUser[]>([]);
-const giftList = ref([
-  { name: '鲜花', ico: '', price: '免费' },
-  { name: '肥宅水', ico: '', price: '2元' },
-  { name: '小鸡腿', ico: '', price: '3元' },
-  { name: '大鸡腿', ico: '', price: '5元' },
-  { name: '一杯咖啡', ico: '', price: '10元' },
-]);
-
-function closeWs() {
-  const instance = networkStore.wsMap.get(roomId.value);
-  if (!instance) return;
-  instance.close();
-}
-
-function closeRtc() {
-  networkStore.rtcMap.forEach((rtc) => {
-    rtc.close();
-  });
-}
-
-function getSocketId() {
-  return networkStore.wsMap.get(roomId.value!)?.socketIo?.id || '-1';
-}
-
-function sendJoin() {
-  const instance = networkStore.wsMap.get(roomId.value);
-  if (!instance) return;
-  instance.send({ msgType: WsMsgTypeEnum.join, data: {} });
-}
-
-function sendDanmu() {
-  if (!danmuStr.value.length) {
-    alert('请输入弹幕内容!');
-  }
-  if (!websocketInstant.value) return;
-  websocketInstant.value.send({
-    msgType: WsMsgTypeEnum.message,
-    data: { msg: danmuStr.value },
-  });
-  damuList.value.push({
-    socketId: getSocketId(),
-    msgType: DanmuMsgTypeEnum.danmu,
-    msg: danmuStr.value,
-  });
-  danmuStr.value = '';
-}
-
-function initReceive() {
-  const instance = websocketInstant.value;
-  if (!instance?.socketIo) return;
-  // websocket连接成功
-  instance.socketIo.on(WsConnectStatusEnum.connect, () => {
-    console.log('【websocket】websocket连接成功', instance.socketIo?.id);
-    if (!instance) return;
-    instance.status = WsConnectStatusEnum.connect;
-    instance.update();
-  });
-
-  // websocket连接断开
-  instance.socketIo.on(WsConnectStatusEnum.disconnect, () => {
-    console.log('【websocket】websocket连接断开', instance);
-    if (!instance) return;
-    instance.status = WsConnectStatusEnum.disconnect;
-    instance.update();
-  });
-
-  // 当前所有在线用户
-  instance.socketIo.on(WsMsgTypeEnum.roomLiveing, (data: IAdminIn) => {
-    console.log('【websocket】收到管理员正在直播', data);
-  });
-
-  // 当前所有在线用户
-  instance.socketIo.on(WsMsgTypeEnum.roomNoLive, (data: IAdminIn) => {
-    console.log('【websocket】收到管理员不在直播', data);
-    roomNoLive.value = true;
-    closeRtc();
-  });
-
-  // 当前所有在线用户
-  instance.socketIo.on(WsMsgTypeEnum.liveUser, (data) => {
-    console.log('【websocket】当前所有在线用户', data);
-    if (!instance) return;
-    liveUserList.value = data.map((item) => ({
-      avatar: 'red',
-      socketId: item.id,
-      expr: 1,
-    }));
-  });
-
-  // 收到用户发送消息
-  instance.socketIo.on(WsMsgTypeEnum.message, (data) => {
-    console.log('【websocket】收到用户发送消息', data);
-    if (!instance) return;
-    damuList.value.push({
-      socketId: data.socketId,
-      msgType: DanmuMsgTypeEnum.danmu,
-      msg: data.data.msg,
-    });
-  });
-
-  // 用户加入房间
-  instance.socketIo.on(WsMsgTypeEnum.joined, (data) => {
-    console.log('【websocket】用户加入房间完成', data);
-    roomName.value = data.roomName;
-    track.audio = data.track_audio;
-    track.video = data.track_video;
-    streamurl.value = data.streamurl;
-    flvurl.value = data.flvurl;
-    useFlvPlay(flvurl.value, localVideoRef.value!);
-    instance.send({ msgType: WsMsgTypeEnum.getLiveUser });
-  });
-
-  // 其他用户加入房间
-  instance.socketIo.on(WsMsgTypeEnum.otherJoin, (data) => {
-    console.log('【websocket】其他用户加入房间', data);
-    damuList.value.push({
-      socketId: data.socketId,
-      msgType: DanmuMsgTypeEnum.otherJoin,
-      msg: '',
-    });
-  });
 
-  // 用户离开房间
-  instance.socketIo.on(WsMsgTypeEnum.leave, (data) => {
-    console.log('【websocket】用户离开房间', data);
-    if (!instance) return;
-    instance.socketIo?.emit(WsMsgTypeEnum.leave, {
-      roomId: instance.roomId,
-    });
-  });
-
-  // 用户离开房间完成
-  instance.socketIo.on(WsMsgTypeEnum.leaved, (data) => {
-    console.log('【websocket】用户离开房间完成', data);
-    if (!instance) return;
-    const res = liveUserList.value.filter(
-      (item) => item.socketId !== data.socketId
-    );
-    liveUserList.value = res;
-    damuList.value.push({
-      socketId: data.socketId,
-      msgType: DanmuMsgTypeEnum.userLeaved,
-      msg: '',
-    });
-  });
-}
+const {
+  initPull,
+  closeWs,
+  closeRtc,
+  getSocketId,
+  keydownDanmu,
+  sendDanmu,
+  roomName,
+  roomNoLive,
+  damuList,
+  giftList,
+  liveUserList,
+  danmuStr,
+} = usePull({
+  localVideoRef,
+  isFlv: route.query.liveType === liveTypeEnum.srsFlvPull,
+  isSRS: route.query.liveType === liveTypeEnum.srsWebrtcPull,
+});
 
 onUnmounted(() => {
   closeWs();
+  closeRtc();
 });
 
 onMounted(() => {
@@ -294,40 +175,12 @@ onMounted(() => {
         topRef.value.getBoundingClientRect().height);
     localVideoRef.value.style.height = `${res}px`;
   }
-  roomId.value = route.params.roomId as string;
-  console.warn('开始new WebSocketClass');
-  websocketInstant.value = new WebSocketClass({
-    roomId: roomId.value,
-    url:
-      process.env.NODE_ENV === 'development'
-        ? 'ws://localhost:4300'
-        : 'wss://live.hsslive.cn',
-    isAdmin: false,
-  });
-  websocketInstant.value.update();
-  initReceive();
-  sendJoin();
-
-  localVideoRef.value?.addEventListener('loadstart', () => {
-    console.warn('视频流-loadstart');
-    const rtc = networkStore.getRtcMap(roomId.value);
-    if (!rtc) return;
-    rtc.rtcStatus.loadstart = true;
-    rtc.update();
-  });
-
-  localVideoRef.value?.addEventListener('loadedmetadata', () => {
-    console.warn('视频流-loadedmetadata');
-    const rtc = networkStore.getRtcMap(roomId.value);
-    if (!rtc) return;
-    rtc.rtcStatus.loadedmetadata = true;
-    rtc.update();
-  });
+  initPull();
 });
 </script>
 
 <style lang="scss" scoped>
-.srt-webrtc-pull-wrap {
+.srs-webrtc-pull-wrap {
   margin: 20px auto 0;
   min-width: $large-width;
   height: 700px;
@@ -338,10 +191,11 @@ onMounted(() => {
     box-sizing: border-box;
     width: $large-left-width;
     height: 100%;
-    border-radius: 10px;
+    border-radius: 6px;
     background-color: white;
     color: #9499a0;
     vertical-align: top;
+    overflow: hidden;
     .head {
       display: flex;
       justify-content: space-between;
@@ -407,13 +261,20 @@ onMounted(() => {
       }
     }
     .video-wrap {
-      // height: 100px;
-      // height: 550px;
+      position: relative;
       background-color: #18191c;
       #localVideo {
         max-width: 100%;
         max-height: 100%;
       }
+      .controls {
+        display: none;
+      }
+      &:hover {
+        .controls {
+          display: block;
+        }
+      }
     }
     .gift {
       position: absolute;
@@ -483,7 +344,7 @@ onMounted(() => {
             border-radius: 50%;
             background-color: skyblue;
           }
-          .nickname {
+          .username {
             color: black;
           }
         }
@@ -546,7 +407,7 @@ onMounted(() => {
 
 // 屏幕宽度小于$large-width的时候
 @media screen and (max-width: $large-width) {
-  .srt-webrtc-pull-wrap {
+  .srs-webrtc-pull-wrap {
     .left {
       width: $medium-left-width;
     }

+ 415 - 0
src/views/push/index.vue

@@ -0,0 +1,415 @@
+<template>
+  <div class="webrtc-push-wrap">
+    <div
+      ref="topRef"
+      class="left"
+    >
+      <div class="video-wrap">
+        <video
+          id="localVideo"
+          ref="localVideoRef"
+          autoplay
+          webkit-playsinline="true"
+          playsinline
+          x-webkit-airplay="allow"
+          x5-video-player-type="h5"
+          x5-video-player-fullscreen="true"
+          x5-video-orientation="portraint"
+          muted
+        ></video>
+        <div
+          v-if="!currMediaTypeList || currMediaTypeList.length <= 0"
+          class="add-wrap"
+        >
+          <n-space>
+            <n-button
+              class="item"
+              @click="startGetUserMedia"
+            >
+              摄像头
+            </n-button>
+            <n-button
+              class="item"
+              @click="startGetDisplayMedia"
+            >
+              窗口
+            </n-button>
+          </n-space>
+        </div>
+      </div>
+      <div
+        ref="bottomRef"
+        class="control"
+      >
+        <div class="info">
+          <div
+            class="avatar"
+            :style="{ backgroundImage: `url(${userStore.userInfo?.avatar})` }"
+          ></div>
+          <div class="detail">
+            <div class="top">
+              <n-input-group>
+                <n-input
+                  v-model:value="roomName"
+                  size="small"
+                  placeholder="输入房间名"
+                  :style="{ width: '50%' }"
+                  :disabled="disabled"
+                />
+                <n-button
+                  size="small"
+                  type="primary"
+                  :disabled="disabled"
+                  @click="confirmRoomName"
+                >
+                  确定
+                </n-button>
+              </n-input-group>
+            </div>
+            <div class="bottom">
+              <span>socketId:{{ getSocketId() }}</span>
+            </div>
+          </div>
+        </div>
+        <div class="other">
+          <div class="top">
+            <span class="item">
+              <i class="ico"></i>
+              <span>正在观看人数:{{ liveUserList.length }}</span>
+            </span>
+          </div>
+          <div class="bottom">
+            <n-space>
+              <n-button
+                type="info"
+                size="small"
+                @click="startLive"
+              >
+                开始直播
+              </n-button>
+              <n-button
+                type="info"
+                size="small"
+                @click="endLive"
+              >
+                结束直播
+              </n-button>
+            </n-space>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="right">
+      <div class="resource-card">
+        <div class="title">素材列表</div>
+        <div class="list">
+          <div
+            v-for="(item, index) in currMediaTypeList"
+            :key="index"
+            class="item"
+          >
+            <span class="name">{{ item.txt }}</span>
+          </div>
+        </div>
+      </div>
+      <div class="danmu-card">
+        <div class="title">弹幕互动</div>
+        <div class="list-wrap">
+          <div class="list">
+            <div
+              v-for="(item, index) in damuList"
+              :key="index"
+              class="item"
+            >
+              <template v-if="item.msgType === DanmuMsgTypeEnum.danmu">
+                <span class="name">{{ item.socketId }}:</span>
+                <span class="msg">{{ item.msg }}</span>
+              </template>
+              <template v-else-if="item.msgType === DanmuMsgTypeEnum.otherJoin">
+                <span class="name system">系统通知:</span>
+                <span class="msg">{{ item.socketId }}进入直播!</span>
+              </template>
+              <template
+                v-else-if="item.msgType === DanmuMsgTypeEnum.userLeaved"
+              >
+                <span class="name system">系统通知:</span>
+                <span class="msg">{{ item.socketId }}离开直播!</span>
+              </template>
+            </div>
+          </div>
+        </div>
+        <div class="send-msg">
+          <input
+            v-model="danmuStr"
+            class="ipt"
+            @keydown="keydownDanmu"
+          />
+          <n-button
+            type="info"
+            size="small"
+            @click="sendDanmu"
+          >
+            发送
+          </n-button>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { onMounted, onUnmounted, ref } from 'vue';
+import { useRoute } from 'vue-router';
+
+import { usePush } from '@/hooks/use-push';
+import { DanmuMsgTypeEnum, liveTypeEnum } from '@/interface';
+import { useUserStore } from '@/store/user';
+
+const route = useRoute();
+const userStore = useUserStore();
+
+const topRef = ref<HTMLDivElement>();
+const bottomRef = ref<HTMLDivElement>();
+const localVideoRef = ref<HTMLVideoElement>();
+const liveType = route.query.liveType;
+
+const {
+  initPush,
+  confirmRoomName,
+  getSocketId,
+  startGetDisplayMedia,
+  startGetUserMedia,
+  startLive,
+  endLive,
+  closeWs,
+  closeRtc,
+  sendDanmu,
+  keydownDanmu,
+  disabled,
+  danmuStr,
+  roomName,
+  damuList,
+  liveUserList,
+  currMediaTypeList,
+} = usePush({
+  localVideoRef,
+  isSRS: liveType === liveTypeEnum.srsPush,
+});
+
+onUnmounted(() => {
+  closeWs();
+  closeRtc();
+});
+
+onMounted(() => {
+  initPush();
+  if (topRef.value && bottomRef.value && localVideoRef.value) {
+    const res =
+      bottomRef.value.getBoundingClientRect().top -
+      topRef.value.getBoundingClientRect().top;
+    localVideoRef.value.style.height = `${res}px`;
+  }
+});
+</script>
+
+<style lang="scss" scoped>
+.webrtc-push-wrap {
+  margin: 20px auto 0;
+  min-width: $large-width;
+  height: 700px;
+  text-align: center;
+  .left {
+    position: relative;
+    display: inline-block;
+    box-sizing: border-box;
+    width: $large-left-width;
+    height: 100%;
+    border-radius: 6px;
+    overflow: hidden;
+    background-color: white;
+    color: #9499a0;
+    vertical-align: top;
+
+    .video-wrap {
+      position: relative;
+      background-color: #18191c;
+      #localVideo {
+        max-width: 100%;
+        max-height: 100%;
+      }
+      .add-wrap {
+        position: absolute;
+        top: 50%;
+        left: 50%;
+        display: flex;
+        align-items: center;
+        justify-content: space-around;
+        padding: 0 20px;
+        height: 50px;
+        border-radius: 5px;
+        background-color: white;
+        transform: translate(-50%, -50%);
+      }
+    }
+    .control {
+      position: absolute;
+      right: 0;
+      bottom: 0;
+      left: 0;
+      display: flex;
+      justify-content: space-between;
+      padding: 20px;
+      background-color: papayawhip;
+
+      .info {
+        display: flex;
+        align-items: center;
+
+        .avatar {
+          margin-right: 20px;
+          width: 64px;
+          height: 64px;
+          border-radius: 50%;
+          background-color: skyblue;
+          background-position: center center;
+          background-size: cover;
+          background-repeat: no-repeat;
+        }
+        .detail {
+          display: flex;
+          flex-direction: column;
+          text-align: initial;
+          .top {
+            margin-bottom: 10px;
+            color: #18191c;
+            .btn {
+              margin-left: 10px;
+            }
+          }
+          .bottom {
+            font-size: 14px;
+          }
+        }
+      }
+      .other {
+        display: flex;
+        flex-direction: column;
+        justify-content: center;
+        font-size: 12px;
+        .top {
+          display: flex;
+          align-items: center;
+          .item {
+            display: flex;
+            align-items: center;
+            margin-right: 20px;
+            .ico {
+              display: inline-block;
+              margin-right: 4px;
+              width: 10px;
+              height: 10px;
+              border-radius: 50%;
+              background-color: skyblue;
+            }
+          }
+        }
+        .bottom {
+          margin-top: 10px;
+        }
+      }
+    }
+  }
+  .right {
+    position: relative;
+    display: inline-block;
+    box-sizing: border-box;
+    margin-left: 10px;
+    width: 240px;
+    height: 100%;
+    border-radius: 6px;
+    background-color: white;
+    color: #9499a0;
+
+    .resource-card {
+      box-sizing: border-box;
+      margin-bottom: 5%;
+      margin-bottom: 10px;
+      padding: 10px;
+      width: 100%;
+      height: 290px;
+      border-radius: 6px;
+      background-color: papayawhip;
+      .title {
+        text-align: initial;
+      }
+      .item {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        margin: 5px 0;
+        font-size: 12px;
+      }
+    }
+    .danmu-card {
+      box-sizing: border-box;
+      padding: 10px;
+      width: 100%;
+      height: 400px;
+      border-radius: 6px;
+      background-color: papayawhip;
+      text-align: initial;
+      .title {
+        margin-bottom: 10px;
+      }
+      .list {
+        margin-bottom: 10px;
+        height: 300px;
+        .item {
+          margin-bottom: 10px;
+          font-size: 12px;
+          .name {
+            color: #9499a0;
+          }
+          .msg {
+            color: #61666d;
+          }
+        }
+      }
+
+      .send-msg {
+        display: flex;
+        align-items: center;
+        box-sizing: border-box;
+        .ipt {
+          display: block;
+          box-sizing: border-box;
+          margin: 0 auto;
+          margin-right: 10px;
+          padding: 10px;
+          width: 80%;
+          height: 30px;
+          outline: none;
+          border: 1px solid hsla(0, 0%, 60%, 0.2);
+          border-radius: 4px;
+          background-color: #f1f2f3;
+          font-size: 14px;
+        }
+      }
+    }
+  }
+}
+// 屏幕宽度小于$large-width的时候
+@media screen and (max-width: $large-width) {
+  .webrtc-push-wrap {
+    .left {
+      width: $medium-left-width;
+    }
+    .right {
+      .list {
+        .item {
+        }
+      }
+    }
+  }
+}
+</style>

+ 218 - 0
src/views/rank/index.vue

@@ -0,0 +1,218 @@
+<template>
+  <div class="rank-wrap">
+    <div class="type-list">
+      <div
+        v-for="(item, index) in rankType"
+        :key="index"
+        :class="{ item: 1, active: item.type === currRankType }"
+        @click="currRankType = item.type"
+      >
+        {{ item.label }}
+      </div>
+    </div>
+
+    <div class="rank-list">
+      <div class="top">
+        <div
+          v-for="(item, index) in [rankList[1], rankList[0], rankList[2]]"
+          :key="index"
+          :class="{ item: 1, [`rank-${item.rank}`]: 1 }"
+        >
+          <div class="avatar">
+            <img
+              :src="item.avatar"
+              alt=""
+            />
+            <div class="border"></div>
+          </div>
+          <div class="username">{{ item.username }}</div>
+          <div class="rank">
+            <i>0{{ item.rank }}</i>
+          </div>
+        </div>
+      </div>
+      <div class="top50-list">
+        <div
+          v-for="(item, index) in rankList.filter((item, index) => index >= 3)"
+          :key="index"
+          class="top50-item"
+        >
+          <div class="rank">
+            <i>{{ item.rank > 10 ? item.rank : '0' + item.rank }}</i>
+          </div>
+          <div class="left">
+            <img
+              :src="item.avatar"
+              class="avatar"
+              alt=""
+            />
+            <div class="username">{{ item.username }}</div>
+          </div>
+          <div class="right"></div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue';
+
+const rankType = ref([
+  {
+    type: 1,
+    label: '主播榜',
+  },
+  {
+    type: 2,
+    label: '打赏榜',
+  },
+  {
+    type: 3,
+    label: '等级榜',
+  },
+]);
+
+const currRankType = ref(1);
+
+const rankList = ref([
+  { username: '111', avatar: 'sss', rank: 1, level: 1, score: 111 },
+  { username: '22', avatar: '222', rank: 2, level: 1, score: 111 },
+  { username: '333', avatar: '333', rank: 3, level: 1, score: 111 },
+  { username: '444', avatar: '444', rank: 4, level: 1, score: 111 },
+  { username: '55', avatar: '555', rank: 5, level: 1, score: 111 },
+  { username: '66', avatar: '666', rank: 6, level: 1, score: 111 },
+  { username: '77', avatar: '777', rank: 7, level: 1, score: 111 },
+]);
+</script>
+
+<style lang="scss" scoped>
+.rank-wrap {
+  box-sizing: border-box;
+  padding-top: 10px;
+  height: calc(100vh - 64px);
+  background-color: #f4f4f4;
+  .type-list {
+    display: flex;
+    align-items: center;
+    margin: 20px 0;
+    width: 100%;
+    .item {
+      flex: 1;
+      margin: 0 10px;
+      height: 40px;
+      border-radius: 10px;
+      background-color: pink;
+      color: white;
+      text-align: center;
+      font-weight: bold;
+      font-size: 20px;
+      line-height: 40px;
+      filter: grayscale(1);
+      cursor: pointer;
+
+      &.active {
+        filter: grayscale(0);
+      }
+    }
+  }
+  .rank-list {
+    width: 100%;
+
+    .top {
+      display: flex;
+      align-items: flex-end;
+      justify-content: center;
+      margin-top: 100px;
+      width: 100%;
+      .item {
+        position: relative;
+        margin: 0 20px;
+        width: 200px;
+        height: 180px;
+        border-radius: 15px;
+        background-color: white;
+        text-align: center;
+        &.rank-1 {
+          height: 200px;
+          border-color: #ff6744;
+          color: #ff6744;
+          .rank {
+            margin-top: 20px;
+          }
+        }
+        &.rank-2 {
+          border-color: #44d6ff;
+          color: #44d6ff;
+        }
+        &.rank-3 {
+          border-color: #ffb200;
+          color: #ffb200;
+        }
+
+        .avatar {
+          position: relative;
+          margin-top: -50px;
+          img {
+            display: inline-block;
+            margin: 0 auto;
+            width: 100px;
+            height: 100px;
+            border-radius: 50%;
+            background-color: pink;
+          }
+        }
+
+        .username {
+          margin-bottom: 10px;
+          font-size: 22px;
+        }
+        .rank {
+          display: inline-block;
+          padding: 0px 20px;
+          border: 1px solid;
+          border-radius: 20px;
+          font-size: 20px;
+        }
+      }
+    }
+    .top50-list {
+      margin-top: 20px;
+      border-radius: 10px;
+      background-color: white;
+      .top50-item {
+        display: flex;
+        align-items: center;
+        padding: 0 10px;
+        height: 40px;
+        color: #666;
+        &:nth-child(2n) {
+          background-color: #fafbfc;
+        }
+        .rank {
+          margin-right: 20px;
+          padding: 0 20px;
+          border-radius: 40px;
+          background-color: #84f9da;
+          color: white;
+          font-size: 20px;
+        }
+        .left {
+          display: flex;
+          align-items: center;
+          .avatar {
+            margin-right: 10px;
+            width: 28px;
+            height: 28px;
+            border-radius: 50%;
+            background-color: pink;
+          }
+          .username {
+            font-size: 12px;
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 1 - 1
src/views/sponsors/index.vue

@@ -79,7 +79,7 @@ import { hrefToTarget, isMobile } from 'billd-utils';
 import QRCode from 'qrcode';
 import { onMounted, onUnmounted, ref } from 'vue';
 
-import { fetchAliPay, fetchAliPayList, fetchAliPayStatus } from '@/api/aliPay';
+import { fetchAliPay, fetchAliPayList, fetchAliPayStatus } from '@/api/order';
 
 const payOk = ref(false);
 const onMountedTime = ref('');

+ 0 - 741
src/views/srs-webrtc-push/index.vue

@@ -1,741 +0,0 @@
-<template>
-  <div class="srs-webrtc-push-wrap">
-    <div
-      ref="topRef"
-      class="left"
-    >
-      <div class="video-wrap">
-        <video
-          id="localVideo"
-          ref="localVideoRef"
-          autoplay
-          webkit-playsinline="true"
-          playsinline
-          x-webkit-airplay="allow"
-          x5-video-player-type="h5"
-          x5-video-player-fullscreen="true"
-          x5-video-orientation="portraint"
-          muted
-          controls
-        ></video>
-        <div
-          v-if="!currMediaTypeList || currMediaTypeList.length <= 0"
-          class="add-wrap"
-        >
-          <div
-            class="item"
-            @click="startMediaDevices"
-          >
-            摄像头
-          </div>
-          <div
-            class="item"
-            @click="startGetDisplayMedia"
-          >
-            窗口
-          </div>
-        </div>
-      </div>
-      <div
-        ref="bottomRef"
-        class="control"
-      >
-        <div class="info">
-          <div class="avatar"></div>
-          <div class="detail">
-            <div class="top">
-              <input
-                ref="roomNameRef"
-                v-model="roomName"
-                type="text"
-                placeholder="输入房间名"
-              />
-              <button
-                ref="roomNameBtnRef"
-                class="btn"
-                @click="confirmRoomName"
-              >
-                确定
-              </button>
-            </div>
-            <div class="bottom">
-              <span>socketId:{{ getSocketId() }}</span>
-            </div>
-          </div>
-        </div>
-        <div class="other">
-          <div class="top">
-            <span class="item">
-              <i class="ico"></i>
-              <span>正在观看人数:{{ liveUserList.length }}</span>
-            </span>
-          </div>
-          <div class="bottom">
-            <button @click="startLive">开始直播</button>
-            <button @click="endLive">结束直播</button>
-          </div>
-        </div>
-      </div>
-    </div>
-    <div class="right">
-      <div class="resource-card">
-        <div class="title">素材列表</div>
-        <div class="list">
-          <div
-            v-for="(item, index) in currMediaTypeList"
-            :key="index"
-            class="item"
-          >
-            <span class="name">{{ item.txt }}</span>
-          </div>
-        </div>
-      </div>
-      <div class="danmu-card">
-        <div class="title">弹幕互动</div>
-        <div class="list-wrap">
-          <div class="list">
-            <div
-              v-for="(item, index) in damuList"
-              :key="index"
-              class="item"
-            >
-              <template v-if="item.msgType === DanmuMsgTypeEnum.danmu">
-                <span class="name">{{ item.socketId }}:</span>
-                <span class="msg">{{ item.msg }}</span>
-              </template>
-              <template v-else-if="item.msgType === DanmuMsgTypeEnum.otherJoin">
-                <span class="name system">系统通知:</span>
-                <span class="msg">{{ item.socketId }}进入直播!</span>
-              </template>
-              <template
-                v-else-if="item.msgType === DanmuMsgTypeEnum.userLeaved"
-              >
-                <span class="name system">系统通知:</span>
-                <span class="msg">{{ item.socketId }}离开直播!</span>
-              </template>
-            </div>
-          </div>
-        </div>
-        <div class="send-msg">
-          <input
-            v-model="danmuStr"
-            class="ipt"
-          />
-          <div
-            class="btn"
-            @click="sendDanmu"
-          >
-            发送
-          </div>
-        </div>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script lang="ts" setup>
-import { getRandomString } from 'billd-utils';
-import { onMounted, onUnmounted, reactive, ref } from 'vue';
-
-import { fetchRtcV1Publish } from '@/api/srs';
-import {
-  DanmuMsgTypeEnum,
-  IAdminIn,
-  IDanmu,
-  ILiveUser,
-  LiveTypeEnum,
-} from '@/interface';
-import { SRSWebRTCClass } from '@/network/srsWebRtc';
-import {
-  WebSocketClass,
-  WsConnectStatusEnum,
-  WsMsgTypeEnum,
-} from '@/network/webSocket';
-import router from '@/router';
-import { useNetworkStore } from '@/store/network';
-
-const networkStore = useNetworkStore();
-
-const topRef = ref<HTMLDivElement>();
-const bottomRef = ref<HTMLDivElement>();
-const roomNameRef = ref<HTMLInputElement>();
-const roomNameBtnRef = ref<HTMLButtonElement>();
-const localVideoRef = ref<HTMLVideoElement>();
-
-const roomId = ref<string>(getRandomString(15));
-const danmuStr = ref('');
-const roomName = ref('');
-const localStream = ref();
-const track = reactive({
-  audio: true,
-  video: true,
-});
-const streamurl = ref(
-  `webrtc://${
-    process.env.NODE_ENV === 'development' ? 'localhost' : 'live.hsslive.cn'
-  }/live/livestream/${roomId.value}`
-);
-const flvurl = ref(
-  `${
-    process.env.NODE_ENV === 'development'
-      ? 'http://localhost:5001'
-      : 'https://live.hsslive.cn/srsflv'
-  }/live/livestream/${roomId.value}.flv`
-);
-
-const websocketInstant = ref<WebSocketClass>();
-const damuList = ref<IDanmu[]>([]);
-const liveUserList = ref<ILiveUser[]>([]);
-
-const allMediaTypeList = {
-  [LiveTypeEnum.camera]: {
-    type: LiveTypeEnum.camera,
-    txt: '摄像头',
-  },
-  [LiveTypeEnum.screen]: {
-    type: LiveTypeEnum.screen,
-    txt: '窗口',
-  },
-};
-const currMediaType = ref<{
-  type: LiveTypeEnum;
-  txt: string;
-}>();
-const currMediaTypeList = ref<
-  {
-    type: LiveTypeEnum;
-    txt: string;
-  }[]
->([]);
-
-function handleCoverImg() {
-  const canvas = document.createElement('canvas');
-  const { width, height } = localVideoRef.value!.getBoundingClientRect();
-  const rate = width / height;
-  const coverWidth = width * 0.5;
-  const coverHeight = coverWidth / rate;
-  canvas.width = coverWidth;
-  canvas.height = coverHeight;
-  canvas
-    .getContext('2d')!
-    .drawImage(localVideoRef.value!, 0, 0, coverWidth, coverHeight);
-  // webp比png的体积小非常多!因此coverWidth就可以不用压缩太夸张
-  const dataURL = canvas.toDataURL('image/webp');
-  return dataURL;
-}
-
-function closeWs() {
-  const instance = networkStore.wsMap.get(roomId.value);
-  if (!instance) return;
-  instance.close();
-}
-
-function closeRtc() {
-  networkStore.rtcMap.forEach((rtc) => {
-    rtc.close();
-  });
-}
-
-function sendDanmu() {
-  if (!danmuStr.value.length) {
-    alert('请输入弹幕内容!');
-  }
-  if (!websocketInstant.value) return;
-  websocketInstant.value.send({
-    msgType: WsMsgTypeEnum.message,
-    data: { msg: danmuStr.value },
-  });
-  damuList.value.push({
-    socketId: getSocketId(),
-    msgType: DanmuMsgTypeEnum.danmu,
-    msg: danmuStr.value,
-  });
-  danmuStr.value = '';
-}
-
-onUnmounted(() => {
-  closeWs();
-  closeRtc();
-});
-
-onMounted(() => {
-  router.push({ query: { roomId: roomId.value } });
-  if (topRef.value && bottomRef.value && localVideoRef.value) {
-    const res =
-      bottomRef.value.getBoundingClientRect().top -
-      topRef.value.getBoundingClientRect().top;
-    localVideoRef.value.style.height = `100px`;
-    localVideoRef.value.style.height = `${res}px`;
-  }
-  localVideoRef.value?.addEventListener('loadstart', () => {
-    console.warn('视频流-loadstart');
-    const rtc = networkStore.getRtcMap(roomId.value);
-    if (!rtc) return;
-    rtc.rtcStatus.loadstart = true;
-    rtc.update();
-  });
-
-  localVideoRef.value?.addEventListener('loadedmetadata', () => {
-    console.warn('视频流-loadedmetadata');
-    const rtc = networkStore.getRtcMap(roomId.value);
-    if (!rtc) return;
-    rtc.rtcStatus.loadedmetadata = true;
-    rtc.update();
-  });
-});
-
-function getSocketId() {
-  return networkStore.wsMap.get(roomId.value!)?.socketIo?.id || '-1';
-}
-
-function initReceive() {
-  const instance = websocketInstant.value;
-  if (!instance?.socketIo) return;
-  // websocket连接成功
-  instance.socketIo.on(WsConnectStatusEnum.connect, () => {
-    console.log('【websocket】websocket连接成功', instance.socketIo?.id);
-    if (!instance) return;
-    instance.status = WsConnectStatusEnum.connect;
-    instance.update();
-    sendJoin();
-  });
-
-  // websocket连接断开
-  instance.socketIo.on(WsConnectStatusEnum.disconnect, () => {
-    console.log('【websocket】websocket连接断开', instance);
-    if (!instance) return;
-    instance.status = WsConnectStatusEnum.disconnect;
-    instance.update();
-  });
-
-  // 当前所有在线用户
-  instance.socketIo.on(WsMsgTypeEnum.roomLiveing, (data: IAdminIn) => {
-    console.log('【websocket】收到管理员正在直播', data);
-  });
-
-  // 当前所有在线用户
-  instance.socketIo.on(WsMsgTypeEnum.liveUser, () => {
-    console.log('【websocket】当前所有在线用户');
-    if (!instance) return;
-  });
-
-  // 收到用户发送消息
-  instance.socketIo.on(WsMsgTypeEnum.message, (data) => {
-    console.log('【websocket】收到用户发送消息', data);
-    if (!instance) return;
-    damuList.value.push({
-      socketId: data.socketId,
-      msgType: DanmuMsgTypeEnum.danmu,
-      msg: data.data.msg,
-    });
-  });
-
-  // 用户加入房间完成
-  instance.socketIo.on(WsMsgTypeEnum.joined, (data) => {
-    console.log('【websocket】用户加入房间完成', data);
-    liveUserList.value.push({
-      avatar: 'red',
-      socketId: `${getSocketId()}`,
-      expr: 1,
-    });
-    handleSrsPush();
-  });
-
-  // 其他用户加入房间
-  instance.socketIo.on(WsMsgTypeEnum.otherJoin, (data) => {
-    console.log('【websocket】其他用户加入房间', data);
-    liveUserList.value.push({
-      avatar: 'red',
-      socketId: data.socketId,
-      expr: 1,
-    });
-    damuList.value.push({
-      socketId: data.socketId,
-      msgType: DanmuMsgTypeEnum.otherJoin,
-      msg: '',
-    });
-  });
-
-  // 用户离开房间
-  instance.socketIo.on(WsMsgTypeEnum.leave, (data) => {
-    console.log('【websocket】用户离开房间', data);
-    if (!instance) return;
-    instance.socketIo?.emit(WsMsgTypeEnum.leave, {
-      roomId: instance.roomId,
-    });
-  });
-
-  // 用户离开房间完成
-  instance.socketIo.on(WsMsgTypeEnum.leaved, (data) => {
-    console.log('【websocket】用户离开房间完成', data);
-    const res = liveUserList.value.filter(
-      (item) => item.socketId !== data.socketId
-    );
-    liveUserList.value = res;
-    damuList.value.push({
-      socketId: data.socketId,
-      msgType: DanmuMsgTypeEnum.userLeaved,
-      msg: '',
-    });
-  });
-}
-
-function roomNameIsOk() {
-  if (!roomName.value.length) {
-    alert('请输入房间名!');
-    return false;
-  }
-  if (roomName.value.length < 3 || roomName.value.length > 10) {
-    alert('房间名要求3-10个字符!');
-    return false;
-  }
-  return true;
-}
-
-function confirmRoomName() {
-  if (!roomNameIsOk()) return;
-  if (!roomNameRef.value) return;
-  roomNameRef.value.disabled = true;
-}
-
-/** 结束直播 */
-function endLive() {
-  roomNameBtnRef.value!.disabled = false;
-  closeRtc();
-  currMediaTypeList.value = [];
-  localStream.value = null;
-  localVideoRef.value!.srcObject = null;
-  const instance = networkStore.wsMap.get(roomId.value);
-  if (!instance) return;
-  instance.send({
-    msgType: WsMsgTypeEnum.roomNoLive,
-    data: {},
-  });
-  setTimeout(() => {
-    instance.close();
-  }, 500);
-}
-
-function sendJoin() {
-  const instance = networkStore.wsMap.get(roomId.value);
-  if (!instance) return;
-  instance.send({
-    msgType: WsMsgTypeEnum.join,
-    data: {
-      roomName: roomName.value,
-      coverImg: handleCoverImg(),
-      srs: {
-        streamurl: streamurl.value,
-        flvurl: flvurl.value,
-      },
-      track,
-    },
-  });
-}
-
-function startLive() {
-  if (!roomNameIsOk()) return;
-  if (currMediaTypeList.value.length <= 0) {
-    alert('请选择一个素材!');
-    return;
-  }
-  roomNameBtnRef.value!.disabled = true;
-  websocketInstant.value = new WebSocketClass({
-    roomId: roomId.value,
-    url:
-      process.env.NODE_ENV === 'development'
-        ? 'ws://localhost:4300'
-        : 'wss://live.hsslive.cn',
-    isAdmin: true,
-  });
-  websocketInstant.value.update();
-  initReceive();
-}
-
-async function handleSrsPush() {
-  const rtc = new SRSWebRTCClass({
-    roomId: `${roomId.value}___${getSocketId()}`,
-  });
-  localStream.value.getTracks().forEach((track) => {
-    rtc.addTrack({ track, stream: localStream.value, direction: 'sendonly' });
-  });
-  try {
-    const offer = await rtc.createOffer();
-    if (!offer) return;
-    await rtc.setLocalDescription(offer);
-    const res: any = await fetchRtcV1Publish({
-      api: `${
-        process.env.NODE_ENV === 'development'
-          ? 'http://localhost:1985'
-          : 'https://live.hsslive.cn/srs'
-      }/rtc/v1/publish/`,
-      clientip: null,
-      sdp: offer.sdp!,
-      streamurl: streamurl.value,
-      tid: getRandomString(10),
-    });
-    await rtc.setRemoteDescription(
-      new RTCSessionDescription({ type: 'answer', sdp: res.sdp })
-    );
-  } catch (error) {
-    console.log(error);
-  }
-}
-
-/** 摄像头 */
-async function startMediaDevices() {
-  if (!localStream.value) {
-    // WARN navigator.mediaDevices在localhost和https才能用,http://192.168.1.103:8000局域网用不了
-    const event = await navigator.mediaDevices.getUserMedia({
-      video: true,
-      audio: true,
-    });
-    console.log('getUserMedia成功', event);
-    currMediaType.value = allMediaTypeList[LiveTypeEnum.camera];
-    currMediaTypeList.value.push(allMediaTypeList[LiveTypeEnum.camera]);
-    if (!localVideoRef.value) return;
-    localVideoRef.value.srcObject = event;
-    localStream.value = event;
-  }
-}
-
-/** 窗口 */
-async function startGetDisplayMedia() {
-  if (!localStream.value) {
-    // WARN navigator.mediaDevices.getDisplayMedia在localhost和https才能用,http://192.168.1.103:8000局域网用不了
-    const event = await navigator.mediaDevices.getDisplayMedia({
-      video: true,
-      audio: true,
-    });
-    const audio = event.getAudioTracks();
-    const video = event.getVideoTracks();
-    track.audio = !!audio.length;
-    track.video = !!video.length;
-    console.log('getDisplayMedia成功', event);
-    currMediaType.value = allMediaTypeList[LiveTypeEnum.screen];
-    currMediaTypeList.value.push(allMediaTypeList[LiveTypeEnum.screen]);
-    if (!localVideoRef.value) return;
-    localVideoRef.value.srcObject = event;
-    localStream.value = event;
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-.srs-webrtc-push-wrap {
-  margin: 20px auto 0;
-  min-width: $large-width;
-  height: 700px;
-  text-align: center;
-  .left {
-    position: relative;
-    display: inline-block;
-    box-sizing: border-box;
-    width: $large-left-width;
-    height: 100%;
-    border-radius: 10px;
-    background-color: white;
-    color: #9499a0;
-    vertical-align: top;
-
-    .video-wrap {
-      position: relative;
-      background-color: #18191c;
-      #localVideo {
-        max-width: 100%;
-        max-height: 100%;
-      }
-      .add-wrap {
-        position: absolute;
-        top: 50%;
-        left: 50%;
-        display: flex;
-        align-items: center;
-        justify-content: space-around;
-        width: 200px;
-        height: 50px;
-        background-color: #fff;
-        transform: translate(-50%, -50%);
-        .item {
-          width: 60px;
-          height: 30px;
-          border-radius: 4px;
-          background-color: skyblue;
-          color: white;
-          font-size: 14px;
-          line-height: 30px;
-          cursor: pointer;
-        }
-      }
-    }
-    .control {
-      position: absolute;
-      right: 0;
-      bottom: 0;
-      left: 0;
-      display: flex;
-      justify-content: space-between;
-      padding: 20px;
-      background-color: papayawhip;
-
-      .info {
-        display: flex;
-        align-items: center;
-
-        .avatar {
-          margin-right: 20px;
-          width: 64px;
-          height: 64px;
-          border-radius: 50%;
-          background-color: skyblue;
-        }
-        .detail {
-          display: flex;
-          flex-direction: column;
-          text-align: initial;
-          .top {
-            margin-bottom: 10px;
-            color: #18191c;
-            .btn {
-              margin-left: 10px;
-            }
-          }
-          .bottom {
-            font-size: 14px;
-          }
-        }
-      }
-      .other {
-        display: flex;
-        flex-direction: column;
-        justify-content: center;
-        font-size: 12px;
-        .top {
-          display: flex;
-          align-items: center;
-          .item {
-            display: flex;
-            align-items: center;
-            margin-right: 20px;
-            .ico {
-              display: inline-block;
-              margin-right: 4px;
-              width: 10px;
-              height: 10px;
-              border-radius: 50%;
-              background-color: skyblue;
-            }
-          }
-        }
-        .bottom {
-          margin-top: 10px;
-        }
-      }
-    }
-  }
-  .right {
-    position: relative;
-    display: inline-block;
-    box-sizing: border-box;
-    margin-left: 10px;
-    width: 240px;
-    height: 100%;
-    border-radius: 10px;
-    background-color: white;
-    color: #9499a0;
-
-    .resource-card {
-      box-sizing: border-box;
-      margin-bottom: 5%;
-      margin-bottom: 10px;
-      padding: 10px;
-      width: 100%;
-      height: 290px;
-      border-radius: 6px;
-      background-color: papayawhip;
-      .title {
-        text-align: initial;
-      }
-      .item {
-        display: flex;
-        align-items: center;
-        justify-content: space-between;
-        margin: 5px 0;
-        font-size: 12px;
-      }
-    }
-    .danmu-card {
-      box-sizing: border-box;
-      padding: 10px;
-      width: 100%;
-      height: 400px;
-      border-radius: 4px;
-      background-color: papayawhip;
-      text-align: initial;
-      .title {
-        margin-bottom: 10px;
-      }
-      .list {
-        margin-bottom: 10px;
-        height: 300px;
-        .item {
-          margin-bottom: 10px;
-          font-size: 12px;
-          .name {
-            color: #9499a0;
-          }
-          .msg {
-            color: #61666d;
-          }
-        }
-      }
-
-      .send-msg {
-        display: flex;
-        align-items: center;
-        box-sizing: border-box;
-        .ipt {
-          display: block;
-          box-sizing: border-box;
-          margin: 0 auto;
-          margin-right: 10px;
-          padding: 10px;
-          width: 80%;
-          height: 30px;
-          outline: none;
-          border: 1px solid hsla(0, 0%, 60%, 0.2);
-          border-radius: 4px;
-          background-color: #f1f2f3;
-          font-size: 14px;
-        }
-        .btn {
-          box-sizing: border-box;
-          width: 80px;
-          height: 30px;
-          border-radius: 4px;
-          background-color: skyblue;
-          color: white;
-          text-align: center;
-          font-size: 12px;
-          line-height: 30px;
-          cursor: pointer;
-        }
-      }
-    }
-  }
-}
-// 屏幕宽度小于$large-width的时候
-@media screen and (max-width: $large-width) {
-  .srs-webrtc-push-wrap {
-    .left {
-      width: $medium-left-width;
-    }
-    .right {
-      .list {
-        .item {
-        }
-      }
-    }
-  }
-}
-</style>

+ 0 - 854
src/views/test1/index.vue

@@ -1,854 +0,0 @@
-<template>
-  <div class="push-wrap">
-    <div
-      ref="topRef"
-      class="left"
-    >
-      <div class="jsmpeg"></div>
-      <video
-        id="blobVideo"
-        style="width: 300px; background-color: red"
-      ></video>
-      <button @click="startRtmp">startRtmp</button>
-      <button @click="startmpeg">startmpeg</button>
-      <div class="video-wrap">
-        <video
-          id="localVideo"
-          ref="localVideoRef"
-          autoplay
-          webkit-playsinline="true"
-          playsinline
-          x-webkit-airplay="allow"
-          x5-video-player-type="h5"
-          x5-video-player-fullscreen="true"
-          x5-video-orientation="portraint"
-          muted
-          controls
-        ></video>
-        <div
-          v-if="currMediaTypeList.length <= 0"
-          class="add-wrap"
-        >
-          <div
-            class="item"
-            @click="startMediaDevices"
-          >
-            摄像头
-          </div>
-          <div
-            class="item"
-            @click="startGetDisplayMedia"
-          >
-            窗口
-          </div>
-        </div>
-      </div>
-      <div
-        ref="bottomRef"
-        class="control"
-      >
-        <div class="info">
-          <div class="avatar"></div>
-          <div class="detail">
-            <div class="top">
-              <input
-                ref="roomNameRef"
-                v-model="roomName"
-                type="text"
-                placeholder="输入房间名"
-              />
-              <button
-                class="btn"
-                @click="confirmRoomName"
-              >
-                确定
-              </button>
-              <!-- 房东的猫livehouse/音乐节 -->
-            </div>
-            <div class="bottom">
-              <span>socketId:{{ getSocketId() }}</span>
-            </div>
-          </div>
-        </div>
-        <div class="other">
-          <div class="top">
-            <span class="item">
-              <i class="ico"></i>
-              <span>正在观看人数:{{ liveUserList.length }}</span>
-            </span>
-          </div>
-          <div class="bottom">
-            <button @click="startLive">开始直播</button>
-            <button @click="endLive">结束直播</button>
-          </div>
-        </div>
-      </div>
-    </div>
-    <div class="right">
-      <div class="resource-card">
-        <div class="title">素材列表</div>
-        <div class="list">
-          <div
-            v-for="(item, index) in currMediaTypeList"
-            :key="index"
-            class="item"
-          >
-            <span class="name">{{ item }}</span>
-          </div>
-        </div>
-      </div>
-      <div class="danmu-card">
-        <div class="title">弹幕互动</div>
-        <div class="list">
-          <div
-            v-for="(item, index) in damuList"
-            :key="index"
-            class="item"
-          >
-            <span class="name">{{ item.socketId }}:</span>
-            <span class="msg">{{ item.msg }}</span>
-          </div>
-        </div>
-
-        <div class="send-msg">
-          <input
-            v-model="danmuStr"
-            class="ipt"
-          />
-          <div
-            class="btn"
-            @click="sendDanmu"
-          >
-            发送
-          </div>
-        </div>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script lang="ts" setup>
-import { getRandomString } from 'billd-utils';
-import { onMounted, onUnmounted, ref } from 'vue';
-
-import { IAdminIn, ICandidate, IOffer, LiveTypeEnum } from '@/interface';
-import { WebRTCClass } from '@/network/webRtc';
-import {
-  WebSocketClass,
-  WsConnectStatusEnum,
-  WsMsgTypeEnum,
-} from '@/network/webSocket';
-import { useNetworkStore } from '@/store/network';
-
-const networkStore = useNetworkStore();
-
-const topRef = ref<HTMLDivElement>();
-const bottomRef = ref<HTMLDivElement>();
-const roomIdRef = ref<HTMLInputElement>();
-const joinRef = ref<HTMLButtonElement>();
-const leaveRef = ref<HTMLButtonElement>();
-const defaultRoomId = getRandomString(15);
-const roomId = ref<string>(defaultRoomId);
-const danmuStr = ref('');
-const roomName = ref('');
-const roomNameRef = ref<HTMLInputElement>();
-const websocketInstant = ref<WebSocketClass>();
-const isDone = ref(false);
-const localVideoRef = ref<HTMLVideoElement>();
-const localStream = ref();
-const currMediaTypeList = ref<LiveTypeEnum[]>([]);
-const currMediaType = ref<LiveTypeEnum>();
-const joined = ref(false);
-const isAdmin = ref(true);
-const offerSended = ref(new Set());
-const damuList = ref<
-  {
-    socketId: string;
-    msgType: number;
-    msg: string;
-  }[]
->([]);
-
-const liveUserList = ref<
-  {
-    socketId: string;
-    avatar: string;
-    expr: number;
-  }[]
->([]);
-
-function closeWs() {
-  const instance = networkStore.wsMap.get(roomId.value);
-  if (!instance) return;
-  instance.close();
-}
-
-function closeRtc() {
-  networkStore.rtcMap.forEach((rtc) => {
-    rtc.close();
-  });
-}
-
-function sendDanmu() {
-  if (!danmuStr.value.length) {
-    alert('请输入弹幕内容!');
-  }
-  if (!websocketInstant.value) return;
-  websocketInstant.value.send({
-    msgType: WsMsgTypeEnum.message,
-    data: { msg: danmuStr.value },
-  });
-  damuList.value.push({
-    socketId: getSocketId(),
-    msgType: 1,
-    msg: danmuStr.value,
-  });
-  danmuStr.value = '';
-}
-
-onUnmounted(() => {
-  closeWs();
-  closeRtc();
-});
-let flvPlayer;
-
-function startRtmp() {
-  try {
-    flvPlayer.play();
-  } catch (error) {
-    console.log(error);
-  }
-}
-function startmpeg() {
-  console.log(JSMpeg, JSMpeg.Player);
-  const res1 = new JSMpeg.Player('./out.ts', { autoplay: true });
-  console.log(res1);
-}
-
-onMounted(() => {
-  // if (flvJs.isSupported()) {
-  //   flvPlayer = flvJs.createPlayer({
-  //     type: 'flv',
-  //     url: 'http://localhost:8080/live/show.flv',
-  //     // url: 'http://42.193.157.44:9000/live/fddm_2.flv',
-  //     // url: 'https://www.hsslive.cn/stream/live/fddm_2.flv',
-  //     // url: 'https://www.hsslive.cn/bilibilistream/live-bvc/173676/live_381307133_59434826.flv?expires=1680798150&len=0&oi=1900220676&pt=web&qn=0&trid=10002eb4d1a458684cc9a93d888d7b580cac&sigparams=cdn,expires,len,oi,pt,qn,trid&cdn=cn-gotcha15&sign=1724a76684e5e1f8d68346613a32f8c0&sk=3e2a893893799632504afbdef4a9e3d7&p2p_type=1&sl=1&free_type=0&mid=381307133&sche=ban&score=1&pp=rtmp&freeze=1&source=onetier&trace=10&site=12e2a543d23e1c612a7145eb5a5cfac4&order=1',
-  //     // url: 'https://xy117x149x235x203xy.mcdn.bilivideo.cn/live-bvc/173676/live_381307133_59434826.flv?expires=1680798150&len=0&oi=1900220676&pt=web&qn=0&trid=10002eb4d1a458684cc9a93d888d7b580cac&sigparams=cdn,expires,len,oi,pt,qn,trid&cdn=cn-gotcha15&sign=1724a76684e5e1f8d68346613a32f8c0&sk=3e2a893893799632504afbdef4a9e3d7&p2p_type=1&sl=1&free_type=0&mid=381307133&sche=ban&score=1&pp=rtmp&freeze=1&source=onetier&trace=10&site=12e2a543d23e1c612a7145eb5a5cfac4&order=1',
-  //   });
-  //   // @ts-ignore
-  //   flvPlayer.attachMediaElement(
-  //     document.querySelector<HTMLVideoElement>('#blobVideo')
-  //   );
-  //   flvPlayer.load();
-  // }
-  if (topRef.value && bottomRef.value && localVideoRef.value) {
-    const res =
-      bottomRef.value.getBoundingClientRect().top -
-      topRef.value.getBoundingClientRect().top;
-    localVideoRef.value.style.height = `${res}px`;
-  }
-  localVideoRef.value?.addEventListener('loadstart', () => {
-    console.warn('视频流-loadstart');
-    const rtc = networkStore.getRtcMap(roomId.value);
-    if (!rtc) return;
-    rtc.rtcStatus.loadstart = true;
-    rtc.update();
-  });
-
-  localVideoRef.value?.addEventListener('loadedmetadata', () => {
-    console.warn('视频流-loadedmetadata');
-    const rtc = networkStore.getRtcMap(roomId.value);
-    if (!rtc) return;
-    rtc.rtcStatus.loadedmetadata = true;
-    rtc.update();
-    if (isAdmin.value) {
-      batchSendOffer();
-    }
-  });
-});
-
-function getSocketId() {
-  return networkStore.wsMap.get(roomId.value!)?.socketIo?.id || '-1';
-}
-
-function sendJoin() {
-  const instance = networkStore.wsMap.get(roomId.value);
-  if (!instance) return;
-  instance.send({
-    msgType: WsMsgTypeEnum.join,
-    data: {
-      roomName: roomName.value,
-    },
-  });
-}
-
-function batchSendOffer() {
-  liveUserList.value.forEach(async (item) => {
-    if (
-      !offerSended.value.has(item.socketId) &&
-      item.socketId !== getSocketId()
-    ) {
-      await startNewWebRtc(item.socketId);
-      await addTrack();
-      console.warn('new WebRTCClass完成');
-      console.log('执行sendOffer', {
-        sender: getSocketId(),
-        receiver: item.socketId,
-      });
-      sendOffer({ sender: getSocketId(), receiver: item.socketId });
-      offerSended.value.add(item.socketId);
-    }
-  });
-}
-
-function initReceive() {
-  const instance = websocketInstant.value;
-  if (!instance?.socketIo) return;
-  // websocket连接成功
-  instance.socketIo.on(WsConnectStatusEnum.connect, () => {
-    console.log('【websocket】websocket连接成功', instance.socketIo?.id);
-    if (!instance) return;
-    instance.status = WsConnectStatusEnum.connect;
-    instance.update();
-  });
-
-  // websocket连接断开
-  instance.socketIo.on(WsConnectStatusEnum.disconnect, () => {
-    console.log('【websocket】websocket连接断开', instance);
-    if (!instance) return;
-    instance.status = WsConnectStatusEnum.disconnect;
-    instance.update();
-  });
-
-  // 当前所有在线用户
-  instance.socketIo.on(WsMsgTypeEnum.roomLiveing, (data: IAdminIn) => {
-    console.log('【websocket】收到管理员正在直播', data);
-  });
-
-  // 当前所有在线用户
-  instance.socketIo.on(WsMsgTypeEnum.liveUser, () => {
-    console.log('【websocket】当前所有在线用户');
-    if (!instance) return;
-  });
-
-  // 收到offer
-  instance.socketIo.on(WsMsgTypeEnum.offer, async (data: IOffer) => {
-    console.warn('【websocket】收到offer', data);
-    if (!instance) return;
-    if (data.data.receiver === getSocketId()) {
-      console.log('收到offer,这个offer是发给我的');
-      const rtc = startNewWebRtc(data.data.sender);
-      await rtc.setRemoteDescription(data.data.sdp);
-      const sdp = await rtc.createAnswer();
-      await rtc.setLocalDescription(sdp);
-      websocketInstant.value?.send({
-        msgType: WsMsgTypeEnum.answer,
-        data: { sdp, sender: getSocketId(), receiver: data.data.sender },
-      });
-    } else {
-      console.log('收到offer,但是这个offer不是发给我的');
-    }
-  });
-
-  // 收到answer
-  instance.socketIo.on(WsMsgTypeEnum.answer, async (data: IOffer) => {
-    console.warn('【websocket】收到answer', data);
-    if (isDone.value) return;
-    if (!instance) return;
-    const rtc = networkStore.getRtcMap(`${roomId.value}___${data.socketId}`);
-    console.log(rtc, '收到answer收到answer');
-    if (!rtc) return;
-    rtc.rtcStatus.answer = true;
-    rtc.update();
-    if (data.data.receiver === getSocketId()) {
-      console.log('收到answer,这个answer是发给我的');
-      await rtc.setRemoteDescription(data.data.sdp);
-    } else {
-      console.log('收到answer,但这个answer不是发给我的');
-    }
-  });
-
-  // 收到candidate
-  instance.socketIo.on(WsMsgTypeEnum.candidate, (data: ICandidate) => {
-    console.warn('【websocket】收到candidate', data);
-    if (isDone.value) return;
-    if (!instance) return;
-    const rtc =
-      networkStore.getRtcMap(`${roomId.value}___${data.socketId}`) ||
-      networkStore.getRtcMap(roomId.value);
-    if (!rtc) return;
-    if (data.socketId !== getSocketId()) {
-      console.log('不是我发的candidate');
-      const candidate = new RTCIceCandidate({
-        sdpMid: data.data.sdpMid,
-        sdpMLineIndex: data.data.sdpMLineIndex,
-        candidate: data.data.candidate,
-      });
-      rtc.peerConnection
-        ?.addIceCandidate(candidate)
-        .then(() => {
-          console.log('candidate成功');
-          // rtc.handleStream();
-        })
-        .catch((err) => {
-          console.error('candidate失败', err);
-        });
-    } else {
-      console.log('是我发的candidate');
-    }
-  });
-
-  // 收到用户发送消息
-  instance.socketIo.on(WsMsgTypeEnum.message, (data) => {
-    console.log('【websocket】收到用户发送消息', data);
-    if (!instance) return;
-    damuList.value.push({
-      socketId: data.socketId,
-      msgType: 1,
-      msg: data.data.msg,
-    });
-  });
-
-  // 用户加入房间
-  instance.socketIo.on(WsMsgTypeEnum.join, (data) => {
-    console.log('【websocket】用户加入房间', data);
-    if (!instance) return;
-  });
-
-  // 用户加入房间
-  instance.socketIo.on(WsMsgTypeEnum.joined, (data) => {
-    console.log('【websocket】用户加入房间完成', data);
-    joined.value = true;
-    liveUserList.value.push({
-      avatar: 'red',
-      socketId: `${getSocketId()}`,
-      expr: 1,
-    });
-    batchSendOffer();
-  });
-
-  // 其他用户加入房间
-  instance.socketIo.on(WsMsgTypeEnum.otherJoin, (data) => {
-    console.log('【websocket】其他用户加入房间', data);
-    liveUserList.value.push({
-      avatar: 'red',
-      socketId: data.socketId,
-      expr: 1,
-    });
-    console.log('当前所有在线用户', JSON.stringify(liveUserList.value));
-    console.log(isAdmin.value, joined.value);
-    if (isAdmin.value && joined.value) {
-      batchSendOffer();
-    }
-  });
-
-  // 用户离开房间
-  instance.socketIo.on(WsMsgTypeEnum.leave, (data) => {
-    console.log('【websocket】用户离开房间', data);
-    if (!instance) return;
-    instance.socketIo?.emit(WsMsgTypeEnum.leave, {
-      roomId: instance.roomId,
-    });
-  });
-
-  // 用户离开房间完成
-  instance.socketIo.on(WsMsgTypeEnum.leaved, (data) => {
-    console.log('【websocket】用户离开房间完成', data);
-    if (!instance) return;
-    const res = liveUserList.value.filter(
-      (item) => item.socketId !== data.socketId
-    );
-    console.log('当前所有在线用户', JSON.stringify(res));
-    liveUserList.value = res;
-  });
-}
-
-function roomNameIsOk() {
-  if (!roomName.value.length) {
-    alert('请输入房间名!');
-    return false;
-  }
-  if (roomName.value.length < 3 || roomName.value.length > 10) {
-    alert('房间名要求3-10个字符!');
-    return false;
-  }
-  return true;
-}
-
-function confirmRoomName() {
-  if (!roomNameIsOk()) return;
-  if (!roomNameRef.value) return;
-  roomNameRef.value.disabled = true;
-}
-
-function endLive() {
-  console.log('endLive');
-  closeRtc();
-  currMediaTypeList.value = [];
-  localStream.value = null;
-  if (localVideoRef.value) {
-    localVideoRef.value.srcObject = null;
-  }
-  if (!websocketInstant.value) return;
-  websocketInstant.value.send({
-    msgType: WsMsgTypeEnum.roomNoLive,
-  });
-}
-
-function startLive() {
-  websocketInstant.value = new WebSocketClass({
-    roomId: roomId.value,
-    url:
-      process.env.NODE_ENV === 'development'
-        ? 'ws://localhost:4300'
-        : 'wss://live.hsslive.cn',
-    isAdmin: isAdmin.value,
-  });
-  websocketInstant.value.update();
-  setTimeout(() => {
-    websocketInstant.value!.socketIo?.emit(WsMsgTypeEnum.message, {
-      data: { debug: 1 },
-    });
-  }, 1000);
-
-  // initReceive();
-  // sendJoin();
-}
-
-const blobArr = ref<Blob[]>([]);
-/** 摄像头 */
-async function startMediaDevices() {
-  if (!localStream.value) {
-    // WARN navigator.mediaDevices在localhost和https才能用,http://192.168.1.103:8000局域网用不了
-    const event = await navigator.mediaDevices.getUserMedia({
-      video: true,
-      audio: false,
-    });
-    console.log('getUserMedia成功', event);
-    currMediaType.value = LiveTypeEnum.camera;
-    currMediaTypeList.value.push(LiveTypeEnum.camera);
-    if (!localVideoRef.value) return;
-    localVideoRef.value.srcObject = event;
-    const rec = new MediaRecorder(event, { mimeType: 'image/png' });
-    // const rec = new MediaRecorder(event, { mimeType: 'video/webm' });
-    console.log('rec', rec);
-    rec.addEventListener('dataavailable', (e) => {
-      console.log(new Date().toLocaleString(), 'dataavailable');
-      if (e.data.size > 0) {
-        blobArr.value.push(e.data);
-      }
-      console.log(e.data.stream());
-      // document.querySelector<HTMLVideoElement>('#blobVideo')!.srcObject =
-      //   e.data.stream();
-      const recordedBlob = new Blob(blobArr.value, { type: 'video/webm' });
-      console.log(recordedBlob);
-      // const url = window.URL.createObjectURL(recordedBlob);
-      // const a = document.createElement('a');
-      // a.style.display = 'none';
-      // a.href = url;
-      // a.download = 'test.webm';
-      // document.body.appendChild(a);
-      // a.click();
-      // setTimeout(() => {
-      //   document.body.removeChild(a);
-      //   window.URL.revokeObjectURL(url);
-      // }, 100);
-      if (!websocketInstant.value) return;
-      // websocketInstant.value.socketIo?.emit(WsMsgTypeEnum.sendBlob, e.data);
-      websocketInstant.value.send({
-        msgType: WsMsgTypeEnum.sendBlob,
-        data: { blob: recordedBlob, timestamp: new Date().getTime() },
-      });
-    });
-    rec.start(1000);
-
-    localStream.value = event;
-  }
-}
-/** 窗口 */
-async function startGetDisplayMedia() {
-  if (!localStream.value) {
-    // WARN navigator.mediaDevices.getDisplayMedia在localhost和https才能用,http://192.168.1.103:8000局域网用不了
-    const event = await navigator.mediaDevices.getDisplayMedia({
-      video: true,
-      audio: true,
-    });
-    console.log('getDisplayMedia成功', event);
-    currMediaType.value = LiveTypeEnum.screen;
-    currMediaTypeList.value.push(LiveTypeEnum.screen);
-    if (!localVideoRef.value) return;
-    localVideoRef.value.srcObject = event;
-    localStream.value = event;
-  }
-}
-function addTrack() {
-  if (!localStream.value) return;
-  liveUserList.value.forEach((item) => {
-    if (item.socketId !== getSocketId()) {
-      localStream.value.getTracks().forEach((track) => {
-        const rtc = networkStore.getRtcMap(
-          `${roomId.value}___${item.socketId}`
-        );
-        rtc?.addTrack(track, localStream.value);
-      });
-    }
-  });
-}
-
-async function sendOffer({
-  sender,
-  receiver,
-}: {
-  sender: string;
-  receiver: string;
-}) {
-  if (isDone.value) return;
-  if (!websocketInstant.value) return;
-  const rtc = networkStore.getRtcMap(`${roomId.value}___${receiver}`);
-  if (!rtc) return;
-  const sdp = await rtc.createOffer();
-  await rtc.setLocalDescription(sdp);
-  websocketInstant.value.send({
-    msgType: WsMsgTypeEnum.offer,
-    data: { sdp, sender, receiver },
-  });
-}
-
-function startNewWebRtc(receiver: string) {
-  console.warn('开始new WebRTCClass', receiver);
-  const rtc = new WebRTCClass({ roomId: `${roomId.value}___${receiver}` });
-  rtc.rtcStatus.joined = true;
-  rtc.update();
-  return rtc;
-}
-
-function leave() {
-  if (joinRef.value && leaveRef.value && roomIdRef.value) {
-    roomIdRef.value.disabled = false;
-    joinRef.value.disabled = false;
-    leaveRef.value.disabled = true;
-  }
-  if (!websocketInstant.value) return;
-  websocketInstant.value.socketIo?.emit(WsMsgTypeEnum.leave, {
-    roomId: websocketInstant.value.roomId,
-  });
-}
-</script>
-
-<style lang="scss" scoped>
-.push-wrap {
-  margin: 20px auto 0;
-  min-width: $large-width;
-  height: 700px;
-  text-align: center;
-  .left {
-    position: relative;
-    display: inline-block;
-    box-sizing: border-box;
-    width: $large-left-width;
-    height: 100%;
-    border-radius: 10px;
-    background-color: white;
-    color: #9499a0;
-    vertical-align: top;
-
-    .video-wrap {
-      position: relative;
-      background-color: #18191c;
-      #localVideo {
-        max-width: 100%;
-        max-height: 100%;
-      }
-      .add-wrap {
-        position: absolute;
-        top: 50%;
-        left: 50%;
-        display: flex;
-        align-items: center;
-        justify-content: space-around;
-        width: 200px;
-        height: 50px;
-        background-color: #fff;
-        transform: translate(-50%, -50%);
-        .item {
-          width: 60px;
-          height: 30px;
-          border-radius: 4px;
-          background-color: rebeccapurple;
-          color: white;
-          font-size: 14px;
-          line-height: 30px;
-          cursor: pointer;
-        }
-      }
-    }
-    .control {
-      position: absolute;
-      right: 0;
-      bottom: 0;
-      left: 0;
-      display: flex;
-      justify-content: space-between;
-      padding: 20px;
-      background-color: papayawhip;
-
-      .info {
-        display: flex;
-        align-items: center;
-
-        .avatar {
-          margin-right: 20px;
-          width: 64px;
-          height: 64px;
-          border-radius: 50%;
-          background-color: yellow;
-        }
-        .detail {
-          display: flex;
-          flex-direction: column;
-          text-align: initial;
-          .top {
-            margin-bottom: 10px;
-            color: #18191c;
-            .btn {
-              margin-left: 10px;
-            }
-          }
-          .bottom {
-            font-size: 14px;
-          }
-        }
-      }
-      .other {
-        display: flex;
-        flex-direction: column;
-        justify-content: center;
-        font-size: 12px;
-        .top {
-          display: flex;
-          align-items: center;
-          .item {
-            display: flex;
-            align-items: center;
-            margin-right: 20px;
-            .ico {
-              display: inline-block;
-              margin-right: 4px;
-              width: 10px;
-              height: 10px;
-              border-radius: 50%;
-              background-color: skyblue;
-            }
-          }
-        }
-        .bottom {
-          margin-top: 10px;
-        }
-      }
-    }
-  }
-  .right {
-    position: relative;
-    display: inline-block;
-    box-sizing: border-box;
-    margin-left: 10px;
-    width: 240px;
-    height: 100%;
-    border-radius: 10px;
-    background-color: white;
-    color: #9499a0;
-
-    .resource-card {
-      margin-bottom: 5%;
-      margin-bottom: 10px;
-      width: 100%;
-      height: 290px;
-      border-radius: 4px;
-      background-color: papayawhip;
-      .title {
-        padding: 10px;
-        text-align: initial;
-      }
-      .item {
-        display: flex;
-        align-items: center;
-        justify-content: space-between;
-        margin-bottom: 10px;
-        font-size: 12px;
-      }
-    }
-    .danmu-card {
-      box-sizing: border-box;
-      padding: 10px;
-      width: 100%;
-      height: 400px;
-      border-radius: 4px;
-      background-color: papayawhip;
-      text-align: initial;
-      .title {
-        margin-bottom: 10px;
-      }
-      .list {
-        margin-bottom: 10px;
-        height: 300px;
-        .item {
-          margin-bottom: 10px;
-          font-size: 12px;
-          .name {
-            color: #9499a0;
-          }
-          .msg {
-            color: #61666d;
-          }
-        }
-      }
-
-      .send-msg {
-        display: flex;
-        align-items: center;
-        box-sizing: border-box;
-        .ipt {
-          display: block;
-          box-sizing: border-box;
-          margin: 0 auto;
-          margin-right: 10px;
-          padding: 10px;
-          width: 80%;
-          height: 30px;
-          outline: none;
-          border: 1px solid hsla(0, 0%, 60%, 0.2);
-          border-radius: 4px;
-          background-color: #f1f2f3;
-          font-size: 14px;
-        }
-        .btn {
-          box-sizing: border-box;
-          width: 80px;
-          height: 30px;
-          border-radius: 4px;
-          background-color: #23ade5;
-          color: white;
-          text-align: center;
-          font-size: 12px;
-          line-height: 30px;
-          cursor: pointer;
-        }
-      }
-    }
-  }
-}
-// 屏幕宽度小于$large-width的时候
-@media screen and (max-width: $large-width) {
-  .push-wrap {
-    .left {
-      width: $medium-left-width;
-    }
-    .right {
-      .list {
-        .item {
-        }
-      }
-    }
-  }
-}
-</style>

+ 0 - 842
src/views/webrtc-push/index.vue

@@ -1,842 +0,0 @@
-<template>
-  <div class="webrtc-push-wrap">
-    <div
-      ref="topRef"
-      class="left"
-    >
-      <div class="video-wrap">
-        <video
-          id="localVideo"
-          ref="localVideoRef"
-          autoplay
-          webkit-playsinline="true"
-          playsinline
-          x-webkit-airplay="allow"
-          x5-video-player-type="h5"
-          x5-video-player-fullscreen="true"
-          x5-video-orientation="portraint"
-          muted
-          controls
-        ></video>
-        <div
-          v-if="currMediaTypeList.length <= 0"
-          class="add-wrap"
-        >
-          <div
-            class="item"
-            @click="startGetUserMedia"
-          >
-            摄像头
-          </div>
-          <div
-            class="item"
-            @click="startGetDisplayMedia"
-          >
-            窗口
-          </div>
-        </div>
-      </div>
-      <div
-        ref="bottomRef"
-        class="control"
-      >
-        <div class="info">
-          <div class="avatar"></div>
-          <div class="detail">
-            <div class="top">
-              <input
-                ref="roomNameRef"
-                v-model="roomName"
-                type="text"
-                placeholder="输入房间名"
-              />
-              <button
-                ref="roomNameBtnRef"
-                class="btn"
-                @click="confirmRoomName"
-              >
-                确定
-              </button>
-            </div>
-            <div class="bottom">
-              <span>socketId:{{ getSocketId() }}</span>
-            </div>
-          </div>
-        </div>
-        <div class="other">
-          <div class="top">
-            <span class="item">
-              <i class="ico"></i>
-              <span>正在观看人数:{{ liveUserList.length }}</span>
-            </span>
-          </div>
-          <div class="bottom">
-            <button @click="startLive">开始直播</button>
-            <button @click="endLive">结束直播</button>
-          </div>
-        </div>
-      </div>
-    </div>
-    <div class="right">
-      <div class="resource-card">
-        <div class="title">素材列表</div>
-        <div class="list">
-          <div
-            v-for="(item, index) in currMediaTypeList"
-            :key="index"
-            class="item"
-          >
-            <span class="name">{{ item.txt }}</span>
-          </div>
-        </div>
-      </div>
-      <div class="danmu-card">
-        <div class="title">弹幕互动</div>
-        <div class="list-wrap">
-          <div class="list">
-            <div
-              v-for="(item, index) in damuList"
-              :key="index"
-              class="item"
-            >
-              <template v-if="item.msgType === DanmuMsgTypeEnum.danmu">
-                <span class="name">{{ item.socketId }}:</span>
-                <span class="msg">{{ item.msg }}</span>
-              </template>
-              <template v-else-if="item.msgType === DanmuMsgTypeEnum.otherJoin">
-                <span class="name system">系统通知:</span>
-                <span class="msg">{{ item.socketId }}进入直播!</span>
-              </template>
-              <template
-                v-else-if="item.msgType === DanmuMsgTypeEnum.userLeaved"
-              >
-                <span class="name system">系统通知:</span>
-                <span class="msg">{{ item.socketId }}离开直播!</span>
-              </template>
-            </div>
-          </div>
-        </div>
-        <div class="send-msg">
-          <input
-            v-model="danmuStr"
-            class="ipt"
-          />
-          <div
-            class="btn"
-            @click="sendDanmu"
-          >
-            发送
-          </div>
-        </div>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script lang="ts" setup>
-import { getRandomString } from 'billd-utils';
-import { onMounted, onUnmounted, ref } from 'vue';
-
-import {
-  DanmuMsgTypeEnum,
-  IAdminIn,
-  ICandidate,
-  IDanmu,
-  ILiveUser,
-  IOffer,
-  LiveTypeEnum,
-} from '@/interface';
-import { WebRTCClass } from '@/network/webRtc';
-import {
-  WebSocketClass,
-  WsConnectStatusEnum,
-  WsMsgTypeEnum,
-} from '@/network/webSocket';
-import router from '@/router';
-import { useNetworkStore } from '@/store/network';
-
-const networkStore = useNetworkStore();
-
-const topRef = ref<HTMLDivElement>();
-const bottomRef = ref<HTMLDivElement>();
-const roomNameRef = ref<HTMLInputElement>();
-const roomNameBtnRef = ref<HTMLButtonElement>();
-const localVideoRef = ref<HTMLVideoElement>();
-
-const roomId = ref<string>(getRandomString(15));
-const danmuStr = ref('');
-const roomName = ref('');
-const localStream = ref();
-
-const websocketInstant = ref<WebSocketClass>();
-const isDone = ref(false);
-const joined = ref(false);
-const offerSended = ref(new Set());
-const damuList = ref<IDanmu[]>([]);
-const liveUserList = ref<ILiveUser[]>([]);
-
-const allMediaTypeList = {
-  [LiveTypeEnum.camera]: {
-    type: LiveTypeEnum.camera,
-    txt: '摄像头',
-  },
-  [LiveTypeEnum.screen]: {
-    type: LiveTypeEnum.screen,
-    txt: '窗口',
-  },
-};
-const currMediaTypeList = ref<
-  {
-    type: LiveTypeEnum;
-    txt: string;
-  }[]
->([]);
-const currMediaType = ref<{
-  type: LiveTypeEnum;
-  txt: string;
-}>();
-
-function closeWs() {
-  const instance = networkStore.wsMap.get(roomId.value);
-  if (!instance) return;
-  instance.close();
-}
-
-function closeRtc() {
-  networkStore.rtcMap.forEach((rtc) => {
-    rtc.close();
-  });
-}
-
-function sendDanmu() {
-  if (!danmuStr.value.length) {
-    alert('请输入弹幕内容!');
-  }
-  if (!websocketInstant.value) return;
-  websocketInstant.value.send({
-    msgType: WsMsgTypeEnum.message,
-    data: { msg: danmuStr.value },
-  });
-  damuList.value.push({
-    socketId: getSocketId(),
-    msgType: DanmuMsgTypeEnum.danmu,
-    msg: danmuStr.value,
-  });
-  danmuStr.value = '';
-}
-
-onUnmounted(() => {
-  closeWs();
-  closeRtc();
-});
-
-function handleCoverImg() {
-  const canvas = document.createElement('canvas');
-  const { width, height } = localVideoRef.value!.getBoundingClientRect();
-  const rate = width / height;
-  const coverWidth = width * 0.5;
-  const coverHeight = coverWidth / rate;
-  canvas.width = coverWidth;
-  canvas.height = coverHeight;
-  canvas
-    .getContext('2d')!
-    .drawImage(localVideoRef.value!, 0, 0, coverWidth, coverHeight);
-  // webp比png的体积小非常多!因此coverWidth就可以不用压缩太夸张
-  const dataURL = canvas.toDataURL('image/webp');
-  return dataURL;
-}
-
-onMounted(async () => {
-  router.push({ query: { roomId: roomId.value } });
-  const all = await getAllMediaDevices();
-  allMediaTypeList[LiveTypeEnum.camera] = {
-    txt: all.find((item) => item.kind === 'videoinput')?.label || '摄像头',
-    type: LiveTypeEnum.camera,
-  };
-  if (topRef.value && bottomRef.value && localVideoRef.value) {
-    const res =
-      bottomRef.value.getBoundingClientRect().top -
-      topRef.value.getBoundingClientRect().top;
-    localVideoRef.value.style.height = `${res}px`;
-  }
-  localVideoRef.value?.addEventListener('loadstart', () => {
-    console.warn('视频流-loadstart');
-    const rtc = networkStore.getRtcMap(roomId.value);
-    if (!rtc) return;
-    rtc.rtcStatus.loadstart = true;
-    rtc.update();
-  });
-
-  localVideoRef.value?.addEventListener('loadedmetadata', () => {
-    console.warn('视频流-loadedmetadata');
-    const rtc = networkStore.getRtcMap(roomId.value);
-    if (!rtc) return;
-    rtc.rtcStatus.loadedmetadata = true;
-    rtc.update();
-    batchSendOffer();
-  });
-});
-
-function getSocketId() {
-  return networkStore.wsMap.get(roomId.value!)?.socketIo?.id || '-1';
-}
-
-function sendJoin() {
-  const instance = networkStore.wsMap.get(roomId.value);
-  if (!instance) return;
-  instance.send({
-    msgType: WsMsgTypeEnum.join,
-    data: {
-      roomName: roomName.value,
-      coverImg: handleCoverImg(),
-      track: {
-        video: true,
-        audio: true,
-      },
-    },
-  });
-}
-
-function batchSendOffer() {
-  liveUserList.value.forEach(async (item) => {
-    if (
-      !offerSended.value.has(item.socketId) &&
-      item.socketId !== getSocketId()
-    ) {
-      await startNewWebRtc(item.socketId);
-      await addTrack();
-      console.warn('new WebRTCClass完成');
-      console.log('执行sendOffer', {
-        sender: getSocketId(),
-        receiver: item.socketId,
-      });
-      sendOffer({ sender: getSocketId(), receiver: item.socketId });
-      offerSended.value.add(item.socketId);
-    }
-  });
-}
-
-function initReceive() {
-  const instance = websocketInstant.value;
-  if (!instance?.socketIo) return;
-  // websocket连接成功
-  instance.socketIo.on(WsConnectStatusEnum.connect, () => {
-    console.log('【websocket】websocket连接成功', instance.socketIo?.id);
-    if (!instance) return;
-    instance.status = WsConnectStatusEnum.connect;
-    instance.update();
-  });
-
-  // websocket连接断开
-  instance.socketIo.on(WsConnectStatusEnum.disconnect, () => {
-    console.log('【websocket】websocket连接断开', instance);
-    if (!instance) return;
-    instance.status = WsConnectStatusEnum.disconnect;
-    instance.update();
-  });
-
-  // 当前所有在线用户
-  instance.socketIo.on(WsMsgTypeEnum.roomLiveing, (data: IAdminIn) => {
-    console.log('【websocket】收到管理员正在直播', data);
-  });
-
-  // 当前所有在线用户
-  instance.socketIo.on(WsMsgTypeEnum.liveUser, () => {
-    console.log('【websocket】当前所有在线用户');
-    if (!instance) return;
-  });
-
-  // 收到offer
-  instance.socketIo.on(WsMsgTypeEnum.offer, async (data: IOffer) => {
-    console.warn('【websocket】收到offer', data);
-    if (!instance) return;
-    if (data.data.receiver === getSocketId()) {
-      console.log('收到offer,这个offer是发给我的');
-      const rtc = startNewWebRtc(data.data.sender);
-      await rtc.setRemoteDescription(data.data.sdp);
-      const sdp = await rtc.createAnswer();
-      await rtc.setLocalDescription(sdp);
-      websocketInstant.value?.send({
-        msgType: WsMsgTypeEnum.answer,
-        data: { sdp, sender: getSocketId(), receiver: data.data.sender },
-      });
-    } else {
-      console.log('收到offer,但是这个offer不是发给我的');
-    }
-  });
-
-  // 收到answer
-  instance.socketIo.on(WsMsgTypeEnum.answer, async (data: IOffer) => {
-    console.warn('【websocket】收到answer', data);
-    if (isDone.value) return;
-    if (!instance) return;
-    const rtc = networkStore.getRtcMap(`${roomId.value}___${data.socketId}`);
-    console.log(rtc, '收到answer收到answer');
-    if (!rtc) return;
-    rtc.rtcStatus.answer = true;
-    rtc.update();
-    if (data.data.receiver === getSocketId()) {
-      console.log('收到answer,这个answer是发给我的');
-      await rtc.setRemoteDescription(data.data.sdp);
-    } else {
-      console.log('收到answer,但这个answer不是发给我的');
-    }
-  });
-
-  // 收到candidate
-  instance.socketIo.on(WsMsgTypeEnum.candidate, (data: ICandidate) => {
-    console.warn('【websocket】收到candidate', data);
-    if (isDone.value) return;
-    if (!instance) return;
-    const rtc =
-      networkStore.getRtcMap(`${roomId.value}___${data.socketId}`) ||
-      networkStore.getRtcMap(roomId.value);
-    if (!rtc) return;
-    if (data.socketId !== getSocketId()) {
-      console.log('不是我发的candidate');
-      const candidate = new RTCIceCandidate({
-        sdpMid: data.data.sdpMid,
-        sdpMLineIndex: data.data.sdpMLineIndex,
-        candidate: data.data.candidate,
-      });
-      rtc.peerConnection
-        ?.addIceCandidate(candidate)
-        .then(() => {
-          console.log('candidate成功');
-        })
-        .catch((err) => {
-          console.error('candidate失败', err);
-        });
-    } else {
-      console.log('是我发的candidate');
-    }
-  });
-
-  // 收到用户发送消息
-  instance.socketIo.on(WsMsgTypeEnum.message, (data) => {
-    console.log('【websocket】收到用户发送消息', data);
-    damuList.value.push({
-      socketId: data.socketId,
-      msgType: DanmuMsgTypeEnum.danmu,
-      msg: data.data.msg,
-    });
-  });
-
-  // 用户加入房间
-  instance.socketIo.on(WsMsgTypeEnum.joined, (data) => {
-    console.log('【websocket】用户加入房间完成', data);
-    joined.value = true;
-    liveUserList.value.push({
-      avatar: 'red',
-      socketId: `${getSocketId()}`,
-      expr: 1,
-    });
-    batchSendOffer();
-  });
-
-  // 其他用户加入房间
-  instance.socketIo.on(WsMsgTypeEnum.otherJoin, (data) => {
-    console.log('【websocket】其他用户加入房间', data);
-    liveUserList.value.push({
-      avatar: 'red',
-      socketId: data.socketId,
-      expr: 1,
-    });
-    damuList.value.push({
-      socketId: data.socketId,
-      msgType: DanmuMsgTypeEnum.otherJoin,
-      msg: '',
-    });
-    if (joined.value) {
-      batchSendOffer();
-    }
-  });
-
-  // 用户离开房间
-  instance.socketIo.on(WsMsgTypeEnum.leave, (data) => {
-    console.log('【websocket】用户离开房间', data);
-    if (!instance) return;
-    instance.socketIo?.emit(WsMsgTypeEnum.leave, {
-      roomId: instance.roomId,
-    });
-  });
-
-  // 用户离开房间完成
-  instance.socketIo.on(WsMsgTypeEnum.leaved, (data) => {
-    console.log('【websocket】用户离开房间完成', data);
-    const res = liveUserList.value.filter(
-      (item) => item.socketId !== data.socketId
-    );
-    console.log('当前所有在线用户', JSON.stringify(res));
-    liveUserList.value = res;
-    damuList.value.push({
-      socketId: data.socketId,
-      msgType: DanmuMsgTypeEnum.userLeaved,
-      msg: '',
-    });
-  });
-}
-
-function roomNameIsOk() {
-  if (!roomName.value.length) {
-    alert('请输入房间名!');
-    return false;
-  }
-  if (roomName.value.length < 3 || roomName.value.length > 10) {
-    alert('房间名要求3-10个字符!');
-    return false;
-  }
-  return true;
-}
-
-function confirmRoomName() {
-  if (!roomNameIsOk()) return;
-  if (!roomNameRef.value) return;
-  roomNameRef.value.disabled = true;
-}
-
-/** 开始直播 */
-function startLive() {
-  if (!roomNameIsOk()) return;
-  if (!currMediaTypeList.value.length) {
-    alert('请选择一个素材!');
-    return;
-  }
-  roomNameBtnRef.value!.disabled = true;
-  websocketInstant.value = new WebSocketClass({
-    roomId: roomId.value,
-    url:
-      process.env.NODE_ENV === 'development'
-        ? 'ws://localhost:4300'
-        : 'wss://live.hsslive.cn',
-    isAdmin: true,
-  });
-  websocketInstant.value.update();
-  initReceive();
-  sendJoin();
-}
-
-/** 结束直播 */
-function endLive() {
-  roomNameBtnRef.value!.disabled = false;
-  closeRtc();
-  currMediaTypeList.value = [];
-  localStream.value = null;
-  localVideoRef.value!.srcObject = null;
-  const instance = networkStore.wsMap.get(roomId.value);
-  if (!instance) return;
-  instance.close();
-}
-
-async function getAllMediaDevices() {
-  const res = await navigator.mediaDevices.enumerateDevices();
-  // const audioInput = res.filter(
-  //   (item) => item.kind === 'audioinput' && item.deviceId !== 'default'
-  // );
-  // const videoInput = res.filter(
-  //   (item) => item.kind === 'videoinput' && item.deviceId !== 'default'
-  // );
-  return res;
-}
-
-/** 摄像头 */
-async function startGetUserMedia() {
-  if (!localStream.value) {
-    // WARN navigator.mediaDevices在localhost和https才能用,http://192.168.1.103:8000局域网用不了
-    const event = await navigator.mediaDevices.getUserMedia({
-      video: true,
-      audio: true,
-    });
-    console.log('getUserMedia成功', event);
-    currMediaType.value = allMediaTypeList[LiveTypeEnum.camera];
-    currMediaTypeList.value.push(allMediaTypeList[LiveTypeEnum.camera]);
-    if (!localVideoRef.value) return;
-    localVideoRef.value.srcObject = event;
-    localStream.value = event;
-  }
-}
-/** 窗口 */
-async function startGetDisplayMedia() {
-  if (!localStream.value) {
-    // WARN navigator.mediaDevices.getDisplayMedia在localhost和https才能用,http://192.168.1.103:8000局域网用不了
-    const event = await navigator.mediaDevices.getDisplayMedia({
-      video: true,
-      audio: true,
-    });
-    console.log('getDisplayMedia成功', event);
-    currMediaType.value = allMediaTypeList[LiveTypeEnum.screen];
-    currMediaTypeList.value.push(allMediaTypeList[LiveTypeEnum.screen]);
-    if (!localVideoRef.value) return;
-    localVideoRef.value.srcObject = event;
-    localStream.value = event;
-  }
-}
-
-function addTrack() {
-  if (!localStream.value) return;
-  liveUserList.value.forEach((item) => {
-    if (item.socketId !== getSocketId()) {
-      localStream.value.getTracks().forEach((track) => {
-        const rtc = networkStore.getRtcMap(
-          `${roomId.value}___${item.socketId}`
-        );
-        rtc?.addTrack(track, localStream.value);
-      });
-    }
-  });
-}
-
-async function sendOffer({
-  sender,
-  receiver,
-}: {
-  sender: string;
-  receiver: string;
-}) {
-  if (isDone.value) return;
-  if (!websocketInstant.value) return;
-  const rtc = networkStore.getRtcMap(`${roomId.value}___${receiver}`);
-  if (!rtc) return;
-  const sdp = await rtc.createOffer();
-  await rtc.setLocalDescription(sdp);
-  websocketInstant.value.send({
-    msgType: WsMsgTypeEnum.offer,
-    data: { sdp, sender, receiver },
-  });
-}
-
-function startNewWebRtc(receiver: string) {
-  console.warn('开始new WebRTCClass', receiver);
-  const rtc = new WebRTCClass({ roomId: `${roomId.value}___${receiver}` });
-  rtc.rtcStatus.joined = true;
-  rtc.update();
-  return rtc;
-}
-</script>
-
-<style lang="scss" scoped>
-.webrtc-push-wrap {
-  margin: 20px auto 0;
-  min-width: $large-width;
-  height: 700px;
-  text-align: center;
-  .left {
-    position: relative;
-    display: inline-block;
-    overflow: hidden;
-    box-sizing: border-box;
-    width: $large-left-width;
-    height: 100%;
-    border-radius: 6px;
-    background-color: white;
-    color: #9499a0;
-    vertical-align: top;
-
-    .video-wrap {
-      position: relative;
-      background-color: #18191c;
-      #localVideo {
-        max-width: 100%;
-        max-height: 100%;
-      }
-      .add-wrap {
-        position: absolute;
-        top: 50%;
-        left: 50%;
-        display: flex;
-        align-items: center;
-        justify-content: space-around;
-        width: 200px;
-        height: 50px;
-        background-color: #fff;
-        transform: translate(-50%, -50%);
-        .item {
-          width: 60px;
-          height: 30px;
-          border-radius: 6px;
-          background-color: skyblue;
-          color: white;
-          font-size: 14px;
-          line-height: 30px;
-          cursor: pointer;
-        }
-      }
-    }
-    .control {
-      position: absolute;
-      right: 0;
-      bottom: 0;
-      left: 0;
-      display: flex;
-      justify-content: space-between;
-      padding: 20px;
-      background-color: papayawhip;
-
-      .info {
-        display: flex;
-        align-items: center;
-
-        .avatar {
-          margin-right: 20px;
-          width: 64px;
-          height: 64px;
-          border-radius: 50%;
-          background-color: skyblue;
-        }
-        .detail {
-          display: flex;
-          flex-direction: column;
-          text-align: initial;
-          .top {
-            margin-bottom: 10px;
-            color: #18191c;
-            .btn {
-              margin-left: 10px;
-            }
-          }
-          .bottom {
-            font-size: 14px;
-          }
-        }
-      }
-      .other {
-        display: flex;
-        flex-direction: column;
-        justify-content: center;
-        font-size: 12px;
-        .top {
-          display: flex;
-          align-items: center;
-          .item {
-            display: flex;
-            align-items: center;
-            margin-right: 20px;
-            .ico {
-              display: inline-block;
-              margin-right: 4px;
-              width: 10px;
-              height: 10px;
-              border-radius: 50%;
-              background-color: skyblue;
-            }
-          }
-        }
-        .bottom {
-          margin-top: 10px;
-        }
-      }
-    }
-  }
-  .right {
-    position: relative;
-    display: inline-block;
-    box-sizing: border-box;
-    margin-left: 10px;
-    width: 240px;
-    height: 100%;
-    border-radius: 6px;
-    background-color: white;
-    color: #9499a0;
-
-    .resource-card {
-      box-sizing: border-box;
-      margin-bottom: 5%;
-      margin-bottom: 10px;
-      padding: 10px;
-      width: 100%;
-      height: 290px;
-      border-radius: 6px;
-      background-color: papayawhip;
-      .title {
-        text-align: initial;
-      }
-      .item {
-        display: flex;
-        align-items: center;
-        justify-content: space-between;
-        margin: 5px 0;
-        font-size: 12px;
-      }
-    }
-    .danmu-card {
-      box-sizing: border-box;
-      padding: 10px;
-      width: 100%;
-      height: 400px;
-      border-radius: 6px;
-      background-color: papayawhip;
-      text-align: initial;
-      .title {
-        margin-bottom: 10px;
-      }
-      .list-wrap {
-        overflow: scroll;
-        height: 80%;
-        .list {
-          margin-bottom: 10px;
-          height: 300px;
-          .item {
-            margin-bottom: 10px;
-            font-size: 12px;
-            .name {
-              color: #9499a0;
-              &.system {
-                color: red;
-              }
-            }
-            .msg {
-              color: #61666d;
-            }
-          }
-        }
-      }
-
-      .send-msg {
-        display: flex;
-        align-items: center;
-        box-sizing: border-box;
-        .ipt {
-          display: block;
-          box-sizing: border-box;
-          margin: 0 auto;
-          margin-right: 10px;
-          padding: 10px;
-          width: 80%;
-          height: 30px;
-          outline: none;
-          border: 1px solid hsla(0, 0%, 60%, 0.2);
-          border-radius: 6px;
-          background-color: #f1f2f3;
-          font-size: 14px;
-        }
-        .btn {
-          box-sizing: border-box;
-          width: 80px;
-          height: 30px;
-          border-radius: 6px;
-          background-color: skyblue;
-          color: white;
-          text-align: center;
-          font-size: 12px;
-          line-height: 30px;
-          cursor: pointer;
-        }
-      }
-    }
-  }
-}
-// 屏幕宽度小于$large-width的时候
-@media screen and (max-width: $large-width) {
-  .webrtc-push-wrap {
-    .left {
-      width: $medium-left-width;
-    }
-    .right {
-      .list {
-        .item {
-        }
-      }
-    }
-  }
-}
-</style>

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