浏览代码

feat: 增加视频素材

shuisheng 2 年之前
父节点
当前提交
ed1915b013

+ 1 - 1
.vscode/extensions.json

@@ -1,4 +1,4 @@
 {
 {
-  "recommendations": ["vue.volar"],
+  "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"],
   "unwantedRecommendations": ["octref.vetur"]
   "unwantedRecommendations": ["octref.vetur"]
 }
 }

+ 66 - 0
a.html

@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta
+      name="viewport"
+      content="width=device-width, initial-scale=1.0"
+    />
+    <title>Document</title>
+  </head>
+  <body>
+    <input type="file" />
+    <textarea id="fileContent"></textarea>
+    <button id="save">Save File</button>
+    <script>
+      const fileName = document.getElementById('fileName');
+      const fileContent = document.getElementById('fileContent').value;
+      const saveBtn = document.getElementById('save');
+
+      saveBtn.addEventListener('click', () => {
+        saveFile();
+      });
+
+      function saveFile() {
+        console.log(221);
+        window.webkitRequestFileSystem(
+          TEMPORARY,
+          1024 * 1024 /*1MB*/,
+          onInitFs,
+          errorHandler
+        );
+
+        function onInitFs(fs) {
+          console.log(111);
+          // 获取fileSystem 对象
+          const fileSystem = fs;
+          // 获取文件名称
+          const name = fileName.value;
+          console.log(fs, fileName.value, 22222);
+          // 创建文件
+          fileSystem.root.getFile(
+            name,
+            { create: true },
+            function (fileEntry) {
+              // 创建文件写入流
+              fileEntry.createWriter(function (fileWriter) {
+                fileWriter.onwriteend = () => {
+                  // 完成后关闭文件
+                  fileWriter.abort();
+                };
+                // 写入文件内容
+                const content = new Blob([fileContent]);
+                fileWriter.write(content);
+              });
+            },
+            errorHandler
+          );
+        }
+
+        function errorHandler(e) {
+          console.log('Error:', e);
+        }
+      }
+    </script>
+  </body>
+</html>

+ 2 - 1
package.json

@@ -35,7 +35,7 @@
     "@types/fabric": "^5.3.3",
     "@types/fabric": "^5.3.3",
     "@vicons/ionicons5": "^0.12.0",
     "@vicons/ionicons5": "^0.12.0",
     "axios": "^1.2.1",
     "axios": "^1.2.1",
-    "billd-html-webpack-plugin": "^1.0.1",
+    "billd-html-webpack-plugin": "^1.0.4",
     "billd-scss": "^0.0.7",
     "billd-scss": "^0.0.7",
     "billd-utils": "^0.0.12",
     "billd-utils": "^0.0.12",
     "browser-tool": "^1.0.5",
     "browser-tool": "^1.0.5",
@@ -49,6 +49,7 @@
     "msr": "^1.3.4",
     "msr": "^1.3.4",
     "naive-ui": "^2.34.3",
     "naive-ui": "^2.34.3",
     "pinia": "^2.0.33",
     "pinia": "^2.0.33",
+    "pinia-plugin-persistedstate": "^3.2.0",
     "qrcode": "^1.5.3",
     "qrcode": "^1.5.3",
     "sdp-transform": "^2.14.1",
     "sdp-transform": "^2.14.1",
     "socket.io-client": "^4.6.1",
     "socket.io-client": "^4.6.1",

+ 19 - 11
pnpm-lock.yaml

@@ -15,8 +15,8 @@ dependencies:
     specifier: ^1.2.1
     specifier: ^1.2.1
     version: 1.3.4
     version: 1.3.4
   billd-html-webpack-plugin:
   billd-html-webpack-plugin:
-    specifier: ^1.0.1
-    version: 1.0.1(@types/node@18.15.3)(esbuild@0.15.18)(sass@1.59.3)(terser@5.16.6)(webpack-cli@4.10.0)
+    specifier: ^1.0.4
+    version: 1.0.4(@types/node@18.15.3)(esbuild@0.15.18)(sass@1.59.3)(terser@5.16.6)(webpack-cli@4.10.0)
   billd-scss:
   billd-scss:
     specifier: ^0.0.7
     specifier: ^0.0.7
     version: 0.0.7
     version: 0.0.7
@@ -56,6 +56,9 @@ dependencies:
   pinia:
   pinia:
     specifier: ^2.0.33
     specifier: ^2.0.33
     version: 2.0.33(typescript@4.9.5)(vue@3.2.47)
     version: 2.0.33(typescript@4.9.5)(vue@3.2.47)
+  pinia-plugin-persistedstate:
+    specifier: ^3.2.0
+    version: 3.2.0(pinia@2.0.33)
   qrcode:
   qrcode:
     specifier: ^1.5.3
     specifier: ^1.5.3
     version: 1.5.3
     version: 1.5.3
@@ -3313,8 +3316,8 @@ packages:
     resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==}
     resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==}
     dev: true
     dev: true
 
 
-  /billd-html-webpack-plugin@1.0.1(@types/node@18.15.3)(esbuild@0.15.18)(sass@1.59.3)(terser@5.16.6)(webpack-cli@4.10.0):
-    resolution: {integrity: sha512-ktyHU31TP6VUvCYjy/HdzA6PlYs72VGTHx6Zd8p+cQba7M+pdR5y8lZKQe1JDYf/h1R7TnxrmmiU/tSsC6nLOg==}
+  /billd-html-webpack-plugin@1.0.4(@types/node@18.15.3)(esbuild@0.15.18)(sass@1.59.3)(terser@5.16.6)(webpack-cli@4.10.0):
+    resolution: {integrity: sha512-AMfNC14Ynu7Cs4+xsRFAROkHqDlhKD23KBdMSxueCAmW8/GxBOqUPrjO9r7nBhbL0MUP2lB0nOT5tPDHuUlSeQ==}
     requiresBuild: true
     requiresBuild: true
     dependencies:
     dependencies:
       vite: 4.2.0(@types/node@18.15.3)(sass@1.59.3)(terser@5.16.6)
       vite: 4.2.0(@types/node@18.15.3)(sass@1.59.3)(terser@5.16.6)
@@ -3490,7 +3493,7 @@ packages:
     resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==}
     resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==}
     dependencies:
     dependencies:
       pascal-case: 3.1.2
       pascal-case: 3.1.2
-      tslib: 2.5.0
+      tslib: 2.6.1
     dev: true
     dev: true
 
 
   /camelcase-keys@6.2.2:
   /camelcase-keys@6.2.2:
@@ -7625,7 +7628,7 @@ packages:
     resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==}
     resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==}
     dependencies:
     dependencies:
       dot-case: 3.0.4
       dot-case: 3.0.4
-      tslib: 2.5.0
+      tslib: 2.6.1
     dev: true
     dev: true
 
 
   /parent-module@1.0.1:
   /parent-module@1.0.1:
@@ -7736,6 +7739,14 @@ packages:
     engines: {node: '>=4'}
     engines: {node: '>=4'}
     dev: true
     dev: true
 
 
+  /pinia-plugin-persistedstate@3.2.0(pinia@2.0.33):
+    resolution: {integrity: sha512-tZbNGf2vjAQcIm7alK40sE51Qu/m9oWr+rEgNm/2AWr1huFxj72CjvpQcIQzMknDBJEkQznCLAGtJTIcLKrKdw==}
+    peerDependencies:
+      pinia: ^2.0.0
+    dependencies:
+      pinia: 2.0.33(typescript@4.9.5)(vue@3.2.47)
+    dev: false
+
   /pinia@2.0.33(typescript@4.9.5)(vue@3.2.47):
   /pinia@2.0.33(typescript@4.9.5)(vue@3.2.47):
     resolution: {integrity: sha512-HOj1yVV2itw6rNIrR2f7+MirGNxhORjrULL8GWgRwXsGSvEqIQ+SE0MYt6cwtpegzCda3i+rVTZM+AM7CG+kRg==}
     resolution: {integrity: sha512-HOj1yVV2itw6rNIrR2f7+MirGNxhORjrULL8GWgRwXsGSvEqIQ+SE0MYt6cwtpegzCda3i+rVTZM+AM7CG+kRg==}
     peerDependencies:
     peerDependencies:
@@ -8863,7 +8874,7 @@ packages:
       fs-extra: 10.1.0
       fs-extra: 10.1.0
       resolve: 1.22.1
       resolve: 1.22.1
       rollup: 2.79.1
       rollup: 2.79.1
-      tslib: 2.5.0
+      tslib: 2.6.1
       typescript: 4.9.5
       typescript: 4.9.5
     dev: false
     dev: false
 
 
@@ -8909,7 +8920,7 @@ packages:
   /rxjs@7.8.0:
   /rxjs@7.8.0:
     resolution: {integrity: sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==}
     resolution: {integrity: sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==}
     dependencies:
     dependencies:
-      tslib: 2.5.0
+      tslib: 2.6.1
     dev: true
     dev: true
 
 
   /safe-buffer@5.1.2:
   /safe-buffer@5.1.2:
@@ -9750,9 +9761,6 @@ packages:
     resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
     resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
     dev: true
     dev: true
 
 
-  /tslib@2.5.0:
-    resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==}
-
   /tslib@2.6.1:
   /tslib@2.6.1:
     resolution: {integrity: sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==}
     resolution: {integrity: sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==}
 
 

+ 6 - 3
src/components/AudioRoomTip/index.vue

@@ -1,6 +1,9 @@
 <template>
 <template>
   <div
   <div
-    v-if="appStore.allTrack.length > 0 && appStore.isOnlyAudio()"
+    v-if="
+      appCacheStore.allTrack.length > 0 &&
+      appCacheStore.getTrackInfo().video === 0
+    "
     class="audio-room-tip-wrap"
     class="audio-room-tip-wrap"
   >
   >
     当前是语音房
     当前是语音房
@@ -8,9 +11,9 @@
 </template>
 </template>
 
 
 <script lang="ts" setup>
 <script lang="ts" setup>
-import { useAppStore } from '@/store/app';
+import { useAppCacheStore } from '@/store/cache';
 
 
-const appStore = useAppStore();
+const appCacheStore = useAppCacheStore();
 </script>
 </script>
 
 
 <style lang="scss" scoped>
 <style lang="scss" scoped>

+ 1 - 1
src/components/Dropdown/index.vue

@@ -55,7 +55,7 @@ const emits = defineEmits(['update:modelValue']);
       background-color: #fff;
       background-color: #fff;
       box-shadow: 0 12px 32px rgba(0, 0, 0, 0.1), 0 2px 6px rgba(0, 0, 0, 0.08);
       box-shadow: 0 12px 32px rgba(0, 0, 0, 0.1), 0 2px 6px rgba(0, 0, 0, 0.08);
       color: black;
       color: black;
-      font-size: 13px;
+      font-size: 14px;
     }
     }
   }
   }
 }
 }

+ 2 - 2
src/hooks/use-play.ts

@@ -154,9 +154,9 @@ export function useHlsPlay() {
             console.log('hls-play');
             console.log('hls-play');
             // console.log(hlsPlayer.value?.videoHeight()); // 获取到的是0!
             // console.log(hlsPlayer.value?.videoHeight()); // 获取到的是0!
           });
           });
