Browse Source

feat: 优化

shuisheng 2 năm trước cách đây
mục cha
commit
4aedae6fe0

+ 15 - 0
Remark.md

@@ -1,3 +1,18 @@
+## 测试
+
+播放错误,重连测试:
+
+```sh
+## 找到ffmpeg进程
+ps aux|grep ffmpeg
+
+## 断掉进程
+kill -9 xxxx
+
+## 重新推流
+ffmpeg -loglevel quiet -readrate 1 -stream_loop -1 -i /Users/huangshuisheng/Desktop/hss/galaxy-s10/billd-live-server/src/video/fddm_mhsw.mp4 -vcodec copy -acodec copy -f flv 'rtmp://localhost/livestream/roomId___2?token=47b650e1196e9c9d5b30f18cbd7a3cf1&random_id=B86Os2F5sf'
+```
+
 ### 破音问题
 
 当前电脑正在播放音频,如果此时开始直播,添加了一个视频素材(有声音的),然后拖拽这个视频素材,就会出现类似破音问题。如果把电脑的外放音频都关闭了,再开始直播添加视频素材,拖拽就不会出现破音问题。

+ 3 - 0
src/constant.ts

@@ -25,6 +25,9 @@ export const LOCALSTORAGE_KEY = {
   verion: '0.0.1',
 };
 
