Browse Source

fix: 优化

shuisheng 1 year ago
parent
commit
30a12c1d30

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

@@ -50,6 +50,18 @@
     </div>
 
     <div class="right">
+      <div
+        class="resolution"
+        v-if="appStore.videoFps"
+      >
+        {{ appStore.videoFps }}帧
+      </div>
+      <div
+        class="resolution"
+        v-if="appStore.videoKBs"
+      >
+        {{ appStore.videoKBs }}KB/s
+      </div>
       <div
         class="resolution"
         v-if="resolution"

+ 12 - 3
src/hooks/use-play.ts

@@ -131,7 +131,12 @@ export function useFlvPlay() {
     return new Promise((resolve) => {
       function main() {
         destroyFlv();
-        if (mpegts.getFeatureList().mseLivePlayback && mpegts.isSupported()) {
+        // mseLivePlayback,指示 HTTP MPEG2-TS/FLV 直播流是否可以在您的浏览器上运行。
+        // msePlayback,与 相同mpegts.isSupported(),表示基本播放是否可以在您的浏览器上运行。
+        if (
+          mpegts.getFeatureList().mseLivePlayback &&
+          mpegts.getFeatureList().msePlayback
+        ) {
           flvPlayer.value = mpegts.createPlayer({
             type: 'flv', // could also be mpegts, m2ts, flv
             isLive: true,
@@ -171,8 +176,12 @@ export function useFlvPlay() {
               }, 1000);
             }
           });
-          flvPlayer.value.on(mpegts.Events.MEDIA_INFO, () => {
-            console.log('mpegts消息:mpegts.Events.MEDIA_INFO');
+          flvPlayer.value.on(mpegts.Events.MEDIA_INFO, (data) => {
+            console.log('mpegts.Events.MEDIA_INFO', data);
+            // appStore.videoFps = data?.fps?.toFixed(2);
+          });
+          flvPlayer.value.on(mpegts.Events.STATISTICS_INFO, (data) => {
+            appStore.videoKBs = data?.speed?.toFixed(2);
           });
           try {
             console.log(`开始播放flv,muted:${cacheStore.muted}`);

+ 45 - 54
src/hooks/use-pull.ts

@@ -28,10 +28,11 @@ export function usePull(roomId: string) {
   const autoplayVal = ref(false);
   const videoLoading = ref(false);
   const isPlaying = ref(false);
+  const showPlayBtn = ref(false);
   const flvurl = ref('');
   const hlsurl = ref('');
   const videoWrapRef = ref<HTMLDivElement>();
-  const videoHeight = ref();
+  const videoResolution = ref();
   const videoElArr = ref<HTMLVideoElement[]>([]);
   const remoteVideo = ref<HTMLElement[]>([]);
   const {
@@ -72,7 +73,7 @@ export function usePull(roomId: string) {
         },
         videoEl: newval,
         videoResize: ({ w, h }) => {
-          videoHeight.value = `${w}x${h}`;
+          videoResolution.value = `${w}x${h}`;
         },
       });
       stopDrawingArr.value.push(stopDrawing);
@@ -82,16 +83,6 @@ export function usePull(roomId: string) {
     }
   });
 
-  function handleHlsPlay(url: string) {
-    console.log('handleHlsPlay', url);
-    handleStopDrawing();
-    videoLoading.value = true;
-    appStore.setLiveLine(LiveLineEnum.hls);
-    startHlsPlay({
-      hlsurl: url,
-    });
-  }
-
   watch(flvVideoEl, (newval) => {
     stopDrawingArr.value = [];
     stopDrawingArr.value.forEach((cb) => cb());
@@ -104,7 +95,7 @@ export function usePull(roomId: string) {
         },
         videoEl: newval,
         videoResize: ({ w, h }) => {
-          videoHeight.value = `${w}x${h}`;
+          videoResolution.value = `${w}x${h}`;
         },
       });
       stopDrawingArr.value.push(stopDrawing);
@@ -140,7 +131,7 @@ export function usePull(roomId: string) {
               },
               videoEl: item.videoEl,
               videoResize: ({ w, h }) => {
-                videoHeight.value = `${w}x${h}`;
+                videoResolution.value = `${w}x${h}`;
               },
             });
             remoteVideo.value.push(item.videoEl);
@@ -168,6 +159,16 @@ export function usePull(roomId: string) {
     }
   );
 
+  function handleHlsPlay(url: string) {
+    console.log('handleHlsPlay', url);
+    handleStopDrawing();
+    videoLoading.value = true;
+    appStore.setLiveLine(LiveLineEnum.hls);
+    startHlsPlay({
+      hlsurl: url,
+    });
+  }
+
   function handleFlvPlay() {
     console.log('handleFlvPlay');
     handleStopDrawing();
@@ -182,45 +183,33 @@ export function usePull(roomId: string) {
     roomLiving.value = true;
     flvurl.value = data.flv_url!;
     hlsurl.value = data.hls_url!;
-    switch (data.type) {
-      case LiveRoomTypeEnum.srs:
-        if (appStore.liveLine === LiveLineEnum.flv) {
-          handleFlvPlay();
-        } else if (appStore.liveLine === LiveLineEnum.hls) {
-          handleHlsPlay(data.hls_url!);
-        }
-        break;
-      case LiveRoomTypeEnum.obs:
-        if (appStore.liveLine === LiveLineEnum.flv) {
-          handleFlvPlay();
-        } else if (appStore.liveLine === LiveLineEnum.hls) {
-          handleHlsPlay(data.hls_url!);
-        }
-        break;
-      case LiveRoomTypeEnum.msr:
-        if (appStore.liveLine === LiveLineEnum.flv) {
-          handleFlvPlay();
-        } else if (appStore.liveLine === LiveLineEnum.hls) {
-          handleHlsPlay(data.hls_url!);
-        }
-        break;
-      case LiveRoomTypeEnum.system:
-        if (appStore.liveLine === LiveLineEnum.flv) {
-          handleFlvPlay();
-        } else if (appStore.liveLine === LiveLineEnum.hls) {
-          handleHlsPlay(data.hls_url!);
-        }
-        break;
-      case LiveRoomTypeEnum.pk:
-        if (appStore.liveLine === LiveLineEnum.flv) {
-          handleFlvPlay();
-        } else if (appStore.liveLine === LiveLineEnum.hls) {
-          handleHlsPlay(data.hls_url!);
-        }
-        break;
-      case LiveRoomTypeEnum.wertc_live:
-        appStore.setLiveLine(LiveLineEnum.rtc);
-        break;
+    function play() {
+      if (appStore.liveLine === LiveLineEnum.flv) {
+        handleFlvPlay();
+      } else if (appStore.liveLine === LiveLineEnum.hls) {
+        handleHlsPlay(data.hls_url!);
+      }
+    }
+    if (LiveRoomTypeEnum.pk === data.type && !route.query.pkKey) {
+      play();
+    } else if (
+      [
+        LiveRoomTypeEnum.system,
+        LiveRoomTypeEnum.srs,
+        LiveRoomTypeEnum.obs,
+        LiveRoomTypeEnum.msr,
+        LiveRoomTypeEnum.pk,
+      ].includes(data.type!)
+    ) {
+      play();
+    } else if (
+      [
+        LiveRoomTypeEnum.wertc_live,
+        LiveRoomTypeEnum.wertc_meeting_one,
+        LiveRoomTypeEnum.wertc_meeting_two,
+      ].includes(data.type!)
+    ) {
+      appStore.setLiveLine(LiveLineEnum.rtc);
     }
   }
 
@@ -228,6 +217,7 @@ export function usePull(roomId: string) {
     [() => roomLiving.value, () => appStore.liveRoomInfo],
     ([val, liveRoomInfo]) => {
       if (val && liveRoomInfo) {
+        showPlayBtn.value = false;
         if (
           [
             LiveRoomTypeEnum.system,
@@ -403,11 +393,12 @@ export function usePull(roomId: string) {
     keydownDanmu,
     sendDanmu,
     handleSendGetLiveUser,
+    showPlayBtn,
     danmuMsgType,
     isPlaying,
     msgIsFile,
     mySocketId,
-    videoHeight,
+    videoResolution,
     remoteVideo,
     roomLiving,
     autoplayVal,

+ 12 - 0
src/interface.ts

@@ -5,6 +5,18 @@ import {
 } from './types/ILiveRoom';
 import { IUser } from './types/IUser';
 
+export interface IFlvStatistics {
+  url: string;
+  hasRedirect: boolean;
+  speed: number;
+  loaderType: string;
+  currentSegmentIndex: number;
+  totalSegmentCount: number;
+  playerType: string;
+  decodedFrames: number;
+  droppedFrames: number;
+}
+
 export interface IQiniuData {
   id?: number;
   user_id?: number;

+ 1 - 1
src/locales/zh/layout.ts

@@ -22,7 +22,7 @@ export default nameSpaceWrap('layout', {
   siteOrder: '全站充值',
   myWallet: '我的收支',
 
-  guide: '快速上',
+  guide: '快速上',
   apiDoc: '接口文档',
   bilibiliTutorial: 'b站教程',
   vipCourses: 'billd-live付费课',

+ 1 - 0
src/router/index.ts

@@ -229,6 +229,7 @@ router.beforeEach((to, from, next) => {
         return next({
           name: mobileRouterName.h5Room,
           params: { roomId: to.params.roomId },
+          query: { ...to.query },
         });
       } else {
         return next({

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

@@ -7,6 +7,8 @@ import { ILiveRoom } from '@/types/ILiveRoom';
 
 export type AppRootState = {
   playing: boolean;
+  videoKBs?: string;
+  videoFps?: number;
   videoRatio: number;
   normalVolume: number;
   navList: { routeName: string; name: string }[];
@@ -48,6 +50,8 @@ export const useAppStore = defineStore('app', {
   state: (): AppRootState => {
     return {
       playing: true,
+      videoKBs: undefined,
+      videoFps: undefined,
       videoRatio: 16 / 9,
       normalVolume: 70,
       navList: [

+ 58 - 0
src/utils/index.ts

@@ -208,6 +208,64 @@ export function formatDownTime(data: {
     return `${m}分${s}秒${msRes}`;
   }
 }
+/**
+ * 格式化倒计时
+ * @param endTime
+ * @param startTime
+ */
+export function formatDownTime2(data: {
+  endTime: number;
+  startTime: number;
+  day?: string;
+  hour?: string;
+  minute?: string;
+  second?: string;
+  millisecond?: string;
+  showMillisecond?: boolean;
+  addZero?: boolean;
+}) {
+  const times = (data.endTime - data.startTime) / 1000;
+  // js获取剩余天数
+  const d = parseInt(String(times / 60 / 60 / 24));
+  // js获取剩余小时
+  let h = parseInt(String((times / 60 / 60) % 24));
+  // js获取剩余分钟
+  let m = parseInt(String((times / 60) % 60));
+  // js获取剩余秒
+  let s = parseInt(String(times % 60));
+  let ms = new Date(data.endTime).getMilliseconds();
+
+  function addZero(num: number, flag: boolean) {
+    if (flag) {
+      return num < 10 ? `0${num}` : `${num}`;
+    } else {
+      return `${num}`;
+    }
+  }
+
+  // @ts-ignore
+  h = addZero(h, data.addZero);
+  // @ts-ignore
+  m = addZero(m, data.addZero);
+  // @ts-ignore
+  s = addZero(s, data.addZero);
+  if (Number(ms) < 100) {
+    if (ms < 10) {
+      // @ts-ignore
+      ms = `00${ms}`;
+    } else {
+      // @ts-ignore
+      ms = `0${ms}`;
+    }
+  }
+  return {
+    d,
+    h,
+    m,
+    s,
+    ms,
+  };
+}
 
 /**
  * requestFileSystem保存文件,成功返回code:1,失败返回code:2

+ 9 - 9
src/utils/network/webRTC.ts

@@ -498,15 +498,15 @@ export class WebRTCClass {
             type: 'warn',
           });
           appStore.setLiveLine(LiveLineEnum.rtc);
-          // if (this.maxBitrate !== -1) {
-          //   this.setMaxBitrate(this.maxBitrate);
-          // }
-          // if (this.maxFramerate !== -1) {
-          //   this.setMaxFramerate(this.maxFramerate);
-          // }
-          // if (this.resolutionRatio !== -1) {
-          //   this.setResolutionRatio(this.resolutionRatio);
-          // }
+          if (this.maxBitrate !== -1) {
+            this.setMaxBitrate(this.maxBitrate);
+          }
+          if (this.maxFramerate !== -1) {
+            this.setMaxFramerate(this.maxFramerate);
+          }
+          if (this.resolutionRatio !== -1) {
+            this.setResolutionRatio(this.resolutionRatio);
+          }
         }
         if (connectionState === 'disconnected') {
           // 表示至少有一个 ICE 连接处于 disconnected 状态,并且没有连接处于 failed、connecting 或 checking 状态。

+ 16 - 32
src/views/h5/room/index.vue

@@ -43,9 +43,8 @@
         主播还没开播~
       </div>
       <div
-        class="media-list"
+        class="remote-video"
         ref="remoteVideoRef"
-        :class="{ item: appStore.allTrack.length > 1 }"
       ></div>
       <div
         v-if="showPlayBtn && roomLiving && appStore.liveRoomInfo"
@@ -55,8 +54,8 @@
         点击播放
       </div>
       <VideoControls
-        v-else
-        :resolution="videoHeight"
+        v-if="roomLiving"
+        :resolution="videoResolution"
         @refresh="handleRefresh"
       ></VideoControls>
     </div>
@@ -95,7 +94,7 @@
                     </span>
                     <span v-else>
                       <span>{{ item.socket_id }}</span>
-                      <span> [游客] </span>
+                      <span>[游客]</span>
                     </span>
                     <span>:</span>
                   </span>
@@ -236,7 +235,6 @@ const appStore = useAppStore();
 const videoWrapTmpRef = ref<HTMLDivElement>();
 const bottomRef = ref<HTMLDivElement>();
 const danmuListRef = ref<HTMLDivElement>();
-const showPlayBtn = ref(false);
 const showEmoji = ref(false);
 
 const containerHeight = ref(0);
@@ -253,7 +251,7 @@ const {
   closeRtc,
   closeWs,
   handleSendGetLiveUser,
-  isPlaying,
+  showPlayBtn,
   autoplayVal,
   videoLoading,
   damuList,
@@ -261,7 +259,7 @@ const {
   roomLiving,
   anchorInfo,
   remoteVideo,
-  videoHeight,
+  videoResolution,
 } = usePull(roomId.value);
 
 onUnmounted(() => {
@@ -271,6 +269,7 @@ onUnmounted(() => {
 });
 
 onMounted(() => {
+  showPlayBtn.value = true;
   videoWrapRef.value = videoWrapTmpRef.value;
   getWechatQrcode();
   setTimeout(() => {
@@ -295,27 +294,6 @@ function handlePushStr(str) {
   showEmoji.value = false;
 }
 
-watch(
-  () => remoteVideo.value,
-  (newVal) => {
-    newVal.forEach((item) => {
-      remoteVideoRef.value?.appendChild(item);
-    });
-  },
-  {
-    deep: true,
-    immediate: true,
-  }
-);
-
-watch(
-  () => isPlaying.value,
-  (newVal) => {
-    if (newVal) {
-      showPlayBtn.value = false;
-    }
-  }
-);
 watch(
   () => damuList.value.length,
   () => {
@@ -431,23 +409,29 @@ async function getWechatQrcode() {
       font-size: 28px;
       transform: translate(-50%, -50%);
     }
-    .media-list {
+    .remote-video {
       position: relative;
+      width: 100%;
+      height: 100%;
       :deep(video) {
         position: absolute;
         left: 50%;
+        left: 50%;
         display: block;
+        margin: 0 auto;
         max-width: 100vw;
         max-height: var(--max-height);
-        transform: translateX(-50%);
+        transform: translate(-50%, -50%);
       }
       :deep(canvas) {
         position: absolute;
         left: 50%;
+        left: 50%;
         display: block;
+        margin: 0 auto;
         max-width: 100vw;
         max-height: var(--max-height);
-        transform: translateX(-50%);
+        transform: translate(-50%, -50%);
       }
     }
     .tip-btn {

+ 3 - 16
src/views/home/index.vue

@@ -65,7 +65,7 @@
           <template v-if="currentLiveRoom">
             <VideoControls
               @click.stop
-              :resolution="videoHeight"
+              :resolution="videoResolution"
               @refresh="handleRefresh"
             ></VideoControls>
             <div
@@ -204,7 +204,7 @@
 </template>
 
 <script lang="ts" setup>
-import { onMounted, ref, watch } from 'vue';
+import { onMounted, ref } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { useRoute, useRouter } from 'vue-router';
 
@@ -245,9 +245,8 @@ const { t } = useI18n();
 const {
   videoWrapRef,
   videoLoading,
-  remoteVideo,
   roomLiving,
-  videoHeight,
+  videoResolution,
   handleStopDrawing,
   handlePlay,
 } = usePull(route.params.roomId as string);
@@ -259,18 +258,6 @@ onMounted(() => {
   videoWrapRef.value = videoWrapTmpRef.value;
 });
 
-watch(
-  () => remoteVideo.value,
-  (newVal) => {
-    newVal.forEach((item) => {
-      remoteVideoRef.value?.appendChild(item);
-    });
-  },
-  {
-    deep: true,
-  }
-);
-
 function handleSlideList() {
   const row = 2;
   const res: any[] = [];

+ 42 - 47
src/views/profile/index.vue

@@ -1,5 +1,8 @@
 <template>
-  <div class="profile-wrap">
+  <div
+    class="profile-wrap"
+    v-loading="getUserLoading"
+  >
     <div class="uid">用户id:{{ userInfo?.id }}</div>
     <div class="avatar">
       <span class="txt">用户头像:</span>
@@ -42,13 +45,13 @@
             )
           "
           class="rtmp-url-wrap"
-          v-loading="keyLoading"
+          v-loading="updateKeyLoading"
         >
           <div>
-            <span>rtmp推流地址:{{ pushRes?.push_rtmp_url! }}, </span>
+            <span>RTMP推流地址:{{ pushRes?.push_rtmp_url! }},</span>
             <span
               class="link"
-              @click="handleCopy"
+              @click="handleCopy(pushRes?.push_rtmp_url!)"
             >
               复制
             </span>
@@ -61,19 +64,19 @@
             </span>
           </div>
           <div>
-            <span> OBS服务器:{{ pushRes?.push_obs_server! }}, </span>
+            <span>OBS服务器:{{ pushRes?.push_obs_server! }},</span>
             <span
               class="link"
-              @click="handleCopy"
+              @click="handleCopy(pushRes?.push_obs_server!)"
             >
               复制
             </span>
           </div>
           <div>
-            <span> OBS推流码:{{ pushRes?.push_obs_stream_key! }}, </span>
+            <span>OBS推流码:{{ pushRes?.push_obs_stream_key! }},</span>
             <span
               class="link"
-              @click="handleCopy"
+              @click="handleCopy(pushRes?.push_obs_stream_key!)"
             >
               复制
             </span>
@@ -86,12 +89,12 @@
 
 <script lang="ts" setup>
 import { copyToClipBoard, openToTarget } from 'billd-utils';
-import { onMounted, ref, watch } from 'vue';
+import { ref, watchEffect } from 'vue';
 import { useRoute, useRouter } from 'vue-router';
 
 import { fetchUpdateLiveRoomKey } from '@/api/liveRoom';
 import { fetchFindUser } from '@/api/user';
-import { DEFAULT_AUTH_INFO, SRS_CB_URL_PARAMS } from '@/constant';
+import { DEFAULT_AUTH_INFO } from '@/constant';
 import { loginTip } from '@/hooks/use-login';
 import { routerName } from '@/router';
 import { useUserStore } from '@/store/user';
@@ -99,48 +102,43 @@ import { LiveRoomTypeEnum } from '@/types/ILiveRoom';
 import { IUser } from '@/types/IUser';
 import { getLiveRoomPageUrl } from '@/utils';
 
-const newRtmpUrl = ref();
-const keyLoading = ref(false);
-const pushRes = ref();
+const userStore = useUserStore();
 const route = useRoute();
 const router = useRouter();
-const userStore = useUserStore();
 
+const pushRes = ref();
+const userId = ref(-1);
 const userInfo = ref<IUser>();
+const getUserLoading = ref(false);
+const updateKeyLoading = ref(false);
 
-watch(
-  () => route.params.userId,
-  (newval) => {
-    if (newval) {
-      handleUserInfo();
-    }
+watchEffect(() => {
+  if (route.params.userId) {
+    userId.value = Number(route.params.userId as string);
+    handleUserInfo();
   }
-);
-
-onMounted(() => {
-  handleUserInfo();
 });
 
 async function handleUserInfo() {
-  const userId = Number(route.params.userId as string);
-  const res = await fetchFindUser(userId);
-  if (res.code === 200) {
-    userInfo.value = res.data;
-  }
-  if (userStore.userInfo) {
-    const liveRoom = userStore.userInfo.live_rooms?.[0];
-    pushRes.value = liveRoom;
+  try {
+    getUserLoading.value = true;
+    const res = await fetchFindUser(userId.value);
+    if (res.code === 200) {
+      userInfo.value = res.data;
+    }
+    if (userStore.userInfo) {
+      const liveRoom = userStore.userInfo.live_rooms?.[0];
+      pushRes.value = liveRoom;
+    }
+  } catch (error) {
+    console.error(error);
+  } finally {
+    getUserLoading.value = false;
   }
 }
 
-function handleCopy() {
-  copyToClipBoard(
-    newRtmpUrl.value ||
-      handleUrl({
-        url: userInfo.value?.live_rooms?.[0].rtmp_url!,
-        token: userInfo.value?.live_rooms?.[0].key!,
-      })
-  );
+function handleCopy(url: string) {
+  copyToClipBoard(url);
   window.$message.success('复制成功!');
 }
 
@@ -155,27 +153,24 @@ function openLiveRoom() {
   openToTarget(url.href);
 }
 
-function handleUrl(data: { url: string; token: string }) {
-  return `${data.url}?${SRS_CB_URL_PARAMS.publishKey}=${data.token}&${SRS_CB_URL_PARAMS.publishType}=${LiveRoomTypeEnum.obs}`;
-}
-
 async function handleUpdateKey() {
   try {
-    keyLoading.value = true;
+    updateKeyLoading.value = true;
     const res = await fetchUpdateLiveRoomKey();
     if (res.code === 200) {
       pushRes.value = res.data;
     }
   } catch (error) {
-    console.log(error);
+    console.error(error);
   } finally {
-    keyLoading.value = false;
+    updateKeyLoading.value = false;
   }
 }
 </script>
 
 <style lang="scss" scoped>
 .profile-wrap {
+  position: relative;
   padding: 10px;
   .link {
     color: $theme-color-gold;

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

@@ -127,11 +127,11 @@
           }"
         ></div>
         <div
-          class="video-list"
+          class="remote-video"
           ref="remoteVideoRef"
         ></div>
         <VideoControls
-          :resolution="videoHeight"
+          :resolution="videoResolution"
           @refresh="handleRefresh"
           @full-screen="handleFullScreen"
         ></VideoControls>
@@ -467,11 +467,10 @@ const {
   handlePlay,
   handleSendGetLiveUser,
   videoWrapRef,
-  remoteVideo,
   danmuMsgType,
   msgIsFile,
   mySocketId,
-  videoHeight,
+  videoResolution,
   videoLoading,
   roomLiving,
   damuList,
@@ -974,7 +973,7 @@ function handleScrollTop() {
       justify-content: space-between;
       height: 562px;
       background-color: rgba($color: #000000, $alpha: 0.5);
-      .video-list {
+      .remote-video {
         position: relative;
         width: 100%;
         height: 100%;

+ 158 - 76
src/views/push/index.vue

@@ -12,8 +12,7 @@
           class="recording"
           v-if="recording"
         >
-          <span class="dot"></span>
-          <span>REC {{ recordVideoTime }}</span>
+          ● REC {{ recordVideoTime }}
         </div>
         <div
           class="record-ico"
@@ -170,11 +169,31 @@
             class="item"
             @click="handleActiveObject(item)"
           >
-            <div class="name">
-              {{ NODE_ENV === 'development' ? item.id : '' }}({{
-                mediaTypeEnumMap[item.type]
-              }}){{ item.mediaName }}
+            <div class="item-left">
+              <div
+                class="control-item"
+                @click="handleEye(item)"
+              >
+                <n-icon
+                  size="16"
+                  v-if="item.openEye && item.type !== MediaTypeEnum.microphone"
+                >
+                  <EyeOutline></EyeOutline>
+                </n-icon>
+                <n-icon
+                  size="16"
+                  v-else
+                >
+                  <EyeOffOutline></EyeOffOutline>
+                </n-icon>
+              </div>
+              <div class="name">
+                {{ NODE_ENV === 'development' ? item.id : '' }}({{
+                  mediaTypeEnumMap[item.type]
+                }}){{ item.mediaName }}
+              </div>
             </div>
+
             <div class="control">
               <div
                 v-if="item.audio === 1"
@@ -201,23 +220,6 @@
                   </div>
                 </n-popover>
               </div>
-              <div
-                class="control-item"
-                @click="handleEye(item)"
-              >
-                <n-icon
-                  size="16"
-                  v-if="item.openEye"
-                >
-                  <EyeOutline></EyeOutline>
-                </n-icon>
-                <n-icon
-                  size="16"
-                  v-else
-                >
-                  <EyeOffOutline></EyeOffOutline>
-                </n-icon>
-              </div>
               <div
                 class="control-item"
                 @click="handleEdit(item)"
@@ -424,6 +426,7 @@ import {
 } from 'vue';
 import { useRoute } from 'vue-router';
 
+import { fetchGetWsMessageList } from '@/api/wsMessage';
 import {
   QINIU_RESOURCE,
   liveRoomTypeEnumMap,
@@ -438,6 +441,8 @@ import {
   DanmuMsgTypeEnum,
   MediaTypeEnum,
   WsMessageMsgIsFileEnum,
+  WsMessageMsgIsShowEnum,
+  WsMessageMsgIsVerifyEnum,
 } from '@/interface';
 import { AppRootState, useAppStore } from '@/store/app';
 import { usePiniaCacheStore } from '@/store/cache';
@@ -446,7 +451,7 @@ import { useUserStore } from '@/store/user';
 import { LiveRoomTypeEnum } from '@/types/ILiveRoom';
 import {
   createVideo,
-  formatDownTime,
+  formatDownTime2,
   generateBase64,
   getRandomEnglishString,
   handleUserMedia,
@@ -507,6 +512,7 @@ const pushCanvasRef = ref<HTMLCanvasElement>();
 const webaudioVideo = ref<HTMLVideoElement>();
 const fabricCanvas = ref<fabric.Canvas>();
 const startTime = ref(+new Date());
+const initAudioFlag = ref(false);
 const msgLoading = ref(false);
 const uploadRef = ref<HTMLInputElement>();
 const nullAudioStream = ref<MediaStream>();
@@ -528,7 +534,7 @@ const msrDelay = ref(1000 * 1);
 const msrMaxDelay = ref(1000 * 5);
 const suggestedName = ref('');
 const recordVideoTimer = ref();
-const recordVideoTime = ref('');
+const recordVideoTime = ref('00:00:00');
 let avRecorder: AVRecorder | null = null;
 
 watch(
@@ -745,6 +751,7 @@ watch(
   (newval) => {
     if (newval) {
       handleSendGetLiveUser(Number(newval));
+      handleHistoryMsg();
     }
   }
 );
@@ -785,11 +792,13 @@ function renderAll() {
     item.text = new Date().toLocaleString();
   });
   stopwatchCanvasDom.value.forEach((item) => {
-    item.text = formatDownTime({
+    const res = formatDownTime2({
       endTime: +new Date(),
       startTime: startTime.value,
-      showMs: true,
+      showMillisecond: true,
+      addZero: true,
     });
+    item.text = `${res.d}天${res.h}时${res.m}分${res.s}秒${res.ms}毫秒`;
   });
   fabricCanvas.value?.renderAll();
 }
@@ -893,7 +902,7 @@ function handleMixedAudio() {
     if (!audioCtx || !item.stream) return;
     const source = audioCtx.createMediaStreamSource(item.stream);
     const gainNode = audioCtx.createGain();
-    gainNode.gain.value = (item.volume || 100) / 100;
+    gainNode.gain.value = (item.volume || 0) / 100;
     source.connect(gainNode);
     res.push({ source, gainNode });
     // console.log('混流', item.stream?.id, item.stream);
@@ -907,7 +916,8 @@ function handleMixedAudio() {
     webaudioVideo.value.remove();
   }
   webaudioVideo.value = createVideo({
-    appendChild: true,
+    appendChild: false,
+    muted: true,
   });
   bodyAppendChildElArr.value.push(webaudioVideo.value);
   webaudioVideo.value.className = 'web-audio-video';
@@ -936,44 +946,109 @@ function handleEndLive() {
   endLive();
 }
 
+async function handleHistoryMsg() {
+  try {
+    const res = await fetchGetWsMessageList({
+      nowPage: 1,
+      pageSize: appStore.liveRoomInfo?.history_msg_total || 10,
+      orderName: 'created_at',
+      orderBy: 'desc',
+      live_room_id: Number(roomId.value),
+      is_show: WsMessageMsgIsShowEnum.yes,
+      is_verify: WsMessageMsgIsVerifyEnum.yes,
+    });
+    if (res.code === 200) {
+      res.data.rows.forEach((v) => {
+        damuList.value.unshift({
+          ...v,
+          live_room_id: v.live_room_id!,
+          msg_id: v.id!,
+          socket_id: '',
+          msgType: v.msg_type!,
+          msgIsFile: v.msg_is_file!,
+          userInfo: v.user,
+          msg: v.content!,
+          username: v.username!,
+          send_msg_time: Number(v.send_msg_time),
+          redbag_send_id: v.redbag_send_id,
+        });
+      });
+      if (
+        appStore.liveRoomInfo?.system_msg &&
+        appStore.liveRoomInfo?.system_msg !== ''
+      ) {
+        damuList.value.push({
+          live_room_id: Number(roomId.value),
+          socket_id: '',
+          msgType: DanmuMsgTypeEnum.system,
+          msgIsFile: WsMessageMsgIsFileEnum.no,
+          msg: appStore.liveRoomInfo.system_msg,
+          send_msg_time: Number(+new Date()),
+        });
+      }
+    }
+  } catch (error) {
+    console.log(error);
+  }
+}
+
 async function handleRecordVideo() {
   if (!window.VideoDecoder || !window.AudioEncoder) {
     window.$message.warning(`当前环境不支持录制视频`);
     return;
   }
-  recording.value = !recording.value;
-  if (recording.value) {
-    const startTime = +new Date();
-    recordVideoTimer.value = setInterval(() => {
-      recordVideoTime.value = formatDownTime({
-        endTime: +new Date(),
-        startTime,
+  initAudio();
+  try {
+    if (!recording.value) {
+      suggestedName.value = `billd直播录制-${+new Date()}.mp4`;
+      const fileHandle = await window.showSaveFilePicker({
+        suggestedName: suggestedName.value,
       });
-    }, 1000);
-
-    avRecorder = new AVRecorder(canvasVideoStream.value!, {});
-    await avRecorder.start();
-    suggestedName.value = `billd直播录制-${+new Date()}.mp4`;
-    const fileHandle = await window.showSaveFilePicker({
-      suggestedName: suggestedName.value,
-    });
-    const writer = await fileHandle.createWritable();
-    avRecorder.outputStream?.pipeTo(writer).catch(console.error);
-  } else {
-    clearInterval(recordVideoTimer.value);
-    recordVideoTime.value = '';
-    await avRecorder?.stop();
-    window.$message.success(`录制文件: ${suggestedName.value} 已保存到本地`);
+      const writer = await fileHandle.createWritable();
+      avRecorder = new AVRecorder(canvasVideoStream.value!.clone(), {});
+      await avRecorder.start();
+      const startTime = +new Date();
+      recordVideoTimer.value = setInterval(() => {
+        const res = formatDownTime2({
+          endTime: +new Date(),
+          startTime,
+          showMillisecond: true,
+          addZero: true,
+        });
+        if (res.d) {
+          recordVideoTime.value = `${res.d}天${res.h}:${res.m}:${res.s}`;
+        } else {
+          recordVideoTime.value = `${res.h}:${res.m}:${res.s}`;
+        }
+      }, 1000);
+      avRecorder.outputStream?.pipeTo(writer).catch(console.error);
+    } else {
+      clearInterval(recordVideoTimer.value);
+      recordVideoTime.value = '00:00:00';
+      await avRecorder?.stop();
+      window.$message.success(`录制文件: ${suggestedName.value} 已保存到本地`);
+      avRecorder = null;
+    }
+    recording.value = !recording.value;
+  } catch (error) {
+    console.log(error);
+    recording.value = false;
   }
 }
 
+function initAudio() {
+  if (initAudioFlag.value) return;
+  initAudioFlag.value = true;
+  handleNullAudio();
+  handleMixedAudio();
+}
+
 function handleStartLive() {
   if (!appStore.allTrack.length) {
     window.$message.warning('至少选择一个素材');
     return;
   }
-  handleNullAudio();
-  handleMixedAudio();
+  initAudio();
   lastCoverImg.value = generateBase64(pushCanvasRef.value!);
   startLive({
     type: liveType,
@@ -1472,7 +1547,7 @@ async function handleCache() {
         audio: { deviceId: obj.deviceId },
       });
       if (!event) return;
-      const videoEl = createVideo({ appendChild: true, muted: false });
+      const videoEl = createVideo({ appendChild: true, muted: true });
       bodyAppendChildElArr.value.push(videoEl);
       videoEl.setAttribute('videoid', obj.id);
       videoEl.srcObject = event;
@@ -1813,7 +1888,7 @@ async function addMediaOk(val: AppRootState['allTrack'][0]) {
       volume: 60,
       scaleInfo: {},
     };
-    const videoEl = createVideo({ appendChild: true, muted: false });
+    const videoEl = createVideo({ appendChild: true, muted: true });
     bodyAppendChildElArr.value.push(videoEl);
     videoEl.setAttribute('videoid', microphoneVideoTrack.id);
     videoEl.srcObject = event;
@@ -2135,11 +2210,13 @@ function editMediaOk(val: AppRootState['allTrack'][0]) {
 
 function handleChangeMuted(item: AppRootState['allTrack'][0]) {
   if (item.videoEl) {
-    const res = !item.videoEl.muted;
-    item.videoEl.muted = res;
-    item.videoEl.volume = res ? 0 : appStore.normalVolume / 100;
+    const res = !item.muted;
     item.volume = res ? 0 : appStore.normalVolume;
     item.muted = res;
+    if (item.type) {
+      item.videoEl.muted = res;
+      item.videoEl.volume = res ? 0 : appStore.normalVolume / 100;
+    }
     cacheStore.setResourceList(appStore.allTrack);
     handleMixedAudio();
   }
@@ -2151,7 +2228,7 @@ function handleChangeVolume(item: AppRootState['allTrack'][0], v) {
       if (item.volume !== undefined) {
         iten.volume = v;
         iten.muted = v === 0;
-        if (iten.videoEl) {
+        if (iten.videoEl && item.type) {
           iten.videoEl.volume = v / 100;
           iten.videoEl.muted = v === 0;
         }
@@ -2244,29 +2321,19 @@ function handleStartMedia(item: { type: MediaTypeEnum; txt: string }) {
       line-height: 0;
       .recording {
         position: absolute;
-        top: 4px;
-        left: 0;
-        font-size: 12px;
-        color: red;
-        display: flex;
-        align-items: center;
+        top: 5px;
+        left: 5px;
         z-index: 100;
-        font-weight: bold;
-        line-height: normal;
-        .dot {
-          width: 6px;
-          height: 6px;
-          background-color: red;
-          border-radius: 50%;
-          margin: 0 6px;
-        }
+        color: red;
+        font-size: 12px;
+        line-height: 1;
       }
       .record-ico {
         position: absolute;
         top: 0;
         left: -10px;
-        transform: translateX(-100%);
         cursor: pointer;
+        transform: translateX(-100%);
       }
 
       .add-wrap {
@@ -2375,8 +2442,8 @@ function handleStartMedia(item: { type: MediaTypeEnum; txt: string }) {
       }
       .list {
         overflow: scroll;
-        height: 218px;
         width: calc(100% + 5px);
+        height: 218px;
 
         @extend %customScrollbar;
 
@@ -2387,11 +2454,26 @@ function handleStartMedia(item: { type: MediaTypeEnum; txt: string }) {
           margin: 5px 0;
           font-size: 14px;
           cursor: pointer;
+
           user-select: none;
+          .item-left {
+            display: flex;
+            align-items: center;
+            height: 100%;
+            .control-item {
+              height: 100%;
+              line-height: 0;
+              cursor: pointer;
+              &:not(:last-child) {
+                margin-right: 6px;
+              }
+            }
+          }
           .control {
             display: flex;
             align-items: center;
             .control-item {
+              line-height: 0;
               cursor: pointer;
               &:not(:last-child) {
                 margin-right: 6px;

+ 10 - 486
test/test.json

@@ -1,487 +1,11 @@
 {
-  "entry": {
-    "main": {
-      "import": "./src/main.ts"
-    }
-  },
-  "output": {
-    "clean": true,
-    "filename": "js/[name]-[contenthash:6]-bundle.js",
-    "chunkFilename": "js/[name]-[contenthash:6]-bundle-chunk.js",
-    "path": "/Users/huangshuisheng/Desktop/hss/galaxy-s10/billd-live/dist",
-    "assetModuleFilename": "assets/[name]-[contenthash:6].[ext]",
-    "publicPath": "/"
-  },
-  "cache": {
-    "type": "memory"
-  },
-  "resolve": {
-    "extensions": [
-      ".js",
-      ".jsx",
-      ".ts",
-      ".tsx",
-      ".vue",
-      ".mjs"
-    ],
-    "alias": {
-      "@": "/Users/huangshuisheng/Desktop/hss/galaxy-s10/billd-live/src",
-      "script": "/Users/huangshuisheng/Desktop/hss/galaxy-s10/billd-live/script",
-      "vue$": "vue/dist/vue.runtime.esm-bundler.js"
-    },
-    "fallback": {}
-  },
-  "resolveLoader": {
-    "modules": [
-      "node_modules"
-    ]
-  },
-  "module": {
-    "noParse": {},
-    "rules": [
-      {
-        "test": {},
-        "use": [
-          {
-            "loader": "vue-loader"
-          }
-        ]
-      },
-      {
-        "test": {},
-        "oneOf": [
-          {
-            "resourceQuery": {},
-            "use": [
-              {
-                "loader": "vue-style-loader",
-                "options": {
-                  "sourceMap": false
-                }
-              },
-              {
-                "loader": "css-loader",
-                "options": {
-                  "importLoaders": 1,
-                  "sourceMap": false,
-                  "modules": {
-                    "localIdentName": "[name]_[local]_[hash:base64:5]"
-                  }
-                }
-              },
-              {
-                "loader": "postcss-loader",
-                "options": {
-                  "sourceMap": false
-                }
-              }
-            ]
-          },
-          {
-            "resourceQuery": {},
-            "use": [
-              {
-                "loader": "vue-style-loader",
-                "options": {
-                  "sourceMap": false
-                }
-              },
-              {
-                "loader": "css-loader",
-                "options": {
-                  "importLoaders": 1,
-                  "sourceMap": false
-                }
-              },
-              {
-                "loader": "postcss-loader",
-                "options": {
-                  "sourceMap": false
-                }
-              }
-            ]
-          },
-          {
-            "test": {},
-            "use": [
-              {
-                "loader": "vue-style-loader",
-                "options": {
-                  "sourceMap": false
-                }
-              },
-              {
-                "loader": "css-loader",
-                "options": {
-                  "importLoaders": 1,
-                  "sourceMap": false,
-                  "modules": {
-                    "localIdentName": "[name]_[local]_[hash:base64:5]"
-                  }
-                }
-              },
-              {
-                "loader": "postcss-loader",
-                "options": {
-                  "sourceMap": false
-                }
-              }
-            ]
-          },
-          {
-            "use": [
-              {
-                "loader": "vue-style-loader",
-                "options": {
-                  "sourceMap": false
-                }
-              },
-              {
-                "loader": "css-loader",
-                "options": {
-                  "importLoaders": 1,
-                  "sourceMap": false
-                }
-              },
-              {
-                "loader": "postcss-loader",
-                "options": {
-                  "sourceMap": false
-                }
-              }
-            ]
-          }
-        ],
-        "sideEffects": true
-      },
-      {
-        "test": {},
-        "oneOf": [
-          {
-            "resourceQuery": {},
-            "use": [
-              {
-                "loader": "vue-style-loader",
-                "options": {
-                  "sourceMap": false
-                }
-              },
-              {
-                "loader": "css-loader",
-                "options": {
-                  "importLoaders": 2,
-                  "sourceMap": false,
-                  "modules": {
-                    "localIdentName": "[name]_[local]_[hash:base64:5]"
-                  }
-                }
-              },
-              {
-                "loader": "postcss-loader",
-                "options": {
-                  "sourceMap": false
-                }
-              },
-              {
-                "loader": "sass-loader",
-                "options": {
-                  "sourceMap": false,
-                  "additionalData": "@use 'billd-scss/src/index.scss' as *;@import '@/assets/constant.scss';"
-                }
-              }
-            ]
-          },
-          {
-            "resourceQuery": {},
-            "use": [
-              {
-                "loader": "vue-style-loader",
-                "options": {
-                  "sourceMap": false
-                }
-              },
-              {
-                "loader": "css-loader",
-                "options": {
-                  "importLoaders": 2,
-                  "sourceMap": false
-                }
-              },
-              {
-                "loader": "postcss-loader",
-                "options": {
-                  "sourceMap": false
-                }
-              },
-              {
-                "loader": "sass-loader",
-                "options": {
-                  "sourceMap": false,
-                  "additionalData": "@use 'billd-scss/src/index.scss' as *;@import '@/assets/constant.scss';"
-                }
-              }
-            ]
-          },
-          {
-            "test": {},
-            "use": [
-              {
-                "loader": "vue-style-loader",
-                "options": {
-                  "sourceMap": false
-                }
-              },
-              {
-                "loader": "css-loader",
-                "options": {
-                  "importLoaders": 2,
-                  "sourceMap": false,
-                  "modules": {
-                    "localIdentName": "[name]_[local]_[hash:base64:5]"
-                  }
-                }
-              },
-              {
-                "loader": "postcss-loader",
-                "options": {
-                  "sourceMap": false
-                }
-              },
-              {
-                "loader": "sass-loader",
-                "options": {
-                  "sourceMap": false,
-                  "additionalData": "@use 'billd-scss/src/index.scss' as *;@import '@/assets/constant.scss';"
-                }
-              }
-            ]
-          },
-          {
-            "use": [
-              {
-                "loader": "vue-style-loader",
-                "options": {
-                  "sourceMap": false
-                }
-              },
-              {
-                "loader": "css-loader",
-                "options": {
-                  "importLoaders": 2,
-                  "sourceMap": false
-                }
-              },
-              {
-                "loader": "postcss-loader",
-                "options": {
-                  "sourceMap": false
-                }
-              },
-              {
-                "loader": "sass-loader",
-                "options": {
-                  "sourceMap": false,
-                  "additionalData": "@use 'billd-scss/src/index.scss' as *;@import '@/assets/constant.scss';"
-                }
-              }
-            ]
-          }
-        ],
-        "sideEffects": true
-      },
-      {
-        "test": {},
-        "type": "asset",
-        "generator": {
-          "filename": "img/[name]-[contenthash:6][ext]"
-        },
-        "parser": {
-          "dataUrlCondition": {
-            "maxSize": 4096
-          }
-        }
-      },
-      {
-        "test": {},
-        "type": "asset/resource",
-        "generator": {
-          "filename": "font/[name]-[contenthash:6][ext]"
-        }
-      },
-      {
-        "test": {},
-        "exclude": {},
-        "use": [
-          {
-            "loader": "swc-loader",
-            "options": {
-              "jsc": {
-                "parser": {
-                  "syntax": "typescript",
-                  "tsx": true
-                }
-              }
-            }
-          }
-        ]
-      },
-      {
-        "test": {},
-        "exclude": {},
-        "use": [
-          {
-            "loader": "swc-loader",
-            "options": {
-              "jsc": {
-                "parser": {
-                  "syntax": "ecmascript",
-                  "jsx": true
-                }
-              }
-            }
-          }
-        ]
-      }
-    ]
-  },
-  "plugins": [
-    {
-      "compilationSuccessInfo": {},
-      "shouldClearConsole": true,
-      "formatters": [
-        null,
-        null,
-        null
-      ],
-      "transformers": [
-        null,
-        null,
-        null
-      ],
-      "previousEndTimes": {}
-    },
-    {},
-    {},
-    {
-      "userOptions": {
-        "filename": "index.html",
-        "title": "billd-live",
-        "template": "/Users/huangshuisheng/Desktop/hss/galaxy-s10/billd-live/public/index.html",
-        "hash": true,
-        "minify": false,
-        "chunks": [
-          "main"
-        ]
-      },
-      "version": 5
-    },
-    {
-      "billdConfig": {
-        "pluginName": "BilldHtmlWebpackPlugin",
-        "options": {
-          "env": "webpack5"
-        },
-        "env": "webpack5",
-        "envList": [
-          "nuxt2",
-          "nuxt3",
-          "nuxt3-6",
-          "vuecli4",
-          "vuecli5",
-          "webpack4",
-          "webpack5",
-          "next12",
-          "vite4"
-        ],
-        "log": {
-          "pkgName": true,
-          "pkgVersion": true,
-          "pkgRepository": true,
-          "commitSubject": true,
-          "commitBranch": true,
-          "committerDate": true,
-          "commitHash": true,
-          "committerName": true,
-          "committerEmail": true,
-          "lastBuildDate": true
-        }
-      }
-    },
-    {
-      "patterns": [
-        {
-          "from": "public",
-          "globOptions": {
-            "ignore": [
-              "**/index.html"
-            ]
-          }
-        }
-      ],
-      "options": {}
-    },
-    {
-      "definitions": {
-        "BASE_URL": "\"/\"",
-        "process.env": {
-          "BilldHtmlWebpackPlugin": "{\"pkgName\":\"billd-live\",\"pkgVersion\":\"0.0.1\",\"pkgRepository\":\"https://github.com/galaxy-s10/billd-live.git\",\"commitSubject\":\"fix: url\",\"commitBranch\":\"master\",\"committerDate\":\"2024-02-03 13:29:10 +0800\",\"commitHash\":\"4ba7708de75fe474609e89253af9ea185d222613\",\"committerName\":\"shuisheng\",\"committerEmail\":\"2274751790@qq.com\",\"lastBuildDate\":\"2024/2/4 18:49:38\",\"nodeVersion\":\"v16.16.0\"}",
-          "NODE_ENV": "\"development\"",
-          "PUBLIC_PATH": "\"/\""
-        },
-        "__VUE_OPTIONS_API__": false,
-        "__VUE_PROD_DEVTOOLS__": false
-      }
-    },
-    {}
-  ],
-  "target": "web",
-  "mode": "development",
-  "stats": "none",
-  "devtool": "eval",
-  "infrastructureLogging": {
-    "level": "none"
-  },
-  "devServer": {
-    "client": {
-      "logging": "none"
-    },
-    "hot": true,
-    "compress": true,
-    "port": 8000,
-    "open": false,
-    "historyApiFallback": {
-      "rewrites": [
-        {
-          "from": {},
-          "to": "/"
-        }
-      ]
-    },
-    "static": {
-      "watch": true,
-      "publicPath": "/",
-      "directory": "/Users/huangshuisheng/Desktop/hss/galaxy-s10/billd-live/public"
-    },
-    "proxy": {
-      "/api": {
-        "target": "http://localhost:4300",
-        "secure": false,
-        "changeOrigin": true,
-        "pathRewrite": {
-          "^/api": ""
-        }
-      },
-      "/prodapi": {
-        "target": "https://live.hsslive.cn",
-        "secure": false,
-        "changeOrigin": true,
-        "pathRewrite": {
-          "^/prodapi": "/api/"
-        }
-      }
-    }
-  },
-  "optimization": {
-    "sideEffects": "flag"
-  }
-}
+  "url": "http://localhost:5001/livestream/roomId___4.flv?usertoken=32b617a7b7c8dd97947e9bdc945d4c65&userid=101&randomid=GJH6evlJ",
+  "hasRedirect": false,
+  "speed": 73.154296875,
+  "loaderType": "fetch-stream-loader",
+  "currentSegmentIndex": 0,
+  "totalSegmentCount": 1,
+  "playerType": "MSEPlayer",
+  "decodedFrames": 4458,
+  "droppedFrames": 2
+}