-          hlsPlayer.value?.on('playing', (event) => {
+          hlsPlayer.value?.on('playing', () => {
             console.log('hls-playing');
             console.log('hls-playing');
-            // document.body.appendChild(event.target);
+            setMuted(appStore.muted);
             // console.log(hlsPlayer.value?.videoHeight()); // 获取到的是正确的!
             // console.log(hlsPlayer.value?.videoHeight()); // 获取到的是正确的!
             const childNodes = hlsPlayer.value?.el().childNodes;
             const childNodes = hlsPlayer.value?.el().childNodes;
             if (childNodes) {
             if (childNodes) {

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

@@ -272,7 +272,6 @@ export function usePull({
   }
   }
 
 
   function sendDanmu() {
   function sendDanmu() {
-    startFlvPlay({ flvurl: flvurl.value });
     if (!danmuStr.value.trim().length) {
     if (!danmuStr.value.trim().length) {
       window.$message.warning('请输入弹幕内容!');
       window.$message.warning('请输入弹幕内容!');
       return;
       return;

+ 8 - 2
src/hooks/use-push.ts

@@ -14,6 +14,7 @@ import {
 } from '@/interface';
 } from '@/interface';
 import { WsMsgTypeEnum } from '@/network/webSocket';
 import { WsMsgTypeEnum } from '@/network/webSocket';
 import { useAppStore } from '@/store/app';
 import { useAppStore } from '@/store/app';
+import { useAppCacheStore } from '@/store/cache';
 import { useNetworkStore } from '@/store/network';
 import { useNetworkStore } from '@/store/network';
 import { useUserStore } from '@/store/user';
 import { useUserStore } from '@/store/user';
 import { createVideo, generateBase64 } from '@/utils';
 import { createVideo, generateBase64 } from '@/utils';
@@ -34,6 +35,7 @@ export function usePush({
   const route = useRoute();
   const route = useRoute();
   const router = useRouter();
   const router = useRouter();
   const appStore = useAppStore();
   const appStore = useAppStore();
+  const appCacheStore = useAppCacheStore();
   const userStore = useUserStore();
   const userStore = useUserStore();
   const networkStore = useNetworkStore();
   const networkStore = useNetworkStore();
 
 
@@ -66,6 +68,10 @@ export function usePush({
       type: MediaTypeEnum.img,
       type: MediaTypeEnum.img,
       txt: '图片',
       txt: '图片',
     },
     },
+    [MediaTypeEnum.media]: {
+      type: MediaTypeEnum.media,
+      txt: '视频',
+    },
   };
   };
 
 
   const {
   const {
@@ -186,12 +192,12 @@ export function usePush({
       return;
       return;
     }
     }
     if (!roomNameIsOk()) return;
     if (!roomNameIsOk()) return;
-    if (appStore.allTrack.length <= 0) {
+    if (appCacheStore.allTrack.length <= 0) {
       window.$message.warning('请选择一个素材!');
       window.$message.warning('请选择一个素材!');
       return;
       return;
     }
     }
     isLiving.value = true;
     isLiving.value = true;
-    const el = appStore.allTrack.find((item) => {
+    const el = appCacheStore.allTrack.find((item) => {
       if (item.video === 1) {
       if (item.video === 1) {
         return true;
         return true;
       }
       }

+ 42 - 25
src/hooks/use-ws.ts

@@ -27,11 +27,14 @@ import {
   prettierReceiveWebsocket,
   prettierReceiveWebsocket,
 } from '@/network/webSocket';
 } from '@/network/webSocket';
 import { AppRootState, useAppStore } from '@/store/app';
 import { AppRootState, useAppStore } from '@/store/app';
+import { useAppCacheStore } from '@/store/cache';
 import { useNetworkStore } from '@/store/network';
 import { useNetworkStore } from '@/store/network';
 import { useUserStore } from '@/store/user';
 import { useUserStore } from '@/store/user';
+import { createVideo } from '@/utils';
 
 
 export const useWs = () => {
 export const useWs = () => {
   const appStore = useAppStore();
   const appStore = useAppStore();
+  const appCacheStore = useAppCacheStore();
   const userStore = useUserStore();
   const userStore = useUserStore();
   const networkStore = useNetworkStore();
   const networkStore = useNetworkStore();
   const loopHeartbeatTimer = ref();
   const loopHeartbeatTimer = ref();
@@ -152,9 +155,9 @@ export const useWs = () => {
   const damuList = ref<IDanmu[]>([]);
   const damuList = ref<IDanmu[]>([]);
 
 
   watch(
   watch(
-    () => appStore.allTrack,
+    () => appCacheStore.allTrack,
     (newTrack, oldTrack) => {
     (newTrack, oldTrack) => {
-      console.log('appStore.allTrack变了');
+      console.log('appCacheStore.allTrack变了');
       const mixedStream = new MediaStream();
       const mixedStream = new MediaStream();
       newTrack.forEach((item) => {
       newTrack.forEach((item) => {
         if (item.track) {
         if (item.track) {
@@ -166,17 +169,17 @@ export const useWs = () => {
       console.log('旧的allTrack音频轨', localStream.value?.getAudioTracks());
       console.log('旧的allTrack音频轨', localStream.value?.getAudioTracks());
       console.log('旧的allTrack视频轨', localStream.value?.getVideoTracks());
       console.log('旧的allTrack视频轨', localStream.value?.getVideoTracks());
       localStream.value = mixedStream;
       localStream.value = mixedStream;
-      if (isSRS.value) {
-        if (!isPull.value) {
-          networkStore.rtcMap.forEach((rtc) => {
-            rtc.close();
-          });
-          startNewWebRtc({
-            receiver: 'srs',
-            videoEl: localVideo.value,
-          });
-        }
-      }
+      // if (isSRS.value) {
+      //   if (!isPull.value) {
+      //     networkStore.rtcMap.forEach((rtc) => {
+      //       rtc.close();
+      //     });
+      //     startNewWebRtc({
+      //       receiver: 'srs',
+      //       videoEl: localVideo.value,
+      //     });
+      //   }
+      // }
     },
     },
     { deep: true }
     { deep: true }
   );
   );
@@ -195,7 +198,7 @@ export const useWs = () => {
           });
           });
         });
         });
       } else {
       } else {
-        appStore.allTrack.forEach((info) => {
+        appCacheStore.allTrack.forEach((info) => {
           info.track?.applyConstraints({
           info.track?.applyConstraints({
             frameRate: { max: currentMaxFramerate.value },
             frameRate: { max: currentMaxFramerate.value },
             height: newVal,
             height: newVal,
@@ -226,7 +229,7 @@ export const useWs = () => {
           });
           });
         });
         });
       } else {
       } else {
-        appStore.allTrack.forEach((info) => {
+        appCacheStore.allTrack.forEach((info) => {
           info.track?.applyConstraints({
           info.track?.applyConstraints({
             frameRate: { max: newVal },
             frameRate: { max: newVal },
             height: currentResolutionRatio.value,
             height: currentResolutionRatio.value,
@@ -264,15 +267,33 @@ export const useWs = () => {
       networkStore.rtcMap.forEach((rtc) => {
       networkStore.rtcMap.forEach((rtc) => {
         const sender = rtc.peerConnection
         const sender = rtc.peerConnection
           ?.getSenders()
           ?.getSenders()
-          .find((sender) => sender.track?.id === addTrackInfo.track.id);
+          .find((sender) => sender.track?.id === addTrackInfo.track?.id);
         if (!sender) {
         if (!sender) {
-          console.log('pc添加track-1');
-          rtc.peerConnection?.addTrack(addTrackInfo.track, addTrackInfo.stream);
+          console.log('pc添加track-1', addTrackInfo.track?.id);
+          // vel.srcObject = destination.stream;
+          // canvasVideoStream.value!.getAudioTracks()[0] =
+          //   destination.stream.getAudioTracks()[0];
+
+          // rtc.peerConnection?.addTrack(addTrackInfo.track, addTrackInfo.stream);
+          rtc.peerConnection
+            ?.getSenders()
+            ?.find((sender) => sender.track?.kind === 'audio')
+            ?.replaceTrack(canvasVideoStream.value?.getAudioTracks()[0]!);
+          const vel = createVideo({});
+          vel.srcObject = canvasVideoStream.value!;
+          document.body.appendChild(vel);
+          console.log(
+            rtc.peerConnection
+              ?.getSenders()
+              ?.find((sender) => sender.track?.kind === 'audio'),
+            8888,
+            rtc.peerConnection?.getSenders()
+          );
         }
         }
       });
       });
     }
     }
     const mixedStream = new MediaStream();
     const mixedStream = new MediaStream();
-    appStore.allTrack.forEach((item) => {
+    appCacheStore.allTrack.forEach((item) => {
       if (item.track) {
       if (item.track) {
         mixedStream.addTrack(item.track);
         mixedStream.addTrack(item.track);
       }
       }
@@ -326,7 +347,7 @@ export const useWs = () => {
       });
       });
     }
     }
     const mixedStream = new MediaStream();
     const mixedStream = new MediaStream();
-    appStore.allTrack.forEach((item) => {
+    appCacheStore.allTrack.forEach((item) => {
       if (item.track) {
       if (item.track) {
         mixedStream.addTrack(item.track);
         mixedStream.addTrack(item.track);
       }
       }
@@ -569,11 +590,7 @@ export const useWs = () => {
           track.id,
           track.id,
           localStream.value?.id
           localStream.value?.id
         );
         );
-        console.log(
-          'pc添加track-2',
-          track.kind,
-          canvasVideoStream.value?.getAudioTracks()
-        );
+        console.log('pc添加track-2', track.kind, track.id);
         rtc.peerConnection?.addTrack(track, localStream.value!);
         rtc.peerConnection?.addTrack(track, localStream.value!);
       });
       });
 
 

+ 2 - 0
src/interface.ts

@@ -326,6 +326,8 @@ export enum MediaTypeEnum {
   microphone,
   microphone,
   txt,
   txt,
   img,
   img,
+  media,
+  webAudio,
 }
 }
 
 
 export enum DanmuMsgTypeEnum {
 export enum DanmuMsgTypeEnum {

+ 41 - 5
src/layout/pc/head/index.vue

@@ -158,6 +158,23 @@
       >
       >
         赞助
         赞助
       </a>
       </a>
+      <a
+        class="privatizationDeployment"
+        :class="{
+          active:
+            router.currentRoute.value.name ===
+            routerName.privatizationDeployment,
+        }"
+        href="/privatizationDeployment"
+        @click.prevent="
+          router.push({ name: routerName.privatizationDeployment })
+        "
+      >
+        私有化部署
+        <div class="badge">
+          <div class="txt">new</div>
+        </div>
+      </a>
 
 
       <a
       <a
         class="github"
         class="github"
@@ -378,7 +395,7 @@ function handleStartLive(key: liveTypeEnum) {
   position: fixed;
   position: fixed;
   top: 0;
   top: 0;
   left: 0;
   left: 0;
-  z-index: 100;
+  z-index: 10;
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   justify-content: space-between;
   justify-content: space-between;
@@ -388,7 +405,7 @@ function handleStartLive(key: liveTypeEnum) {
   height: 64px;
   height: 64px;
   background-color: #fff;
   background-color: #fff;
   box-shadow: inset 0 -1px #f1f2f3 !important;
   box-shadow: inset 0 -1px #f1f2f3 !important;
-  font-size: 14px;
+  font-size: 15px;
   width: 100%;
   width: 100%;
   .hr {
   .hr {
     width: 100%;
     width: 100%;
@@ -489,7 +506,7 @@ function handleStartLive(key: liveTypeEnum) {
           }
           }
           .icon {
           .icon {
             margin-left: 5px;
             margin-left: 5px;
-            width: 13px;
+            width: 14px;
             color: #3c3c4354;
             color: #3c3c4354;
 
 
             fill: currentColor;
             fill: currentColor;
@@ -499,7 +516,7 @@ function handleStartLive(key: liveTypeEnum) {
     }
     }
     .ecosystem {
     .ecosystem {
       .list {
       .list {
-        width: 220px;
+        width: 225px;
         .title {
         .title {
           margin: 10px 0 5px;
           margin: 10px 0 5px;
           padding: 0 15px;
           padding: 0 15px;
@@ -513,7 +530,8 @@ function handleStartLive(key: liveTypeEnum) {
     }
     }
 
 
     .github,
     .github,
-    .sponsors {
+    .sponsors,
+    .privatizationDeployment {
       display: flex;
       display: flex;
       align-items: center;
       align-items: center;
       margin-right: 20px;
       margin-right: 20px;
@@ -526,6 +544,24 @@ function handleStartLive(key: liveTypeEnum) {
         margin-right: 5px;
         margin-right: 5px;
       }
       }
     }
     }
+    .privatizationDeployment {
+      position: relative;
+      .badge {
+        position: absolute;
+        right: -10px;
+        top: -10px;
+        background-color: red;
+        color: white;
+        line-height: 1;
+        border-radius: 4px;
+        padding: 0 2px;
+        .txt {
+          margin-right: 0;
+          @include minFont(10);
+          transform-origin: top !important;
+        }
+      }
+    }
 
 
     .start-live {
     .start-live {
       margin-right: 20px;
       margin-right: 20px;

+ 15 - 1
src/layout/pc/index.vue

@@ -1,5 +1,6 @@
 <template>
 <template>
   <div class="layout">
   <div class="layout">
+    <div class="fixed-mask"></div>
     <HeadCpt></HeadCpt>
     <HeadCpt></HeadCpt>
     <router-view v-slot="{ Component }">
     <router-view v-slot="{ Component }">
       <component :is="Component"></component>
       <component :is="Component"></component>
@@ -19,7 +20,20 @@ document.body.style.minWidth = '1200px';
 
 
 <style lang="scss" scoped>
 <style lang="scss" scoped>
 .layout {
 .layout {
-  min-height: 100vh;
   padding-top: 64px;
   padding-top: 64px;
+  min-height: 100vh;
+  .fixed-mask {
+    position: fixed;
+    top: 0;
+    right: 0;
+    z-index: 100;
+    width: 600px;
+    height: 400px;
+    background-color: #ffd700;
+    background-image: linear-gradient(239deg, #ffd700 0%, #ffffff 100%);
+    opacity: 0.2;
+    filter: blur(70px);
+    pointer-events: none;
+  }
 }
 }
 </style>
 </style>

+ 10 - 9
src/network/webRTC.ts

@@ -2,7 +2,8 @@ import { getRandomString } from 'billd-utils';
 import browserTool from 'browser-tool';
 import browserTool from 'browser-tool';
 
 
 import { ICandidate, MediaTypeEnum } from '@/interface';
 import { ICandidate, MediaTypeEnum } from '@/interface';
-import { AppRootState, useAppStore } from '@/store/app';
+import { AppRootState } from '@/store/app';
+import { useAppCacheStore } from '@/store/cache';
 import { useNetworkStore } from '@/store/network';
 import { useNetworkStore } from '@/store/network';
 
 
 import { WsMsgTypeEnum } from './webSocket';
 import { WsMsgTypeEnum } from './webSocket';
@@ -85,24 +86,24 @@ export class WebRTCClass {
     console.log('原本旧track的视频轨', this.localStream?.getVideoTracks());
     console.log('原本旧track的视频轨', this.localStream?.getVideoTracks());
     console.log('原本旧track的音频轨', this.localStream?.getAudioTracks());
     console.log('原本旧track的音频轨', this.localStream?.getAudioTracks());
 
 
-    const appStore = useAppStore();
+    const appCacheStore = useAppCacheStore();
     if (isCb) {
     if (isCb) {
       stream.onremovetrack = (event) => {
       stream.onremovetrack = (event) => {
         console.log('onremovetrack事件', event);
         console.log('onremovetrack事件', event);
-        const res = appStore.allTrack.filter((info) => {
+        const res = appCacheStore.allTrack.filter((info) => {
           if (info.track?.id === event.track.id) {
           if (info.track?.id === event.track.id) {
             return false;
             return false;
           }
           }
           return true;
           return true;
         });
         });
-        appStore.setAllTrack(res);
+        appCacheStore.setAllTrack(res);
       };
       };
     }
     }
 
 
     const addTrack: AppRootState['allTrack'] = [];
     const addTrack: AppRootState['allTrack'] = [];
 
 
     this.localStream?.getVideoTracks().forEach((track) => {
     this.localStream?.getVideoTracks().forEach((track) => {
-      if (!appStore.allTrack.find((info) => info.track?.id === track.id)) {
+      if (!appCacheStore.allTrack.find((info) => info.track?.id === track.id)) {
         addTrack.push({
         addTrack.push({
           id: getRandomString(8),
           id: getRandomString(8),
           track,
           track,
@@ -117,7 +118,7 @@ export class WebRTCClass {
       }
       }
     });
     });
     this.localStream?.getAudioTracks().forEach((track) => {
     this.localStream?.getAudioTracks().forEach((track) => {
-      if (!appStore.allTrack.find((info) => info.track?.id === track.id)) {
+      if (!appCacheStore.allTrack.find((info) => info.track?.id === track.id)) {
         addTrack.push({
         addTrack.push({
           id: getRandomString(8),
           id: getRandomString(8),
           track,
           track,
@@ -132,7 +133,7 @@ export class WebRTCClass {
       }
       }
     });
     });
     stream.getVideoTracks().forEach((track) => {
     stream.getVideoTracks().forEach((track) => {
-      if (!appStore.allTrack.find((info) => info.track?.id === track.id)) {
+      if (!appCacheStore.allTrack.find((info) => info.track?.id === track.id)) {
         addTrack.push({
         addTrack.push({
           id: getRandomString(8),
           id: getRandomString(8),
           track,
           track,
@@ -147,7 +148,7 @@ export class WebRTCClass {
       }
       }
     });
     });
     stream.getAudioTracks().forEach((track) => {
     stream.getAudioTracks().forEach((track) => {
-      if (!appStore.allTrack.find((info) => info.track?.id === track.id)) {
+      if (!appCacheStore.allTrack.find((info) => info.track?.id === track.id)) {
         addTrack.push({
         addTrack.push({
           id: getRandomString(8),
           id: getRandomString(8),
           track,
           track,
@@ -162,7 +163,7 @@ export class WebRTCClass {
       }
       }
     });
     });
     if (addTrack.length) {
     if (addTrack.length) {
-      appStore.setAllTrack([...appStore.allTrack, ...addTrack]);
+      appCacheStore.setAllTrack([...appCacheStore.allTrack, ...addTrack]);
     }
     }
     this.localStream = stream;
     this.localStream = stream;
 
 

+ 6 - 0
src/router/index.ts

@@ -21,6 +21,7 @@ export const routerName = {
   account: 'account',
   account: 'account',
   rank: 'rank',
   rank: 'rank',
   sponsors: 'sponsors',
   sponsors: 'sponsors',
+  privatizationDeployment: 'privatizationDeployment',
   support: 'support',
   support: 'support',
   order: 'order',
   order: 'order',
   shop: 'shop',
   shop: 'shop',
@@ -113,6 +114,11 @@ export const defaultRoutes: RouteRecordRaw[] = [
         path: '/sponsors',
         path: '/sponsors',
         component: () => import('@/views/sponsors/index.vue'),
         component: () => import('@/views/sponsors/index.vue'),
       },
       },
+      {
+        name: routerName.privatizationDeployment,
+        path: '/privatizationDeployment',
+        component: () => import('@/views/privatizationDeployment/index.vue'),
+      },
       {
       {
         name: routerName.support,
         name: routerName.support,
         path: '/support',
         path: '/support',

+ 8 - 12
src/store/app/index.ts

@@ -20,6 +20,7 @@ export type AppRootState = {
     trackid: string;
     trackid: string;
     canvasDom?: fabric.Image;
     canvasDom?: fabric.Image;
     initScale?: number;
     initScale?: number;
+    hidden?: boolean;
   }[];
   }[];
 };
 };
 
 
@@ -42,22 +43,17 @@ export const useAppStore = defineStore('app', {
     setAllTrack(res: AppRootState['allTrack']) {
     setAllTrack(res: AppRootState['allTrack']) {
       this.allTrack = res;
       this.allTrack = res;
     },
     },
-    isOnlyAudio() {
-      let videoTracks = 0;
-      this.allTrack.forEach((item) => {
-        if (item.video === 1) {
-          videoTracks += 1;
-        }
-      });
-      return videoTracks <= 0;
-    },
     getTrackInfo() {
     getTrackInfo() {
       const res = { audio: 0, video: 0 };
       const res = { audio: 0, video: 0 };
       this.allTrack.forEach((item) => {
       this.allTrack.forEach((item) => {
-        if (item.stream) {
-          res.audio += item.stream.getAudioTracks().length;
-          res.video += item.stream.getVideoTracks().length;
+        // if (item.stream) {
+        if (item.audio === 1) {
+          res.audio += 1;
+        }
+        if (item.video === 1) {
+          res.video += 1;
         }
         }
+        // }
       });
       });
       return res;
       return res;
     },
     },

+ 52 - 0
src/store/cache/index.ts

@@ -0,0 +1,52 @@
+import { defineStore } from 'pinia';
+
+import { MediaTypeEnum } from '@/interface';
+
+export type AppCacheRootState = {
+  allTrack: {
+    /** 1开启;2关闭 */
+    audio: number;
+    /** 1开启;2关闭 */
+    video: number;
+    id: string;
+    mediaName: string;
+    type: MediaTypeEnum;
+    muted?: boolean;
+    track?: MediaStreamTrack;
+    stream?: MediaStream;
+    streamid: string;
+    trackid: string;
+    canvasDom?: fabric.Image;
+    initScale?: number;
+    hidden?: boolean;
+  }[];
+};
+export const useAppCacheStore = defineStore('appCache', {
+  persist: {
+    key: 'appCache',
+  },
+  state: (): AppCacheRootState => {
+    return {
+      allTrack: [], // 当前是否横屏
+    };
+  },
+  actions: {
+    setAllTrack(res: AppCacheRootState['allTrack']) {
+      this.allTrack = res;
+    },
+    getTrackInfo() {
+      const res = { audio: 0, video: 0 };
+      this.allTrack.forEach((item) => {
+        // if (item.stream) {
+        if (item.audio === 1) {
+          res.audio += 1;
+        }
+        if (item.video === 1) {
+          res.video += 1;
+        }
+        // }
+      });
+      return res;
+    },
+  },
+});

+ 2 - 0
src/store/index.ts

@@ -1,4 +1,6 @@
 import { createPinia } from 'pinia';
 import { createPinia } from 'pinia';
+import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
 
 
 const store = createPinia();
 const store = createPinia();
+store.use(piniaPluginPersistedstate);
 export default store;
 export default store;

+ 1 - 1
src/utils/index.ts

@@ -99,7 +99,7 @@ export function videoToCanvas(data: {
     cancelAnimationFrame(timer);
     cancelAnimationFrame(timer);
   }
   }
 
 
-  // document.body.appendChild(videoEl);
+  document.body.appendChild(videoEl);
 
 
   drawCanvas();
   drawCanvas();
 
 

+ 6 - 1
src/utils/request.ts

@@ -57,7 +57,7 @@ class MyAxios {
         const statusCode = error.response.status as number;
         const statusCode = error.response.status as number;
         const errorResponse = error.response;
         const errorResponse = error.response;
         const errorResponseData = errorResponse.data;
         const errorResponseData = errorResponse.data;
-        const whiteList = ['400', '401', '403', '404'];
+        const whiteList = ['400', '401', '403', '404', '500'];
         if (error.response) {
         if (error.response) {
           if (!whiteList.includes(`${statusCode}`)) {
           if (!whiteList.includes(`${statusCode}`)) {
             window.$message.error(error.message);
             window.$message.error(error.message);
@@ -85,6 +85,11 @@ class MyAxios {
             window.$message.error(errorResponseData.message);
             window.$message.error(errorResponseData.message);
             return Promise.reject(errorResponseData);
             return Promise.reject(errorResponseData);
           }
           }
+          if (statusCode === 500) {
+            console.error(errorResponseData.error);
+            window.$message.error(errorResponseData.error);
+            return Promise.reject(errorResponseData);
+          }
         } else {
         } else {
           // 请求超时没有response
           // 请求超时没有response
           console.error(error.message);
           console.error(error.message);

+ 6 - 4
src/views/h5/room/index.vue

@@ -34,7 +34,7 @@
       <div
       <div
         ref="canvasRef"
         ref="canvasRef"
         class="media-list"
         class="media-list"
-        :class="{ item: appStore.allTrack.length > 1 }"
+        :class="{ item: appCacheStore.allTrack.length > 1 }"
       ></div>
       ></div>
       <div
       <div
         v-if="showPlayBtn"
         v-if="showPlayBtn"
@@ -107,10 +107,10 @@ import { fetchFindLiveRoom } from '@/api/liveRoom';
 import { usePull } from '@/hooks/use-pull';
 import { usePull } from '@/hooks/use-pull';
 import { DanmuMsgTypeEnum, LiveRoomTypeEnum, liveTypeEnum } from '@/interface';
 import { DanmuMsgTypeEnum, LiveRoomTypeEnum, liveTypeEnum } from '@/interface';
 import router, { mobileRouterName } from '@/router';
 import router, { mobileRouterName } from '@/router';
-import { useAppStore } from '@/store/app';
+import { useAppCacheStore } from '@/store/cache';
 
 
 const route = useRoute();
 const route = useRoute();
-const appStore = useAppStore();
+const appCacheStore = useAppCacheStore();
 
 
 const bottomRef = ref<HTMLDivElement>();
 const bottomRef = ref<HTMLDivElement>();
 const containerRef = ref<HTMLDivElement>();
 const containerRef = ref<HTMLDivElement>();
@@ -183,7 +183,9 @@ function startPull() {
 }
 }
 
 
 onMounted(() => {
 onMounted(() => {
-  scrollTo({ top: 0 });
+  setTimeout(() => {
+    scrollTo(0, 0);
+  }, 100);
   getLiveRoomInfo();
   getLiveRoomInfo();
   if (containerRef.value && bottomRef.value) {
   if (containerRef.value && bottomRef.value) {
     const res =
     const res =

+ 27 - 17
src/views/home/index.vue

@@ -58,7 +58,12 @@
                   LiveRoomTypeEnum.user_wertc
                   LiveRoomTypeEnum.user_wertc
                 "
                 "
                 class="btn flv"
                 class="btn flv"
-                @click="joinRoom(true)"
+                @click="
+                  joinRoom({
+                    isFlv: true,
+                    roomId: currentLiveRoom.live_room_id!,
+                  })
+                "
               >
               >
                 进入直播(flv)
                 进入直播(flv)
               </div>
               </div>
@@ -68,7 +73,12 @@
                   LiveRoomTypeEnum.user_wertc
                   LiveRoomTypeEnum.user_wertc
                 "
                 "
                 class="btn hls"
                 class="btn hls"
-                @click="joinRoom(false)"
+                @click="
+                  joinRoom({
+                    isFlv: false,
+                    roomId: currentLiveRoom.live_room_id!,
+                  })
+                "
               >
               >
                 进入直播(hls)
                 进入直播(hls)
               </div>
               </div>
@@ -125,7 +135,12 @@
             v-for="(iten, indey) in otherLiveRoomList"
             v-for="(iten, indey) in otherLiveRoomList"
             :key="indey"
             :key="indey"
             class="live-room"
             class="live-room"
-            @click="joinOtherRoom(iten.live_room!)"
+            @click="
+              joinRoom({
+                isFlv: false,
+                roomId: iten.live_room?.id!,
+              })
+            "
           >
           >
             <div
             <div
               class="cover"
               class="cover"
@@ -166,7 +181,7 @@ import { useRoute, useRouter } from 'vue-router';
 
 
 import { fetchLiveList } from '@/api/live';
 import { fetchLiveList } from '@/api/live';
 import { usePull } from '@/hooks/use-pull';
 import { usePull } from '@/hooks/use-pull';
-import { ILive, ILiveRoom, LiveRoomTypeEnum, liveTypeEnum } from '@/interface';
+import { ILive, LiveRoomTypeEnum, liveTypeEnum } from '@/interface';
 import { routerName } from '@/router';
 import { routerName } from '@/router';
 
 
 const route = useRoute();
 const route = useRoute();
@@ -238,11 +253,6 @@ onMounted(() => {
   getLiveRoomList();
   getLiveRoomList();
 });
 });
 
 
-function joinOtherRoom(item: ILiveRoom) {
-  currentLiveRoom.value = item;
-  joinRoom(false);
-}
-
 function joinRtcRoom() {
 function joinRtcRoom() {
   if (currentLiveRoom.value?.live_room?.type === LiveRoomTypeEnum.user_srs) {
   if (currentLiveRoom.value?.live_room?.type === LiveRoomTypeEnum.user_srs) {
     router.push({
     router.push({
@@ -267,12 +277,12 @@ function joinRtcRoom() {
   }
   }
 }
 }
 
 
-function joinRoom(isFlv: boolean) {
+function joinRoom(data: { roomId: number; isFlv: boolean }) {
   router.push({
   router.push({
     name: routerName.pull,
     name: routerName.pull,
-    params: { roomId: currentLiveRoom.value?.live_room_id },
+    params: { roomId: data.roomId },
     query: {
     query: {
-      liveType: isFlv ? liveTypeEnum.srsFlvPull : liveTypeEnum.srsHlsPull,
+      liveType: data.isFlv ? liveTypeEnum.srsFlvPull : liveTypeEnum.srsHlsPull,
     },
     },
   });
   });
 }
 }
@@ -387,14 +397,14 @@ function joinRoom(isFlv: boolean) {
 
 
           .btn {
           .btn {
             padding: 14px 26px;
             padding: 14px 26px;
-            border: 2px solid rgba($color: papayawhip, $alpha: 0.5);
+            border: 2px solid rgba($color: $theme-color-gold, $alpha: 0.5);
             border-radius: 6px;
             border-radius: 6px;
             background-color: rgba(0, 0, 0, 0.3);
             background-color: rgba(0, 0, 0, 0.3);
-            color: papayawhip;
+            color: $theme-color-gold;
             font-size: 16px;
             font-size: 16px;
             cursor: pointer;
             cursor: pointer;
             &:hover {
             &:hover {
-              background-color: rgba($color: papayawhip, $alpha: 0.5);
+              background-color: $theme-color-gold;
               color: white;
               color: white;
             }
             }
             &.webrtc {
             &.webrtc {
@@ -441,7 +451,7 @@ function joinRoom(isFlv: boolean) {
               bottom: 0;
               bottom: 0;
               left: 0;
               left: 0;
               z-index: 1;
               z-index: 1;
-              border: 2px solid papayawhip;
+              border: 2px solid $theme-color-gold;
               border-radius: 4px;
               border-radius: 4px;
             }
             }
             .triangle {
             .triangle {
@@ -450,7 +460,7 @@ function joinRoom(isFlv: boolean) {
               left: 0;
               left: 0;
               display: inline-block;
               display: inline-block;
               border: 5px solid transparent;
               border: 5px solid transparent;
-              border-right-color: papayawhip;
+              border-right-color: $theme-color-gold;
               transform: translate(-100%, -50%);
               transform: translate(-100%, -50%);
             }
             }
             &.active {
             &.active {

+ 128 - 0
src/views/privatizationDeployment/index.vue

@@ -0,0 +1,128 @@
+<template>
+  <div class="privatizationDeployment-wrap">
+    <div class="content">
+      <h1 class="title">私有化部署</h1>
+      <div class="desc">
+        billd-live
+        允许个人用于学习/交流用途,如需用做商业,请提前联系并告知作者。
+      </div>
+      <div class="list">
+        <div class="hr"></div>
+        <div class="item">
+          <h2>远程协助</h2>
+          <p>如果在部署期间遇到问题,可以联系作者获取远程协助(付费)。</p>
+        </div>
+        <div class="hr"></div>
+        <div class="item">
+          <h2>定制服务</h2>
+          <p>
+            billd-live作为开源项目,不可能满足所有需求。但提供定制服务(付费),如有需要请联系作者。
+          </p>
+        </div>
+        <div class="hr"></div>
+        <div class="item">
+          <h2>收费标准</h2>
+          <p>1.咨询费用:每小时50元。</p>
+          <p>2.开发费用:每小时150-300元,具体视情况而定。</p>
+        </div>
+        <div class="hr"></div>
+        <div class="item">
+          <h2>联系方式</h2>
+          <p>微信号: shuisheng0095,加微信请备注: 私有化部署+用途</p>
+        </div>
+      </div>
+    </div>
+    <div class="aside">
+      <div class="title">本页目录</div>
+      <div class="item">远程协助</div>
+      <div class="item">定制服务</div>
+      <div class="item">收费标准</div>
+      <div class="item">联系方式</div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup></script>
+
+<style lang="scss" scoped>
+.privatizationDeployment-wrap {
+  display: flex;
+  box-sizing: border-box;
+  margin: 0 auto;
+  padding-top: 50px;
+  width: 960px;
+  color: rgb(33, 53, 71);
+
+  .content {
+    flex: 1;
+    font-size: 16px;
+    .title {
+      margin: 0;
+      margin-bottom: 60px;
+      font-weight: 500;
+      font-size: 40px;
+    }
+    .hr {
+      margin: 60px 0 20px 0;
+      width: 100%;
+      height: 1px;
+      background-color: #e7e7e7;
+    }
+
+    .link {
+      color: $theme-color-gold;
+      text-decoration: none;
+      font-weight: 500;
+    }
+    .list {
+      h2 {
+        font-weight: 600;
+      }
+
+      .item {
+        font-size: 16px;
+        &.sponsors {
+          h3 {
+            margin-top: 50px;
+            text-align: center;
+          }
+          .list {
+            display: flex;
+            flex-wrap: wrap;
+            justify-content: space-evenly;
+          }
+          .bg {
+            display: flex;
+            align-items: center;
+            flex-shrink: 0;
+            justify-content: center;
+            box-sizing: border-box;
+            margin-bottom: 5px;
+            border-radius: 4px;
+            background-color: #f9f9f9;
+            cursor: pointer;
+          }
+        }
+      }
+    }
+  }
+  .aside {
+    padding-left: 90px;
+    .title {
+      margin-bottom: 8px;
+      color: rgb(33, 53, 71);
+      font-weight: 700;
+      font-size: 12px;
+    }
+    .item {
+      margin-bottom: 8px;
+      color: rgba(60, 60, 60, 0.7);
+      font-size: 13px;
+      cursor: pointer;
+      &:hover {
+        color: #213547;
+      }
+    }
+  }
+}
+</style>

+ 5 - 6
src/views/pull/index.vue

@@ -45,7 +45,7 @@
             <div
             <div
               ref="canvasRef"
               ref="canvasRef"
               class="media-list"
               class="media-list"
-              :class="{ item: appStore.allTrack.length > 1 }"
+              :class="{ item: appCacheStore.allTrack.length > 1 }"
             ></div>
             ></div>
             <AudioRoomTip></AudioRoomTip>
             <AudioRoomTip></AudioRoomTip>
             <VideoControls></VideoControls>
             <VideoControls></VideoControls>
@@ -189,15 +189,14 @@ import {
   IGoods,
   IGoods,
   liveTypeEnum,
   liveTypeEnum,
 } from '@/interface';
 } from '@/interface';
-import { useAppStore } from '@/store/app';
+import { useAppCacheStore } from '@/store/cache';
 import { useUserStore } from '@/store/user';
 import { useUserStore } from '@/store/user';
 
 
 import RechargeCpt from './recharge/index.vue';
 import RechargeCpt from './recharge/index.vue';
 
 
 const route = useRoute();
 const route = useRoute();
 const userStore = useUserStore();
 const userStore = useUserStore();
-const appStore = useAppStore();
-
+const appCacheStore = useAppCacheStore();
 const giftGoodsList = ref<IGoods[]>([]);
 const giftGoodsList = ref<IGoods[]>([]);
 const height = ref(0);
 const height = ref(0);
 const giftLoading = ref(false);
 const giftLoading = ref(false);
@@ -432,14 +431,14 @@ onMounted(() => {
           z-index: 1;
           z-index: 1;
           align-items: center;
           align-items: center;
           padding: 12px 26px;
           padding: 12px 26px;
-          border: 2px solid rgba($color: papayawhip, $alpha: 0.5);
+          border: 2px solid rgba($color: $theme-color-gold, $alpha: 0.5);
           border-radius: 6px;
           border-radius: 6px;
           background-color: rgba(0, 0, 0, 0.3);
           background-color: rgba(0, 0, 0, 0.3);
           color: $theme-color-gold;
           color: $theme-color-gold;
           cursor: pointer;
           cursor: pointer;
           transform: translate(-50%, -50%);
           transform: translate(-50%, -50%);
           &:hover {
           &:hover {
-            background-color: rgba($color: papayawhip, $alpha: 0.5);
+            background-color: rgba($color: $theme-color-gold, $alpha: 0.5);
             color: white;
             color: white;
           }
           }
         }
         }

+ 23 - 17
src/views/push/index.vue

@@ -13,10 +13,10 @@
           <div
           <div
             ref="localVideoRef"
             ref="localVideoRef"
             class="media-list"
             class="media-list"
-            :class="{ item: appStore.allTrack.length > 1 }"
+            :class="{ item: appCacheStore.allTrack.length > 1 }"
           ></div>
           ></div>
           <div
           <div
-            v-if="!appStore.allTrack || appStore.allTrack.length <= 0"
+            v-if="!appCacheStore.allTrack || appCacheStore.allTrack.length <= 0"
             class="add-wrap"
             class="add-wrap"
           >
           >
             <n-space>
             <n-space>
@@ -160,7 +160,7 @@
         <div class="title">素材列表</div>
         <div class="title">素材列表</div>
         <div class="list">
         <div class="list">
           <div
           <div
-            v-for="(item, index) in appStore.allTrack"
+            v-for="(item, index) in appCacheStore.allTrack"
             :key="index"
             :key="index"
             class="item"
             class="item"
           >
           >
@@ -264,6 +264,7 @@ import { useRoute } from 'vue-router';
 import { usePush } from '@/hooks/use-push';
 import { usePush } from '@/hooks/use-push';
 import { DanmuMsgTypeEnum, MediaTypeEnum, liveTypeEnum } from '@/interface';
 import { DanmuMsgTypeEnum, MediaTypeEnum, liveTypeEnum } from '@/interface';
 import { AppRootState, useAppStore } from '@/store/app';
 import { AppRootState, useAppStore } from '@/store/app';
+import { useAppCacheStore } from '@/store/cache';
 import { useUserStore } from '@/store/user';
 import { useUserStore } from '@/store/user';
 
 
 import MediaModalCpt from './mediaModal/index.vue';
 import MediaModalCpt from './mediaModal/index.vue';
@@ -272,6 +273,7 @@ import SelectMediaModalCpt from './selectMediaModal/index.vue';
 const route = useRoute();
 const route = useRoute();
 const userStore = useUserStore();
 const userStore = useUserStore();
 const appStore = useAppStore();
 const appStore = useAppStore();
+const appCacheStore = useAppCacheStore();
 const currentMediaType = ref(MediaTypeEnum.camera);
 const currentMediaType = ref(MediaTypeEnum.camera);
 const showSelectMediaModalCpt = ref(false);
 const showSelectMediaModalCpt = ref(false);
 const showMediaModalCpt = ref(false);
 const showMediaModalCpt = ref(false);
@@ -357,15 +359,15 @@ async function addMediaOk(val: {
       mediaName: val.mediaName,
       mediaName: val.mediaName,
       type: MediaTypeEnum.screen,
       type: MediaTypeEnum.screen,
       track: event.getVideoTracks()[0],
       track: event.getVideoTracks()[0],
+      trackid: event.getVideoTracks()[0].id,
       stream: event,
       stream: event,
       streamid: event.id,
       streamid: event.id,
-      trackid: event.getVideoTracks()[0].id,
     };
     };
     const audio = event.getAudioTracks();
     const audio = event.getAudioTracks();
     if (audio.length) {
     if (audio.length) {
       if (
       if (
         isSRS &&
         isSRS &&
-        appStore.allTrack.filter((item) => item.audio === 1).length >= 1
+        appCacheStore.allTrack.filter((item) => item.audio === 1).length >= 1
       ) {
       ) {
         window.$message.error('srs模式最多只能有一个音频');
         window.$message.error('srs模式最多只能有一个音频');
         return;
         return;
@@ -377,22 +379,26 @@ async function addMediaOk(val: {
         mediaName: val.mediaName,
         mediaName: val.mediaName,
         type: MediaTypeEnum.screen,
         type: MediaTypeEnum.screen,
         track: event.getAudioTracks()[0],
         track: event.getAudioTracks()[0],
+        trackid: event.getAudioTracks()[0].id,
         stream: event,
         stream: event,
         streamid: event.id,
         streamid: event.id,
-        trackid: event.getAudioTracks()[0].id,
       };
       };
-      appStore.setAllTrack([...appStore.allTrack, videoTrack, audioTrack]);
+      appCacheStore.setAllTrack([
+        ...appCacheStore.allTrack,
+        videoTrack,
+        audioTrack,
+      ]);
       addTrack(videoTrack);
       addTrack(videoTrack);
       addTrack(audioTrack);
       addTrack(audioTrack);
     } else {
     } else {
       if (
       if (
         isSRS &&
         isSRS &&
-        appStore.allTrack.filter((item) => item.video === 1).length >= 1
+        appCacheStore.allTrack.filter((item) => item.video === 1).length >= 1
       ) {
       ) {
         window.$message.error('srs模式最多只能有一个视频');
         window.$message.error('srs模式最多只能有一个视频');
         return;
         return;
       }
       }
-      appStore.setAllTrack([...appStore.allTrack, videoTrack]);
+      appCacheStore.setAllTrack([...appCacheStore.allTrack, videoTrack]);
       addTrack(videoTrack);
       addTrack(videoTrack);
     }
     }
 
 
@@ -408,7 +414,7 @@ async function addMediaOk(val: {
     });
     });
     if (
     if (
       isSRS &&
       isSRS &&
-      appStore.allTrack.filter((item) => item.video === 1).length >= 1
+      appCacheStore.allTrack.filter((item) => item.video === 1).length >= 1
     ) {
     ) {
       window.$message.error('srs模式最多只能有一个视频');
       window.$message.error('srs模式最多只能有一个视频');
       return;
       return;
@@ -420,11 +426,11 @@ async function addMediaOk(val: {
       mediaName: val.mediaName,
       mediaName: val.mediaName,
       type: MediaTypeEnum.camera,
       type: MediaTypeEnum.camera,
       track: event.getVideoTracks()[0],
       track: event.getVideoTracks()[0],
+      trackid: event.getVideoTracks()[0].id,
       stream: event,
       stream: event,
       streamid: event.id,
       streamid: event.id,
-      trackid: event.getVideoTracks()[0].id,
     };
     };
-    appStore.setAllTrack([...appStore.allTrack, track]);
+    appCacheStore.setAllTrack([...appCacheStore.allTrack, track]);
     addTrack(track);
     addTrack(track);
     console.log('获取摄像头成功');
     console.log('获取摄像头成功');
   } else if (val.type === MediaTypeEnum.microphone) {
   } else if (val.type === MediaTypeEnum.microphone) {
@@ -434,7 +440,7 @@ async function addMediaOk(val: {
     });
     });
     if (
     if (
       isSRS &&
       isSRS &&
-      appStore.allTrack.filter((item) => item.audio === 1).length >= 1
+      appCacheStore.allTrack.filter((item) => item.audio === 1).length >= 1
     ) {
     ) {
       window.$message.error('srs模式最多只能有一个音频');
       window.$message.error('srs模式最多只能有一个音频');
       return;
       return;
@@ -446,11 +452,11 @@ async function addMediaOk(val: {
       mediaName: val.mediaName,
       mediaName: val.mediaName,
       type: MediaTypeEnum.microphone,
       type: MediaTypeEnum.microphone,
       track: event.getAudioTracks()[0],
       track: event.getAudioTracks()[0],
+      trackid: event.getAudioTracks()[0].id,
       stream: event,
       stream: event,
       streamid: event.id,
       streamid: event.id,
-      trackid: event.getAudioTracks()[0].id,
     };
     };
-    appStore.setAllTrack([...appStore.allTrack, track]);
+    appCacheStore.setAllTrack([...appCacheStore.allTrack, track]);
     addTrack(track);
     addTrack(track);
     console.log('获取麦克风成功');
     console.log('获取麦克风成功');
   }
   }
@@ -458,8 +464,8 @@ async function addMediaOk(val: {
 
 
 function handleDelTrack(item: AppRootState['allTrack'][0]) {
 function handleDelTrack(item: AppRootState['allTrack'][0]) {
   console.log('handleDelTrack', item);
   console.log('handleDelTrack', item);
-  const res = appStore.allTrack.filter((iten) => iten.id !== item.id);
-  appStore.setAllTrack(res);
+  const res = appCacheStore.allTrack.filter((iten) => iten.id !== item.id);
+  appCacheStore.setAllTrack(res);
   delTrack(item);
   delTrack(item);
 }
 }
 
 

+ 11 - 8
src/views/push/mediaModal/index.vue

@@ -44,12 +44,12 @@
 import { defineEmits, defineProps, onMounted, ref, withDefaults } from 'vue';
 import { defineEmits, defineProps, onMounted, ref, withDefaults } from 'vue';
 
 
 import { MediaTypeEnum } from '@/interface';
 import { MediaTypeEnum } from '@/interface';
-import { useAppStore } from '@/store/app';
+import { useAppCacheStore } from '@/store/cache';
 import { useNetworkStore } from '@/store/network';
 import { useNetworkStore } from '@/store/network';
 
 
 const mediaName = ref('');
 const mediaName = ref('');
 const networkStore = useNetworkStore();
 const networkStore = useNetworkStore();
-const appStore = useAppStore();
+const appCacheStore = useAppCacheStore();
 
 
 const props = withDefaults(
 const props = withDefaults(
   defineProps<{
   defineProps<{
@@ -95,8 +95,9 @@ async function init() {
       type: MediaTypeEnum.microphone,
       type: MediaTypeEnum.microphone,
     };
     };
     mediaName.value = `麦克风-${
     mediaName.value = `麦克风-${
-      appStore.allTrack.filter((item) => item.type === MediaTypeEnum.microphone)
-        .length + 1
+      appCacheStore.allTrack.filter(
+        (item) => item.type === MediaTypeEnum.microphone
+      ).length + 1
     }`;
     }`;
   } else if (props.mediaType === MediaTypeEnum.camera) {
   } else if (props.mediaType === MediaTypeEnum.camera) {
     res.forEach((item) => {
     res.forEach((item) => {
@@ -113,8 +114,9 @@ async function init() {
       type: MediaTypeEnum.camera,
       type: MediaTypeEnum.camera,
     };
     };
     mediaName.value = `摄像头-${
     mediaName.value = `摄像头-${
-      appStore.allTrack.filter((item) => item.type === MediaTypeEnum.camera)
-        .length + 1
+      appCacheStore.allTrack.filter(
+        (item) => item.type === MediaTypeEnum.camera
+      ).length + 1
     }`;
     }`;
   } else if (props.mediaType === MediaTypeEnum.screen) {
   } else if (props.mediaType === MediaTypeEnum.screen) {
     currentInput.value = {
     currentInput.value = {
@@ -122,8 +124,9 @@ async function init() {
       type: MediaTypeEnum.screen,
       type: MediaTypeEnum.screen,
     };
     };
     mediaName.value = `窗口-${
     mediaName.value = `窗口-${
-      appStore.allTrack.filter((item) => item.type === MediaTypeEnum.screen)
-        .length + 1
+      appCacheStore.allTrack.filter(
+        (item) => item.type === MediaTypeEnum.screen
+      ).length + 1
     }`;
     }`;
   }
   }
 }
 }

+ 383 - 41
src/views/pushByCanvas/index.vue

@@ -1,5 +1,10 @@
 <template>
 <template>
   <div class="push-wrap">
   <div class="push-wrap">
+    <input
+      type="file"
+      @change="ddddd"
+    />
+    <div @click="aaaa">读取</div>
     <div
     <div
       ref="topRef"
       ref="topRef"
       class="left"
       class="left"
@@ -14,7 +19,9 @@
           ref="pushCanvasRef"
           ref="pushCanvasRef"
         ></canvas>
         ></canvas>
         <div
         <div
-          v-if="!appStore.allTrack || appStore.allTrack.length <= 0"
+          v-if="
+            appCacheStore.allTrack.filter((item) => !item.hidden).length <= 0
+          "
           class="add-wrap"
           class="add-wrap"
         >
         >
           <n-space>
           <n-space>
@@ -133,18 +140,35 @@
         <div class="title">素材列表</div>
         <div class="title">素材列表</div>
         <div class="list">
         <div class="list">
           <div
           <div
-            v-for="(item, index) in appStore.allTrack"
+            v-for="(item, index) in appCacheStore.allTrack.filter(
+              (item) => !item.hidden
+            )"
             :key="index"
             :key="index"
             class="item"
             class="item"
           >
           >
             <span class="name">
             <span class="name">
-              ({{ mediaTypeEnumMap[item.type] }}){{ item.mediaName }}
+              ({{ mediaTypeEnumMap[item.type] }}-{{
+                item.audio === 1 ? '音频' : '视频'
+              }}){{ item.mediaName }}
             </span>
             </span>
-            <div
-              class="del"
-              @click="handleDelTrack(item)"
-            >
-              x
+            <div class="control">
+              <div class="control-item">
+                <n-icon
+                  size="16"
+                  @click="handleChangeMuted(item)"
+                >
+                  <VolumeMuteOutline v-if="item.muted"></VolumeMuteOutline>
+                  <VolumeHighOutline v-else></VolumeHighOutline>
+                </n-icon>
+              </div>
+              <div class="control-item">
+                <n-icon
+                  size="16"
+                  @click="handleDelTrack(item)"
+                >
+                  <Close></Close>
+                </n-icon>
+              </div>
             </div>
             </div>
           </div>
           </div>
         </div>
         </div>
@@ -225,10 +249,15 @@
       @close="showMediaModalCpt = false"
       @close="showMediaModalCpt = false"
       @ok="addMediaOk"
       @ok="addMediaOk"
     ></MediaModalCpt>
     ></MediaModalCpt>
+    <OpenMicophoneTipCpt
+      v-if="showOpenMicophoneTipCpt"
+      @close="showOpenMicophoneTipCpt = false"
+    ></OpenMicophoneTipCpt>
   </div>
   </div>
 </template>
 </template>
 
 
 <script lang="ts" setup>
 <script lang="ts" setup>
+import { Close, VolumeHighOutline, VolumeMuteOutline } from '@vicons/ionicons5';
 import { fabric } from 'fabric';
 import { fabric } from 'fabric';
 import { UploadFileInfo } from 'naive-ui';
 import { UploadFileInfo } from 'naive-ui';
 import { NODE_ENV } from 'script/constant';
 import { NODE_ENV } from 'script/constant';
@@ -238,17 +267,20 @@ import * as workerTimers from 'worker-timers';
 
 
 import { usePush } from '@/hooks/use-push';
 import { usePush } from '@/hooks/use-push';
 import { DanmuMsgTypeEnum, MediaTypeEnum, liveTypeEnum } from '@/interface';
 import { DanmuMsgTypeEnum, MediaTypeEnum, liveTypeEnum } from '@/interface';
-import { AppRootState, useAppStore } from '@/store/app';
+import { AppRootState } from '@/store/app';
+import { useAppCacheStore } from '@/store/cache';
 import { useUserStore } from '@/store/user';
 import { useUserStore } from '@/store/user';
 import { createVideo, generateBase64, getRandomEnglishString } from '@/utils';
 import { createVideo, generateBase64, getRandomEnglishString } from '@/utils';
 
 
 import MediaModalCpt from './mediaModal/index.vue';
 import MediaModalCpt from './mediaModal/index.vue';
+import OpenMicophoneTipCpt from './openMicophoneTip/index.vue';
 import SelectMediaModalCpt from './selectMediaModal/index.vue';
 import SelectMediaModalCpt from './selectMediaModal/index.vue';
 
 
 const route = useRoute();
 const route = useRoute();
 const userStore = useUserStore();
 const userStore = useUserStore();
-const appStore = useAppStore();
+const appCacheStore = useAppCacheStore();
 const currentMediaType = ref(MediaTypeEnum.camera);
 const currentMediaType = ref(MediaTypeEnum.camera);
+const showOpenMicophoneTipCpt = ref(false);
 const showSelectMediaModalCpt = ref(false);
 const showSelectMediaModalCpt = ref(false);
 const showMediaModalCpt = ref(false);
 const showMediaModalCpt = ref(false);
 const topRef = ref<HTMLDivElement>();
 const topRef = ref<HTMLDivElement>();
@@ -304,6 +336,7 @@ const mediaTypeEnumMap = {
   [MediaTypeEnum.screen]: '窗口',
   [MediaTypeEnum.screen]: '窗口',
   [MediaTypeEnum.img]: '图片',
   [MediaTypeEnum.img]: '图片',
   [MediaTypeEnum.txt]: '文字',
   [MediaTypeEnum.txt]: '文字',
+  [MediaTypeEnum.media]: '视频',
 };
 };
 
 
 watch(
 watch(
@@ -338,26 +371,121 @@ function onPageVisibility() {
   }
   }
 }
 }
 
 
-function handleStartLive() {
-  lastCoverImg.value = generateBase64(pushCanvasRef.value!);
+function initUserMedia() {
+  navigator.mediaDevices
+    .getUserMedia({
+      video: true,
+      audio: false,
+    })
+    .then(() => {
+      console.log('初始化获取摄像头成功');
+    })
+    .catch(() => {
+      console.log('初始化获取摄像头失败');
+    })
+    .finally(() => {
+      navigator.mediaDevices
+        .getUserMedia({
+          video: false,
+          audio: true,
+        })
+        .then(() => {
+          console.log('初始化获取麦克风成功');
+        })
+        .catch(() => {
+          console.log('初始化获取麦克风失败');
+          showOpenMicophoneTipCpt.value = true;
+        });
+    });
+}
 
 
-  const allAudioTrack = appStore.allTrack.filter((item) => item.audio === 1);
-  if (allAudioTrack.length) {
-    audioCtx.value = new AudioContext();
+// 处理空音频轨
+function initNullAudio() {
+  // 创建一个AudioContext实例
+  const audioContext = new window.AudioContext();
+
+  // 创建一个GainNode实例来控制音频的音量
+  const gainNode = audioContext.createGain();
+
+  // 创建一个空的音频缓存
+  const buffer = audioContext.createBuffer(
+    2,
+    audioContext.sampleRate * 3,
+    audioContext.sampleRate
+  );
+
+  // 创建一个用于播放音频的AudioBufferSourceNode
+  const source = audioContext.createBufferSource();
+  source.buffer = buffer;
+
+  // 将源连接到gain node,再连接到输出
+  source.connect(gainNode);
+  gainNode.connect(audioContext.destination);
+  const destination = audioContext.createMediaStreamDestination();
+
+  const audioTrack = {
+    id: getRandomEnglishString(8),
+    audio: 1,
+    video: 2,
+    mediaName: '',
+    type: MediaTypeEnum.webAudio,
+    track: destination.stream.getAudioTracks()[0],
+    trackid: destination.stream.getAudioTracks()[0].id,
+    stream: destination.stream,
+    streamid: destination.stream.id,
+    hidden: true,
+  };
+  appCacheStore.setAllTrack([...appCacheStore.allTrack, audioTrack]);
+  // const vel = createVideo({});
+  // vel.srcObject = destination.stream;
+  // document.body.appendChild(vel);
+}
+let streamTmp: MediaStream;
+let vel;
+
+function handleMixedAudio() {
+  const allAudioTrack = appCacheStore.allTrack.filter(
+    (item) => item.audio === 1
+  );
+  if (allAudioTrack.length && audioCtx.value) {
     const gainNode = audioCtx.value.createGain();
     const gainNode = audioCtx.value.createGain();
     allAudioTrack.forEach((item) => {
     allAudioTrack.forEach((item) => {
       if (!audioCtx.value) return;
       if (!audioCtx.value) return;
-      // const destination = audioCtx.value.createMediaStreamDestination();
       const audioInput = audioCtx.value.createMediaStreamSource(item.stream!);
       const audioInput = audioCtx.value.createMediaStreamSource(item.stream!);
-      // gainNode.connect(destination);
       audioInput.connect(gainNode);
       audioInput.connect(gainNode);
+      console.log('混流', item.stream?.id, item.stream);
     });
     });
+    if (streamTmp) {
+      const destination = audioCtx.value.createMediaStreamDestination();
+      streamTmp.addTrack(destination.stream.getAudioTracks()[0]);
+      gainNode.connect(destination);
+      const mixedStream = new MediaStream();
+      mixedStream.addTrack(destination.stream.getAudioTracks()[0]);
+      mixedStream.addTrack(canvasVideoStream.value!.getVideoTracks()[0]);
+      canvasVideoStream.value = mixedStream;
+      console.log('替换了');
+      return;
+    }
     const destination = audioCtx.value.createMediaStreamDestination();
     const destination = audioCtx.value.createMediaStreamDestination();
-    // video.onloadeddata = () => {
+    streamTmp = destination.stream;
     // @ts-ignore
     // @ts-ignore
     canvasVideoStream.value?.addTrack(destination.stream.getAudioTracks()[0]);
     canvasVideoStream.value?.addTrack(destination.stream.getAudioTracks()[0]);
-    // video.srcObject = destination.stream;
     gainNode.connect(destination);
     gainNode.connect(destination);
+    vel = createVideo({});
+    vel.srcObject = destination.stream;
+    document.body.appendChild(vel);
+  }
+}
+
+function handleStartLive() {
+  lastCoverImg.value = generateBase64(pushCanvasRef.value!);
+
+  const allAudioTrack = appCacheStore.allTrack.filter(
+    (item) => item.audio === 1
+  );
+  if (allAudioTrack.length) {
+    audioCtx.value = new AudioContext();
+    handleMixedAudio();
   }
   }
   startLive();
   startLive();
 }
 }
@@ -485,7 +613,7 @@ function changeCanvasAttr({
     // fabricCanvas.value.forEachObject((canvas) => {
     // fabricCanvas.value.forEachObject((canvas) => {
     //   canvas.setCoords();
     //   canvas.setCoords();
     // });
     // });
-    appStore.allTrack.forEach((item) => {
+    appCacheStore.allTrack.forEach((item) => {
       if (item.canvasDom) {
       if (item.canvasDom) {
         // 分辨率变小了,将图片变小
         // 分辨率变小了,将图片变小
         if (newHeight < oldHeight) {
         if (newHeight < oldHeight) {
@@ -562,6 +690,11 @@ function initCanvas() {
 }
 }
 
 
 onMounted(() => {
 onMounted(() => {
+  setTimeout(() => {
+    scrollTo(0, 0);
+  }, 100);
+  initNullAudio();
+  initUserMedia();
   initCanvas();
   initCanvas();
   document.addEventListener('visibilitychange', onPageVisibility);
   document.addEventListener('visibilitychange', onPageVisibility);
 });
 });
@@ -582,6 +715,7 @@ async function addMediaOk(val: {
   mediaName: string;
   mediaName: string;
   txtInfo?: { txt: string; color: string };
   txtInfo?: { txt: string; color: string };
   imgInfo?: UploadFileInfo[];
   imgInfo?: UploadFileInfo[];
+  mediaInfo?: UploadFileInfo[];
 }) {
 }) {
   showMediaModalCpt.value = false;
   showMediaModalCpt.value = false;
   if (val.type === MediaTypeEnum.screen) {
   if (val.type === MediaTypeEnum.screen) {
@@ -600,9 +734,9 @@ async function addMediaOk(val: {
       mediaName: val.mediaName,
       mediaName: val.mediaName,
       type: MediaTypeEnum.screen,
       type: MediaTypeEnum.screen,
       track: event.getVideoTracks()[0],
       track: event.getVideoTracks()[0],
+      trackid: event.getVideoTracks()[0].id,
       stream: event,
       stream: event,
       streamid: event.id,
       streamid: event.id,
-      trackid: event.getVideoTracks()[0].id,
     };
     };
 
 
     const { canvasDom, initScale } = await autoCreateVideo({
     const { canvasDom, initScale } = await autoCreateVideo({
@@ -622,15 +756,20 @@ async function addMediaOk(val: {
         mediaName: val.mediaName,
         mediaName: val.mediaName,
         type: MediaTypeEnum.screen,
         type: MediaTypeEnum.screen,
         track: event.getAudioTracks()[0],
         track: event.getAudioTracks()[0],
+        trackid: event.getAudioTracks()[0].id,
         stream: event,
         stream: event,
         streamid: event.id,
         streamid: event.id,
-        trackid: event.getAudioTracks()[0].id,
       };
       };
-      appStore.setAllTrack([...appStore.allTrack, videoTrack, audioTrack]);
+      appCacheStore.setAllTrack([
+        ...appCacheStore.allTrack,
+        videoTrack,
+        audioTrack,
+      ]);
+      handleMixedAudio();
       addTrack(videoTrack);
       addTrack(videoTrack);
       addTrack(audioTrack);
       addTrack(audioTrack);
     } else {
     } else {
-      appStore.setAllTrack([...appStore.allTrack, videoTrack]);
+      appCacheStore.setAllTrack([...appCacheStore.allTrack, videoTrack]);
       addTrack(videoTrack);
       addTrack(videoTrack);
     }
     }
 
 
@@ -649,9 +788,9 @@ async function addMediaOk(val: {
       mediaName: val.mediaName,
       mediaName: val.mediaName,
       type: MediaTypeEnum.camera,
       type: MediaTypeEnum.camera,
       track: event.getVideoTracks()[0],
       track: event.getVideoTracks()[0],
+      trackid: event.getVideoTracks()[0].id,
       stream: event,
       stream: event,
       streamid: event.id,
       streamid: event.id,
-      trackid: event.getVideoTracks()[0].id,
     };
     };
     const { canvasDom, initScale } = await autoCreateVideo({
     const { canvasDom, initScale } = await autoCreateVideo({
       stream: event,
       stream: event,
@@ -661,7 +800,7 @@ async function addMediaOk(val: {
     // @ts-ignore
     // @ts-ignore
     videoTrack.initScale = initScale;
     videoTrack.initScale = initScale;
 
 
-    appStore.setAllTrack([...appStore.allTrack, videoTrack]);
+    appCacheStore.setAllTrack([...appCacheStore.allTrack, videoTrack]);
     addTrack(videoTrack);
     addTrack(videoTrack);
     console.log('获取摄像头成功');
     console.log('获取摄像头成功');
   } else if (val.type === MediaTypeEnum.microphone) {
   } else if (val.type === MediaTypeEnum.microphone) {
@@ -676,11 +815,12 @@ async function addMediaOk(val: {
       mediaName: val.mediaName,
       mediaName: val.mediaName,
       type: MediaTypeEnum.microphone,
       type: MediaTypeEnum.microphone,
       track: event.getAudioTracks()[0],
       track: event.getAudioTracks()[0],
+      trackid: event.getAudioTracks()[0].id,
       stream: event,
       stream: event,
       streamid: event.id,
       streamid: event.id,
-      trackid: event.getAudioTracks()[0].id,
     };
     };
-    appStore.setAllTrack([...appStore.allTrack, audioTrack]);
+    appCacheStore.setAllTrack([...appCacheStore.allTrack, audioTrack]);
+    handleMixedAudio();
     addTrack(audioTrack);
     addTrack(audioTrack);
 
 
     console.log('获取麦克风成功');
     console.log('获取麦克风成功');
@@ -688,13 +828,13 @@ async function addMediaOk(val: {
     const txtTrack = {
     const txtTrack = {
       id: getRandomEnglishString(8),
       id: getRandomEnglishString(8),
       audio: 2,
       audio: 2,
-      video: 2,
+      video: 1,
       mediaName: val.mediaName,
       mediaName: val.mediaName,
       type: MediaTypeEnum.txt,
       type: MediaTypeEnum.txt,
       track: undefined,
       track: undefined,
+      trackid: undefined,
       stream: undefined,
       stream: undefined,
       streamid: undefined,
       streamid: undefined,
-      trackid: undefined,
     };
     };
     if (fabricCanvas.value) {
     if (fabricCanvas.value) {
       const dom = markRaw(
       const dom = markRaw(
@@ -710,7 +850,7 @@ async function addMediaOk(val: {
     }
     }
 
 
     // @ts-ignore
     // @ts-ignore
-    appStore.setAllTrack([...appStore.allTrack, txtTrack]);
+    appCacheStore.setAllTrack([...appCacheStore.allTrack, txtTrack]);
     addTrack(txtTrack);
     addTrack(txtTrack);
 
 
     console.log('获取文字成功', fabricCanvas.value);
     console.log('获取文字成功', fabricCanvas.value);
@@ -718,13 +858,13 @@ async function addMediaOk(val: {
     const imgTrack = {
     const imgTrack = {
       id: getRandomEnglishString(8),
       id: getRandomEnglishString(8),
       audio: 2,
       audio: 2,
-      video: 2,
+      video: 1,
       mediaName: val.mediaName,
       mediaName: val.mediaName,
       type: MediaTypeEnum.img,
       type: MediaTypeEnum.img,
       track: undefined,
       track: undefined,
+      trackid: undefined,
       stream: undefined,
       stream: undefined,
       streamid: undefined,
       streamid: undefined,
-      trackid: undefined,
     };
     };
 
 
     if (fabricCanvas.value) {
     if (fabricCanvas.value) {
@@ -772,25 +912,221 @@ async function addMediaOk(val: {
     }
     }
 
 
     // @ts-ignore
     // @ts-ignore
-    appStore.setAllTrack([...appStore.allTrack, imgTrack]);
+    appCacheStore.setAllTrack([...appCacheStore.allTrack, imgTrack]);
     addTrack(imgTrack);
     addTrack(imgTrack);
 
 
     console.log('获取图片成功', fabricCanvas.value);
     console.log('获取图片成功', fabricCanvas.value);
+  } else if (val.type === MediaTypeEnum.media) {
+    const mediaVideoTrack = {
+      id: getRandomEnglishString(8),
+      audio: 2,
+      video: 1,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.media,
+      track: undefined,
+      trackid: undefined,
+      stream: undefined,
+      streamid: undefined,
+    };
+    if (fabricCanvas.value) {
+      if (!val.mediaInfo) return;
+      const file = val.mediaInfo[0].file!;
+      console.log(file);
+      console.log(file.name);
+      const url = URL.createObjectURL(file);
+      console.log(url);
+      const videoEl = createVideo({});
+      videoEl.src = url;
+      videoEl.muted = false;
+      videoEl.style.width = `1px`;
+      videoEl.style.height = `1px`;
+      videoEl.style.position = 'fixed';
+      videoEl.style.bottom = '0';
+      videoEl.style.right = '0';
+      videoEl.style.opacity = '0';
+      videoEl.style.pointerEvents = 'none';
+      document.body.appendChild(videoEl);
+      const videoRes = await new Promise<HTMLVideoElement>((resolve) => {
+        videoEl.onloadedmetadata = () => {
+          resolve(videoEl);
+        };
+      });
+      // @ts-ignore
+      const stream = videoRes.captureStream();
+      const { canvasDom, initScale } = await autoCreateVideo({
+        stream,
+      });
+      // @ts-ignore
+      mediaVideoTrack.canvasDom = canvasDom;
+      // @ts-ignore
+      mediaVideoTrack.initScale = initScale;
+      console.log('视频有音频', stream.getAudioTracks()[0]);
+      if (stream.getAudioTracks()[0]) {
+        const audioTrack = {
+          id: getRandomEnglishString(8),
+          audio: 1,
+          video: 2,
+          mediaName: val.mediaName,
+          type: MediaTypeEnum.media,
+          track: stream.getAudioTracks()[0],
+          trackid: stream.getAudioTracks()[0].id,
+          stream,
+          streamid: stream.id,
+        };
+        // @ts-ignore
+        appCacheStore.setAllTrack([...appCacheStore.allTrack, audioTrack]);
+        handleMixedAudio();
+        addTrack(audioTrack);
+      }
+    }
+    // @ts-ignore
+    appCacheStore.setAllTrack([...appCacheStore.allTrack, mediaVideoTrack]);
+    addTrack(mediaVideoTrack);
+
+    console.log('获取视频成功', fabricCanvas.value);
   }
   }
+
   canvasVideoStream.value = pushCanvasRef.value!.captureStream();
   canvasVideoStream.value = pushCanvasRef.value!.captureStream();
 }
 }
 
 
+function handleChangeMuted(item: AppRootState['allTrack'][0]) {}
+
 function handleDelTrack(item: AppRootState['allTrack'][0]) {
 function handleDelTrack(item: AppRootState['allTrack'][0]) {
   console.log('handleDelTrack', item);
   console.log('handleDelTrack', item);
   if (item.canvasDom !== undefined) {
   if (item.canvasDom !== undefined) {
     // @ts-ignore
     // @ts-ignore
     fabricCanvas.value?.remove(item.canvasDom);
     fabricCanvas.value?.remove(item.canvasDom);
   }
   }
-  const res = appStore.allTrack.filter((iten) => iten.id !== item.id);
-  appStore.setAllTrack(res);
+  const res = appCacheStore.allTrack.filter((iten) => iten.id !== item.id);
+  appCacheStore.setAllTrack(res);
   delTrack(item);
   delTrack(item);
 }
 }
 
 
+function aaaa(e) {
+  // @ts-ignore
+  const requestFileSystem =
+    // @ts-ignore
+    window.requestFileSystem || window.webkitRequestFileSystem;
+  // @ts-ignore
+  const directoryEntry = window.directoryEntry || window.webkitDirectoryEntry;
+  function onError(err) {
+    console.log(err);
+  }
+  function onFs(fs) {
+    fs.root.getFile(
+      'myvideo',
+      {},
+      function (fileEntry) {
+        fileEntry.file(function (file) {
+          console.log(file, 777);
+          const url = URL.createObjectURL(file);
+          console.log(url);
+          const videoEl = createVideo({});
+          videoEl.src = url;
+          // videoEl.muted = false;
+          // videoEl.style.width = `1px`;
+          // videoEl.style.height = `1px`;
+          // videoEl.style.position = 'fixed';
+          // videoEl.style.bottom = '0';
+          // videoEl.style.right = '0';
+          // videoEl.style.opacity = '0';
+          // videoEl.style.pointerEvents = 'none';
+          document.body.appendChild(videoEl);
+        }, onError);
+      },
+      onError
+    );
+  }
+
+  // Opening a file system with temporary storage
+  requestFileSystem(
+    // @ts-ignore
+    window.PERSISTENT,
+    1024 * 1024 * 1024 /* 1GB */,
+    onFs,
+    onError
+  );
+}
+function ddddd(e) {
+  const file = e.target.files[0] as File;
+  // @ts-ignore
+  const requestFileSystem =
+    // @ts-ignore
+    window.requestFileSystem || window.webkitRequestFileSystem;
+  // @ts-ignore
+  const directoryEntry = window.directoryEntry || window.webkitDirectoryEntry;
+  function onError(err) {
+    console.log(err);
+  }
+  function onFs(fs) {
+    // 创建文件
+    fs.root.getFile(
+      'myvideo',
+      { create: true },
+      function (fileEntry) {
+        // 创建文件写入流
+        fileEntry.createWriter(function (fileWriter) {
+          fileWriter.onwriteend = () => {
+            // 完成后关闭文件
+            fileWriter.abort();
+          };
+          // 写入文件内容
+          fileWriter.write(file);
+          console.log('写入文件内容');
+          // const content = new Blob([fileContent]);
+          // fileWriter.write(content);
+        });
+      },
+      () => {
+        console.log('err');
+      }
+    );
+    // fs.root.getDirectory(
+    //   'Documents',
+    //   { create: true },
+    //   function (directoryEntry) {
+    //     console.log(directoryEntry);
+    //     // directoryEntry.isFile === false
+    //     // directoryEntry.isDirectory === true
+    //     // directoryEntry.name === 'Documents'
+    //     // directoryEntry.fullPath === '/Documents'
+    //   },
+    //   onError
+    // );
+  }
+
+  // Opening a file system with temporary storage
+  requestFileSystem(
+    // @ts-ignore
+    window.PERSISTENT,
+    1024 * 1024 * 1024 /* 1GB */,
+    onFs,
+    onError
+  );
+}
+
+function readDirectory(directory) {
+  const dirReader = directory.createReader();
+  let entries = [];
+
+  const getEntries = () => {
+    dirReader.readEntries(
+      (results) => {
+        if (results.length) {
+          entries = entries.concat(toArray(results));
+          getEntries();
+        }
+      },
+      (error) => {
+        /* handle error — error is a FileError object */
+      }
+    );
+  };
+
+  getEntries();
+  return entries;
+}
+
 function handleStartMedia(item: { type: MediaTypeEnum; txt: string }) {
 function handleStartMedia(item: { type: MediaTypeEnum; txt: string }) {
   currentMediaType.value = item.type;
   currentMediaType.value = item.type;
   showMediaModalCpt.value = true;
   showMediaModalCpt.value = true;
@@ -935,15 +1271,21 @@ function handleStartMedia(item: { type: MediaTypeEnum; txt: string }) {
         align-items: center;
         align-items: center;
         justify-content: space-between;
         justify-content: space-between;
         margin: 5px 0;
         margin: 5px 0;
-        font-size: 12px;
+        font-size: 14px;
         &:hover {
         &:hover {
-          .del {
-            display: block;
+          .control {
+            display: flex;
+            align-items: center;
           }
           }
         }
         }
-        .del {
+        .control {
           display: none;
           display: none;
-          cursor: pointer;
+          .control-item {
+            cursor: pointer;
+            &:not(:last-child) {
+              margin-right: 6px;
+            }
+          }
         }
         }
       }
       }
       .bottom {
       .bottom {

+ 57 - 13
src/views/pushByCanvas/mediaModal/index.vue

@@ -29,7 +29,10 @@
           <div class="item">
           <div class="item">
             <div class="label">内容</div>
             <div class="label">内容</div>
             <div class="value">
             <div class="value">
-              <n-input v-model:value="txtInfo.txt" />
+              <n-input
+                ref="inputInstRef"
+                v-model:value="txtInfo.txt"
+              />
             </div>
             </div>
           </div>
           </div>
           <div class="item">
           <div class="item">
@@ -50,7 +53,20 @@
               >
               >
                 <n-button>选择文件</n-button>
                 <n-button>选择文件</n-button>
               </n-upload>
               </n-upload>
-              <!-- <input type="file" /> -->
+            </div>
+          </div>
+        </template>
+        <template v-if="props.mediaType === MediaTypeEnum.media">
+          <div class="item">
+            <div class="label">视频</div>
+            <div class="value">
+              <n-upload
+                :max="1"
+                accept="video/mp4, video/quicktime"
+                :on-update:file-list="changMedia"
+              >
+                <n-button>选择文件</n-button>
+              </n-upload>
             </div>
             </div>
           </div>
           </div>
         </template>
         </template>
@@ -71,14 +87,15 @@
 </template>
 </template>
 
 
 <script lang="ts" setup>
 <script lang="ts" setup>
-import { UploadFileInfo } from 'naive-ui';
+import { InputInst, UploadFileInfo } from 'naive-ui';
 import { defineEmits, defineProps, onMounted, ref, withDefaults } from 'vue';
 import { defineEmits, defineProps, onMounted, ref, withDefaults } from 'vue';
 
 
 import { MediaTypeEnum } from '@/interface';
 import { MediaTypeEnum } from '@/interface';
-import { useAppStore } from '@/store/app';
+import { useAppCacheStore } from '@/store/cache';
 
 
+const inputInstRef = ref<InputInst | null>(null);
 const mediaName = ref('');
 const mediaName = ref('');
-const appStore = useAppStore();
+const appCacheStore = useAppCacheStore();
 
 
 const props = withDefaults(
 const props = withDefaults(
   defineProps<{
   defineProps<{
@@ -93,6 +110,7 @@ const emits = defineEmits(['close', 'ok']);
 const inputOptions = ref<{ label: string; value: string }[]>([]);
 const inputOptions = ref<{ label: string; value: string }[]>([]);
 const txtInfo = ref<{ txt: string; color: string }>();
 const txtInfo = ref<{ txt: string; color: string }>();
 const imgInfo = ref<UploadFileInfo[]>();
 const imgInfo = ref<UploadFileInfo[]>();
+const mediaInfo = ref<UploadFileInfo[]>();
 const currentInput = ref<{
 const currentInput = ref<{
   type: MediaTypeEnum;
   type: MediaTypeEnum;
   deviceId: string;
   deviceId: string;
@@ -108,6 +126,9 @@ onMounted(() => {
 function changImg(list: UploadFileInfo[]) {
 function changImg(list: UploadFileInfo[]) {
   imgInfo.value = list;
   imgInfo.value = list;
 }
 }
+function changMedia(list: UploadFileInfo[]) {
+  mediaInfo.value = list;
+}
 
 
 function handleOk() {
 function handleOk() {
   if (mediaName.value.length < 4 || mediaName.value.length > 10) {
   if (mediaName.value.length < 4 || mediaName.value.length > 10) {
@@ -126,12 +147,19 @@ function handleOk() {
       return;
       return;
     }
     }
   }
   }
+  if (props.mediaType === MediaTypeEnum.media) {
+    if (mediaInfo.value?.length! !== 1) {
+      window.$message.info('请选择视频!');
+      return;
+    }
+  }
 
 
   emits('ok', {
   emits('ok', {
     ...currentInput.value,
     ...currentInput.value,
     mediaName: mediaName.value,
     mediaName: mediaName.value,
     txtInfo: txtInfo.value,
     txtInfo: txtInfo.value,
     imgInfo: imgInfo.value,
     imgInfo: imgInfo.value,
+    mediaInfo: mediaInfo.value,
   });
   });
 }
 }
 
 
@@ -152,8 +180,9 @@ async function init() {
       type: MediaTypeEnum.microphone,
       type: MediaTypeEnum.microphone,
     };
     };
     mediaName.value = `麦克风-${
     mediaName.value = `麦克风-${
-      appStore.allTrack.filter((item) => item.type === MediaTypeEnum.microphone)
-        .length + 1
+      appCacheStore.allTrack.filter(
+        (item) => item.type === MediaTypeEnum.microphone
+      ).length + 1
     }`;
     }`;
   } else if (props.mediaType === MediaTypeEnum.camera) {
   } else if (props.mediaType === MediaTypeEnum.camera) {
     res.forEach((item) => {
     res.forEach((item) => {
@@ -170,8 +199,9 @@ async function init() {
       type: MediaTypeEnum.camera,
       type: MediaTypeEnum.camera,
     };
     };
     mediaName.value = `摄像头-${
     mediaName.value = `摄像头-${
-      appStore.allTrack.filter((item) => item.type === MediaTypeEnum.camera)
-        .length + 1
+      appCacheStore.allTrack.filter(
+        (item) => item.type === MediaTypeEnum.camera
+      ).length + 1
     }`;
     }`;
   } else if (props.mediaType === MediaTypeEnum.screen) {
   } else if (props.mediaType === MediaTypeEnum.screen) {
     currentInput.value = {
     currentInput.value = {
@@ -179,8 +209,9 @@ async function init() {
       type: MediaTypeEnum.screen,
       type: MediaTypeEnum.screen,
     };
     };
     mediaName.value = `窗口-${
     mediaName.value = `窗口-${
-      appStore.allTrack.filter((item) => item.type === MediaTypeEnum.screen)
-        .length + 1
+      appCacheStore.allTrack.filter(
+        (item) => item.type === MediaTypeEnum.screen
+      ).length + 1
     }`;
     }`;
   } else if (props.mediaType === MediaTypeEnum.txt) {
   } else if (props.mediaType === MediaTypeEnum.txt) {
     currentInput.value = {
     currentInput.value = {
@@ -189,9 +220,12 @@ async function init() {
     };
     };
     txtInfo.value = { txt: '', color: 'rgba(255,215,0,1)' };
     txtInfo.value = { txt: '', color: 'rgba(255,215,0,1)' };
     mediaName.value = `文字-${
     mediaName.value = `文字-${
-      appStore.allTrack.filter((item) => item.type === MediaTypeEnum.txt)
+      appCacheStore.allTrack.filter((item) => item.type === MediaTypeEnum.txt)
         .length + 1
         .length + 1
     }`;
     }`;
+    setTimeout(() => {
+      inputInstRef.value?.focus();
+    }, 100);
   } else if (props.mediaType === MediaTypeEnum.img) {
   } else if (props.mediaType === MediaTypeEnum.img) {
     currentInput.value = {
     currentInput.value = {
       ...currentInput.value,
       ...currentInput.value,
@@ -199,7 +233,17 @@ async function init() {
     };
     };
     imgInfo.value = [];
     imgInfo.value = [];
     mediaName.value = `图片-${
     mediaName.value = `图片-${
-      appStore.allTrack.filter((item) => item.type === MediaTypeEnum.img)
+      appCacheStore.allTrack.filter((item) => item.type === MediaTypeEnum.img)
+        .length + 1
+    }`;
+  } else if (props.mediaType === MediaTypeEnum.media) {
+    currentInput.value = {
+      ...currentInput.value,
+      type: MediaTypeEnum.media,
+    };
+    mediaInfo.value = [];
+    mediaName.value = `视频-${
+      appCacheStore.allTrack.filter((item) => item.type === MediaTypeEnum.media)
         .length + 1
         .length + 1
     }`;
     }`;
   }
   }

+ 32 - 0
src/views/pushByCanvas/openMicophoneTip/index.vue

@@ -0,0 +1,32 @@
+<template>
+  <div class="openMicophoneTip-wrap">
+    <Modal
+      title="提示"
+      :mask-closable="false"
+      @close="emits('close')"
+    >
+      <div class="container">
+        <p>采集麦克风声音失败,请确认是否授权!</p>
+      </div>
+      <template #footer></template>
+    </Modal>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { defineEmits, onMounted } from 'vue';
+
+const emits = defineEmits(['close']);
+
+onMounted(() => {});
+</script>
+
+<style lang="scss" scoped>
+.openMicophoneTip-wrap {
+  text-align: initial;
+
+  .container {
+    padding-top: 10px;
+  }
+}
+</style>