+export const bilibiliCollectiondetail =
+  'https://space.bilibili.com/381307133/channel/collectiondetail?sid=1458070&ctype=0';
+
 export const mediaTypeEnumMap = {
   [MediaTypeEnum.camera]: '摄像头',
   [MediaTypeEnum.microphone]: '麦克风',

+ 88 - 85
src/hooks/use-play.ts

@@ -14,6 +14,9 @@ export function useFlvPlay() {
   const flvPlayer = ref<mpegts.Player>();
   const flvVideoEl = ref<HTMLVideoElement>();
   const appStore = useAppStore();
+  const retryMax = ref(30);
+  const retry = ref(0);
+  const retrying = ref(false);
 
   onMounted(() => {});
 
@@ -24,6 +27,7 @@ export function useFlvPlay() {
   function destroyFlv() {
     if (flvPlayer.value) {
       flvPlayer.value.destroy();
+      flvPlayer.value = undefined;
     }
     flvVideoEl.value?.remove();
   }
@@ -46,47 +50,60 @@ export function useFlvPlay() {
 
   function startFlvPlay(data: { flvurl: string }) {
     console.log('startFlvPlay', data.flvurl);
-    destroyFlv();
     return new Promise<{ width: number; height: number }>((resolve) => {
-      if (mpegts.getFeatureList().mseLivePlayback && mpegts.isSupported()) {
-        flvPlayer.value = mpegts.createPlayer({
-          type: 'flv', // could also be mpegts, m2ts, flv
-          isLive: true,
-          url: data.flvurl,
-        });
-        const videoEl = createVideo({ muted: true, autoplay: true });
-        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);
-        flvVideoEl.value = videoEl;
-        flvVideoEl.value.addEventListener('play', () => {
-          console.log('flv-play');
-        });
-        flvVideoEl.value.addEventListener('playing', () => {
-          console.log('flv-playing');
-          setMuted(appStore.muted);
-          resolve({
-            width: flvVideoEl.value?.videoWidth || 0,
-            height: flvVideoEl.value?.videoHeight || 0,
+      function main() {
+        destroyFlv();
+        if (mpegts.getFeatureList().mseLivePlayback && mpegts.isSupported()) {
+          flvPlayer.value = mpegts.createPlayer({
+            type: 'flv', // could also be mpegts, m2ts, flv
+            isLive: true,
+            url: data.flvurl,
           });
-        });
-        flvPlayer.value.attachMediaElement(flvVideoEl.value);
-        flvPlayer.value.load();
-        try {
-          console.log(`开始播放flv,muted:${appStore.muted}`);
-          flvPlayer.value.play();
-        } catch (err) {
-          console.error('flv播放失败');
-          console.log(err);
+          flvPlayer.value.on(mpegts.Events.ERROR, () => {
+            console.log('ERRORERROR');
+            if (retry.value < retryMax.value && !retrying.value) {
+              retrying.value = true;
+              destroyFlv();
+              setTimeout(() => {
+                console.log(
+                  '播放flv错误,重新加载,剩余次数:',
+                  retryMax.value - retry.value
+                );
+                retry.value += 1;
+                retrying.value = false;
+                main();
+              }, 1000);
+            }
+          });
+          const videoEl = createVideo({});
+          flvVideoEl.value = videoEl;
+          flvVideoEl.value.addEventListener('play', () => {
+            console.log('flv-play');
+          });
+          flvVideoEl.value.addEventListener('playing', () => {
+            console.log('flv-playing');
+            retry.value = 0;
+            setMuted(appStore.muted);
+            document.body.appendChild(videoEl);
+            resolve({
+              width: flvVideoEl.value?.videoWidth || 0,
+              height: flvVideoEl.value?.videoHeight || 0,
+            });
+          });
+          flvPlayer.value.attachMediaElement(flvVideoEl.value);
+          flvPlayer.value.load();
+          try {
+            console.log(`开始播放flv,muted:${appStore.muted}`);
+            flvPlayer.value.play();
+          } catch (err) {
+            console.error('flv播放失败');
+            console.log(err);
+          }
+        } else {
+          console.error('不支持flv');
         }
-      } else {
-        console.error('不支持flv');
       }
+      main();
     });
   }
 
@@ -107,6 +124,7 @@ export function useHlsPlay() {
   function destroyHls() {
     if (hlsPlayer.value) {
       hlsPlayer.value.dispose();
+      hlsPlayer.value = undefined;
     }
     hlsVideoEl.value?.remove();
   }
@@ -129,56 +147,41 @@ export function useHlsPlay() {
   );
 
   function startHlsPlay(data: { hlsurl: string }) {
-    console.log('startHlsPlay', data.hlsurl);
-    destroyHls();
-    const videoEl = createVideo({ muted: appStore.muted, autoplay: true });
-    hlsVideoEl.value = videoEl;
     return new Promise<{ width: number; height: number }>((resolve) => {
-      hlsPlayer.value = videoJs(
-        videoEl,
-        {
-          sources: [
-            {
-              src: data.hlsurl,
-              type: 'application/x-mpegURL',
-            },
-          ],
-        },
-        function () {
-          try {
-            console.log(`开始播放hls,muted:${appStore.muted}`);
-            hlsPlayer.value?.play();
-          } catch (err) {
-            console.error('hls播放失败');
-            console.log(err);
-          }
-          hlsPlayer.value?.on('play', () => {
-            console.log('hls-play');
-            // console.log(hlsPlayer.value?.videoHeight()); // 获取到的是0!
-          });
-          hlsPlayer.value?.on('playing', () => {
-            console.log('hls-playing');
-            setMuted(appStore.muted);
-            // console.log(hlsPlayer.value?.videoHeight()); // 获取到的是正确的!
-            const childNodes = hlsPlayer.value?.el().childNodes;
-            if (childNodes) {
-              childNodes.forEach((item) => {
-                if (item.nodeName.toLowerCase() === 'video') {
-                  // @ts-ignore
-                  hlsVideoEl.value = item;
-                }
-              });
+      function main() {
+        console.log('startHlsPlay', data.hlsurl);
+        destroyHls();
+        const videoEl = createVideo({ muted: appStore.muted, autoplay: true });
+        hlsVideoEl.value = videoEl;
+        hlsPlayer.value = videoJs(
+          videoEl,
+          {
+            sources: [
+              {
+                src: data.hlsurl,
+                type: 'application/x-mpegURL',
+              },
+            ],
+          },
+          function () {
+            try {
+              console.log(`开始播放hls,muted:${appStore.muted}`);
+              hlsPlayer.value?.play();
+            } catch (err) {
+              console.error('hls播放失败');
+              console.log(err);
             }
-            resolve({
-              width: hlsPlayer.value?.videoWidth() || 0,
-              height: hlsPlayer.value?.videoHeight() || 0,
-            });
-          });
-          hlsPlayer.value?.on('loadedmetadata', () => {
-            console.log('hls-loadedmetadata');
-          });
-        }
-      );
+          }
+        );
+        hlsPlayer.value?.on('play', () => {
+          console.log('hls-play');
+          // console.log(hlsPlayer.value?.videoHeight()); // 获取到的是0!
+        });
+        hlsPlayer.value?.on('loadedmetadata', () => {
+          console.log('hls-loadedmetadata');
+        });
+      }
+      main();
     });
   }
 

+ 61 - 38
src/hooks/use-pull.ts

@@ -1,29 +1,23 @@
 import mpegts from 'mpegts.js';
-import { Ref, onUnmounted, ref, watch } from 'vue';
+import { onUnmounted, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 
 import { useFlvPlay, useHlsPlay } from '@/hooks/use-play';
 import { useSrsWs } from '@/hooks/use-srs-ws';
-import { DanmuMsgTypeEnum, IDanmu, IMessage, liveTypeEnum } from '@/interface';
+import { DanmuMsgTypeEnum, IDanmu, IMessage, LiveTypeEnum } from '@/interface';
 import { WsMsgTypeEnum } from '@/network/webSocket';
 import { useAppStore } from '@/store/app';
 import { useNetworkStore } from '@/store/network';
 import { useUserStore } from '@/store/user';
 import { createVideo, videoToCanvas } from '@/utils';
 
-export function usePull({
-  remoteVideoRef,
-  liveType,
-}: {
-  remoteVideoRef: Ref<HTMLDivElement>;
-  liveType: liveTypeEnum;
-}) {
+export function usePull({ liveType }: { liveType: LiveTypeEnum }) {
   const route = useRoute();
   const userStore = useUserStore();
   const networkStore = useNetworkStore();
   const appStore = useAppStore();
   const roomId = ref(route.params.roomId as string);
-  const roomLiveType = ref<liveTypeEnum>(liveType);
+  const roomLiveType = ref<LiveTypeEnum>(liveType);
   const localStream = ref<MediaStream>();
   const danmuStr = ref('');
   const autoplayVal = ref(false);
@@ -41,7 +35,6 @@ export function usePull({
     isPull,
     mySocketId,
     initSrsWs,
-    handleStartLive,
     roomLiving,
     liveRoomInfo,
     anchorInfo,
@@ -49,8 +42,8 @@ export function usePull({
     damuList,
   } = useSrsWs();
   isPull.value = true;
-  const { flvPlayer, flvVideoEl, startFlvPlay } = useFlvPlay();
-  const { hlsVideoEl, startHlsPlay } = useHlsPlay();
+  const { flvPlayer, flvVideoEl, startFlvPlay, destroyFlv } = useFlvPlay();
+  const { hlsPlayer, hlsVideoEl, startHlsPlay, destroyHls } = useHlsPlay();
   const stopDrawingArr = ref<any[]>([]);
 
   onUnmounted(() => {
@@ -58,24 +51,35 @@ export function usePull({
   });
 
   function handleStopDrawing() {
+    destroyFlv();
+    destroyHls();
     stopDrawingArr.value.forEach((cb) => cb());
     stopDrawingArr.value = [];
   }
 
+  watch(hlsVideoEl, () => {
+    handleHlsPlay();
+  });
+
   async function handleHlsPlay(url?: string) {
     console.log('handleHlsPlay');
+
     handleStopDrawing();
     videoLoading.value = true;
-    const { width, height } = await startHlsPlay({
-      hlsurl: url || hlsurl.value,
-    });
-    const { canvas, stopDrawing } = videoToCanvas({
-      videoEl: hlsVideoEl.value!,
-      size: { width, height },
-    });
-    stopDrawingArr.value.push(stopDrawing);
-    remoteVideo.value.push(canvas);
-    videoLoading.value = false;
+    try {
+      const { width, height } = await startHlsPlay({
+        hlsurl: url || hlsurl.value,
+      });
+      const { canvas, stopDrawing } = videoToCanvas({
+        videoEl: hlsVideoEl.value!,
+        size: { width, height },
+      });
+      stopDrawingArr.value.push(stopDrawing);
+      remoteVideo.value.push(canvas);
+      videoLoading.value = false;
+    } catch (error) {
+      console.log('2532616', error);
+    }
   }
 
   async function handleFlvPlay() {
@@ -91,8 +95,10 @@ export function usePull({
     });
     stopDrawingArr.value.push(initCanvas.stopDrawing);
     remoteVideo.value.push(initCanvas.canvas);
+
     flvPlayer.value!.on(mpegts.Events.MEDIA_INFO, () => {
-      console.log('数据变了');
+      console.log('数据变了', flvVideoEl.value?.videoHeight);
+      document.body.appendChild(flvVideoEl.value);
       size.width = flvVideoEl.value!.videoWidth!;
       size.height = flvVideoEl.value!.videoHeight!;
     });
@@ -101,10 +107,10 @@ export function usePull({
 
   async function handlePlay() {
     console.warn('handlePlay');
-    if (roomLiveType.value === liveTypeEnum.srsFlvPull) {
+    if (roomLiveType.value === LiveTypeEnum.srsFlvPull) {
       if (!autoplayVal.value) return;
       await handleFlvPlay();
-    } else if (roomLiveType.value === liveTypeEnum.srsHlsPull) {
+    } else if (roomLiveType.value === LiveTypeEnum.srsHlsPull) {
       if (!autoplayVal.value) return;
       await handleHlsPlay();
     }
@@ -114,7 +120,7 @@ export function usePull({
     () => autoplayVal.value,
     (val) => {
       console.log('autoplayVal变了', val);
-      if (val && roomLiveType.value === liveTypeEnum.webrtcPull) {
+      if (val && roomLiveType.value === LiveTypeEnum.webrtcPull) {
         handlePlay();
       }
     }
@@ -124,21 +130,17 @@ export function usePull({
     () => roomLiving.value,
     (val) => {
       if (val) {
-        console.log(roomLiveType.value, '32323312');
-        if (roomLiveType.value === liveTypeEnum.webrtcPull) {
-          // handleStartLive({
-          //   type: LiveRoomTypeEnum.user_wertc,
-          //   receiver: '',
-          //   videoEl: document.createElement('video'),
-          // });
-        } else {
+        if (roomLiveType.value !== LiveTypeEnum.webrtcPull) {
           flvurl.value = liveRoomInfo.value?.flv_url!;
           hlsurl.value = liveRoomInfo.value?.hls_url!;
           handlePlay();
         }
+      } else {
+        closeRtc();
       }
     }
   );
+
   watch(
     () => appStore.muted,
     (newVal) => {
@@ -149,6 +151,26 @@ export function usePull({
     }
   );
 
+  watch(
+    () => networkStore.rtcMap,
+    (newVal) => {
+      if (liveType === LiveTypeEnum.webrtcPull) {
+        newVal.forEach((item) => {
+          videoLoading.value = false;
+          const { canvas } = videoToCanvas({
+            videoEl: item.videoEl,
+          });
+          videoElArr.value.push(item.videoEl);
+          remoteVideo.value.push(canvas);
+        });
+      }
+    },
+    {
+      deep: true,
+      immediate: true,
+    }
+  );
+
   watch(
     () => localStream.value,
     (val) => {
@@ -156,7 +178,7 @@ export function usePull({
         console.log('localStream变了');
         console.log('音频轨:', val?.getAudioTracks());
         console.log('视频轨:', val?.getVideoTracks());
-        if (roomLiveType.value === liveTypeEnum.webrtcPull) {
+        if (roomLiveType.value === LiveTypeEnum.webrtcPull) {
           videoElArr.value.forEach((dom) => {
             dom.remove();
           });
@@ -177,7 +199,7 @@ export function usePull({
             videoElArr.value.push(video);
           });
           videoLoading.value = false;
-        } else if (roomLiveType.value === liveTypeEnum.srsWebrtcPull) {
+        } else if (roomLiveType.value === LiveTypeEnum.srsWebrtcPull) {
           videoElArr.value.forEach((dom) => {
             dom.remove();
           });
@@ -186,7 +208,6 @@ export function usePull({
             const video = createVideo({});
             video.setAttribute('track-id', track.id);
             video.srcObject = new MediaStream([track]);
-            // document.body.appendChild(video);
             remoteVideo.value.push(video);
             videoElArr.value.push(video);
           });
@@ -241,6 +262,7 @@ export function usePull({
   function closeRtc() {
     networkStore.rtcMap.forEach((rtc) => {
       rtc.close();
+      networkStore.removeRtc(rtc.roomId);
     });
   }
 
@@ -291,6 +313,7 @@ export function usePull({
   }
 
   return {
+    handleStopDrawing,
     initPull,
     closeWs,
     closeRtc,

+ 5 - 4
src/hooks/use-push.ts

@@ -27,12 +27,12 @@ export function usePush() {
   const roomId = ref('');
   const roomName = ref('');
   const danmuStr = ref('');
-  const isLiving = ref(false);
   const liveRoomInfo = ref<ILiveRoom>();
   const localStream = ref<MediaStream>();
   const videoElArr = ref<HTMLVideoElement[]>([]);
 
   const {
+    roomLiving,
     isPull,
     initSrsWs,
     handleStartLive,
@@ -251,6 +251,7 @@ export function usePush() {
   function closeRtc() {
     networkStore.rtcMap.forEach((rtc) => {
       rtc.close();
+      networkStore.removeRtc(rtc.roomId);
     });
   }
 
@@ -300,7 +301,7 @@ export function usePush() {
       return;
     }
 
-    isLiving.value = true;
+    roomLiving.value = true;
     const el = appStore.allTrack.find((item) => {
       if (item.video === 1) {
         return true;
@@ -327,7 +328,7 @@ export function usePush() {
 
   /** 结束直播 */
   function endLive() {
-    isLiving.value = false;
+    roomLiving.value = false;
     localStream.value = undefined;
     const instance = networkStore.wsMap.get(roomId.value);
     if (instance) {
@@ -401,7 +402,7 @@ export function usePush() {
     lastCoverImg,
     localStream,
     canvasVideoStream,
-    isLiving,
+    roomLiving,
     currentResolutionRatio,
     currentMaxBitrate,
     currentMaxFramerate,

+ 0 - 495
src/hooks/use-rtc-ws.ts

@@ -1,495 +0,0 @@
-import { getRandomString } from 'billd-utils';
-import { computed, onUnmounted, ref, watch } from 'vue';
-
-import { fetchRtcV1Publish } from '@/api/srs';
-import { WEBSOCKET_URL } from '@/constant';
-import {
-  DanmuMsgTypeEnum,
-  IDanmu,
-  ILiveRoom,
-  ILiveUser,
-  IUser,
-  LiveRoomTypeEnum,
-} from '@/interface';
-import {
-  WSGetRoomAllUserType,
-  WsGetLiveUserType,
-  WsHeartbeatType,
-  WsJoinType,
-  WsLeavedType,
-  WsMessageType,
-  WsOtherJoinType,
-  WsRoomLivingType,
-  WsStartLiveType,
-  WsUpdateJoinInfoType,
-} from '@/interface-ws';
-import { WebRTCClass } from '@/network/webRTC';
-import {
-  WebSocketClass,
-  WsConnectStatusEnum,
-  WsMsgTypeEnum,
-  prettierReceiveWsMsg,
-} from '@/network/webSocket';
-import { AppRootState, useAppStore } from '@/store/app';
-import { useNetworkStore } from '@/store/network';
-import { useUserStore } from '@/store/user';
-import { createVideo } from '@/utils';
-
-import { useRTCParams } from './use-rtc-params';
-
-export const useRtcWs = () => {
-  const appStore = useAppStore();
-  const userStore = useUserStore();
-  const networkStore = useNetworkStore();
-  const loopHeartbeatTimer = ref();
-  const liveUserList = ref<ILiveUser[]>([]);
-  const roomId = ref('');
-  const roomLiving = ref(false);
-  const liveRoomInfo = ref<ILiveRoom>();
-  const anchorInfo = ref<IUser>();
-  const isAnchor = ref(false);
-  const localStream = ref<MediaStream>();
-  const canvasVideoStream = ref<MediaStream>();
-  const lastCoverImg = ref('');
-  const { maxBitrate, maxFramerate, resolutionRatio } = useRTCParams();
-  const currentMaxBitrate = ref(maxBitrate.value[2].value);
-  const currentResolutionRatio = ref(resolutionRatio.value[3].value);
-  const currentMaxFramerate = ref(maxFramerate.value[2].value);
-
-  const damuList = ref<IDanmu[]>([]);
-
-  watch(
-    () => appStore.allTrack,
-    (newTrack, oldTrack) => {
-      console.log('appStore.allTrack变了', newTrack, oldTrack);
-      const mixedStream = new MediaStream();
-      newTrack.forEach((item) => {
-        if (item.track) {
-          mixedStream.addTrack(item.track);
-        }
-      });
-      console.log('新的allTrack音频轨', mixedStream.getAudioTracks());
-      console.log('新的allTrack视频轨', mixedStream.getVideoTracks());
-      console.log('旧的allTrack音频轨', localStream.value?.getAudioTracks());
-      console.log('旧的allTrack视频轨', localStream.value?.getVideoTracks());
-      localStream.value = mixedStream;
-    },
-    { deep: true }
-  );
-
-  onUnmounted(() => {
-    clearInterval(loopHeartbeatTimer.value);
-  });
-
-  watch(
-    () => currentResolutionRatio.value,
-    (newVal) => {
-      if (canvasVideoStream.value) {
-        canvasVideoStream.value.getVideoTracks().forEach((track) => {
-          track.applyConstraints({
-            frameRate: { max: currentMaxFramerate.value },
-            height: newVal,
-          });
-        });
-      } else {
-        appStore.allTrack.forEach((info) => {
-          info.track?.applyConstraints({
-            frameRate: { max: currentMaxFramerate.value },
-            height: newVal,
-          });
-        });
-      }
-
-      networkStore.rtcMap.forEach(async (rtc) => {
-        const res = await rtc.setResolutionRatio(newVal);
-        if (res === 1) {
-          window.$message.success('切换分辨率成功!');
-        } else {
-          window.$message.success('切换分辨率失败!');
-        }
-      });
-    }
-  );
-
-  watch(
-    () => currentMaxFramerate.value,
-    (newVal) => {
-      console.log(currentMaxFramerate.value, 'currentMaxFramerate.value');
-      if (canvasVideoStream.value) {
-        canvasVideoStream.value.getVideoTracks().forEach((track) => {
-          track.applyConstraints({
-            frameRate: { max: newVal },
-            height: currentResolutionRatio.value,
-          });
-        });
-      } else {
-        appStore.allTrack.forEach((info) => {
-          info.track?.applyConstraints({
-            frameRate: { max: newVal },
-            height: currentResolutionRatio.value,
-          });
-        });
-      }
-
-      networkStore.rtcMap.forEach(async (rtc) => {
-        const res = await rtc.setMaxFramerate(newVal);
-        if (res === 1) {
-          window.$message.success('切换帧率成功!');
-        } else {
-          window.$message.success('切换帧率失败!');
-        }
-      });
-    }
-  );
-
-  watch(
-    () => currentMaxBitrate.value,
-    (newVal) => {
-      networkStore.rtcMap.forEach(async (rtc) => {
-        const res = await rtc.setMaxBitrate(newVal);
-        if (res === 1) {
-          window.$message.success('切换码率成功!');
-        } else {
-          window.$message.success('切换码率失败!');
-        }
-      });
-    }
-  );
-
-  function addTrack(addTrackInfo: { track; stream }) {
-    if (isAnchor.value) {
-      networkStore.rtcMap.forEach((rtc) => {
-        const sender = rtc.peerConnection
-          ?.getSenders()
-          .find((sender) => sender.track?.id === addTrackInfo.track?.id);
-        if (!sender) {
-          console.log('pc添加track-开播后中途添加', addTrackInfo.track?.id);
-          rtc.peerConnection
-            ?.getSenders()
-            ?.find((sender) => sender.track?.kind === 'audio')
-            ?.replaceTrack(canvasVideoStream.value!.getAudioTracks()[0]);
-          const vel = createVideo({});
-          vel.srcObject = canvasVideoStream.value!;
-        }
-      });
-    }
-    const mixedStream = new MediaStream();
-    appStore.allTrack.forEach((item) => {
-      if (item.track) {
-        mixedStream.addTrack(item.track);
-      }
-    });
-    console.log('addTrack后结果的音频轨', mixedStream.getAudioTracks());
-    console.log('addTrack后结果的视频轨', mixedStream.getVideoTracks());
-    localStream.value = mixedStream;
-  }
-
-  function delTrack(delTrackInfo: AppRootState['allTrack'][0]) {
-    if (isAnchor.value) {
-      networkStore.rtcMap.forEach((rtc) => {
-        const sender = rtc.peerConnection
-          ?.getSenders()
-          .find((sender) => sender.track?.id === delTrackInfo.track?.id);
-        if (sender) {
-          console.log('删除track', delTrackInfo, sender);
-          rtc.peerConnection?.removeTrack(sender);
-        }
-      });
-    }
-    const mixedStream = new MediaStream();
-    appStore.allTrack.forEach((item) => {
-      if (item.track) {
-        mixedStream.addTrack(item.track);
-      }
-    });
-    console.log('delTrack后结果的音频轨', mixedStream.getAudioTracks());
-    console.log('delTrack后结果的视频轨', mixedStream.getVideoTracks());
-    localStream.value = mixedStream;
-  }
-
-  const mySocketId = computed(() => {
-    return networkStore.wsMap.get(roomId.value)?.socketIo?.id || '-1';
-  });
-
-  function handleHeartbeat(socketId: string) {
-    loopHeartbeatTimer.value = setInterval(() => {
-      const ws = networkStore.wsMap.get(roomId.value);
-      if (!ws) return;
-      ws.send<WsHeartbeatType['data']>({
-        msgType: WsMsgTypeEnum.heartbeat,
-        data: {
-          socket_id: socketId,
-        },
-      });
-    }, 1000 * 5);
-  }
-
-  async function sendOffer({ receiver }: { receiver: string }) {
-    console.log('开始sendOffer');
-    const ws = networkStore.wsMap.get(roomId.value);
-    if (!ws) return;
-    const rtc = networkStore.getRtcMap(`${roomId.value}___${receiver}`);
-    if (!rtc) return;
-    const sdp = await rtc.createOffer();
-    await rtc.setLocalDescription(sdp!);
-
-    const myLiveRoom = userStore.userInfo!.live_rooms![0];
-    const res = await fetchRtcV1Publish({
-      api: `/rtc/v1/publish/`,
-      clientip: null,
-      sdp: sdp!.sdp!,
-      streamurl: `${myLiveRoom.rtmp_url!}?token=${myLiveRoom.key!}&type=${
-        LiveRoomTypeEnum.user_srs
-      }`,
-      tid: getRandomString(10),
-    });
-    networkStore.wsMap.get(roomId.value)?.send<WsUpdateJoinInfoType['data']>({
-      msgType: WsMsgTypeEnum.updateJoinInfo,
-      data: {
-        live_room_id: Number(roomId.value),
-        track: {
-          audio: 1,
-          video: 1,
-        },
-      },
-    });
-    if (res.data.code !== 0) {
-      console.error('/rtc/v1/publish/拿不到sdp');
-      window.$message.error('/rtc/v1/publish/拿不到sdp');
-      return;
-    }
-    await rtc.setRemoteDescription(
-      new RTCSessionDescription({ type: 'answer', sdp: res.data.sdp })
-    );
-  }
-
-  function handleStartLive({ coverImg, name }) {
-    networkStore.wsMap.get(roomId.value)?.send<WsStartLiveType['data']>({
-      msgType: WsMsgTypeEnum.startLive,
-      data: {
-        cover_img: coverImg,
-        name,
-        type: LiveRoomTypeEnum.user_srs,
-      },
-    });
-    startNewSrsWebRtc({
-      videoEl: document.createElement('video'),
-      receiver: 'srs',
-    });
-  }
-
-  function sendJoin() {
-    const instance = networkStore.wsMap.get(roomId.value);
-    if (!instance) return;
-    instance.send<WsJoinType['data']>({
-      msgType: WsMsgTypeEnum.join,
-      data: {
-        socket_id: mySocketId.value,
-        live_room: {
-          id: Number(roomId.value),
-        },
-        user_info: userStore.userInfo,
-      },
-    });
-  }
-
-  /** 原生的webrtc时,receiver必传 */
-  function startNewSrsWebRtc({
-    receiver,
-    videoEl,
-  }: {
-    receiver: string;
-    videoEl: HTMLVideoElement;
-  }) {
-    console.warn('SRS开始new WebRTCClass', `${roomId.value}___${receiver!}`);
-    const rtc = new WebRTCClass({
-      maxBitrate: currentMaxBitrate.value,
-      maxFramerate: currentMaxFramerate.value,
-      resolutionRatio: currentResolutionRatio.value,
-      roomId: `${roomId.value}___${receiver!}`,
-      videoEl,
-      isSRS: true,
-      receiver,
-    });
-    if (canvasVideoStream.value) {
-      localStream.value = canvasVideoStream.value;
-      rtc.localStream = canvasVideoStream.value;
-      canvasVideoStream.value.getTracks().forEach((track) => {
-        console.log('pc添加track-srs', track.kind, track.id);
-        rtc.peerConnection?.addTrack(track, localStream.value!);
-      });
-    }
-
-    sendOffer({
-      receiver,
-    });
-  }
-
-  function initReceive() {
-    const ws = networkStore.wsMap.get(roomId.value);
-    if (!ws?.socketIo) return;
-    // websocket连接成功
-    ws.socketIo.on(WsConnectStatusEnum.connect, () => {
-      prettierReceiveWsMsg(WsConnectStatusEnum.connect, ws.socketIo);
-      handleHeartbeat(ws.socketIo!.id);
-      if (!ws) return;
-      ws.status = WsConnectStatusEnum.connect;
-      ws.update();
-      sendJoin();
-    });
-
-    // websocket连接断开
-    ws.socketIo.on(WsConnectStatusEnum.disconnect, () => {
-      prettierReceiveWsMsg(WsConnectStatusEnum.disconnect, ws);
-      if (!ws) return;
-      ws.status = WsConnectStatusEnum.disconnect;
-      ws.update();
-    });
-
-    // 主播正在直播
-    ws.socketIo.on(WsMsgTypeEnum.roomLiving, (data: WsRoomLivingType) => {
-      prettierReceiveWsMsg(WsMsgTypeEnum.roomLiving, data);
-      roomLiving.value = true;
-    });
-
-    // 主播不在直播
-    ws.socketIo.on(WsMsgTypeEnum.roomNoLive, (data) => {
-      prettierReceiveWsMsg(WsMsgTypeEnum.roomNoLive, data);
-      roomLiving.value = false;
-    });
-
-    // 当前所有在线用户
-    ws.socketIo.on(
-      WsMsgTypeEnum.liveUser,
-      (data: WSGetRoomAllUserType['data']) => {
-        prettierReceiveWsMsg(WsMsgTypeEnum.liveUser, data);
-        const res = data.liveUser.map((item) => {
-          return {
-            id: item.id,
-            // userInfo: item.id,
-          };
-        });
-        liveUserList.value = res;
-      }
-    );
-
-    // 收到用户发送消息
-    ws.socketIo.on(WsMsgTypeEnum.message, (data: WsMessageType) => {
-      prettierReceiveWsMsg(WsMsgTypeEnum.message, data);
-      damuList.value.push({
-        socket_id: data.socket_id,
-        msgType: DanmuMsgTypeEnum.danmu,
-        msg: data.data.msg,
-        userInfo: data.user_info,
-      });
-    });
-
-    // 用户加入房间完成
-    ws.socketIo.on(WsMsgTypeEnum.joined, (data: WsJoinType['data']) => {
-      prettierReceiveWsMsg(WsMsgTypeEnum.joined, data);
-      liveUserList.value.push({
-        id: data.socket_id,
-        userInfo: data.user_info,
-      });
-      liveRoomInfo.value = data.live_room;
-      anchorInfo.value = data.anchor_info;
-      ws.send<WsGetLiveUserType['data']>({
-        msgType: WsMsgTypeEnum.getLiveUser,
-        data: {
-          live_room_id: data.live_room.id!,
-        },
-      });
-    });
-
-    // 其他用户加入房间
-    ws.socketIo.on(WsMsgTypeEnum.otherJoin, (data: WsOtherJoinType['data']) => {
-      prettierReceiveWsMsg(WsMsgTypeEnum.otherJoin, data);
-      liveUserList.value.push({
-        id: data.join_socket_id,
-        userInfo: data.join_user_info,
-      });
-      const danmu: IDanmu = {
-        msgType: DanmuMsgTypeEnum.otherJoin,
-        socket_id: data.join_socket_id,
-        userInfo: data.join_user_info,
-        msg: '',
-      };
-      damuList.value.push(danmu);
-      ws.send<WsGetLiveUserType['data']>({
-        msgType: WsMsgTypeEnum.getLiveUser,
-        data: {
-          live_room_id: data.live_room.id!,
-        },
-      });
-    });
-
-    // 用户离开房间
-    ws.socketIo.on(WsMsgTypeEnum.leave, (data) => {
-      prettierReceiveWsMsg(WsMsgTypeEnum.leave, data);
-    });
-
-    // 用户离开房间完成
-    ws.socketIo.on(WsMsgTypeEnum.leaved, (data: WsLeavedType['data']) => {
-      prettierReceiveWsMsg(WsMsgTypeEnum.leaved, data);
-      networkStore.rtcMap
-        .get(`${roomId.value}___${data.socket_id as string}`)
-        ?.close();
-      networkStore.removeRtc(`${roomId.value}___${data.socket_id as string}`);
-      const res = liveUserList.value.filter(
-        (item) => item.id !== data.socket_id
-      );
-      liveUserList.value = res;
-      damuList.value.push({
-        socket_id: data.socket_id,
-        msgType: DanmuMsgTypeEnum.userLeaved,
-        userInfo: data.user_info,
-        msg: '',
-      });
-    });
-  }
-
-  function initSrsWs(data: {
-    isAnchor: boolean;
-    roomId: string;
-    currentResolutionRatio?: number;
-    currentMaxFramerate?: number;
-    currentMaxBitrate?: number;
-  }) {
-    roomId.value = data.roomId;
-    isAnchor.value = data.isAnchor;
-    if (data.currentMaxBitrate) {
-      currentMaxBitrate.value = data.currentMaxBitrate;
-    }
-    if (data.currentMaxFramerate) {
-      currentMaxFramerate.value = data.currentMaxFramerate;
-    }
-    if (data.currentResolutionRatio) {
-      currentResolutionRatio.value = data.currentResolutionRatio;
-    }
-    new WebSocketClass({
-      roomId: roomId.value,
-      url: WEBSOCKET_URL,
-      isAnchor: data.isAnchor,
-    });
-    initReceive();
-  }
-
-  return {
-    initSrsWs,
-    addTrack,
-    delTrack,
-    handleStartLive,
-    mySocketId,
-    canvasVideoStream,
-    lastCoverImg,
-    roomLiving,
-    liveRoomInfo,
-    anchorInfo,
-    localStream,
-    liveUserList,
-    damuList,
-    currentMaxFramerate,
-    currentMaxBitrate,
-    currentResolutionRatio,
-  };
-};

+ 26 - 47
src/hooks/use-srs-ws.ts

@@ -1,5 +1,5 @@
 import { getRandomString } from 'billd-utils';
-import { computed, onUnmounted, ref, watch } from 'vue';
+import { computed, onUnmounted, ref } from 'vue';
 
 import { fetchRtcV1Publish } from '@/api/srs';
 import { WEBSOCKET_URL } from '@/constant';
@@ -33,12 +33,15 @@ import {
   WsMsgTypeEnum,
   prettierReceiveWsMsg,
 } from '@/network/webSocket';
+import { useAppStore } from '@/store/app';
 import { useNetworkStore } from '@/store/network';
 import { useUserStore } from '@/store/user';
+import { createVideo } from '@/utils';
 
 import { useRTCParams } from './use-rtc-params';
 
 export const useSrsWs = () => {
+  const appStore = useAppStore();
   const userStore = useUserStore();
   const networkStore = useNetworkStore();
   const { maxBitrate, maxFramerate, resolutionRatio } = useRTCParams();
@@ -124,38 +127,6 @@ export const useSrsWs = () => {
     );
   }
 
-  watch(liveUserList, () => {
-    // if (!isPull.value) return;
-    // console.log('>>>>>');
-    // liveUserList.value.forEach(async (item) => {
-    //   console.log(item);
-    //   const receiver = item.id;
-    //   if (receiver === mySocketId.value) return;
-    //   console.log(receiver, 'ppdpsd');
-    //   const rtc = new WebRTCClass({
-    //     maxBitrate: currentMaxBitrate.value,
-    //     maxFramerate: currentMaxFramerate.value,
-    //     resolutionRatio: currentResolutionRatio.value,
-    //     roomId: `${roomId.value}___${receiver!}`,
-    //     videoEl: document.createElement('video'),
-    //     isSRS: false,
-    //     receiver,
-    //   });
-    //   const ws = networkStore.wsMap.get(roomId.value)!;
-    //   const offer = await rtc.createOffer();
-    //   await rtc.setLocalDescription(offer!);
-    //   ws.send<WsOfferType['data']>({
-    //     msgType: WsMsgTypeEnum.offer,
-    //     data: {
-    //       sdp: offer,
-    //       live_room_id: Number(roomId.value),
-    //       sender: mySocketId.value,
-    //       receiver,
-    //     },
-    //   });
-    // });
-  });
-
   function handleStartLive({
     coverImg,
     name,
@@ -181,7 +152,7 @@ export const useSrsWs = () => {
       return;
     }
     startNewWebRtc({
-      videoEl: document.createElement('video'),
+      videoEl: createVideo({}),
       receiver,
       type,
     });
@@ -212,7 +183,7 @@ export const useSrsWs = () => {
     videoEl: HTMLVideoElement;
     type: LiveRoomTypeEnum;
   }) {
-    console.warn('开始new WebRTCClass', `${roomId.value}___${receiver!}`);
+    console.warn('33开始new WebRTCClass', `${roomId.value}___${receiver!}`);
     new WebRTCClass({
       maxBitrate: currentMaxBitrate.value,
       maxFramerate: currentMaxFramerate.value,
@@ -253,21 +224,20 @@ export const useSrsWs = () => {
       console.log('收到offer', data);
       if (data.receiver === mySocketId.value) {
         console.warn('是发给我的offer');
+        console.warn(
+          '11开始new WebRTCClass',
+          `${roomId.value}___${data.sender}`
+        );
+        const videoEl = createVideo({ appendChild: true });
         const rtc = new WebRTCClass({
           maxBitrate: currentMaxBitrate.value,
           maxFramerate: currentMaxFramerate.value,
           resolutionRatio: currentResolutionRatio.value,
-          roomId: `${roomId.value}___${data.receiver!}`,
-          videoEl: document.createElement('video'),
+          roomId: `${roomId.value}___${data.sender}`,
+          videoEl,
           isSRS: true,
           receiver: data.receiver,
         });
-        canvasVideoStream.value?.getTracks().forEach((track) => {
-          if (rtc && canvasVideoStream.value) {
-            console.log('插入track', track);
-            rtc.peerConnection?.addTrack(track, canvasVideoStream.value);
-          }
-        });
         await rtc.setRemoteDescription(data.sdp);
         const answer = await rtc.createAnswer();
         if (answer) {
@@ -303,9 +273,7 @@ export const useSrsWs = () => {
       console.log('收到candidate', data);
       if (data.receiver === mySocketId.value) {
         console.warn('是发给我的candidate');
-        const rtc = networkStore.getRtcMap(
-          `${roomId.value}___${data.receiver}`
-        )!;
+        const rtc = networkStore.getRtcMap(`${roomId.value}___${data.sender}`)!;
         rtc.addIceCandidate(data.candidate);
       } else {
         console.error('不是发给我的candidate');
@@ -388,6 +356,7 @@ export const useSrsWs = () => {
         },
       });
       if (!isPull.value) {
+        if (!roomLiving.value) return;
         console.log('>>>>>');
         liveUserList.value.forEach(async (item) => {
           console.log(item);
@@ -398,16 +367,26 @@ export const useSrsWs = () => {
           )
             return;
           console.log(receiver, 'ppdpsd');
+          console.warn(
+            '22开始new WebRTCClass',
+            `${roomId.value}___${receiver!}`
+          );
           const rtc = new WebRTCClass({
             maxBitrate: currentMaxBitrate.value,
             maxFramerate: currentMaxFramerate.value,
             resolutionRatio: currentResolutionRatio.value,
             roomId: `${roomId.value}___${receiver!}`,
-            videoEl: document.createElement('video'),
+            videoEl: createVideo({}),
             isSRS: false,
             receiver,
           });
           networkStore.updateRtcMap(`${roomId.value}___${receiver!}`, rtc);
+          canvasVideoStream.value?.getTracks().forEach((track) => {
+            if (rtc && canvasVideoStream.value) {
+              console.log('插入track', track);
+              rtc.peerConnection?.addTrack(track, canvasVideoStream.value);
+            }
+          });
           const ws = networkStore.wsMap.get(roomId.value)!;
           const offer = await rtc.createOffer();
           await rtc.setLocalDescription(offer!);

+ 1 - 1
src/interface.ts

@@ -177,7 +177,7 @@ export interface IUserLiveRoom {
   deleted_at?: string;
 }
 
-export enum liveTypeEnum {
+export enum LiveTypeEnum {
   webrtcPull = 'webrtcPull',
   srsWebrtcPull = 'srsWebrtcPull',
   srsFlvPull = 'srsFlvPull',

+ 7 - 7
src/layout/pc/head/index.vue

@@ -195,13 +195,13 @@
           <div class="list">
             <a
               class="item"
-              @click.prevent="handleStartLive(liveTypeEnum.srsPush)"
+              @click.prevent="handleStartLive(LiveTypeEnum.srsPush)"
             >
               <div class="txt">srs开播</div>
             </a>
             <a
               class="item"
-              @click.prevent="handleStartLive(liveTypeEnum.webrtcPush)"
+              @click.prevent="handleStartLive(LiveTypeEnum.webrtcPush)"
             >
               <div class="txt">webrtc开播</div>
             </a>
@@ -256,9 +256,9 @@ import { fetchAreaList } from '@/api/area';
 import Dropdown from '@/components/Dropdown/index.vue';
 import VPIconChevronDown from '@/components/icons/VPIconChevronDown.vue';
 import VPIconExternalLink from '@/components/icons/VPIconExternalLink.vue';
-import { APIFOX_URL } from '@/constant';
+import { APIFOX_URL, bilibiliCollectiondetail } from '@/constant';
 import { loginTip, useQQLogin } from '@/hooks/use-login';
-import { IArea, liveTypeEnum } from '@/interface';
+import { IArea, LiveTypeEnum } from '@/interface';
 import { routerName } from '@/router';
 import { useUserStore } from '@/store/user';
 
@@ -289,7 +289,7 @@ const about = ref([
   },
   {
     label: 'b站视频',
-    url: 'https://space.bilibili.com/381307133/channel/collectiondetail?sid=1458070&ctype=0',
+    url: bilibiliCollectiondetail,
   },
 ]);
 const resource = ref([
@@ -361,11 +361,11 @@ function quickStart() {
   window.$message.info('敬请期待!');
 }
 
-function handleStartLive(key: liveTypeEnum) {
+function handleStartLive(key: LiveTypeEnum) {
   if (!loginTip()) {
     return;
   }
-  // if (key === liveTypeEnum.canvasPush) {
+  // if (key === LiveTypeEnum.canvasPush) {
   const url = router.resolve({
     name: routerName.push,
     query: { liveType: key },

+ 1 - 2
src/network/webRTC.ts

@@ -8,8 +8,6 @@ import { useNetworkStore } from '@/store/network';
 
 import { WsMsgTypeEnum } from './webSocket';
 
-export const audioElArr: HTMLVideoElement[] = [];
-
 export class WebRTCClass {
   roomId = '-1';
   receiver = '';
@@ -357,6 +355,7 @@ export class WebRTCClass {
       console.log('track事件的视频轨', event.streams[0].getVideoTracks());
       console.log('track事件的音频轨', event.streams[0].getAudioTracks());
       this.addTrack(event.streams[0], true);
+      this.videoEl.srcObject = event.streams[0];
     });
   };
 

+ 28 - 10
src/utils/index.ts

@@ -41,11 +41,11 @@ export function formatDownTime(endTime: number, startTime: number) {
     }
   }
   if (d > 0) {
-    return `${d}:${h}:${m}:${s}.${ms}`;
+    return `${d}天${h}时${m}分${s}秒${ms}毫秒`;
   } else if (h > 0) {
-    return `${h}:${m}:${s}.${ms}`;
+    return `${h}时${m}分${s}秒${ms}毫秒`;
   } else {
-    return `${m}:${s}.${ms}`;
+    return `${m}分${s}秒${ms}毫秒`;
   }
 }
 
@@ -180,12 +180,17 @@ export const getRandomEnglishString = (length: number): string => {
   return res;
 };
 
-export const createVideo = ({ muted = true, autoplay = true }) => {
+export const createVideo = ({
+  muted = true,
+  autoplay = true,
+  appendChild = false,
+}) => {
   const videoEl = document.createElement('video');
   videoEl.autoplay = autoplay;
   videoEl.muted = muted;
   videoEl.playsInline = true;
   videoEl.loop = true;
+  videoEl.controls = true;
   videoEl.setAttribute('webkit-playsinline', 'true');
   videoEl.setAttribute('x5-video-player-type', 'h5');
   videoEl.setAttribute('x5-video-player-fullscreen', 'true');
@@ -193,9 +198,16 @@ export const createVideo = ({ muted = true, autoplay = true }) => {
   videoEl.oncontextmenu = (e) => {
     e.preventDefault();
   };
-  // if (NODE_ENV === 'development') {
-  videoEl.controls = true;
-  // }
+  if (appendChild) {
+    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);
+  }
   return videoEl;
 };
 
@@ -210,8 +222,7 @@ export function videoToCanvas(data: {
   const canvas = document.createElement('canvas');
 
   const ctx = canvas.getContext('2d')!;
-
-  let timer;
+  let timer = -1;
   function drawCanvas() {
     if (data.size) {
       const { width, height } = data.size;
@@ -233,14 +244,21 @@ export function videoToCanvas(data: {
         // console.log('没有size', width, height, performance.now());
       }
     }
+
     timer = requestAnimationFrame(drawCanvas);
   }
 
   function stopDrawing() {
+    // if (timer !== -1) {
+    //   workerTimers.clearInterval(timer);
+    // }
     cancelAnimationFrame(timer);
   }
 
-  // document.body.appendChild(videoEl);
+  // const delay = 1000 / 60; // 16.666666666666668
+  // timer = workerTimers.setInterval(() => {
+  //   drawCanvas();
+  // }, delay);
 
   drawCanvas();
 

+ 2 - 2
src/views/account/index.vue

@@ -71,7 +71,7 @@ import { useRouter } from 'vue-router';
 import { fetchUpdateLiveRoomKey } from '@/api/liveRoom';
 import { fetchUserInfo } from '@/api/user';
 import { loginTip } from '@/hooks/use-login';
-import { IUser, LiveRoomTypeEnum, liveTypeEnum } from '@/interface';
+import { IUser, LiveRoomTypeEnum, LiveTypeEnum } from '@/interface';
 import { routerName } from '@/router';
 
 const newRtmpUrl = ref();
@@ -100,7 +100,7 @@ function openLiveRoom() {
   }
   const url = router.resolve({
     name: routerName.push,
-    query: { liveType: liveTypeEnum.srsWebrtcPull },
+    query: { liveType: LiveTypeEnum.srsWebrtcPull },
   });
   openToTarget(url.href);
 }

+ 2 - 2
src/views/area/index.vue

@@ -43,7 +43,7 @@ import { onMounted, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 
 import { fetchLiveRoomList } from '@/api/area';
-import { ILiveRoom, liveTypeEnum } from '@/interface';
+import { ILiveRoom, LiveTypeEnum } from '@/interface';
 import router, { routerName } from '@/router';
 
 const liveRoomList = ref<ILiveRoom[]>([]);
@@ -69,7 +69,7 @@ function goRoom(item: ILiveRoom) {
     name: routerName.pull,
     params: { roomId: item.id },
     query: {
-      liveType: liveTypeEnum.srsHlsPull,
+      liveType: LiveTypeEnum.srsHlsPull,
     },
   });
 }

+ 121 - 8
src/views/group/index.vue

@@ -1,11 +1,107 @@
 <template>
   <div class="group-wrap">
-    <h1>微信交流群 & 我的微信</h1>
-    <img
-      src="@/assets/img/wechat-group.webp"
-      alt=""
-      class="wechat-group"
-    />
+    <h1 class="title">📢 加群注意</h1>
+    <h3 class="title">
+      因为<b class="red"> 混子 </b
+      >太多,为保证群质量,因此设置了一个小小的加群门槛
+    </h3>
+    <div class="content">
+      <p class="red">进群硬性门槛</p>
+      <p>
+        1. 本地运行billd-live项目的前台:
+        <span
+          class="link"
+          @click="openToTarget('https://github.com/galaxy-s10/billd-live')"
+        >
+          https://github.com/galaxy-s10/billd-live
+        </span>
+      </p>
+      <p>
+        2. 本地运行billd-live项目的后台:
+        <span
+          class="link"
+          @click="
+            openToTarget('https://github.com/galaxy-s10/billd-live-admin')
+          "
+        >
+          https://github.com/galaxy-s10/billd-live-admin
+        </span>
+      </p>
+      <p>
+        3. 本地运行billd-live项目的后端:
+        <span
+          class="link"
+          @click="
+            openToTarget('https://github.com/galaxy-s10/billd-live-server')
+          "
+        >
+          https://github.com/galaxy-s10/billd-live-server
+        </span>
+      </p>
+      <p>
+        4. 没有免费的资料,所有免费的都在
+        <span
+          class="link"
+          @click="openToTarget(bilibiliCollectiondetail)"
+        >
+          b站视频合集
+        </span>
+        里,如果你是想进群看看有没有其他免费的资料的话,可以不用进了。
+      </p>
+      <p>
+        5.
+        如果你想进群,那就先运行起来项目,不要求正常的运行起来,但起码要运行。然后截图发我确认。
+        微信群的初衷是希望让对billd-live感兴趣且热衷开源的人有个交流的地方,
+        如果连运行都懒得运行,那么很抱歉,道不同不相为谋。
+      </p>
+    </div>
+    <br />
+    <br />
+    <div class="content">
+      <p>如果你懂技术,建议:</p>
+      <div>1. 直接提PR、Issue即可,多为开源做贡献。</div>
+    </div>
+    <br />
+    <br />
+    <div class="content">
+      <div>如果你不太懂技术,建议:</div>
+      <p>
+        1. 免费:看我的b站视频合集:<span
+          class="link"
+          @click="openToTarget(bilibiliCollectiondetail)"
+        >
+          从零搭建迷你版b站web直播间
+        </span>
+      </p>
+      <p>
+        2. 付费:考虑billd-live付费课:
+        <span
+          class="link"
+          @click="openToTarget('https://www.hsslive.cn/article/151')"
+        >
+          https://www.hsslive.cn/article/151
+        </span>
+      </p>
+      <div>3. 闲聊勿扰。</div>
+    </div>
+    <br />
+    <br />
+    <div class="content">
+      <div>如果你不懂技术,建议:</div>
+      <p>
+        1. 你想要私有化部署的,参考:
+        <span
+          class="link"
+          @click="
+            openToTarget('https://live.hsslive.cn/privatizationDeployment')
+          "
+        >
+          https://live.hsslive.cn/privatizationDeployment
+        </span>
+      </p>
+      <p>2. 闲聊勿扰。</p>
+    </div>
+
     <img
       src="@/assets/img/my-wechat.webp"
       alt=""
@@ -14,11 +110,28 @@
   </div>
 </template>
 
-<script lang="ts" setup></script>
+<script lang="ts" setup>
+import { openToTarget } from 'billd-utils';
+
+import { bilibiliCollectiondetail } from '@/constant';
+</script>
 
 <style lang="scss" scoped>
 .group-wrap {
-  text-align: center;
+  padding: 0 10px;
+  p {
+    margin: 4px 0;
+  }
+  .title {
+    text-align: center;
+  }
+  .red {
+    color: red;
+  }
+  .link {
+    color: $theme-color-gold;
+    cursor: pointer;
+  }
   .wechat-group {
     height: 500px;
   }

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

@@ -106,7 +106,7 @@ import { useRoute } from 'vue-router';
 
 import { fetchFindLiveRoom } from '@/api/liveRoom';
 import { usePull } from '@/hooks/use-pull';
-import { DanmuMsgTypeEnum, LiveRoomTypeEnum, liveTypeEnum } from '@/interface';
+import { DanmuMsgTypeEnum, LiveRoomTypeEnum, LiveTypeEnum } from '@/interface';
 import router, { mobileRouterName } from '@/router';
 import { useAppStore } from '@/store/app';
 
@@ -136,8 +136,7 @@ const {
   anchorInfo,
   remoteVideo,
 } = usePull({
-  localVideoRef,
-  liveType: liveTypeEnum.srsHlsPull,
+  liveType: LiveTypeEnum.srsHlsPull,
 });
 
 watch(
@@ -171,7 +170,7 @@ async function getLiveRoomInfo() {
     if (res.code === 200) {
       if (res.data.type === LiveRoomTypeEnum.user_wertc) {
         autoplayVal.value = true;
-        roomLiveType.value = liveTypeEnum.webrtcPull;
+        roomLiveType.value = LiveTypeEnum.webrtcPull;
         showPlayBtn.value = false;
       } else {
         showPlayBtn.value = true;

+ 11 - 18
src/views/home/index.vue

@@ -51,15 +51,6 @@
               >
                 进入直播(webrtc)
               </div>
-              <!-- <div
-                v-if="
-                  currentLiveRoom.live_room?.type === LiveRoomTypeEnum.user_srs
-                "
-                class="btn webrtc"
-                @click="joinRtcRoom()"
-              >
-                进入直播(srs-webrtc)
-              </div> -->
               <div
                 v-if="
                   currentLiveRoom.live_room?.type !==
@@ -190,7 +181,7 @@ import { useRoute, useRouter } from 'vue-router';
 import { fetchLiveList } from '@/api/live';
 import { sliderList } from '@/constant';
 import { usePull } from '@/hooks/use-pull';
-import { ILive, LiveRoomTypeEnum, liveTypeEnum } from '@/interface';
+import { ILive, LiveRoomTypeEnum, LiveTypeEnum } from '@/interface';
 import { routerName } from '@/router';
 
 const route = useRoute();
@@ -202,13 +193,13 @@ const topLiveRoomList = ref<ILive[]>([]);
 const otherLiveRoomList = ref<ILive[]>([]);
 const currentLiveRoom = ref<ILive>();
 const interactionList = ref(sliderList);
-const localVideoRef = ref<HTMLVideoElement[]>([]);
 const remoteVideoRef = ref<HTMLDivElement>();
 
-const { handleHlsPlay, videoLoading, remoteVideo } = usePull({
-  localVideoRef,
-  liveType: route.query.liveType as liveTypeEnum,
-});
+const { handleHlsPlay, videoLoading, remoteVideo, handleStopDrawing } = usePull(
+  {
+    liveType: route.query.liveType as LiveTypeEnum,
+  }
+);
 
 watch(
   () => remoteVideo.value,
@@ -224,6 +215,8 @@ watch(
 );
 
 function changeLiveRoom(item: ILive) {
+  console.log(item, 'llkk');
+  handleStopDrawing();
   if (item.id === currentLiveRoom.value?.id) return;
   currentLiveRoom.value = item;
   canvasRef.value?.childNodes?.forEach((item) => {
@@ -279,7 +272,7 @@ function joinRtcRoom() {
         roomId: currentLiveRoom.value.live_room_id,
       },
       query: {
-        liveType: liveTypeEnum.srsWebrtcPull,
+        liveType: LiveTypeEnum.srsWebrtcPull,
       },
     });
   } else {
@@ -289,7 +282,7 @@ function joinRtcRoom() {
         roomId: currentLiveRoom.value?.live_room_id,
       },
       query: {
-        liveType: liveTypeEnum.webrtcPull,
+        liveType: LiveTypeEnum.webrtcPull,
       },
     });
   }
@@ -300,7 +293,7 @@ function joinRoom(data: { roomId: number; isFlv: boolean }) {
     name: routerName.pull,
     params: { roomId: data.roomId },
     query: {
-      liveType: data.isFlv ? liveTypeEnum.srsFlvPull : liveTypeEnum.srsHlsPull,
+      liveType: data.isFlv ? LiveTypeEnum.srsFlvPull : LiveTypeEnum.srsHlsPull,
     },
   });
 }

+ 10 - 25
src/views/pull/index.vue

@@ -52,11 +52,6 @@
             class="media-list"
             :class="{ item: appStore.allTrack.length > 1 }"
           ></div>
-          <!-- <div
-            ref="remoteVideoRef"
-            class="media-list"
-            :class="{ item: appStore.allTrack.length > 1 }"
-          ></div> -->
           <VideoControls></VideoControls>
         </div>
       </div>
@@ -186,7 +181,7 @@
 </template>
 
 <script lang="ts" setup>
-import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
+import { onMounted, onUnmounted, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 
 import { fetchGoodsList } from '@/api/goods';
@@ -196,9 +191,10 @@ import {
   DanmuMsgTypeEnum,
   GoodsTypeEnum,
   IGoods,
-  liveTypeEnum,
+  LiveTypeEnum,
 } from '@/interface';
 import { useAppStore } from '@/store/app';
+import { useNetworkStore } from '@/store/network';
 import { useUserStore } from '@/store/user';
 import { NODE_ENV } from 'script/constant';
 
@@ -207,17 +203,18 @@ import RechargeCpt from './recharge/index.vue';
 const route = useRoute();
 const userStore = useUserStore();
 const appStore = useAppStore();
+const networkStore = useNetworkStore();
 const giftGoodsList = ref<IGoods[]>([]);
 const height = ref(0);
 const giftLoading = ref(false);
 const showRecharge = ref(false);
-const showJoin = ref(true);
 const showSidebar = ref(true);
 const topRef = ref<HTMLDivElement>();
 const bottomRef = ref<HTMLDivElement>();
 const danmuListRef = ref<HTMLDivElement>();
 const remoteVideoRef = ref<HTMLDivElement>();
 const containerRef = ref<HTMLDivElement>();
+const queryLiveType = ref(route.query.liveType as LiveTypeEnum);
 const {
   initPull,
   closeWs,
@@ -225,7 +222,6 @@ const {
   mySocketId,
   keydownDanmu,
   sendDanmu,
-  addVideo,
   videoLoading,
   remoteVideo,
   roomLiving,
@@ -235,8 +231,7 @@ const {
   liveRoomInfo,
   anchorInfo,
 } = usePull({
-  remoteVideoRef,
-  liveType: route.query.liveType as liveTypeEnum,
+  liveType: queryLiveType.value,
 });
 
 onUnmounted(() => {
@@ -264,10 +259,10 @@ onMounted(() => {
   getGoodsList();
   if (
     [
-      liveTypeEnum.srsHlsPull,
-      liveTypeEnum.srsFlvPull,
-      liveTypeEnum.srsWebrtcPull,
-    ].includes(route.query.liveType as liveTypeEnum)
+      LiveTypeEnum.srsHlsPull,
+      LiveTypeEnum.srsFlvPull,
+      LiveTypeEnum.srsWebrtcPull,
+    ].includes(route.query.liveType as LiveTypeEnum)
   ) {
     showSidebar.value = false;
   }
@@ -304,20 +299,10 @@ async function getGoodsList() {
 }
 
 function handleRecharge() {
-  console.log(showRecharge.value);
   if (!loginTip()) return;
   showRecharge.value = true;
 }
 
-function handleJoin() {
-  window.$message.info('维护中~');
-  return;
-  showJoin.value = !showJoin.value;
-  nextTick(() => {
-    addVideo();
-  });
-}
-
 watch(
   () => damuList.value.length,
   () => {

+ 3 - 3
src/views/push/index.vue

@@ -1,8 +1,8 @@
 <template>
   <div>
-    <SrsCpt v-if="route.query.liveType === liveTypeEnum.srsPush"></SrsCpt>
+    <SrsCpt v-if="route.query.liveType === LiveTypeEnum.srsPush"></SrsCpt>
     <RtcCpt
-      v-else-if="route.query.liveType === liveTypeEnum.webrtcPush"
+      v-else-if="route.query.liveType === LiveTypeEnum.webrtcPush"
     ></RtcCpt>
   </div>
 </template>
@@ -10,7 +10,7 @@
 <script lang="ts" setup>
 import { useRoute } from 'vue-router';
 
-import { liveTypeEnum } from '@/interface';
+import { LiveTypeEnum } from '@/interface';
 
 import RtcCpt from './rtc/index.vue';
 import SrsCpt from './srs/index.vue';

+ 19 - 54
src/views/push/rtc/index.vue

@@ -106,12 +106,12 @@
           </div>
           <div class="bottom">
             <n-button
-              v-if="!isLiving"
+              v-if="!roomLiving"
               type="info"
               size="small"
               @click="handleStartLive"
             >
-              开始rtc直播
+              开始直播
             </n-button>
             <n-button
               v-else
@@ -312,7 +312,7 @@ const {
   mySocketId,
   lastCoverImg,
   canvasVideoStream,
-  isLiving,
+  roomLiving,
   currentResolutionRatio,
   currentMaxBitrate,
   currentMaxFramerate,
@@ -333,6 +333,9 @@ const containerRef = ref<HTMLDivElement>();
 const pushCanvasRef = ref<HTMLCanvasElement>();
 const fabricCanvas = ref<fabric.Canvas>();
 const audioCtx = ref<AudioContext>();
+const startTime = ref(+new Date());
+// const startTime = ref(1692807352565); // 1693027352565
+
 const timeCanvasDom = ref<Raw<fabric.Text>[]>([]);
 const stopwatchCanvasDom = ref<Raw<fabric.Text>[]>([]);
 const wrapSize = reactive({
@@ -403,7 +406,7 @@ function renderAll() {
     item.text = new Date().toLocaleString();
   });
   stopwatchCanvasDom.value.forEach((item) => {
-    item.text = formatDownTime(+new Date(), +new Date());
+    item.text = formatDownTime(+new Date(), startTime.value);
   });
   fabricCanvas.value?.renderAll();
 }
@@ -462,17 +465,9 @@ function initNullAudio() {
   };
   const res = [...appStore.allTrack, webAudioTrack];
   appStore.setAllTrack(res);
-  const vel = createVideo({});
-  vel.style.width = `1px`;
-  vel.style.height = `1px`;
-  vel.style.position = 'fixed';
-  vel.style.bottom = '0';
-  vel.style.right = '0';
-  vel.style.opacity = '0';
-  vel.style.pointerEvents = 'none';
-  vel.srcObject = destination.stream;
-  document.body.appendChild(vel);
-  bodyAppendChildElArr.value.push(vel);
+  const videoEl = createVideo({ appendChild: true });
+  videoEl.srcObject = destination.stream;
+  bodyAppendChildElArr.value.push(videoEl);
 }
 
 let streamTmp: MediaStream;
@@ -504,16 +499,8 @@ function handleMixedAudio() {
     // @ts-ignore
     canvasVideoStream.value?.addTrack(destination.stream.getAudioTracks()[0]);
     gainNode.connect(destination);
-    vel = createVideo({});
-    vel.style.width = `1px`;
-    vel.style.height = `1px`;
-    vel.style.position = 'fixed';
-    vel.style.bottom = '0';
-    vel.style.right = '0';
-    vel.style.opacity = '0';
-    vel.style.pointerEvents = 'none';
+    vel = createVideo({ appendChild: true });
     vel.srcObject = destination.stream;
-    document.body.appendChild(vel);
     bodyAppendChildElArr.value.push(vel);
   }
 }
@@ -562,19 +549,11 @@ function autoCreateVideo({
   muted?: boolean;
 }) {
   console.warn('autoCreateVideoautoCreateVideo', id);
-  const videoEl = createVideo({});
+  const videoEl = createVideo({ appendChild: true });
   if (muted !== undefined) {
     videoEl.muted = muted;
   }
   videoEl.srcObject = stream;
-  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);
   bodyAppendChildElArr.value.push(videoEl);
   return new Promise<{
     canvasDom: fabric.Image;
@@ -795,17 +774,11 @@ async function handleCache() {
       const { code, file } = await readFile(item.id);
       if (code === 1 && file) {
         const url = URL.createObjectURL(file);
-        const videoEl = createVideo({});
+        const videoEl = createVideo({
+          muted: item.muted ? item.muted : false,
+          appendChild: true,
+        });
         videoEl.src = url;
-        videoEl.muted = item.muted ? item.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);
         bodyAppendChildElArr.value.push(videoEl);
         await new Promise((resolve) => {
           videoEl.onloadedmetadata = () => {
@@ -935,7 +908,7 @@ async function handleCache() {
       obj.scaleInfo = item.scaleInfo;
       if (fabricCanvas.value) {
         const canvasDom = markRaw(
-          new fabric.Text('00:00:00.000', {
+          new fabric.Text('00天00时00分00秒000毫秒', {
             top: (item.rect?.top || 0) / window.devicePixelRatio,
             left: (item.rect?.left || 0) / window.devicePixelRatio,
             fill: item.stopwatchInfo?.color,
@@ -1202,7 +1175,7 @@ async function addMediaOk(val: {
     };
     if (fabricCanvas.value) {
       const canvasDom = markRaw(
-        new fabric.Text('00:00:00.000', {
+        new fabric.Text('00天00时00分00秒000毫秒', {
           top: 0,
           left: 0,
           fill: val.stopwatchInfo?.color,
@@ -1312,17 +1285,9 @@ async function addMediaOk(val: {
       const { code } = await saveFile({ file, fileName: mediaVideoTrack.id });
       if (code !== 1) return;
       const url = URL.createObjectURL(file);
-      const videoEl = createVideo({});
+      const videoEl = createVideo({ muted: false, appendChild: true });
       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);
       bodyAppendChildElArr.value.push(videoEl);
       const videoRes = await new Promise<HTMLVideoElement>((resolve) => {
         videoEl.onloadedmetadata = () => {

+ 17 - 61
src/views/push/srs/index.vue

@@ -106,12 +106,12 @@
           </div>
           <div class="bottom">
             <n-button
-              v-if="!isLiving"
+              v-if="!roomLiving"
               type="info"
               size="small"
               @click="handleStartLive"
             >
-              开始srs直播
+              开始直播
             </n-button>
             <n-button
               v-else
@@ -278,12 +278,7 @@ import * as workerTimers from 'worker-timers';
 import { mediaTypeEnumMap } from '@/constant';
 import { usePush } from '@/hooks/use-push';
 import { useRTCParams } from '@/hooks/use-rtc-params';
-import {
-  DanmuMsgTypeEnum,
-  LiveRoomTypeEnum,
-  MediaTypeEnum,
-  liveTypeEnum,
-} from '@/interface';
+import { DanmuMsgTypeEnum, LiveRoomTypeEnum, MediaTypeEnum } from '@/interface';
 import { AppRootState, useAppStore } from '@/store/app';
 import { useResourceCacheStore } from '@/store/cache';
 import { useUserStore } from '@/store/user';
@@ -319,7 +314,7 @@ const {
   mySocketId,
   lastCoverImg,
   canvasVideoStream,
-  isLiving,
+  roomLiving,
   currentResolutionRatio,
   currentMaxBitrate,
   currentMaxFramerate,
@@ -340,6 +335,8 @@ const containerRef = ref<HTMLDivElement>();
 const pushCanvasRef = ref<HTMLCanvasElement>();
 const fabricCanvas = ref<fabric.Canvas>();
 const audioCtx = ref<AudioContext>();
+const startTime = ref(+new Date());
+
 const timeCanvasDom = ref<Raw<fabric.Text>[]>([]);
 const stopwatchCanvasDom = ref<Raw<fabric.Text>[]>([]);
 const wrapSize = reactive({
@@ -349,7 +346,6 @@ const wrapSize = reactive({
 const workerTimerId = ref(-1);
 const videoRatio = ref(16 / 9);
 const bodyAppendChildElArr = ref<HTMLElement[]>([]);
-const isSRS = route.query.liveType === liveTypeEnum.srsPush;
 
 watch(
   () => damuList.value.length,
@@ -405,13 +401,12 @@ function initUserMedia() {
         });
     });
 }
-
 function renderAll() {
   timeCanvasDom.value.forEach((item) => {
     item.text = new Date().toLocaleString();
   });
   stopwatchCanvasDom.value.forEach((item) => {
-    item.text = formatDownTime(+new Date(), +new Date());
+    item.text = formatDownTime(+new Date(), startTime.value);
   });
   fabricCanvas.value?.renderAll();
 }
@@ -470,16 +465,8 @@ function initNullAudio() {
   };
   const res = [...appStore.allTrack, webAudioTrack];
   appStore.setAllTrack(res);
-  const vel = createVideo({});
-  vel.style.width = `1px`;
-  vel.style.height = `1px`;
-  vel.style.position = 'fixed';
-  vel.style.bottom = '0';
-  vel.style.right = '0';
-  vel.style.opacity = '0';
-  vel.style.pointerEvents = 'none';
+  const vel = createVideo({ appendChild: true });
   vel.srcObject = destination.stream;
-  document.body.appendChild(vel);
   bodyAppendChildElArr.value.push(vel);
 }
 
@@ -512,16 +499,8 @@ function handleMixedAudio() {
     // @ts-ignore
     canvasVideoStream.value?.addTrack(destination.stream.getAudioTracks()[0]);
     gainNode.connect(destination);
-    vel = createVideo({});
-    vel.style.width = `1px`;
-    vel.style.height = `1px`;
-    vel.style.position = 'fixed';
-    vel.style.bottom = '0';
-    vel.style.right = '0';
-    vel.style.opacity = '0';
-    vel.style.pointerEvents = 'none';
+    vel = createVideo({ appendChild: true });
     vel.srcObject = destination.stream;
-    document.body.appendChild(vel);
     bodyAppendChildElArr.value.push(vel);
   }
 }
@@ -570,19 +549,11 @@ function autoCreateVideo({
   muted?: boolean;
 }) {
   console.warn('autoCreateVideoautoCreateVideo', id);
-  const videoEl = createVideo({});
+  const videoEl = createVideo({ appendChild: true });
   if (muted !== undefined) {
     videoEl.muted = muted;
   }
   videoEl.srcObject = stream;
-  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);
   bodyAppendChildElArr.value.push(videoEl);
   return new Promise<{
     canvasDom: fabric.Image;
@@ -803,17 +774,11 @@ async function handleCache() {
       const { code, file } = await readFile(item.id);
       if (code === 1 && file) {
         const url = URL.createObjectURL(file);
-        const videoEl = createVideo({});
+        const videoEl = createVideo({
+          appendChild: true,
+          muted: item.muted ? item.muted : false,
+        });
         videoEl.src = url;
-        videoEl.muted = item.muted ? item.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);
         bodyAppendChildElArr.value.push(videoEl);
         await new Promise((resolve) => {
           videoEl.onloadedmetadata = () => {
@@ -943,7 +908,7 @@ async function handleCache() {
       obj.scaleInfo = item.scaleInfo;
       if (fabricCanvas.value) {
         const canvasDom = markRaw(
-          new fabric.Text('00:00:00.000', {
+          new fabric.Text('00天00时00分00秒000毫秒', {
             top: (item.rect?.top || 0) / window.devicePixelRatio,
             left: (item.rect?.left || 0) / window.devicePixelRatio,
             fill: item.stopwatchInfo?.color,
@@ -1210,7 +1175,7 @@ async function addMediaOk(val: {
     };
     if (fabricCanvas.value) {
       const canvasDom = markRaw(
-        new fabric.Text('00:00:00.000', {
+        new fabric.Text('00天00时00分00秒000毫秒', {
           top: 0,
           left: 0,
           fill: val.stopwatchInfo?.color,
@@ -1320,17 +1285,8 @@ async function addMediaOk(val: {
       const { code } = await saveFile({ file, fileName: mediaVideoTrack.id });
       if (code !== 1) return;
       const url = URL.createObjectURL(file);
-      const videoEl = createVideo({});
+      const videoEl = createVideo({ appendChild: true, muted: false });
       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);
       bodyAppendChildElArr.value.push(videoEl);
       const videoRes = await new Promise<HTMLVideoElement>((resolve) => {
         videoEl.onloadedmetadata = () => {

+ 4 - 4
src/views/rank/index.vue

@@ -100,8 +100,8 @@ import {
   ILiveRoom,
   IUser,
   LiveRoomTypeEnum,
+  LiveTypeEnum,
   RankTypeEnum,
-  liveTypeEnum,
 } from '@/interface';
 import router, { routerName } from '@/router';
 
@@ -193,14 +193,14 @@ const mockRank: {
 const rankList = ref(mockRank);
 
 function handleJoin(item) {
-  let liveType: liveTypeEnum = liveTypeEnum.webrtcPull;
+  let liveType: LiveTypeEnum = LiveTypeEnum.webrtcPull;
   if (
     item?.type === LiveRoomTypeEnum.system ||
     item?.type === LiveRoomTypeEnum.user_obs
   ) {
-    liveType = liveTypeEnum.srsFlvPull;
+    liveType = LiveTypeEnum.srsFlvPull;
   } else if (item?.type === LiveRoomTypeEnum.user_srs) {
-    liveType = liveTypeEnum.srsWebrtcPull;
+    liveType = LiveTypeEnum.srsWebrtcPull;
   }
   router.push({
     name: routerName.pull,