Explorar o código

feat: 阶段完成

shuisheng %!s(int64=2) %!d(string=hai) anos
pai
achega
95fe2cc419
Modificáronse 7 ficheiros con 434 adicións e 148 borrados
  1. 12 24
      README.md
  2. 1 24
      script/constant.ts
  3. 0 10
      src/App.vue
  4. 135 25
      src/network/webRtc.ts
  5. 18 14
      src/network/websocket.ts
  6. 1 4
      src/router/index.ts
  7. 267 47
      src/views/home/index.vue

+ 12 - 24
README.md

@@ -1,26 +1,9 @@
 # 简介
 
-> 主要实现了 vuecli5 的大部分功能
-
-- [x] 基于 vue3 + webpack5
-- [x] 路由管理:vue-router4.x
-- [x] 状态管理:pinia2.x
-- [x] css 处理:scss + windicss(可选)
-- [x] 代码规范:eslint + prettier
-- [x] 项目规范:husky + commitizen + commitlint + lintstaged
-
-- [x] 支持热更新、typescript、路由懒加载
-
-> 一些相关的配置(如 eslint、windicss、outputStaticUrl 等)暴露在 script/constant.ts 了
+billd 直播间
 
 # 安装依赖
 
-更新 billd 依赖:
-
-```bash
-pnpm i billd-utils@latest billd-scss@latest billd-html-webpack-plugin@latest billd-deploy@latest
-```
-
 ```bash
 pnpm install
 ```
@@ -42,14 +25,19 @@ script/constant.ts 里的 outputStaticUrl 如果是'/aaa/'的话,默认就运
 pnpm run build
 ```
 
-# git 提交
+# billd 依赖
+
+更新 billd:
 
 ```bash
