فهرست منبع

feat: 阶段保存

shuisheng 2 سال پیش
والد
کامیت
0c85c90afe
12فایلهای تغییر یافته به همراه545 افزوده شده و 212 حذف شده
  1. 0 44
      src/App copy.vue
  2. 2 41
      src/App.vue
  3. 4 0
      src/interface.ts
  4. 24 3
      src/layout/head/index.vue
  5. 1 1
      src/main.ts
  6. 80 51
      src/network/webRtc.ts
  7. 48 41
      src/network/websocket.ts
  8. 7 0
      src/store/app/index.ts
  9. 1 0
      src/store/network/index.ts
  10. 1 1
      src/store/user/index.ts
  11. 27 26
      src/views/home/index copy.vue
  12. 350 4
      src/views/home/index.vue

+ 0 - 44
src/App copy.vue

@@ -1,44 +0,0 @@
-<template>
-  <div>
-    <video
-      id="videoElement"
-      ref="videoRef"
-      style="width: 100vw"
-      :muted="muted"
-      controls
-    ></video>
-
-    <div class="btn">btn</div>
-  </div>
-</template>
-
-<script lang="ts" setup>
-import { onMounted, ref } from 'vue';
-
-import { WebSocketClass, wsConnectStatus } from '@/network/webSocket';
-
-const muted = ref(true);
-const videoRef = ref<HTMLVideoElement>();
-
-onMounted(() => {
-  // if (flvJs.isSupported() && videoRef.value) {
-  //   const flvPlayer = flvJs.createPlayer({
-  //     type: 'flv',
-  //     url: 'http://localhost:8000/live/fddm_2.flv',
-  //   });
-  //   flvPlayer.attachMediaElement(videoRef.value);
-  //   flvPlayer.load();
-  //   try {
-  //     flvPlayer.play();
-  //   } catch (error) {
-  //     console.log(error);
-  //   }
-  // }
-  const instance = new WebSocketClass({ url: 'ws://localhost:4300' });
-  instance.wsInstance?.on(wsConnectStatus.connect, () => {
-    console.log('连接websocket成功!');
-  });
-});
-</script>
-
-<style lang="scss" scoped></style>

+ 2 - 41
src/App.vue

@@ -3,48 +3,9 @@
 </template>
 
 <script lang="ts" setup>
-import { onMounted, ref } from 'vue';
+import { onMounted } from 'vue';
 
-import { WebRTCClass } from '@/network/webRtc';
-import { useNetworkStore } from '@/store//network';
-
-const muted = ref(true);
-const localVideoRef = ref<HTMLVideoElement>();
-
-const networkStore = useNetworkStore();
-let stream: MediaStream;
-
-onMounted(() => {
-  // const instance = new WebSocketClass({ url: 'ws://localhost:4300' });
-  // networkStore.updateWsMap(roomId.value, instance);
-  // instance.wsInstance?.on(wsConnectStatus.connect, () => {
-  //   console.log('连接websocket成功!');
-  //   handleWebRtc();
-  // });
-});
-
-async function handleWebRtc() {
-  const webrtc = new WebRTCClass();
-  console.log(webrtc);
-  const offer = await webrtc.createOffer();
-  console.log(offer);
-}
-
-// Handles start button action: creates local MediaStream.
-function startAction() {
-  // WARN navigator.mediaDevices在localhost和https才能用,http://192.168.1.103:8000局域网用不了
-  navigator.mediaDevices
-    .getUserMedia({ video: true, audio: true })
-    .then((event) => {
-      console.log('getUserMedia成功', event);
-      stream = event;
-      // if (!localVideoRef.value) return;
-      // localVideoRef.value.srcObject = event;
-    })
-    .catch((err) => {
-      console.log('getUserMedia失败', err);
-    });
-}
+onMounted(() => {});
 </script>
 
 <style lang="scss" scoped></style>

+ 4 - 0
src/interface.ts

@@ -1 +1,5 @@
 // 这里放项目里面的类型
+export enum liveTypeEnum {
+  camera,
+  screen,
+}

+ 24 - 3
src/layout/head/index.vue

@@ -20,17 +20,33 @@
       />
     </div>
     <div class="right">
-      <div class="avatar"></div>
+      <div class="avatar">{{ !userStore.detail && '登录' }}</div>
       <div class="item">动态</div>
       <div class="item">签到</div>
       <div class="item">饭贩</div>
-      <div class="start">我要开播</div>
+      <div
+        v-if="id === '1234'"
+        class="start"
+        @click="appStore.setLiveStatus(true)"
+      >
+        我要开播
+      </div>
     </div>
   </div>
 </template>
 
 <script lang="ts" setup>
 import { ref } from 'vue';
