shuisheng 2 gadi atpakaļ
vecāks
revīzija
f4db25d96b

+ 3 - 0
remark.md

@@ -0,0 +1,3 @@
+## use-push
+
+> 推流

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

@@ -29,7 +29,7 @@ export function usePull({
   isSRS,
   isFlv,
 }: {
-  localVideoRef: Ref<HTMLVideoElement | undefined>;
+  localVideoRef: Ref<HTMLVideoElement[]>;
   remoteVideoRef: Ref<HTMLVideoElement | undefined>;
   isSRS?: boolean;
   isFlv?: boolean;
@@ -49,6 +49,12 @@ export function usePull({
   const isDone = ref(false);
   const roomNoLive = ref(false);
   const localStream = ref();
+  const sidebarList = ref<
+    {
+      socketId: string;
+    }[]
+  >([]);
+
   const track = reactive({
     audio: true,
     video: true,
@@ -62,6 +68,7 @@ export function usePull({
   ]);
   const offerSended = ref(new Set());
   const hooksRtcMap = ref(new Set());
+  const sender = ref();
 
   const allMediaTypeList = {
     [MediaTypeEnum.camera]: {
@@ -95,8 +102,9 @@ export function usePull({
       console.log('getUserMedia成功', event);
       currMediaType.value = allMediaTypeList[MediaTypeEnum.camera];
       currMediaTypeList.value.push(allMediaTypeList[MediaTypeEnum.camera]);
-      if (!localVideoRef.value) return;
-      localVideoRef.value.srcObject = event;
+      // localVideoRef.value.forEach((item) => {
+      //   item.srcObject = event;
+      // });
       localStream.value = event;
     }
   }
@@ -116,8 +124,6 @@ export function usePull({
       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;
     }
   }
@@ -204,6 +210,20 @@ export function usePull({
     });
   }
 
+  function addTransceiver() {
+    if (!localStream.value) return;
+    liveUserList.value.forEach((item) => {
+      if (item.socketId !== getSocketId()) {
+        localStream.value.getTracks().forEach((track) => {
+          const rtc = networkStore.getRtcMap(
+            `${roomId.value}___${item.socketId}`
+          );
+          rtc?.addTransceiver(track, localStream.value);
+        });
+      }
+    });
+  }
+
   function addTrack() {
     if (!localStream.value) return;
     liveUserList.value.forEach((item) => {
@@ -212,6 +232,7 @@ export function usePull({
           const rtc = networkStore.getRtcMap(
             `${roomId.value}___${item.socketId}`
           );
+          console.log(rtc, track, localStream.value, 9998);
           rtc?.addTrack(track, localStream.value);
         });
       }
@@ -237,14 +258,17 @@ export function usePull({
       data: { sdp, sender, receiver },
     });
   }
+
   function batchSendOffer() {
     liveUserList.value.forEach(async (item) => {
       if (
         !offerSended.value.has(item.socketId) &&
         item.socketId !== getSocketId()
       ) {
-        hooksRtcMap.value.add(await startNewWebRtc(item.socketId));
-        await addTrack();
+        hooksRtcMap.value.add(
+          await startNewWebRtc({ receiver: item.socketId })
+        );
+        await addTransceiver();
         console.warn('new WebRTCClass完成');
         console.log('执行sendOffer', {
           sender: getSocketId(),
@@ -256,13 +280,38 @@ export function usePull({
     });
   }
 
+  async function addVideo() {
+    const socketId = sender.value;
+    localVideoRef.value[socketId].srcObject = localStream.value;
+    hooksRtcMap.value.add(
+      await startNewWebRtc({
+        receiver: socketId,
+        videoEl: localVideoRef.value[socketId],
+      })
+    );
+    await addTransceiver();
+    console.warn('new WebRTCClass完成');
+    console.log('执行sendOffer', {
+      sender: getSocketId(),
+      receiver: socketId,
+    });
+    sendOffer({ sender: getSocketId(), receiver: socketId });
+    offerSended.value.add(socketId);
+  }
+
   /** 原生的webrtc时,receiver必传 */
-  async function startNewWebRtc(receiver?: string) {
+  async function startNewWebRtc({
+    receiver,
+    videoEl = remoteVideoRef.value!,
+  }: {
+    receiver?: string;
+    videoEl?: HTMLVideoElement;
+  }) {
     if (isSRS) {
       console.warn('开始new SRSWebRTCClass', getSocketId());
       const rtc = new SRSWebRTCClass({
         roomId: `${roomId.value}___${getSocketId()}`,
-        videoEl: remoteVideoRef.value!,
+        videoEl,
       });
       rtc.rtcStatus.joined = true;
       rtc.update();
@@ -360,7 +409,9 @@ export function usePull({
       if (!instance) return;
       if (data.data.receiver === getSocketId()) {
         console.log('收到offer,这个offer是发给我的');
-        const rtc = await startNewWebRtc(data.data.sender);
+        sidebarList.value.push({ socketId: data.data.sender });
+        sender.value = data.data.sender;
+        const rtc = await startNewWebRtc({ receiver: data.data.sender });
         if (rtc) {
           await rtc.setRemoteDescription(data.data.sdp);
           const sdp = await rtc.createAnswer();
@@ -372,6 +423,18 @@ export function usePull({
         }
       } else {
         console.log('收到offer,但是这个offer不是发给我的');
+        // sidebarList.value.push({ socketId: data.data.sender });
+        // sender.value = data.data.sender;
+        // const rtc = await startNewWebRtc({ receiver: data.data.sender });
+        // if (rtc) {
+        //   await rtc.setRemoteDescription(data.data.sdp);
+        //   const sdp = await rtc.createAnswer();
+        //   await rtc.setLocalDescription(sdp);
+        //   instance.send({
+        //     msgType: WsMsgTypeEnum.answer,
+        //     data: { sdp, sender: getSocketId(), receiver: data.data.sender },
+        //   });
+        // }
       }
     });
 
@@ -423,7 +486,7 @@ export function usePull({
     instance.socketIo.on(WsMsgTypeEnum.roomLiveing, (data: IAdminIn) => {
       console.log('【websocket】收到管理员正在直播', data);
       if (isSRS && !isFlv) {
-        startNewWebRtc();
+        startNewWebRtc({});
       }
     });
 
@@ -520,6 +583,8 @@ export function usePull({
     batchSendOffer,
     startGetUserMedia,
     startGetDisplayMedia,
+    addTrack,
+    addVideo,
     roomName,
     roomNoLive,
     damuList,
@@ -527,5 +592,7 @@ export function usePull({
     liveUserList,
     danmuStr,
     localStream,
+    sender,
+    sidebarList,
   };
 }

+ 49 - 18
src/hooks/use-push.ts

@@ -1,5 +1,5 @@
 import { getRandomString } from 'billd-utils';
-import { Ref, reactive, ref } from 'vue';
+import { Ref, nextTick, reactive, ref } from 'vue';
 import { useRoute, useRouter } from 'vue-router';
 
 import { fetchRtcV1Publish } from '@/api/srs';
@@ -24,9 +24,11 @@ import { useUserStore } from '@/store/user';
 
 export function usePush({
   localVideoRef,
+  remoteVideoRef,
   isSRS,
 }: {
   localVideoRef: Ref<HTMLVideoElement | undefined>;
+  remoteVideoRef: Ref<HTMLVideoElement[]>;
   isSRS?: boolean;
 }) {
   const route = useRoute();
@@ -43,6 +45,11 @@ export function usePush({
   const localStream = ref();
   const offerSended = ref(new Set());
   const hooksRtcMap = ref(new Set());
+  const sidebarList = ref<
+    {
+      socketId: string;
+    }[]
+  >([]);
 
   const track = reactive({
     audio: true,
@@ -109,15 +116,21 @@ export function usePush({
   }
 
   /** 原生的webrtc时,receiver必传 */
-  async function startNewWebRtc(receiver?: string) {
+  async function startNewWebRtc({
+    receiver,
+    videoEl = localVideoRef.value!,
+  }: {
+    receiver?: string;
+    videoEl?: HTMLVideoElement;
+  }) {
     if (isSRS) {
       console.warn('开始new SRSWebRTCClass');
       const rtc = new SRSWebRTCClass({
         roomId: `${roomId.value}___${getSocketId()}`,
-        videoEl: localVideoRef.value!,
+        videoEl,
       });
       localStream.value.getTracks().forEach((track) => {
-        rtc.addTrack({
+        rtc.addTransceiver({
           track,
           stream: localStream.value,
           direction: 'sendonly',
@@ -148,7 +161,7 @@ export function usePush({
       console.warn('开始new WebRTCClass');
       const rtc = new WebRTCClass({
         roomId: `${roomId.value}___${receiver!}`,
-        videoEl: localVideoRef.value!,
+        videoEl,
       });
       return rtc;
     }
@@ -189,7 +202,7 @@ export function usePush({
           const rtc = networkStore.getRtcMap(
             `${roomId.value}___${item.socketId}`
           );
-          rtc?.addTrack(track, localStream.value);
+          rtc?.addTransceiver(track, localStream.value);
         });
       }
     });
@@ -241,7 +254,9 @@ export function usePush({
         !offerSended.value.has(item.socketId) &&
         item.socketId !== getSocketId()
       ) {
-        hooksRtcMap.value.add(await startNewWebRtc(item.socketId));
+        hooksRtcMap.value.add(
+          await startNewWebRtc({ receiver: item.socketId })
+        );
         await addTrack();
         console.warn('new WebRTCClass完成');
         console.log('执行sendOffer', {
@@ -281,22 +296,33 @@ export function usePush({
     });
 
     // 收到offer
-    instance.socketIo.on(WsMsgTypeEnum.offer, async (data: IOffer) => {
+    instance.socketIo.on(WsMsgTypeEnum.offer, (data: IOffer) => {
       console.warn('【websocket】收到offer', data);
       if (isSRS) return;
       if (!instance) return;
       if (data.data.receiver === getSocketId()) {
         console.log('收到offer,这个offer是发给我的');
-        const rtc = await startNewWebRtc(data.data.sender);
-        if (rtc) {
-          await rtc.setRemoteDescription(data.data.sdp);
-          const sdp = await rtc.createAnswer();
-          await rtc.setLocalDescription(sdp);
-          instance.send({
-            msgType: WsMsgTypeEnum.answer,
-            data: { sdp, sender: getSocketId(), receiver: data.data.sender },
+        sidebarList.value.push({ socketId: data.data.sender });
+        nextTick(async () => {
+          console.log(
+            remoteVideoRef.value[data.data.sender],
+            remoteVideoRef.value,
+            22222
+          );
+          const rtc = await startNewWebRtc({
+            receiver: data.data.sender,
+            videoEl: remoteVideoRef.value[data.data.sender],
           });
-        }
+          if (rtc) {
+            await rtc.setRemoteDescription(data.data.sdp);
+            const sdp = await rtc.createAnswer();
+            await rtc.setLocalDescription(sdp);
+            instance.send({
+              msgType: WsMsgTypeEnum.answer,
+              data: { sdp, sender: getSocketId(), receiver: data.data.sender },
+            });
+          }
+        });
       } else {
         console.log('收到offer,但是这个offer不是发给我的');
       }
@@ -378,7 +404,7 @@ export function usePush({
         socketId: `${getSocketId()}`,
       });
       if (isSRS) {
-        startNewWebRtc();
+        startNewWebRtc({});
       } else {
         batchSendOffer();
       }
@@ -446,6 +472,10 @@ export function usePush({
         video: true,
         audio: true,
       });
+      const audio = event.getAudioTracks();
+      const video = event.getVideoTracks();
+      track.audio = !!audio.length;
+      track.video = !!video.length;
       console.log('getUserMedia成功', event);
       currMediaType.value = allMediaTypeList[MediaTypeEnum.camera];
       currMediaTypeList.value.push(allMediaTypeList[MediaTypeEnum.camera]);
@@ -587,5 +617,6 @@ export function usePush({
     liveUserList,
     currMediaTypeList,
     hooksRtcMap,
+    sidebarList,
   };
 }

+ 2 - 2
src/network/srsWebRtc.ts

@@ -76,8 +76,8 @@ export class SRSWebRTCClass {
     this.update();
   }
 
-  addTrack = ({ track, stream, direction }) => {
-    console.warn('addTrackaddTrack', track, stream);
+  addTransceiver = ({ track, stream, direction }) => {
+    console.warn('addTransceiveraddTransceiver', track, stream);
     this.sender = this.peerConnection?.addTransceiver(track, {
       streams: [stream],
       direction,

+ 7 - 2
src/network/webRtc.ts

@@ -121,8 +121,8 @@ export class WebRTCClass {
     // this.handleWebRtcError();
   }
 
-  addTrack = (track, stream) => {
-    console.warn('addTrackaddTrack', track, stream);
+  addTransceiver = (track, stream) => {
+    console.warn('addTransceiveraddTransceiver', track, stream);
     this.sender = this.peerConnection?.addTransceiver(track, {
       streams: [stream],
       direction: 'sendonly',
@@ -130,6 +130,11 @@ export class WebRTCClass {
     // this.peerConnection?.addTrack(track, stream);
   };
 
+  addTrack = (track, stream) => {
+    console.warn('addTrackaddTrack', track, stream);
+    this.peerConnection?.addTrack(track, stream);
+  };
+
   handleWebRtcError = () => {
     this.getStatsSetIntervalTimer = setInterval(() => {
       this.peerConnection

+ 4 - 4
src/showBilldVersion.ts

@@ -4,8 +4,8 @@ import BilldScss from 'billd-scss/package.json';
 import BilldUtils from 'billd-utils/package.json';
 
 console.table({
-  'billd-utils version': BilldUtils.version,
-  'billd-scss version': BilldScss.version,
-  'billd-deploy version': BilldDeploy.version,
-  'billd-html-webpack-plugin version': BilldHtmlWebpackPlugin.version,
+  'billd-utils': BilldUtils.version,
+  'billd-scss': BilldScss.version,
+  'billd-deploy': BilldDeploy.version,
+  'billd-html-webpack-plugin': BilldHtmlWebpackPlugin.version,
 });

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

@@ -55,7 +55,7 @@
           :key="index"
           :class="{ item: 1, active: item.roomId === currentLiveRoom?.roomId }"
           :style="{ backgroundImage: `url(${item.coverImg})` }"
-          @click="currentLiveRoom = item"
+          @click="changeLiveRoom(item)"
         >
           <div
             class="border"
@@ -96,6 +96,15 @@ const liveRoomList = ref<ILive[]>([]);
 const currentLiveRoom = ref<ILive>();
 const localVideoRef = ref<HTMLVideoElement>();
 
+function changeLiveRoom(item: ILive) {
+  currentLiveRoom.value = item;
+  nextTick(() => {
+    if (item.flvurl) {
+      useFlvPlay(item.flvurl, localVideoRef.value!);
+    }
+  });
+}
+
 async function getLiveRoomList() {
   try {
     const res = await fetchLiveList({

+ 127 - 55
src/views/meeting/index.vue

@@ -4,42 +4,75 @@
       ref="topRef"
       class="left"
     >
-      <div class="video-wrap">
-        <video
-          id="localVideo"
-          ref="localVideoRef"
-          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
-          v-if="!currMediaTypeList || currMediaTypeList.length <= 0"
-          class="add-wrap"
-        >
-          <n-space>
-            <n-button
-              class="item"
-              @click="startGetUserMedia"
-            >
-              摄像头
-            </n-button>
-            <n-button
-              class="item"
-              @click="startGetDisplayMedia"
-            >
-              窗口
-            </n-button>
-          </n-space>
+      <div
+        ref="containerRef"
+        class="container"
+      >
+        <div class="video-wrap">
+          <video
+            id="localVideo"
+            ref="localVideoRef"
+            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
+            v-if="currMediaTypeList.length > 0"
+            class="controls"
+          >
+            <VideoControls></VideoControls>
+          </div>
+          <div
+            v-if="!currMediaTypeList || currMediaTypeList.length <= 0"
+            class="add-wrap"
+          >
+            <n-space>
+              <n-button
+                class="item"
+                @click="startGetUserMedia"
+              >
+                摄像头
+              </n-button>
+              <n-button
+                class="item"
+                @click="startGetDisplayMedia"
+              >
+                窗口
+              </n-button>
+            </n-space>
+          </div>
+        </div>
+
+        <div class="sidebar">
+          <div class="title">在线人员</div>
+          <div
+            v-for="(item, index) in sidebarList"
+            :key="index"
+            class="item"
+          >
+            <video
+              :ref="(el) => (remoteVideoRef[item.socketId] = 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>
         </div>
       </div>
+
       <div
         ref="bottomRef"
-        class="control"
+        class="room-control"
       >
         <div class="info">
           <div
@@ -99,6 +132,7 @@
         </div>
       </div>
     </div>
+
     <div class="right">
       <div class="resource-card">
         <div class="title">素材列表</div>
@@ -168,10 +202,13 @@ import { useUserStore } from '@/store/user';
 const route = useRoute();
 const userStore = useUserStore();
 
+const liveType = route.query.liveType;
+
 const topRef = ref<HTMLDivElement>();
 const bottomRef = ref<HTMLDivElement>();
+const containerRef = ref<HTMLDivElement>();
 const localVideoRef = ref<HTMLVideoElement>();
-const liveType = route.query.liveType;
+const remoteVideoRef = ref<HTMLVideoElement[]>([]);
 
 const {
   initPush,
@@ -191,8 +228,10 @@ const {
   damuList,
   liveUserList,
   currMediaTypeList,
+  sidebarList,
 } = usePush({
   localVideoRef,
+  remoteVideoRef,
   isSRS: liveType === liveTypeEnum.srsPush,
 });
 
@@ -203,11 +242,11 @@ onUnmounted(() => {
 
 onMounted(() => {
   initPush();
-  if (topRef.value && bottomRef.value && localVideoRef.value) {
+  if (topRef.value && bottomRef.value && containerRef.value) {
     const res =
       bottomRef.value.getBoundingClientRect().top -
       topRef.value.getBoundingClientRect().top;
-    localVideoRef.value.style.height = `${res}px`;
+    containerRef.value.style.height = `${res}px`;
   }
 });
 </script>
@@ -221,37 +260,70 @@ onMounted(() => {
   .left {
     position: relative;
     display: inline-block;
+    overflow: hidden;
     box-sizing: border-box;
     width: $large-left-width;
     height: 100%;
     border-radius: 6px;
-    overflow: hidden;
     background-color: white;
     color: #9499a0;
     vertical-align: top;
 
-    .video-wrap {
-      position: relative;
-      background-color: #18191c;
-      #localVideo {
-        max-width: 100%;
-        max-height: 100%;
-      }
-      .add-wrap {
-        position: absolute;
-        top: 50%;
-        left: 50%;
+    .container {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      height: 100%;
+      background-color: #fff;
+      .video-wrap {
+        position: relative;
         display: flex;
-        align-items: center;
-        justify-content: space-around;
-        padding: 0 20px;
-        height: 50px;
-        border-radius: 5px;
-        background-color: white;
-        transform: translate(-50%, -50%);
+        flex: 1;
+        height: 100%;
+        background-color: rgba($color: #000000, $alpha: 0.5);
+        #localVideo {
+          max-width: 100%;
+          max-height: 100%;
+        }
+        .controls {
+          display: none;
+        }
+        &:hover {
+          .controls {
+            display: block;
+          }
+        }
+        .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 {
+        width: 130px;
+        height: 100%;
+        background-color: rgba($color: #000000, $alpha: 0.3);
+        .title {
+          color: white;
+        }
+        .join {
+          color: white;
+          cursor: pointer;
+        }
+        video {
+          max-width: 100%;
+        }
       }
     }
-    .control {
+    .room-control {
       position: absolute;
       right: 0;
       bottom: 0;

+ 76 - 50
src/views/pull/index.vue

@@ -17,27 +17,14 @@
             </div>
           </div>
         </div>
-        <div class="video-wrap">
-          <video
-            id="remoteVideo"
-            ref="remoteVideoRef"
-            autoplay
-            webkit-playsinline="true"
-            playsinline
-            x-webkit-airplay="allow"
-            x5-video-player-type="h5"
-            x5-video-player-fullscreen="true"
-            x5-video-orientation="portraint"
-            :muted="appStore.muted"
-          ></video>
-          <div class="controls">
-            <VideoControls></VideoControls>
-          </div>
-          <div class="sidebar">
+        <div
+          ref="containerRef"
+          class="container"
+        >
+          <div class="video-wrap">
             <video
-              id="localVideo"
-              ref="localVideoRef"
-              style="width: 100px"
+              id="remoteVideo"
+              ref="remoteVideoRef"
               autoplay
               webkit-playsinline="true"
               playsinline
@@ -47,14 +34,38 @@
               x5-video-orientation="portraint"
               :muted="appStore.muted"
             ></video>
+            <div class="controls">
+              <VideoControls></VideoControls>
+            </div>
+          </div>
+          <div class="sidebar">
+            <div
+              v-for="(item, index) in sidebarList"
+              :key="index"
+              class="item"
+            >
+              <video
+                :ref="(el) => (localVideoRef[item.socketId] = 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>
+
             <div
-              class="plus"
+              class="join"
               @click="handleJoin()"
             >
               加入
             </div>
           </div>
         </div>
+
         <div
           ref="bottomRef"
           class="gift"
@@ -148,7 +159,7 @@
 </template>
 
 <script lang="ts" setup>
-import { onMounted, onUnmounted, ref, watch } from 'vue';
+import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 
 import { usePull } from '@/hooks/use-pull';
@@ -162,8 +173,9 @@ const appStore = useAppStore();
 
 const topRef = ref<HTMLDivElement>();
 const bottomRef = ref<HTMLDivElement>();
+const containerRef = ref<HTMLDivElement>();
 const remoteVideoRef = ref<HTMLVideoElement>();
-const localVideoRef = ref<HTMLVideoElement>();
+const localVideoRef = ref<HTMLVideoElement[]>([]);
 
 const {
   initPull,
@@ -175,6 +187,8 @@ const {
   batchSendOffer,
   startGetUserMedia,
   startGetDisplayMedia,
+  addTrack,
+  addVideo,
   roomName,
   roomNoLive,
   damuList,
@@ -182,6 +196,8 @@ const {
   liveUserList,
   danmuStr,
   localStream,
+  sender,
+  sidebarList,
 } = usePull({
   localVideoRef,
   remoteVideoRef,
@@ -189,14 +205,16 @@ const {
   isSRS: route.query.liveType === liveTypeEnum.srsWebrtcPull,
 });
 
-async function handleJoin() {
-  await startGetDisplayMedia();
-  batchSendOffer();
+function handleJoin() {
+  nextTick(async () => {
+    await startGetDisplayMedia();
+    addVideo();
+  });
 }
 watch(
   () => localStream,
   (newVal) => {
-    localVideoRef.value!.srcObject = newVal.value;
+    // localVideoRef.value!.srcObject = newVal.value;
   }
 );
 
@@ -206,12 +224,12 @@ onUnmounted(() => {
 });
 
 onMounted(() => {
-  if (topRef.value && bottomRef.value && remoteVideoRef.value) {
+  if (topRef.value && bottomRef.value && containerRef.value) {
     const res =
       bottomRef.value.getBoundingClientRect().top -
       (topRef.value.getBoundingClientRect().top +
         topRef.value.getBoundingClientRect().height);
-    remoteVideoRef.value.style.height = `${res}px`;
+    containerRef.value.style.height = `${res}px`;
   }
   initPull();
 });
@@ -226,19 +244,18 @@ onMounted(() => {
   .left {
     position: relative;
     display: inline-block;
+    overflow: hidden;
     box-sizing: border-box;
     width: $large-left-width;
     height: 100%;
     border-radius: 6px;
-    background-color: white;
+    background-color: papayawhip;
     color: #9499a0;
     vertical-align: top;
-    overflow: hidden;
     .head {
       display: flex;
       justify-content: space-between;
       padding: 20px;
-      background-color: papayawhip;
       .tag {
         display: inline-block;
         margin-right: 5px;
@@ -298,33 +315,43 @@ onMounted(() => {
         }
       }
     }
-    .video-wrap {
-      position: relative;
-      background-color: #18191c;
-      #remoteVideo {
-        max-width: 100%;
-        max-height: 100%;
-      }
-      .controls {
-        display: none;
+    .container {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      .video-wrap {
+        position: relative;
+        flex: 1;
+        height: 100%;
+        background-color: rgba($color: #000000, $alpha: 0.5);
+        #remoteVideo {
+          width: 100%;
+          height: 100%;
+        }
+        .controls {
+          display: none;
+        }
+
+        &:hover {
+          .controls {
+            display: block;
+          }
+        }
       }
       .sidebar {
-        position: absolute;
-        right: 0;
-        top: 0;
+        width: 120px;
         height: 100%;
         background-color: rgba($color: #000000, $alpha: 0.3);
-        .plus {
+        .join {
           color: white;
           cursor: pointer;
         }
-      }
-      &:hover {
-        .controls {
-          display: block;
+        video {
+          max-width: 100%;
         }
       }
     }
+
     .gift {
       position: absolute;
       right: 0;
@@ -334,7 +361,6 @@ onMounted(() => {
       align-items: center;
       justify-content: space-around;
       height: 100px;
-      background-color: papayawhip;
       .item {
         margin-right: 10px;
         text-align: center;

+ 127 - 55
src/views/push/index.vue

@@ -4,42 +4,75 @@
       ref="topRef"
       class="left"
     >
-      <div class="video-wrap">
-        <video
-          id="localVideo"
-          ref="localVideoRef"
-          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
-          v-if="!currMediaTypeList || currMediaTypeList.length <= 0"
-          class="add-wrap"
-        >
-          <n-space>
-            <n-button
-              class="item"
-              @click="startGetUserMedia"
-            >
-              摄像头
-            </n-button>
-            <n-button
-              class="item"
-              @click="startGetDisplayMedia"
-            >
-              窗口
-            </n-button>
-          </n-space>
+      <div
+        ref="containerRef"
+        class="container"
+      >
+        <div class="video-wrap">
+          <video
+            id="localVideo"
+            ref="localVideoRef"
+            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
+            v-if="currMediaTypeList.length > 0"
+            class="controls"
+          >
+            <VideoControls></VideoControls>
+          </div>
+          <div
+            v-if="!currMediaTypeList || currMediaTypeList.length <= 0"
+            class="add-wrap"
+          >
+            <n-space>
+              <n-button
+                class="item"
+                @click="startGetUserMedia"
+              >
+                摄像头
+              </n-button>
+              <n-button
+                class="item"
+                @click="startGetDisplayMedia"
+              >
+                窗口
+              </n-button>
+            </n-space>
+          </div>
+        </div>
+
+        <div class="sidebar">
+          <div class="title">在线人员</div>
+          <div
+            v-for="(item, index) in sidebarList"
+            :key="index"
+            class="item"
+          >
+            <video
+              :ref="(el) => (remoteVideoRef[item.socketId] = 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>
         </div>
       </div>
+
       <div
         ref="bottomRef"
-        class="control"
+        class="room-control"
       >
         <div class="info">
           <div
@@ -99,6 +132,7 @@
         </div>
       </div>
     </div>
+
     <div class="right">
       <div class="resource-card">
         <div class="title">素材列表</div>
@@ -168,10 +202,13 @@ import { useUserStore } from '@/store/user';
 const route = useRoute();
 const userStore = useUserStore();
 
+const liveType = route.query.liveType;
+
 const topRef = ref<HTMLDivElement>();
 const bottomRef = ref<HTMLDivElement>();
+const containerRef = ref<HTMLDivElement>();
 const localVideoRef = ref<HTMLVideoElement>();
-const liveType = route.query.liveType;
+const remoteVideoRef = ref<HTMLVideoElement[]>([]);
 
 const {
   initPush,
@@ -191,8 +228,10 @@ const {
   damuList,
   liveUserList,
   currMediaTypeList,
+  sidebarList,
 } = usePush({
   localVideoRef,
+  remoteVideoRef,
   isSRS: liveType === liveTypeEnum.srsPush,
 });
 
@@ -203,11 +242,11 @@ onUnmounted(() => {
 
 onMounted(() => {
   initPush();
-  if (topRef.value && bottomRef.value && localVideoRef.value) {
+  if (topRef.value && bottomRef.value && containerRef.value) {
     const res =
       bottomRef.value.getBoundingClientRect().top -
       topRef.value.getBoundingClientRect().top;
-    localVideoRef.value.style.height = `${res}px`;
+    containerRef.value.style.height = `${res}px`;
   }
 });
 </script>
@@ -221,37 +260,70 @@ onMounted(() => {
   .left {
     position: relative;
     display: inline-block;
+    overflow: hidden;
     box-sizing: border-box;
     width: $large-left-width;
     height: 100%;
     border-radius: 6px;
-    overflow: hidden;
     background-color: white;
     color: #9499a0;
     vertical-align: top;
 
-    .video-wrap {
-      position: relative;
-      background-color: #18191c;
-      #localVideo {
-        max-width: 100%;
-        max-height: 100%;
-      }
-      .add-wrap {
-        position: absolute;
-        top: 50%;
-        left: 50%;
+    .container {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      height: 100%;
+      background-color: #fff;
+      .video-wrap {
+        position: relative;
         display: flex;
-        align-items: center;
-        justify-content: space-around;
-        padding: 0 20px;
-        height: 50px;
-        border-radius: 5px;
-        background-color: white;
-        transform: translate(-50%, -50%);
+        flex: 1;
+        height: 100%;
+        background-color: rgba($color: #000000, $alpha: 0.5);
+        #localVideo {
+          max-width: 100%;
+          max-height: 100%;
+        }
+        .controls {
+          display: none;
+        }
+        &:hover {
+          .controls {
+            display: block;
+          }
+        }
+        .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 {
+        width: 130px;
+        height: 100%;
+        background-color: rgba($color: #000000, $alpha: 0.3);
+        .title {
+          color: white;
+        }
+        .join {
+          color: white;
+          cursor: pointer;
+        }
+        video {
+          max-width: 100%;
+        }
       }
     }
-    .control {
+    .room-control {
       position: absolute;
       right: 0;
       bottom: 0;