-pnpm run cz
+pnpm i billd-utils@latest billd-scss@latest billd-html-webpack-plugin@latest billd-deploy@latest
 ```
 
-# 内置第三方包
+| 包名                                                                                 | 版本                                                                                                                      |
+| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------- |
+| [billd-utils](https://github.com/galaxy-s10/billd-utils)                             | [![npm](https://img.shields.io/npm/v/billd-utils)](https://www.npmjs.com/package/billd-utils)                             |
+| [billd-scss](https://github.com/galaxy-s10/billd-scss)                               | [![npm](https://img.shields.io/npm/v/billd-scss)](https://www.npmjs.com/package/billd-scss)                               |
+| [billd-html-webpack-plugin](https://github.com/galaxy-s10/billd-html-webpack-plugin) | [![npm](https://img.shields.io/npm/v/billd-html-webpack-plugin)](https://www.npmjs.com/package/billd-html-webpack-plugin) |
 
-- [billd-utils](https://github.com/galaxy-s10/billd-utils)
-- [billd-scss](https://github.com/galaxy-s10/billd-scss),已在 sass-loader 里配置了 additionalData: `@use 'billd-scss/src/index.scss' as *;`
-- [billd-html-webpack-plugin](https://github.com/galaxy-s10/billd-html-webpack-plugin),已在 webpack 配置里使用了该插件
+- billd-scss,已在 sass-loader 里配置了 additionalData: `@use 'billd-scss/src/index.scss' as *;`
+- billd-html-webpack-plugin,已在 webpack 配置里使用了该插件

+ 1 - 24
script/constant.ts

@@ -12,28 +12,5 @@ export const windicssEnable = false; // 是否开启windicss
 export const htmlWebpackPluginTitle = 'billd-live'; // htmlWebpackPlugin的标题
 
 export const outputStaticUrl = (isProduction: boolean) => {
-  // console.table({ isProduction, APP_NAME, APP_ENV, NODE_ENV, PUBLIC_PATH });
-  if (APP_ENV === undefined && APP_NAME === undefined) {
-    return '/';
-  }
-  if (isProduction) {
-    // 如果是jenkins里面构建,会执行build.sh,一定会有APP_NAME,APP_ENV可能是:'null'|'beta'|'preview'|'prod'
-    if (APP_ENV === 'null') {
-      return `/${APP_NAME!}/`;
-    } else {
-      return `/${APP_NAME!}/${APP_ENV!}/`;
-    }
-  } else {
-    if (APP_NAME === undefined) {
-      // 如果没设置项目名称,则判断是否设置了项目环境,如果设置了,则返回/项目环境/,否则返回'/'
-      return APP_ENV ? `/${APP_ENV}/` : '/';
-    }
-    if (APP_ENV === undefined) {
-      // 如果没设置项目环境,则判断是否设置了项目名称,如果设置了,则返回/项目名称/,否则返回'/'
-      return APP_NAME ? `/${APP_NAME}/` : '/';
-    }
-
-    // 返回:/项目名称/项目环境/
-    return `/${APP_NAME}/${APP_ENV}/`;
-  }
+  return '/';
 };

+ 0 - 10
src/App.vue

@@ -1,16 +1,6 @@
 <template>
   <div>
     <router-view></router-view>
-    <video
-      id="localVideo"
-      ref="localVideoRef"
-      autoplay
-      playsinline
-      :muted="muted"
-    ></video>
-    <div>
-      <button @click="startAction">start</button>
-    </div>
   </div>
 </template>
 

+ 135 - 25
src/network/webRtc.ts

@@ -1,5 +1,9 @@
 import browserTool from 'browser-tool';
 
+import { useNetworkStore } from '@/store/network';
+
+import { wsMsgType } from './webSocket';
+
 function prettierInfo(
   str: string,
   data: {
@@ -57,6 +61,8 @@ export const frontendErrorCode = {
 };
 
 export class WebRTCClass {
+  roomId = '-1';
+
   peerConnection: RTCPeerConnection | null = null;
   dataChannel: RTCDataChannel | null = null;
 
@@ -96,12 +102,22 @@ export class WebRTCClass {
     loadedmetadata: false, // true代表成功,false代表失败
   };
 
-  constructor() {
+  localDescription: any;
+  stream: any;
+
+  constructor({ roomId }) {
+    this.roomId = roomId;
     this.browser = browserTool();
     this.createPeerConnection();
+    this.update();
     // this.handleWebRtcError();
   }
 
+  myAddTrack = (track, stream) => {
+    console.warn('myAddTrackmyAddTrack', track, stream);
+    this.peerConnection?.addTrack(track, stream);
+  };
+
   handleWebRtcError = () => {
     this.getStatsSetIntervalTimer = setInterval(() => {
       this.peerConnection
@@ -293,17 +309,59 @@ export class WebRTCClass {
 
   // 创建offer
   createOffer = async () => {
+    console.log('开始createOffer');
     if (!this.peerConnection) return;
+    if (this.rtcStatus.createOffer) return;
     try {
-      const description = await this.peerConnection.createOffer();
+      const description = await this.peerConnection.createOffer({
+        offerToReceiveAudio: false,
+        offerToReceiveVideo: true,
+      });
       this.rtcStatus.createOffer = true;
+      this.update();
       prettierInfo(
         'createOffer成功',
         { 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;
+    } catch (error) {
+      prettierInfo(
+        'createOffer失败',
+        { browser: this.browser.browser },
+        'error'
+      );
+      console.log(error);
+    }
+  };
+
+  // 创建answer
+  createAnswer = async () => {
+    console.log('开始createAnswer');
+    if (!this.peerConnection) return;
+    try {
+      const description = await this.peerConnection.createAnswer();
+      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 },
@@ -311,18 +369,26 @@ export class WebRTCClass {
       );
       return description;
     } catch (error) {
-      prettierInfo('创建offer失败', { browser: this.browser.browser }, 'error');
+      prettierInfo(
+        'createAnswer失败',
+        { browser: this.browser.browser },
+        'error'
+      );
+      console.log(error);
     }
   };
 
   // 设置远端描述
-  setRemoteDescription = async (description: any) => {
+  setRemoteDescription = async (description) => {
+    console.log('开始设置远端描述', description);
     if (!this.peerConnection) return;
+    if (this.rtcStatus.setRemoteDescription) return;
     try {
       await this.peerConnection.setRemoteDescription(
         new RTCSessionDescription(description)
       );
       this.rtcStatus.setRemoteDescription = true;
+      this.update();
       prettierInfo(
         'setRemoteDescription成功',
         { browser: this.browser.browser },
@@ -336,7 +402,11 @@ export class WebRTCClass {
   addStream = (stream) => {
     if (!this.peerConnection || this.rtcStatus.addStream) return;
     this.rtcStatus.addStream = true;
+    this.stream = stream;
+    console.log(stream, 22222);
+    document.querySelector<HTMLVideoElement>('#localVideo')!.srcObject = stream;
     prettierInfo('addStream成功', { browser: this.browser.browser }, 'warn');
+    this.update();
   };
 
   // 创建连接
@@ -345,6 +415,7 @@ export class WebRTCClass {
     console.warn('createConnect');
     this.peerConnection.addEventListener('icecandidate', (event) => {
       this.rtcStatus.icecandidate = true;
+      this.update();
       prettierInfo(
         'pc收到icecandidate',
         { browser: this.browser.browser },
@@ -352,23 +423,54 @@ export class WebRTCClass {
       );
       if (event.candidate) {
         if (this.candidateFlag) return;
+        const networkStore = useNetworkStore();
         this.candidateFlag = true;
-        console.log('准备发送_candidate', event.candidate.candidate);
+        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();
       }
     });
-
+    console.warn('开始监听addstream');
     this.peerConnection.addEventListener('addstream', (event: any) => {
-      console.log('addstream', event.stream);
+      console.log('pc收到addstream事件', event.stream);
+      // document.querySelector<HTMLVideoElement>('#localVideo')!.srcObject =
+      //   event.stream;
+      // this.addStream(event.stream);
     });
 
-    // 已经有视频或者声音通道
+    console.warn('开始监听ontrack');
     this.peerConnection.addEventListener('ontrack', (event: any) => {
-      console.log('ontrack', event.stream.id);
+      console.log('pc收到ontrack事件', event.stream);
     });
 
-    // 有视频或者声音通道
+    console.warn('开始监听addtrack');
     this.peerConnection.addEventListener('addtrack', (event: any) => {
-      console.log('addtrack', event.stream.id);
+      console.log('pc收到addtrack事件', event.stream);
+    });
+
+    console.warn('开始监听track');
+    this.peerConnection.addEventListener('track', (event: any) => {
+      console.log('pc收到track事件', event);
+      // setTimeout(() => {
+      // const video = document.createElement('video');
+      // video.srcObject = event.streams[0];
+      // video.autoplay = true;
+      // video.controls = true;
+      // video.playsInline = true;
+      // document.body.appendChild(video);
+      // this.addStream(event.streams[0]);
+      document.querySelector<HTMLVideoElement>('#localVideo')!.srcObject =
+        event.streams[0];
+      // }, 1000);
     });
 
     // connectionstatechange
@@ -406,21 +508,22 @@ export class WebRTCClass {
     }
     if (!this.peerConnection) {
       this.peerConnection = new RTCPeerConnection();
-      this.dataChannel =
-        this.peerConnection.createDataChannel('MessageChannel');
-
-      this.dataChannel.onopen = (event) => {
-        console.warn('dataChannel---onopen', event);
-      };
-      this.dataChannel.onerror = (event) => {
-        console.warn('dataChannel---onerror', event);
-      };
-      this.dataChannel.onmessage = (event) => {
-        console.log('dataChannel---onmessage', event);
-      };
-      this.peerConnection.addTransceiver('video', { direction: 'recvonly' });
-      this.peerConnection.addTransceiver('audio', { direction: 'recvonly' });
+      // this.dataChannel =
+      //   this.peerConnection.createDataChannel('MessageChannel');
+
+      // this.dataChannel.onopen = (event) => {
+      //   console.warn('dataChannel---onopen', event);
+      // };
+      // this.dataChannel.onerror = (event) => {
+      //   console.warn('dataChannel---onerror', event);
+      // };
+      // this.dataChannel.onmessage = (event) => {
+      //   console.log('dataChannel---onmessage', event);
+      // };
+      // this.peerConnection.addTransceiver('video', { direction: 'recvonly' });
+      // this.peerConnection.addTransceiver('audio', { direction: 'recvonly' });
       this.createConnect();
+      this.update();
     }
   }
 
@@ -431,5 +534,12 @@ export class WebRTCClass {
     this.dataChannel?.close();
     this.peerConnection = null;
     this.dataChannel = null;
+    this.update();
   }
+
+  // 更新store
+  update = () => {
+    const networkStore = useNetworkStore();
+    networkStore.updateRtcMap(this.roomId, this);
+  };
 }

+ 18 - 14
src/network/websocket.ts

@@ -36,6 +36,9 @@ export const wsMsgType = {
   full: 'full',
   /** 用户发送消息 */
   message: 'message',
+  offer: 'offer',
+  answer: 'answer',
+  candidate: 'candidate',
 };
 
 export enum statusEnum {
@@ -52,7 +55,7 @@ export enum statusEnum {
 
 export class WebSocketClass {
   socketIo: Socket | null = null;
-  status: statusEnum = statusEnum.connect;
+  status: statusEnum = statusEnum.disconnect;
 
   url = 'ws://localhost:3300';
   roomId = '-1';
@@ -97,19 +100,20 @@ export class WebSocketClass {
     //   console.log('用户离开房间', data);
     // });
 
-    // 用户发送消息
-    this.socketIo.on(wsMsgType.message, (data) => {
-      console.log('用户发送消息', data);
-    });
-
-    // 用户发送 offer
-    this.socketIo.on('offer', (data) => {
-      console.log('用户发送 offer', data);
-    });
-    // 用户发送 answer
-    this.socketIo.on('answer', (data) => {
-      console.log('用户发送 answer', data);
-    });
+    // // 用户发送消息
+    // this.socketIo.on(wsMsgType.message, (data) => {
+    //   console.log('用户发送消息', data);
+    // });
+
+    // // 用户发送 offer
+    // this.socketIo.on(wsMsgType.offer, (data) => {
+    //   console.log('用户发送 offer', data);
+    // });
+
+    // // 用户发送 answer
+    // this.socketIo.on(wsMsgType.answer, (data) => {
+    //   console.log('用户发送 answer', data);
+    // });
   }
 
   // 发送websocket消息

+ 1 - 4
src/router/index.ts

@@ -1,4 +1,3 @@
-import { outputStaticUrl } from 'script/constant';
 import { createRouter, createWebHistory } from 'vue-router';
 
 import type { RouteRecordRaw } from 'vue-router';
@@ -13,9 +12,7 @@ export const defaultRoutes: RouteRecordRaw[] = [
 ];
 const router = createRouter({
   routes: defaultRoutes,
-  history: createWebHistory(
-    outputStaticUrl(process.env.NODE_ENV === 'production')
-  ),
+  history: createWebHistory(),
 });
 
 export default router;

+ 267 - 47
src/views/home/index.vue

@@ -1,45 +1,81 @@
 <template>
   <div class="home-wrap">
-    <div class="left">
-      房间号:<input
-        v-model="roomId"
-        type="text"
-        placeholder="输入房间号"
-      />
-      <button
-        ref="joinRef"
-        class="join-btn"
-        @click="join"
-      >
-        进入
-      </button>
-      <button
-        ref="leaveRef"
-        class="join-btn"
-        disabled
-        @click="leave"
-      >
-        退出
-      </button>
-      <div>ws状态:{{ networkStore.wsMap.get(roomId!)?.status }}</div>
+    <div>
+      需求:
+      <div>1.房主的话,直接调用摄像头然后展示画面</div>
+      <div>
+        1.用户a进来了,默认不显示用户a的画面,用户a直接new
+        rtc,然后将自己的sdp房主,房主给他的rtc添加音视频轨,让用户a能看到房主的画面
+      </div>
     </div>
-    <div class="right">
-      <div>当前房间用户:</div>
-      <ul>
-        <li
-          v-for="item in userList"
-          :key="item"
+    <br />
+    <div class="content">
+      <div class="left">
+        房间号:<input
+          v-model="roomId"
+          type="text"
+          placeholder="输入房间号"
+        />
+        <button
+          ref="joinRef"
+          class="join-btn"
+          @click="join"
         >
-          {{ item }}
-        </li>
-      </ul>
+          进入
+        </button>
+        <button
+          ref="leaveRef"
+          class="join-btn"
+          disabled
+          @click="leave"
+        >
+          退出
+        </button>
+        <div>socketId:{{ getSocketId() }}</div>
+        <div>ws状态:{{ networkStore.wsMap.get(roomId!)?.status }}</div>
+        <div>rtcStatus:{{ networkStore.rtcMap.get(roomId!)?.rtcStatus }}</div>
+        <button @click="startAction">开始直播</button>
+        <div>
+          <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>
+      <div class="right">
+        <div>当前房间用户:</div>
+        <ul>
+          <li
+            v-for="item in userList"
+            :key="item"
+          >
+            {{ item }}
+            <button
+              v-if="item !== getSocketId()"
+              @click="sendOffer"
+            >
+              开始视频
+            </button>
+          </li>
+        </ul>
+      </div>
     </div>
   </div>
 </template>
 
 <script lang="ts" setup>
-import { ref } from 'vue';
+import { onMounted, ref } from 'vue';
 
+import { WebRTCClass } from '@/network/webRtc';
 import {
   WebSocketClass,
   statusEnum,
@@ -52,9 +88,82 @@ const networkStore = useNetworkStore();
 
 const joinRef = ref<HTMLButtonElement>();
 const leaveRef = ref<HTMLButtonElement>();
-const roomId = ref<string>();
+const roomId = ref<string>('123456');
 const instance = ref<WebSocketClass>();
 const userList = ref<string[]>([]);
+const muted = ref(true);
+const localVideoRef = ref<HTMLVideoElement>();
+const localStream = ref();
+
+function getSocketId() {
+  return networkStore.wsMap.get(roomId.value!)?.socketIo?.id;
+}
+
+interface IOffer {
+  socketId: string;
+  roomId: string;
+  sdp: any;
+}
+interface ICandidate {
+  socketId: string;
+  roomId: string;
+  candidate: string;
+  sdpMid: string | null;
+  sdpMLineIndex: number | null;
+}
+
+onMounted(() => {
+  localVideoRef.value?.addEventListener('loadstart', () => {
+    console.warn('视频流-loadstart');
+  });
+  localVideoRef.value?.addEventListener('loadedmetadata', () => {
+    console.warn('视频流-loadedmetadata');
+  });
+});
+
+// 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);
+      if (!localVideoRef.value) return;
+      localVideoRef.value.srcObject = event;
+      localStream.value = event;
+      // event.getTracks().forEach((track) => {
+      //   console.log('tracktrack', track, event);
+      //   networkStore.rtcMap.get(roomId.value)?.myAddTrack(track, event);
+      // });
+    })
+    .catch((err) => {
+      console.log('getUserMedia失败', err);
+    });
+}
+async function sendOffer() {
+  if (!instance.value) return;
+  const localDesc = await networkStore.rtcMap.get(roomId.value)?.createOffer();
+  console.warn('发送offer');
+  instance.value.socketIo?.emit(wsMsgType.offer, {
+    socketId: getSocketId(),
+    roomId: roomId.value,
+    sdp: localDesc,
+  });
+}
+async function sendAnswer(sdp) {
+  if (!instance.value) return;
+  console.warn('发送answer');
+  const createOffer = await networkStore.rtcMap.get(roomId.value)?.rtcStatus
+    .createOffer;
+  if (!createOffer) {
+    await networkStore.rtcMap.get(roomId.value)?.createOffer();
+  }
+  instance.value.socketIo?.emit(wsMsgType.answer, {
+    socketId: getSocketId(),
+    roomId: roomId.value,
+    sdp,
+  });
+}
 
 function join() {
   console.log('join的房间号', roomId.value);
@@ -71,7 +180,8 @@ function join() {
   // websocket连接成功
   instance.value.socketIo.on(wsConnectStatus.connect, () => {
     if (!instance.value) return;
-    console.log('websocket连接成功', instance.value.socketIo?.id);
+    console.log('【websocket】websocket连接成功', instance.value.socketIo?.id);
+    if (!instance.value) return;
     instance.value.status = statusEnum.connect;
     if (joinRef.value && leaveRef.value) {
       joinRef.value.disabled = true;
@@ -85,36 +195,139 @@ function join() {
 
   // websocket连接断开
   instance.value.socketIo.on(wsConnectStatus.disconnect, () => {
+    console.log('【websocket】websocket连接断开', instance.value);
     if (!instance.value) return;
-    console.log('websocket连接断开', instance.value);
     instance.value.status = statusEnum.disconnect;
     instance.value.update();
   });
 
+  // 收到offer
+  instance.value.socketIo.on(wsMsgType.offer, async (data: IOffer) => {
+    console.warn('【websocket】收到offer', data);
+    if (!instance.value) return;
+    const rtc = networkStore.rtcMap.get(roomId.value);
+    if (data.socketId !== getSocketId()) {
+      console.log('收到offer,并且这个offer不是我发的');
+      // const createOffer = await rtc?.rtcStatus.createOffer;
+      // if (!createOffer) {
+      //   console.log('没有创建offer成功');
+      rtc?.setRemoteDescription(data.sdp);
+      const sdp = await rtc?.createAnswer();
+      console.log(sdp, 'lllll');
+      instance.value.socketIo?.emit(wsMsgType.answer, {
+        socketId: getSocketId(),
+        roomId: roomId.value,
+        sdp,
+      });
+      // }
+      // setTimeout(() => {
+      //   rtc?.setRemoteDescription(data.sdp);
+      // }, 500);
+    } else {
+      console.log('收到offer,并且这个offer是我发的');
+    }
+  });
+
+  // 收到answer
+  instance.value.socketIo.on(wsMsgType.answer, async (data: IOffer) => {
+    console.warn('【websocket】收到answer', data);
+    if (!instance.value) return;
+    if (!networkStore.rtcMap.get(roomId.value)?.rtcStatus.createOffer) return;
+    if (data.socketId !== getSocketId()) {
+      console.log('不是我发的answer');
+      await networkStore.rtcMap
+        .get(roomId.value)
+        ?.setRemoteDescription(data.sdp);
+      // sendAnswer(networkStore.rtcMap.get(roomId.value)?.localDescription);
+    } else {
+      console.log('是我发的answer');
+      // sendAnswer(data.sdp);
+    }
+  });
+
+  // 收到candidate
+  instance.value.socketIo.on(wsMsgType.candidate, (data: ICandidate) => {
+    console.warn('【websocket】收到candidate', data);
+    if (!instance.value) return;
+    const rtc = networkStore.rtcMap.get(roomId.value);
+    if (!rtc) return;
+    if (data.socketId !== getSocketId()) {
+      console.log('不是我发的candidate');
+      setTimeout(() => {
+        console.log(data.sdpMid, data.sdpMLineIndex, 888888);
+        const candidate = new RTCIceCandidate({
+          sdpMLineIndex: data.sdpMLineIndex,
+          candidate: data.candidate,
+        });
+        console.log('addIceCandidateaddIceCandidate');
+        rtc.peerConnection
+          ?.addIceCandidate(candidate)
+          .then(() => {
+            console.log('candidate成功');
+          })
+          .catch((err) => {
+            console.error('candidate失败', err);
+          });
+      }, 1000);
+    } else {
+      console.log('是我发的candidate');
+    }
+  });
+
   // 用户加入房间
   instance.value.socketIo.on(wsMsgType.join, (data) => {
+    console.log('【websocket】用户加入房间', data);
     if (!instance.value) return;
-    console.log('用户加入房间', data);
   });
 
   // 用户加入房间
   instance.value.socketIo.on(wsMsgType.joined, (data) => {
+    console.log('【websocket】用户加入房间完成', data);
     if (!instance.value) return;
-    console.log('用户加入房间完成', data);
     userList.value.push(instance.value.socketIo?.id!);
+    console.warn('开始new WebRTCClass');
+    const rtc = new WebRTCClass({ roomId: roomId.value });
+    networkStore.updateRtcMap(roomId.value, rtc);
+    // const localDesc = await rtc.createOffer();
+    // console.log(localDesc);
+    // instance.value.socketIo?.emit(wsMsgType.offer, {
+    //   sdp: localDesc,
+    // });
+    if (userList.value.length > 1) {
+      sendOffer();
+    }
   });
 
   // 其他用户加入房间
   instance.value.socketIo.on(wsMsgType.otherJoin, (data) => {
+    console.log('【websocket】其他用户加入房间', data);
     if (!instance.value) return;
-    console.log('其他用户加入房间', data);
     userList.value.push(data.socketId);
+    // sendAnswer(networkStore.rtcMap.get(roomId.value)?.localDescription);
+    console.log(localStream.value.getTracks(), 888);
+    localStream.value.getTracks().forEach((track) => {
+      console.log('tracktrack', track, localStream.value);
+      networkStore.rtcMap
+        .get(roomId.value)
+        ?.myAddTrack(track, localStream.value);
+    });
+    sendOffer();
   });
 
   // 用户离开房间
   instance.value.socketIo.on(wsMsgType.leave, (data) => {
+    console.log('【websocket】用户离开房间', data);
     if (!instance.value) return;
-    console.log('用户离开房间', data);
+    instance.value.socketIo?.emit(wsMsgType.leave, {
+      roomId: instance.value.roomId,
+    });
+  });
+
+  // 用户离开房间完成
+  instance.value.socketIo.on(wsMsgType.leaved, (data) => {
+    console.log('【websocket】用户离开房间完成', data);
+    if (!instance.value) return;
+    instance.value.close();
   });
 }
 
@@ -124,21 +337,28 @@ function leave() {
     leaveRef.value.disabled = true;
   }
   if (!instance.value) return;
-  instance.value.close();
+  instance.value.socketIo?.emit(wsMsgType.leave, {
+    roomId: instance.value.roomId,
+  });
 }
 </script>
 
 <style lang="scss" scoped>
 .home-wrap {
-  display: flex;
   padding: 10px;
-  .left {
-    margin-right: 50px;
-    .join-btn {
-      margin-left: 10px;
-    }
+  video {
+    width: 500px;
   }
-  .right {
+  .content {
+    display: flex;
+    .left {
+      margin-right: 50px;
+      .join-btn {
+        margin-left: 10px;
+      }
+    }
+    .right {
+    }
   }
 }
 </style>