+import { useRoute } from 'vue-router';
+
+import { useAppStore } from '@/store/app';
+import { useUserStore } from '@/store/user';
+
+const route = useRoute();
+
+const id = route.query.id;
+const userStore = useUserStore();
+const appStore = useAppStore();
 
 const list = ref([
   { ico: '', title: '首页' },
@@ -92,11 +108,16 @@ const list = ref([
     align-items: center;
     padding-right: 20px;
     .avatar {
+      display: flex;
+      align-items: center;
+      justify-content: center;
       margin-right: 20px;
       width: 36px;
       height: 36px;
       border-radius: 50%;
-      background-color: yellow;
+      background-color: skyblue;
+      color: white;
+      font-size: 14px;
     }
     .item {
       margin-right: 20px;

+ 1 - 1
src/main.ts

@@ -10,7 +10,7 @@ import store from '@/store/index';
 
 import App from './App.vue';
 
-console.log(adapter.browserDetails.browser, adapter.browserDetails.version);
+console.log('webrtc-adapter', adapter.browserDetails);
 
 const app = createApp(App);
 

+ 80 - 51
src/network/webRtc.ts

@@ -2,7 +2,7 @@ import browserTool from 'browser-tool';
 
 import { useNetworkStore } from '@/store/network';
 
-import { wsMsgType } from './webSocket';
+import { WsMsgTypeEnum } from './webSocket';
 
 function prettierInfo(
   str: string,
@@ -103,6 +103,7 @@ export class WebRTCClass {
   };
 
   localDescription: any;
+  answerDescription: any;
   stream: any;
 
   constructor({ roomId }) {
@@ -113,8 +114,8 @@ export class WebRTCClass {
     // this.handleWebRtcError();
   }
 
-  myAddTrack = (track, stream) => {
-    console.warn('myAddTrackmyAddTrack', track, stream);
+  addTrack = (track, stream) => {
+    console.warn('addTrackaddTrack', track, stream);
     this.peerConnection?.addTrack(track, stream);
   };
 
@@ -309,15 +310,15 @@ export class WebRTCClass {
 
   // 创建offer
   createOffer = async () => {
-    console.log('开始createOffer');
     if (!this.peerConnection) return;
-    if (this.rtcStatus.createOffer) return this.localDescription;
+    prettierInfo('createOffer开始', { browser: this.browser.browser }, 'warn');
     try {
       const description = await this.peerConnection.createOffer({
         iceRestart: true,
         offerToReceiveAudio: true,
         offerToReceiveVideo: true,
       });
+      this.localDescription = description;
       this.rtcStatus.createOffer = true;
       this.update();
       prettierInfo(
@@ -325,9 +326,28 @@ export class WebRTCClass {
         { browser: this.browser.browser },
         'warn'
       );
-      console.log('开始设置本地描述', description);
+      return description;
+    } catch (error) {
+      prettierInfo(
+        'createOffer失败',
+        { browser: this.browser.browser },
+        'error'
+      );
+      console.log(error);
+    }
+  };
+
+  // 设置本地描述
+  setLocalDescription = async (description) => {
+    if (!this.peerConnection) return;
+    prettierInfo(
+      'setLocalDescription开始',
+      { browser: this.browser.browser },
+      'warn'
+    );
+    console.log(description);
+    try {
       await this.peerConnection.setLocalDescription(description);
-      this.localDescription = description;
       this.rtcStatus.setLocalDescription = true;
       this.update();
       prettierInfo(
@@ -335,40 +355,40 @@ export class WebRTCClass {
         { browser: this.browser.browser },
         'warn'
       );
-      return description;
     } catch (error) {
       prettierInfo(
-        'createOffer失败',
+        'setLocalDescription失败',
         { browser: this.browser.browser },
         'error'
       );
+      console.log('setLocalDescription', description);
       console.log(error);
     }
   };
 
   // 创建answer
   createAnswer = async () => {
-    console.log('开始createAnswer');
     if (!this.peerConnection) return;
+    prettierInfo('createAnswer开始', { browser: this.browser.browser }, 'warn');
     try {
       const description = await this.peerConnection.createAnswer();
+      this.answerDescription = description;
       this.update();
       prettierInfo(
         'createAnswer成功',
         { browser: this.browser.browser },
         'warn'
       );
-      console.log('开始设置本地描述', description);
-      await this.peerConnection.setLocalDescription(description);
-      this.localDescription = description;
-      this.rtcStatus.setLocalDescription = true;
-      this.update();
-      prettierInfo(
-        'setLocalDescription成功',
-        { browser: this.browser.browser },
-        'warn'
-      );
       return description;
+      // console.log('开始设置本地描述', description);
+      // await this.peerConnection.setLocalDescription(description);
+      // this.localDescription = description;
+      // this.rtcStatus.setLocalDescription = true;
+      // prettierInfo(
+      //   'setLocalDescription成功',
+      //   { browser: this.browser.browser },
+      //   'warn'
+      // );
     } catch (error) {
       prettierInfo(
         'createAnswer失败',
@@ -380,10 +400,14 @@ export class WebRTCClass {
   };
 
   // 设置远端描述
-  setRemoteDescription = async (description) => {
-    console.log('开始设置远端描述', description);
+  setRemoteDescription = async (description: any) => {
     if (!this.peerConnection) return;
-    if (this.rtcStatus.setRemoteDescription) return;
+    prettierInfo(
+      `setRemoteDescription开始`,
+      { browser: this.browser.browser },
+      'warn'
+    );
+    console.log(description);
     try {
       await this.peerConnection.setRemoteDescription(
         new RTCSessionDescription(description)
@@ -396,24 +420,30 @@ export class WebRTCClass {
         'warn'
       );
     } catch (error) {
-      console.error('设置远端描述错误', error);
+      prettierInfo(
+        'setRemoteDescription失败',
+        { browser: this.browser.browser },
+        'error'
+      );
+      console.log('setRemoteDescription', description);
+      console.log(error);
     }
   };
 
   addStream = (stream) => {
-    if (!this.peerConnection || this.rtcStatus.addStream) return;
+    if (!this.peerConnection) return;
+    // if (!this.peerConnection || this.rtcStatus.addStream) return;
     this.rtcStatus.addStream = true;
-    this.stream = stream;
-    console.log(stream, 22222);
+    this.update();
+    console.log('addStreamaddStreamaddStream', stream);
     document.querySelector<HTMLVideoElement>('#localVideo')!.srcObject = stream;
     prettierInfo('addStream成功', { browser: this.browser.browser }, 'warn');
-    this.update();
   };
 
   // 创建连接
   createConnect = () => {
     if (!this.peerConnection) return;
-    console.warn('createConnect');
+    console.warn('开始监听pc的icecandidate');
     this.peerConnection.addEventListener('icecandidate', (event) => {
       this.rtcStatus.icecandidate = true;
       this.update();
@@ -423,46 +453,45 @@ export class WebRTCClass {
         'warn'
       );
       if (event.candidate) {
-        // if (this.candidateFlag) return;
         const networkStore = useNetworkStore();
         this.candidateFlag = true;
+        this.update();
         console.log('准备发送candidate', event.candidate.candidate);
         const data = {
-          socketId: networkStore.wsMap.get(this.roomId)?.socketIo?.id,
-          roomId: this.roomId,
           candidate: event.candidate.candidate,
           sdpMid: event.candidate.sdpMid,
           sdpMLineIndex: event.candidate.sdpMLineIndex,
         };
         networkStore.wsMap
           .get(this.roomId)
-          ?.socketIo?.emit(wsMsgType.candidate, data);
-        this.update();
+          ?.send({ msgType: WsMsgTypeEnum.candidate, data });
       }
     });
-    console.warn('开始监听addstream');
+
+    console.warn('开始监听pc的addstream');
     this.peerConnection.addEventListener('addstream', (event: any) => {
       console.log('pc收到addstream事件', event.stream);
       // document.querySelector<HTMLVideoElement>('#localVideo')!.srcObject =
       //   event.stream;
-      // this.addStream(event.stream);
+      this.addStream(event.stream);
     });
 
-    console.warn('开始监听ontrack');
+    console.warn('开始监听pc的ontrack');
     this.peerConnection.addEventListener('ontrack', (event: any) => {
       console.log('pc收到ontrack事件', event.stream);
     });
 
-    console.warn('开始监听addtrack');
+    console.warn('开始监听pc的addtrack');
     this.peerConnection.addEventListener('addtrack', (event: any) => {
       console.log('pc收到addtrack事件', event.stream);
     });
 
-    console.warn('开始监听track');
+    console.warn('开始监听pc的track');
     this.peerConnection.addEventListener('track', (event: any) => {
       console.log('pc收到track事件', event);
-      document.querySelector<HTMLVideoElement>('#localVideo')!.srcObject =
-        event.streams[0];
+      this.addStream(event.streams[0]);
+      // document.querySelector<HTMLVideoElement>('#localVideo')!.srcObject =
+      //   event.streams[0];
     });
 
     // connectionstatechange
@@ -500,16 +529,16 @@ export class WebRTCClass {
     }
     if (!this.peerConnection) {
       this.peerConnection = new RTCPeerConnection({
-        iceServers: [
-          // {
-          //   urls: 'stun:stun.l.google.com:19302',
-          // },
-          {
-            urls: 'turn:hsslive.cn:3478',
-            username: 'hss',
-            credential: '123456',
-          },
-        ],
+        // iceServers: [
+        //   // {
+        //   //   urls: 'stun:stun.l.google.com:19302',
+        //   // },
+        //   {
+        //     urls: 'turn:hsslive.cn:3478',
+        //     username: 'hss',
+        //     credential: '123456',
+        //   },
+        // ],
       });
       // this.dataChannel =
       //   this.peerConnection.createDataChannel('MessageChannel');

+ 48 - 41
src/network/websocket.ts

@@ -3,116 +3,123 @@ import { Socket, io } from 'socket.io-client';
 import { useNetworkStore } from '@/store/network';
 
 // websocket连接状态
-export const wsConnectStatus = {
+export enum WsConnectStatusEnum {
   /** 已连接 */
-  connection: 'connection',
+  connection = 'connection',
   /** 连接中 */
-  connecting: 'connecting',
+  connecting = 'connecting',
   /** 已连接 */
-  connected: 'connected',
+  connected = 'connected',
   /** 断开连接中 */
-  disconnecting: 'disconnecting',
+  disconnecting = 'disconnecting',
   /** 已断开连接 */
-  disconnect: 'disconnect',
+  disconnect = 'disconnect',
   /** 重新连接 */
-  reconnect: 'reconnect',
+  reconnect = 'reconnect',
   /** 客户端的已连接 */
-  connect: 'connect',
-};
+  connect = 'connect',
+}
 
 // websocket消息类型
-export const wsMsgType = {
+export enum WsMsgTypeEnum {
   /** 用户进入聊天 */
-  join: 'join',
+  join = 'join',
   /** 用户进入聊天完成 */
-  joined: 'joined',
+  joined = 'joined',
   /** 用户进入聊天 */
-  otherJoin: 'otherJoin',
+  otherJoin = 'otherJoin',
   /** 用户退出聊天 */
-  leave: 'leave',
+  leave = 'leave',
   /** 用户退出聊天完成 */
-  leaved: 'leaved',
+  leaved = 'leaved',
   /** 当前所有在线用户 */
-  liveUser: 'liveUser',
+  liveUser = 'liveUser',
   /** 人满了 */
-  full: 'full',
+  full = 'full',
   /** 用户发送消息 */
-  message: 'message',
-  offer: 'offer',
-  answer: 'answer',
-  candidate: 'candidate',
-};
-
-export enum statusEnum {
-  connect = 'connect',
-  disconnect = 'disconnect',
+  message = 'message',
+  /** 管理员在线 */
+  adminIn = 'adminIn',
+  offer = 'offer',
+  answer = 'answer',
+  candidate = 'candidate',
 }
 
 export class WebSocketClass {
   socketIo: Socket | null = null;
-  status: statusEnum = statusEnum.disconnect;
+  status: WsConnectStatusEnum = WsConnectStatusEnum.disconnect;
+  url = '';
 
-  url = 'ws://localhost:3300';
   roomId = '-1';
+  isAdmin = false;
 
-  constructor({ roomId, url }) {
+  constructor({ roomId, url, isAdmin }) {
     if (!window.WebSocket) {
       alert('当前环境不支持WebSocket!');
       return;
     }
     this.roomId = roomId;
+    this.isAdmin = isAdmin;
+
     this.url = url;
     this.socketIo = io(url, { transports: ['websocket'] });
     this.update();
 
     // // websocket连接成功
-    // this.socketIo.on(wsConnectStatus.connect, (socket) => {
+    // this.socketIo.on(WsConnectStatusEnum.connect, (socket) => {
     //   console.log('websocket连接成功', socket);
-    //   this.status = statusEnum.connect;
+    //   this.status = WsStatusEnum.connect;
     //   this.update();
-    //   this.socketIo?.emit(wsMsgType.join, { roomId: this.roomId });
+    //   this.socketIo?.emit(WsMsgTypeEnum.join, { roomId: this.roomId });
     // });
 
     // // websocket连接断开
-    // this.socketIo.on(wsConnectStatus.disconnect, () => {
+    // this.socketIo.on(WsConnectStatusEnum.disconnect, () => {
     //   console.log('websocket连接断开', this);
-    //   this.status = statusEnum.disconnect;
+    //   this.status = WsStatusEnum.disconnect;
     //   this.update();
     // });
 
     // // 用户加入房间
-    // this.socketIo.on(wsMsgType.join, (data) => {
+    // this.socketIo.on(WsMsgTypeEnum.join, (data) => {
     //   console.log('用户加入房间', data);
     // });
 
     // // 其他用户加入房间
-    // this.socketIo.on(wsMsgType.otherJoin, (data) => {
+    // this.socketIo.on(WsMsgTypeEnum.otherJoin, (data) => {
     //   console.log('其他用户加入房间', data);
     // });
 
     // // 用户离开房间
-    // this.socketIo.on(wsMsgType.leave, (data) => {
+    // this.socketIo.on(WsMsgTypeEnum.leave, (data) => {
     //   console.log('用户离开房间', data);
     // });
 
     // // 用户发送消息
-    // this.socketIo.on(wsMsgType.message, (data) => {
+    // this.socketIo.on(WsMsgTypeEnum.message, (data) => {
     //   console.log('用户发送消息', data);
     // });
 
     // // 用户发送 offer
-    // this.socketIo.on(wsMsgType.offer, (data) => {
+    // this.socketIo.on(WsMsgTypeEnum.offer, (data) => {
     //   console.log('用户发送 offer', data);
     // });
 
     // // 用户发送 answer
-    // this.socketIo.on(wsMsgType.answer, (data) => {
+    // this.socketIo.on(WsMsgTypeEnum.answer, (data) => {
     //   console.log('用户发送 answer', data);
     // });
   }
 
   // 发送websocket消息
-  send = () => {};
+  send = ({ msgType, data }: { msgType: WsMsgTypeEnum; data?: any }) => {
+    this.socketIo?.emit(msgType, {
+      roomId: this.roomId,
+      socketId: this.socketIo.id,
+      data,
+      isAdmin: this.isAdmin,
+    });
+  };
 
   // 更新store
   update = () => {

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

@@ -4,11 +4,18 @@ export const useAppStore = defineStore('app', {
   state: () => {
     return {
       counter: 1,
+      liveStatus: false,
+    } as {
+      counter: number;
+      liveStatus: boolean;
     };
   },
   actions: {
     setCounter(res) {
       this.counter = res;
     },
+    setLiveStatus(res: boolean) {
+      this.liveStatus = res;
+    },
   },
 });

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

@@ -26,6 +26,7 @@ export const useNetworkStore = defineStore('network', {
       }
     },
     updateRtcMap(roomId: string, arg) {
+      // console.log('updateRtcMap', roomId, arg);
       const val = this.rtcMap.get(roomId);
       if (val) {
         this.rtcMap.set(roomId, { ...val, ...arg });

+ 1 - 1
src/store/user/index.ts

@@ -8,7 +8,7 @@ type RootState = {
 export const useUserStore = defineStore('user', {
   state: () => {
     return {
-      detail: { id: -1 },
+      detail: null,
     } as RootState;
   },
   actions: {

+ 27 - 26
src/views/home/index copy.vue

@@ -96,9 +96,9 @@ import { useRoute } from 'vue-router';
 import { WebRTCClass } from '@/network/webRtc';
 import {
   WebSocketClass,
-  statusEnum,
-  wsConnectStatus,
-  wsMsgType,
+  WsConnectStatusEnum,
+  WsMsgTypeEnum,
+  WsStatusEnum,
 } from '@/network/webSocket';
 import { useNetworkStore } from '@/store/network';
 
@@ -114,16 +114,15 @@ const muted = ref(true);
 const localVideoRef = ref<HTMLVideoElement>();
 const localStream = ref();
 const currType = ref(1); // 1:摄像头,2:录屏
-function getSocketId() {
-  return networkStore.wsMap.get(roomId.value!)?.socketIo?.id;
-}
 const id = ref('');
 const route = useRoute();
+
 interface IOffer {
   socketId: string;
   roomId: string;
   sdp: any;
 }
+
 interface ICandidate {
   socketId: string;
   roomId: string;
@@ -159,7 +158,9 @@ onMounted(() => {
     rtc.update();
   });
 });
-
+function getSocketId() {
+  return networkStore.wsMap.get(roomId.value!)?.socketIo?.id;
+}
 function join() {
   console.log('join的房间号', roomId.value);
   if (!roomId.value) {
@@ -174,7 +175,7 @@ function join() {
   }
   const instance = networkStore.wsMap.get(roomId.value);
   if (!instance?.socketIo) return;
-  instance.socketIo.emit(wsMsgType.join, {
+  instance.socketIo.emit(WsMsgTypeEnum.join, {
     roomId: instance.roomId,
   });
   if (id.value === '1234') {
@@ -190,30 +191,30 @@ function initReceive() {
   const instance = websocketInstant.value;
   if (!instance?.socketIo) return;
   // websocket连接成功
-  instance.socketIo.on(wsConnectStatus.connect, () => {
+  instance.socketIo.on(WsConnectStatusEnum.connect, () => {
     console.log('【websocket】websocket连接成功', instance.socketIo?.id);
     if (!instance) return;
-    instance.status = statusEnum.connect;
+    instance.status = WsStatusEnum.connect;
     instance.update();
   });
 
   // 当前所有在线用户
-  instance.socketIo.on(wsMsgType.liveUser, (data) => {
+  instance.socketIo.on(WsMsgTypeEnum.liveUser, (data) => {
     console.log('【websocket】当前所有在线用户');
     if (!instance) return;
     userList.value = data;
   });
 
   // websocket连接断开
-  instance.socketIo.on(wsConnectStatus.disconnect, () => {
+  instance.socketIo.on(WsConnectStatusEnum.disconnect, () => {
     console.log('【websocket】websocket连接断开', instance);
     if (!instance) return;
-    instance.status = statusEnum.disconnect;
+    instance.status = WsStatusEnum.disconnect;
     instance.update();
   });
 
   // 收到offer
-  instance.socketIo.on(wsMsgType.offer, async (data: IOffer) => {
+  instance.socketIo.on(WsMsgTypeEnum.offer, async (data: IOffer) => {
     console.warn('【websocket】收到offer', data);
     if (!instance) return;
     const rtc = networkStore.rtcMap.get(roomId.value);
@@ -223,7 +224,7 @@ function initReceive() {
       setTimeout(async () => {
         await rtc?.setRemoteDescription(data.sdp);
         const sdp = await rtc?.createAnswer();
-        instance.socketIo?.emit(wsMsgType.answer, {
+        instance.socketIo?.emit(WsMsgTypeEnum.answer, {
           socketId: getSocketId(),
           roomId: roomId.value,
           sdp,
@@ -235,7 +236,7 @@ function initReceive() {
   });
 
   // 收到answer
-  instance.socketIo.on(wsMsgType.answer, async (data: IOffer) => {
+  instance.socketIo.on(WsMsgTypeEnum.answer, async (data: IOffer) => {
     console.warn('【websocket】收到answer', data);
     if (!instance) return;
     if (!networkStore.rtcMap.get(roomId.value)?.rtcStatus.createOffer) return;
@@ -252,7 +253,7 @@ function initReceive() {
   });
 
   // 收到candidate
-  instance.socketIo.on(wsMsgType.candidate, (data: ICandidate) => {
+  instance.socketIo.on(WsMsgTypeEnum.candidate, (data: ICandidate) => {
     console.warn('【websocket】收到candidate', data);
     if (!instance) return;
     const rtc = networkStore.rtcMap.get(roomId.value);
@@ -282,13 +283,13 @@ function initReceive() {
   });
 
   // 用户加入房间
-  instance.socketIo.on(wsMsgType.join, (data) => {
+  instance.socketIo.on(WsMsgTypeEnum.join, (data) => {
     console.log('【websocket】用户加入房间', data);
     if (!instance) return;
   });
 
   // 用户加入房间
-  instance.socketIo.on(wsMsgType.joined, (data) => {
+  instance.socketIo.on(WsMsgTypeEnum.joined, (data) => {
     console.log('【websocket】用户加入房间完成', data);
     if (!instance) return;
     console.warn('开始new WebRTCClass');
@@ -301,7 +302,7 @@ function initReceive() {
   });
 
   // 其他用户加入房间
-  instance.socketIo.on(wsMsgType.otherJoin, (data) => {
+  instance.socketIo.on(WsMsgTypeEnum.otherJoin, (data) => {
     console.log('【websocket】其他用户加入房间', data);
     if (!instance) return;
     console.log('加轨');
@@ -315,16 +316,16 @@ function initReceive() {
   });
 
   // 用户离开房间
-  instance.socketIo.on(wsMsgType.leave, (data) => {
+  instance.socketIo.on(WsMsgTypeEnum.leave, (data) => {
     console.log('【websocket】用户离开房间', data);
     if (!instance) return;
-    instance.socketIo?.emit(wsMsgType.leave, {
+    instance.socketIo?.emit(WsMsgTypeEnum.leave, {
       roomId: instance.roomId,
     });
   });
 
   // 用户离开房间完成
-  instance.socketIo.on(wsMsgType.leaved, (data) => {
+  instance.socketIo.on(WsMsgTypeEnum.leaved, (data) => {
     console.log('【websocket】用户离开房间完成', data);
     if (!instance) return;
     instance.close();
@@ -384,7 +385,7 @@ async function sendOffer() {
     sdp: localDesc,
   };
   console.warn('【websocket】发送offer', data);
-  websocketInstant.value.socketIo?.emit(wsMsgType.offer, data);
+  websocketInstant.value.socketIo?.emit(WsMsgTypeEnum.offer, data);
 }
 // async function sendAnswer(sdp) {
 //   if (!websocketInstant.value) return;
@@ -394,7 +395,7 @@ async function sendOffer() {
 //   if (!createOffer) {
 //     await networkStore.rtcMap.get(roomId.value)?.createOffer();
 //   }
-//   websocketInstant.value.socketIo?.emit(wsMsgType.answer, {
+//   websocketInstant.value.socketIo?.emit(WsMsgTypeEnum.answer, {
 //     socketId: getSocketId(),
 //     roomId: roomId.value,
 //     sdp,
@@ -408,7 +409,7 @@ function leave() {
     leaveRef.value.disabled = true;
   }
   if (!websocketInstant.value) return;
-  websocketInstant.value.socketIo?.emit(wsMsgType.leave, {
+  websocketInstant.value.socketIo?.emit(WsMsgTypeEnum.leave, {
     roomId: websocketInstant.value.roomId,
   });
 }

+ 350 - 4
src/views/home/index.vue

@@ -7,7 +7,8 @@
           <div class="detail">
             <div class="top">
               <span class="tag">未开播</span>
-              房东的猫livehouse/音乐节
+              <!-- 房东的猫livehouse/音乐节 -->
+              {{ networkStore.rtcMap.get(roomId)?.rtcStatus }}
             </div>
             <div class="bottom">
               <span class="tag">UP 3</span>
@@ -33,7 +34,21 @@
           <div class="bottom">关注量:5</div>
         </div>
       </div>
-      <div class="video"></div>
+      <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="muted"
+          controls
+        ></video>
+      </div>
       <div class="gift">
         <div
           v-for="(item, index) in giftList"
@@ -84,7 +99,36 @@
 </template>
 
 <script lang="ts" setup>
-import { ref } from 'vue';
+import { onMounted, ref, watch } from 'vue';
+import { useRoute } from 'vue-router';
+
+import { liveTypeEnum } from '@/interface';
+import { WebRTCClass } from '@/network/webRtc';
+import {
+  WebSocketClass,
+  WsConnectStatusEnum,
+  WsMsgTypeEnum,
+} from '@/network/webSocket';
+import { useAppStore } from '@/store/app';
+import { useNetworkStore } from '@/store/network';
+
+const networkStore = useNetworkStore();
+
+const roomIdRef = ref<HTMLInputElement>();
+const joinRef = ref<HTMLButtonElement>();
+const leaveRef = ref<HTMLButtonElement>();
+const roomId = ref<string>('19990507');
+const websocketInstant = ref<WebSocketClass>();
+// const userList = ref<{ id: string; rooms: string[] }[]>([]);
+const muted = ref(true);
+const localVideoRef = ref<HTMLVideoElement>();
+const localStream = ref();
+const currType = ref(liveTypeEnum.camera); // 1:摄像头,2:录屏
+const id = ref('');
+
+const route = useRoute();
+const appStore = useAppStore();
+const isAdmin = ref(route.query.id === '1234');
 
 const giftList = ref([
   { name: '鲜花', ico: '', price: '免费' },
@@ -118,6 +162,304 @@ const userList = ref([
   { nickname: '大鸡腿', avatar: '46326fb26', expr: 100 },
   { nickname: '一杯咖啡', avatar: 'shgd544', expr: 100 },
 ]);
+
+interface IOffer {
+  socketId: string;
+  roomId: string;
+  data: {
+    sdp: any;
+  };
+  isAdmin: boolean;
+}
+
+interface ICandidate {
+  socketId: string;
+  roomId: string;
+  data: {
+    candidate: string;
+    sdpMid: string | null;
+    sdpMLineIndex: number | null;
+  };
+}
+
+onMounted(() => {
+  id.value = route.query.id as string;
+  websocketInstant.value = new WebSocketClass({
+    roomId: roomId.value,
+    url:
+      process.env.NODE_ENV === 'development'
+        ? 'ws://localhost:4300'
+        : 'wss://live.hsslive.cn',
+    isAdmin: isAdmin.value,
+  });
+  websocketInstant.value.update();
+  initReceive();
+  sendJoin();
+  localVideoRef.value?.addEventListener('loadstart', () => {
+    console.warn('视频流-loadstart');
+    const rtc = networkStore.rtcMap.get(roomId.value);
+    if (!rtc) return;
+    rtc.rtcStatus.loadstart = true;
+    rtc.update();
+  });
+
+  localVideoRef.value?.addEventListener('loadedmetadata', () => {
+    console.warn('视频流-loadedmetadata');
+    const rtc = networkStore.rtcMap.get(roomId.value);
+    if (!rtc) return;
+    rtc.rtcStatus.loadedmetadata = true;
+    rtc.update();
+    setTimeout(async () => {
+      if (isAdmin.value) {
+        console.warn('发送管理员正在直播消息');
+        websocketInstant.value?.send({
+          msgType: WsMsgTypeEnum.adminIn,
+          data: { socketId: getSocketId(), roomId: roomId.value },
+        });
+        await sendOffer();
+      }
+    }, 100);
+  });
+});
+
+watch(
+  () => appStore.liveStatus,
+  (newVal) => {
+    if (newVal) {
+      console.log('开始直播');
+      join();
+    }
+  }
+);
+
+function getSocketId() {
+  return networkStore.wsMap.get(roomId.value!)?.socketIo?.id;
+}
+
+function sendJoin() {
+  const instance = networkStore.wsMap.get(roomId.value);
+  if (!instance) return;
+  instance.send({ msgType: WsMsgTypeEnum.join, data: {} });
+}
+
+async function join() {
+  console.log('join的房间号', roomId.value);
+  if (!roomId.value) {
+    console.error('房间号不能为空!');
+    alert('房间号不能为空!');
+    return;
+  }
+
+  if (isAdmin.value) {
+    try {
+      if (currType.value === liveTypeEnum.camera) {
+        await startMediaDevices();
+      } else if (currType.value === liveTypeEnum.screen) {
+        await startGetDisplayMedia();
+      }
+    } catch (error) {
+      console.log('用户拒绝', error);
+    }
+  }
+}
+
+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();
+  });
+
+  // websocket连接断开
+  instance.socketIo.on(WsConnectStatusEnum.disconnect, () => {
+    console.log('【websocket】websocket连接断开', instance);
+    if (!instance) return;
+    instance.status = WsConnectStatusEnum.disconnect;
+    instance.update();
+  });
+
+  // 当前所有在线用户
+  instance.socketIo.on(WsMsgTypeEnum.adminIn, (data) => {
+    console.log('【websocket】收到管理员正在直播', data);
+    sendOffer();
+  });
+
+  // 当前所有在线用户
+  instance.socketIo.on(WsMsgTypeEnum.liveUser, (data) => {
+    console.log('【websocket】当前所有在线用户');
+    if (!instance) return;
+    userList.value = data;
+  });
+
+  // 收到offer
+  instance.socketIo.on(WsMsgTypeEnum.offer, async (data: IOffer) => {
+    console.warn('【websocket】收到offer', data);
+    if (!instance) return;
+    if (data.socketId !== getSocketId()) {
+      const rtc = networkStore.rtcMap.get(roomId.value);
+      if (!rtc) return;
+      console.log('收到offer,并且这个offer不是我发的', data);
+      await rtc.setRemoteDescription(data.data.sdp);
+      const sdp = await rtc.createAnswer();
+      console.warn('【websocket】发送answer', sdp);
+      websocketInstant.value?.send({
+        msgType: WsMsgTypeEnum.answer,
+        data: { sdp },
+      });
+    } else {
+      console.log('收到offer,并且这个offer是我发的');
+    }
+  });
+
+  // 收到answer
+  instance.socketIo.on(WsMsgTypeEnum.answer, async (data: IOffer) => {
+    console.warn('【websocket】收到answer', data);
+    if (!instance) return;
+    // if (!networkStore.rtcMap.get(roomId.value)?.rtcStatus.createOffer) return;
+    if (data.socketId !== getSocketId()) {
+      console.log('不是我发的answer');
+      const rtc = networkStore.rtcMap.get(roomId.value);
+      if (!rtc) return;
+      // await rtc.setRemoteDescription(data.data.sdp);
+      // const sdp = await rtc.createAnswer();
+      // console.warn('【websocket】发送answer', sdp);
+      // websocketInstant.value?.send({
+      //   msgType: WsMsgTypeEnum.answer,
+      //   data: { sdp },
+      // });
+    } else {
+      console.log('是我发的answer');
+    }
+  });
+
+  // 收到candidate
+  instance.socketIo.on(WsMsgTypeEnum.candidate, (data: ICandidate) => {
+    if (!instance) return;
+    console.warn('【websocket】收到candidate', data);
+    const rtc = networkStore.rtcMap.get(roomId.value);
+    if (!rtc) return;
+    if (data.socketId !== getSocketId()) {
+      console.log('不是我发的candidate');
+      const candidate = new RTCIceCandidate({
+        sdpMid: data.data.sdpMid,
+        sdpMLineIndex: data.data.sdpMLineIndex,
+        candidate: data.data.candidate,
+      });
+      rtc.peerConnection
+        ?.addIceCandidate(candidate)
+        .then(() => {
+          console.log('candidate成功');
+          rtc.rtcStatus.icecandidate = true;
+          rtc.update();
+        })
+        .catch((err) => {
+          console.error('candidate失败', err);
+        });
+    } else {
+      console.log('是我发的candidate');
+    }
+  });
+
+  // 用户加入房间
+  instance.socketIo.on(WsMsgTypeEnum.join, (data) => {
+    console.log('【websocket】用户加入房间', data);
+    if (!instance) return;
+  });
+
+  // 用户加入房间
+  instance.socketIo.on(WsMsgTypeEnum.joined, (data) => {
+    console.log('【websocket】用户加入房间完成', data);
+    if (!instance) return;
+    console.warn('开始new WebRTCClass');
+    const rtc = new WebRTCClass({ roomId: roomId.value });
+    rtc.rtcStatus.joined = true;
+    rtc.update();
+  });
+
+  // 其他用户加入房间
+  instance.socketIo.on(WsMsgTypeEnum.otherJoin, (data) => {
+    console.log('【websocket】其他用户加入房间', data);
+    if (!instance) return;
+  });
+
+  // 用户离开房间
+  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;
+    instance.close();
+  });
+}
+
+async function startMediaDevices() {
+  currType.value = liveTypeEnum.camera;
+  // 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);
+  if (!localVideoRef.value) return;
+  localVideoRef.value.srcObject = event;
+  localStream.value = event;
+  localStream.value.getTracks().forEach((track) => {
+    networkStore.rtcMap.get(roomId.value)?.addTrack(track, localStream.value);
+  });
+}
+
+async function startGetDisplayMedia() {
+  currType.value = liveTypeEnum.screen;
+  // 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;
+  localStream.value.getTracks().forEach((track) => {
+    console.log(track, networkStore.rtcMap.get(roomId.value));
+    networkStore.rtcMap.get(roomId.value)?.addTrack(track, localStream.value);
+  });
+}
+
+async function sendOffer() {
+  if (!websocketInstant.value) return;
+  const rtc = networkStore.rtcMap.get(roomId.value);
+  if (!rtc) return;
+  const sdp = await rtc.createOffer();
+  await rtc.setLocalDescription(sdp);
+  console.warn('【websocket】发送offer', sdp);
+  websocketInstant.value.send({
+    msgType: WsMsgTypeEnum.offer,
+    data: { sdp },
+  });
+}
+
+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>
@@ -193,9 +535,13 @@ const userList = ref([
         }
       }
     }
-    .video {
+    .video-wrap {
       height: 500px;
       background-color: #18191c;
+      #localVideo {
+        width: 100%;
+        height: 100%;
+      }
     }
     .gift {
       display: flex;