shuisheng 2 سال پیش
والد
کامیت
d98cb2c622

+ 9 - 11
src/hooks/use-pull.ts

@@ -44,7 +44,6 @@ export function usePull({
     roomLiving,
     liveRoomInfo,
     anchorInfo,
-    roomNoLive,
     localStream,
     liveUserList,
     damuList,
@@ -141,17 +140,17 @@ export function usePull({
   );
 
   watch(
-    () => localStream,
-    (stream) => {
-      if (stream.value) {
+    () => localStream.value,
+    (val) => {
+      if (val) {
         console.log('localStream变了');
-        console.log('音频轨:', stream.value?.getAudioTracks());
-        console.log('视频轨:', stream.value?.getVideoTracks());
+        console.log('音频轨:', val?.getAudioTracks());
+        console.log('视频轨:', val?.getVideoTracks());
         if (roomLiveType.value === liveTypeEnum.webrtcPull) {
           videoElArr.value.forEach((dom) => {
             dom.remove();
           });
-          stream.value?.getVideoTracks().forEach((track) => {
+          val?.getVideoTracks().forEach((track) => {
             console.log('视频轨enabled:', track.id, track.enabled);
             const video = createVideo({});
             video.setAttribute('track-id', track.id);
@@ -159,7 +158,7 @@ export function usePull({
             remoteVideo.value.push(video);
             videoElArr.value.push(video);
           });
-          stream.value?.getAudioTracks().forEach((track) => {
+          val?.getAudioTracks().forEach((track) => {
             console.log('音频轨enabled:', track.id, track.enabled);
             const video = createVideo({});
             video.setAttribute('track-id', track.id);
@@ -172,7 +171,7 @@ export function usePull({
           videoElArr.value.forEach((dom) => {
             dom.remove();
           });
-          stream.value?.getVideoTracks().forEach((track) => {
+          val?.getVideoTracks().forEach((track) => {
             console.log('视频轨enabled:', track.id, track.enabled);
             const video = createVideo({});
             video.setAttribute('track-id', track.id);
@@ -181,7 +180,7 @@ export function usePull({
             remoteVideo.value.push(video);
             videoElArr.value.push(video);
           });
-          stream.value?.getAudioTracks().forEach((track) => {
+          val?.getAudioTracks().forEach((track) => {
             console.log('音频轨enabled:', track.id, track.enabled);
             const video = createVideo({});
             video.setAttribute('track-id', track.id);
@@ -296,7 +295,6 @@ export function usePull({
     roomLiving,
     autoplayVal,
     videoLoading,
-    roomNoLive,
     damuList,
     liveUserList,
     sidebarList,

+ 17 - 68
src/hooks/use-push.ts

@@ -6,13 +6,7 @@ import {
   fetchCreateUserLiveRoom,
   fetchUserHasLiveRoom,
 } from '@/api/userLiveRoom';
-import {
-  DanmuMsgTypeEnum,
-  ILiveRoom,
-  IMessage,
-  LiveRoomTypeEnum,
-  MediaTypeEnum,
-} from '@/interface';
+import { DanmuMsgTypeEnum, ILiveRoom, IMessage } from '@/interface';
 import { WsMsgTypeEnum } from '@/network/webSocket';
 import { useAppStore } from '@/store/app';
 import { useNetworkStore } from '@/store/network';
@@ -23,8 +17,7 @@ import { loginTip } from './use-login';
 import { useSrsWs } from './use-srs-ws';
 import { useTip } from './use-tip';
 
-export function usePush({ isSRS }: { isSRS: boolean }) {
-  console.log('usePushusePush', isSRS);
+export function usePush() {
   const route = useRoute();
   const router = useRouter();
   const appStore = useAppStore();
@@ -38,45 +31,12 @@ export function usePush({ isSRS }: { isSRS: boolean }) {
   const liveRoomInfo = ref<ILiveRoom>();
   const videoElArr = ref<HTMLVideoElement[]>([]);
 
-  const allMediaTypeList: Record<string, { type: MediaTypeEnum; txt: string }> =
-    {
-      [MediaTypeEnum.camera]: {
-        type: MediaTypeEnum.camera,
-        txt: '摄像头',
-      },
-      [MediaTypeEnum.screen]: {
-        type: MediaTypeEnum.screen,
-        txt: '窗口',
-      },
-      [MediaTypeEnum.microphone]: {
-        type: MediaTypeEnum.microphone,
-        txt: '麦克风',
-      },
-      [MediaTypeEnum.txt]: {
-        type: MediaTypeEnum.txt,
-        txt: '文字',
-      },
-      [MediaTypeEnum.img]: {
-        type: MediaTypeEnum.img,
-        txt: '图片',
-      },
-      [MediaTypeEnum.media]: {
-        type: MediaTypeEnum.media,
-        txt: '视频',
-      },
-      [MediaTypeEnum.time]: {
-        type: MediaTypeEnum.time,
-        txt: '时间',
-      },
-      [MediaTypeEnum.stopwatch]: {
-        type: MediaTypeEnum.stopwatch,
-        txt: '秒表',
-      },
-    };
-
   const {
-    mySocketId,
     initSrsWs,
+    addTrack,
+    delTrack,
+    handleStartLive,
+    mySocketId,
     canvasVideoStream,
     lastCoverImg,
     localStream,
@@ -85,9 +45,6 @@ export function usePush({ isSRS }: { isSRS: boolean }) {
     currentMaxFramerate,
     currentMaxBitrate,
     currentResolutionRatio,
-    addTrack,
-    delTrack,
-    handleStartLive,
   } = useSrsWs();
 
   watch(
@@ -104,7 +61,6 @@ export function usePush({ isSRS }: { isSRS: boolean }) {
         const video = createVideo({});
         video.setAttribute('track-id', track.id);
         video.srcObject = new MediaStream([track]);
-        // localVideoRef.value?.appendChild(video);
         videoElArr.value.push(video);
       });
       stream?.getAudioTracks().forEach((track) => {
@@ -112,7 +68,6 @@ export function usePush({ isSRS }: { isSRS: boolean }) {
         const video = createVideo({});
         video.setAttribute('track-id', track.id);
         video.srcObject = new MediaStream([track]);
-        // localVideoRef.value?.appendChild(video);
         videoElArr.value.push(video);
       });
     },
@@ -192,8 +147,7 @@ export function usePush({ isSRS }: { isSRS: boolean }) {
     });
   }
 
-  async function startLive(type: LiveRoomTypeEnum) {
-    console.log('startLivestartLive', type);
+  async function startLive() {
     if (!loginTip()) return;
     const flag = await userHasLiveRoom();
     if (!flag) {
@@ -233,10 +187,7 @@ export function usePush({ isSRS }: { isSRS: boolean }) {
         msgType: WsMsgTypeEnum.roomNoLive,
       });
     }
-    setTimeout(() => {
-      closeWs();
-      closeRtc();
-    }, 500);
+    closeRtc();
   }
 
   function roomNameIsOk() {
@@ -273,14 +224,13 @@ export function usePush({ isSRS }: { isSRS: boolean }) {
       window.$message.error('还没开播,不能发送弹幕!');
       return;
     }
-    const messageData: IMessage['data'] = {
-      msg: danmuStr.value,
-      msgType: DanmuMsgTypeEnum.danmu,
-      live_room_id: Number(roomId.value),
-    };
-    instance.send({
+    instance.send<IMessage['data']>({
       msgType: WsMsgTypeEnum.message,
-      data: messageData,
+      data: {
+        msg: danmuStr.value,
+        msgType: DanmuMsgTypeEnum.danmu,
+        live_room_id: Number(roomId.value),
+      },
     });
     damuList.value.push({
       socket_id: mySocketId.value,
@@ -293,16 +243,17 @@ export function usePush({ isSRS }: { isSRS: boolean }) {
 
   return {
     confirmRoomName,
-    mySocketId,
     startLive,
     endLive,
     sendDanmu,
     keydownDanmu,
+    addTrack,
+    delTrack,
+    mySocketId,
     lastCoverImg,
     localStream,
     canvasVideoStream,
     isLiving,
-    allMediaTypeList,
     currentResolutionRatio,
     currentMaxBitrate,
     currentMaxFramerate,
@@ -311,7 +262,5 @@ export function usePush({ isSRS }: { isSRS: boolean }) {
     damuList,
     liveUserList,
     liveRoomInfo,
-    addTrack,
-    delTrack,
   };
 }

+ 38 - 1
src/hooks/use-rtc-params.ts

@@ -1,5 +1,7 @@
 import { ref } from 'vue';
 
+import { MediaTypeEnum } from '@/interface';
+
 export const useRTCParams = () => {
   const maxBitrate = ref([
     {
@@ -95,6 +97,41 @@ export const useRTCParams = () => {
       disabled: true,
     },
   ]);
+  const allMediaTypeList: Record<string, { type: MediaTypeEnum; txt: string }> =
+    {
+      [MediaTypeEnum.camera]: {
+        type: MediaTypeEnum.camera,
+        txt: '摄像头',
+      },
+      [MediaTypeEnum.screen]: {
+        type: MediaTypeEnum.screen,
+        txt: '窗口',
+      },
+      [MediaTypeEnum.microphone]: {
+        type: MediaTypeEnum.microphone,
+        txt: '麦克风',
+      },
+      [MediaTypeEnum.txt]: {
+        type: MediaTypeEnum.txt,
+        txt: '文字',
+      },
+      [MediaTypeEnum.img]: {
+        type: MediaTypeEnum.img,
+        txt: '图片',
+      },
+      [MediaTypeEnum.media]: {
+        type: MediaTypeEnum.media,
+        txt: '视频',
+      },
+      [MediaTypeEnum.time]: {
+        type: MediaTypeEnum.time,
+        txt: '时间',
+      },
+      [MediaTypeEnum.stopwatch]: {
+        type: MediaTypeEnum.stopwatch,
+        txt: '秒表',
+      },
+    };
 
-  return { maxBitrate, maxFramerate, resolutionRatio };
+  return { maxBitrate, maxFramerate, resolutionRatio, allMediaTypeList };
 };

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

@@ -0,0 +1,495 @@
+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,
+  };
+};

+ 7 - 11
src/hooks/use-srs-ws.ts

@@ -33,7 +33,6 @@ import {
 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';
 
@@ -41,18 +40,18 @@ export const useSrsWs = () => {
   const appStore = useAppStore();
   const userStore = useUserStore();
   const networkStore = useNetworkStore();
+  const { maxBitrate, maxFramerate, resolutionRatio } = useRTCParams();
+
   const loopHeartbeatTimer = ref();
   const liveUserList = ref<ILiveUser[]>([]);
   const roomId = ref('');
-  const roomNoLive = ref(false);
   const roomLiving = ref(false);
+  const isAnchor = 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);
@@ -164,13 +163,14 @@ export const useSrsWs = () => {
           ?.getSenders()
           .find((sender) => sender.track?.id === addTrackInfo.track?.id);
         if (!sender) {
-          console.log('pc添加track-开播后中途添加', addTrackInfo.track?.id);
+          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!;
         }
       });
     }
@@ -314,7 +314,6 @@ export const useSrsWs = () => {
     });
     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!);
@@ -351,13 +350,11 @@ export const useSrsWs = () => {
     ws.socketIo.on(WsMsgTypeEnum.roomLiving, (data: WsRoomLivingType) => {
       prettierReceiveWsMsg(WsMsgTypeEnum.roomLiving, data);
       roomLiving.value = true;
-      roomNoLive.value = false;
     });
 
     // 主播不在直播
     ws.socketIo.on(WsMsgTypeEnum.roomNoLive, (data) => {
       prettierReceiveWsMsg(WsMsgTypeEnum.roomNoLive, data);
-      roomNoLive.value = true;
       roomLiving.value = false;
     });
 
@@ -488,7 +485,6 @@ export const useSrsWs = () => {
     roomLiving,
     liveRoomInfo,
     anchorInfo,
-    roomNoLive,
     localStream,
     liveUserList,
     damuList,

+ 1 - 1
src/network/webRTC.ts

@@ -363,7 +363,7 @@ export class WebRTCClass {
         const roomId = this.roomId.split('___')[0];
         const receiver = this.roomId.split('___')[1];
         networkStore.wsMap.get(roomId)?.send<WsCandidateType['data']>({
-          msgType: WsMsgTypeEnum.srsCandidate,
+          msgType: WsMsgTypeEnum.candidate,
           data: {
             candidate: event.candidate,
             sender: networkStore.wsMap.get(roomId)?.socketIo?.id || '',

+ 3 - 6
src/network/webSocket.ts

@@ -47,12 +47,9 @@ export enum WsMsgTypeEnum {
   heartbeat = 'heartbeat',
   startLive = 'startLive',
 
-  srsOffer = 'srsOffer',
-  srsAnswer = 'srsAnswer',
-  srsCandidate = 'srsCandidate',
-  webrtcOffer = 'webrtcOffer',
-  webrtcAnswer = 'webrtcAnswer',
-  webrtcCandidate = 'webrtcCandidate',
+  offer = 'offer',
+  answer = 'answer',
+  candidate = 'candidate',
 }
 
 export function prettierReceiveWsMsg(...arg) {

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

@@ -30,7 +30,7 @@
       >
         <div
           class="no-live"
-          v-if="roomNoLive"
+          v-if="!roomLiving"
         >
           当前房间没在直播~
         </div>
@@ -229,7 +229,7 @@ const {
   addVideo,
   videoLoading,
   remoteVideo,
-  roomNoLive,
+  roomLiving,
   damuList,
   liveUserList,
   danmuStr,

+ 0 - 744
src/views/push-old/index.vue

@@ -1,744 +0,0 @@
-<template>
-  <div class="push-wrap">
-    <div
-      ref="topRef"
-      class="left"
-    >
-      <div
-        ref="containerRef"
-        class="container"
-      >
-        <div class="video-wrap">
-          <div
-            ref="localVideoRef"
-            class="media-list"
-            :class="{ item: appStore.allTrack.length > 1 }"
-          ></div>
-          <div
-            v-if="!appStore.allTrack || appStore.allTrack.length <= 0"
-            class="add-wrap"
-          >
-            <n-space>
-              <n-button
-                v-for="(item, index) in allMediaTypeList"
-                :key="index"
-                class="item"
-                @click="handleStartMedia(item)"
-              >
-                {{ item.txt }}
-              </n-button>
-            </n-space>
-          </div>
-        </div>
-
-        <div class="sidebar">
-          <div class="title">在线人员</div>
-          <div
-            v-for="(item, index) in liveUserList.filter(
-              (item) => item.id !== getSocketId()
-            )"
-            :key="index"
-            class="item"
-          >
-            <video
-              :ref="(el) => (remoteVideoRef[item.id] = el)"
-              autoplay
-              webkit-playsinline="true"
-              playsinline
-              x-webkit-airplay="allow"
-              x5-video-player-type="h5"
-              x5-video-player-fullscreen="true"
-              x5-video-orientation="portraint"
-              muted
-            ></video>
-            <div>{{ item.userInfo?.username || item.id }}</div>
-          </div>
-        </div>
-      </div>
-
-      <div
-        ref="bottomRef"
-        class="room-control"
-      >
-        <div class="info">
-          <div
-            class="avatar"
-            :style="{ backgroundImage: `url(${userStore.userInfo?.avatar})` }"
-          ></div>
-          <div class="detail">
-            <div class="top">
-              <n-input-group>
-                <n-input
-                  v-model:value="roomName"
-                  size="small"
-                  placeholder="输入房间名"
-                  :style="{ width: '50%' }"
-                />
-                <n-button
-                  size="small"
-                  type="primary"
-                  @click="confirmRoomName"
-                >
-                  确定
-                </n-button>
-              </n-input-group>
-            </div>
-            <div class="bottom">
-              <span v-if="NODE_ENV === 'development'">
-                socketId:{{ getSocketId() }}
-              </span>
-            </div>
-          </div>
-        </div>
-        <div class="rtc">
-          <div class="item">
-            <div class="txt">码率设置</div>
-            <div class="down">
-              <n-select
-                v-model:value="currentMaxBitrate"
-                :options="maxBitrate"
-              />
-            </div>
-          </div>
-          <div class="item">
-            <div class="txt">帧率设置</div>
-            <div class="down">
-              <n-select
-                v-model:value="currentMaxFramerate"
-                :options="maxFramerate"
-              />
-            </div>
-          </div>
-          <div class="item">
-            <div class="txt">分辨率设置</div>
-            <div class="down">
-              <n-select
-                v-model:value="currentResolutionRatio"
-                :options="resolutionRatio"
-              />
-            </div>
-          </div>
-        </div>
-        <div class="other">
-          <div class="top">
-            <span class="item">
-              <i class="ico"></i>
-              <span>
-                正在观看:
-                {{
-                  liveUserList.filter((item) => item.id !== getSocketId())
-                    .length
-                }}
-              </span>
-            </span>
-          </div>
-          <div class="bottom">
-            <n-button
-              v-if="!isLiving"
-              type="info"
-              size="small"
-              @click="startLive"
-            >
-              开始直播
-            </n-button>
-            <n-button
-              v-else
-              type="error"
-              size="small"
-              @click="endLive"
-            >
-              结束直播
-            </n-button>
-          </div>
-        </div>
-      </div>
-    </div>
-
-    <div class="right">
-      <div class="resource-card">
-        <div class="title">素材列表</div>
-        <div class="list">
-          <div
-            v-for="(item, index) in appStore.allTrack"
-            :key="index"
-            class="item"
-          >
-            <span class="name">
-              ({{ item.audio === 1 ? '音频' : '视频' }}){{ item.mediaName }}
-            </span>
-            <div
-              class="del"
-              @click="handleDelTrack(item)"
-            >
-              x
-            </div>
-          </div>
-        </div>
-        <div class="bottom">
-          <n-button
-            size="small"
-            type="primary"
-            @click="showSelectMediaModalCpt = true"
-          >
-            添加素材
-          </n-button>
-        </div>
-      </div>
-      <div class="danmu-card">
-        <div class="title">弹幕互动</div>
-        <div class="list-wrap">
-          <div
-            ref="danmuListRef"
-            class="list"
-          >
-            <div
-              v-for="(item, index) in damuList"
-              :key="index"
-              class="item"
-            >
-              <template v-if="item.msgType === DanmuMsgTypeEnum.danmu">
-                <span class="name">
-                  {{ item.userInfo?.username || item.socket_id }}:
-                </span>
-                <span class="msg">{{ item.msg }}</span>
-              </template>
-              <template v-else-if="item.msgType === DanmuMsgTypeEnum.otherJoin">
-                <span class="name system">系统通知:</span>
-                <span class="msg">
-                  <span>{{ item.userInfo?.username || item.socket_id }}</span>
-                  <span>进入直播!</span>
-                </span>
-              </template>
-              <template
-                v-else-if="item.msgType === DanmuMsgTypeEnum.userLeaved"
-              >
-                <span class="name system">系统通知:</span>
-                <span class="msg">
-                  <span>{{ item.userInfo?.username || item.socket_id }}</span>
-                  <span>离开直播!</span>
-                </span>
-              </template>
-            </div>
-          </div>
-        </div>
-        <div class="send-msg">
-          <input
-            v-model="danmuStr"
-            class="ipt"
-            @keydown="keydownDanmu"
-          />
-          <n-button
-            type="info"
-            size="small"
-            @click="sendDanmu"
-          >
-            发送
-          </n-button>
-        </div>
-      </div>
-    </div>
-
-    <SelectMediaModalCpt
-      v-if="showSelectMediaModalCpt"
-      :all-media-type-list="allMediaTypeList"
-      @close="showSelectMediaModalCpt = false"
-      @ok="selectMediaOk"
-    ></SelectMediaModalCpt>
-
-    <MediaModalCpt
-      v-if="showMediaModalCpt"
-      :media-type="currentMediaType"
-      @close="showMediaModalCpt = false"
-      @ok="addMediaOk"
-    ></MediaModalCpt>
-  </div>
-</template>
-
-<script lang="ts" setup>
-import { getRandomString } from 'billd-utils';
-import { NODE_ENV } from 'script/constant';
-import { onMounted, ref, watch } from 'vue';
-import { useRoute } from 'vue-router';
-
-import { usePush } from '@/hooks/use-push';
-import { DanmuMsgTypeEnum, MediaTypeEnum, liveTypeEnum } from '@/interface';
-import { AppRootState, useAppStore } from '@/store/app';
-import { useUserStore } from '@/store/user';
-
-import MediaModalCpt from './mediaModal/index.vue';
-import SelectMediaModalCpt from './selectMediaModal/index.vue';
-
-const route = useRoute();
-const userStore = useUserStore();
-const appStore = useAppStore();
-const currentMediaType = ref(MediaTypeEnum.camera);
-const showSelectMediaModalCpt = ref(false);
-const showMediaModalCpt = ref(false);
-const topRef = ref<HTMLDivElement>();
-const bottomRef = ref<HTMLDivElement>();
-const danmuListRef = ref<HTMLDivElement>();
-const containerRef = ref<HTMLDivElement>();
-const localVideoRef = ref<HTMLVideoElement>();
-const remoteVideoRef = ref<HTMLVideoElement[]>([]);
-const isSRS = route.query.liveType === liveTypeEnum.srsPush;
-const {
-  confirmRoomName,
-  getSocketId,
-  startLive,
-  endLive,
-  sendDanmu,
-  keydownDanmu,
-  localStream,
-  isLiving,
-  allMediaTypeList,
-  currentResolutionRatio,
-  currentMaxBitrate,
-  currentMaxFramerate,
-  resolutionRatio,
-  maxBitrate,
-  maxFramerate,
-  danmuStr,
-  roomName,
-  damuList,
-  liveUserList,
-  addTrack,
-  delTrack,
-} = usePush({
-  localVideoRef,
-  remoteVideoRef,
-  isSRS,
-});
-watch(
-  () => damuList.value.length,
-  () => {
-    setTimeout(() => {
-      if (danmuListRef.value) {
-        danmuListRef.value.scrollTop = danmuListRef.value.scrollHeight;
-      }
-    }, 0);
-  }
-);
-
-onMounted(() => {
-  if (topRef.value && bottomRef.value && containerRef.value) {
-    const res =
-      bottomRef.value.getBoundingClientRect().top -
-      topRef.value.getBoundingClientRect().top;
-    containerRef.value.style.height = `${res}px`;
-  }
-});
-
-function selectMediaOk(val: MediaTypeEnum) {
-  showMediaModalCpt.value = true;
-  showSelectMediaModalCpt.value = false;
-  currentMediaType.value = val;
-}
-
-async function addMediaOk(val: {
-  type: MediaTypeEnum;
-  deviceId: string;
-  mediaName: string;
-}) {
-  showMediaModalCpt.value = false;
-  if (val.type === MediaTypeEnum.screen) {
-    const event = await navigator.mediaDevices.getDisplayMedia({
-      video: {
-        deviceId: val.deviceId,
-        height: currentResolutionRatio.value,
-        frameRate: { max: currentMaxFramerate.value },
-      },
-      audio: true,
-    });
-    const videoTrack = {
-      id: getRandomString(8),
-      audio: 2,
-      video: 1,
-      mediaName: val.mediaName,
-      type: MediaTypeEnum.screen,
-      track: event.getVideoTracks()[0],
-      trackid: event.getVideoTracks()[0].id,
-      stream: event,
-      streamid: event.id,
-    };
-    const audio = event.getAudioTracks();
-    if (audio.length) {
-      if (
-        isSRS &&
-        appStore.allTrack.filter((item) => item.audio === 1).length >= 1
-      ) {
-        window.$message.error('srs模式最多只能有一个音频');
-        return;
-      }
-      const audioTrack = {
-        id: getRandomString(8),
-        audio: 1,
-        video: 2,
-        mediaName: val.mediaName,
-        type: MediaTypeEnum.screen,
-        track: event.getAudioTracks()[0],
-        trackid: event.getAudioTracks()[0].id,
-        stream: event,
-        streamid: event.id,
-      };
-      appStore.setAllTrack([...appStore.allTrack, videoTrack, audioTrack]);
-      addTrack(videoTrack);
-      addTrack(audioTrack);
-    } else {
-      if (
-        isSRS &&
-        appStore.allTrack.filter((item) => item.video === 1).length >= 1
-      ) {
-        window.$message.error('srs模式最多只能有一个视频');
-        return;
-      }
-      appStore.setAllTrack([...appStore.allTrack, videoTrack]);
-      addTrack(videoTrack);
-    }
-
-    console.log('获取窗口成功');
-  } else if (val.type === MediaTypeEnum.camera) {
-    const event = await navigator.mediaDevices.getUserMedia({
-      video: {
-        deviceId: val.deviceId,
-        height: currentResolutionRatio.value,
-        frameRate: { max: currentMaxFramerate.value },
-      },
-      audio: false,
-    });
-    if (
-      isSRS &&
-      appStore.allTrack.filter((item) => item.video === 1).length >= 1
-    ) {
-      window.$message.error('srs模式最多只能有一个视频');
-      return;
-    }
-    const track = {
-      id: getRandomString(8),
-      audio: 2,
-      video: 1,
-      mediaName: val.mediaName,
-      type: MediaTypeEnum.camera,
-      track: event.getVideoTracks()[0],
-      trackid: event.getVideoTracks()[0].id,
-      stream: event,
-      streamid: event.id,
-    };
-    appStore.setAllTrack([...appStore.allTrack, track]);
-    addTrack(track);
-    console.log('获取摄像头成功');
-  } else if (val.type === MediaTypeEnum.microphone) {
-    const event = await navigator.mediaDevices.getUserMedia({
-      video: false,
-      audio: { deviceId: val.deviceId },
-    });
-    if (
-      isSRS &&
-      appStore.allTrack.filter((item) => item.audio === 1).length >= 1
-    ) {
-      window.$message.error('srs模式最多只能有一个音频');
-      return;
-    }
-    const track = {
-      id: getRandomString(8),
-      audio: 1,
-      video: 2,
-      mediaName: val.mediaName,
-      type: MediaTypeEnum.microphone,
-      track: event.getAudioTracks()[0],
-      trackid: event.getAudioTracks()[0].id,
-      stream: event,
-      streamid: event.id,
-    };
-    appStore.setAllTrack([...appStore.allTrack, track]);
-    addTrack(track);
-    console.log('获取麦克风成功');
-  }
-}
-
-function handleDelTrack(item: AppRootState['allTrack'][0]) {
-  console.log('handleDelTrack', item);
-  const res = appStore.allTrack.filter((iten) => iten.id !== item.id);
-  appStore.setAllTrack(res);
-  delTrack(item);
-}
-
-function handleStartMedia(item: { type: MediaTypeEnum; txt: string }) {
-  currentMediaType.value = item.type;
-  showMediaModalCpt.value = true;
-}
-</script>
-
-<style lang="scss" scoped>
-.push-wrap {
-  margin: 20px auto 0;
-  width: $w-1275;
-  height: 700px;
-  text-align: center;
-  .left {
-    position: relative;
-    display: inline-block;
-    overflow: hidden;
-    box-sizing: border-box;
-    width: $w-1000;
-    height: 100%;
-    border-radius: 6px;
-    background-color: white;
-    color: #9499a0;
-    vertical-align: top;
-
-    .container {
-      display: flex;
-      align-items: center;
-      justify-content: space-between;
-      height: 100%;
-      background-color: #fff;
-      .video-wrap {
-        position: relative;
-        display: flex;
-        flex: 1;
-        justify-content: center;
-        height: 100%;
-        background-color: rgba($color: #000000, $alpha: 0.5);
-        .media-list {
-          :deep(video) {
-            width: 100%;
-            height: 100%;
-          }
-          :deep(canvas) {
-            width: 100%;
-            height: 100%;
-          }
-          &.item {
-            :deep(video) {
-              width: 50%;
-              height: initial !important;
-            }
-            :deep(canvas) {
-              width: 50%;
-              height: initial !important;
-            }
-          }
-        }
-
-        // #localVideo {
-        //   max-width: 100%;
-        //   max-height: 100%;
-        // }
-        .add-wrap {
-          position: absolute;
-          top: 50%;
-          left: 50%;
-          display: flex;
-          align-items: center;
-          justify-content: space-around;
-          padding: 0 20px;
-          height: 50px;
-          border-radius: 5px;
-          background-color: white;
-          transform: translate(-50%, -50%);
-        }
-      }
-      .sidebar {
-        overflow: scroll;
-        width: 130px;
-        height: 100%;
-        background-color: rgba($color: #000000, $alpha: 0.3);
-
-        @extend %hideScrollbar;
-        .title {
-          color: white;
-        }
-        .join {
-          color: white;
-          cursor: pointer;
-        }
-        video {
-          max-width: 100%;
-        }
-      }
-    }
-    .room-control {
-      position: absolute;
-      right: 0;
-      bottom: 0;
-      left: 0;
-      display: flex;
-      justify-content: space-between;
-      padding: 20px;
-      background-color: papayawhip;
-
-      .info {
-        display: flex;
-        align-items: center;
-
-        .avatar {
-          margin-right: 20px;
-          width: 64px;
-          height: 64px;
-          border-radius: 50%;
-          background-position: center center;
-          background-size: cover;
-          background-repeat: no-repeat;
-        }
-        .detail {
-          display: flex;
-          flex-direction: column;
-          flex-shrink: 0;
-          width: 200px;
-          text-align: initial;
-          .top {
-            margin-bottom: 10px;
-            color: #18191c;
-          }
-          .bottom {
-            font-size: 14px;
-          }
-        }
-      }
-      .rtc {
-        display: flex;
-        align-items: center;
-        flex: 1;
-        font-size: 14px;
-        .item {
-          display: flex;
-          align-items: center;
-          flex: 1;
-          .txt {
-            flex-shrink: 0;
-            width: 80px;
-          }
-          .down {
-            width: 90px;
-
-            user-select: none;
-          }
-        }
-      }
-      .other {
-        display: flex;
-        flex-direction: column;
-        justify-content: center;
-        font-size: 12px;
-        .top {
-        }
-        .bottom {
-          margin-top: 10px;
-        }
-      }
-    }
-  }
-  .right {
-    position: relative;
-    display: inline-block;
-    box-sizing: border-box;
-    margin-left: 10px;
-    width: $w-250;
-    height: 100%;
-    border-radius: 6px;
-    background-color: white;
-    color: #9499a0;
-
-    .resource-card {
-      position: relative;
-      box-sizing: border-box;
-      margin-bottom: 5%;
-      margin-bottom: 10px;
-      padding: 10px;
-      width: 100%;
-      height: 290px;
-      border-radius: 6px;
-      background-color: papayawhip;
-      .title {
-        text-align: initial;
-      }
-      .item {
-        display: flex;
-        align-items: center;
-        justify-content: space-between;
-        margin: 5px 0;
-        font-size: 12px;
-        &:hover {
-          .del {
-            display: block;
-          }
-        }
-        .del {
-          display: none;
-          cursor: pointer;
-        }
-      }
-      .bottom {
-        position: absolute;
-        bottom: 0;
-        left: 0;
-        padding: 10px;
-      }
-    }
-    .danmu-card {
-      box-sizing: border-box;
-      padding: 10px;
-      width: 100%;
-      height: 400px;
-      border-radius: 6px;
-      background-color: papayawhip;
-      text-align: initial;
-      .title {
-        margin-bottom: 10px;
-      }
-      .list {
-        overflow: scroll;
-        margin-bottom: 10px;
-        height: 300px;
-
-        @extend %hideScrollbar;
-
-        .item {
-          margin-bottom: 10px;
-          font-size: 12px;
-          .name {
-            color: #9499a0;
-          }
-          .msg {
-            color: #61666d;
-          }
-        }
-      }
-
-      .send-msg {
-        display: flex;
-        align-items: center;
-        box-sizing: border-box;
-        .ipt {
-          display: block;
-          box-sizing: border-box;
-          margin: 0 auto;
-          margin-right: 10px;
-          padding: 10px;
-          width: 80%;
-          height: 30px;
-          outline: none;
-          border: 1px solid hsla(0, 0%, 60%, 0.2);
-          border-radius: 4px;
-          background-color: #f1f2f3;
-          font-size: 14px;
-        }
-      }
-    }
-  }
-}
-
-// 屏幕宽度大于1500的时候
-@media screen and (min-width: $w-1500) {
-  .push-wrap {
-    width: $w-1475;
-
-    .left {
-      width: $w-1200;
-    }
-    .right {
-      width: $w-250;
-    }
-  }
-}
-</style>

+ 0 - 145
src/views/push-old/mediaModal/index.vue

@@ -1,145 +0,0 @@
-<template>
-  <div class="media-wrap">
-    <Modal
-      title="添加直播素材"
-      :mask-closable="false"
-      @close="emits('close')"
-    >
-      <div class="container">
-        <div
-          v-if="inputOptions.length"
-          class="item"
-        >
-          <div class="label">设备选择</div>
-          <div class="value">
-            <n-select
-              v-model:value="currentInput.deviceId"
-              :options="inputOptions"
-            />
-          </div>
-        </div>
-        <div class="item">
-          <div class="label">名称</div>
-          <div class="value">
-            <n-input v-model:value="mediaName" />
-          </div>
-        </div>
-      </div>
-
-      <template #footer>
-        <div class="margin-right">
-          <n-button
-            type="primary"
-            @click="handleOk"
-          >
-            确定
-          </n-button>
-        </div>
-      </template>
-    </Modal>
-  </div>
-</template>
-
-<script lang="ts" setup>
-import { onMounted, ref } from 'vue';
-
-import { MediaTypeEnum } from '@/interface';
-import { useAppStore } from '@/store/app';
-
-const mediaName = ref('');
-const appStore = useAppStore();
-
-const props = withDefaults(
-  defineProps<{
-    mediaType?: MediaTypeEnum;
-  }>(),
-  {
-    mediaType: MediaTypeEnum.camera,
-  }
-);
-const emits = defineEmits(['close', 'ok']);
-
-const inputOptions = ref<{ label: string; value: string }[]>([]);
-const currentInput = ref<{
-  type: MediaTypeEnum;
-  deviceId: string;
-}>({
-  type: MediaTypeEnum.camera,
-  deviceId: '',
-});
-
-onMounted(() => {
-  init();
-});
-
-function handleOk() {
-  emits('ok', { ...currentInput.value, mediaName: mediaName.value });
-}
-
-async function init() {
-  const res = await navigator.mediaDevices.enumerateDevices();
-  if (props.mediaType === MediaTypeEnum.microphone) {
-    res.forEach((item) => {
-      if (item.kind === 'audioinput' && item.deviceId !== 'default') {
-        inputOptions.value.push({
-          label: item.label,
-          value: item.deviceId,
-        });
-      }
-    });
-    currentInput.value = {
-      ...currentInput.value,
-      deviceId: inputOptions.value[0].value,
-      type: MediaTypeEnum.microphone,
-    };
-    mediaName.value = `麦克风-${
-      appStore.allTrack.filter((item) => item.type === MediaTypeEnum.microphone)
-        .length + 1
-    }`;
-  } else if (props.mediaType === MediaTypeEnum.camera) {
-    res.forEach((item) => {
-      if (item.kind === 'videoinput' && item.deviceId !== 'default') {
-        inputOptions.value.push({
-          label: item.label,
-          value: item.deviceId,
-        });
-      }
-    });
-    currentInput.value = {
-      ...currentInput.value,
-      deviceId: inputOptions.value[0].value,
-      type: MediaTypeEnum.camera,
-    };
-    mediaName.value = `摄像头-${
-      appStore.allTrack.filter((item) => item.type === MediaTypeEnum.camera)
-        .length + 1
-    }`;
-  } else if (props.mediaType === MediaTypeEnum.screen) {
-    currentInput.value = {
-      ...currentInput.value,
-      type: MediaTypeEnum.screen,
-    };
-    mediaName.value = `窗口-${
-      appStore.allTrack.filter((item) => item.type === MediaTypeEnum.screen)
-        .length + 1
-    }`;
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-.media-wrap {
-  text-align: initial;
-
-  .container {
-    .item {
-      .label {
-        margin: 6px 0;
-      }
-    }
-    .margin-right {
-      text-align: right;
-    }
-  }
-}
-</style>

+ 0 - 54
src/views/push-old/selectMediaModal/index.vue

@@ -1,54 +0,0 @@
-<template>
-  <div class="select-media-wrap">
-    <Modal
-      title="选择直播素材"
-      :mask-closable="false"
-      @close="emits('close')"
-    >
-      <div class="container">
-        <n-space justify="center">
-          <n-button
-            v-for="(item, index) in allMediaTypeList"
-            :key="index"
-            class="item"
-            @click="emits('ok', item.type)"
-          >
-            {{ item.txt }}
-          </n-button>
-        </n-space>
-      </div>
-      <template #footer></template>
-    </Modal>
-  </div>
-</template>
-
-<script lang="ts" setup>
-import { onMounted } from 'vue';
-
-import { MediaTypeEnum } from '@/interface';
-
-const props = withDefaults(
-  defineProps<{
-    allMediaTypeList: {
-      [index: string]: {
-        type: MediaTypeEnum;
-        txt: string;
-      };
-    };
-  }>(),
-  {}
-);
-const emits = defineEmits(['close', 'ok']);
-
-onMounted(() => {});
-</script>
-
-<style lang="scss" scoped>
-.select-media-wrap {
-  text-align: initial;
-
-  .container {
-    padding-top: 10px;
-  }
-}
-</style>

+ 9 - 1704
src/views/push/index.vue

@@ -1,1716 +1,21 @@
 <template>
-  <div class="push-wrap">
-    <div
-      ref="topRef"
-      class="left"
-    >
-      <div
-        ref="containerRef"
-        class="container"
-      >
-        <canvas
-          id="pushCanvasRef"
-          ref="pushCanvasRef"
-        ></canvas>
-        <div
-          v-if="appStore.allTrack.filter((item) => !item.hidden).length <= 0"
-          class="add-wrap"
-        >
-          <n-space>
-            <n-button
-              v-for="(item, index) in allMediaTypeList"
-              :key="index"
-              class="item"
-              @click="handleStartMedia(item)"
-            >
-              {{ item.txt }}
-            </n-button>
-          </n-space>
-        </div>
-      </div>
-
-      <div
-        ref="bottomRef"
-        class="room-control"
-      >
-        <div class="info">
-          <div
-            class="avatar"
-            :style="{ backgroundImage: `url(${userStore.userInfo?.avatar})` }"
-          ></div>
-          <div class="detail">
-            <div class="top">
-              <n-input-group>
-                <n-input
-                  v-model:value="roomName"
-                  size="small"
-                  placeholder="输入房间名"
-                  :style="{ width: '50%' }"
-                />
-                <n-button
-                  size="small"
-                  type="primary"
-                  @click="confirmRoomName"
-                >
-                  确定
-                </n-button>
-              </n-input-group>
-            </div>
-            <div class="bottom">
-              <span v-if="NODE_ENV === 'development'">
-                {{ mySocketId }}
-              </span>
-            </div>
-          </div>
-        </div>
-        <div class="rtc">
-          <div class="item">
-            <div class="txt">码率设置</div>
-            <div class="down">
-              <n-select
-                v-model:value="currentMaxBitrate"
-                :options="maxBitrate"
-              />
-            </div>
-          </div>
-          <div class="item">
-            <div class="txt">帧率设置</div>
-            <div class="down">
-              <n-select
-                v-model:value="currentMaxFramerate"
-                :options="maxFramerate"
-              />
-            </div>
-          </div>
-          <div class="item">
-            <div class="txt">分辨率设置</div>
-            <div class="down">
-              <n-select
-                v-model:value="currentResolutionRatio"
-                :options="resolutionRatio"
-              />
-            </div>
-          </div>
-        </div>
-        <div class="other">
-          <div class="top">
-            <span class="item">
-              <i class="ico"></i>
-              <span>
-                正在观看:
-                {{
-                  liveUserList.filter((item) => item.id !== mySocketId).length
-                }}
-              </span>
-            </span>
-          </div>
-          <div class="bottom">
-            <n-button
-              v-if="!isLiving"
-              type="info"
-              size="small"
-              @click="handleStartLive"
-            >
-              开始直播
-            </n-button>
-            <n-button
-              v-else
-              type="error"
-              size="small"
-              @click="endLive"
-            >
-              结束直播
-            </n-button>
-          </div>
-        </div>
-      </div>
-    </div>
-
-    <div class="right">
-      <div class="resource-card">
-        <div class="title">素材列表</div>
-        <div class="list">
-          <div
-            v-for="(item, index) in appStore.allTrack.filter(
-              (item) => !item.hidden
-            )"
-            :key="index"
-            class="item"
-          >
-            <span class="name">
-              ({{ mediaTypeEnumMap[item.type] }}){{ item.mediaName }}
-            </span>
-            <div class="control">
-              <div
-                v-if="item.audio === 1"
-                class="control-item"
-                @click="handleChangeMuted(item)"
-              >
-                <n-icon size="16">
-                  <VolumeMuteOutline v-if="item.muted"></VolumeMuteOutline>
-                  <VolumeHighOutline v-else></VolumeHighOutline>
-                </n-icon>
-              </div>
-              <div
-                class="control-item"
-                @click="handleEdit(item)"
-              >
-                <n-icon size="16">
-                  <CreateOutline></CreateOutline>
-                </n-icon>
-              </div>
-              <div
-                class="control-item"
-                @click="handleDel(item)"
-              >
-                <n-icon size="16">
-                  <Close></Close>
-                </n-icon>
-              </div>
-            </div>
-          </div>
-        </div>
-        <div class="bottom">
-          <n-button
-            size="small"
-            type="primary"
-            @click="showSelectMediaModalCpt = true"
-          >
-            添加素材
-          </n-button>
-        </div>
-      </div>
-      <div class="danmu-card">
-        <div class="title">弹幕互动</div>
-        <div class="list-wrap">
-          <div
-            ref="danmuListRef"
-            class="list"
-          >
-            <div
-              v-for="(item, index) in damuList"
-              :key="index"
-              class="item"
-            >
-              <template v-if="item.msgType === DanmuMsgTypeEnum.danmu">
-                <span class="name">
-                  {{ item.userInfo?.username || item.socket_id }}:
-                </span>
-                <span class="msg">{{ item.msg }}</span>
-              </template>
-              <template v-else-if="item.msgType === DanmuMsgTypeEnum.otherJoin">
-                <span class="name system">系统通知:</span>
-                <span class="msg">
-                  <span>{{ item.userInfo?.username || item.socket_id }}</span>
-                  <span>进入直播!</span>
-                </span>
-              </template>
-              <template
-                v-else-if="item.msgType === DanmuMsgTypeEnum.userLeaved"
-              >
-                <span class="name system">系统通知:</span>
-                <span class="msg">
-                  <span>{{ item.userInfo?.username || item.socket_id }}</span>
-                  <span>离开直播!</span>
-                </span>
-              </template>
-            </div>
-          </div>
-        </div>
-        <div class="send-msg">
-          <input
-            v-model="danmuStr"
-            class="ipt"
-            @keydown="keydownDanmu"
-          />
-          <n-button
-            type="info"
-            size="small"
-            @click="sendDanmu"
-          >
-            发送
-          </n-button>
-        </div>
-      </div>
-    </div>
-
-    <SelectMediaModalCpt
-      v-if="showSelectMediaModalCpt"
-      :all-media-type-list="allMediaTypeList"
-      @close="showSelectMediaModalCpt = false"
-      @ok="selectMediaOk"
-    ></SelectMediaModalCpt>
-
-    <MediaModalCpt
-      v-if="showMediaModalCpt"
-      :media-type="currentMediaType"
-      @close="showMediaModalCpt = false"
-      @ok="addMediaOk"
-    ></MediaModalCpt>
-    <OpenMicophoneTipCpt
-      v-if="showOpenMicophoneTipCpt"
-      @close="showOpenMicophoneTipCpt = false"
-    ></OpenMicophoneTipCpt>
+  <div>
+    <SrsCpt v-if="route.query.liveType === liveTypeEnum.srsPush"></SrsCpt>
+    <RtcCpt
+      v-else-if="route.query.liveType === liveTypeEnum.webrtcPush"
+    ></RtcCpt>
   </div>
 </template>
 
 <script lang="ts" setup>
-import {
-  Close,
-  CreateOutline,
-  VolumeHighOutline,
-  VolumeMuteOutline,
-} from '@vicons/ionicons5';
-import { fabric } from 'fabric';
-import { UploadFileInfo } from 'naive-ui';
-import {
-  Raw,
-  markRaw,
-  onMounted,
-  onUnmounted,
-  reactive,
-  ref,
-  watch,
-} from 'vue';
 import { useRoute } from 'vue-router';
-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 { AppRootState, useAppStore } from '@/store/app';
-import { useResourceCacheStore } from '@/store/cache';
-import { useUserStore } from '@/store/user';
-import {
-  createVideo,
-  generateBase64,
-  getRandomEnglishString,
-  readFile,
-  saveFile,
-} from '@/utils';
-import { NODE_ENV } from 'script/constant';
+import { liveTypeEnum } from '@/interface';
 
-import MediaModalCpt from './mediaModal/index.vue';
-import OpenMicophoneTipCpt from './openMicophoneTip/index.vue';
-import SelectMediaModalCpt from './selectMediaModal/index.vue';
+import RtcCpt from './rtc/index.vue';
+import SrsCpt from './srs/index.vue';
 
 const route = useRoute();
-const userStore = useUserStore();
-const appStore = useAppStore();
-const resourceCacheStore = useResourceCacheStore();
-const currentMediaType = ref(MediaTypeEnum.camera);
-const showOpenMicophoneTipCpt = ref(false);
-const showSelectMediaModalCpt = ref(false);
-const showMediaModalCpt = ref(false);
-const topRef = ref<HTMLDivElement>();
-const bottomRef = ref<HTMLDivElement>();
-const danmuListRef = ref<HTMLDivElement>();
-const containerRef = ref<HTMLDivElement>();
-const pushCanvasRef = ref<HTMLCanvasElement>();
-const fabricCanvas = ref<fabric.Canvas>();
-const audioCtx = ref<AudioContext>();
-const timeCanvasDom = ref<Raw<fabric.Text>[]>([]);
-const stopwatchCanvasDom = ref<Raw<fabric.Text>[]>([]);
-const isSRS = route.query.liveType === liveTypeEnum.srsPush;
-console.log(route.query.liveType, liveTypeEnum.srsPush, 22222121);
-const wrapSize = reactive({
-  width: 0,
-  height: 0,
-});
-const workerTimerId = ref(-1);
-const videoRatio = ref(16 / 9);
-const { maxBitrate, maxFramerate, resolutionRatio } = useRTCParams();
-
-const {
-  confirmRoomName,
-  mySocketId,
-  startLive,
-  endLive,
-  sendDanmu,
-  keydownDanmu,
-  canvasVideoStream,
-  lastCoverImg,
-  isLiving,
-  allMediaTypeList,
-  currentResolutionRatio,
-  currentMaxBitrate,
-  currentMaxFramerate,
-  danmuStr,
-  roomName,
-  damuList,
-  liveUserList,
-  addTrack,
-  delTrack,
-} = usePush({
-  isSRS,
-});
-
-const bodyAppendChildElArr = ref<HTMLElement[]>([]);
-
-watch(
-  () => damuList.value.length,
-  () => {
-    setTimeout(() => {
-      if (danmuListRef.value) {
-        danmuListRef.value.scrollTop = danmuListRef.value.scrollHeight;
-      }
-    }, 0);
-  }
-);
-
-onMounted(() => {
-  setTimeout(() => {
-    scrollTo(0, 0);
-  }, 100);
-  initUserMedia();
-  initCanvas();
-  handleCache();
-});
-
-onUnmounted(() => {
-  bodyAppendChildElArr.value.forEach((el) => {
-    el.remove();
-  });
-  clearFrame();
-});
-
-function initUserMedia() {
-  navigator.mediaDevices
-    .getUserMedia({
-      video: true,
-      audio: false,
-    })
-    .then(() => {
-      console.log('初始化获取摄像头成功');
-    })
-    .catch(() => {
-      console.log('初始化获取摄像头失败');
-    })
-    .finally(() => {
-      navigator.mediaDevices
-        .getUserMedia({
-          video: false,
-          audio: true,
-        })
-        .then(() => {
-          console.log('初始化获取麦克风成功');
-        })
-        .catch(() => {
-          console.log('初始化获取麦克风失败');
-          showOpenMicophoneTipCpt.value = true;
-        });
-    });
-}
-
-function formatDownTime(endTime: number, startTime: number) {
-  const times = (endTime - 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(endTime).getMilliseconds();
-
-  if (h < 10) {
-    // @ts-ignore
-    h = `0${h}`;
-  }
-  if (m < 10) {
-    // @ts-ignore
-    m = `0${m}`;
-  }
-  if (s < 10) {
-    // @ts-ignore
-    s = `0${s}`;
-  }
-  if (Number(ms) < 100) {
-    if (ms < 10) {
-      // @ts-ignore
-      ms = `00${ms}`;
-    } else {
-      // @ts-ignore
-      ms = `0${ms}`;
-    }
-  }
-  if (d > 0) {
-    return `${d}:${h}:${m}:${s}.${ms}`;
-  } else if (h > 0) {
-    return `${h}:${m}:${s}.${ms}`;
-  } else {
-    return `${m}:${s}.${ms}`;
-  }
-}
-
-function renderAll() {
-  timeCanvasDom.value.forEach((item) => {
-    item.text = new Date().toLocaleString();
-  });
-  stopwatchCanvasDom.value.forEach((item) => {
-    item.text = formatDownTime(+new Date(), +new Date());
-  });
-  fabricCanvas.value?.renderAll();
-}
-
-function clearFrame() {
-  if (workerTimerId.value !== -1) {
-    workerTimers.clearInterval(workerTimerId.value);
-  }
-}
-
-function renderFrame() {
-  const delay = 1000 / 60; // 16.666666666666668
-  workerTimerId.value = workerTimers.setInterval(() => {
-    renderAll();
-  }, delay);
-  console.log('workerTimerId.value', workerTimerId.value);
-}
-
-// 处理空音频轨
-function initNullAudio() {
-  console.warn('处理空音频轨');
-  // 创建一个AudioContext实例
-  const audioContext = new AudioContext();
-
-  // 创建一个GainNode实例来控制音频的音量
-  const gainNode = audioContext.createGain();
-
-  // 创建一个空的音频缓存
-  const buffer = audioContext.createBuffer(
-    2,
-    audioContext.sampleRate * 3,
-    audioContext.sampleRate
-  );
-
-  // 创建一个用于播放音频的AudioBufferSourceNode
-  const source = audioContext.createBufferSource();
-  source.buffer = buffer;
-
-  // 将源连接到gain node,再连接到输出
-  source.connect(gainNode);
-  gainNode.connect(audioContext.destination);
-  const destination = audioContext.createMediaStreamDestination();
-
-  const webAudioTrack: AppRootState['allTrack'][0] = {
-    id: getRandomEnglishString(8),
-    audio: 1,
-    video: 2,
-    mediaName: 'webAudio占位',
-    type: MediaTypeEnum.webAudio,
-    track: destination.stream.getAudioTracks()[0],
-    trackid: destination.stream.getAudioTracks()[0].id,
-    stream: destination.stream,
-    streamid: destination.stream.id,
-    hidden: true,
-    muted: false,
-  };
-  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);
-}
-
-let streamTmp: MediaStream;
-let vel;
-
-function handleMixedAudio() {
-  console.log('handleMixedAudiohandleMixedAudio');
-  const allAudioTrack = appStore.allTrack.filter((item) => item.audio === 1);
-  if (audioCtx.value) {
-    const gainNode = audioCtx.value.createGain();
-    allAudioTrack.forEach((item) => {
-      if (!audioCtx.value || !item.stream) return;
-      const audioInput = audioCtx.value.createMediaStreamSource(item.stream);
-      audioInput.connect(gainNode);
-      console.log('混流', item.stream?.id, item.stream);
-    });
-    if (streamTmp) {
-      const destination = audioCtx.value.createMediaStreamDestination();
-      streamTmp.addTrack(destination.stream.getAudioTracks()[0]);
-      gainNode.connect(destination);
-      const mixedStream = new MediaStream();
-      mixedStream.addTrack(destination.stream.getAudioTracks()[0]);
-      mixedStream.addTrack(canvasVideoStream.value!.getVideoTracks()[0]);
-      canvasVideoStream.value = mixedStream;
-      return;
-    }
-    const destination = audioCtx.value.createMediaStreamDestination();
-    streamTmp = destination.stream;
-    // @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.srcObject = destination.stream;
-    document.body.appendChild(vel);
-    bodyAppendChildElArr.value.push(vel);
-  }
-}
-
-function handleStartLive() {
-  // WARN 不能省略initNullAudio,否则开播时候没有音频的时候,srs那边的audio是 Stream #0:0: Audio: aac, 44100 Hz, stereo, 128 kb/s
-  // 会导致加载直播很慢,正常的audio应该是Stream #0:0: Audio: aac (LC), 48000 Hz, stereo, fltp
-  // 开播前执行initNullAudio,audio就会是正常的
-  initNullAudio();
-  if (!audioCtx.value) {
-    audioCtx.value = new AudioContext();
-  }
-  handleMixedAudio();
-  lastCoverImg.value = generateBase64(pushCanvasRef.value!);
-  startLive(isSRS ? LiveRoomTypeEnum.user_srs : LiveRoomTypeEnum.user_wertc);
-}
-
-function handleScale({ width, height }: { width: number; height: number }) {
-  const resolutionHeight =
-    currentResolutionRatio.value * window.devicePixelRatio;
-  const resolutionWidth =
-    currentResolutionRatio.value * window.devicePixelRatio * videoRatio.value;
-  console.log(
-    '当前分辨率',
-    { resolutionWidth, resolutionHeight },
-    { width, height },
-    { devicePixelRatio, currentResolutionRatio: currentResolutionRatio.value }
-  );
-  let ratio = 1;
-  if (width > resolutionWidth) {
-    const r1 = resolutionWidth / width;
-    ratio = r1;
-  }
-  if (height > resolutionHeight) {
-    const r1 = resolutionHeight / height;
-    if (ratio > r1) {
-      ratio = r1;
-    }
-  }
-  console.log({ ratio });
-  // if (width > wrapSize.width) {
-  //   const r1 = wrapSize.width / width;
-  //   ratio = r1;
-  // }
-  // if (height > wrapSize.height) {
-  //   const r1 = wrapSize.height / height;
-  //   if (ratio > r1) {
-  //     ratio = r1;
-  //   }
-  // }
-
-  return ratio;
-}
-
-function autoCreateVideo({
-  stream,
-  id,
-  rect,
-  muted,
-}: {
-  stream: MediaStream;
-  id: string;
-  rect?: { left: number; top: number };
-  muted?: boolean;
-}) {
-  console.warn('autoCreateVideoautoCreateVideo', id);
-  const videoEl = createVideo({});
-  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;
-    videoEl: HTMLVideoElement;
-    scale: number;
-  }>((resolve) => {
-    videoEl.onloadedmetadata = () => {
-      const width = stream.getVideoTracks()[0].getSettings().width!;
-      const height = stream.getVideoTracks()[0].getSettings().height!;
-      const ratio = handleScale({ width, height });
-      videoEl.width = width;
-      videoEl.height = height;
-
-      const canvasDom = markRaw(
-        new fabric.Image(videoEl, {
-          top: rect?.top || 0,
-          left: rect?.left || 0,
-          width,
-          height,
-        })
-      );
-      console.log(
-        '初始化',
-        ratio,
-        canvasDom.width,
-        canvasDom.height,
-        width * ratio,
-        height * ratio,
-        canvasDom
-      );
-      handleMoving({ canvasDom, id });
-      handleScaling({ canvasDom, id });
-      canvasDom.scale(ratio / window.devicePixelRatio);
-      // canvasDom.scaleToWidth((width * ratio) / window.devicePixelRatio);
-      // canvasDom.scaleToHeight((height * ratio) / window.devicePixelRatio);
-      fabricCanvas.value!.add(canvasDom);
-
-      resolve({ canvasDom, scale: ratio, videoEl });
-    };
-  });
-}
-
-watch(
-  () => currentResolutionRatio.value,
-  (newHeight, oldHeight) => {
-    changeCanvasAttr({ newHeight, oldHeight });
-  }
-);
-
-// 容器宽高,1280*720,即720p
-// canvas容器宽高,2560*1440,即1440p
-
-// ======
-// 容器宽高,960*540,即540p
-// dom宽高,640*480
-// canvas容器宽高,960*540,即540p
-// 将dom绘制到容器里,此时dom的大小就是640*480
-// 需求,不管切换多少分辨率,我要看到的dom都是一样大小,即
-// 960*540时,dom是640*480
-// 1280*720时,dom不能是640*480了,因为这样他就会对比上一个分辨率的dom看起来小了,960/1280=0.75,540/720=0.75,
-// 其实就是分辨率变大了,我们就要将图片也变大,即图片的宽是640/0.75=853.4,高是480/0.75=640
-// 坐标变化,960*540时,dom坐标是100,100
-// 1280*720时,dom的坐标不能再是100,100了,否则对比上一个分辨率看起来偏
-
-function changeCanvasAttr({
-  newHeight,
-  oldHeight,
-}: {
-  newHeight: number;
-  oldHeight: number;
-}) {
-  if (fabricCanvas.value) {
-    const resolutionHeight =
-      currentResolutionRatio.value / window.devicePixelRatio;
-    const resolutionWidth =
-      (currentResolutionRatio.value / window.devicePixelRatio) *
-      videoRatio.value;
-    fabricCanvas.value.setWidth(resolutionWidth);
-    fabricCanvas.value.setHeight(resolutionHeight);
-    appStore.allTrack.forEach((iten) => {
-      console.log('当前类型', iten.type);
-      const item = iten.canvasDom;
-
-      if (item) {
-        // 分辨率变小了,将图片变小
-        if (newHeight < oldHeight) {
-          const ratio2 = oldHeight / newHeight;
-          item.left = item.left! / ratio2;
-          item.top = item.top! / ratio2;
-        } else {
-          // 分辨率变大了,将图片变大
-          const ratio2 = oldHeight / newHeight;
-          item.left = item.left! / ratio2;
-          item.top = item.top! / ratio2;
-        }
-      }
-    });
-    appStore.allTrack.forEach((iten) => {
-      console.log('当前类型', iten.type);
-      const item = iten.canvasDom;
-
-      if (item) {
-        // 分辨率变小了,将图片变小
-        if (newHeight < oldHeight) {
-          const ratio = newHeight / oldHeight;
-          const ratio1 = (item.scaleX || 1) * ratio;
-          item.scale(ratio1);
-        } else {
-          // 分辨率变大了,将图片变大
-          const ratio = newHeight / oldHeight;
-          const ratio1 = (item.scaleX || 1) * ratio;
-          item.scale(ratio1);
-        }
-      }
-    });
-
-    changeCanvasStyle();
-  }
-}
-
-function changeCanvasStyle() {
-  // @ts-ignore
-  fabricCanvas.value.wrapperEl.style.width = `${wrapSize.width}px`;
-  // @ts-ignore
-  fabricCanvas.value.wrapperEl.style.height = `${wrapSize.height}px`;
-  // @ts-ignore
-  fabricCanvas.value.lowerCanvasEl.style.width = `${wrapSize.width}px`;
-  // @ts-ignore
-  fabricCanvas.value.lowerCanvasEl.style.height = `${wrapSize.height}px`;
-  // @ts-ignore
-  fabricCanvas.value.upperCanvasEl.style.width = `${wrapSize.width}px`;
-  // @ts-ignore
-  fabricCanvas.value.upperCanvasEl.style.height = `${wrapSize.height}px`;
-}
-
-function initCanvas() {
-  const resolutionHeight =
-    currentResolutionRatio.value / window.devicePixelRatio;
-  const resolutionWidth =
-    (currentResolutionRatio.value / window.devicePixelRatio) * videoRatio.value;
-  const wrapWidth = containerRef.value!.getBoundingClientRect().width;
-  // const wrapWidth = 1920;
-  const ratio = wrapWidth / resolutionWidth;
-  const wrapHeight = resolutionHeight * ratio;
-  // const wrapHeight = 1080;
-  // lower-canvas: 实际的canvas画面,也就是pushCanvasRef
-  // upper-canvas: 操作时候的canvas
-  const ins = markRaw(new fabric.Canvas(pushCanvasRef.value!));
-  ins.setWidth(resolutionWidth);
-  ins.setHeight(resolutionHeight);
-  console.log('initCanvas', { resolutionWidth, resolutionHeight });
-  ins.setBackgroundColor('black', () => {
-    console.log('setBackgroundColor回调');
-  });
-  wrapSize.width = wrapWidth;
-  wrapSize.height = wrapHeight;
-  fabricCanvas.value = ins;
-  renderFrame();
-  changeCanvasStyle();
-}
-
-function handleScaling({ canvasDom, id }) {
-  canvasDom.on('scaling', () => {
-    appStore.allTrack.forEach((item) => {
-      if (id === item.id) {
-        item.scaleInfo = {
-          scaleX: canvasDom.scaleX || 1,
-          scaleY: canvasDom.scaleY || 1,
-        };
-      }
-    });
-    resourceCacheStore.setList(appStore.allTrack);
-  });
-}
-function handleMoving({
-  canvasDom,
-  id,
-}: {
-  canvasDom: fabric.Image | fabric.Text;
-  id: string;
-}) {
-  canvasDom.on('moving', () => {
-    console.log(
-      'moving',
-      canvasDom.width,
-      canvasDom.height,
-      canvasDom.scaleX,
-      canvasDom.scaleY
-    );
-    appStore.allTrack.forEach((item) => {
-      if (id === item.id) {
-        item.rect = {
-          top: (canvasDom.top || 0) * window.devicePixelRatio,
-          left: (canvasDom.left || 0) * window.devicePixelRatio,
-        };
-      }
-    });
-    resourceCacheStore.setList(appStore.allTrack);
-  });
-}
-
-async function handleCache() {
-  const res: AppRootState['allTrack'] = [];
-  const queue: any[] = [];
-  resourceCacheStore.list.forEach((item) => {
-    // @ts-ignore
-    const obj: AppRootState['allTrack'][0] = {};
-    obj.audio = item.audio;
-    obj.video = item.video;
-    obj.id = item.id;
-    obj.type = item.type;
-    obj.hidden = item.hidden;
-    obj.mediaName = item.mediaName;
-    obj.muted = item.muted;
-    obj.rect = item.rect;
-    obj.scaleInfo = item.scaleInfo;
-    obj.stopwatchInfo = item.stopwatchInfo;
-
-    async function handleMediaVideo() {
-      const { code, file } = await readFile(item.id);
-      if (code === 1 && file) {
-        const url = URL.createObjectURL(file);
-        const videoEl = createVideo({});
-        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 = () => {
-            const stream = videoEl
-              // @ts-ignore
-              .captureStream();
-            const width = stream.getVideoTracks()[0].getSettings().width!;
-            const height = stream.getVideoTracks()[0].getSettings().height!;
-            videoEl.width = width;
-            videoEl.height = height;
-
-            const canvasDom = markRaw(
-              new fabric.Image(videoEl, {
-                top: (item.rect?.top || 0) / window.devicePixelRatio,
-                left: (item.rect?.left || 0) / window.devicePixelRatio,
-                width,
-                height,
-              })
-            );
-            handleMoving({ canvasDom, id: item.id });
-            handleScaling({ canvasDom, id: item.id });
-            canvasDom.scale(
-              (item.scaleInfo?.scaleX || 1) / window.devicePixelRatio
-            );
-            fabricCanvas.value!.add(canvasDom);
-            obj.videoEl = videoEl;
-            obj.canvasDom = canvasDom;
-            resolve({ videoEl, canvasDom });
-          };
-        });
-        const stream = videoEl
-          // @ts-ignore
-          .captureStream() as MediaStream;
-        obj.stream = stream;
-        obj.streamid = stream.id;
-        obj.track = stream.getVideoTracks()[0];
-        obj.trackid = stream.getVideoTracks()[0].id;
-      } else {
-        console.error('读取文件失败');
-      }
-    }
-
-    async function handleImg() {
-      const { code, file } = await readFile(item.id);
-      if (code === 1 && file) {
-        const imgEl = await new Promise<HTMLImageElement>((resolve) => {
-          const reader = new FileReader();
-          reader.addEventListener(
-            'load',
-            function () {
-              const img = document.createElement('img');
-              img.src = reader.result as string;
-              img.onload = () => {
-                resolve(img);
-              };
-            },
-            false
-          );
-          if (file) {
-            reader.readAsDataURL(file);
-          }
-        });
-        if (fabricCanvas.value) {
-          const canvasDom = markRaw(
-            new fabric.Image(imgEl, {
-              top: (item.rect?.top || 0) / window.devicePixelRatio,
-              left: (item.rect?.left || 0) / window.devicePixelRatio,
-              width: imgEl.width,
-              height: imgEl.height,
-            })
-          );
-          handleMoving({ canvasDom, id: obj.id });
-          handleScaling({ canvasDom, id: obj.id });
-          canvasDom.scale(
-            (item.scaleInfo?.scaleX || 1) / window.devicePixelRatio
-          );
-          fabricCanvas.value.add(canvasDom);
-          obj.canvasDom = canvasDom;
-        }
-      } else {
-        console.error('读取文件失败');
-      }
-    }
-    if (item.type === MediaTypeEnum.media && item.video === 1) {
-      queue.push(handleMediaVideo());
-    } else if (item.type === MediaTypeEnum.img) {
-      queue.push(handleImg());
-    } else if (item.type === MediaTypeEnum.txt) {
-      obj.txtInfo = item.txtInfo;
-      obj.scaleInfo = item.scaleInfo;
-      if (fabricCanvas.value) {
-        const canvasDom = markRaw(
-          new fabric.Text(item.txtInfo?.txt || '', {
-            top: (item.rect?.top || 0) / window.devicePixelRatio,
-            left: (item.rect?.left || 0) / window.devicePixelRatio,
-            fill: item.txtInfo?.color,
-          })
-        );
-        handleMoving({ canvasDom, id: obj.id });
-        handleScaling({ canvasDom, id: obj.id });
-        // fabric.Text类型不能除以分辨率
-        canvasDom.scale(item.scaleInfo?.scaleX || 1);
-        fabricCanvas.value.add(canvasDom);
-        obj.canvasDom = canvasDom;
-      }
-    } else if (item.type === MediaTypeEnum.time) {
-      obj.timeInfo = item.timeInfo;
-      obj.scaleInfo = item.scaleInfo;
-      if (fabricCanvas.value) {
-        const canvasDom = markRaw(
-          new fabric.Text(new Date().toLocaleString(), {
-            top: (item.rect?.top || 0) / window.devicePixelRatio,
-            left: (item.rect?.left || 0) / window.devicePixelRatio,
-            fill: item.timeInfo?.color,
-          })
-        );
-        timeCanvasDom.value.push(canvasDom);
-        handleMoving({ canvasDom, id: obj.id });
-        handleScaling({ canvasDom, id: obj.id });
-        // fabric.Text类型不能除以分辨率
-        canvasDom.scale(item.scaleInfo?.scaleX || 1);
-        fabricCanvas.value.add(canvasDom);
-        obj.canvasDom = canvasDom;
-      }
-    } else if (item.type === MediaTypeEnum.stopwatch) {
-      obj.stopwatchInfo = item.stopwatchInfo;
-      obj.scaleInfo = item.scaleInfo;
-      if (fabricCanvas.value) {
-        const canvasDom = markRaw(
-          new fabric.Text('00:00:00.000', {
-            top: (item.rect?.top || 0) / window.devicePixelRatio,
-            left: (item.rect?.left || 0) / window.devicePixelRatio,
-            fill: item.stopwatchInfo?.color,
-          })
-        );
-        stopwatchCanvasDom.value.push(canvasDom);
-        handleMoving({ canvasDom, id: obj.id });
-        handleScaling({ canvasDom, id: obj.id });
-        // fabric.Text类型不能除以分辨率
-        canvasDom.scale(item.scaleInfo?.scaleX || 1);
-        fabricCanvas.value.add(canvasDom);
-        obj.canvasDom = canvasDom;
-      }
-    }
-    res.push(obj);
-  });
-  await Promise.all(queue);
-  canvasVideoStream.value = pushCanvasRef.value!.captureStream();
-  appStore.setAllTrack(res);
-}
-
-function selectMediaOk(val: MediaTypeEnum) {
-  showMediaModalCpt.value = true;
-  showSelectMediaModalCpt.value = false;
-  currentMediaType.value = val;
-}
-
-async function addMediaOk(val: {
-  type: MediaTypeEnum;
-  deviceId: string;
-  mediaName: string;
-  txtInfo?: { txt: string; color: string };
-  timeInfo?: { color: string };
-  stopwatchInfo?: { color: string };
-  imgInfo?: UploadFileInfo[];
-  mediaInfo?: UploadFileInfo[];
-}) {
-  if (!audioCtx.value) {
-    audioCtx.value = new AudioContext();
-  }
-  showMediaModalCpt.value = false;
-  if (val.type === MediaTypeEnum.screen) {
-    const event = await navigator.mediaDevices.getDisplayMedia({
-      video: {
-        deviceId: val.deviceId,
-        // displaySurface: 'monitor', // browser默认标签页;window默认窗口;monitor默认整个屏幕
-      },
-      audio: true,
-    });
-
-    const videoTrack: AppRootState['allTrack'][0] = {
-      id: getRandomEnglishString(8),
-      audio: 2,
-      video: 1,
-      mediaName: val.mediaName,
-      type: MediaTypeEnum.screen,
-      track: event.getVideoTracks()[0],
-      trackid: event.getVideoTracks()[0].id,
-      stream: event,
-      streamid: event.id,
-      hidden: false,
-      muted: false,
-    };
-
-    const { canvasDom, videoEl, scale } = await autoCreateVideo({
-      stream: event,
-      id: videoTrack.id,
-    });
-    videoTrack.scaleInfo = { scaleX: scale, scaleY: scale };
-    videoTrack.videoEl = videoEl;
-    // @ts-ignore
-    videoTrack.canvasDom = canvasDom;
-
-    const audio = event.getAudioTracks();
-    if (audio.length) {
-      videoTrack.audio = 1;
-      const audioTrack: AppRootState['allTrack'][0] = {
-        id: videoTrack.id,
-        audio: 1,
-        video: 2,
-        mediaName: val.mediaName,
-        type: MediaTypeEnum.screen,
-        track: event.getAudioTracks()[0],
-        trackid: event.getAudioTracks()[0].id,
-        stream: event,
-        streamid: event.id,
-        hidden: true,
-        muted: false,
-      };
-      const res = [...appStore.allTrack, videoTrack, audioTrack];
-      appStore.setAllTrack(res);
-      resourceCacheStore.setList(res);
-      handleMixedAudio();
-      // @ts-ignore
-      addTrack(videoTrack);
-      // @ts-ignore
-      addTrack(audioTrack);
-    } else {
-      const res = [...appStore.allTrack, videoTrack];
-      appStore.setAllTrack(res);
-      resourceCacheStore.setList(res);
-      // @ts-ignore
-      addTrack(videoTrack);
-    }
-
-    console.log('获取窗口成功');
-  } else if (val.type === MediaTypeEnum.camera) {
-    const event = await navigator.mediaDevices.getUserMedia({
-      video: {
-        deviceId: val.deviceId,
-      },
-      audio: false,
-    });
-    const videoTrack: AppRootState['allTrack'][0] = {
-      id: getRandomEnglishString(8),
-      audio: 2,
-      video: 1,
-      mediaName: val.mediaName,
-      type: MediaTypeEnum.camera,
-      track: event.getVideoTracks()[0],
-      trackid: event.getVideoTracks()[0].id,
-      stream: event,
-      streamid: event.id,
-      hidden: false,
-      muted: false,
-    };
-    const { canvasDom, videoEl, scale } = await autoCreateVideo({
-      stream: event,
-      id: videoTrack.id,
-    });
-    videoTrack.scaleInfo = { scaleX: scale, scaleY: scale };
-    videoTrack.videoEl = videoEl;
-    // @ts-ignore
-    videoTrack.canvasDom = canvasDom;
-
-    const res = [...appStore.allTrack, videoTrack];
-    appStore.setAllTrack(res);
-    resourceCacheStore.setList(res);
-    // @ts-ignore
-    addTrack(videoTrack);
-    console.log('获取摄像头成功');
-  } else if (val.type === MediaTypeEnum.microphone) {
-    const event = await navigator.mediaDevices.getUserMedia({
-      video: false,
-      audio: { deviceId: val.deviceId },
-    });
-    const audioTrack: AppRootState['allTrack'][0] = {
-      id: getRandomEnglishString(8),
-      audio: 1,
-      video: 2,
-      mediaName: val.mediaName,
-      type: MediaTypeEnum.microphone,
-      track: event.getAudioTracks()[0],
-      trackid: event.getAudioTracks()[0].id,
-      stream: event,
-      streamid: event.id,
-      hidden: false,
-      muted: false,
-    };
-    const res = [...appStore.allTrack, audioTrack];
-    appStore.setAllTrack(res);
-    resourceCacheStore.setList(res);
-    handleMixedAudio();
-    // @ts-ignore
-    addTrack(audioTrack);
-
-    console.log('获取麦克风成功');
-  } else if (val.type === MediaTypeEnum.txt) {
-    const txtTrack: AppRootState['allTrack'][0] = {
-      id: getRandomEnglishString(8),
-      audio: 2,
-      video: 1,
-      mediaName: val.mediaName,
-      type: MediaTypeEnum.txt,
-      track: undefined,
-      trackid: undefined,
-      stream: undefined,
-      streamid: undefined,
-      hidden: false,
-      muted: false,
-    };
-    if (fabricCanvas.value) {
-      console.log('val.txtInfo?.txt ', val.txtInfo?.txt);
-      const canvasDom = markRaw(
-        new fabric.Text(val.txtInfo?.txt || '', {
-          top: 0,
-          left: 0,
-          fill: val.txtInfo?.color,
-        })
-      );
-      handleMoving({ canvasDom, id: txtTrack.id });
-      handleScaling({ canvasDom, id: txtTrack.id });
-      txtTrack.txtInfo = val.txtInfo;
-      txtTrack.scaleInfo = { scaleX: 1, scaleY: 1 };
-      // @ts-ignore
-      txtTrack.canvasDom = canvasDom;
-      fabricCanvas.value.add(canvasDom);
-    }
-
-    const res = [...appStore.allTrack, txtTrack];
-    // @ts-ignore
-    appStore.setAllTrack(res);
-    // @ts-ignore
-    resourceCacheStore.setList(res);
-    // @ts-ignore
-    addTrack(txtTrack);
-
-    console.log('获取文字成功', fabricCanvas.value);
-  } else if (val.type === MediaTypeEnum.time) {
-    const timeTrack: AppRootState['allTrack'][0] = {
-      id: getRandomEnglishString(8),
-      audio: 2,
-      video: 1,
-      mediaName: val.mediaName,
-      type: MediaTypeEnum.time,
-      track: undefined,
-      trackid: undefined,
-      stream: undefined,
-      streamid: undefined,
-      hidden: false,
-      muted: false,
-    };
-    if (fabricCanvas.value) {
-      const canvasDom = markRaw(
-        new fabric.Text(new Date().toLocaleString(), {
-          top: 0,
-          left: 0,
-          fill: val.timeInfo?.color,
-        })
-      );
-      timeCanvasDom.value.push(canvasDom);
-      handleMoving({ canvasDom, id: timeTrack.id });
-      handleScaling({ canvasDom, id: timeTrack.id });
-      timeTrack.timeInfo = val.timeInfo;
-      timeTrack.scaleInfo = { scaleX: 1, scaleY: 1 };
-
-      // @ts-ignore
-      timeTrack.canvasDom = canvasDom;
-      fabricCanvas.value.add(canvasDom);
-    }
-
-    const res = [...appStore.allTrack, timeTrack];
-    // @ts-ignore
-    appStore.setAllTrack(res);
-    // @ts-ignore
-    resourceCacheStore.setList(res);
-    // @ts-ignore
-    addTrack(timeTrack);
-
-    console.log('获取时间成功', fabricCanvas.value);
-  } else if (val.type === MediaTypeEnum.stopwatch) {
-    const stopwatchTrack: AppRootState['allTrack'][0] = {
-      id: getRandomEnglishString(8),
-      audio: 2,
-      video: 1,
-      mediaName: val.mediaName,
-      type: MediaTypeEnum.stopwatch,
-      track: undefined,
-      trackid: undefined,
-      stream: undefined,
-      streamid: undefined,
-      hidden: false,
-      muted: false,
-    };
-    if (fabricCanvas.value) {
-      const canvasDom = markRaw(
-        new fabric.Text('00:00:00.000', {
-          top: 0,
-          left: 0,
-          fill: val.stopwatchInfo?.color,
-          // editable: true,
-        })
-      );
-      stopwatchCanvasDom.value.push(canvasDom);
-      handleMoving({ canvasDom, id: stopwatchTrack.id });
-      handleScaling({ canvasDom, id: stopwatchTrack.id });
-      stopwatchTrack.stopwatchInfo = val.stopwatchInfo;
-      // @ts-ignore
-      stopwatchTrack.canvasDom = canvasDom;
-      fabricCanvas.value.add(canvasDom);
-    }
-
-    const res = [...appStore.allTrack, stopwatchTrack];
-    // @ts-ignore
-    appStore.setAllTrack(res);
-    // @ts-ignore
-    resourceCacheStore.setList(res);
-    // @ts-ignore
-    addTrack(stopwatchTrack);
-
-    console.log('获取秒表成功', fabricCanvas.value);
-  } else if (val.type === MediaTypeEnum.img) {
-    const imgTrack: AppRootState['allTrack'][0] = {
-      id: getRandomEnglishString(8),
-      audio: 2,
-      video: 1,
-      mediaName: val.mediaName,
-      type: MediaTypeEnum.img,
-      track: undefined,
-      trackid: undefined,
-      stream: undefined,
-      streamid: undefined,
-      hidden: false,
-      muted: false,
-    };
-
-    if (fabricCanvas.value) {
-      if (!val.imgInfo) return;
-      const file = val.imgInfo[0].file!;
-      const { code } = await saveFile({ file, fileName: imgTrack.id });
-      if (code !== 1) return;
-      const imgEl = await new Promise<HTMLImageElement>((resolve) => {
-        const reader = new FileReader();
-        reader.addEventListener(
-          'load',
-          function () {
-            const img = document.createElement('img');
-            img.src = reader.result as string;
-            img.onload = () => {
-              resolve(img);
-            };
-          },
-          false
-        );
-        if (file) {
-          reader.readAsDataURL(file);
-        }
-      });
-
-      const canvasDom = markRaw(
-        new fabric.Image(imgEl, {
-          top: 0,
-          left: 0,
-          width: imgEl.width,
-          height: imgEl.height,
-        })
-      );
-      handleMoving({ canvasDom, id: imgTrack.id });
-      handleScaling({ canvasDom, id: imgTrack.id });
-      const ratio = handleScale({ width: imgEl.width, height: imgEl.height });
-      // @ts-ignore
-      imgTrack.canvasDom = canvasDom;
-      imgTrack.scaleInfo = { scaleX: ratio, scaleY: ratio };
-      canvasDom.scale(ratio / window.devicePixelRatio);
-      fabricCanvas.value.add(canvasDom);
-    }
-
-    const res = [...appStore.allTrack, imgTrack];
-    // @ts-ignore
-    appStore.setAllTrack(res);
-    // @ts-ignore
-    resourceCacheStore.setList(res);
-    // @ts-ignore
-    addTrack(imgTrack);
-
-    console.log('获取图片成功', fabricCanvas.value);
-  } else if (val.type === MediaTypeEnum.media) {
-    const mediaVideoTrack: AppRootState['allTrack'][0] = {
-      id: getRandomEnglishString(8),
-      audio: 2,
-      video: 1,
-      mediaName: val.mediaName,
-      type: MediaTypeEnum.media,
-      track: undefined,
-      trackid: undefined,
-      stream: undefined,
-      streamid: undefined,
-      hidden: false,
-      muted: false,
-    };
-    if (fabricCanvas.value) {
-      if (!val.mediaInfo) return;
-      const file = val.mediaInfo[0].file!;
-      const { code } = await saveFile({ file, fileName: mediaVideoTrack.id });
-      if (code !== 1) return;
-      const url = URL.createObjectURL(file);
-      const videoEl = createVideo({});
-      videoEl.src = url;
-      videoEl.muted = false;
-      videoEl.style.width = `1px`;
-      videoEl.style.height = `1px`;
-      videoEl.style.position = 'fixed';
-      videoEl.style.bottom = '0';
-      videoEl.style.right = '0';
-      videoEl.style.opacity = '0';
-      videoEl.style.pointerEvents = 'none';
-      document.body.appendChild(videoEl);
-      bodyAppendChildElArr.value.push(videoEl);
-      const videoRes = await new Promise<HTMLVideoElement>((resolve) => {
-        videoEl.onloadedmetadata = () => {
-          resolve(videoEl);
-        };
-      });
-      // @ts-ignore
-      const stream = videoRes.captureStream();
-      const { canvasDom, scale } = await autoCreateVideo({
-        stream,
-        id: mediaVideoTrack.id,
-      });
-      mediaVideoTrack.scaleInfo = { scaleX: scale, scaleY: scale };
-      mediaVideoTrack.videoEl = videoEl;
-      // @ts-ignore
-      mediaVideoTrack.canvasDom = canvasDom;
-      if (stream.getAudioTracks()[0]) {
-        console.log('视频有音频', stream.getAudioTracks()[0]);
-        mediaVideoTrack.audio = 1;
-        const audioTrack: AppRootState['allTrack'][0] = {
-          id: mediaVideoTrack.id,
-          audio: 1,
-          video: 2,
-          mediaName: val.mediaName,
-          type: MediaTypeEnum.media,
-          track: stream.getAudioTracks()[0],
-          trackid: stream.getAudioTracks()[0].id,
-          stream,
-          streamid: stream.id,
-          hidden: true,
-          muted: false,
-        };
-        // @ts-ignore
-        const res = [...appStore.allTrack, audioTrack];
-        appStore.setAllTrack(res);
-        resourceCacheStore.setList(res);
-        handleMixedAudio();
-        // @ts-ignore
-
-        addTrack(audioTrack);
-      }
-    }
-    const res = [...appStore.allTrack, mediaVideoTrack];
-    // @ts-ignore
-    appStore.setAllTrack(res);
-    // @ts-ignore
-    resourceCacheStore.setList(res);
-    // @ts-ignore
-
-    addTrack(mediaVideoTrack);
-
-    console.log('获取视频成功', fabricCanvas.value);
-  }
-
-  canvasVideoStream.value = pushCanvasRef.value!.captureStream();
-}
-
-function handleChangeMuted(item: AppRootState['allTrack'][0]) {
-  if (item.videoEl) {
-    const res = !item.videoEl.muted;
-    item.videoEl.muted = res;
-    item.muted = res;
-    resourceCacheStore.setList(appStore.allTrack);
-  }
-}
-
-function handleEdit(item: AppRootState['allTrack'][0]) {
-  console.log('handleEdit', item);
-}
-
-function handleDel(item: AppRootState['allTrack'][0]) {
-  console.log('handleDel', item);
-  if (item.canvasDom !== undefined) {
-    // @ts-ignore
-    fabricCanvas.value?.remove(item.canvasDom);
-    item.videoEl?.remove();
-  }
-  const res = appStore.allTrack.filter((iten) => iten.id !== item.id);
-  appStore.setAllTrack(res);
-  resourceCacheStore.setList(res);
-  delTrack(item);
-}
-
-function handleStartMedia(item: { type: MediaTypeEnum; txt: string }) {
-  currentMediaType.value = item.type;
-  showMediaModalCpt.value = true;
-}
 </script>
 
-<style lang="scss" scoped>
-.push-wrap {
-  display: flex;
-  justify-content: space-between;
-  margin: 15px auto 0;
-  width: $w-1250;
-  .left {
-    position: relative;
-    display: inline-block;
-    overflow: hidden;
-    box-sizing: border-box;
-    width: $w-960;
-    height: 100%;
-    border-radius: 6px;
-    background-color: white;
-    color: #9499a0;
-    vertical-align: top;
-
-    .container {
-      position: relative;
-      overflow: hidden;
-      height: 100%;
-      background-color: rgba($color: #000000, $alpha: 0.5);
-      line-height: 0;
-
-      .add-wrap {
-        position: absolute;
-        top: 50%;
-        left: 50%;
-        display: flex;
-        align-items: center;
-        justify-content: space-around;
-        padding: 10px 20px;
-        width: 50%;
-        border-radius: 6px;
-        background-color: white;
-        transform: translate(-50%, -50%);
-      }
-    }
-    .room-control {
-      display: flex;
-      justify-content: space-between;
-      padding: 20px;
-      background-color: papayawhip;
-
-      .info {
-        display: flex;
-        align-items: center;
-
-        .avatar {
-          margin-right: 20px;
-          width: 55px;
-          height: 55px;
-          border-radius: 50%;
-          background-position: center center;
-          background-size: cover;
-          background-repeat: no-repeat;
-        }
-        .detail {
-          display: flex;
-          flex-direction: column;
-          flex-shrink: 0;
-          width: 200px;
-          text-align: initial;
-          .top {
-            margin-bottom: 10px;
-            color: #18191c;
-          }
-          .bottom {
-            font-size: 14px;
-          }
-        }
-      }
-      .rtc {
-        display: flex;
-        align-items: center;
-        flex: 1;
-        font-size: 14px;
-        .item {
-          display: flex;
-          align-items: center;
-          flex: 1;
-          .txt {
-            flex-shrink: 0;
-            width: 80px;
-          }
-          .down {
-            width: 90px;
-
-            user-select: none;
-          }
-        }
-      }
-      .other {
-        display: flex;
-        flex-direction: column;
-        justify-content: center;
-        font-size: 12px;
-        .top {
-        }
-        .bottom {
-          margin-top: 10px;
-        }
-      }
-    }
-  }
-  .right {
-    display: flex;
-    flex-direction: column;
-    justify-content: space-between;
-    box-sizing: border-box;
-    margin-left: 10px;
-    width: $w-250;
-    border-radius: 6px;
-    background-color: white;
-    color: #9499a0;
-
-    .resource-card {
-      position: relative;
-      box-sizing: border-box;
-      margin-bottom: 10px;
-      padding: 10px;
-      width: 100%;
-      height: 290px;
-      border-radius: 6px;
-      background-color: papayawhip;
-      .title {
-        text-align: initial;
-      }
-      .item {
-        display: flex;
-        align-items: center;
-        justify-content: space-between;
-        margin: 5px 0;
-        font-size: 14px;
-        // &:hover {
-        //   .control {
-        //     display: flex;
-        //     align-items: center;
-        //   }
-        // }
-        .control {
-          display: flex;
-          align-items: center;
-          .control-item {
-            cursor: pointer;
-            &:not(:last-child) {
-              margin-right: 6px;
-            }
-          }
-        }
-      }
-      .bottom {
-        position: absolute;
-        bottom: 0;
-        left: 0;
-        padding: 10px;
-      }
-    }
-    .danmu-card {
-      position: relative;
-      flex: 1;
-      box-sizing: border-box;
-      padding: 10px;
-      width: 100%;
-      border-radius: 6px;
-      background-color: papayawhip;
-      text-align: initial;
-      .title {
-        margin-bottom: 10px;
-      }
-      .list {
-        overflow: scroll;
-        height: 360px;
-
-        @extend %hideScrollbar;
-
-        .item {
-          margin-bottom: 10px;
-          font-size: 12px;
-          .name {
-            color: #9499a0;
-          }
-          .msg {
-            color: #61666d;
-          }
-        }
-      }
-
-      .send-msg {
-        position: absolute;
-        bottom: 10px;
-        left: 50%;
-        display: flex;
-        align-items: center;
-        box-sizing: border-box;
-        width: calc(100% - 20px);
-        transform: translateX(-50%);
-        .ipt {
-          display: block;
-          box-sizing: border-box;
-          margin: 0 auto;
-          margin-right: 10px;
-          padding: 10px;
-          width: 80%;
-          height: 30px;
-          outline: none;
-          border: 1px solid hsla(0, 0%, 60%, 0.2);
-          border-radius: 4px;
-          background-color: #f1f2f3;
-          font-size: 14px;
-        }
-      }
-    }
-  }
-}
-
-// 屏幕宽度大于1500的时候
-@media screen and (min-width: $w-1500) {
-  .push-wrap {
-    width: $w-1475;
-    .left {
-      width: $w-1150;
-    }
-    .right {
-      width: $w-300;
-    }
-  }
-}
-</style>
+<style lang="scss" scoped></style>

+ 1693 - 0
src/views/push/rtc/index.vue

@@ -0,0 +1,1693 @@
+<template>
+  <div class="push-wrap">
+    <div
+      ref="topRef"
+      class="left"
+    >
+      <div
+        ref="containerRef"
+        class="container"
+      >
+        <canvas
+          id="pushCanvasRef"
+          ref="pushCanvasRef"
+        ></canvas>
+        <div
+          v-if="appStore.allTrack.filter((item) => !item.hidden).length <= 0"
+          class="add-wrap"
+        >
+          <n-space>
+            <n-button
+              v-for="(item, index) in allMediaTypeList"
+              :key="index"
+              class="item"
+              @click="handleStartMedia(item)"
+            >
+              {{ item.txt }}
+            </n-button>
+          </n-space>
+        </div>
+      </div>
+
+      <div
+        ref="bottomRef"
+        class="room-control"
+      >
+        <div class="info">
+          <div
+            class="avatar"
+            :style="{ backgroundImage: `url(${userStore.userInfo?.avatar})` }"
+          ></div>
+          <div class="detail">
+            <div class="top">
+              <n-input-group>
+                <n-input
+                  v-model:value="roomName"
+                  size="small"
+                  placeholder="输入房间名"
+                  :style="{ width: '50%' }"
+                />
+                <n-button
+                  size="small"
+                  type="primary"
+                  @click="confirmRoomName"
+                >
+                  确定
+                </n-button>
+              </n-input-group>
+            </div>
+            <div class="bottom">
+              <span v-if="NODE_ENV === 'development'">
+                {{ mySocketId }}
+              </span>
+            </div>
+          </div>
+        </div>
+        <div class="rtc">
+          <div class="item">
+            <div class="txt">码率设置</div>
+            <div class="down">
+              <n-select
+                v-model:value="currentMaxBitrate"
+                :options="maxBitrate"
+              />
+            </div>
+          </div>
+          <div class="item">
+            <div class="txt">帧率设置</div>
+            <div class="down">
+              <n-select
+                v-model:value="currentMaxFramerate"
+                :options="maxFramerate"
+              />
+            </div>
+          </div>
+          <div class="item">
+            <div class="txt">分辨率设置</div>
+            <div class="down">
+              <n-select
+                v-model:value="currentResolutionRatio"
+                :options="resolutionRatio"
+              />
+            </div>
+          </div>
+        </div>
+        <div class="other">
+          <div class="top">
+            <span class="item">
+              <i class="ico"></i>
+              <span>
+                正在观看:
+                {{
+                  liveUserList.filter((item) => item.id !== mySocketId).length
+                }}
+              </span>
+            </span>
+          </div>
+          <div class="bottom">
+            <n-button
+              v-if="!isLiving"
+              type="info"
+              size="small"
+              @click="handleStartLive"
+            >
+              开始rtc直播
+            </n-button>
+            <n-button
+              v-else
+              type="error"
+              size="small"
+              @click="endLive"
+            >
+              结束直播
+            </n-button>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div class="right">
+      <div class="resource-card">
+        <div class="title">素材列表</div>
+        <div class="list">
+          <div
+            v-for="(item, index) in appStore.allTrack.filter(
+              (item) => !item.hidden
+            )"
+            :key="index"
+            class="item"
+          >
+            <span class="name">
+              ({{ mediaTypeEnumMap[item.type] }}){{ item.mediaName }}
+            </span>
+            <div class="control">
+              <div
+                v-if="item.audio === 1"
+                class="control-item"
+                @click="handleChangeMuted(item)"
+              >
+                <n-icon size="16">
+                  <VolumeMuteOutline v-if="item.muted"></VolumeMuteOutline>
+                  <VolumeHighOutline v-else></VolumeHighOutline>
+                </n-icon>
+              </div>
+              <div
+                class="control-item"
+                @click="handleEdit(item)"
+              >
+                <n-icon size="16">
+                  <CreateOutline></CreateOutline>
+                </n-icon>
+              </div>
+              <div
+                class="control-item"
+                @click="handleDel(item)"
+              >
+                <n-icon size="16">
+                  <Close></Close>
+                </n-icon>
+              </div>
+            </div>
+          </div>
+        </div>
+        <div class="bottom">
+          <n-button
+            size="small"
+            type="primary"
+            @click="showSelectMediaModalCpt = true"
+          >
+            添加素材
+          </n-button>
+        </div>
+      </div>
+      <div class="danmu-card">
+        <div class="title">弹幕互动</div>
+        <div class="list-wrap">
+          <div
+            ref="danmuListRef"
+            class="list"
+          >
+            <div
+              v-for="(item, index) in damuList"
+              :key="index"
+              class="item"
+            >
+              <template v-if="item.msgType === DanmuMsgTypeEnum.danmu">
+                <span class="name">
+                  {{ item.userInfo?.username || item.socket_id }}:
+                </span>
+                <span class="msg">{{ item.msg }}</span>
+              </template>
+              <template v-else-if="item.msgType === DanmuMsgTypeEnum.otherJoin">
+                <span class="name system">系统通知:</span>
+                <span class="msg">
+                  <span>{{ item.userInfo?.username || item.socket_id }}</span>
+                  <span>进入直播!</span>
+                </span>
+              </template>
+              <template
+                v-else-if="item.msgType === DanmuMsgTypeEnum.userLeaved"
+              >
+                <span class="name system">系统通知:</span>
+                <span class="msg">
+                  <span>{{ item.userInfo?.username || item.socket_id }}</span>
+                  <span>离开直播!</span>
+                </span>
+              </template>
+            </div>
+          </div>
+        </div>
+        <div class="send-msg">
+          <input
+            v-model="danmuStr"
+            class="ipt"
+            @keydown="keydownDanmu"
+          />
+          <n-button
+            type="info"
+            size="small"
+            @click="sendDanmu"
+          >
+            发送
+          </n-button>
+        </div>
+      </div>
+    </div>
+
+    <SelectMediaModalCpt
+      v-if="showSelectMediaModalCpt"
+      :all-media-type-list="allMediaTypeList"
+      @close="showSelectMediaModalCpt = false"
+      @ok="selectMediaOk"
+    ></SelectMediaModalCpt>
+
+    <MediaModalCpt
+      v-if="showMediaModalCpt"
+      :media-type="currentMediaType"
+      @close="showMediaModalCpt = false"
+      @ok="addMediaOk"
+    ></MediaModalCpt>
+    <OpenMicophoneTipCpt
+      v-if="showOpenMicophoneTipCpt"
+      @close="showOpenMicophoneTipCpt = false"
+    ></OpenMicophoneTipCpt>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import {
+  Close,
+  CreateOutline,
+  VolumeHighOutline,
+  VolumeMuteOutline,
+} from '@vicons/ionicons5';
+import { fabric } from 'fabric';
+import { UploadFileInfo } from 'naive-ui';
+import {
+  Raw,
+  markRaw,
+  onMounted,
+  onUnmounted,
+  reactive,
+  ref,
+  watch,
+} from 'vue';
+import { useRoute } from 'vue-router';
+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 { AppRootState, useAppStore } from '@/store/app';
+import { useResourceCacheStore } from '@/store/cache';
+import { useUserStore } from '@/store/user';
+import {
+  createVideo,
+  generateBase64,
+  getRandomEnglishString,
+  readFile,
+  saveFile,
+} from '@/utils';
+import { NODE_ENV } from 'script/constant';
+
+import MediaModalCpt from '../mediaModal/index.vue';
+import OpenMicophoneTipCpt from '../openMicophoneTip/index.vue';
+import SelectMediaModalCpt from '../selectMediaModal/index.vue';
+
+const route = useRoute();
+const userStore = useUserStore();
+const appStore = useAppStore();
+const resourceCacheStore = useResourceCacheStore();
+const { maxBitrate, maxFramerate, resolutionRatio, allMediaTypeList } =
+  useRTCParams();
+
+const {
+  confirmRoomName,
+  startLive,
+  endLive,
+  sendDanmu,
+  keydownDanmu,
+  addTrack,
+  delTrack,
+  mySocketId,
+  lastCoverImg,
+  canvasVideoStream,
+  isLiving,
+  currentResolutionRatio,
+  currentMaxBitrate,
+  currentMaxFramerate,
+  danmuStr,
+  roomName,
+  damuList,
+  liveUserList,
+} = usePush();
+
+const currentMediaType = ref(MediaTypeEnum.camera);
+const showOpenMicophoneTipCpt = ref(false);
+const showSelectMediaModalCpt = ref(false);
+const showMediaModalCpt = ref(false);
+const topRef = ref<HTMLDivElement>();
+const bottomRef = ref<HTMLDivElement>();
+const danmuListRef = ref<HTMLDivElement>();
+const containerRef = ref<HTMLDivElement>();
+const pushCanvasRef = ref<HTMLCanvasElement>();
+const fabricCanvas = ref<fabric.Canvas>();
+const audioCtx = ref<AudioContext>();
+const timeCanvasDom = ref<Raw<fabric.Text>[]>([]);
+const stopwatchCanvasDom = ref<Raw<fabric.Text>[]>([]);
+const wrapSize = reactive({
+  width: 0,
+  height: 0,
+});
+const workerTimerId = ref(-1);
+const videoRatio = ref(16 / 9);
+const bodyAppendChildElArr = ref<HTMLElement[]>([]);
+const isSRS = route.query.liveType === liveTypeEnum.srsPush;
+
+watch(
+  () => damuList.value.length,
+  () => {
+    setTimeout(() => {
+      if (danmuListRef.value) {
+        danmuListRef.value.scrollTop = danmuListRef.value.scrollHeight;
+      }
+    }, 0);
+  }
+);
+
+onMounted(() => {
+  setTimeout(() => {
+    scrollTo(0, 0);
+  }, 100);
+  initUserMedia();
+  initCanvas();
+  handleCache();
+});
+
+onUnmounted(() => {
+  bodyAppendChildElArr.value.forEach((el) => {
+    el.remove();
+  });
+  clearFrame();
+});
+
+function initUserMedia() {
+  navigator.mediaDevices
+    .getUserMedia({
+      video: true,
+      audio: false,
+    })
+    .then(() => {
+      console.log('初始化获取摄像头成功');
+    })
+    .catch(() => {
+      console.log('初始化获取摄像头失败');
+    })
+    .finally(() => {
+      navigator.mediaDevices
+        .getUserMedia({
+          video: false,
+          audio: true,
+        })
+        .then(() => {
+          console.log('初始化获取麦克风成功');
+        })
+        .catch(() => {
+          console.log('初始化获取麦克风失败');
+          showOpenMicophoneTipCpt.value = true;
+        });
+    });
+}
+
+function formatDownTime(endTime: number, startTime: number) {
+  const times = (endTime - 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(endTime).getMilliseconds();
+
+  if (h < 10) {
+    // @ts-ignore
+    h = `0${h}`;
+  }
+  if (m < 10) {
+    // @ts-ignore
+    m = `0${m}`;
+  }
+  if (s < 10) {
+    // @ts-ignore
+    s = `0${s}`;
+  }
+  if (Number(ms) < 100) {
+    if (ms < 10) {
+      // @ts-ignore
+      ms = `00${ms}`;
+    } else {
+      // @ts-ignore
+      ms = `0${ms}`;
+    }
+  }
+  if (d > 0) {
+    return `${d}:${h}:${m}:${s}.${ms}`;
+  } else if (h > 0) {
+    return `${h}:${m}:${s}.${ms}`;
+  } else {
+    return `${m}:${s}.${ms}`;
+  }
+}
+
+function renderAll() {
+  timeCanvasDom.value.forEach((item) => {
+    item.text = new Date().toLocaleString();
+  });
+  stopwatchCanvasDom.value.forEach((item) => {
+    item.text = formatDownTime(+new Date(), +new Date());
+  });
+  fabricCanvas.value?.renderAll();
+}
+
+function clearFrame() {
+  if (workerTimerId.value !== -1) {
+    workerTimers.clearInterval(workerTimerId.value);
+  }
+}
+
+function renderFrame() {
+  const delay = 1000 / 60; // 16.666666666666668
+  workerTimerId.value = workerTimers.setInterval(() => {
+    renderAll();
+  }, delay);
+  console.log('workerTimerId.value', workerTimerId.value);
+}
+
+// 处理空音频轨
+function initNullAudio() {
+  console.warn('处理空音频轨');
+  // 创建一个AudioContext实例
+  const audioContext = new AudioContext();
+
+  // 创建一个GainNode实例来控制音频的音量
+  const gainNode = audioContext.createGain();
+
+  // 创建一个空的音频缓存
+  const buffer = audioContext.createBuffer(
+    2,
+    audioContext.sampleRate * 3,
+    audioContext.sampleRate
+  );
+
+  // 创建一个用于播放音频的AudioBufferSourceNode
+  const source = audioContext.createBufferSource();
+  source.buffer = buffer;
+
+  // 将源连接到gain node,再连接到输出
+  source.connect(gainNode);
+  gainNode.connect(audioContext.destination);
+  const destination = audioContext.createMediaStreamDestination();
+
+  const webAudioTrack: AppRootState['allTrack'][0] = {
+    id: getRandomEnglishString(8),
+    audio: 1,
+    video: 2,
+    mediaName: 'webAudio占位',
+    type: MediaTypeEnum.webAudio,
+    track: destination.stream.getAudioTracks()[0],
+    trackid: destination.stream.getAudioTracks()[0].id,
+    stream: destination.stream,
+    streamid: destination.stream.id,
+    hidden: true,
+    muted: false,
+  };
+  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);
+}
+
+let streamTmp: MediaStream;
+let vel;
+
+function handleMixedAudio() {
+  console.log('handleMixedAudiohandleMixedAudio');
+  const allAudioTrack = appStore.allTrack.filter((item) => item.audio === 1);
+  if (audioCtx.value) {
+    const gainNode = audioCtx.value.createGain();
+    allAudioTrack.forEach((item) => {
+      if (!audioCtx.value || !item.stream) return;
+      const audioInput = audioCtx.value.createMediaStreamSource(item.stream);
+      audioInput.connect(gainNode);
+      console.log('混流', item.stream?.id, item.stream);
+    });
+    if (streamTmp) {
+      const destination = audioCtx.value.createMediaStreamDestination();
+      streamTmp.addTrack(destination.stream.getAudioTracks()[0]);
+      gainNode.connect(destination);
+      const mixedStream = new MediaStream();
+      mixedStream.addTrack(destination.stream.getAudioTracks()[0]);
+      mixedStream.addTrack(canvasVideoStream.value!.getVideoTracks()[0]);
+      canvasVideoStream.value = mixedStream;
+      return;
+    }
+    const destination = audioCtx.value.createMediaStreamDestination();
+    streamTmp = destination.stream;
+    // @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.srcObject = destination.stream;
+    document.body.appendChild(vel);
+    bodyAppendChildElArr.value.push(vel);
+  }
+}
+
+function handleStartLive() {
+  // WARN 不能省略initNullAudio,否则开播时候没有音频的时候,srs那边的audio是 Stream #0:0: Audio: aac, 44100 Hz, stereo, 128 kb/s
+  // 会导致加载直播很慢,正常的audio应该是Stream #0:0: Audio: aac (LC), 48000 Hz, stereo, fltp
+  // 开播前执行initNullAudio,audio就会是正常的
+  initNullAudio();
+  if (!audioCtx.value) {
+    audioCtx.value = new AudioContext();
+  }
+  handleMixedAudio();
+  lastCoverImg.value = generateBase64(pushCanvasRef.value!);
+  startLive(isSRS ? LiveRoomTypeEnum.user_srs : LiveRoomTypeEnum.user_wertc);
+}
+
+function handleScale({ width, height }: { width: number; height: number }) {
+  const resolutionHeight =
+    currentResolutionRatio.value * window.devicePixelRatio;
+  const resolutionWidth =
+    currentResolutionRatio.value * window.devicePixelRatio * videoRatio.value;
+  let ratio = 1;
+  if (width > resolutionWidth) {
+    const r1 = resolutionWidth / width;
+    ratio = r1;
+  }
+  if (height > resolutionHeight) {
+    const r1 = resolutionHeight / height;
+    if (ratio > r1) {
+      ratio = r1;
+    }
+  }
+  return ratio;
+}
+
+function autoCreateVideo({
+  stream,
+  id,
+  rect,
+  muted,
+}: {
+  stream: MediaStream;
+  id: string;
+  rect?: { left: number; top: number };
+  muted?: boolean;
+}) {
+  console.warn('autoCreateVideoautoCreateVideo', id);
+  const videoEl = createVideo({});
+  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;
+    videoEl: HTMLVideoElement;
+    scale: number;
+  }>((resolve) => {
+    videoEl.onloadedmetadata = () => {
+      const width = stream.getVideoTracks()[0].getSettings().width!;
+      const height = stream.getVideoTracks()[0].getSettings().height!;
+      const ratio = handleScale({ width, height });
+      videoEl.width = width;
+      videoEl.height = height;
+
+      const canvasDom = markRaw(
+        new fabric.Image(videoEl, {
+          top: rect?.top || 0,
+          left: rect?.left || 0,
+          width,
+          height,
+        })
+      );
+      console.log(
+        '初始化',
+        ratio,
+        canvasDom.width,
+        canvasDom.height,
+        width * ratio,
+        height * ratio,
+        canvasDom
+      );
+      handleMoving({ canvasDom, id });
+      handleScaling({ canvasDom, id });
+      canvasDom.scale(ratio / window.devicePixelRatio);
+      fabricCanvas.value!.add(canvasDom);
+
+      resolve({ canvasDom, scale: ratio, videoEl });
+    };
+  });
+}
+
+watch(
+  () => currentResolutionRatio.value,
+  (newHeight, oldHeight) => {
+    changeCanvasAttr({ newHeight, oldHeight });
+  }
+);
+
+// 容器宽高,1280*720,即720p
+// canvas容器宽高,2560*1440,即1440p
+
+// ======
+// 容器宽高,960*540,即540p
+// dom宽高,640*480
+// canvas容器宽高,960*540,即540p
+// 将dom绘制到容器里,此时dom的大小就是640*480
+// 需求,不管切换多少分辨率,我要看到的dom都是一样大小,即
+// 960*540时,dom是640*480
+// 1280*720时,dom不能是640*480了,因为这样他就会对比上一个分辨率的dom看起来小了,960/1280=0.75,540/720=0.75,
+// 其实就是分辨率变大了,我们就要将图片也变大,即图片的宽是640/0.75=853.4,高是480/0.75=640
+// 坐标变化,960*540时,dom坐标是100,100
+// 1280*720时,dom的坐标不能再是100,100了,否则对比上一个分辨率看起来偏
+
+function changeCanvasAttr({
+  newHeight,
+  oldHeight,
+}: {
+  newHeight: number;
+  oldHeight: number;
+}) {
+  if (fabricCanvas.value) {
+    const resolutionHeight =
+      currentResolutionRatio.value / window.devicePixelRatio;
+    const resolutionWidth =
+      (currentResolutionRatio.value / window.devicePixelRatio) *
+      videoRatio.value;
+    fabricCanvas.value.setWidth(resolutionWidth);
+    fabricCanvas.value.setHeight(resolutionHeight);
+    appStore.allTrack.forEach((iten) => {
+      console.log('当前类型', iten.type);
+      const item = iten.canvasDom;
+
+      if (item) {
+        // 分辨率变小了,将图片变小
+        if (newHeight < oldHeight) {
+          const ratio2 = oldHeight / newHeight;
+          item.left = item.left! / ratio2;
+          item.top = item.top! / ratio2;
+        } else {
+          // 分辨率变大了,将图片变大
+          const ratio2 = oldHeight / newHeight;
+          item.left = item.left! / ratio2;
+          item.top = item.top! / ratio2;
+        }
+      }
+    });
+    appStore.allTrack.forEach((iten) => {
+      console.log('当前类型', iten.type);
+      const item = iten.canvasDom;
+
+      if (item) {
+        // 分辨率变小了,将图片变小
+        if (newHeight < oldHeight) {
+          const ratio = newHeight / oldHeight;
+          const ratio1 = (item.scaleX || 1) * ratio;
+          item.scale(ratio1);
+        } else {
+          // 分辨率变大了,将图片变大
+          const ratio = newHeight / oldHeight;
+          const ratio1 = (item.scaleX || 1) * ratio;
+          item.scale(ratio1);
+        }
+      }
+    });
+
+    changeCanvasStyle();
+  }
+}
+
+function changeCanvasStyle() {
+  // @ts-ignore
+  fabricCanvas.value.wrapperEl.style.width = `${wrapSize.width}px`;
+  // @ts-ignore
+  fabricCanvas.value.wrapperEl.style.height = `${wrapSize.height}px`;
+  // @ts-ignore
+  fabricCanvas.value.lowerCanvasEl.style.width = `${wrapSize.width}px`;
+  // @ts-ignore
+  fabricCanvas.value.lowerCanvasEl.style.height = `${wrapSize.height}px`;
+  // @ts-ignore
+  fabricCanvas.value.upperCanvasEl.style.width = `${wrapSize.width}px`;
+  // @ts-ignore
+  fabricCanvas.value.upperCanvasEl.style.height = `${wrapSize.height}px`;
+}
+
+function initCanvas() {
+  const resolutionHeight =
+    currentResolutionRatio.value / window.devicePixelRatio;
+  const resolutionWidth =
+    (currentResolutionRatio.value / window.devicePixelRatio) * videoRatio.value;
+  const wrapWidth = containerRef.value!.getBoundingClientRect().width;
+  // const wrapWidth = 1920;
+  const ratio = wrapWidth / resolutionWidth;
+  const wrapHeight = resolutionHeight * ratio;
+  // const wrapHeight = 1080;
+  // lower-canvas: 实际的canvas画面,也就是pushCanvasRef
+  // upper-canvas: 操作时候的canvas
+  const ins = markRaw(new fabric.Canvas(pushCanvasRef.value!));
+  ins.setWidth(resolutionWidth);
+  ins.setHeight(resolutionHeight);
+  console.log('initCanvas', { resolutionWidth, resolutionHeight });
+  ins.setBackgroundColor('black', () => {
+    console.log('setBackgroundColor回调');
+  });
+  wrapSize.width = wrapWidth;
+  wrapSize.height = wrapHeight;
+  fabricCanvas.value = ins;
+  renderFrame();
+  changeCanvasStyle();
+}
+
+function handleScaling({ canvasDom, id }) {
+  canvasDom.on('scaling', () => {
+    appStore.allTrack.forEach((item) => {
+      if (id === item.id) {
+        item.scaleInfo = {
+          scaleX: canvasDom.scaleX || 1,
+          scaleY: canvasDom.scaleY || 1,
+        };
+      }
+    });
+    resourceCacheStore.setList(appStore.allTrack);
+  });
+}
+function handleMoving({
+  canvasDom,
+  id,
+}: {
+  canvasDom: fabric.Image | fabric.Text;
+  id: string;
+}) {
+  canvasDom.on('moving', () => {
+    console.log(
+      'moving',
+      canvasDom.width,
+      canvasDom.height,
+      canvasDom.scaleX,
+      canvasDom.scaleY
+    );
+    appStore.allTrack.forEach((item) => {
+      if (id === item.id) {
+        item.rect = {
+          top: (canvasDom.top || 0) * window.devicePixelRatio,
+          left: (canvasDom.left || 0) * window.devicePixelRatio,
+        };
+      }
+    });
+    resourceCacheStore.setList(appStore.allTrack);
+  });
+}
+
+async function handleCache() {
+  const res: AppRootState['allTrack'] = [];
+  const queue: any[] = [];
+  resourceCacheStore.list.forEach((item) => {
+    // @ts-ignore
+    const obj: AppRootState['allTrack'][0] = {};
+    obj.audio = item.audio;
+    obj.video = item.video;
+    obj.id = item.id;
+    obj.type = item.type;
+    obj.hidden = item.hidden;
+    obj.mediaName = item.mediaName;
+    obj.muted = item.muted;
+    obj.rect = item.rect;
+    obj.scaleInfo = item.scaleInfo;
+    obj.stopwatchInfo = item.stopwatchInfo;
+
+    async function handleMediaVideo() {
+      const { code, file } = await readFile(item.id);
+      if (code === 1 && file) {
+        const url = URL.createObjectURL(file);
+        const videoEl = createVideo({});
+        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 = () => {
+            const stream = videoEl
+              // @ts-ignore
+              .captureStream();
+            const width = stream.getVideoTracks()[0].getSettings().width!;
+            const height = stream.getVideoTracks()[0].getSettings().height!;
+            videoEl.width = width;
+            videoEl.height = height;
+
+            const canvasDom = markRaw(
+              new fabric.Image(videoEl, {
+                top: (item.rect?.top || 0) / window.devicePixelRatio,
+                left: (item.rect?.left || 0) / window.devicePixelRatio,
+                width,
+                height,
+              })
+            );
+            handleMoving({ canvasDom, id: item.id });
+            handleScaling({ canvasDom, id: item.id });
+            canvasDom.scale(
+              (item.scaleInfo?.scaleX || 1) / window.devicePixelRatio
+            );
+            fabricCanvas.value!.add(canvasDom);
+            obj.videoEl = videoEl;
+            obj.canvasDom = canvasDom;
+            resolve({ videoEl, canvasDom });
+          };
+        });
+        const stream = videoEl
+          // @ts-ignore
+          .captureStream() as MediaStream;
+        obj.stream = stream;
+        obj.streamid = stream.id;
+        obj.track = stream.getVideoTracks()[0];
+        obj.trackid = stream.getVideoTracks()[0].id;
+      } else {
+        console.error('读取文件失败');
+      }
+    }
+
+    async function handleImg() {
+      const { code, file } = await readFile(item.id);
+      if (code === 1 && file) {
+        const imgEl = await new Promise<HTMLImageElement>((resolve) => {
+          const reader = new FileReader();
+          reader.addEventListener(
+            'load',
+            function () {
+              const img = document.createElement('img');
+              img.src = reader.result as string;
+              img.onload = () => {
+                resolve(img);
+              };
+            },
+            false
+          );
+          if (file) {
+            reader.readAsDataURL(file);
+          }
+        });
+        if (fabricCanvas.value) {
+          const canvasDom = markRaw(
+            new fabric.Image(imgEl, {
+              top: (item.rect?.top || 0) / window.devicePixelRatio,
+              left: (item.rect?.left || 0) / window.devicePixelRatio,
+              width: imgEl.width,
+              height: imgEl.height,
+            })
+          );
+          handleMoving({ canvasDom, id: obj.id });
+          handleScaling({ canvasDom, id: obj.id });
+          canvasDom.scale(
+            (item.scaleInfo?.scaleX || 1) / window.devicePixelRatio
+          );
+          fabricCanvas.value.add(canvasDom);
+          obj.canvasDom = canvasDom;
+        }
+      } else {
+        console.error('读取文件失败');
+      }
+    }
+    if (item.type === MediaTypeEnum.media && item.video === 1) {
+      queue.push(handleMediaVideo());
+    } else if (item.type === MediaTypeEnum.img) {
+      queue.push(handleImg());
+    } else if (item.type === MediaTypeEnum.txt) {
+      obj.txtInfo = item.txtInfo;
+      obj.scaleInfo = item.scaleInfo;
+      if (fabricCanvas.value) {
+        const canvasDom = markRaw(
+          new fabric.Text(item.txtInfo?.txt || '', {
+            top: (item.rect?.top || 0) / window.devicePixelRatio,
+            left: (item.rect?.left || 0) / window.devicePixelRatio,
+            fill: item.txtInfo?.color,
+          })
+        );
+        handleMoving({ canvasDom, id: obj.id });
+        handleScaling({ canvasDom, id: obj.id });
+        // fabric.Text类型不能除以分辨率
+        canvasDom.scale(item.scaleInfo?.scaleX || 1);
+        fabricCanvas.value.add(canvasDom);
+        obj.canvasDom = canvasDom;
+      }
+    } else if (item.type === MediaTypeEnum.time) {
+      obj.timeInfo = item.timeInfo;
+      obj.scaleInfo = item.scaleInfo;
+      if (fabricCanvas.value) {
+        const canvasDom = markRaw(
+          new fabric.Text(new Date().toLocaleString(), {
+            top: (item.rect?.top || 0) / window.devicePixelRatio,
+            left: (item.rect?.left || 0) / window.devicePixelRatio,
+            fill: item.timeInfo?.color,
+          })
+        );
+        timeCanvasDom.value.push(canvasDom);
+        handleMoving({ canvasDom, id: obj.id });
+        handleScaling({ canvasDom, id: obj.id });
+        // fabric.Text类型不能除以分辨率
+        canvasDom.scale(item.scaleInfo?.scaleX || 1);
+        fabricCanvas.value.add(canvasDom);
+        obj.canvasDom = canvasDom;
+      }
+    } else if (item.type === MediaTypeEnum.stopwatch) {
+      obj.stopwatchInfo = item.stopwatchInfo;
+      obj.scaleInfo = item.scaleInfo;
+      if (fabricCanvas.value) {
+        const canvasDom = markRaw(
+          new fabric.Text('00:00:00.000', {
+            top: (item.rect?.top || 0) / window.devicePixelRatio,
+            left: (item.rect?.left || 0) / window.devicePixelRatio,
+            fill: item.stopwatchInfo?.color,
+          })
+        );
+        stopwatchCanvasDom.value.push(canvasDom);
+        handleMoving({ canvasDom, id: obj.id });
+        handleScaling({ canvasDom, id: obj.id });
+        // fabric.Text类型不能除以分辨率
+        canvasDom.scale(item.scaleInfo?.scaleX || 1);
+        fabricCanvas.value.add(canvasDom);
+        obj.canvasDom = canvasDom;
+      }
+    }
+    res.push(obj);
+  });
+  await Promise.all(queue);
+  canvasVideoStream.value = pushCanvasRef.value!.captureStream();
+  appStore.setAllTrack(res);
+}
+
+function selectMediaOk(val: MediaTypeEnum) {
+  showMediaModalCpt.value = true;
+  showSelectMediaModalCpt.value = false;
+  currentMediaType.value = val;
+}
+
+async function addMediaOk(val: {
+  type: MediaTypeEnum;
+  deviceId: string;
+  mediaName: string;
+  txtInfo?: { txt: string; color: string };
+  timeInfo?: { color: string };
+  stopwatchInfo?: { color: string };
+  imgInfo?: UploadFileInfo[];
+  mediaInfo?: UploadFileInfo[];
+}) {
+  if (!audioCtx.value) {
+    audioCtx.value = new AudioContext();
+  }
+  showMediaModalCpt.value = false;
+  if (val.type === MediaTypeEnum.screen) {
+    const event = await navigator.mediaDevices.getDisplayMedia({
+      video: {
+        deviceId: val.deviceId,
+        // displaySurface: 'monitor', // browser默认标签页;window默认窗口;monitor默认整个屏幕
+      },
+      audio: true,
+    });
+
+    const videoTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(8),
+      audio: 2,
+      video: 1,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.screen,
+      track: event.getVideoTracks()[0],
+      trackid: event.getVideoTracks()[0].id,
+      stream: event,
+      streamid: event.id,
+      hidden: false,
+      muted: false,
+    };
+
+    const { canvasDom, videoEl, scale } = await autoCreateVideo({
+      stream: event,
+      id: videoTrack.id,
+    });
+    videoTrack.scaleInfo = { scaleX: scale, scaleY: scale };
+    videoTrack.videoEl = videoEl;
+    // @ts-ignore
+    videoTrack.canvasDom = canvasDom;
+
+    const audio = event.getAudioTracks();
+    if (audio.length) {
+      videoTrack.audio = 1;
+      const audioTrack: AppRootState['allTrack'][0] = {
+        id: videoTrack.id,
+        audio: 1,
+        video: 2,
+        mediaName: val.mediaName,
+        type: MediaTypeEnum.screen,
+        track: event.getAudioTracks()[0],
+        trackid: event.getAudioTracks()[0].id,
+        stream: event,
+        streamid: event.id,
+        hidden: true,
+        muted: false,
+      };
+      const res = [...appStore.allTrack, videoTrack, audioTrack];
+      appStore.setAllTrack(res);
+      resourceCacheStore.setList(res);
+      handleMixedAudio();
+      // @ts-ignore
+      addTrack(videoTrack);
+      // @ts-ignore
+      addTrack(audioTrack);
+    } else {
+      const res = [...appStore.allTrack, videoTrack];
+      appStore.setAllTrack(res);
+      resourceCacheStore.setList(res);
+      // @ts-ignore
+      addTrack(videoTrack);
+    }
+
+    console.log('获取窗口成功');
+  } else if (val.type === MediaTypeEnum.camera) {
+    const event = await navigator.mediaDevices.getUserMedia({
+      video: {
+        deviceId: val.deviceId,
+      },
+      audio: false,
+    });
+    const videoTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(8),
+      audio: 2,
+      video: 1,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.camera,
+      track: event.getVideoTracks()[0],
+      trackid: event.getVideoTracks()[0].id,
+      stream: event,
+      streamid: event.id,
+      hidden: false,
+      muted: false,
+    };
+    const { canvasDom, videoEl, scale } = await autoCreateVideo({
+      stream: event,
+      id: videoTrack.id,
+    });
+    videoTrack.scaleInfo = { scaleX: scale, scaleY: scale };
+    videoTrack.videoEl = videoEl;
+    // @ts-ignore
+    videoTrack.canvasDom = canvasDom;
+
+    const res = [...appStore.allTrack, videoTrack];
+    appStore.setAllTrack(res);
+    resourceCacheStore.setList(res);
+    // @ts-ignore
+    addTrack(videoTrack);
+    console.log('获取摄像头成功');
+  } else if (val.type === MediaTypeEnum.microphone) {
+    const event = await navigator.mediaDevices.getUserMedia({
+      video: false,
+      audio: { deviceId: val.deviceId },
+    });
+    const audioTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(8),
+      audio: 1,
+      video: 2,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.microphone,
+      track: event.getAudioTracks()[0],
+      trackid: event.getAudioTracks()[0].id,
+      stream: event,
+      streamid: event.id,
+      hidden: false,
+      muted: false,
+    };
+    const res = [...appStore.allTrack, audioTrack];
+    appStore.setAllTrack(res);
+    resourceCacheStore.setList(res);
+    handleMixedAudio();
+    // @ts-ignore
+    addTrack(audioTrack);
+
+    console.log('获取麦克风成功');
+  } else if (val.type === MediaTypeEnum.txt) {
+    const txtTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(8),
+      audio: 2,
+      video: 1,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.txt,
+      track: undefined,
+      trackid: undefined,
+      stream: undefined,
+      streamid: undefined,
+      hidden: false,
+      muted: false,
+    };
+    if (fabricCanvas.value) {
+      console.log('val.txtInfo?.txt ', val.txtInfo?.txt);
+      const canvasDom = markRaw(
+        new fabric.Text(val.txtInfo?.txt || '', {
+          top: 0,
+          left: 0,
+          fill: val.txtInfo?.color,
+        })
+      );
+      handleMoving({ canvasDom, id: txtTrack.id });
+      handleScaling({ canvasDom, id: txtTrack.id });
+      txtTrack.txtInfo = val.txtInfo;
+      txtTrack.scaleInfo = { scaleX: 1, scaleY: 1 };
+      // @ts-ignore
+      txtTrack.canvasDom = canvasDom;
+      fabricCanvas.value.add(canvasDom);
+    }
+
+    const res = [...appStore.allTrack, txtTrack];
+    // @ts-ignore
+    appStore.setAllTrack(res);
+    // @ts-ignore
+    resourceCacheStore.setList(res);
+    // @ts-ignore
+    addTrack(txtTrack);
+
+    console.log('获取文字成功', fabricCanvas.value);
+  } else if (val.type === MediaTypeEnum.time) {
+    const timeTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(8),
+      audio: 2,
+      video: 1,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.time,
+      track: undefined,
+      trackid: undefined,
+      stream: undefined,
+      streamid: undefined,
+      hidden: false,
+      muted: false,
+    };
+    if (fabricCanvas.value) {
+      const canvasDom = markRaw(
+        new fabric.Text(new Date().toLocaleString(), {
+          top: 0,
+          left: 0,
+          fill: val.timeInfo?.color,
+        })
+      );
+      timeCanvasDom.value.push(canvasDom);
+      handleMoving({ canvasDom, id: timeTrack.id });
+      handleScaling({ canvasDom, id: timeTrack.id });
+      timeTrack.timeInfo = val.timeInfo;
+      timeTrack.scaleInfo = { scaleX: 1, scaleY: 1 };
+
+      // @ts-ignore
+      timeTrack.canvasDom = canvasDom;
+      fabricCanvas.value.add(canvasDom);
+    }
+
+    const res = [...appStore.allTrack, timeTrack];
+    // @ts-ignore
+    appStore.setAllTrack(res);
+    // @ts-ignore
+    resourceCacheStore.setList(res);
+    // @ts-ignore
+    addTrack(timeTrack);
+
+    console.log('获取时间成功', fabricCanvas.value);
+  } else if (val.type === MediaTypeEnum.stopwatch) {
+    const stopwatchTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(8),
+      audio: 2,
+      video: 1,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.stopwatch,
+      track: undefined,
+      trackid: undefined,
+      stream: undefined,
+      streamid: undefined,
+      hidden: false,
+      muted: false,
+    };
+    if (fabricCanvas.value) {
+      const canvasDom = markRaw(
+        new fabric.Text('00:00:00.000', {
+          top: 0,
+          left: 0,
+          fill: val.stopwatchInfo?.color,
+          // editable: true,
+        })
+      );
+      stopwatchCanvasDom.value.push(canvasDom);
+      handleMoving({ canvasDom, id: stopwatchTrack.id });
+      handleScaling({ canvasDom, id: stopwatchTrack.id });
+      stopwatchTrack.stopwatchInfo = val.stopwatchInfo;
+      // @ts-ignore
+      stopwatchTrack.canvasDom = canvasDom;
+      fabricCanvas.value.add(canvasDom);
+    }
+
+    const res = [...appStore.allTrack, stopwatchTrack];
+    // @ts-ignore
+    appStore.setAllTrack(res);
+    // @ts-ignore
+    resourceCacheStore.setList(res);
+    // @ts-ignore
+    addTrack(stopwatchTrack);
+
+    console.log('获取秒表成功', fabricCanvas.value);
+  } else if (val.type === MediaTypeEnum.img) {
+    const imgTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(8),
+      audio: 2,
+      video: 1,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.img,
+      track: undefined,
+      trackid: undefined,
+      stream: undefined,
+      streamid: undefined,
+      hidden: false,
+      muted: false,
+    };
+
+    if (fabricCanvas.value) {
+      if (!val.imgInfo) return;
+      const file = val.imgInfo[0].file!;
+      const { code } = await saveFile({ file, fileName: imgTrack.id });
+      if (code !== 1) return;
+      const imgEl = await new Promise<HTMLImageElement>((resolve) => {
+        const reader = new FileReader();
+        reader.addEventListener(
+          'load',
+          function () {
+            const img = document.createElement('img');
+            img.src = reader.result as string;
+            img.onload = () => {
+              resolve(img);
+            };
+          },
+          false
+        );
+        if (file) {
+          reader.readAsDataURL(file);
+        }
+      });
+
+      const canvasDom = markRaw(
+        new fabric.Image(imgEl, {
+          top: 0,
+          left: 0,
+          width: imgEl.width,
+          height: imgEl.height,
+        })
+      );
+      handleMoving({ canvasDom, id: imgTrack.id });
+      handleScaling({ canvasDom, id: imgTrack.id });
+      const ratio = handleScale({ width: imgEl.width, height: imgEl.height });
+      // @ts-ignore
+      imgTrack.canvasDom = canvasDom;
+      imgTrack.scaleInfo = { scaleX: ratio, scaleY: ratio };
+      canvasDom.scale(ratio / window.devicePixelRatio);
+      fabricCanvas.value.add(canvasDom);
+    }
+
+    const res = [...appStore.allTrack, imgTrack];
+    // @ts-ignore
+    appStore.setAllTrack(res);
+    // @ts-ignore
+    resourceCacheStore.setList(res);
+    // @ts-ignore
+    addTrack(imgTrack);
+
+    console.log('获取图片成功', fabricCanvas.value);
+  } else if (val.type === MediaTypeEnum.media) {
+    const mediaVideoTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(8),
+      audio: 2,
+      video: 1,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.media,
+      track: undefined,
+      trackid: undefined,
+      stream: undefined,
+      streamid: undefined,
+      hidden: false,
+      muted: false,
+    };
+    if (fabricCanvas.value) {
+      if (!val.mediaInfo) return;
+      const file = val.mediaInfo[0].file!;
+      const { code } = await saveFile({ file, fileName: mediaVideoTrack.id });
+      if (code !== 1) return;
+      const url = URL.createObjectURL(file);
+      const videoEl = createVideo({});
+      videoEl.src = url;
+      videoEl.muted = false;
+      videoEl.style.width = `1px`;
+      videoEl.style.height = `1px`;
+      videoEl.style.position = 'fixed';
+      videoEl.style.bottom = '0';
+      videoEl.style.right = '0';
+      videoEl.style.opacity = '0';
+      videoEl.style.pointerEvents = 'none';
+      document.body.appendChild(videoEl);
+      bodyAppendChildElArr.value.push(videoEl);
+      const videoRes = await new Promise<HTMLVideoElement>((resolve) => {
+        videoEl.onloadedmetadata = () => {
+          resolve(videoEl);
+        };
+      });
+      // @ts-ignore
+      const stream = videoRes.captureStream();
+      const { canvasDom, scale } = await autoCreateVideo({
+        stream,
+        id: mediaVideoTrack.id,
+      });
+      mediaVideoTrack.scaleInfo = { scaleX: scale, scaleY: scale };
+      mediaVideoTrack.videoEl = videoEl;
+      // @ts-ignore
+      mediaVideoTrack.canvasDom = canvasDom;
+      if (stream.getAudioTracks()[0]) {
+        console.log('视频有音频', stream.getAudioTracks()[0]);
+        mediaVideoTrack.audio = 1;
+        const audioTrack: AppRootState['allTrack'][0] = {
+          id: mediaVideoTrack.id,
+          audio: 1,
+          video: 2,
+          mediaName: val.mediaName,
+          type: MediaTypeEnum.media,
+          track: stream.getAudioTracks()[0],
+          trackid: stream.getAudioTracks()[0].id,
+          stream,
+          streamid: stream.id,
+          hidden: true,
+          muted: false,
+        };
+        // @ts-ignore
+        const res = [...appStore.allTrack, audioTrack];
+        appStore.setAllTrack(res);
+        resourceCacheStore.setList(res);
+        handleMixedAudio();
+        // @ts-ignore
+
+        addTrack(audioTrack);
+      }
+    }
+    const res = [...appStore.allTrack, mediaVideoTrack];
+    // @ts-ignore
+    appStore.setAllTrack(res);
+    // @ts-ignore
+    resourceCacheStore.setList(res);
+    // @ts-ignore
+
+    addTrack(mediaVideoTrack);
+
+    console.log('获取视频成功', fabricCanvas.value);
+  }
+
+  canvasVideoStream.value = pushCanvasRef.value!.captureStream();
+}
+
+function handleChangeMuted(item: AppRootState['allTrack'][0]) {
+  if (item.videoEl) {
+    const res = !item.videoEl.muted;
+    item.videoEl.muted = res;
+    item.muted = res;
+    resourceCacheStore.setList(appStore.allTrack);
+  }
+}
+
+function handleEdit(item: AppRootState['allTrack'][0]) {
+  console.log('handleEdit', item);
+}
+
+function handleDel(item: AppRootState['allTrack'][0]) {
+  console.log('handleDel', item);
+  if (item.canvasDom !== undefined) {
+    // @ts-ignore
+    fabricCanvas.value?.remove(item.canvasDom);
+    item.videoEl?.remove();
+  }
+  const res = appStore.allTrack.filter((iten) => iten.id !== item.id);
+  appStore.setAllTrack(res);
+  resourceCacheStore.setList(res);
+  delTrack(item);
+}
+
+function handleStartMedia(item: { type: MediaTypeEnum; txt: string }) {
+  currentMediaType.value = item.type;
+  showMediaModalCpt.value = true;
+}
+</script>
+
+<style lang="scss" scoped>
+.push-wrap {
+  display: flex;
+  justify-content: space-between;
+  margin: 15px auto 0;
+  width: $w-1250;
+  .left {
+    position: relative;
+    display: inline-block;
+    overflow: hidden;
+    box-sizing: border-box;
+    width: $w-960;
+    height: 100%;
+    border-radius: 6px;
+    background-color: white;
+    color: #9499a0;
+    vertical-align: top;
+
+    .container {
+      position: relative;
+      overflow: hidden;
+      height: 100%;
+      background-color: rgba($color: #000000, $alpha: 0.5);
+      line-height: 0;
+
+      .add-wrap {
+        position: absolute;
+        top: 50%;
+        left: 50%;
+        display: flex;
+        align-items: center;
+        justify-content: space-around;
+        padding: 10px 20px;
+        width: 50%;
+        border-radius: 6px;
+        background-color: white;
+        transform: translate(-50%, -50%);
+      }
+    }
+    .room-control {
+      display: flex;
+      justify-content: space-between;
+      padding: 20px;
+      background-color: papayawhip;
+
+      .info {
+        display: flex;
+        align-items: center;
+
+        .avatar {
+          margin-right: 20px;
+          width: 55px;
+          height: 55px;
+          border-radius: 50%;
+          background-position: center center;
+          background-size: cover;
+          background-repeat: no-repeat;
+        }
+        .detail {
+          display: flex;
+          flex-direction: column;
+          flex-shrink: 0;
+          width: 200px;
+          text-align: initial;
+          .top {
+            margin-bottom: 10px;
+            color: #18191c;
+          }
+          .bottom {
+            font-size: 14px;
+          }
+        }
+      }
+      .rtc {
+        display: flex;
+        align-items: center;
+        flex: 1;
+        font-size: 14px;
+        .item {
+          display: flex;
+          align-items: center;
+          flex: 1;
+          .txt {
+            flex-shrink: 0;
+            width: 80px;
+          }
+          .down {
+            width: 90px;
+
+            user-select: none;
+          }
+        }
+      }
+      .other {
+        display: flex;
+        flex-direction: column;
+        justify-content: center;
+        font-size: 12px;
+        .top {
+        }
+        .bottom {
+          margin-top: 10px;
+        }
+      }
+    }
+  }
+  .right {
+    display: flex;
+    flex-direction: column;
+    justify-content: space-between;
+    box-sizing: border-box;
+    margin-left: 10px;
+    width: $w-250;
+    border-radius: 6px;
+    background-color: white;
+    color: #9499a0;
+
+    .resource-card {
+      position: relative;
+      box-sizing: border-box;
+      margin-bottom: 10px;
+      padding: 10px;
+      width: 100%;
+      height: 290px;
+      border-radius: 6px;
+      background-color: papayawhip;
+      .title {
+        text-align: initial;
+      }
+      .item {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        margin: 5px 0;
+        font-size: 14px;
+        // &:hover {
+        //   .control {
+        //     display: flex;
+        //     align-items: center;
+        //   }
+        // }
+        .control {
+          display: flex;
+          align-items: center;
+          .control-item {
+            cursor: pointer;
+            &:not(:last-child) {
+              margin-right: 6px;
+            }
+          }
+        }
+      }
+      .bottom {
+        position: absolute;
+        bottom: 0;
+        left: 0;
+        padding: 10px;
+      }
+    }
+    .danmu-card {
+      position: relative;
+      flex: 1;
+      box-sizing: border-box;
+      padding: 10px;
+      width: 100%;
+      border-radius: 6px;
+      background-color: papayawhip;
+      text-align: initial;
+      .title {
+        margin-bottom: 10px;
+      }
+      .list {
+        overflow: scroll;
+        height: 360px;
+
+        @extend %hideScrollbar;
+
+        .item {
+          margin-bottom: 10px;
+          font-size: 12px;
+          .name {
+            color: #9499a0;
+          }
+          .msg {
+            color: #61666d;
+          }
+        }
+      }
+
+      .send-msg {
+        position: absolute;
+        bottom: 10px;
+        left: 50%;
+        display: flex;
+        align-items: center;
+        box-sizing: border-box;
+        width: calc(100% - 20px);
+        transform: translateX(-50%);
+        .ipt {
+          display: block;
+          box-sizing: border-box;
+          margin: 0 auto;
+          margin-right: 10px;
+          padding: 10px;
+          width: 80%;
+          height: 30px;
+          outline: none;
+          border: 1px solid hsla(0, 0%, 60%, 0.2);
+          border-radius: 4px;
+          background-color: #f1f2f3;
+          font-size: 14px;
+        }
+      }
+    }
+  }
+}
+
+// 屏幕宽度大于1500的时候
+@media screen and (min-width: $w-1500) {
+  .push-wrap {
+    width: $w-1475;
+    .left {
+      width: $w-1150;
+    }
+    .right {
+      width: $w-300;
+    }
+  }
+}
+</style>

+ 1693 - 0
src/views/push/srs/index.vue

@@ -0,0 +1,1693 @@
+<template>
+  <div class="push-wrap">
+    <div
+      ref="topRef"
+      class="left"
+    >
+      <div
+        ref="containerRef"
+        class="container"
+      >
+        <canvas
+          id="pushCanvasRef"
+          ref="pushCanvasRef"
+        ></canvas>
+        <div
+          v-if="appStore.allTrack.filter((item) => !item.hidden).length <= 0"
+          class="add-wrap"
+        >
+          <n-space>
+            <n-button
+              v-for="(item, index) in allMediaTypeList"
+              :key="index"
+              class="item"
+              @click="handleStartMedia(item)"
+            >
+              {{ item.txt }}
+            </n-button>
+          </n-space>
+        </div>
+      </div>
+
+      <div
+        ref="bottomRef"
+        class="room-control"
+      >
+        <div class="info">
+          <div
+            class="avatar"
+            :style="{ backgroundImage: `url(${userStore.userInfo?.avatar})` }"
+          ></div>
+          <div class="detail">
+            <div class="top">
+              <n-input-group>
+                <n-input
+                  v-model:value="roomName"
+                  size="small"
+                  placeholder="输入房间名"
+                  :style="{ width: '50%' }"
+                />
+                <n-button
+                  size="small"
+                  type="primary"
+                  @click="confirmRoomName"
+                >
+                  确定
+                </n-button>
+              </n-input-group>
+            </div>
+            <div class="bottom">
+              <span v-if="NODE_ENV === 'development'">
+                {{ mySocketId }}
+              </span>
+            </div>
+          </div>
+        </div>
+        <div class="rtc">
+          <div class="item">
+            <div class="txt">码率设置</div>
+            <div class="down">
+              <n-select
+                v-model:value="currentMaxBitrate"
+                :options="maxBitrate"
+              />
+            </div>
+          </div>
+          <div class="item">
+            <div class="txt">帧率设置</div>
+            <div class="down">
+              <n-select
+                v-model:value="currentMaxFramerate"
+                :options="maxFramerate"
+              />
+            </div>
+          </div>
+          <div class="item">
+            <div class="txt">分辨率设置</div>
+            <div class="down">
+              <n-select
+                v-model:value="currentResolutionRatio"
+                :options="resolutionRatio"
+              />
+            </div>
+          </div>
+        </div>
+        <div class="other">
+          <div class="top">
+            <span class="item">
+              <i class="ico"></i>
+              <span>
+                正在观看:
+                {{
+                  liveUserList.filter((item) => item.id !== mySocketId).length
+                }}
+              </span>
+            </span>
+          </div>
+          <div class="bottom">
+            <n-button
+              v-if="!isLiving"
+              type="info"
+              size="small"
+              @click="handleStartLive"
+            >
+              开始srs直播
+            </n-button>
+            <n-button
+              v-else
+              type="error"
+              size="small"
+              @click="endLive"
+            >
+              结束直播
+            </n-button>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div class="right">
+      <div class="resource-card">
+        <div class="title">素材列表</div>
+        <div class="list">
+          <div
+            v-for="(item, index) in appStore.allTrack.filter(
+              (item) => !item.hidden
+            )"
+            :key="index"
+            class="item"
+          >
+            <span class="name">
+              ({{ mediaTypeEnumMap[item.type] }}){{ item.mediaName }}
+            </span>
+            <div class="control">
+              <div
+                v-if="item.audio === 1"
+                class="control-item"
+                @click="handleChangeMuted(item)"
+              >
+                <n-icon size="16">
+                  <VolumeMuteOutline v-if="item.muted"></VolumeMuteOutline>
+                  <VolumeHighOutline v-else></VolumeHighOutline>
+                </n-icon>
+              </div>
+              <div
+                class="control-item"
+                @click="handleEdit(item)"
+              >
+                <n-icon size="16">
+                  <CreateOutline></CreateOutline>
+                </n-icon>
+              </div>
+              <div
+                class="control-item"
+                @click="handleDel(item)"
+              >
+                <n-icon size="16">
+                  <Close></Close>
+                </n-icon>
+              </div>
+            </div>
+          </div>
+        </div>
+        <div class="bottom">
+          <n-button
+            size="small"
+            type="primary"
+            @click="showSelectMediaModalCpt = true"
+          >
+            添加素材
+          </n-button>
+        </div>
+      </div>
+      <div class="danmu-card">
+        <div class="title">弹幕互动</div>
+        <div class="list-wrap">
+          <div
+            ref="danmuListRef"
+            class="list"
+          >
+            <div
+              v-for="(item, index) in damuList"
+              :key="index"
+              class="item"
+            >
+              <template v-if="item.msgType === DanmuMsgTypeEnum.danmu">
+                <span class="name">
+                  {{ item.userInfo?.username || item.socket_id }}:
+                </span>
+                <span class="msg">{{ item.msg }}</span>
+              </template>
+              <template v-else-if="item.msgType === DanmuMsgTypeEnum.otherJoin">
+                <span class="name system">系统通知:</span>
+                <span class="msg">
+                  <span>{{ item.userInfo?.username || item.socket_id }}</span>
+                  <span>进入直播!</span>
+                </span>
+              </template>
+              <template
+                v-else-if="item.msgType === DanmuMsgTypeEnum.userLeaved"
+              >
+                <span class="name system">系统通知:</span>
+                <span class="msg">
+                  <span>{{ item.userInfo?.username || item.socket_id }}</span>
+                  <span>离开直播!</span>
+                </span>
+              </template>
+            </div>
+          </div>
+        </div>
+        <div class="send-msg">
+          <input
+            v-model="danmuStr"
+            class="ipt"
+            @keydown="keydownDanmu"
+          />
+          <n-button
+            type="info"
+            size="small"
+            @click="sendDanmu"
+          >
+            发送
+          </n-button>
+        </div>
+      </div>
+    </div>
+
+    <SelectMediaModalCpt
+      v-if="showSelectMediaModalCpt"
+      :all-media-type-list="allMediaTypeList"
+      @close="showSelectMediaModalCpt = false"
+      @ok="selectMediaOk"
+    ></SelectMediaModalCpt>
+
+    <MediaModalCpt
+      v-if="showMediaModalCpt"
+      :media-type="currentMediaType"
+      @close="showMediaModalCpt = false"
+      @ok="addMediaOk"
+    ></MediaModalCpt>
+    <OpenMicophoneTipCpt
+      v-if="showOpenMicophoneTipCpt"
+      @close="showOpenMicophoneTipCpt = false"
+    ></OpenMicophoneTipCpt>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import {
+  Close,
+  CreateOutline,
+  VolumeHighOutline,
+  VolumeMuteOutline,
+} from '@vicons/ionicons5';
+import { fabric } from 'fabric';
+import { UploadFileInfo } from 'naive-ui';
+import {
+  Raw,
+  markRaw,
+  onMounted,
+  onUnmounted,
+  reactive,
+  ref,
+  watch,
+} from 'vue';
+import { useRoute } from 'vue-router';
+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 { AppRootState, useAppStore } from '@/store/app';
+import { useResourceCacheStore } from '@/store/cache';
+import { useUserStore } from '@/store/user';
+import {
+  createVideo,
+  generateBase64,
+  getRandomEnglishString,
+  readFile,
+  saveFile,
+} from '@/utils';
+import { NODE_ENV } from 'script/constant';
+
+import MediaModalCpt from '../mediaModal/index.vue';
+import OpenMicophoneTipCpt from '../openMicophoneTip/index.vue';
+import SelectMediaModalCpt from '../selectMediaModal/index.vue';
+
+const route = useRoute();
+const userStore = useUserStore();
+const appStore = useAppStore();
+const resourceCacheStore = useResourceCacheStore();
+const { maxBitrate, maxFramerate, resolutionRatio, allMediaTypeList } =
+  useRTCParams();
+
+const {
+  confirmRoomName,
+  startLive,
+  endLive,
+  sendDanmu,
+  keydownDanmu,
+  addTrack,
+  delTrack,
+  mySocketId,
+  lastCoverImg,
+  canvasVideoStream,
+  isLiving,
+  currentResolutionRatio,
+  currentMaxBitrate,
+  currentMaxFramerate,
+  danmuStr,
+  roomName,
+  damuList,
+  liveUserList,
+} = usePush();
+
+const currentMediaType = ref(MediaTypeEnum.camera);
+const showOpenMicophoneTipCpt = ref(false);
+const showSelectMediaModalCpt = ref(false);
+const showMediaModalCpt = ref(false);
+const topRef = ref<HTMLDivElement>();
+const bottomRef = ref<HTMLDivElement>();
+const danmuListRef = ref<HTMLDivElement>();
+const containerRef = ref<HTMLDivElement>();
+const pushCanvasRef = ref<HTMLCanvasElement>();
+const fabricCanvas = ref<fabric.Canvas>();
+const audioCtx = ref<AudioContext>();
+const timeCanvasDom = ref<Raw<fabric.Text>[]>([]);
+const stopwatchCanvasDom = ref<Raw<fabric.Text>[]>([]);
+const wrapSize = reactive({
+  width: 0,
+  height: 0,
+});
+const workerTimerId = ref(-1);
+const videoRatio = ref(16 / 9);
+const bodyAppendChildElArr = ref<HTMLElement[]>([]);
+const isSRS = route.query.liveType === liveTypeEnum.srsPush;
+
+watch(
+  () => damuList.value.length,
+  () => {
+    setTimeout(() => {
+      if (danmuListRef.value) {
+        danmuListRef.value.scrollTop = danmuListRef.value.scrollHeight;
+      }
+    }, 0);
+  }
+);
+
+onMounted(() => {
+  setTimeout(() => {
+    scrollTo(0, 0);
+  }, 100);
+  initUserMedia();
+  initCanvas();
+  handleCache();
+});
+
+onUnmounted(() => {
+  bodyAppendChildElArr.value.forEach((el) => {
+    el.remove();
+  });
+  clearFrame();
+});
+
+function initUserMedia() {
+  navigator.mediaDevices
+    .getUserMedia({
+      video: true,
+      audio: false,
+    })
+    .then(() => {
+      console.log('初始化获取摄像头成功');
+    })
+    .catch(() => {
+      console.log('初始化获取摄像头失败');
+    })
+    .finally(() => {
+      navigator.mediaDevices
+        .getUserMedia({
+          video: false,
+          audio: true,
+        })
+        .then(() => {
+          console.log('初始化获取麦克风成功');
+        })
+        .catch(() => {
+          console.log('初始化获取麦克风失败');
+          showOpenMicophoneTipCpt.value = true;
+        });
+    });
+}
+
+function formatDownTime(endTime: number, startTime: number) {
+  const times = (endTime - 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(endTime).getMilliseconds();
+
+  if (h < 10) {
+    // @ts-ignore
+    h = `0${h}`;
+  }
+  if (m < 10) {
+    // @ts-ignore
+    m = `0${m}`;
+  }
+  if (s < 10) {
+    // @ts-ignore
+    s = `0${s}`;
+  }
+  if (Number(ms) < 100) {
+    if (ms < 10) {
+      // @ts-ignore
+      ms = `00${ms}`;
+    } else {
+      // @ts-ignore
+      ms = `0${ms}`;
+    }
+  }
+  if (d > 0) {
+    return `${d}:${h}:${m}:${s}.${ms}`;
+  } else if (h > 0) {
+    return `${h}:${m}:${s}.${ms}`;
+  } else {
+    return `${m}:${s}.${ms}`;
+  }
+}
+
+function renderAll() {
+  timeCanvasDom.value.forEach((item) => {
+    item.text = new Date().toLocaleString();
+  });
+  stopwatchCanvasDom.value.forEach((item) => {
+    item.text = formatDownTime(+new Date(), +new Date());
+  });
+  fabricCanvas.value?.renderAll();
+}
+
+function clearFrame() {
+  if (workerTimerId.value !== -1) {
+    workerTimers.clearInterval(workerTimerId.value);
+  }
+}
+
+function renderFrame() {
+  const delay = 1000 / 60; // 16.666666666666668
+  workerTimerId.value = workerTimers.setInterval(() => {
+    renderAll();
+  }, delay);
+  console.log('workerTimerId.value', workerTimerId.value);
+}
+
+// 处理空音频轨
+function initNullAudio() {
+  console.warn('处理空音频轨');
+  // 创建一个AudioContext实例
+  const audioContext = new AudioContext();
+
+  // 创建一个GainNode实例来控制音频的音量
+  const gainNode = audioContext.createGain();
+
+  // 创建一个空的音频缓存
+  const buffer = audioContext.createBuffer(
+    2,
+    audioContext.sampleRate * 3,
+    audioContext.sampleRate
+  );
+
+  // 创建一个用于播放音频的AudioBufferSourceNode
+  const source = audioContext.createBufferSource();
+  source.buffer = buffer;
+
+  // 将源连接到gain node,再连接到输出
+  source.connect(gainNode);
+  gainNode.connect(audioContext.destination);
+  const destination = audioContext.createMediaStreamDestination();
+
+  const webAudioTrack: AppRootState['allTrack'][0] = {
+    id: getRandomEnglishString(8),
+    audio: 1,
+    video: 2,
+    mediaName: 'webAudio占位',
+    type: MediaTypeEnum.webAudio,
+    track: destination.stream.getAudioTracks()[0],
+    trackid: destination.stream.getAudioTracks()[0].id,
+    stream: destination.stream,
+    streamid: destination.stream.id,
+    hidden: true,
+    muted: false,
+  };
+  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);
+}
+
+let streamTmp: MediaStream;
+let vel;
+
+function handleMixedAudio() {
+  console.log('handleMixedAudiohandleMixedAudio');
+  const allAudioTrack = appStore.allTrack.filter((item) => item.audio === 1);
+  if (audioCtx.value) {
+    const gainNode = audioCtx.value.createGain();
+    allAudioTrack.forEach((item) => {
+      if (!audioCtx.value || !item.stream) return;
+      const audioInput = audioCtx.value.createMediaStreamSource(item.stream);
+      audioInput.connect(gainNode);
+      console.log('混流', item.stream?.id, item.stream);
+    });
+    if (streamTmp) {
+      const destination = audioCtx.value.createMediaStreamDestination();
+      streamTmp.addTrack(destination.stream.getAudioTracks()[0]);
+      gainNode.connect(destination);
+      const mixedStream = new MediaStream();
+      mixedStream.addTrack(destination.stream.getAudioTracks()[0]);
+      mixedStream.addTrack(canvasVideoStream.value!.getVideoTracks()[0]);
+      canvasVideoStream.value = mixedStream;
+      return;
+    }
+    const destination = audioCtx.value.createMediaStreamDestination();
+    streamTmp = destination.stream;
+    // @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.srcObject = destination.stream;
+    document.body.appendChild(vel);
+    bodyAppendChildElArr.value.push(vel);
+  }
+}
+
+function handleStartLive() {
+  // WARN 不能省略initNullAudio,否则开播时候没有音频的时候,srs那边的audio是 Stream #0:0: Audio: aac, 44100 Hz, stereo, 128 kb/s
+  // 会导致加载直播很慢,正常的audio应该是Stream #0:0: Audio: aac (LC), 48000 Hz, stereo, fltp
+  // 开播前执行initNullAudio,audio就会是正常的
+  initNullAudio();
+  if (!audioCtx.value) {
+    audioCtx.value = new AudioContext();
+  }
+  handleMixedAudio();
+  lastCoverImg.value = generateBase64(pushCanvasRef.value!);
+  startLive(isSRS ? LiveRoomTypeEnum.user_srs : LiveRoomTypeEnum.user_wertc);
+}
+
+function handleScale({ width, height }: { width: number; height: number }) {
+  const resolutionHeight =
+    currentResolutionRatio.value * window.devicePixelRatio;
+  const resolutionWidth =
+    currentResolutionRatio.value * window.devicePixelRatio * videoRatio.value;
+  let ratio = 1;
+  if (width > resolutionWidth) {
+    const r1 = resolutionWidth / width;
+    ratio = r1;
+  }
+  if (height > resolutionHeight) {
+    const r1 = resolutionHeight / height;
+    if (ratio > r1) {
+      ratio = r1;
+    }
+  }
+  return ratio;
+}
+
+function autoCreateVideo({
+  stream,
+  id,
+  rect,
+  muted,
+}: {
+  stream: MediaStream;
+  id: string;
+  rect?: { left: number; top: number };
+  muted?: boolean;
+}) {
+  console.warn('autoCreateVideoautoCreateVideo', id);
+  const videoEl = createVideo({});
+  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;
+    videoEl: HTMLVideoElement;
+    scale: number;
+  }>((resolve) => {
+    videoEl.onloadedmetadata = () => {
+      const width = stream.getVideoTracks()[0].getSettings().width!;
+      const height = stream.getVideoTracks()[0].getSettings().height!;
+      const ratio = handleScale({ width, height });
+      videoEl.width = width;
+      videoEl.height = height;
+
+      const canvasDom = markRaw(
+        new fabric.Image(videoEl, {
+          top: rect?.top || 0,
+          left: rect?.left || 0,
+          width,
+          height,
+        })
+      );
+      console.log(
+        '初始化',
+        ratio,
+        canvasDom.width,
+        canvasDom.height,
+        width * ratio,
+        height * ratio,
+        canvasDom
+      );
+      handleMoving({ canvasDom, id });
+      handleScaling({ canvasDom, id });
+      canvasDom.scale(ratio / window.devicePixelRatio);
+      fabricCanvas.value!.add(canvasDom);
+
+      resolve({ canvasDom, scale: ratio, videoEl });
+    };
+  });
+}
+
+watch(
+  () => currentResolutionRatio.value,
+  (newHeight, oldHeight) => {
+    changeCanvasAttr({ newHeight, oldHeight });
+  }
+);
+
+// 容器宽高,1280*720,即720p
+// canvas容器宽高,2560*1440,即1440p
+
+// ======
+// 容器宽高,960*540,即540p
+// dom宽高,640*480
+// canvas容器宽高,960*540,即540p
+// 将dom绘制到容器里,此时dom的大小就是640*480
+// 需求,不管切换多少分辨率,我要看到的dom都是一样大小,即
+// 960*540时,dom是640*480
+// 1280*720时,dom不能是640*480了,因为这样他就会对比上一个分辨率的dom看起来小了,960/1280=0.75,540/720=0.75,
+// 其实就是分辨率变大了,我们就要将图片也变大,即图片的宽是640/0.75=853.4,高是480/0.75=640
+// 坐标变化,960*540时,dom坐标是100,100
+// 1280*720时,dom的坐标不能再是100,100了,否则对比上一个分辨率看起来偏
+
+function changeCanvasAttr({
+  newHeight,
+  oldHeight,
+}: {
+  newHeight: number;
+  oldHeight: number;
+}) {
+  if (fabricCanvas.value) {
+    const resolutionHeight =
+      currentResolutionRatio.value / window.devicePixelRatio;
+    const resolutionWidth =
+      (currentResolutionRatio.value / window.devicePixelRatio) *
+      videoRatio.value;
+    fabricCanvas.value.setWidth(resolutionWidth);
+    fabricCanvas.value.setHeight(resolutionHeight);
+    appStore.allTrack.forEach((iten) => {
+      console.log('当前类型', iten.type);
+      const item = iten.canvasDom;
+
+      if (item) {
+        // 分辨率变小了,将图片变小
+        if (newHeight < oldHeight) {
+          const ratio2 = oldHeight / newHeight;
+          item.left = item.left! / ratio2;
+          item.top = item.top! / ratio2;
+        } else {
+          // 分辨率变大了,将图片变大
+          const ratio2 = oldHeight / newHeight;
+          item.left = item.left! / ratio2;
+          item.top = item.top! / ratio2;
+        }
+      }
+    });
+    appStore.allTrack.forEach((iten) => {
+      console.log('当前类型', iten.type);
+      const item = iten.canvasDom;
+
+      if (item) {
+        // 分辨率变小了,将图片变小
+        if (newHeight < oldHeight) {
+          const ratio = newHeight / oldHeight;
+          const ratio1 = (item.scaleX || 1) * ratio;
+          item.scale(ratio1);
+        } else {
+          // 分辨率变大了,将图片变大
+          const ratio = newHeight / oldHeight;
+          const ratio1 = (item.scaleX || 1) * ratio;
+          item.scale(ratio1);
+        }
+      }
+    });
+
+    changeCanvasStyle();
+  }
+}
+
+function changeCanvasStyle() {
+  // @ts-ignore
+  fabricCanvas.value.wrapperEl.style.width = `${wrapSize.width}px`;
+  // @ts-ignore
+  fabricCanvas.value.wrapperEl.style.height = `${wrapSize.height}px`;
+  // @ts-ignore
+  fabricCanvas.value.lowerCanvasEl.style.width = `${wrapSize.width}px`;
+  // @ts-ignore
+  fabricCanvas.value.lowerCanvasEl.style.height = `${wrapSize.height}px`;
+  // @ts-ignore
+  fabricCanvas.value.upperCanvasEl.style.width = `${wrapSize.width}px`;
+  // @ts-ignore
+  fabricCanvas.value.upperCanvasEl.style.height = `${wrapSize.height}px`;
+}
+
+function initCanvas() {
+  const resolutionHeight =
+    currentResolutionRatio.value / window.devicePixelRatio;
+  const resolutionWidth =
+    (currentResolutionRatio.value / window.devicePixelRatio) * videoRatio.value;
+  const wrapWidth = containerRef.value!.getBoundingClientRect().width;
+  // const wrapWidth = 1920;
+  const ratio = wrapWidth / resolutionWidth;
+  const wrapHeight = resolutionHeight * ratio;
+  // const wrapHeight = 1080;
+  // lower-canvas: 实际的canvas画面,也就是pushCanvasRef
+  // upper-canvas: 操作时候的canvas
+  const ins = markRaw(new fabric.Canvas(pushCanvasRef.value!));
+  ins.setWidth(resolutionWidth);
+  ins.setHeight(resolutionHeight);
+  console.log('initCanvas', { resolutionWidth, resolutionHeight });
+  ins.setBackgroundColor('black', () => {
+    console.log('setBackgroundColor回调');
+  });
+  wrapSize.width = wrapWidth;
+  wrapSize.height = wrapHeight;
+  fabricCanvas.value = ins;
+  renderFrame();
+  changeCanvasStyle();
+}
+
+function handleScaling({ canvasDom, id }) {
+  canvasDom.on('scaling', () => {
+    appStore.allTrack.forEach((item) => {
+      if (id === item.id) {
+        item.scaleInfo = {
+          scaleX: canvasDom.scaleX || 1,
+          scaleY: canvasDom.scaleY || 1,
+        };
+      }
+    });
+    resourceCacheStore.setList(appStore.allTrack);
+  });
+}
+function handleMoving({
+  canvasDom,
+  id,
+}: {
+  canvasDom: fabric.Image | fabric.Text;
+  id: string;
+}) {
+  canvasDom.on('moving', () => {
+    console.log(
+      'moving',
+      canvasDom.width,
+      canvasDom.height,
+      canvasDom.scaleX,
+      canvasDom.scaleY
+    );
+    appStore.allTrack.forEach((item) => {
+      if (id === item.id) {
+        item.rect = {
+          top: (canvasDom.top || 0) * window.devicePixelRatio,
+          left: (canvasDom.left || 0) * window.devicePixelRatio,
+        };
+      }
+    });
+    resourceCacheStore.setList(appStore.allTrack);
+  });
+}
+
+async function handleCache() {
+  const res: AppRootState['allTrack'] = [];
+  const queue: any[] = [];
+  resourceCacheStore.list.forEach((item) => {
+    // @ts-ignore
+    const obj: AppRootState['allTrack'][0] = {};
+    obj.audio = item.audio;
+    obj.video = item.video;
+    obj.id = item.id;
+    obj.type = item.type;
+    obj.hidden = item.hidden;
+    obj.mediaName = item.mediaName;
+    obj.muted = item.muted;
+    obj.rect = item.rect;
+    obj.scaleInfo = item.scaleInfo;
+    obj.stopwatchInfo = item.stopwatchInfo;
+
+    async function handleMediaVideo() {
+      const { code, file } = await readFile(item.id);
+      if (code === 1 && file) {
+        const url = URL.createObjectURL(file);
+        const videoEl = createVideo({});
+        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 = () => {
+            const stream = videoEl
+              // @ts-ignore
+              .captureStream();
+            const width = stream.getVideoTracks()[0].getSettings().width!;
+            const height = stream.getVideoTracks()[0].getSettings().height!;
+            videoEl.width = width;
+            videoEl.height = height;
+
+            const canvasDom = markRaw(
+              new fabric.Image(videoEl, {
+                top: (item.rect?.top || 0) / window.devicePixelRatio,
+                left: (item.rect?.left || 0) / window.devicePixelRatio,
+                width,
+                height,
+              })
+            );
+            handleMoving({ canvasDom, id: item.id });
+            handleScaling({ canvasDom, id: item.id });
+            canvasDom.scale(
+              (item.scaleInfo?.scaleX || 1) / window.devicePixelRatio
+            );
+            fabricCanvas.value!.add(canvasDom);
+            obj.videoEl = videoEl;
+            obj.canvasDom = canvasDom;
+            resolve({ videoEl, canvasDom });
+          };
+        });
+        const stream = videoEl
+          // @ts-ignore
+          .captureStream() as MediaStream;
+        obj.stream = stream;
+        obj.streamid = stream.id;
+        obj.track = stream.getVideoTracks()[0];
+        obj.trackid = stream.getVideoTracks()[0].id;
+      } else {
+        console.error('读取文件失败');
+      }
+    }
+
+    async function handleImg() {
+      const { code, file } = await readFile(item.id);
+      if (code === 1 && file) {
+        const imgEl = await new Promise<HTMLImageElement>((resolve) => {
+          const reader = new FileReader();
+          reader.addEventListener(
+            'load',
+            function () {
+              const img = document.createElement('img');
+              img.src = reader.result as string;
+              img.onload = () => {
+                resolve(img);
+              };
+            },
+            false
+          );
+          if (file) {
+            reader.readAsDataURL(file);
+          }
+        });
+        if (fabricCanvas.value) {
+          const canvasDom = markRaw(
+            new fabric.Image(imgEl, {
+              top: (item.rect?.top || 0) / window.devicePixelRatio,
+              left: (item.rect?.left || 0) / window.devicePixelRatio,
+              width: imgEl.width,
+              height: imgEl.height,
+            })
+          );
+          handleMoving({ canvasDom, id: obj.id });
+          handleScaling({ canvasDom, id: obj.id });
+          canvasDom.scale(
+            (item.scaleInfo?.scaleX || 1) / window.devicePixelRatio
+          );
+          fabricCanvas.value.add(canvasDom);
+          obj.canvasDom = canvasDom;
+        }
+      } else {
+        console.error('读取文件失败');
+      }
+    }
+    if (item.type === MediaTypeEnum.media && item.video === 1) {
+      queue.push(handleMediaVideo());
+    } else if (item.type === MediaTypeEnum.img) {
+      queue.push(handleImg());
+    } else if (item.type === MediaTypeEnum.txt) {
+      obj.txtInfo = item.txtInfo;
+      obj.scaleInfo = item.scaleInfo;
+      if (fabricCanvas.value) {
+        const canvasDom = markRaw(
+          new fabric.Text(item.txtInfo?.txt || '', {
+            top: (item.rect?.top || 0) / window.devicePixelRatio,
+            left: (item.rect?.left || 0) / window.devicePixelRatio,
+            fill: item.txtInfo?.color,
+          })
+        );
+        handleMoving({ canvasDom, id: obj.id });
+        handleScaling({ canvasDom, id: obj.id });
+        // fabric.Text类型不能除以分辨率
+        canvasDom.scale(item.scaleInfo?.scaleX || 1);
+        fabricCanvas.value.add(canvasDom);
+        obj.canvasDom = canvasDom;
+      }
+    } else if (item.type === MediaTypeEnum.time) {
+      obj.timeInfo = item.timeInfo;
+      obj.scaleInfo = item.scaleInfo;
+      if (fabricCanvas.value) {
+        const canvasDom = markRaw(
+          new fabric.Text(new Date().toLocaleString(), {
+            top: (item.rect?.top || 0) / window.devicePixelRatio,
+            left: (item.rect?.left || 0) / window.devicePixelRatio,
+            fill: item.timeInfo?.color,
+          })
+        );
+        timeCanvasDom.value.push(canvasDom);
+        handleMoving({ canvasDom, id: obj.id });
+        handleScaling({ canvasDom, id: obj.id });
+        // fabric.Text类型不能除以分辨率
+        canvasDom.scale(item.scaleInfo?.scaleX || 1);
+        fabricCanvas.value.add(canvasDom);
+        obj.canvasDom = canvasDom;
+      }
+    } else if (item.type === MediaTypeEnum.stopwatch) {
+      obj.stopwatchInfo = item.stopwatchInfo;
+      obj.scaleInfo = item.scaleInfo;
+      if (fabricCanvas.value) {
+        const canvasDom = markRaw(
+          new fabric.Text('00:00:00.000', {
+            top: (item.rect?.top || 0) / window.devicePixelRatio,
+            left: (item.rect?.left || 0) / window.devicePixelRatio,
+            fill: item.stopwatchInfo?.color,
+          })
+        );
+        stopwatchCanvasDom.value.push(canvasDom);
+        handleMoving({ canvasDom, id: obj.id });
+        handleScaling({ canvasDom, id: obj.id });
+        // fabric.Text类型不能除以分辨率
+        canvasDom.scale(item.scaleInfo?.scaleX || 1);
+        fabricCanvas.value.add(canvasDom);
+        obj.canvasDom = canvasDom;
+      }
+    }
+    res.push(obj);
+  });
+  await Promise.all(queue);
+  canvasVideoStream.value = pushCanvasRef.value!.captureStream();
+  appStore.setAllTrack(res);
+}
+
+function selectMediaOk(val: MediaTypeEnum) {
+  showMediaModalCpt.value = true;
+  showSelectMediaModalCpt.value = false;
+  currentMediaType.value = val;
+}
+
+async function addMediaOk(val: {
+  type: MediaTypeEnum;
+  deviceId: string;
+  mediaName: string;
+  txtInfo?: { txt: string; color: string };
+  timeInfo?: { color: string };
+  stopwatchInfo?: { color: string };
+  imgInfo?: UploadFileInfo[];
+  mediaInfo?: UploadFileInfo[];
+}) {
+  if (!audioCtx.value) {
+    audioCtx.value = new AudioContext();
+  }
+  showMediaModalCpt.value = false;
+  if (val.type === MediaTypeEnum.screen) {
+    const event = await navigator.mediaDevices.getDisplayMedia({
+      video: {
+        deviceId: val.deviceId,
+        // displaySurface: 'monitor', // browser默认标签页;window默认窗口;monitor默认整个屏幕
+      },
+      audio: true,
+    });
+
+    const videoTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(8),
+      audio: 2,
+      video: 1,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.screen,
+      track: event.getVideoTracks()[0],
+      trackid: event.getVideoTracks()[0].id,
+      stream: event,
+      streamid: event.id,
+      hidden: false,
+      muted: false,
+    };
+
+    const { canvasDom, videoEl, scale } = await autoCreateVideo({
+      stream: event,
+      id: videoTrack.id,
+    });
+    videoTrack.scaleInfo = { scaleX: scale, scaleY: scale };
+    videoTrack.videoEl = videoEl;
+    // @ts-ignore
+    videoTrack.canvasDom = canvasDom;
+
+    const audio = event.getAudioTracks();
+    if (audio.length) {
+      videoTrack.audio = 1;
+      const audioTrack: AppRootState['allTrack'][0] = {
+        id: videoTrack.id,
+        audio: 1,
+        video: 2,
+        mediaName: val.mediaName,
+        type: MediaTypeEnum.screen,
+        track: event.getAudioTracks()[0],
+        trackid: event.getAudioTracks()[0].id,
+        stream: event,
+        streamid: event.id,
+        hidden: true,
+        muted: false,
+      };
+      const res = [...appStore.allTrack, videoTrack, audioTrack];
+      appStore.setAllTrack(res);
+      resourceCacheStore.setList(res);
+      handleMixedAudio();
+      // @ts-ignore
+      addTrack(videoTrack);
+      // @ts-ignore
+      addTrack(audioTrack);
+    } else {
+      const res = [...appStore.allTrack, videoTrack];
+      appStore.setAllTrack(res);
+      resourceCacheStore.setList(res);
+      // @ts-ignore
+      addTrack(videoTrack);
+    }
+
+    console.log('获取窗口成功');
+  } else if (val.type === MediaTypeEnum.camera) {
+    const event = await navigator.mediaDevices.getUserMedia({
+      video: {
+        deviceId: val.deviceId,
+      },
+      audio: false,
+    });
+    const videoTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(8),
+      audio: 2,
+      video: 1,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.camera,
+      track: event.getVideoTracks()[0],
+      trackid: event.getVideoTracks()[0].id,
+      stream: event,
+      streamid: event.id,
+      hidden: false,
+      muted: false,
+    };
+    const { canvasDom, videoEl, scale } = await autoCreateVideo({
+      stream: event,
+      id: videoTrack.id,
+    });
+    videoTrack.scaleInfo = { scaleX: scale, scaleY: scale };
+    videoTrack.videoEl = videoEl;
+    // @ts-ignore
+    videoTrack.canvasDom = canvasDom;
+
+    const res = [...appStore.allTrack, videoTrack];
+    appStore.setAllTrack(res);
+    resourceCacheStore.setList(res);
+    // @ts-ignore
+    addTrack(videoTrack);
+    console.log('获取摄像头成功');
+  } else if (val.type === MediaTypeEnum.microphone) {
+    const event = await navigator.mediaDevices.getUserMedia({
+      video: false,
+      audio: { deviceId: val.deviceId },
+    });
+    const audioTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(8),
+      audio: 1,
+      video: 2,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.microphone,
+      track: event.getAudioTracks()[0],
+      trackid: event.getAudioTracks()[0].id,
+      stream: event,
+      streamid: event.id,
+      hidden: false,
+      muted: false,
+    };
+    const res = [...appStore.allTrack, audioTrack];
+    appStore.setAllTrack(res);
+    resourceCacheStore.setList(res);
+    handleMixedAudio();
+    // @ts-ignore
+    addTrack(audioTrack);
+
+    console.log('获取麦克风成功');
+  } else if (val.type === MediaTypeEnum.txt) {
+    const txtTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(8),
+      audio: 2,
+      video: 1,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.txt,
+      track: undefined,
+      trackid: undefined,
+      stream: undefined,
+      streamid: undefined,
+      hidden: false,
+      muted: false,
+    };
+    if (fabricCanvas.value) {
+      console.log('val.txtInfo?.txt ', val.txtInfo?.txt);
+      const canvasDom = markRaw(
+        new fabric.Text(val.txtInfo?.txt || '', {
+          top: 0,
+          left: 0,
+          fill: val.txtInfo?.color,
+        })
+      );
+      handleMoving({ canvasDom, id: txtTrack.id });
+      handleScaling({ canvasDom, id: txtTrack.id });
+      txtTrack.txtInfo = val.txtInfo;
+      txtTrack.scaleInfo = { scaleX: 1, scaleY: 1 };
+      // @ts-ignore
+      txtTrack.canvasDom = canvasDom;
+      fabricCanvas.value.add(canvasDom);
+    }
+
+    const res = [...appStore.allTrack, txtTrack];
+    // @ts-ignore
+    appStore.setAllTrack(res);
+    // @ts-ignore
+    resourceCacheStore.setList(res);
+    // @ts-ignore
+    addTrack(txtTrack);
+
+    console.log('获取文字成功', fabricCanvas.value);
+  } else if (val.type === MediaTypeEnum.time) {
+    const timeTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(8),
+      audio: 2,
+      video: 1,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.time,
+      track: undefined,
+      trackid: undefined,
+      stream: undefined,
+      streamid: undefined,
+      hidden: false,
+      muted: false,
+    };
+    if (fabricCanvas.value) {
+      const canvasDom = markRaw(
+        new fabric.Text(new Date().toLocaleString(), {
+          top: 0,
+          left: 0,
+          fill: val.timeInfo?.color,
+        })
+      );
+      timeCanvasDom.value.push(canvasDom);
+      handleMoving({ canvasDom, id: timeTrack.id });
+      handleScaling({ canvasDom, id: timeTrack.id });
+      timeTrack.timeInfo = val.timeInfo;
+      timeTrack.scaleInfo = { scaleX: 1, scaleY: 1 };
+
+      // @ts-ignore
+      timeTrack.canvasDom = canvasDom;
+      fabricCanvas.value.add(canvasDom);
+    }
+
+    const res = [...appStore.allTrack, timeTrack];
+    // @ts-ignore
+    appStore.setAllTrack(res);
+    // @ts-ignore
+    resourceCacheStore.setList(res);
+    // @ts-ignore
+    addTrack(timeTrack);
+
+    console.log('获取时间成功', fabricCanvas.value);
+  } else if (val.type === MediaTypeEnum.stopwatch) {
+    const stopwatchTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(8),
+      audio: 2,
+      video: 1,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.stopwatch,
+      track: undefined,
+      trackid: undefined,
+      stream: undefined,
+      streamid: undefined,
+      hidden: false,
+      muted: false,
+    };
+    if (fabricCanvas.value) {
+      const canvasDom = markRaw(
+        new fabric.Text('00:00:00.000', {
+          top: 0,
+          left: 0,
+          fill: val.stopwatchInfo?.color,
+          // editable: true,
+        })
+      );
+      stopwatchCanvasDom.value.push(canvasDom);
+      handleMoving({ canvasDom, id: stopwatchTrack.id });
+      handleScaling({ canvasDom, id: stopwatchTrack.id });
+      stopwatchTrack.stopwatchInfo = val.stopwatchInfo;
+      // @ts-ignore
+      stopwatchTrack.canvasDom = canvasDom;
+      fabricCanvas.value.add(canvasDom);
+    }
+
+    const res = [...appStore.allTrack, stopwatchTrack];
+    // @ts-ignore
+    appStore.setAllTrack(res);
+    // @ts-ignore
+    resourceCacheStore.setList(res);
+    // @ts-ignore
+    addTrack(stopwatchTrack);
+
+    console.log('获取秒表成功', fabricCanvas.value);
+  } else if (val.type === MediaTypeEnum.img) {
+    const imgTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(8),
+      audio: 2,
+      video: 1,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.img,
+      track: undefined,
+      trackid: undefined,
+      stream: undefined,
+      streamid: undefined,
+      hidden: false,
+      muted: false,
+    };
+
+    if (fabricCanvas.value) {
+      if (!val.imgInfo) return;
+      const file = val.imgInfo[0].file!;
+      const { code } = await saveFile({ file, fileName: imgTrack.id });
+      if (code !== 1) return;
+      const imgEl = await new Promise<HTMLImageElement>((resolve) => {
+        const reader = new FileReader();
+        reader.addEventListener(
+          'load',
+          function () {
+            const img = document.createElement('img');
+            img.src = reader.result as string;
+            img.onload = () => {
+              resolve(img);
+            };
+          },
+          false
+        );
+        if (file) {
+          reader.readAsDataURL(file);
+        }
+      });
+
+      const canvasDom = markRaw(
+        new fabric.Image(imgEl, {
+          top: 0,
+          left: 0,
+          width: imgEl.width,
+          height: imgEl.height,
+        })
+      );
+      handleMoving({ canvasDom, id: imgTrack.id });
+      handleScaling({ canvasDom, id: imgTrack.id });
+      const ratio = handleScale({ width: imgEl.width, height: imgEl.height });
+      // @ts-ignore
+      imgTrack.canvasDom = canvasDom;
+      imgTrack.scaleInfo = { scaleX: ratio, scaleY: ratio };
+      canvasDom.scale(ratio / window.devicePixelRatio);
+      fabricCanvas.value.add(canvasDom);
+    }
+
+    const res = [...appStore.allTrack, imgTrack];
+    // @ts-ignore
+    appStore.setAllTrack(res);
+    // @ts-ignore
+    resourceCacheStore.setList(res);
+    // @ts-ignore
+    addTrack(imgTrack);
+
+    console.log('获取图片成功', fabricCanvas.value);
+  } else if (val.type === MediaTypeEnum.media) {
+    const mediaVideoTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(8),
+      audio: 2,
+      video: 1,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.media,
+      track: undefined,
+      trackid: undefined,
+      stream: undefined,
+      streamid: undefined,
+      hidden: false,
+      muted: false,
+    };
+    if (fabricCanvas.value) {
+      if (!val.mediaInfo) return;
+      const file = val.mediaInfo[0].file!;
+      const { code } = await saveFile({ file, fileName: mediaVideoTrack.id });
+      if (code !== 1) return;
+      const url = URL.createObjectURL(file);
+      const videoEl = createVideo({});
+      videoEl.src = url;
+      videoEl.muted = false;
+      videoEl.style.width = `1px`;
+      videoEl.style.height = `1px`;
+      videoEl.style.position = 'fixed';
+      videoEl.style.bottom = '0';
+      videoEl.style.right = '0';
+      videoEl.style.opacity = '0';
+      videoEl.style.pointerEvents = 'none';
+      document.body.appendChild(videoEl);
+      bodyAppendChildElArr.value.push(videoEl);
+      const videoRes = await new Promise<HTMLVideoElement>((resolve) => {
+        videoEl.onloadedmetadata = () => {
+          resolve(videoEl);
+        };
+      });
+      // @ts-ignore
+      const stream = videoRes.captureStream();
+      const { canvasDom, scale } = await autoCreateVideo({
+        stream,
+        id: mediaVideoTrack.id,
+      });
+      mediaVideoTrack.scaleInfo = { scaleX: scale, scaleY: scale };
+      mediaVideoTrack.videoEl = videoEl;
+      // @ts-ignore
+      mediaVideoTrack.canvasDom = canvasDom;
+      if (stream.getAudioTracks()[0]) {
+        console.log('视频有音频', stream.getAudioTracks()[0]);
+        mediaVideoTrack.audio = 1;
+        const audioTrack: AppRootState['allTrack'][0] = {
+          id: mediaVideoTrack.id,
+          audio: 1,
+          video: 2,
+          mediaName: val.mediaName,
+          type: MediaTypeEnum.media,
+          track: stream.getAudioTracks()[0],
+          trackid: stream.getAudioTracks()[0].id,
+          stream,
+          streamid: stream.id,
+          hidden: true,
+          muted: false,
+        };
+        // @ts-ignore
+        const res = [...appStore.allTrack, audioTrack];
+        appStore.setAllTrack(res);
+        resourceCacheStore.setList(res);
+        handleMixedAudio();
+        // @ts-ignore
+
+        addTrack(audioTrack);
+      }
+    }
+    const res = [...appStore.allTrack, mediaVideoTrack];
+    // @ts-ignore
+    appStore.setAllTrack(res);
+    // @ts-ignore
+    resourceCacheStore.setList(res);
+    // @ts-ignore
+
+    addTrack(mediaVideoTrack);
+
+    console.log('获取视频成功', fabricCanvas.value);
+  }
+
+  canvasVideoStream.value = pushCanvasRef.value!.captureStream();
+}
+
+function handleChangeMuted(item: AppRootState['allTrack'][0]) {
+  if (item.videoEl) {
+    const res = !item.videoEl.muted;
+    item.videoEl.muted = res;
+    item.muted = res;
+    resourceCacheStore.setList(appStore.allTrack);
+  }
+}
+
+function handleEdit(item: AppRootState['allTrack'][0]) {
+  console.log('handleEdit', item);
+}
+
+function handleDel(item: AppRootState['allTrack'][0]) {
+  console.log('handleDel', item);
+  if (item.canvasDom !== undefined) {
+    // @ts-ignore
+    fabricCanvas.value?.remove(item.canvasDom);
+    item.videoEl?.remove();
+  }
+  const res = appStore.allTrack.filter((iten) => iten.id !== item.id);
+  appStore.setAllTrack(res);
+  resourceCacheStore.setList(res);
+  delTrack(item);
+}
+
+function handleStartMedia(item: { type: MediaTypeEnum; txt: string }) {
+  currentMediaType.value = item.type;
+  showMediaModalCpt.value = true;
+}
+</script>
+
+<style lang="scss" scoped>
+.push-wrap {
+  display: flex;
+  justify-content: space-between;
+  margin: 15px auto 0;
+  width: $w-1250;
+  .left {
+    position: relative;
+    display: inline-block;
+    overflow: hidden;
+    box-sizing: border-box;
+    width: $w-960;
+    height: 100%;
+    border-radius: 6px;
+    background-color: white;
+    color: #9499a0;
+    vertical-align: top;
+
+    .container {
+      position: relative;
+      overflow: hidden;
+      height: 100%;
+      background-color: rgba($color: #000000, $alpha: 0.5);
+      line-height: 0;
+
+      .add-wrap {
+        position: absolute;
+        top: 50%;
+        left: 50%;
+        display: flex;
+        align-items: center;
+        justify-content: space-around;
+        padding: 10px 20px;
+        width: 50%;
+        border-radius: 6px;
+        background-color: white;
+        transform: translate(-50%, -50%);
+      }
+    }
+    .room-control {
+      display: flex;
+      justify-content: space-between;
+      padding: 20px;
+      background-color: papayawhip;
+
+      .info {
+        display: flex;
+        align-items: center;
+
+        .avatar {
+          margin-right: 20px;
+          width: 55px;
+          height: 55px;
+          border-radius: 50%;
+          background-position: center center;
+          background-size: cover;
+          background-repeat: no-repeat;
+        }
+        .detail {
+          display: flex;
+          flex-direction: column;
+          flex-shrink: 0;
+          width: 200px;
+          text-align: initial;
+          .top {
+            margin-bottom: 10px;
+            color: #18191c;
+          }
+          .bottom {
+            font-size: 14px;
+          }
+        }
+      }
+      .rtc {
+        display: flex;
+        align-items: center;
+        flex: 1;
+        font-size: 14px;
+        .item {
+          display: flex;
+          align-items: center;
+          flex: 1;
+          .txt {
+            flex-shrink: 0;
+            width: 80px;
+          }
+          .down {
+            width: 90px;
+
+            user-select: none;
+          }
+        }
+      }
+      .other {
+        display: flex;
+        flex-direction: column;
+        justify-content: center;
+        font-size: 12px;
+        .top {
+        }
+        .bottom {
+          margin-top: 10px;
+        }
+      }
+    }
+  }
+  .right {
+    display: flex;
+    flex-direction: column;
+    justify-content: space-between;
+    box-sizing: border-box;
+    margin-left: 10px;
+    width: $w-250;
+    border-radius: 6px;
+    background-color: white;
+    color: #9499a0;
+
+    .resource-card {
+      position: relative;
+      box-sizing: border-box;
+      margin-bottom: 10px;
+      padding: 10px;
+      width: 100%;
+      height: 290px;
+      border-radius: 6px;
+      background-color: papayawhip;
+      .title {
+        text-align: initial;
+      }
+      .item {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        margin: 5px 0;
+        font-size: 14px;
+        // &:hover {
+        //   .control {
+        //     display: flex;
+        //     align-items: center;
+        //   }
+        // }
+        .control {
+          display: flex;
+          align-items: center;
+          .control-item {
+            cursor: pointer;
+            &:not(:last-child) {
+              margin-right: 6px;
+            }
+          }
+        }
+      }
+      .bottom {
+        position: absolute;
+        bottom: 0;
+        left: 0;
+        padding: 10px;
+      }
+    }
+    .danmu-card {
+      position: relative;
+      flex: 1;
+      box-sizing: border-box;
+      padding: 10px;
+      width: 100%;
+      border-radius: 6px;
+      background-color: papayawhip;
+      text-align: initial;
+      .title {
+        margin-bottom: 10px;
+      }
+      .list {
+        overflow: scroll;
+        height: 360px;
+
+        @extend %hideScrollbar;
+
+        .item {
+          margin-bottom: 10px;
+          font-size: 12px;
+          .name {
+            color: #9499a0;
+          }
+          .msg {
+            color: #61666d;
+          }
+        }
+      }
+
+      .send-msg {
+        position: absolute;
+        bottom: 10px;
+        left: 50%;
+        display: flex;
+        align-items: center;
+        box-sizing: border-box;
+        width: calc(100% - 20px);
+        transform: translateX(-50%);
+        .ipt {
+          display: block;
+          box-sizing: border-box;
+          margin: 0 auto;
+          margin-right: 10px;
+          padding: 10px;
+          width: 80%;
+          height: 30px;
+          outline: none;
+          border: 1px solid hsla(0, 0%, 60%, 0.2);
+          border-radius: 4px;
+          background-color: #f1f2f3;
+          font-size: 14px;
+        }
+      }
+    }
+  }
+}
+
+// 屏幕宽度大于1500的时候
+@media screen and (min-width: $w-1500) {
+  .push-wrap {
+    width: $w-1475;
+    .left {
+      width: $w-1150;
+    }
+    .right {
+      width: $w-300;
+    }
+  }
+}
+</style>

+ 0 - 742
src/views/push2/index.vue

@@ -1,742 +0,0 @@
-<template>
-  <div class="push-wrap">
-    <div
-      ref="topRef"
-      class="left"
-    >
-      <div
-        ref="containerRef"
-        class="container"
-      >
-        <div class="video-wrap">
-          <div
-            ref="localVideoRef"
-            class="media-list"
-            :class="{ item: appStore.allTrack.length > 1 }"
-          ></div>
-          <div
-            v-if="!appStore.allTrack || appStore.allTrack.length <= 0"
-            class="add-wrap"
-          >
-            <n-space>
-              <n-button
-                v-for="(item, index) in allMediaTypeList"
-                :key="index"
-                class="item"
-                @click="handleStartMedia(item)"
-              >
-                {{ item.txt }}
-              </n-button>
-            </n-space>
-          </div>
-        </div>
-
-        <div class="sidebar">
-          <div class="title">在线人员</div>
-          <div
-            v-for="(item, index) in liveUserList.filter(
-              (item) => item.id !== getSocketId()
-            )"
-            :key="index"
-            class="item"
-          >
-            <video
-              :ref="(el) => (remoteVideoRef[item.id] = el)"
-              autoplay
-              webkit-playsinline="true"
-              playsinline
-              x-webkit-airplay="allow"
-              x5-video-player-type="h5"
-              x5-video-player-fullscreen="true"
-              x5-video-orientation="portraint"
-              muted
-            ></video>
-            <div>{{ item.userInfo?.username || item.id }}</div>
-          </div>
-        </div>
-      </div>
-
-      <div
-        ref="bottomRef"
-        class="room-control"
-      >
-        <div class="info">
-          <div
-            class="avatar"
-            :style="{ backgroundImage: `url(${userStore.userInfo?.avatar})` }"
-          ></div>
-          <div class="detail">
-            <div class="top">
-              <n-input-group>
-                <n-input
-                  v-model:value="roomName"
-                  size="small"
-                  placeholder="输入房间名"
-                  :style="{ width: '50%' }"
-                />
-                <n-button
-                  size="small"
-                  type="primary"
-                  @click="confirmRoomName"
-                >
-                  确定
-                </n-button>
-              </n-input-group>
-            </div>
-            <div class="bottom">
-              <span v-if="NODE_ENV === 'development'">
-                socketId:{{ getSocketId() }}
-              </span>
-            </div>
-          </div>
-        </div>
-        <div class="rtc">
-          <div class="item">
-            <div class="txt">码率设置</div>
-            <div class="down">
-              <n-select
-                v-model:value="currentMaxBitrate"
-                :options="maxBitrate"
-              />
-            </div>
-          </div>
-          <div class="item">
-            <div class="txt">帧率设置</div>
-            <div class="down">
-              <n-select
-                v-model:value="currentMaxFramerate"
-                :options="maxFramerate"
-              />
-            </div>
-          </div>
-          <div class="item">
-            <div class="txt">分辨率设置</div>
-            <div class="down">
-              <n-select
-                v-model:value="currentResolutionRatio"
-                :options="resolutionRatio"
-              />
-            </div>
-          </div>
-        </div>
-        <div class="other">
-          <div class="top">
-            <span class="item">
-              <i class="ico"></i>
-              <span>
-                正在观看:
-                {{
-                  liveUserList.filter((item) => item.id !== getSocketId())
-                    .length
-                }}
-              </span>
-            </span>
-          </div>
-          <div class="bottom">
-            <n-button
-              v-if="!isLiving"
-              type="info"
-              size="small"
-              @click="startLive"
-            >
-              开始直播
-            </n-button>
-            <n-button
-              v-else
-              type="error"
-              size="small"
-              @click="endLive"
-            >
-              结束直播
-            </n-button>
-          </div>
-        </div>
-      </div>
-    </div>
-
-    <div class="right">
-      <div class="resource-card">
-        <div class="title">素材列表</div>
-        <div class="list">
-          <div
-            v-for="(item, index) in appStore.allTrack"
-            :key="index"
-            class="item"
-          >
-            <span class="name">
-              ({{ item.audio === 1 ? '音频' : '视频' }}){{ item.mediaName }}
-            </span>
-            <div
-              class="del"
-              @click="handleDelTrack(item)"
-            >
-              x
-            </div>
-          </div>
-        </div>
-        <div class="bottom">
-          <n-button
-            size="small"
-            type="primary"
-            @click="showSelectMediaModalCpt = true"
-          >
-            添加素材
-          </n-button>
-        </div>
-      </div>
-      <div class="danmu-card">
-        <div class="title">弹幕互动</div>
-        <div class="list-wrap">
-          <div
-            ref="danmuListRef"
-            class="list"
-          >
-            <div
-              v-for="(item, index) in damuList"
-              :key="index"
-              class="item"
-            >
-              <template v-if="item.msgType === DanmuMsgTypeEnum.danmu">
-                <span class="name">
-                  {{ item.userInfo?.username || item.socket_id }}:
-                </span>
-                <span class="msg">{{ item.msg }}</span>
-              </template>
-              <template v-else-if="item.msgType === DanmuMsgTypeEnum.otherJoin">
-                <span class="name system">系统通知:</span>
-                <span class="msg">
-                  <span>{{ item.userInfo?.username || item.socket_id }}</span>
-                  <span>进入直播!</span>
-                </span>
-              </template>
-              <template
-                v-else-if="item.msgType === DanmuMsgTypeEnum.userLeaved"
-              >
-                <span class="name system">系统通知:</span>
-                <span class="msg">
-                  <span>{{ item.userInfo?.username || item.socket_id }}</span>
-                  <span>离开直播!</span>
-                </span>
-              </template>
-            </div>
-          </div>
-        </div>
-        <div class="send-msg">
-          <input
-            v-model="danmuStr"
-            class="ipt"
-            @keydown="keydownDanmu"
-          />
-          <n-button
-            type="info"
-            size="small"
-            @click="sendDanmu"
-          >
-            发送
-          </n-button>
-        </div>
-      </div>
-    </div>
-
-    <SelectMediaModalCpt
-      v-if="showSelectMediaModalCpt"
-      :all-media-type-list="allMediaTypeList"
-      @close="showSelectMediaModalCpt = false"
-      @ok="selectMediaOk"
-    ></SelectMediaModalCpt>
-
-    <MediaModalCpt
-      v-if="showMediaModalCpt"
-      :media-type="currentMediaType"
-      @close="showMediaModalCpt = false"
-      @ok="addMediaOk"
-    ></MediaModalCpt>
-  </div>
-</template>
-
-<script lang="ts" setup>
-import { getRandomString } from 'billd-utils';
-
-import { onMounted, ref, watch } from 'vue';
-import { useRoute } from 'vue-router';
-
-import { usePush } from '@/hooks/use-push';
-import { DanmuMsgTypeEnum, MediaTypeEnum, liveTypeEnum } from '@/interface';
-import { AppRootState, useAppStore } from '@/store/app';
-import { useUserStore } from '@/store/user';
-import { NODE_ENV } from 'script/constant';
-
-import MediaModalCpt from './mediaModal/index.vue';
-import SelectMediaModalCpt from './selectMediaModal/index.vue';
-
-const route = useRoute();
-const userStore = useUserStore();
-const appStore = useAppStore();
-const currentMediaType = ref(MediaTypeEnum.camera);
-const showSelectMediaModalCpt = ref(false);
-const showMediaModalCpt = ref(false);
-const topRef = ref<HTMLDivElement>();
-const bottomRef = ref<HTMLDivElement>();
-const danmuListRef = ref<HTMLDivElement>();
-const containerRef = ref<HTMLDivElement>();
-const localVideoRef = ref<HTMLVideoElement>();
-const remoteVideoRef = ref<HTMLVideoElement[]>([]);
-const isSRS = route.query.liveType === liveTypeEnum.srsPush;
-const {
-  confirmRoomName,
-  getSocketId,
-  startLive,
-  endLive,
-  sendDanmu,
-  keydownDanmu,
-  localStream,
-  isLiving,
-  allMediaTypeList,
-  currentResolutionRatio,
-  currentMaxBitrate,
-  currentMaxFramerate,
-  danmuStr,
-  roomName,
-  damuList,
-  liveUserList,
-  addTrack,
-  delTrack,
-} = usePush({
-  localVideoRef,
-  remoteVideoRef,
-  isSRS,
-});
-watch(
-  () => damuList.value.length,
-  () => {
-    setTimeout(() => {
-      if (danmuListRef.value) {
-        danmuListRef.value.scrollTop = danmuListRef.value.scrollHeight;
-      }
-    }, 0);
-  }
-);
-
-onMounted(() => {
-  if (topRef.value && bottomRef.value && containerRef.value) {
-    const res =
-      bottomRef.value.getBoundingClientRect().top -
-      topRef.value.getBoundingClientRect().top;
-    containerRef.value.style.height = `${res}px`;
-  }
-});
-
-function selectMediaOk(val: MediaTypeEnum) {
-  showMediaModalCpt.value = true;
-  showSelectMediaModalCpt.value = false;
-  currentMediaType.value = val;
-}
-
-async function addMediaOk(val: {
-  type: MediaTypeEnum;
-  deviceId: string;
-  mediaName: string;
-}) {
-  showMediaModalCpt.value = false;
-  if (val.type === MediaTypeEnum.screen) {
-    const event = await navigator.mediaDevices.getDisplayMedia({
-      video: {
-        deviceId: val.deviceId,
-        height: currentResolutionRatio.value,
-        frameRate: { max: currentMaxFramerate.value },
-      },
-      audio: true,
-    });
-    const videoTrack = {
-      id: getRandomString(8),
-      audio: 2,
-      video: 1,
-      mediaName: val.mediaName,
-      type: MediaTypeEnum.screen,
-      track: event.getVideoTracks()[0],
-      trackid: event.getVideoTracks()[0].id,
-      stream: event,
-      streamid: event.id,
-    };
-    const audio = event.getAudioTracks();
-    if (audio.length) {
-      if (
-        isSRS &&
-        appStore.allTrack.filter((item) => item.audio === 1).length >= 1
-      ) {
-        window.$message.error('srs模式最多只能有一个音频');
-        return;
-      }
-      const audioTrack = {
-        id: getRandomString(8),
-        audio: 1,
-        video: 2,
-        mediaName: val.mediaName,
-        type: MediaTypeEnum.screen,
-        track: event.getAudioTracks()[0],
-        trackid: event.getAudioTracks()[0].id,
-        stream: event,
-        streamid: event.id,
-      };
-      appStore.setAllTrack([...appStore.allTrack, videoTrack, audioTrack]);
-      addTrack(videoTrack);
-      addTrack(audioTrack);
-    } else {
-      if (
-        isSRS &&
-        appStore.allTrack.filter((item) => item.video === 1).length >= 1
-      ) {
-        window.$message.error('srs模式最多只能有一个视频');
-        return;
-      }
-      appStore.setAllTrack([...appStore.allTrack, videoTrack]);
-      addTrack(videoTrack);
-    }
-
-    console.log('获取窗口成功');
-  } else if (val.type === MediaTypeEnum.camera) {
-    const event = await navigator.mediaDevices.getUserMedia({
-      video: {
-        deviceId: val.deviceId,
-        height: currentResolutionRatio.value,
-        frameRate: { max: currentMaxFramerate.value },
-      },
-      audio: false,
-    });
-    if (
-      isSRS &&
-      appStore.allTrack.filter((item) => item.video === 1).length >= 1
-    ) {
-      window.$message.error('srs模式最多只能有一个视频');
-      return;
-    }
-    const track = {
-      id: getRandomString(8),
-      audio: 2,
-      video: 1,
-      mediaName: val.mediaName,
-      type: MediaTypeEnum.camera,
-      track: event.getVideoTracks()[0],
-      trackid: event.getVideoTracks()[0].id,
-      stream: event,
-      streamid: event.id,
-    };
-    appStore.setAllTrack([...appStore.allTrack, track]);
-    addTrack(track);
-    console.log('获取摄像头成功');
-  } else if (val.type === MediaTypeEnum.microphone) {
-    const event = await navigator.mediaDevices.getUserMedia({
-      video: false,
-      audio: { deviceId: val.deviceId },
-    });
-    if (
-      isSRS &&
-      appStore.allTrack.filter((item) => item.audio === 1).length >= 1
-    ) {
-      window.$message.error('srs模式最多只能有一个音频');
-      return;
-    }
-    const track = {
-      id: getRandomString(8),
-      audio: 1,
-      video: 2,
-      mediaName: val.mediaName,
-      type: MediaTypeEnum.microphone,
-      track: event.getAudioTracks()[0],
-      trackid: event.getAudioTracks()[0].id,
-      stream: event,
-      streamid: event.id,
-    };
-    appStore.setAllTrack([...appStore.allTrack, track]);
-    addTrack(track);
-    console.log('获取麦克风成功');
-  }
-}
-
-function handleDelTrack(item: AppRootState['allTrack'][0]) {
-  console.log('handleDelTrack', item);
-  const res = appStore.allTrack.filter((iten) => iten.id !== item.id);
-  appStore.setAllTrack(res);
-  delTrack(item);
-}
-
-function handleStartMedia(item: { type: MediaTypeEnum; txt: string }) {
-  currentMediaType.value = item.type;
-  showMediaModalCpt.value = true;
-}
-</script>
-
-<style lang="scss" scoped>
-.push-wrap {
-  margin: 20px auto 0;
-  width: $w-1275;
-  height: 700px;
-  text-align: center;
-  .left {
-    position: relative;
-    display: inline-block;
-    overflow: hidden;
-    box-sizing: border-box;
-    width: $w-1000;
-    height: 100%;
-    border-radius: 6px;
-    background-color: white;
-    color: #9499a0;
-    vertical-align: top;
-
-    .container {
-      display: flex;
-      align-items: center;
-      justify-content: space-between;
-      height: 100%;
-      background-color: #fff;
-      .video-wrap {
-        position: relative;
-        display: flex;
-        flex: 1;
-        justify-content: center;
-        height: 100%;
-        background-color: rgba($color: #000000, $alpha: 0.5);
-        .media-list {
-          :deep(video) {
-            width: 100%;
-            height: 100%;
-          }
-          :deep(canvas) {
-            width: 100%;
-            height: 100%;
-          }
-          &.item {
-            :deep(video) {
-              width: 50%;
-              height: initial !important;
-            }
-            :deep(canvas) {
-              width: 50%;
-              height: initial !important;
-            }
-          }
-        }
-
-        // #localVideo {
-        //   max-width: 100%;
-        //   max-height: 100%;
-        // }
-        .add-wrap {
-          position: absolute;
-          top: 50%;
-          left: 50%;
-          display: flex;
-          align-items: center;
-          justify-content: space-around;
-          padding: 0 20px;
-          height: 50px;
-          border-radius: 5px;
-          background-color: white;
-          transform: translate(-50%, -50%);
-        }
-      }
-      .sidebar {
-        overflow: scroll;
-        width: 130px;
-        height: 100%;
-        background-color: rgba($color: #000000, $alpha: 0.3);
-
-        @extend %hideScrollbar;
-        .title {
-          color: white;
-        }
-        .join {
-          color: white;
-          cursor: pointer;
-        }
-        video {
-          max-width: 100%;
-        }
-      }
-    }
-    .room-control {
-      position: absolute;
-      right: 0;
-      bottom: 0;
-      left: 0;
-      display: flex;
-      justify-content: space-between;
-      padding: 20px;
-      background-color: papayawhip;
-
-      .info {
-        display: flex;
-        align-items: center;
-
-        .avatar {
-          margin-right: 20px;
-          width: 64px;
-          height: 64px;
-          border-radius: 50%;
-          background-position: center center;
-          background-size: cover;
-          background-repeat: no-repeat;
-        }
-        .detail {
-          display: flex;
-          flex-direction: column;
-          flex-shrink: 0;
-          width: 200px;
-          text-align: initial;
-          .top {
-            margin-bottom: 10px;
-            color: #18191c;
-          }
-          .bottom {
-            font-size: 14px;
-          }
-        }
-      }
-      .rtc {
-        display: flex;
-        align-items: center;
-        flex: 1;
-        font-size: 14px;
-        .item {
-          display: flex;
-          align-items: center;
-          flex: 1;
-          .txt {
-            flex-shrink: 0;
-            width: 80px;
-          }
-          .down {
-            width: 90px;
-
-            user-select: none;
-          }
-        }
-      }
-      .other {
-        display: flex;
-        flex-direction: column;
-        justify-content: center;
-        font-size: 12px;
-        .top {
-        }
-        .bottom {
-          margin-top: 10px;
-        }
-      }
-    }
-  }
-  .right {
-    position: relative;
-    display: inline-block;
-    box-sizing: border-box;
-    margin-left: 10px;
-    width: $w-250;
-    height: 100%;
-    border-radius: 6px;
-    background-color: white;
-    color: #9499a0;
-
-    .resource-card {
-      position: relative;
-      box-sizing: border-box;
-      margin-bottom: 5%;
-      margin-bottom: 10px;
-      padding: 10px;
-      width: 100%;
-      height: 290px;
-      border-radius: 6px;
-      background-color: papayawhip;
-      .title {
-        text-align: initial;
-      }
-      .item {
-        display: flex;
-        align-items: center;
-        justify-content: space-between;
-        margin: 5px 0;
-        font-size: 12px;
-        &:hover {
-          .del {
-            display: block;
-          }
-        }
-        .del {
-          display: none;
-          cursor: pointer;
-        }
-      }
-      .bottom {
-        position: absolute;
-        bottom: 0;
-        left: 0;
-        padding: 10px;
-      }
-    }
-    .danmu-card {
-      box-sizing: border-box;
-      padding: 10px;
-      width: 100%;
-      height: 400px;
-      border-radius: 6px;
-      background-color: papayawhip;
-      text-align: initial;
-      .title {
-        margin-bottom: 10px;
-      }
-      .list {
-        overflow: scroll;
-        margin-bottom: 10px;
-        height: 300px;
-
-        @extend %hideScrollbar;
-
-        .item {
-          margin-bottom: 10px;
-          font-size: 12px;
-          .name {
-            color: #9499a0;
-          }
-          .msg {
-            color: #61666d;
-          }
-        }
-      }
-
-      .send-msg {
-        display: flex;
-        align-items: center;
-        box-sizing: border-box;
-        .ipt {
-          display: block;
-          box-sizing: border-box;
-          margin: 0 auto;
-          margin-right: 10px;
-          padding: 10px;
-          width: 80%;
-          height: 30px;
-          outline: none;
-          border: 1px solid hsla(0, 0%, 60%, 0.2);
-          border-radius: 4px;
-          background-color: #f1f2f3;
-          font-size: 14px;
-        }
-      }
-    }
-  }
-}
-
-// 屏幕宽度大于1500的时候
-@media screen and (min-width: $w-1500) {
-  .push-wrap {
-    width: $w-1475;
-
-    .left {
-      width: $w-1200;
-    }
-    .right {
-      width: $w-250;
-    }
-  }
-}
-</style>

+ 0 - 145
src/views/push2/mediaModal/index.vue

@@ -1,145 +0,0 @@
-<template>
-  <div class="media-wrap">
-    <Modal
-      title="添加直播素材"
-      :mask-closable="false"
-      @close="emits('close')"
-    >
-      <div class="container">
-        <div
-          v-if="inputOptions.length"
-          class="item"
-        >
-          <div class="label">设备选择</div>
-          <div class="value">
-            <n-select
-              v-model:value="currentInput.deviceId"
-              :options="inputOptions"
-            />
-          </div>
-        </div>
-        <div class="item">
-          <div class="label">名称</div>
-          <div class="value">
-            <n-input v-model:value="mediaName" />
-          </div>
-        </div>
-      </div>
-
-      <template #footer>
-        <div class="margin-right">
-          <n-button
-            type="primary"
-            @click="handleOk"
-          >
-            确定
-          </n-button>
-        </div>
-      </template>
-    </Modal>
-  </div>
-</template>
-
-<script lang="ts" setup>
-import { onMounted, ref } from 'vue';
-
-import { MediaTypeEnum } from '@/interface';
-import { useAppStore } from '@/store/app';
-
-const mediaName = ref('');
-const appStore = useAppStore();
-
-const props = withDefaults(
-  defineProps<{
-    mediaType?: MediaTypeEnum;
-  }>(),
-  {
-    mediaType: MediaTypeEnum.camera,
-  }
-);
-const emits = defineEmits(['close', 'ok']);
-
-const inputOptions = ref<{ label: string; value: string }[]>([]);
-const currentInput = ref<{
-  type: MediaTypeEnum;
-  deviceId: string;
-}>({
-  type: MediaTypeEnum.camera,
-  deviceId: '',
-});
-
-onMounted(() => {
-  init();
-});
-
-function handleOk() {
-  emits('ok', { ...currentInput.value, mediaName: mediaName.value });
-}
-
-async function init() {
-  const res = await navigator.mediaDevices.enumerateDevices();
-  if (props.mediaType === MediaTypeEnum.microphone) {
-    res.forEach((item) => {
-      if (item.kind === 'audioinput' && item.deviceId !== 'default') {
-        inputOptions.value.push({
-          label: item.label,
-          value: item.deviceId,
-        });
-      }
-    });
-    currentInput.value = {
-      ...currentInput.value,
-      deviceId: inputOptions.value[0].value,
-      type: MediaTypeEnum.microphone,
-    };
-    mediaName.value = `麦克风-${
-      appStore.allTrack.filter((item) => item.type === MediaTypeEnum.microphone)
-        .length + 1
-    }`;
-  } else if (props.mediaType === MediaTypeEnum.camera) {
-    res.forEach((item) => {
-      if (item.kind === 'videoinput' && item.deviceId !== 'default') {
-        inputOptions.value.push({
-          label: item.label,
-          value: item.deviceId,
-        });
-      }
-    });
-    currentInput.value = {
-      ...currentInput.value,
-      deviceId: inputOptions.value[0].value,
-      type: MediaTypeEnum.camera,
-    };
-    mediaName.value = `摄像头-${
-      appStore.allTrack.filter((item) => item.type === MediaTypeEnum.camera)
-        .length + 1
-    }`;
-  } else if (props.mediaType === MediaTypeEnum.screen) {
-    currentInput.value = {
-      ...currentInput.value,
-      type: MediaTypeEnum.screen,
-    };
-    mediaName.value = `窗口-${
-      appStore.allTrack.filter((item) => item.type === MediaTypeEnum.screen)
-        .length + 1
-    }`;
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-.media-wrap {
-  text-align: initial;
-
-  .container {
-    .item {
-      .label {
-        margin: 6px 0;
-      }
-    }
-    .margin-right {
-      text-align: right;
-    }
-  }
-}
-</style>

+ 0 - 54
src/views/push2/selectMediaModal/index.vue

@@ -1,54 +0,0 @@
-<template>
-  <div class="select-media-wrap">
-    <Modal
-      title="选择直播素材"
-      :mask-closable="false"
-      @close="emits('close')"
-    >
-      <div class="container">
-        <n-space justify="center">
-          <n-button
-            v-for="(item, index) in allMediaTypeList"
-            :key="index"
-            class="item"
-            @click="emits('ok', item.type)"
-          >
-            {{ item.txt }}
-          </n-button>
-        </n-space>
-      </div>
-      <template #footer></template>
-    </Modal>
-  </div>
-</template>
-
-<script lang="ts" setup>
-import { onMounted } from 'vue';
-
-import { MediaTypeEnum } from '@/interface';
-
-const props = withDefaults(
-  defineProps<{
-    allMediaTypeList: {
-      [index: string]: {
-        type: MediaTypeEnum;
-        txt: string;
-      };
-    };
-  }>(),
-  {}
-);
-const emits = defineEmits(['close', 'ok']);
-
-onMounted(() => {});
-</script>
-
-<style lang="scss" scoped>
-.select-media-wrap {
-  text-align: initial;
-
-  .container {
-    padding-top: 10px;
-  }
-}
-</style>