Parcourir la source

feat: 尝试混流

shuisheng il y a 2 ans
Parent
commit
97f87e9cf9

+ 25 - 0
src/components/AudioRoomTip/index.vue

@@ -0,0 +1,25 @@
+<template>
+  <div
+    v-if="appStore.allTrack.length > 0 && appStore.isOnlyAudio()"
+    class="audio-room-tip-wrap"
+  >
+    当前是语音房
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { useAppStore } from '@/store/app';
+
+const appStore = useAppStore();
+</script>
+
+<style lang="scss" scoped>
+.audio-room-tip-wrap {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  color: $theme-color-gold;
+  font-size: 40px;
+}
+</style>

+ 2 - 7
src/components/Modal/index.vue

@@ -62,8 +62,8 @@ const emits = defineEmits(['close']);
     left: 50%;
     box-sizing: border-box;
     padding: 20px;
+    // min-height: 200px;
     width: 320px;
-    height: 200px;
     border-radius: 10px;
     background-color: #fff;
     font-size: 14px;
@@ -86,11 +86,7 @@ const emits = defineEmits(['close']);
       margin-top: 10px;
     }
     .footer {
-      position: absolute;
-      right: 20px;
-      bottom: 20px;
-      left: 20px;
-
+      margin-top: 10px;
       .btn {
         width: 280px;
         height: 44px;
@@ -99,7 +95,6 @@ const emits = defineEmits(['close']);
           linear-gradient(270deg, #b3acff 4.71%, #3ccffd 103.24%),
           linear-gradient(270deg, #b3acff 4.71%, #3ccffd 103.24%);
         color: white;
-        text-align: center;
         font-weight: 600;
         font-size: 16px;
         line-height: 44px;

+ 60 - 25
src/hooks/use-pull.ts

@@ -21,7 +21,7 @@ import {
   MediaTypeEnum,
   liveTypeEnum,
 } from '@/interface';
-import { WebRTCClass } from '@/network/webRTC';
+import { WebRTCClass, audioElArr } from '@/network/webRTC';
 import {
   WebSocketClass,
   WsConnectStatusEnum,
@@ -54,9 +54,9 @@ export function usePull({
   videoEl.autoplay = true;
   // videoEl.controls = true; // 调试用
   videoEl.setAttribute('webkit-playsinline', 'true');
-  videoEl.oncontextmenu = (e) => {
-    e.preventDefault();
-  };
+  // videoEl.oncontextmenu = (e) => {
+  //   e.preventDefault();
+  // };
   const remoteVideoRef = ref(videoEl);
   const heartbeatTimer = ref();
   const roomId = ref(route.params.roomId as string);
@@ -143,7 +143,7 @@ export function usePull({
       const video = event.getVideoTracks();
       track.audio = audio.length ? 1 : 2;
       track.video = video.length ? 1 : 2;
-      console.log('getUserMedia成功', event);
+      console.log('getUserMedia成功', event, audio, video);
       currMediaType.value = allMediaTypeList[MediaTypeEnum.camera];
       currMediaTypeList.value.push(allMediaTypeList[MediaTypeEnum.camera]);
       localStream.value = event;
@@ -162,16 +162,41 @@ export function usePull({
       const video = event.getVideoTracks();
       track.audio = audio.length ? 1 : 2;
       track.video = video.length ? 1 : 2;
-      console.log('getDisplayMedia成功', event);
+      console.log('getDisplayMedia成功', event, audio, video);
       currMediaType.value = allMediaTypeList[MediaTypeEnum.screen];
       currMediaTypeList.value.push(allMediaTypeList[MediaTypeEnum.screen]);
       localStream.value = event;
     }
   }
+
+  watch(
+    () => appStore.allTrack,
+    () => {
+      // networkStore.rtcMap.forEach((rtc) => {
+      //   if (appStore.getTrackInfo().audio > 0) {
+      //     rtc!.peerConnection?.addTransceiver('audio', {
+      //       direction: 'recvonly',
+      //     });
+      //   }
+      //   if (appStore.getTrackInfo().video > 0) {
+      //     rtc!.peerConnection?.addTransceiver('video', {
+      //       direction: 'recvonly',
+      //     });
+      //   }
+      // });
+    },
+    { deep: true }
+  );
+
   watch(
     () => appStore.muted,
     (val) => {
+      console.log(val, audioElArr, 2222);
       remoteVideoRef.value.muted = val;
+      audioElArr.forEach((el) => {
+        console.log(el, el.muted);
+        el.muted = val;
+      });
     }
   );
 
@@ -284,7 +309,7 @@ export function usePull({
         localStream.value.getTracks().forEach((track) => {
           const rtc = networkStore.getRtcMap(`${roomId.value}___${item.id}`);
           console.log('pull-addTrack', `${roomId.value}___${item.id}`);
-          rtc?.addTrack(track, localStream.value);
+          rtc?.addTrack(localStream.value);
         });
       }
     });
@@ -329,7 +354,7 @@ export function usePull({
           receiver: socketId,
         });
         sendOffer({ sender: getSocketId(), receiver: socketId });
-        offerSended.value.add(socketId);
+        // offerSended.value.add(socketId);
       }
     });
   }
@@ -369,24 +394,21 @@ export function usePull({
     receiver,
     videoEl = remoteVideoRef.value!,
   }: {
-    receiver?: string;
+    receiver: string;
     videoEl?: HTMLVideoElement;
   }) {
+    let rtc: WebRTCClass;
     if (isSRS) {
       if (!autoplayVal.value) return;
       console.warn('开始new SRSWebRTCClass', getSocketId());
-      const rtc = new WebRTCClass({
+      rtc = new WebRTCClass({
         roomId: `${roomId.value}___${getSocketId()}`,
         videoEl,
         isSRS: true,
+        direction: 'recvonly',
+        receiver,
       });
       rtc.update();
-      if (track.video === 1) {
-        rtc.peerConnection?.addTransceiver('video', { direction: 'recvonly' });
-      }
-      if (track.audio === 1) {
-        rtc.peerConnection?.addTransceiver('audio', { direction: 'recvonly' });
-      }
       try {
         const offer = await rtc.createOffer();
         if (!offer) return;
@@ -407,13 +429,15 @@ export function usePull({
     } else {
       if (!autoplayVal.value) return;
       console.warn('开始new WebRTCClass');
-      const rtc = new WebRTCClass({
+      rtc = new WebRTCClass({
         roomId: `${roomId.value}___${receiver!}`,
         videoEl,
         isSRS: false,
+        direction: 'recvonly',
+        receiver,
       });
-      return rtc;
     }
+    return rtc;
   }
 
   function keydownDanmu(event: KeyboardEvent) {
@@ -568,12 +592,23 @@ export function usePull({
         await nextTick(async () => {
           console.log('收到offer,这个offer是发给我的', data);
           sender.value = data.data.sender;
-          const rtc = await startNewWebRtc({
-            receiver: data.data.sender,
-            videoEl: data.is_anchor
-              ? remoteVideoRef.value
-              : localVideoRef.value[data.data.sender],
-          });
+          let rtc = networkStore.getRtcMap(
+            `${roomId.value}___${data.data.sender}`
+          );
+          if (!rtc) {
+            rtc = await startNewWebRtc({
+              receiver: data.data.sender,
+              videoEl: data.is_anchor
+                ? remoteVideoRef.value
+                : localVideoRef.value[data.data.sender],
+            });
+          }
+          // const rtc = await startNewWebRtc({
+          //   receiver: data.data.sender,
+          //   videoEl: data.is_anchor
+          //     ? remoteVideoRef.value
+          //     : localVideoRef.value[data.data.sender],
+          // });
           if (rtc) {
             await rtc.setRemoteDescription(data.data.sdp);
             const sdp = await rtc.createAnswer();
@@ -650,7 +685,7 @@ export function usePull({
     instance.socketIo.on(WsMsgTypeEnum.roomLiveing, (data) => {
       prettierReceiveWebsocket(WsMsgTypeEnum.roomLiveing, data);
       if (isSRS && roomLiveType.value !== liveTypeEnum.srsFlvPull) {
-        startNewWebRtc({});
+        startNewWebRtc({ receiver: getSocketId() });
       }
     });
 

+ 208 - 146
src/hooks/use-push.ts

@@ -27,6 +27,7 @@ import {
   IMessage,
   IOffer,
   IOtherJoin,
+  IUpdateJoinInfo,
   LiveRoomTypeEnum,
   MediaTypeEnum,
 } from '@/interface';
@@ -37,6 +38,7 @@ import {
   WsMsgTypeEnum,
   prettierReceiveWebsocket,
 } from '@/network/webSocket';
+import { useAppStore } from '@/store/app';
 import { useNetworkStore } from '@/store/network';
 import { useUserStore } from '@/store/user';
 
@@ -54,6 +56,7 @@ export function usePush({
 }) {
   const route = useRoute();
   const router = useRouter();
+  const appStore = useAppStore();
   const userStore = useUserStore();
   const networkStore = useNetworkStore();
   const heartbeatTimer = ref();
@@ -61,11 +64,11 @@ export function usePush({
   const roomName = ref('');
   const danmuStr = ref('');
   const isLiving = ref(false);
-  const isDone = ref(false);
   const joined = ref(false);
   const localStream = ref<MediaStream>();
   const offerSended = ref(new Set());
   const webRTC = ref<WebRTCClass>();
+  const srsSdp = ref('');
   const maxBitrate = ref([
     {
       label: '1000',
@@ -112,29 +115,33 @@ export function usePush({
 
   const resolutionRatio = ref([
     {
-      label: '1440P',
-      value: 1440,
-    },
-    {
-      label: '1080P',
-      value: 1080,
+      label: '360P',
+      value: 360,
     },
     {
       label: '720P',
       value: 720,
     },
     {
-      label: '360P',
-      value: 360,
+      label: '1080P',
+      value: 1080,
+    },
+    {
+      label: '1440P',
+      value: 1440,
     },
   ]);
-  const currentResolutionRatio = ref(resolutionRatio.value[1].value);
+  const currentResolutionRatio = ref(resolutionRatio.value[2].value);
 
   const maxFramerate = ref([
     {
       label: '10帧',
       value: 10,
     },
+    {
+      label: '20帧',
+      value: 20,
+    },
     {
       label: '24帧',
       value: 24,
@@ -159,7 +166,9 @@ export function usePush({
   const damuList = ref<IDanmu[]>([]);
   const liveUserList = ref<ILiveUser[]>([]);
 
-  const allMediaTypeList = {
+  const allMediaTypeList: {
+    [index: string]: { type: MediaTypeEnum; txt: string };
+  } = {
     [MediaTypeEnum.camera]: {
       type: MediaTypeEnum.camera,
       txt: '摄像头',
@@ -168,17 +177,85 @@ export function usePush({
       type: MediaTypeEnum.screen,
       txt: '窗口',
     },
+    [MediaTypeEnum.microphone]: {
+      type: MediaTypeEnum.microphone,
+      txt: '麦克风',
+    },
   };
-  const currMediaTypeList = ref<
+  const userSelectMediaList = ref<
     {
-      type: MediaTypeEnum;
-      txt: string;
+      mediaName: string;
+      audio: boolean;
+      video: boolean;
     }[]
   >([]);
-  const currMediaType = ref<{
-    type: MediaTypeEnum;
-    txt: string;
-  }>();
+
+  watch(
+    () => localStream.value,
+    (newStream) => {
+      console.log('localStream变了');
+      console.log('新的视频流', newStream?.getVideoTracks());
+      console.log('新的音频流', newStream?.getAudioTracks());
+      if (!localVideoRef.value || !newStream) return;
+      localVideoRef.value.srcObject = newStream;
+      if (isSRS) {
+        if (isLiving.value) {
+          networkStore.getRtcMap(`${roomId.value}___${getSocketId()}`)?.close();
+          networkStore.removeRtc(`${roomId.value}___${getSocketId()}`);
+          startNewWebRtc({
+            receiver: getSocketId(),
+            videoEl: localVideoRef.value,
+          });
+        }
+      } else {
+        networkStore.rtcMap.forEach((rtc) => {
+          newStream?.getTracks().forEach((track) => {
+            const sender = rtc.peerConnection
+              ?.getSenders()
+              .find((s) => s.track?.id === track.id);
+            if (!sender) {
+              console.warn('localStream变了,pc插入track');
+              // rtc.peerConnection?.addTransceiver(track, {
+              //   streams: [newStream],
+              //   direction: 'sendonly',
+              // });
+              rtc.peerConnection?.addTrack(track, newStream);
+            }
+          });
+        });
+      }
+    },
+    { deep: true }
+  );
+
+  watch(
+    () => appStore.allTrack,
+    () => {
+      console.log('allTrack变了');
+      const data: IUpdateJoinInfo['data'] = {
+        live_room_id: Number(roomId.value),
+        track: {
+          audio: appStore.getTrackInfo().audio > 0 ? 1 : 2,
+          video: appStore.getTrackInfo().video > 0 ? 1 : 2,
+        },
+      };
+      networkStore.wsMap.get(roomId.value)?.send({
+        msgType: WsMsgTypeEnum.updateJoinInfo,
+        data,
+      });
+    },
+    {
+      deep: true,
+    }
+  );
+
+  watch(
+    () => appStore.muted,
+    (newVal) => {
+      console.log(newVal);
+      // const rtc = networkStore.getRtcMap(`${roomId.value}___${item.id}`);
+    }
+  );
 
   watch(
     () => currentMaxFramerate.value,
@@ -194,6 +271,7 @@ export function usePush({
       }
     }
   );
+
   watch(
     () => currentMaxBitrate.value,
     async (newVal) => {
@@ -300,7 +378,7 @@ export function usePush({
       return;
     }
     if (!roomNameIsOk()) return;
-    if (currMediaTypeList.value.length <= 0) {
+    if (appStore.allTrack.length <= 0) {
       window.$message.warning('请选择一个素材!');
       return;
     }
@@ -317,8 +395,9 @@ export function usePush({
   /** 结束直播 */
   function endLive() {
     isLiving.value = false;
-    currMediaTypeList.value = [];
+    userSelectMediaList.value = [];
     localStream.value = undefined;
+
     localVideoRef.value!.srcObject = null;
     clearInterval(heartbeatTimer.value);
     const instance = networkStore.wsMap.get(roomId.value);
@@ -334,61 +413,83 @@ export function usePush({
     }, 500);
   }
 
+  function handleNegotiationneeded(data: { roomId: string; isSRS: boolean }) {
+    console.warn(`${data.roomId},开始监听pc的negotiationneeded`);
+    const rtc = networkStore.getRtcMap(data.roomId);
+    if (!rtc) return;
+    rtc.peerConnection?.addEventListener('negotiationneeded', (event) => {
+      console.warn(`${data.roomId},pc收到negotiationneeded`, event);
+      sendOffer({
+        sender: getSocketId(),
+        receiver: rtc.receiver,
+        isSRS: data.isSRS,
+      });
+    });
+  }
+
   /** 原生的webrtc时,receiver必传 */
-  async function startNewWebRtc({
+  function startNewWebRtc({
     receiver,
     videoEl = localVideoRef.value!,
   }: {
-    receiver?: string;
+    receiver: string;
     videoEl?: HTMLVideoElement;
   }) {
+    let rtc: WebRTCClass;
     if (isSRS) {
-      console.warn('开始new SRSWebRTCClass', `${roomId.value}___${receiver!}`);
-      const rtc = new WebRTCClass({
+      console.warn('SRS开始new WebRTCClass', `${roomId.value}___${receiver!}`);
+      rtc = new WebRTCClass({
         maxBitrate: currentMaxBitrate.value,
         maxFramerate: currentMaxFramerate.value,
         resolutionRatio: currentResolutionRatio.value,
         roomId: `${roomId.value}___${getSocketId()}`,
         videoEl,
         isSRS: true,
+        direction: 'sendonly',
+        receiver,
       });
-      webRTC.value = rtc;
+      handleNegotiationneeded({
+        roomId: `${roomId.value}___${receiver}`,
+        isSRS: true,
+      });
+      rtc.localStream = localStream.value;
       localStream.value?.getTracks().forEach((track) => {
-        rtc.addTrack(track, localStream.value);
+        console.warn('srs startNewWebRtc,pc插入track');
+        // rtc.peerConnection?.addTransceiver(track, {
+        //   streams: [localStream.value!],
+        //   direction: 'sendonly',
+        // });
+        rtc.peerConnection?.addTrack(track, localStream.value!);
       });
-      try {
-        const offer = await rtc.createOffer();
-        if (!offer) return;
-        await rtc.setLocalDescription(offer);
-        const res = await fetchRtcV1Publish({
-          api: `/rtc/v1/publish/`,
-          clientip: null,
-          sdp: offer.sdp!,
-          streamurl: userStore.userInfo!.live_rooms![0]!.rtmp_url!.replace(
-            'rtmp',
-            'webrtc'
-          ),
-          tid: getRandomString(10),
-        });
-        await rtc.setRemoteDescription(
-          new RTCSessionDescription({ type: 'answer', sdp: res.data.sdp })
-        );
-      } catch (error) {
-        console.log(error);
-      }
+      webRTC.value = rtc;
     } else {
       console.warn('开始new WebRTCClass', `${roomId.value}___${receiver!}`);
-      const rtc = new WebRTCClass({
+      rtc = new WebRTCClass({
         maxBitrate: currentMaxBitrate.value,
         maxFramerate: currentMaxFramerate.value,
         resolutionRatio: currentResolutionRatio.value,
         roomId: `${roomId.value}___${receiver!}`,
         videoEl,
         isSRS: false,
+        direction: 'sendonly',
+        receiver,
+      });
+      handleNegotiationneeded({
+        roomId: `${roomId.value}___${receiver}`,
+        isSRS: false,
+      });
+      rtc.localStream = localStream.value;
+      localStream.value?.getTracks().forEach((track) => {
+        console.warn('startNewWebRtc,pc插入track');
+        // rtc.peerConnection?.addTransceiver(track, {
+        //   streams: [localStream.value!],
+        //   direction: 'sendonly',
+        // });
+        rtc.peerConnection?.addTrack(track, localStream.value!);
       });
       webRTC.value = rtc;
-      return rtc;
     }
+    return rtc;
   }
 
   function handleCoverImg() {
@@ -428,7 +529,8 @@ export function usePush({
       if (item.id !== getSocketId()) {
         localStream.value?.getTracks().forEach((track) => {
           const rtc = networkStore.getRtcMap(`${roomId.value}___${item.id}`);
-          rtc?.addTrack(track, localStream.value);
+          console.log('4444444444');
+          rtc?.addTrack(localStream.value!);
         });
       }
     });
@@ -444,7 +546,10 @@ export function usePush({
         cover_img: handleCoverImg(),
         type: isSRS ? LiveRoomTypeEnum.user_srs : LiveRoomTypeEnum.user_wertc,
       },
-      track,
+      track: {
+        audio: appStore.getTrackInfo().audio > 0 ? 1 : 2,
+        video: appStore.getTrackInfo().video > 0 ? 1 : 2,
+      },
     };
     instance.send({
       msgType: WsMsgTypeEnum.join,
@@ -455,47 +560,69 @@ export function usePush({
   async function sendOffer({
     sender,
     receiver,
+    isSRS,
   }: {
     sender: string;
     receiver: string;
+    isSRS: boolean;
   }) {
-    if (isDone.value) return;
-    const instance = networkStore.wsMap.get(roomId.value);
-    if (!instance) return;
+    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!);
-    instance.send({
-      msgType: WsMsgTypeEnum.offer,
-      data: {
-        sdp,
-        sender,
-        receiver,
-        live_room_id: roomId.value,
-      },
-    });
+    if (!isSRS) {
+      const sdp = await rtc.createOffer();
+      await rtc.setLocalDescription(sdp!);
+      ws.send({
+        msgType: WsMsgTypeEnum.offer,
+        data: {
+          sdp,
+          sender,
+          receiver,
+          live_room_id: roomId.value,
+        },
+      });
+    } else {
+      const sdp = await rtc.createOffer();
+      console.log(sdp, 22);
+      await rtc.setLocalDescription(sdp!);
+      const res = await fetchRtcV1Publish({
+        api: `/rtc/v1/publish/`,
+        clientip: null,
+        sdp: sdp!.sdp!,
+        streamurl: userStore.userInfo!.live_rooms![0]!.rtmp_url!.replace(
+          'rtmp',
+          'webrtc'
+        ),
+        tid: getRandomString(10),
+      });
+      srsSdp.value = res.data.sdp;
+      await rtc.setRemoteDescription(
+        new RTCSessionDescription({ type: 'answer', sdp: srsSdp.value })
+      );
+    }
   }
 
   function batchSendOffer() {
-    console.log('batchSendOffer');
+    console.log('开始batchSendOffer');
     liveUserList.value.forEach(async (item) => {
       const socketId = item.id;
       if (!offerSended.value.has(socketId) && socketId !== getSocketId()) {
-        const rtc = networkStore.getRtcMap(`${roomId.value}___${socketId}`);
+        let rtc = networkStore.getRtcMap(`${roomId.value}___${socketId}`);
         if (!rtc) {
-          await startNewWebRtc({
+          rtc = await startNewWebRtc({
             receiver: socketId,
             videoEl: localVideoRef.value,
           });
         }
-        await addTrack();
+        // await addTrack();
         console.log('执行sendOffer', {
           sender: getSocketId(),
           receiver: socketId,
         });
-        sendOffer({ sender: getSocketId(), receiver: socketId });
-        offerSended.value.add(socketId);
+
+        sendOffer({ sender: getSocketId(), receiver: socketId, isSRS: false });
       }
     });
   }
@@ -569,7 +696,6 @@ export function usePush({
         data
       );
       if (isSRS) return;
-      if (isDone.value) return;
       if (!instance) return;
       const rtc = networkStore.getRtcMap(`${roomId.value}___${data.socket_id}`);
       if (!rtc) return;
@@ -590,7 +716,6 @@ export function usePush({
         data
       );
       if (isSRS) return;
-      if (isDone.value) return;
       if (!instance) return;
       const rtc = networkStore.getRtcMap(`${roomId.value}___${data.socket_id}`);
       if (!rtc) return;
@@ -646,9 +771,7 @@ export function usePush({
         userInfo: data.user_info,
       });
       if (isSRS) {
-        startNewWebRtc({});
-      } else {
-        batchSendOffer();
+        startNewWebRtc({ receiver: getSocketId() });
       }
     });
 
@@ -668,7 +791,10 @@ export function usePush({
       damuList.value.push(danmu);
       if (isSRS) return;
       if (joined.value) {
-        batchSendOffer();
+        startNewWebRtc({
+          receiver: data.data.join_socket_id,
+          videoEl: localVideoRef.value,
+        });
       }
     });
 
@@ -712,54 +838,6 @@ export function usePush({
     return true;
   }
 
-  /** 摄像头 */
-  async function startGetUserMedia() {
-    if (!localStream.value) {
-      // WARN navigator.mediaDevices在localhost和https才能用,http://192.168.1.103:8000局域网用不了
-      const event = await navigator.mediaDevices.getUserMedia({
-        video: {
-          height: currentResolutionRatio.value,
-          frameRate: { ideal: currentMaxFramerate.value, max: 90 },
-        },
-        audio: true,
-      });
-      const audio = event.getAudioTracks();
-      const video = event.getVideoTracks();
-      track.audio = audio.length ? 1 : 2;
-      track.video = video.length ? 1 : 2;
-      console.log('getUserMedia成功', event);
-      currMediaType.value = allMediaTypeList[MediaTypeEnum.camera];
-      currMediaTypeList.value.push(allMediaTypeList[MediaTypeEnum.camera]);
-      if (!localVideoRef.value) return;
-      localVideoRef.value.srcObject = event;
-      localStream.value = event;
-    }
-  }
-
-  /** 窗口 */
-  async function startGetDisplayMedia() {
-    if (!localStream.value) {
-      // WARN navigator.mediaDevices.getDisplayMedia在localhost和https才能用,http://192.168.1.103:8000局域网用不了
-      const event = await navigator.mediaDevices.getDisplayMedia({
-        video: {
-          height: currentResolutionRatio.value,
-          frameRate: { ideal: currentMaxFramerate.value, max: 90 },
-        },
-        audio: true,
-      });
-      const audio = event.getAudioTracks();
-      const video = event.getVideoTracks();
-      track.audio = audio.length ? 1 : 2;
-      track.video = video.length ? 1 : 2;
-      console.log('getDisplayMedia成功', event);
-      currMediaType.value = allMediaTypeList[MediaTypeEnum.screen];
-      currMediaTypeList.value.push(allMediaTypeList[MediaTypeEnum.screen]);
-      if (!localVideoRef.value) return;
-      localVideoRef.value.srcObject = event;
-      localStream.value = event;
-    }
-  }
-
   function keydownDanmu(event: KeyboardEvent) {
     const key = event.key.toLowerCase();
     if (key === 'enter') {
@@ -800,23 +878,7 @@ export function usePush({
     danmuStr.value = '';
   }
 
-  async function getAllMediaDevices() {
-    const res = await navigator.mediaDevices.enumerateDevices();
-    // const audioInput = res.filter(
-    //   (item) => item.kind === 'audioinput' && item.deviceId !== 'default'
-    // );
-    // const videoInput = res.filter(
-    //   (item) => item.kind === 'videoinput' && item.deviceId !== 'default'
-    // );
-    return res;
-  }
-
-  async function initPush() {
-    const all = await getAllMediaDevices();
-    allMediaTypeList[MediaTypeEnum.camera] = {
-      txt: all.find((item) => item.kind === 'videoinput')?.label || '摄像头',
-      type: MediaTypeEnum.camera,
-    };
+  function initPush() {
     localVideoRef.value?.addEventListener('loadstart', () => {
       console.warn('视频流-loadstart');
       const rtc = networkStore.getRtcMap(roomId.value);
@@ -831,22 +893,22 @@ export function usePush({
       rtc.update();
       if (isSRS) return;
       if (joined.value) {
-        batchSendOffer();
+        // batchSendOffer();
       }
     });
   }
 
   return {
     initPush,
-    isLiving,
     confirmRoomName,
     getSocketId,
-    startGetDisplayMedia,
-    startGetUserMedia,
     startLive,
     endLive,
     sendDanmu,
     keydownDanmu,
+    localStream,
+    isLiving,
+    allMediaTypeList,
     currentResolutionRatio,
     currentMaxBitrate,
     currentMaxFramerate,
@@ -857,6 +919,6 @@ export function usePush({
     roomName,
     damuList,
     liveUserList,
-    currMediaTypeList,
+    userSelectMediaList,
   };
 }

+ 2 - 0
src/interface.ts

@@ -322,6 +322,7 @@ export interface ILive {
 export enum MediaTypeEnum {
   camera,
   screen,
+  microphone,
 }
 
 export enum DanmuMsgTypeEnum {
@@ -336,6 +337,7 @@ export interface IUpdateJoinInfo {
   user_info?: IUser;
   data: {
     live_room_id: number;
+    track?: { audio: number; video: number };
   };
 }
 

+ 77 - 54
src/network/webRTC.ts

@@ -1,18 +1,22 @@
 import browserTool from 'browser-tool';
+import { NODE_ENV } from 'script/constant';
 
 import { ICandidate } from '@/interface';
 import { useNetworkStore } from '@/store/network';
 
 import { WsMsgTypeEnum } from './webSocket';
 
+export const audioElArr: HTMLVideoElement[] = [];
+
 export class WebRTCClass {
   roomId = '-1';
+  receiver = '';
 
   videoEl: HTMLVideoElement;
 
-  peerConnection: RTCPeerConnection | null = null;
+  direction: RTCRtpTransceiverDirection;
 
-  sender?: RTCRtpTransceiver;
+  peerConnection: RTCPeerConnection | null = null;
 
   /** 最大码率 */
   maxBitrate = -1;
@@ -38,35 +42,38 @@ export class WebRTCClass {
     version: string;
   };
 
-  constructor({
-    roomId,
-    videoEl,
-    maxBitrate,
-    maxFramerate,
-    resolutionRatio,
-    isSRS,
-  }: {
+  constructor(data: {
     roomId: string;
     videoEl: HTMLVideoElement;
     maxBitrate?: number;
     maxFramerate?: number;
     resolutionRatio?: number;
     isSRS: boolean;
+    direction: RTCRtpTransceiverDirection;
+    receiver: string;
   }) {
-    this.roomId = roomId;
-    this.videoEl = videoEl;
-    if (maxBitrate) {
-      this.maxBitrate = maxBitrate;
+    this.roomId = data.roomId;
+    this.videoEl = data.videoEl;
+    this.direction = data.direction;
+    this.receiver = data.receiver;
+    if (data.maxBitrate) {
+      this.maxBitrate = data.maxBitrate;
     }
-    if (resolutionRatio) {
-      this.resolutionRatio = resolutionRatio;
+    if (data.resolutionRatio) {
+      this.resolutionRatio = data.resolutionRatio;
     }
-    if (maxFramerate) {
-      this.maxFramerate = maxFramerate;
+    if (data.maxFramerate) {
+      this.maxFramerate = data.maxFramerate;
     }
-    this.isSRS = isSRS;
+    this.isSRS = data.isSRS;
+    console.warn('new webrtc参数:', data);
     this.browser = browserTool();
     this.createPeerConnection();
+    // setInterval(() => {
+    //   const getAudioTracks = this.localStream?.getAudioTracks().length;
+    //   const getVideoTracks = this.localStream?.getVideoTracks().length;
+    //   console.log(getAudioTracks, getVideoTracks, '----');
+    // }, 1000);
   }
 
   prettierLog = (msg: string, type?: 'log' | 'warn' | 'error', ...args) => {
@@ -78,18 +85,43 @@ export class WebRTCClass {
     );
   };
 
-  addTrack = (track, stream) => {
-    const sender = this.peerConnection
-      ?.getSenders()
-      .find((s) => s.track === track);
-    if (!sender) {
-      console.warn('开始addTrack', this.roomId, track, stream);
-      this.peerConnection?.addTrack(track, stream);
-      this.localStream = stream;
+  addTrack = (stream: MediaStream, isCb?: boolean) => {
+    console.log('开始addTrack,是否是pc的track回调', isCb);
+    console.log('收到新stream', stream);
+    console.log('收到新stream的视频轨', stream.getVideoTracks());
+    console.log('收到新stream的音频轨', stream.getAudioTracks());
+    console.log('原本旧stream的视频轨', this.localStream?.getVideoTracks());
+    console.log('原本旧stream的音频轨', this.localStream?.getAudioTracks());
+    const mixedStream = new MediaStream();
+    this.localStream
+      ?.getVideoTracks()
+      .forEach((track) => mixedStream.addTrack(track));
+    this.localStream
+      ?.getAudioTracks()
+      .forEach((track) => mixedStream.addTrack(track));
+    stream.getVideoTracks().forEach((track) => mixedStream.addTrack(track));
+    stream.getAudioTracks().forEach((track) => mixedStream.addTrack(track));
+    console.log('混流stream', stream);
+    console.log('混流stream的视频流', mixedStream.getVideoTracks());
+    console.log('混流stream的音频流', mixedStream.getAudioTracks());
+    // const sender = this.peerConnection
+    //   ?.getSenders()
+    //   .find((sender) => sender.track !== track);
+    // console.log('getSenders', this.peerConnection?.getSenders());
+    // console.log('sender', sender);
+    if (NODE_ENV === 'development') {
+      this.videoEl.controls = true;
+    }
+    this.videoEl.srcObject = mixedStream;
+    this.localStream = mixedStream;
+    if (this.maxBitrate !== -1) {
       this.setMaxBitrate(this.maxBitrate);
+    }
+    if (this.maxFramerate !== -1) {
+      this.setMaxFramerate(this.maxFramerate);
+    }
+    if (this.resolutionRatio !== -1) {
       this.setResolutionRatio(this.resolutionRatio);
-    } else {
-      console.warn('不addTrack了', this.roomId, track, stream);
     }
   };
 
@@ -228,42 +260,33 @@ export class WebRTCClass {
     }
   };
 
-  addStream = (stream) => {
-    if (!this.peerConnection) return;
-    console.log('开始addStream', this.videoEl, stream, this.roomId);
-    this.videoEl.srcObject = stream;
-    this.prettierLog('addStream成功', 'warn');
-  };
-
   handleStreamEvent = () => {
     if (!this.peerConnection) return;
+    // 废弃:https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addStream
     console.warn(`${this.roomId},开始监听pc的addstream`);
     this.peerConnection.addEventListener('addstream', (event: any) => {
-      console.warn(`${this.roomId},pc收到addstream事件`, event, event.stream);
-      this.addStream(event.stream);
-    });
-
-    console.warn(`${this.roomId},开始监听pc的ontrack`);
-    this.peerConnection.addEventListener('ontrack', (event: any) => {
-      console.warn(`${this.roomId},pc收到ontrack事件`, event);
-      this.addStream(event.streams[0]);
-    });
-
-    console.warn(`${this.roomId},开始监听pc的addtrack`);
-    this.peerConnection.addEventListener('addtrack', (event: any) => {
-      console.warn(`${this.roomId},pc收到addtrack事件`, event);
+      console.warn(`${this.roomId},pc收到addstream事件`, event);
+      console.log('addstream事件的stream', event.stream);
+      console.log('addstream事件的视频轨', event.stream.getVideoTracks());
+      console.log('addstream事件的音频轨', event.stream.getAudioTracks());
+      this.addTrack(event.stream, true);
     });
 
     console.warn(`${this.roomId},开始监听pc的track`);
-    this.peerConnection.addEventListener('track', (event: any) => {
+    this.peerConnection.addEventListener('track', (event) => {
       console.warn(`${this.roomId},pc收到track事件`, event);
-      this.addStream(event.streams[0]);
+      console.log('track事件的stream', event.streams[0]);
+      console.log('track事件的视频轨', event.streams[0].getVideoTracks());
+      console.log('track事件的音频轨', event.streams[0].getAudioTracks());
+      this.addTrack(event.streams[0], true);
     });
   };
 
   handleConnectionEvent = () => {
     if (!this.peerConnection) return;
+
     console.warn(`${this.roomId},开始监听pc的icecandidate`);
+    // icecandidate
     this.peerConnection.addEventListener('icecandidate', (event) => {
       this.prettierLog('pc收到icecandidate', 'warn');
       if (event.candidate) {
@@ -389,9 +412,9 @@ export class WebRTCClass {
   // 手动关闭webrtc连接
   close = () => {
     console.warn(`${new Date().toLocaleString()},手动关闭webrtc连接`);
-    if (this.sender?.sender) {
-      this.peerConnection?.removeTrack(this.sender?.sender);
-    }
+    this.peerConnection?.getSenders().forEach((sender) => {
+      this.peerConnection?.removeTrack(sender);
+    });
     this.peerConnection?.close();
     this.peerConnection = null;
   };

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

@@ -1,31 +1,56 @@
 import { defineStore } from 'pinia';
 
+import { MediaTypeEnum } from '@/interface';
 import { mobileRouterName } from '@/router';
 
 export type AppRootState = {
-  liveStatus: boolean;
   muted: boolean;
   navList: { routeName: string; name: string }[];
+  allTrack: {
+    /** 1开启;2关闭 */
+    audio: number;
+    /** 1开启;2关闭 */
+    video: number;
+    mediaName: string;
+    type: MediaTypeEnum;
+    track: MediaStreamTrack;
+    stream: MediaStream;
+  }[];
 };
 
 export const useAppStore = defineStore('app', {
   state: (): AppRootState => {
     return {
-      liveStatus: false,
       muted: true,
       navList: [
         { routeName: mobileRouterName.h5, name: '频道' },
         { routeName: mobileRouterName.h5Rank, name: '排行' },
         { routeName: mobileRouterName.h5Profile, name: '我的' },
       ],
+      allTrack: [],
     };
   },
   actions: {
-    setLiveStatus(res: AppRootState['liveStatus']) {
-      this.liveStatus = res;
-    },
     setMuted(res: AppRootState['muted']) {
       this.muted = res;
     },
+    setAllTrack(res: AppRootState['allTrack']) {
+      this.allTrack = res;
+    },
+    isOnlyAudio() {
+      let videoTracks = 0;
+      this.allTrack.forEach((item) => {
+        videoTracks += item.stream.getVideoTracks().length;
+      });
+      return videoTracks <= 0;
+    },
+    getTrackInfo() {
+      const res = { audio: 0, video: 0 };
+      this.allTrack.forEach((item) => {
+        res.audio += item.stream.getAudioTracks().length;
+        res.video += item.stream.getVideoTracks().length;
+      });
+      return res;
+    },
   },
 });

+ 3 - 0
src/store/network/index.ts

@@ -32,6 +32,9 @@ export const useNetworkStore = defineStore('network', {
         this.rtcMap.set(roomId, arg);
       }
     },
+    removeRtc(roomId: string) {
+      this.rtcMap.delete(roomId);
+    },
     getRtcMap(roomId: string) {
       return this.rtcMap.get(roomId);
     },

+ 2 - 0
src/views/h5/room/index.vue

@@ -273,6 +273,7 @@ onMounted(() => {
       position: absolute;
       top: 0;
       left: 50%;
+      width: 100%;
       height: 100%;
       transform: translate(-50%);
 
@@ -282,6 +283,7 @@ onMounted(() => {
       position: absolute;
       top: 0;
       left: 50%;
+      width: 100%;
       height: 100%;
       transform: translate(-50%);
 

+ 1 - 0
src/views/home/index.vue

@@ -359,6 +359,7 @@ function joinHlsRoom() {
         position: absolute;
         top: 0;
         left: 50%;
+        width: 100%;
         height: 100%;
         transform: translate(-50%);
 

+ 3 - 0
src/views/pull/index.vue

@@ -40,6 +40,7 @@
               }"
             ></div>
             <div ref="canvasRef"></div>
+            <AudioRoomTip></AudioRoomTip>
             <VideoControls></VideoControls>
           </div>
 
@@ -436,6 +437,7 @@ onMounted(() => {
           position: absolute;
           top: 0;
           left: 50%;
+          width: 100%;
           height: 100%;
           transform: translate(-50%);
 
@@ -445,6 +447,7 @@ onMounted(() => {
           position: absolute;
           top: 0;
           left: 50%;
+          width: 100%;
           height: 100%;
           transform: translate(-50%);
 

+ 183 - 19
src/views/push/index.vue

@@ -9,6 +9,7 @@
         class="container"
       >
         <div class="video-wrap">
+          <AudioRoomTip></AudioRoomTip>
           <video
             id="localVideo"
             ref="localVideoRef"
@@ -20,24 +21,20 @@
             x5-video-player-fullscreen="true"
             x5-video-orientation="portraint"
             muted
+            controls
           ></video>
-          <VideoControls v-if="currMediaTypeList.length > 0"></VideoControls>
           <div
-            v-if="!currMediaTypeList || currMediaTypeList.length <= 0"
+            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="startGetUserMedia"
+                @click="handleStartMedia(item)"
               >
-                摄像头
-              </n-button>
-              <n-button
-                class="item"
-                @click="startGetDisplayMedia"
-              >
-                窗口
+                {{ item.txt }}
               </n-button>
             </n-space>
           </div>
@@ -133,7 +130,13 @@
           <div class="top">
             <span class="item">
               <i class="ico"></i>
-              <span>正在观看:{{ liveUserList.length }}</span>
+              <span>
+                正在观看:
+                {{
+                  liveUserList.filter((item) => item.id !== getSocketId())
+                    .length
+                }}
+              </span>
             </span>
           </div>
           <div class="bottom">
@@ -163,13 +166,25 @@
         <div class="title">素材列表</div>
         <div class="list">
           <div
-            v-for="(item, index) in currMediaTypeList"
+            v-for="(item, index) in appStore.allTrack"
             :key="index"
             class="item"
           >
-            <span class="name">{{ item.txt }}</span>
+            <span class="name">
+              ({{ item.audio === 1 ? '音频' : ''
+              }}{{ item.video === 1 ? '视频' : '' }}){{ item.mediaName }}
+            </span>
           </div>
         </div>
+        <div class="bottom">
+          <n-button
+            size="small"
+            type="primary"
+            @click="handleAddMedia"
+          >
+            添加音频
+          </n-button>
+        </div>
       </div>
       <div class="danmu-card">
         <div class="title">弹幕互动</div>
@@ -224,6 +239,13 @@
         </div>
       </div>
     </div>
+
+    <MediaModalCpt
+      v-if="showMediaModalCpt"
+      :media-type="currentMediaType"
+      @close="showMediaModalCpt = false"
+      @ok="selectMediaOk"
+    ></MediaModalCpt>
   </div>
 </template>
 
@@ -232,12 +254,17 @@ import { onMounted, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 
 import { usePush } from '@/hooks/use-push';
-import { DanmuMsgTypeEnum, liveTypeEnum } from '@/interface';
+import { DanmuMsgTypeEnum, MediaTypeEnum, liveTypeEnum } from '@/interface';
+import { useAppStore } from '@/store/app';
 import { useUserStore } from '@/store/user';
 
+import MediaModalCpt from './mediaModal/index.vue';
+
 const route = useRoute();
 const userStore = useUserStore();
-
+const appStore = useAppStore();
+const currentMediaType = ref(MediaTypeEnum.camera);
+const showMediaModalCpt = ref(false);
 const liveType = route.query.liveType;
 const topRef = ref<HTMLDivElement>();
 const bottomRef = ref<HTMLDivElement>();
@@ -248,15 +275,15 @@ const remoteVideoRef = ref<HTMLVideoElement[]>([]);
 
 const {
   initPush,
-  isLiving,
   confirmRoomName,
   getSocketId,
-  startGetDisplayMedia,
-  startGetUserMedia,
   startLive,
   endLive,
   sendDanmu,
   keydownDanmu,
+  localStream,
+  isLiving,
+  allMediaTypeList,
   currentResolutionRatio,
   currentMaxBitrate,
   currentMaxFramerate,
@@ -267,12 +294,13 @@ const {
   roomName,
   damuList,
   liveUserList,
-  currMediaTypeList,
+  userSelectMediaList,
 } = usePush({
   localVideoRef,
   remoteVideoRef,
   isSRS: liveType === liveTypeEnum.srsPush,
 });
+
 watch(
   () => damuList.value.length,
   () => {
@@ -283,7 +311,13 @@ watch(
     }, 0);
   }
 );
+
 onMounted(() => {
+  if (localVideoRef.value) {
+    // localVideoRef.value.oncontextmenu = (e) => {
+    //   e.preventDefault();
+    // };
+  }
   if (topRef.value && bottomRef.value && containerRef.value) {
     const res =
       bottomRef.value.getBoundingClientRect().top -
@@ -292,6 +326,129 @@ onMounted(() => {
   }
   initPush();
 });
+
+async function selectMediaOk(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: { ideal: currentMaxFramerate.value, max: 90 },
+      },
+      audio: true,
+    });
+    if (localStream.value) {
+      const mixedStream = new MediaStream();
+      localStream.value
+        ?.getVideoTracks()
+        .forEach((track) => mixedStream.addTrack(track));
+      localStream.value
+        ?.getAudioTracks()
+        .forEach((track) => mixedStream.addTrack(track));
+      event.getVideoTracks().forEach((track) => mixedStream.addTrack(track));
+      event.getAudioTracks().forEach((track) => mixedStream.addTrack(track));
+      localStream.value = mixedStream;
+    } else {
+      localStream.value = event;
+    }
+    const audio = event.getAudioTracks();
+    appStore.setAllTrack([
+      ...appStore.allTrack,
+      {
+        audio: audio.length > 0 ? 1 : 2,
+        video: 1,
+        mediaName: val.mediaName,
+        type: MediaTypeEnum.screen,
+        track: event.getVideoTracks()[0],
+        stream: event,
+      },
+    ]);
+    console.log('获取窗口成功');
+  } else if (val.type === MediaTypeEnum.camera) {
+    const event = await navigator.mediaDevices.getUserMedia({
+      video: {
+        deviceId: val.deviceId,
+        height: currentResolutionRatio.value,
+        frameRate: { ideal: currentMaxFramerate.value, max: 90 },
+      },
+      audio: false,
+    });
+    if (localStream.value) {
+      const mixedStream = new MediaStream();
+      localStream.value
+        ?.getVideoTracks()
+        .forEach((track) => mixedStream.addTrack(track));
+      localStream.value
+        ?.getAudioTracks()
+        .forEach((track) => mixedStream.addTrack(track));
+      event.getVideoTracks().forEach((track) => mixedStream.addTrack(track));
+      event.getAudioTracks().forEach((track) => mixedStream.addTrack(track));
+      localStream.value = mixedStream;
+    } else {
+      localStream.value = event;
+    }
+    appStore.setAllTrack([
+      ...appStore.allTrack,
+      {
+        audio: 2,
+        video: 1,
+        mediaName: val.mediaName,
+        type: MediaTypeEnum.camera,
+        track: event.getVideoTracks()[0],
+        stream: event,
+      },
+    ]);
+    console.log('获取摄像头成功');
+  } else if (val.type === MediaTypeEnum.microphone) {
+    const event = await navigator.mediaDevices.getUserMedia({
+      video: false,
+      audio: { deviceId: val.deviceId },
+    });
+    if (localStream.value) {
+      const mixedStream = new MediaStream();
+      localStream.value
+        ?.getVideoTracks()
+        .forEach((track) => mixedStream.addTrack(track));
+      localStream.value
+        ?.getAudioTracks()
+        .forEach((track) => mixedStream.addTrack(track));
+      event.getVideoTracks().forEach((track) => mixedStream.addTrack(track));
+      event.getAudioTracks().forEach((track) => mixedStream.addTrack(track));
+      localStream.value = mixedStream;
+    } else {
+      localStream.value = event;
+    }
+    console.log(localStream.value, event);
+    appStore.setAllTrack([
+      ...appStore.allTrack,
+      {
+        audio: 1,
+        video: 2,
+        mediaName: val.mediaName,
+        type: MediaTypeEnum.microphone,
+        track: event.getAudioTracks()[0],
+        stream: event,
+      },
+    ]);
+    console.log('获取麦克风成功');
+  }
+}
+
+function handleAddMedia() {
+  currentMediaType.value = MediaTypeEnum.microphone;
+  showMediaModalCpt.value = true;
+}
+
+function handleStartMedia(item: { type: MediaTypeEnum; txt: string }) {
+  console.log(item);
+  currentMediaType.value = item.type;
+  showMediaModalCpt.value = true;
+}
 </script>
 
 <style lang="scss" scoped>
@@ -445,6 +602,7 @@ onMounted(() => {
     color: #9499a0;
 
     .resource-card {
+      position: relative;
       box-sizing: border-box;
       margin-bottom: 5%;
       margin-bottom: 10px;
@@ -463,6 +621,12 @@ onMounted(() => {
         margin: 5px 0;
         font-size: 12px;
       }
+      .bottom {
+        position: absolute;
+        bottom: 0;
+        left: 0;
+        padding: 10px;
+      }
     }
     .danmu-card {
       box-sizing: border-box;

+ 147 - 0
src/views/push/mediaModal/index.vue

@@ -0,0 +1,147 @@
+<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 { defineEmits, defineProps, onMounted, ref, withDefaults } from 'vue';
+
+import { MediaTypeEnum } from '@/interface';
+import { useAppStore } from '@/store/app';
+import { useNetworkStore } from '@/store/network';
+
+const mediaName = ref('');
+const networkStore = useNetworkStore();
+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>