shuisheng 2 rokov pred
rodič
commit
13ca214564

+ 12 - 12
.cz-config.js

@@ -9,28 +9,28 @@ console.log(
 module.exports = {
   types: [
     {
-      value: 'build',
-      name: '🚀  build:     版本打包/Tag',
-    },
-    {
-      value: 'ci',
-      name: '👷  ci:        CI Build',
+      value: 'feat',
+      name: '✨  feat:      新功能',
     },
     {
-      value: 'chore',
-      name: '📦️  chore:     构建工具调整',
+      value: 'fix',
+      name: '🐛  fix:       修补bug',
     },
     {
       value: 'docs',
       name: '📝  docs:      文档新增/变更',
     },
     {
-      value: 'feat',
-      name: '✨  feat:      新功能',
+      value: 'ci',
+      name: '👷  ci:        CI Build',
     },
     {
-      value: 'fix',
-      name: '🐛  fix:       修补bug',
+      value: 'build',
+      name: '🚀  build:     版本打包/Tag',
+    },
+    {
+      value: 'chore',
+      name: '📦️  chore:     构建工具调整',
     },
     {
       value: 'perf',

+ 2 - 1
src/api/live.ts

@@ -1,8 +1,9 @@
 import request from '@/utils/request';
 
-export function fetchLiveList() {
+export function fetchLiveList(params: { orderName: string; orderBy: string }) {
   return request.instance({
     url: '/api/live/list',
     method: 'get',
+    params,
   });
 }

+ 12 - 0
src/interface.ts

@@ -10,6 +10,18 @@ export enum DanmuMsgTypeEnum {
   userLeaved,
 }
 
+export interface ILiveUser {
+  socketId: string;
+  avatar: string;
+  expr: number;
+}
+
+export interface IDanmu {
+  socketId: string;
+  msgType: DanmuMsgTypeEnum;
+  msg: string;
+}
+
 export interface IAdminIn {
   roomId: string;
   socketId: string;

+ 75 - 33
src/layout/head/index.vue

@@ -9,7 +9,9 @@
       </div>
       <div class="nav">
         <div
-          v-for="(item, index) in pushList"
+          v-for="(item, index) in pushList.filter(
+            (item) => router.currentRoute.value.name === item.routerName
+          )"
           :key="index"
           :class="{
             item: 1,
@@ -19,23 +21,39 @@
         >
           {{ item.title }}
         </div>
+        <div
+          v-for="(item, index) in pullList.filter(
+            (item) => router.currentRoute.value.name === item.routerName
+          )"
+          :key="index"
+          :class="{
+            item: 1,
+            active: router.currentRoute.value.name === item.routerName,
+          }"
+        >
+          {{ item.title }}
+        </div>
       </div>
     </div>
-    <div class="search">
+    <!-- <div class="search">
       <input
         class="ipt"
         type="text"
         placeholder="搜索"
       />
-    </div>
+    </div> -->
     <div class="right">
-      <iframe
-        src="https://ghbtns.com/github-btn.html?user=galaxy-s10&repo=billd-live&type=star&count=true&v=2"
-        frameborder="0"
-        scrolling="0"
-        width="105px"
-        height="21px"
-      ></iframe>
+      <a
+        class="github-btn"
+        target="_blank"
+        href="https://github.com/galaxy-s10/billd-live"
+      >
+        <img
+          src="https://img.shields.io/github/stars/galaxy-s10/billd-live?label=Star&logo=GitHub&labelColor=white&logoColor=black&style=social&cacheSeconds=3600"
+          alt=""
+        />
+      </a>
+
       <!-- <iframe
         src="https://ghbtns.com/github-btn.html?user=galaxy-s10&repo=billd-live&type=fork&count=true&v=2"
         frameborder="0"
@@ -46,17 +64,24 @@
 
       <div
         v-if="router.currentRoute.value.name !== routerName.sponsors"
-        class="start"
+        class="sponsors"
         @click="router.push({ name: routerName.sponsors })"
       >
         赞助支持
       </div>
       <div
-        v-if="router.currentRoute.value.name !== routerName.webrtcPush"
+        v-if="![routerName.webrtcPush].includes(router.currentRoute.value.name as string)"
         class="start ani"
         @click="goPushPage(routerName.webrtcPush)"
       >
-        我要开播
+        webrtc开播
+      </div>
+      <div
+        v-if="![routerName.srsWebRtcPush].includes(router.currentRoute.value.name as string)"
+        class="start ani"
+        @click="goPushPage(routerName.srsWebRtcPush)"
+      >
+        srs-webrtc开播
       </div>
     </div>
   </div>
@@ -65,20 +90,22 @@
 <script lang="ts" setup>
 import { openToTarget } from 'billd-utils';
 import { ref } from 'vue';
-import { useRoute, useRouter } from 'vue-router';
+import { useRouter } from 'vue-router';
 
 import { routerName } from '@/router';
 
 const router = useRouter();
-const route = useRoute();
 
 const pushList = ref([
-  // { title: 'Webrtc Pull', routerName: 'webrtcPull' },
-  // { title: 'SRS WebRTC Pull', routerName: 'srsWebRtcPull' },
   { title: 'Webrtc Push', routerName: routerName.webrtcPush },
   { title: 'SRS WebRTC Push', routerName: routerName.srsWebRtcPush },
 ]);
 
+const pullList = ref([
+  { title: 'Webrtc Pull', routerName: routerName.webrtcPull },
+  { title: 'SRS WebRTC Pull', routerName: routerName.srsWebRtcPull },
+]);
+
 function goPushPage(routerName: string) {
   const url = router.resolve({ name: routerName });
   openToTarget(url.href);
@@ -90,6 +117,7 @@ function goPushPage(routerName: string) {
   display: flex;
   align-items: center;
   justify-content: space-between;
+  min-width: $medium-width;
   height: 64px;
   background-color: #fff;
   box-shadow: inset 0 -1px #f1f2f3 !important;
@@ -145,23 +173,23 @@ function goPushPage(routerName: string) {
       }
     }
   }
-  .search {
-    flex: 1;
+  // .search {
+  //   flex: 1;
 
-    .ipt {
-      display: block;
-      box-sizing: border-box;
-      margin: 0 auto;
-      padding: 10px 20px;
-      min-width: 200px;
-      outline: none;
-      border: 1px solid hsla(0, 0%, 60%, 0.2);
-      border-radius: 8px;
-      border-radius: 10px;
-      background-color: #f1f2f3;
-      font-size: 14px;
-    }
-  }
+  //   .ipt {
+  //     display: block;
+  //     box-sizing: border-box;
+  //     margin: 0 auto;
+  //     padding: 10px 20px;
+  //     min-width: 200px;
+  //     outline: none;
+  //     border: 1px solid hsla(0, 0%, 60%, 0.2);
+  //     border-radius: 8px;
+  //     border-radius: 10px;
+  //     background-color: #f1f2f3;
+  //     font-size: 14px;
+  //   }
+  // }
   .right {
     display: flex;
     align-items: center;
@@ -177,6 +205,20 @@ function goPushPage(routerName: string) {
       }
     }
 
+    .github-btn {
+      margin-right: 10px;
+      img {
+        display: block;
+      }
+    }
+
+    .sponsors {
+      margin-right: 10px;
+      padding: 5px 10px;
+      border-radius: 6px;
+      font-size: 14px;
+      cursor: pointer;
+    }
     .start {
       margin-right: 10px;
       padding: 5px 10px;

+ 8 - 0
src/layout/index.vue

@@ -13,6 +13,14 @@ import HeadCpt from './head/index.vue';
 
 <style lang="scss" scoped>
 .layout {
+  min-width: $large-width;
   min-height: 100vh;
 }
+
+// 屏幕宽度小于$large-width的时候
+@media screen and (max-width: $large-width) {
+  .layout {
+    min-width: $medium-width;
+  }
+}
 </style>

+ 22 - 6
src/views/home/index.vue

@@ -68,7 +68,10 @@ const currentLiveRoom = ref<IRoom>();
 
 async function getLiveRoomList() {
   try {
-    const res = await fetchLiveList();
+    const res = await fetchLiveList({
+      orderName: 'created_at',
+      orderBy: 'desc',
+    });
     if (res.code === 200) {
       liveRoomList.value = res.data.rows.map((item) => {
         return {
@@ -78,7 +81,7 @@ async function getLiveRoomList() {
           srs: JSON.parse(item.data).data.srs,
         };
       });
-      if (res.data.count) {
+      if (res.data.total) {
         const item = res.data.rows[0].data;
         currentLiveRoom.value = {
           roomId: res.data.rows[0].roomId,
@@ -142,11 +145,11 @@ function joinRoom() {
       top: 50%;
       left: 50%;
       display: none;
-      padding: 10px 20px;
+      padding: 14px 26px;
       border: 1px solid rgba($color: skyblue, $alpha: 0.3);
       border-radius: 4px;
       color: skyblue;
-      font-size: 14px;
+      font-size: 16px;
       cursor: pointer;
       transform: translate(-50%, -50%);
       &:hover {
@@ -172,6 +175,7 @@ function joinRoom() {
         margin-bottom: 10px;
         width: 200px;
         height: 110px;
+        border-radius: 4px;
         background-color: rgba($color: #000000, $alpha: 0.3);
         cursor: pointer;
 
@@ -186,7 +190,9 @@ function joinRoom() {
           right: 0;
           bottom: 0;
           left: 0;
+          z-index: 1;
           border: 2px solid skyblue;
+          border-radius: 4px;
         }
         .triangle {
           position: absolute;
@@ -219,10 +225,20 @@ function joinRoom() {
         }
         .txt {
           position: absolute;
-          bottom: 2px;
-          left: 4px;
+          bottom: 0;
+          left: 0;
+          box-sizing: border-box;
+          padding: 4px 8px;
+          width: 100%;
+          background-image: linear-gradient(
+            -180deg,
+            rgba(0, 0, 0, 0),
+            rgba(0, 0, 0, 0.6)
+          );
           color: white;
+          text-align: initial;
           font-size: 13px;
+          border-radius: 0 0 4px 4px;
         }
       }
     }

+ 25 - 8
src/views/sponsors/index.vue

@@ -9,10 +9,13 @@
         :key="index"
         class="item"
       >
-        <div class="info">
-          支付宝账号:{{ item.buyer_logon_id }},赞助了:{{ item.subject }}({{
-            item.total_amount
-          }}元),状态:{{
+        <div class="time">发起时间:{{ item.created_at }},</div>
+        <div class="account">支付宝账号:{{ item.buyer_logon_id }},</div>
+        <div class="gift">
+          赞助了:{{ item.subject }}({{ item.total_amount }}元),
+        </div>
+        <div class="status">
+          状态:{{
             item.trade_status === PayStatusEnum.WAIT_BUYER_PAY
               ? '支付中'
               : '已支付'
@@ -279,21 +282,35 @@ function getPayStatus(outTradeNo: string) {
     height: 200px;
     background-color: papayawhip;
     .item {
-      display: flex;
+      display: inline-flex;
+      flex-wrap: wrap;
       justify-content: center;
       margin-bottom: 4px;
       width: 100%;
+      text-align: left;
+
+      .account {
+        width: 250px;
+      }
+      .gift {
+        width: 260px;
+      }
+      .status {
+        width: 120px;
+        text-align: left;
+      }
       .time {
-        width: 300px;
+        width: 280px;
       }
     }
   }
   .gift-list {
     display: flex;
     align-items: center;
+    flex-wrap: wrap;
     justify-content: center;
     .item {
-      margin: 0 10px;
+      margin: 5px;
       padding: 5px 10px;
       border-radius: 4px;
       background-color: skyblue;
@@ -324,8 +341,8 @@ function getPayStatus(outTradeNo: string) {
     }
   }
   .bottom {
-    width: 100%;
     margin-top: 2px;
+    width: 100%;
     text-align: center;
     font-size: 14px;
   }

+ 126 - 213
src/views/srs-webrtc-pull/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="pull-wrap">
+  <div class="srt-webrtc-pull-wrap">
     <template v-if="roomNoLive">当前房间没在直播~</template>
     <template v-else>
       <div class="left">
@@ -10,7 +10,7 @@
           <div class="info">
             <div class="avatar"></div>
             <div class="detail">
-              <div class="top">房间号:{{ route.params.roomId }}</div>
+              <div class="top">房间名:{{ roomName }}</div>
               <div class="bottom">
                 <span>你的socketId:{{ getSocketId() }}</span>
               </div>
@@ -72,8 +72,18 @@
             :key="index"
             class="item"
           >
-            <span class="name">{{ item.socketId }}:</span>
-            <span class="msg">{{ item.msg }}</span>
+            <template v-if="item.msgType === DanmuMsgTypeEnum.danmu">
+              <span class="name">{{ item.socketId }}:</span>
+              <span class="msg">{{ item.msg }}</span>
+            </template>
+            <template v-else-if="item.msgType === DanmuMsgTypeEnum.otherJoin">
+              <span class="name system">系统通知:</span>
+              <span class="msg">{{ item.socketId }}进入直播!</span>
+            </template>
+            <template v-else-if="item.msgType === DanmuMsgTypeEnum.userLeaved">
+              <span class="name system">系统通知:</span>
+              <span class="msg">{{ item.socketId }}离开直播!</span>
+            </template>
           </div>
         </div>
         <div class="send-msg">
@@ -95,13 +105,12 @@
 
 <script lang="ts" setup>
 import { getRandomString } from 'billd-utils';
-import { onMounted, onUnmounted, ref } from 'vue';
+import { onMounted, onUnmounted, reactive, ref } from 'vue';
 import { useRoute } from 'vue-router';
 
 import { fetchRtcV1Play } from '@/api/srs';
-import { IAdminIn, LiveTypeEnum } from '@/interface';
+import { DanmuMsgTypeEnum, IAdminIn, IDanmu, ILiveUser } from '@/interface';
 import { SRSWebRTCClass } from '@/network/srsWebRtc';
-import { WebRTCClass } from '@/network/webRtc';
 import {
   WebSocketClass,
   WsConnectStatusEnum,
@@ -111,23 +120,22 @@ import { useNetworkStore } from '@/store/network';
 
 const networkStore = useNetworkStore();
 const route = useRoute();
-const danmuStr = ref('');
+
 const topRef = ref<HTMLDivElement>();
 const bottomRef = ref<HTMLDivElement>();
+const localVideoRef = ref<HTMLVideoElement>();
+const trackInfo = reactive({
+  audio: true,
+  video: true,
+});
+const streamurl = ref();
 const roomNoLive = ref(false);
-const isAddTrack = ref(false);
-const roomIdRef = ref<HTMLInputElement>();
-const joinRef = ref<HTMLButtonElement>();
-const leaveRef = ref<HTMLButtonElement>();
 const roomId = ref('');
+const roomName = ref('');
+const danmuStr = ref('');
 const websocketInstant = ref<WebSocketClass>();
-const isDone = ref(false);
-const localVideoRef = ref<HTMLVideoElement>();
-const localStream = ref();
-const currType = ref(LiveTypeEnum.camera); // 1:摄像头,2:录屏
-const joined = ref(false);
-const offerSended = ref(new Set());
-
+const damuList = ref<IDanmu[]>([]);
+const liveUserList = ref<ILiveUser[]>([]);
 const giftList = ref([
   { name: '鲜花', ico: '', price: '免费' },
   { name: '肥宅水', ico: '', price: '2元' },
@@ -136,89 +144,18 @@ const giftList = ref([
   { name: '一杯咖啡', ico: '', price: '10元' },
 ]);
 
-const damuList = ref<
-  {
-    socketId: string;
-    msgType: number;
-    msg: string;
-  }[]
->([]);
-
-const liveUserList = ref<
-  {
-    socketId: string;
-    avatar: string;
-    expr: number;
-  }[]
->([]);
-
 function closeWs() {
   const instance = networkStore.wsMap.get(roomId.value);
   if (!instance) return;
   instance.close();
 }
 
-function sendDanmu() {
-  if (!danmuStr.value.length) {
-    alert('请输入弹幕内容!');
-  }
-  if (!websocketInstant.value) return;
-  websocketInstant.value.send({
-    msgType: WsMsgTypeEnum.message,
-    data: { msg: danmuStr.value },
-  });
-  damuList.value.push({
-    socketId: getSocketId(),
-    msgType: 1,
-    msg: danmuStr.value,
+function closeRtc() {
+  networkStore.rtcMap.forEach((rtc) => {
+    rtc.close();
   });
-  danmuStr.value = '';
 }
 
-onUnmounted(() => {
-  closeWs();
-});
-
-onMounted(() => {
-  if (topRef.value && bottomRef.value && localVideoRef.value) {
-    const res =
-      bottomRef.value.getBoundingClientRect().top -
-      (topRef.value.getBoundingClientRect().top +
-        topRef.value.getBoundingClientRect().height);
-    localVideoRef.value.style.height = `${res}px`;
-  }
-  roomId.value = route.params.roomId as string;
-  console.warn('开始new WebSocketClass');
-  websocketInstant.value = new WebSocketClass({
-    roomId: roomId.value,
-    url:
-      process.env.NODE_ENV === 'development'
-        ? 'ws://localhost:4300'
-        : 'wss://live.hsslive.cn',
-    isAdmin: false,
-  });
-  websocketInstant.value.update();
-  initReceive();
-  sendJoin();
-
-  localVideoRef.value?.addEventListener('loadstart', () => {
-    console.warn('视频流-loadstart');
-    const rtc = networkStore.getRtcMap(roomId.value);
-    if (!rtc) return;
-    rtc.rtcStatus.loadstart = true;
-    rtc.update();
-  });
-
-  localVideoRef.value?.addEventListener('loadedmetadata', () => {
-    console.warn('视频流-loadedmetadata');
-    const rtc = networkStore.getRtcMap(roomId.value);
-    if (!rtc) return;
-    rtc.rtcStatus.loadedmetadata = true;
-    rtc.update();
-    batchSendOffer();
-  });
-});
-
 function getSocketId() {
   return networkStore.wsMap.get(roomId.value!)?.socketIo?.id || '-1';
 }
@@ -229,48 +166,52 @@ function sendJoin() {
   instance.send({ msgType: WsMsgTypeEnum.join, data: {} });
 }
 
-function batchSendOffer() {
-  liveUserList.value.forEach(async (item) => {
-    if (
-      !offerSended.value.has(item.socketId) &&
-      item.socketId !== getSocketId()
-    ) {
-      await startNewWebRtc(item.socketId);
-      await addTrack();
-      console.warn('new WebRTCClass完成');
-      console.log('执行sendOffer', {
-        sender: getSocketId(),
-        receiver: item.socketId,
-      });
-      sendOffer({ sender: getSocketId(), receiver: item.socketId });
-      offerSended.value.add(item.socketId);
-    }
+function sendDanmu() {
+  if (!danmuStr.value.length) {
+    alert('请输入弹幕内容!');
+  }
+  if (!websocketInstant.value) return;
+  websocketInstant.value.send({
+    msgType: WsMsgTypeEnum.message,
+    data: { msg: danmuStr.value },
+  });
+  damuList.value.push({
+    socketId: getSocketId(),
+    msgType: DanmuMsgTypeEnum.danmu,
+    msg: danmuStr.value,
   });
+  danmuStr.value = '';
 }
 
-function closeRtc() {
-  networkStore.rtcMap.forEach((rtc) => {
-    rtc.close();
-  });
+function startNewWebRtc(receiver: string) {
+  console.warn('开始new SRSWebRTCClass', receiver);
+  const rtc = new SRSWebRTCClass({ roomId: `${roomId.value}___${receiver}` });
+  rtc.rtcStatus.joined = true;
+  rtc.update();
+  return rtc;
 }
 
 async function handleSrsPlay() {
-  const rtc = new SRSWebRTCClass({
-    roomId: `${roomId.value}___${getSocketId()}`,
-  });
-  if (!rtc) return;
-  // rtc.addTrack({ track, stream: localStream.value, direction: 'recvonly' });
-  rtc.peerConnection?.addTransceiver('audio', { direction: 'recvonly' });
-  rtc.peerConnection?.addTransceiver('video', { direction: 'recvonly' });
+  const rtc = startNewWebRtc(getSocketId());
+  if (trackInfo.video) {
+    rtc.peerConnection?.addTransceiver('video', { direction: 'recvonly' });
+  }
+  if (trackInfo.audio) {
+    rtc.peerConnection?.addTransceiver('audio', { direction: 'recvonly' });
+  }
   try {
     const offer = await rtc.createOffer();
     if (!offer) return;
     await rtc.setLocalDescription(offer);
     const res: any = await fetchRtcV1Play({
-      api: 'http://localhost:1985/rtc/v1/play/',
+      api: `http://${
+        process.env.NODE_ENV === 'development'
+          ? 'localhost:1985'
+          : 'live.hsslive.cn:1985'
+      }/rtc/v1/play/`,
       clientip: null,
       sdp: offer.sdp!,
-      streamurl: `webrtc://localhost/live/livestream/${roomId.value}`,
+      streamurl: streamurl.value,
       tid: getRandomString(10),
     });
     await rtc.setRemoteDescription(
@@ -330,7 +271,7 @@ function initReceive() {
     if (!instance) return;
     damuList.value.push({
       socketId: data.socketId,
-      msgType: 1,
+      msgType: DanmuMsgTypeEnum.danmu,
       msg: data.data.msg,
     });
   });
@@ -338,15 +279,20 @@ function initReceive() {
   // 用户加入房间
   instance.socketIo.on(WsMsgTypeEnum.joined, (data) => {
     console.log('【websocket】用户加入房间完成', data);
-    joined.value = true;
+    roomName.value = data.data.roomName;
+    trackInfo.audio = data.data.trackInfo.audio;
+    trackInfo.video = data.data.trackInfo.video;
+    streamurl.value = data.data.srs.streamurl;
   });
 
   // 其他用户加入房间
   instance.socketIo.on(WsMsgTypeEnum.otherJoin, (data) => {
     console.log('【websocket】其他用户加入房间', data);
-    if (joined.value) {
-      batchSendOffer();
-    }
+    damuList.value.push({
+      socketId: data.socketId,
+      msgType: DanmuMsgTypeEnum.otherJoin,
+      msg: '',
+    });
   });
 
   // 用户离开房间
@@ -366,96 +312,60 @@ function initReceive() {
       (item) => item.socketId !== data.socketId
     );
     liveUserList.value = res;
-    console.log('当前所有在线用户', JSON.stringify(res));
-  });
-}
-
-async function startMediaDevices() {
-  currType.value = LiveTypeEnum.camera;
-  if (!localStream.value) {
-    // WARN navigator.mediaDevices在localhost和https才能用,http://192.168.1.103:8000局域网用不了
-    const event = await navigator.mediaDevices.getUserMedia({
-      video: true,
-      audio: true,
+    damuList.value.push({
+      socketId: data.socketId,
+      msgType: DanmuMsgTypeEnum.userLeaved,
+      msg: '',
     });
-    console.log('getUserMedia成功', event);
-    if (!localVideoRef.value) return;
-    localVideoRef.value.srcObject = event;
-    localStream.value = event;
-  }
-}
-function addTrack() {
-  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?.addTrack(track, localStream.value);
-      });
-    }
   });
-  isAddTrack.value = true;
 }
 
-async function startGetDisplayMedia() {
-  currType.value = LiveTypeEnum.screen;
-  if (!localStream.value) {
-    // WARN navigator.mediaDevices.getDisplayMedia在localhost和https才能用,http://192.168.1.103:8000局域网用不了
-    const event = await navigator.mediaDevices.getDisplayMedia({
-      video: true,
-      audio: true,
-    });
-    console.log('getDisplayMedia成功', event);
-    if (!localVideoRef.value) return;
-    localVideoRef.value.srcObject = event;
-    localStream.value = event;
-  }
-}
+onUnmounted(() => {
+  closeWs();
+});
 
-async function sendOffer({
-  sender,
-  receiver,
-}: {
-  sender: string;
-  receiver: string;
-}) {
-  if (isDone.value) return;
-  if (!websocketInstant.value) return;
-  const rtc = networkStore.getRtcMap(`${roomId.value}___${receiver}`);
-  if (!rtc) return;
-  const sdp = await rtc.createOffer();
-  await rtc.setLocalDescription(sdp);
-  websocketInstant.value.send({
-    msgType: WsMsgTypeEnum.offer,
-    data: { sdp, sender, receiver },
+onMounted(() => {
+  if (topRef.value && bottomRef.value && localVideoRef.value) {
+    const res =
+      bottomRef.value.getBoundingClientRect().top -
+      (topRef.value.getBoundingClientRect().top +
+        topRef.value.getBoundingClientRect().height);
+    localVideoRef.value.style.height = `${res}px`;
+  }
+  roomId.value = route.params.roomId as string;
+  console.warn('开始new WebSocketClass');
+  websocketInstant.value = new WebSocketClass({
+    roomId: roomId.value,
+    url:
+      process.env.NODE_ENV === 'development'
+        ? 'ws://localhost:4300'
+        : 'wss://live.hsslive.cn',
+    isAdmin: false,
   });
-}
+  websocketInstant.value.update();
+  initReceive();
+  sendJoin();
 
-function startNewWebRtc(receiver: string) {
-  console.warn('开始new WebRTCClass', receiver);
-  const rtc = new WebRTCClass({ roomId: `${roomId.value}___${receiver}` });
-  rtc.rtcStatus.joined = true;
-  rtc.update();
-  return rtc;
-}
+  localVideoRef.value?.addEventListener('loadstart', () => {
+    console.warn('视频流-loadstart');
+    const rtc = networkStore.getRtcMap(roomId.value);
+    if (!rtc) return;
+    rtc.rtcStatus.loadstart = true;
+    rtc.update();
+  });
 
-function leave() {
-  if (joinRef.value && leaveRef.value && roomIdRef.value) {
-    roomIdRef.value.disabled = false;
-    joinRef.value.disabled = false;
-    leaveRef.value.disabled = true;
-  }
-  if (!websocketInstant.value) return;
-  websocketInstant.value.socketIo?.emit(WsMsgTypeEnum.leave, {
-    roomId: websocketInstant.value.roomId,
+  localVideoRef.value?.addEventListener('loadedmetadata', () => {
+    console.warn('视频流-loadedmetadata');
+    const rtc = networkStore.getRtcMap(roomId.value);
+    if (!rtc) return;
+    rtc.rtcStatus.loadedmetadata = true;
+    rtc.update();
   });
-}
+});
 </script>
 
 <style lang="scss" scoped>
-.pull-wrap {
+.srt-webrtc-pull-wrap {
   margin: 20px auto 0;
   min-width: $large-width;
   height: 700px;
@@ -495,7 +405,7 @@ function leave() {
           width: 64px;
           height: 64px;
           border-radius: 50%;
-          background-color: yellow;
+          background-color: skyblue;
         }
         .detail {
           .top {
@@ -552,7 +462,7 @@ function leave() {
       align-items: center;
       justify-content: space-around;
       height: 100px;
-      background-color: yellow;
+      background-color: papayawhip;
       .item {
         margin-right: 10px;
         text-align: center;
@@ -577,12 +487,11 @@ function leave() {
     position: relative;
     display: inline-block;
     box-sizing: border-box;
-    box-sizing: border-box;
     margin-left: 10px;
     min-width: 300px;
     height: 100%;
-    border-radius: 10px;
-    background-color: white;
+    border-radius: 6px;
+    background-color: papayawhip;
     color: #9499a0;
     .tab {
       display: flex;
@@ -621,13 +530,16 @@ function leave() {
     .danmu-list {
       overflow-y: scroll;
       padding: 0 15px;
-      height: 350px;
+      height: 450px;
       text-align: initial;
       .item {
         margin-bottom: 10px;
         font-size: 12px;
         .name {
           color: #9499a0;
+          &.system {
+            color: red;
+          }
         }
         .msg {
           color: #61666d;
@@ -660,10 +572,11 @@ function leave() {
         padding: 5px;
         width: 80px;
         border-radius: 4px;
-        background-color: #23ade5;
+        background-color: skyblue;
         color: white;
         text-align: center;
         font-size: 12px;
+        cursor: pointer;
       }
     }
   }
@@ -671,7 +584,7 @@ function leave() {
 
 // 屏幕宽度小于$large-width的时候
 @media screen and (max-width: $large-width) {
-  .pull-wrap {
+  .srt-webrtc-pull-wrap {
     .left {
       width: $medium-left-width;
     }

+ 0 - 689
src/views/srs-webrtc-push/index copy.vue

@@ -1,689 +0,0 @@
-<template>
-  <div class="push-wrap">
-    <div
-      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
-          controls
-        ></video>
-        <div
-          v-if="!currMediaTypeList || currMediaTypeList.length <= 0"
-          class="add-wrap"
-        >
-          <div
-            class="item"
-            @click="startMediaDevices"
-          >
-            摄像头
-          </div>
-          <div
-            class="item"
-            @click="startGetDisplayMedia"
-          >
-            窗口
-          </div>
-        </div>
-      </div>
-      <div
-        ref="bottomRef"
-        class="control"
-      >
-        <div class="info">
-          <div class="avatar"></div>
-          <div class="detail">
-            <div class="top">
-              <input
-                ref="roomNameRef"
-                v-model="roomName"
-                type="text"
-                placeholder="输入房间名"
-              />
-              <button
-                class="btn"
-                @click="confirmRoomName"
-              >
-                确定
-              </button>
-              <!-- 房东的猫livehouse/音乐节 -->
-            </div>
-            <div class="bottom">
-              <span>socketId:{{ getSocketId() }}</span>
-            </div>
-          </div>
-        </div>
-        <div class="other">
-          <div class="top">
-            <span class="item">
-              <i class="ico"></i>
-              <span>正在观看人数:{{ liveUserList.length }}</span>
-            </span>
-          </div>
-          <div class="bottom">
-            <button @click="startLive">srs-webrtc直播</button>
-            <button @click="endLive">结束直播</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 currMediaTypeList"
-            :key="index"
-            class="item"
-          >
-            <span class="name">{{ item }}</span>
-          </div>
-        </div>
-      </div>
-      <div class="danmu-card">
-        <div class="title">弹幕互动</div>
-        <div class="list">
-          <div
-            v-for="(item, index) in damuList"
-            :key="index"
-            class="item"
-          >
-            <span class="name">{{ item.socketId }}:</span>
-            <span class="msg">{{ item.msg }}</span>
-          </div>
-        </div>
-
-        <div class="send-msg">
-          <input
-            v-model="danmuStr"
-            class="ipt"
-          />
-          <div
-            class="btn"
-            @click="sendDanmu"
-          >
-            发送
-          </div>
-        </div>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script lang="ts" setup>
-import { getRandomString } from 'billd-utils';
-import { onMounted, onUnmounted, ref } from 'vue';
-
-import { fetchRtcV1Publish } from '@/api/srs';
-import { IAdminIn, LiveTypeEnum } from '@/interface';
-import { SRSWebRTCClass } from '@/network/srsWebRtc';
-import {
-  WebSocketClass,
-  WsConnectStatusEnum,
-  WsMsgTypeEnum,
-} from '@/network/webSocket';
-import { useNetworkStore } from '@/store/network';
-
-const networkStore = useNetworkStore();
-
-const topRef = ref<HTMLDivElement>();
-const bottomRef = ref<HTMLDivElement>();
-const roomIdRef = ref<HTMLInputElement>();
-const joinRef = ref<HTMLButtonElement>();
-const leaveRef = ref<HTMLButtonElement>();
-const defaultRoomId = getRandomString(15);
-const roomId = ref<string>(defaultRoomId);
-const streamurl = ref(`webrtc://localhost/live/livestream/${roomId.value}`);
-const danmuStr = ref('');
-const roomName = ref('');
-const roomNameRef = ref<HTMLInputElement>();
-const websocketInstant = ref<WebSocketClass>();
-const localVideoRef = ref<HTMLVideoElement>();
-const localStream = ref();
-const allMediaTypeList = {
-  [LiveTypeEnum.camera]: {
-    type: LiveTypeEnum.camera,
-    txt: '摄像头',
-  },
-  [LiveTypeEnum.screen]: {
-    type: LiveTypeEnum.screen,
-    txt: '窗口',
-  },
-};
-const currMediaTypeList = ref<
-  {
-    type: LiveTypeEnum;
-    txt: string;
-  }[]
->([]);
-const currMediaType = ref<{
-  type: LiveTypeEnum;
-  txt: string;
-}>();
-const damuList = ref<
-  {
-    socketId: string;
-    msgType: number;
-    msg: string;
-  }[]
->([]);
-
-const liveUserList = ref<
-  {
-    socketId: string;
-    avatar: string;
-    expr: number;
-  }[]
->([]);
-
-function closeWs() {
-  const instance = networkStore.wsMap.get(roomId.value);
-  if (!instance) return;
-  instance.close();
-}
-
-function closeRtc() {
-  networkStore.rtcMap.forEach((rtc) => {
-    rtc.close();
-  });
-}
-
-function sendDanmu() {
-  if (!danmuStr.value.length) {
-    alert('请输入弹幕内容!');
-  }
-  if (!websocketInstant.value) return;
-  websocketInstant.value.send({
-    msgType: WsMsgTypeEnum.message,
-    data: { msg: danmuStr.value },
-  });
-  damuList.value.push({
-    socketId: getSocketId(),
-    msgType: 1,
-    msg: danmuStr.value,
-  });
-  danmuStr.value = '';
-}
-
-onUnmounted(() => {
-  closeWs();
-  closeRtc();
-});
-
-onMounted(() => {
-  if (topRef.value && bottomRef.value && localVideoRef.value) {
-    const res =
-      bottomRef.value.getBoundingClientRect().top -
-      topRef.value.getBoundingClientRect().top;
-    localVideoRef.value.style.height = `100px`;
-    localVideoRef.value.style.height = `${res}px`;
-  }
-  localVideoRef.value?.addEventListener('loadstart', () => {
-    console.warn('视频流-loadstart');
-    const rtc = networkStore.getRtcMap(roomId.value);
-    if (!rtc) return;
-    rtc.rtcStatus.loadstart = true;
-    rtc.update();
-  });
-
-  localVideoRef.value?.addEventListener('loadedmetadata', () => {
-    console.warn('视频流-loadedmetadata');
-    const rtc = networkStore.getRtcMap(roomId.value);
-    if (!rtc) return;
-    rtc.rtcStatus.loadedmetadata = true;
-    rtc.update();
-  });
-});
-
-function getSocketId() {
-  return networkStore.wsMap.get(roomId.value!)?.socketIo?.id || '-1';
-}
-
-function initReceive() {
-  const instance = websocketInstant.value;
-  if (!instance?.socketIo) return;
-  // websocket连接成功
-  instance.socketIo.on(WsConnectStatusEnum.connect, () => {
-    console.log('【websocket】websocket连接成功', instance.socketIo?.id);
-    if (!instance) return;
-    instance.status = WsConnectStatusEnum.connect;
-    instance.update();
-    sendJoin();
-  });
-
-  // websocket连接断开
-  instance.socketIo.on(WsConnectStatusEnum.disconnect, () => {
-    console.log('【websocket】websocket连接断开', instance);
-    if (!instance) return;
-    instance.status = WsConnectStatusEnum.disconnect;
-    instance.update();
-  });
-
-  // 当前所有在线用户
-  instance.socketIo.on(WsMsgTypeEnum.roomLiveing, (data: IAdminIn) => {
-    console.log('【websocket】收到管理员正在直播', data);
-  });
-
-  // 当前所有在线用户
-  instance.socketIo.on(WsMsgTypeEnum.liveUser, () => {
-    console.log('【websocket】当前所有在线用户');
-    if (!instance) return;
-  });
-
-  // 收到用户发送消息
-  instance.socketIo.on(WsMsgTypeEnum.message, (data) => {
-    console.log('【websocket】收到用户发送消息', data);
-    if (!instance) return;
-    damuList.value.push({
-      socketId: data.socketId,
-      msgType: 1,
-      msg: data.data.msg,
-    });
-  });
-
-  // 用户加入房间完成
-  instance.socketIo.on(WsMsgTypeEnum.joined, (data) => {
-    console.log('【websocket】用户加入房间完成', data);
-    liveUserList.value.push({
-      avatar: 'red',
-      socketId: `${getSocketId()}`,
-      expr: 1,
-    });
-    handleSrsPush();
-  });
-
-  // 其他用户加入房间
-  instance.socketIo.on(WsMsgTypeEnum.otherJoin, (data) => {
-    console.log('【websocket】其他用户加入房间', data);
-    liveUserList.value.push({
-      avatar: 'red',
-      socketId: data.socketId,
-      expr: 1,
-    });
-    console.log('当前所有在线用户', JSON.stringify(liveUserList.value));
-  });
-
-  // 用户离开房间
-  instance.socketIo.on(WsMsgTypeEnum.leave, (data) => {
-    console.log('【websocket】用户离开房间', data);
-    if (!instance) return;
-    instance.socketIo?.emit(WsMsgTypeEnum.leave, {
-      roomId: instance.roomId,
-    });
-  });
-
-  // 用户离开房间完成
-  instance.socketIo.on(WsMsgTypeEnum.leaved, (data) => {
-    console.log('【websocket】用户离开房间完成', data);
-    if (!instance) return;
-    const res = liveUserList.value.filter(
-      (item) => item.socketId !== data.socketId
-    );
-    console.log('当前所有在线用户', JSON.stringify(res));
-    liveUserList.value = res;
-  });
-}
-
-function roomNameIsOk() {
-  if (!roomName.value.length) {
-    alert('请输入房间名!');
-    return false;
-  }
-  if (roomName.value.length < 3 || roomName.value.length > 10) {
-    alert('房间名要求3-10个字符!');
-    return false;
-  }
-  return true;
-}
-
-function confirmRoomName() {
-  if (!roomNameIsOk()) return;
-  if (!roomNameRef.value) return;
-  roomNameRef.value.disabled = true;
-}
-
-function endLive() {
-  console.log('endLive');
-  closeRtc();
-  currMediaTypeList.value = [];
-  localStream.value = null;
-  if (localVideoRef.value) {
-    localVideoRef.value.srcObject = null;
-  }
-  if (!websocketInstant.value) return;
-  websocketInstant.value.close();
-}
-
-function sendJoin() {
-  const instance = networkStore.wsMap.get(roomId.value);
-  if (!instance) return;
-  instance.send({
-    msgType: WsMsgTypeEnum.join,
-    data: {
-      roomName: roomName.value,
-      srs: {
-        streamurl: streamurl.value,
-      },
-    },
-  });
-}
-
-function startLive() {
-  if (!roomNameIsOk()) return;
-  if (currMediaTypeList.value.length <= 0) {
-    alert('请选择一个素材!');
-    return;
-  }
-  websocketInstant.value = new WebSocketClass({
-    roomId: roomId.value,
-    url:
-      process.env.NODE_ENV === 'development'
-        ? 'ws://localhost:4300'
-        : 'wss://live.hsslive.cn',
-    isAdmin: true,
-  });
-  websocketInstant.value.update();
-  initReceive();
-}
-
-async function handleSrsPush() {
-  const rtc = new SRSWebRTCClass({
-    roomId: `${roomId.value}___${getSocketId()}`,
-  });
-  if (!rtc) return;
-  localStream.value.getTracks().forEach((track) => {
-    rtc.addTrack({ track, stream: localStream.value, direction: 'sendonly' });
-  });
-  try {
-    const offer = await rtc.createOffer();
-    if (!offer) return;
-    await rtc.setLocalDescription(offer);
-    const res: any = await fetchRtcV1Publish({
-      api: 'http://localhost:1985/rtc/v1/publish/',
-      clientip: null,
-      sdp: offer.sdp!,
-      streamurl: streamurl.value,
-      tid: getRandomString(10),
-    });
-    await rtc.setRemoteDescription(
-      new RTCSessionDescription({ type: 'answer', sdp: res.sdp })
-    );
-  } catch (error) {
-    console.log(error);
-  }
-}
-
-/** 摄像头 */
-async function startMediaDevices() {
-  if (!localStream.value) {
-    // WARN navigator.mediaDevices在localhost和https才能用,http://192.168.1.103:8000局域网用不了
-    const event = await navigator.mediaDevices.getUserMedia({
-      video: true,
-      audio: true,
-    });
-    console.log('getUserMedia成功', event);
-    currMediaType.value = allMediaTypeList[LiveTypeEnum.camera];
-    currMediaTypeList.value.push(allMediaTypeList[LiveTypeEnum.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: true,
-      audio: true,
-    });
-    console.log('getDisplayMedia成功', event);
-    currMediaType.value = allMediaTypeList[LiveTypeEnum.screen];
-    currMediaTypeList.value.push(allMediaTypeList[LiveTypeEnum.screen]);
-    if (!localVideoRef.value) return;
-    localVideoRef.value.srcObject = event;
-    localStream.value = event;
-  }
-}
-
-function leave() {
-  if (joinRef.value && leaveRef.value && roomIdRef.value) {
-    roomIdRef.value.disabled = false;
-    joinRef.value.disabled = false;
-    leaveRef.value.disabled = true;
-  }
-  if (!websocketInstant.value) return;
-  websocketInstant.value.socketIo?.emit(WsMsgTypeEnum.leave, {
-    roomId: websocketInstant.value.roomId,
-  });
-}
-</script>
-
-<style lang="scss" scoped>
-.push-wrap {
-  margin: 20px auto 0;
-  min-width: $large-width;
-  height: 700px;
-  text-align: center;
-  .left {
-    position: relative;
-    display: inline-block;
-    box-sizing: border-box;
-    width: $large-left-width;
-    height: 100%;
-    border-radius: 10px;
-    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%;
-        display: flex;
-        align-items: center;
-        justify-content: space-around;
-        width: 200px;
-        height: 50px;
-        background-color: #fff;
-        transform: translate(-50%, -50%);
-        .item {
-          width: 60px;
-          height: 30px;
-          border-radius: 4px;
-          background-color: rebeccapurple;
-          color: white;
-          font-size: 14px;
-          line-height: 30px;
-          cursor: pointer;
-        }
-      }
-    }
-    .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-color: yellow;
-        }
-        .detail {
-          display: flex;
-          flex-direction: column;
-          text-align: initial;
-          .top {
-            margin-bottom: 10px;
-            color: #18191c;
-            .btn {
-              margin-left: 10px;
-            }
-          }
-          .bottom {
-            font-size: 14px;
-          }
-        }
-      }
-      .other {
-        display: flex;
-        flex-direction: column;
-        justify-content: center;
-        font-size: 12px;
-        .top {
-          display: flex;
-          align-items: center;
-          .item {
-            display: flex;
-            align-items: center;
-            margin-right: 20px;
-            .ico {
-              display: inline-block;
-              margin-right: 4px;
-              width: 10px;
-              height: 10px;
-              border-radius: 50%;
-              background-color: skyblue;
-            }
-          }
-        }
-        .bottom {
-          margin-top: 10px;
-        }
-      }
-    }
-  }
-  .right {
-    position: relative;
-    display: inline-block;
-    box-sizing: border-box;
-    margin-left: 10px;
-    width: 240px;
-    height: 100%;
-    border-radius: 10px;
-    background-color: white;
-    color: #9499a0;
-
-    .resource-card {
-      margin-bottom: 5%;
-      margin-bottom: 10px;
-      width: 100%;
-      height: 290px;
-      border-radius: 4px;
-      background-color: papayawhip;
-      .title {
-        padding: 10px;
-        text-align: initial;
-      }
-      .item {
-        display: flex;
-        align-items: center;
-        justify-content: space-between;
-        margin-bottom: 10px;
-        font-size: 12px;
-      }
-    }
-    .danmu-card {
-      box-sizing: border-box;
-      padding: 10px;
-      width: 100%;
-      height: 400px;
-      border-radius: 4px;
-      background-color: papayawhip;
-      text-align: initial;
-      .title {
-        margin-bottom: 10px;
-      }
-      .list {
-        margin-bottom: 10px;
-        height: 300px;
-        .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;
-        }
-        .btn {
-          box-sizing: border-box;
-          width: 80px;
-          height: 30px;
-          border-radius: 4px;
-          background-color: #23ade5;
-          color: white;
-          text-align: center;
-          font-size: 12px;
-          line-height: 30px;
-          cursor: pointer;
-        }
-      }
-    }
-  }
-}
-// 屏幕宽度小于$large-width的时候
-@media screen and (max-width: $large-width) {
-  .push-wrap {
-    .left {
-      width: $medium-left-width;
-    }
-    .right {
-      .list {
-        .item {
-        }
-      }
-    }
-  }
-}
-</style>

+ 731 - 4
src/views/srs-webrtc-push/index.vue

@@ -1,9 +1,736 @@
 <template>
-  <div style="text-align: center; margin-top: 100px; font-size: 30px">
-    正在开发:50%
+  <div class="srs-webrtc-push-wrap">
+    <div
+      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
+          controls
+        ></video>
+        <div
+          v-if="!currMediaTypeList || currMediaTypeList.length <= 0"
+          class="add-wrap"
+        >
+          <div
+            class="item"
+            @click="startMediaDevices"
+          >
+            摄像头
+          </div>
+          <div
+            class="item"
+            @click="startGetDisplayMedia"
+          >
+            窗口
+          </div>
+        </div>
+      </div>
+      <div
+        ref="bottomRef"
+        class="control"
+      >
+        <div class="info">
+          <div class="avatar"></div>
+          <div class="detail">
+            <div class="top">
+              <input
+                ref="roomNameRef"
+                v-model="roomName"
+                type="text"
+                placeholder="输入房间名"
+              />
+              <button
+                ref="roomNameBtnRef"
+                class="btn"
+                @click="confirmRoomName"
+              >
+                确定
+              </button>
+            </div>
+            <div class="bottom">
+              <span>socketId:{{ getSocketId() }}</span>
+            </div>
+          </div>
+        </div>
+        <div class="other">
+          <div class="top">
+            <span class="item">
+              <i class="ico"></i>
+              <span>正在观看人数:{{ liveUserList.length }}</span>
+            </span>
+          </div>
+          <div class="bottom">
+            <button @click="startLive">开始直播</button>
+            <button @click="endLive">结束直播</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 currMediaTypeList"
+            :key="index"
+            class="item"
+          >
+            <span class="name">{{ item.txt }}</span>
+          </div>
+        </div>
+      </div>
+      <div class="danmu-card">
+        <div class="title">弹幕互动</div>
+        <div class="list-wrap">
+          <div class="list">
+            <div
+              v-for="(item, index) in damuList"
+              :key="index"
+              class="item"
+            >
+              <template v-if="item.msgType === DanmuMsgTypeEnum.danmu">
+                <span class="name">{{ item.socketId }}:</span>
+                <span class="msg">{{ item.msg }}</span>
+              </template>
+              <template v-else-if="item.msgType === DanmuMsgTypeEnum.otherJoin">
+                <span class="name system">系统通知:</span>
+                <span class="msg">{{ item.socketId }}进入直播!</span>
+              </template>
+              <template
+                v-else-if="item.msgType === DanmuMsgTypeEnum.userLeaved"
+              >
+                <span class="name system">系统通知:</span>
+                <span class="msg">{{ item.socketId }}离开直播!</span>
+              </template>
+            </div>
+          </div>
+        </div>
+        <div class="send-msg">
+          <input
+            v-model="danmuStr"
+            class="ipt"
+          />
+          <div
+            class="btn"
+            @click="sendDanmu"
+          >
+            发送
+          </div>
+        </div>
+      </div>
+    </div>
   </div>
 </template>
 
-<script lang="ts" setup></script>
+<script lang="ts" setup>
+import { getRandomString } from 'billd-utils';
+import { onMounted, onUnmounted, reactive, ref } from 'vue';
 
-<style lang="scss" scoped></style>
+import { fetchRtcV1Publish } from '@/api/srs';
+import {
+  DanmuMsgTypeEnum,
+  IAdminIn,
+  IDanmu,
+  ILiveUser,
+  LiveTypeEnum,
+} from '@/interface';
+import { SRSWebRTCClass } from '@/network/srsWebRtc';
+import {
+  WebSocketClass,
+  WsConnectStatusEnum,
+  WsMsgTypeEnum,
+} from '@/network/webSocket';
+import router from '@/router';
+import { useNetworkStore } from '@/store/network';
+
+const networkStore = useNetworkStore();
+
+const topRef = ref<HTMLDivElement>();
+const bottomRef = ref<HTMLDivElement>();
+const roomNameRef = ref<HTMLInputElement>();
+const roomNameBtnRef = ref<HTMLButtonElement>();
+const localVideoRef = ref<HTMLVideoElement>();
+
+const roomId = ref<string>(getRandomString(15));
+const danmuStr = ref('');
+const roomName = ref('');
+const localStream = ref();
+const trackInfo = reactive({
+  audio: true,
+  video: true,
+});
+// const streamurl = ref(`webrtc://localhost/live/livestream/${roomId.value}`);
+const streamurl = ref(
+  `webrtc://${
+    process.env.NODE_ENV === 'development'
+      ? 'localhost'
+      : 'live.hsslive.cn:5001'
+  }/live/livestream/${roomId.value}`
+);
+
+const websocketInstant = ref<WebSocketClass>();
+const damuList = ref<IDanmu[]>([]);
+const liveUserList = ref<ILiveUser[]>([]);
+
+const allMediaTypeList = {
+  [LiveTypeEnum.camera]: {
+    type: LiveTypeEnum.camera,
+    txt: '摄像头',
+  },
+  [LiveTypeEnum.screen]: {
+    type: LiveTypeEnum.screen,
+    txt: '窗口',
+  },
+};
+const currMediaType = ref<{
+  type: LiveTypeEnum;
+  txt: string;
+}>();
+const currMediaTypeList = ref<
+  {
+    type: LiveTypeEnum;
+    txt: string;
+  }[]
+>([]);
+
+function handleCoverImg() {
+  const canvas = document.createElement('canvas');
+  const { width, height } = localVideoRef.value!.getBoundingClientRect();
+  const rate = width / height;
+  const coverWidth = width * 0.5;
+  const coverHeight = coverWidth / rate;
+  canvas.width = coverWidth;
+  canvas.height = coverHeight;
+  canvas
+    .getContext('2d')!
+    .drawImage(localVideoRef.value!, 0, 0, coverWidth, coverHeight);
+  // webp比png的体积小非常多!因此coverWidth就可以不用压缩太夸张
+  const dataURL = canvas.toDataURL('image/webp');
+  return dataURL;
+}
+
+function closeWs() {
+  const instance = networkStore.wsMap.get(roomId.value);
+  if (!instance) return;
+  instance.close();
+}
+
+function closeRtc() {
+  networkStore.rtcMap.forEach((rtc) => {
+    rtc.close();
+  });
+}
+
+function sendDanmu() {
+  if (!danmuStr.value.length) {
+    alert('请输入弹幕内容!');
+  }
+  if (!websocketInstant.value) return;
+  websocketInstant.value.send({
+    msgType: WsMsgTypeEnum.message,
+    data: { msg: danmuStr.value },
+  });
+  damuList.value.push({
+    socketId: getSocketId(),
+    msgType: DanmuMsgTypeEnum.danmu,
+    msg: danmuStr.value,
+  });
+  danmuStr.value = '';
+}
+
+onUnmounted(() => {
+  closeWs();
+  closeRtc();
+});
+
+onMounted(() => {
+  router.push({ query: { roomId: roomId.value } });
+  if (topRef.value && bottomRef.value && localVideoRef.value) {
+    const res =
+      bottomRef.value.getBoundingClientRect().top -
+      topRef.value.getBoundingClientRect().top;
+    localVideoRef.value.style.height = `100px`;
+    localVideoRef.value.style.height = `${res}px`;
+  }
+  localVideoRef.value?.addEventListener('loadstart', () => {
+    console.warn('视频流-loadstart');
+    const rtc = networkStore.getRtcMap(roomId.value);
+    if (!rtc) return;
+    rtc.rtcStatus.loadstart = true;
+    rtc.update();
+  });
+
+  localVideoRef.value?.addEventListener('loadedmetadata', () => {
+    console.warn('视频流-loadedmetadata');
+    const rtc = networkStore.getRtcMap(roomId.value);
+    if (!rtc) return;
+    rtc.rtcStatus.loadedmetadata = true;
+    rtc.update();
+  });
+});
+
+function getSocketId() {
+  return networkStore.wsMap.get(roomId.value!)?.socketIo?.id || '-1';
+}
+
+function initReceive() {
+  const instance = websocketInstant.value;
+  if (!instance?.socketIo) return;
+  // websocket连接成功
+  instance.socketIo.on(WsConnectStatusEnum.connect, () => {
+    console.log('【websocket】websocket连接成功', instance.socketIo?.id);
+    if (!instance) return;
+    instance.status = WsConnectStatusEnum.connect;
+    instance.update();
+    sendJoin();
+  });
+
+  // websocket连接断开
+  instance.socketIo.on(WsConnectStatusEnum.disconnect, () => {
+    console.log('【websocket】websocket连接断开', instance);
+    if (!instance) return;
+    instance.status = WsConnectStatusEnum.disconnect;
+    instance.update();
+  });
+
+  // 当前所有在线用户
+  instance.socketIo.on(WsMsgTypeEnum.roomLiveing, (data: IAdminIn) => {
+    console.log('【websocket】收到管理员正在直播', data);
+  });
+
+  // 当前所有在线用户
+  instance.socketIo.on(WsMsgTypeEnum.liveUser, () => {
+    console.log('【websocket】当前所有在线用户');
+    if (!instance) return;
+  });
+
+  // 收到用户发送消息
+  instance.socketIo.on(WsMsgTypeEnum.message, (data) => {
+    console.log('【websocket】收到用户发送消息', data);
+    if (!instance) return;
+    damuList.value.push({
+      socketId: data.socketId,
+      msgType: DanmuMsgTypeEnum.danmu,
+      msg: data.data.msg,
+    });
+  });
+
+  // 用户加入房间完成
+  instance.socketIo.on(WsMsgTypeEnum.joined, (data) => {
+    console.log('【websocket】用户加入房间完成', data);
+    liveUserList.value.push({
+      avatar: 'red',
+      socketId: `${getSocketId()}`,
+      expr: 1,
+    });
+    handleSrsPush();
+  });
+
+  // 其他用户加入房间
+  instance.socketIo.on(WsMsgTypeEnum.otherJoin, (data) => {
+    console.log('【websocket】其他用户加入房间', data);
+    liveUserList.value.push({
+      avatar: 'red',
+      socketId: data.socketId,
+      expr: 1,
+    });
+    damuList.value.push({
+      socketId: data.socketId,
+      msgType: DanmuMsgTypeEnum.otherJoin,
+      msg: '',
+    });
+  });
+
+  // 用户离开房间
+  instance.socketIo.on(WsMsgTypeEnum.leave, (data) => {
+    console.log('【websocket】用户离开房间', data);
+    if (!instance) return;
+    instance.socketIo?.emit(WsMsgTypeEnum.leave, {
+      roomId: instance.roomId,
+    });
+  });
+
+  // 用户离开房间完成
+  instance.socketIo.on(WsMsgTypeEnum.leaved, (data) => {
+    console.log('【websocket】用户离开房间完成', data);
+    const res = liveUserList.value.filter(
+      (item) => item.socketId !== data.socketId
+    );
+    liveUserList.value = res;
+    damuList.value.push({
+      socketId: data.socketId,
+      msgType: DanmuMsgTypeEnum.userLeaved,
+      msg: '',
+    });
+  });
+}
+
+function roomNameIsOk() {
+  if (!roomName.value.length) {
+    alert('请输入房间名!');
+    return false;
+  }
+  if (roomName.value.length < 3 || roomName.value.length > 10) {
+    alert('房间名要求3-10个字符!');
+    return false;
+  }
+  return true;
+}
+
+function confirmRoomName() {
+  if (!roomNameIsOk()) return;
+  if (!roomNameRef.value) return;
+  roomNameRef.value.disabled = true;
+}
+
+/** 结束直播 */
+function endLive() {
+  roomNameBtnRef.value!.disabled = false;
+  closeRtc();
+  currMediaTypeList.value = [];
+  localStream.value = null;
+  localVideoRef.value!.srcObject = null;
+  const instance = networkStore.wsMap.get(roomId.value);
+  if (!instance) return;
+  instance.send({
+    msgType: WsMsgTypeEnum.roomNoLive,
+    data: {},
+  });
+  setTimeout(() => {
+    instance.close();
+  }, 500);
+}
+
+function sendJoin() {
+  const instance = networkStore.wsMap.get(roomId.value);
+  if (!instance) return;
+  instance.send({
+    msgType: WsMsgTypeEnum.join,
+    data: {
+      roomName: roomName.value,
+      coverImg: handleCoverImg(),
+      srs: {
+        streamurl: streamurl.value,
+      },
+      trackInfo,
+    },
+  });
+}
+
+function startLive() {
+  if (!roomNameIsOk()) return;
+  if (currMediaTypeList.value.length <= 0) {
+    alert('请选择一个素材!');
+    return;
+  }
+  roomNameBtnRef.value!.disabled = true;
+  websocketInstant.value = new WebSocketClass({
+    roomId: roomId.value,
+    url:
+      process.env.NODE_ENV === 'development'
+        ? 'ws://localhost:4300'
+        : 'wss://live.hsslive.cn',
+    isAdmin: true,
+  });
+  websocketInstant.value.update();
+  initReceive();
+}
+
+async function handleSrsPush() {
+  const rtc = new SRSWebRTCClass({
+    roomId: `${roomId.value}___${getSocketId()}`,
+  });
+  localStream.value.getTracks().forEach((track) => {
+    rtc.addTrack({ track, stream: localStream.value, direction: 'sendonly' });
+  });
+  try {
+    const offer = await rtc.createOffer();
+    if (!offer) return;
+    await rtc.setLocalDescription(offer);
+    const res: any = await fetchRtcV1Publish({
+      api: `http://${
+        process.env.NODE_ENV === 'development'
+          ? 'localhost:1985'
+          : 'live.hsslive.cn:1985'
+      }/rtc/v1/publish/`,
+      clientip: null,
+      sdp: offer.sdp!,
+      streamurl: streamurl.value,
+      tid: getRandomString(10),
+    });
+    await rtc.setRemoteDescription(
+      new RTCSessionDescription({ type: 'answer', sdp: res.sdp })
+    );
+  } catch (error) {
+    console.log(error);
+  }
+}
+
+/** 摄像头 */
+async function startMediaDevices() {
+  if (!localStream.value) {
+    // WARN navigator.mediaDevices在localhost和https才能用,http://192.168.1.103:8000局域网用不了
+    const event = await navigator.mediaDevices.getUserMedia({
+      video: true,
+      audio: true,
+    });
+    console.log('getUserMedia成功', event);
+    currMediaType.value = allMediaTypeList[LiveTypeEnum.camera];
+    currMediaTypeList.value.push(allMediaTypeList[LiveTypeEnum.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: true,
+      audio: true,
+    });
+    const audio = event.getAudioTracks();
+    const video = event.getVideoTracks();
+    trackInfo.audio = !!audio.length;
+    trackInfo.video = !!video.length;
+    console.log('getDisplayMedia成功', event);
+    currMediaType.value = allMediaTypeList[LiveTypeEnum.screen];
+    currMediaTypeList.value.push(allMediaTypeList[LiveTypeEnum.screen]);
+    if (!localVideoRef.value) return;
+    localVideoRef.value.srcObject = event;
+    localStream.value = event;
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.srs-webrtc-push-wrap {
+  margin: 20px auto 0;
+  min-width: $large-width;
+  height: 700px;
+  text-align: center;
+  .left {
+    position: relative;
+    display: inline-block;
+    box-sizing: border-box;
+    width: $large-left-width;
+    height: 100%;
+    border-radius: 10px;
+    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%;
+        display: flex;
+        align-items: center;
+        justify-content: space-around;
+        width: 200px;
+        height: 50px;
+        background-color: #fff;
+        transform: translate(-50%, -50%);
+        .item {
+          width: 60px;
+          height: 30px;
+          border-radius: 4px;
+          background-color: skyblue;
+          color: white;
+          font-size: 14px;
+          line-height: 30px;
+          cursor: pointer;
+        }
+      }
+    }
+    .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-color: skyblue;
+        }
+        .detail {
+          display: flex;
+          flex-direction: column;
+          text-align: initial;
+          .top {
+            margin-bottom: 10px;
+            color: #18191c;
+            .btn {
+              margin-left: 10px;
+            }
+          }
+          .bottom {
+            font-size: 14px;
+          }
+        }
+      }
+      .other {
+        display: flex;
+        flex-direction: column;
+        justify-content: center;
+        font-size: 12px;
+        .top {
+          display: flex;
+          align-items: center;
+          .item {
+            display: flex;
+            align-items: center;
+            margin-right: 20px;
+            .ico {
+              display: inline-block;
+              margin-right: 4px;
+              width: 10px;
+              height: 10px;
+              border-radius: 50%;
+              background-color: skyblue;
+            }
+          }
+        }
+        .bottom {
+          margin-top: 10px;
+        }
+      }
+    }
+  }
+  .right {
+    position: relative;
+    display: inline-block;
+    box-sizing: border-box;
+    margin-left: 10px;
+    width: 240px;
+    height: 100%;
+    border-radius: 10px;
+    background-color: white;
+    color: #9499a0;
+
+    .resource-card {
+      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;
+      }
+    }
+    .danmu-card {
+      box-sizing: border-box;
+      padding: 10px;
+      width: 100%;
+      height: 400px;
+      border-radius: 4px;
+      background-color: papayawhip;
+      text-align: initial;
+      .title {
+        margin-bottom: 10px;
+      }
+      .list {
+        margin-bottom: 10px;
+        height: 300px;
+        .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;
+        }
+        .btn {
+          box-sizing: border-box;
+          width: 80px;
+          height: 30px;
+          border-radius: 4px;
+          background-color: skyblue;
+          color: white;
+          text-align: center;
+          font-size: 12px;
+          line-height: 30px;
+          cursor: pointer;
+        }
+      }
+    }
+  }
+}
+// 屏幕宽度小于$large-width的时候
+@media screen and (max-width: $large-width) {
+  .srs-webrtc-push-wrap {
+    .left {
+      width: $medium-left-width;
+    }
+    .right {
+      .list {
+        .item {
+        }
+      }
+    }
+  }
+}
+</style>

+ 80 - 153
src/views/webrtc-pull/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="pull-wrap">
+  <div class="webrtc-pull-wrap">
     <template v-if="roomNoLive">当前房间没在直播~</template>
     <template v-else>
       <div class="left">
@@ -10,7 +10,7 @@
           <div class="info">
             <div class="avatar"></div>
             <div class="detail">
-              <div class="top">房间号:{{ route.params.roomId }}</div>
+              <div class="top">房间名:{{ roomName }}</div>
               <div class="bottom">
                 <span>你的socketId:{{ getSocketId() }}</span>
               </div>
@@ -107,7 +107,14 @@
 import { onMounted, onUnmounted, ref } from 'vue';
 import { useRoute } from 'vue-router';
 
-import { DanmuMsgTypeEnum, IAdminIn, ICandidate, IOffer } from '@/interface';
+import {
+  DanmuMsgTypeEnum,
+  IAdminIn,
+  ICandidate,
+  IDanmu,
+  ILiveUser,
+  IOffer,
+} from '@/interface';
 import { WebRTCClass } from '@/network/webRtc';
 import {
   WebSocketClass,
@@ -118,22 +125,17 @@ import { useNetworkStore } from '@/store/network';
 
 const networkStore = useNetworkStore();
 const route = useRoute();
-const danmuStr = ref('');
+
 const topRef = ref<HTMLDivElement>();
 const bottomRef = ref<HTMLDivElement>();
+const localVideoRef = ref<HTMLVideoElement>();
 const roomNoLive = ref(false);
-const isAddTrack = ref(false);
-const roomIdRef = ref<HTMLInputElement>();
-const joinRef = ref<HTMLButtonElement>();
-const leaveRef = ref<HTMLButtonElement>();
 const roomId = ref('');
+const roomName = ref('');
+const danmuStr = ref('');
 const websocketInstant = ref<WebSocketClass>();
-const isDone = ref(false);
-const localVideoRef = ref<HTMLVideoElement>();
-const localStream = ref();
-const joined = ref(false);
-const offerSended = ref(new Set());
-
+const damuList = ref<IDanmu[]>([]);
+const liveUserList = ref<ILiveUser[]>([]);
 const giftList = ref([
   { name: '鲜花', ico: '', price: '免费' },
   { name: '肥宅水', ico: '', price: '2元' },
@@ -142,28 +144,28 @@ const giftList = ref([
   { name: '一杯咖啡', ico: '', price: '10元' },
 ]);
 
-const damuList = ref<
-  {
-    socketId: string;
-    msgType: number;
-    msg: string;
-  }[]
->([]);
-
-const liveUserList = ref<
-  {
-    socketId: string;
-    avatar: string;
-    expr: number;
-  }[]
->([]);
-
 function closeWs() {
   const instance = networkStore.wsMap.get(roomId.value);
   if (!instance) return;
   instance.close();
 }
 
+function closeRtc() {
+  networkStore.rtcMap.forEach((rtc) => {
+    rtc.close();
+  });
+}
+
+function getSocketId() {
+  return networkStore.wsMap.get(roomId.value!)?.socketIo?.id || '-1';
+}
+
+function sendJoin() {
+  const instance = networkStore.wsMap.get(roomId.value);
+  if (!instance) return;
+  instance.send({ msgType: WsMsgTypeEnum.join, data: {} });
+}
+
 function sendDanmu() {
   if (!danmuStr.value.length) {
     alert('请输入弹幕内容!');
@@ -181,83 +183,12 @@ function sendDanmu() {
   danmuStr.value = '';
 }
 
-onUnmounted(() => {
-  closeWs();
-});
-
-onMounted(() => {
-  if (topRef.value && bottomRef.value && localVideoRef.value) {
-    const res =
-      bottomRef.value.getBoundingClientRect().top -
-      (topRef.value.getBoundingClientRect().top +
-        topRef.value.getBoundingClientRect().height);
-    localVideoRef.value.style.height = `${res}px`;
-  }
-  roomId.value = route.params.roomId as string;
-  console.warn('开始new WebSocketClass');
-  websocketInstant.value = new WebSocketClass({
-    roomId: roomId.value,
-    url:
-      process.env.NODE_ENV === 'development'
-        ? 'ws://localhost:4300'
-        : 'wss://live.hsslive.cn',
-    isAdmin: false,
-  });
-  websocketInstant.value.update();
-  initReceive();
-  sendJoin();
-
-  localVideoRef.value?.addEventListener('loadstart', () => {
-    console.warn('视频流-loadstart');
-    const rtc = networkStore.getRtcMap(roomId.value);
-    if (!rtc) return;
-    rtc.rtcStatus.loadstart = true;
-    rtc.update();
-  });
-
-  localVideoRef.value?.addEventListener('loadedmetadata', () => {
-    console.warn('视频流-loadedmetadata');
-    const rtc = networkStore.getRtcMap(roomId.value);
-    if (!rtc) return;
-    rtc.rtcStatus.loadedmetadata = true;
-    rtc.update();
-    batchSendOffer();
-  });
-});
-
-function getSocketId() {
-  return networkStore.wsMap.get(roomId.value!)?.socketIo?.id || '-1';
-}
-
-function sendJoin() {
-  const instance = networkStore.wsMap.get(roomId.value);
-  if (!instance) return;
-  instance.send({ msgType: WsMsgTypeEnum.join, data: {} });
-}
-
-function batchSendOffer() {
-  liveUserList.value.forEach(async (item) => {
-    if (
-      !offerSended.value.has(item.socketId) &&
-      item.socketId !== getSocketId()
-    ) {
-      await startNewWebRtc(item.socketId);
-      await addTrack();
-      console.warn('new WebRTCClass完成');
-      console.log('执行sendOffer', {
-        sender: getSocketId(),
-        receiver: item.socketId,
-      });
-      sendOffer({ sender: getSocketId(), receiver: item.socketId });
-      offerSended.value.add(item.socketId);
-    }
-  });
-}
-
-function closeRtc() {
-  networkStore.rtcMap.forEach((rtc) => {
-    rtc.close();
-  });
+function startNewWebRtc(receiver: string) {
+  console.warn('开始new WebRTCClass', receiver);
+  const rtc = new WebRTCClass({ roomId: `${roomId.value}___${receiver}` });
+  rtc.rtcStatus.joined = true;
+  rtc.update();
+  return rtc;
 }
 
 function initReceive() {
@@ -324,7 +255,6 @@ function initReceive() {
   // 收到answer
   instance.socketIo.on(WsMsgTypeEnum.answer, async (data: IOffer) => {
     console.warn('【websocket】收到answer', data);
-    if (isDone.value) return;
     if (!instance) return;
     const rtc = networkStore.getRtcMap(`${roomId.value}___${data.socketId}`);
     console.log(rtc, '收到answer收到answer');
@@ -342,7 +272,6 @@ function initReceive() {
   // 收到candidate
   instance.socketIo.on(WsMsgTypeEnum.candidate, (data: ICandidate) => {
     console.warn('【websocket】收到candidate', data);
-    if (isDone.value) return;
     if (!instance) return;
     const rtc =
       networkStore.getRtcMap(`${roomId.value}___${data.socketId}`) ||
@@ -382,15 +311,12 @@ function initReceive() {
   // 用户加入房间完成
   instance.socketIo.on(WsMsgTypeEnum.joined, (data) => {
     console.log('【websocket】用户加入房间完成', data);
-    joined.value = true;
+    roomName.value = data.data.roomName;
   });
 
   // 其他用户加入房间
   instance.socketIo.on(WsMsgTypeEnum.otherJoin, (data) => {
     console.log('【websocket】其他用户加入房间', data);
-    if (joined.value) {
-      batchSendOffer();
-    }
     damuList.value.push({
       socketId: data.socketId,
       msgType: DanmuMsgTypeEnum.otherJoin,
@@ -415,7 +341,6 @@ function initReceive() {
       (item) => item.socketId !== data.socketId
     );
     liveUserList.value = res;
-    console.log('当前所有在线用户', JSON.stringify(res));
     damuList.value.push({
       socketId: data.socketId,
       msgType: DanmuMsgTypeEnum.userLeaved,
@@ -424,51 +349,52 @@ function initReceive() {
   });
 }
 
-function addTrack() {
-  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?.addTrack(track, localStream.value);
-      });
-    }
+onUnmounted(() => {
+  closeWs();
+});
+
+onMounted(() => {
+  if (topRef.value && bottomRef.value && localVideoRef.value) {
+    const res =
+      bottomRef.value.getBoundingClientRect().top -
+      (topRef.value.getBoundingClientRect().top +
+        topRef.value.getBoundingClientRect().height);
+    localVideoRef.value.style.height = `${res}px`;
+  }
+  roomId.value = route.params.roomId as string;
+  console.warn('开始new WebSocketClass');
+  websocketInstant.value = new WebSocketClass({
+    roomId: roomId.value,
+    url:
+      process.env.NODE_ENV === 'development'
+        ? 'ws://localhost:4300'
+        : 'wss://live.hsslive.cn',
+    isAdmin: false,
   });
-  isAddTrack.value = true;
-}
+  websocketInstant.value.update();
+  initReceive();
+  sendJoin();
 
-async function sendOffer({
-  sender,
-  receiver,
-}: {
-  sender: string;
-  receiver: string;
-}) {
-  if (isDone.value) return;
-  if (!websocketInstant.value) return;
-  const rtc = networkStore.getRtcMap(`${roomId.value}___${receiver}`);
-  if (!rtc) return;
-  const sdp = await rtc.createOffer();
-  await rtc.setLocalDescription(sdp);
-  websocketInstant.value.send({
-    msgType: WsMsgTypeEnum.offer,
-    data: { sdp, sender, receiver },
+  localVideoRef.value?.addEventListener('loadstart', () => {
+    console.warn('视频流-loadstart');
+    const rtc = networkStore.getRtcMap(roomId.value);
+    if (!rtc) return;
+    rtc.rtcStatus.loadstart = true;
+    rtc.update();
   });
-}
 
-function startNewWebRtc(receiver: string) {
-  console.warn('开始new WebRTCClass', receiver);
-  const rtc = new WebRTCClass({ roomId: `${roomId.value}___${receiver}` });
-  rtc.rtcStatus.joined = true;
-  rtc.update();
-  return rtc;
-}
+  localVideoRef.value?.addEventListener('loadedmetadata', () => {
+    console.warn('视频流-loadedmetadata');
+    const rtc = networkStore.getRtcMap(roomId.value);
+    if (!rtc) return;
+    rtc.rtcStatus.loadedmetadata = true;
+    rtc.update();
+  });
+});
 </script>
 
 <style lang="scss" scoped>
-.pull-wrap {
+.webrtc-pull-wrap {
   margin: 20px auto 0;
   min-width: $large-width;
   height: 700px;
@@ -671,6 +597,7 @@ function startNewWebRtc(receiver: string) {
         color: white;
         text-align: center;
         font-size: 12px;
+        cursor: pointer;
       }
     }
   }
@@ -678,7 +605,7 @@ function startNewWebRtc(receiver: string) {
 
 // 屏幕宽度小于$large-width的时候
 @media screen and (max-width: $large-width) {
-  .pull-wrap {
+  .webrtc-pull-wrap {
     .left {
       width: $medium-left-width;
     }

+ 27 - 61
src/views/webrtc-push/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="push-wrap">
+  <div class="webrtc-push-wrap">
     <div
       ref="topRef"
       class="left"
@@ -141,6 +141,8 @@ import {
   DanmuMsgTypeEnum,
   IAdminIn,
   ICandidate,
+  IDanmu,
+  ILiveUser,
   IOffer,
   LiveTypeEnum,
 } from '@/interface';
@@ -150,26 +152,29 @@ import {
   WsConnectStatusEnum,
   WsMsgTypeEnum,
 } from '@/network/webSocket';
+import router from '@/router';
 import { useNetworkStore } from '@/store/network';
 
 const networkStore = useNetworkStore();
 
 const topRef = ref<HTMLDivElement>();
 const bottomRef = ref<HTMLDivElement>();
-const roomIdRef = ref<HTMLInputElement>();
-const joinRef = ref<HTMLButtonElement>();
-const leaveRef = ref<HTMLButtonElement>();
-const defaultRoomId = getRandomString(15);
-const roomId = ref<string>(defaultRoomId);
-const danmuStr = ref('');
-const roomName = ref('');
 const roomNameRef = ref<HTMLInputElement>();
 const roomNameBtnRef = ref<HTMLButtonElement>();
-const websocketInstant = ref<WebSocketClass>();
-const isDone = ref(false);
 const localVideoRef = ref<HTMLVideoElement>();
+
+const roomId = ref<string>(getRandomString(15));
+const danmuStr = ref('');
+const roomName = ref('');
 const localStream = ref();
 
+const websocketInstant = ref<WebSocketClass>();
+const isDone = ref(false);
+const joined = ref(false);
+const offerSended = ref(new Set());
+const damuList = ref<IDanmu[]>([]);
+const liveUserList = ref<ILiveUser[]>([]);
+
 const allMediaTypeList = {
   [LiveTypeEnum.camera]: {
     type: LiveTypeEnum.camera,
@@ -191,25 +196,6 @@ const currMediaType = ref<{
   txt: string;
 }>();
 
-const joined = ref(false);
-const offerSended = ref(new Set());
-
-const damuList = ref<
-  {
-    socketId: string;
-    msgType: DanmuMsgTypeEnum;
-    msg: string;
-  }[]
->([]);
-
-const liveUserList = ref<
-  {
-    socketId: string;
-    avatar: string;
-    expr: number;
-  }[]
->([]);
-
 function closeWs() {
   const instance = networkStore.wsMap.get(roomId.value);
   if (!instance) return;
@@ -261,6 +247,7 @@ function handleCoverImg() {
 }
 
 onMounted(async () => {
+  router.push({ query: { roomId: roomId.value } });
   const all = await getAllMediaDevices();
   allMediaTypeList[LiveTypeEnum.camera] = {
     txt: all.find((item) => item.kind === 'videoinput')?.label || '摄像头',
@@ -412,7 +399,6 @@ function initReceive() {
         ?.addIceCandidate(candidate)
         .then(() => {
           console.log('candidate成功');
-          // rtc.handleStream();
         })
         .catch((err) => {
           console.error('candidate失败', err);
@@ -463,7 +449,6 @@ function initReceive() {
       msgType: DanmuMsgTypeEnum.otherJoin,
       msg: '',
     });
-    console.log('当前所有在线用户', JSON.stringify(liveUserList.value));
     if (joined.value) {
       batchSendOffer();
     }
@@ -481,7 +466,6 @@ function initReceive() {
   // 用户离开房间完成
   instance.socketIo.on(WsMsgTypeEnum.leaved, (data) => {
     console.log('【websocket】用户离开房间完成', data);
-    if (!instance) return;
     const res = liveUserList.value.filter(
       (item) => item.socketId !== data.socketId
     );
@@ -532,9 +516,6 @@ function startLive() {
   websocketInstant.value.update();
   initReceive();
   sendJoin();
-  setTimeout(() => {
-    handleCoverImg();
-  }, 0);
 }
 
 /** 结束直播 */
@@ -544,22 +525,19 @@ function endLive() {
   currMediaTypeList.value = [];
   localStream.value = null;
   localVideoRef.value!.srcObject = null;
-  if (websocketInstant.value) {
-    websocketInstant.value.send({
-      msgType: WsMsgTypeEnum.roomNoLive,
-    });
-  }
+  const instance = networkStore.wsMap.get(roomId.value);
+  if (!instance) return;
+  instance.close();
 }
 
 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'
-  );
-  console.log(audioInput, videoInput, res);
+  // const audioInput = res.filter(
+  //   (item) => item.kind === 'audioinput' && item.deviceId !== 'default'
+  // );
+  // const videoInput = res.filter(
+  //   (item) => item.kind === 'videoinput' && item.deviceId !== 'default'
+  // );
   return res;
 }
 
@@ -636,22 +614,10 @@ function startNewWebRtc(receiver: string) {
   rtc.update();
   return rtc;
 }
-
-function leave() {
-  if (joinRef.value && leaveRef.value && roomIdRef.value) {
-    roomIdRef.value.disabled = false;
-    joinRef.value.disabled = false;
-    leaveRef.value.disabled = true;
-  }
-  if (!websocketInstant.value) return;
-  websocketInstant.value.socketIo?.emit(WsMsgTypeEnum.leave, {
-    roomId: websocketInstant.value.roomId,
-  });
-}
 </script>
 
 <style lang="scss" scoped>
-.push-wrap {
+.webrtc-push-wrap {
   margin: 20px auto 0;
   min-width: $large-width;
   height: 700px;
@@ -863,7 +829,7 @@ function leave() {
 }
 // 屏幕宽度小于$large-width的时候
 @media screen and (max-width: $large-width) {
-  .push-wrap {
+  .webrtc-push-wrap {
     .left {
       width: $medium-left-width;
     }