caner 2 years ago
parent
commit
9ca84ac6aa
4 changed files with 704 additions and 0 deletions
  1. 534 0
      index.vue
  2. 130 0
      meeting.service.ts
  3. 18 0
      package.json
  4. 22 0
      yarn.lock

+ 534 - 0
index.vue

@@ -0,0 +1,534 @@
+<template>
+  <n-modal
+    :show="props.modelValue"
+    preset="dialog"
+    class="meeting"
+    :show-icon="false"
+    @close="close"
+  >
+    <template #header>
+      <div class="meeting-header">
+        刘明明创建视频会议 | 01:35:26 | 5人参会中
+      </div>
+    </template>
+    <div class="meeting-content">
+      <div class="meeting-content-left">
+        <div class="meeting-content-left-top">
+          <template
+            v-for="s in 20"
+            :key="s"
+          >
+            <div class="meeting-content-left-top-item">
+              <div class="meeting-content-left-top-item-names">
+                刘
+              </div>
+              <div class="meeting-content-left-top-item-info">
+                <p>刘明明</p>
+                <div>
+                  <span>西南交大研究院</span>
+                  <Icon
+                    name="mic"
+                    :size="23"
+                  />
+                </div>
+              </div>
+            </div>
+          </template>
+        </div>
+        <div class="meeting-content-left-bottom">
+          <div class="meeting-content-left-bottom-item">
+            <icon
+              :size="28"
+              name="mic"
+            />
+            <p>静音</p>
+          </div>
+          <div class="meeting-content-left-bottom-item">
+            <icon
+              :size="28"
+              name="camera"
+            />
+            <p>摄像头</p>
+          </div>
+
+          <div class="meeting-content-left-bottom-btn">
+            <n-button
+              type="error"
+              @click="close"
+            >
+              结束会议
+            </n-button>
+          </div>
+        </div>
+      </div>
+      <div class="meeting-content-right">
+        <n-collapse
+          arrow-placement="right"
+          accordion
+          :default-expanded-names="['1']"
+        >
+          <n-collapse-item name="1">
+            <template #header>
+              <div class="meeting-content-right-header">
+                会议成员12
+              </div>
+            </template>
+            <div class="meeting-content-right-content">
+              <div class="meeting-content-right-content-search">
+                <n-input
+                  round
+                  placeholder="请输入内容"
+                >
+                  <template #prefix>
+                    <icon
+                      name="serch"
+                      :size="18"
+                    />
+                  </template>
+                </n-input>
+              </div>
+
+              <template
+                v-for="s in 20"
+                :key="s"
+              >
+                <div class="meeting-content-right-content-item">
+                  <div class="meeting-content-right-content-item-icon">
+                    刘
+                  </div>
+                  <div class="meeting-content-right-content-item-info">
+                    <div>
+                      <p>
+                        <span>刘明明</span>
+                        <span class="isActive">主持</span>
+                      </p>
+                      <p>行政区域1</p>
+                    </div>
+                    <div>
+                      <span>到场</span>
+                      <icon
+                        name="mic"
+                        :size="20"
+                      />
+                    </div>
+                  </div>
+                </div>
+              </template>
+            </div>
+          </n-collapse-item>
+          <n-collapse-item name="2">
+            <template #header>
+              <div class="meeting-content-right-header">
+                文件列表
+              </div>
+            </template>
+            <div class="meeting-content-right-content">
+              <template
+                v-for="s in 12"
+                :key="s"
+              >
+                <div class="meeting-content-right-content-item meeting-content-right-content-file">
+                  <div class="meeting-content-right-content-item-icon">
+                    图标
+                  </div>
+                  <div class="meeting-content-right-content-item-info">
+                    <div>
+                      <p>
+                        <span>我是文档名称描述.pdf</span>
+                      </p>
+                      <p>刘明明 04.28 13:08:29</p>
+                    </div>
+                    <div>
+                      <icon
+                        name="downLoad"
+                        :size="20"
+                      />
+                      <icon
+                        name="del"
+                        :size="20"
+                      />
+                    </div>
+                  </div>
+                </div>
+              </template>
+            </div>
+          </n-collapse-item>
+          <n-collapse-item name="3">
+            <template #header>
+              <div class="meeting-content-right-header">
+                会议记录
+              </div>
+            </template>
+            <div class="meeting-content-right-content">
+              <div class="meeting-content-right-content-top">
+                <template
+                  v-for="s in 20"
+                  :key="s"
+                >
+                  <div class="meeting-content-right-content-top-item">
+                    <div>头像</div>
+                    <div>
+                      <div>
+                        <span>刘明明</span>
+                        <span>07.10 13:00:09</span>
+                      </div>
+                      <div>
+                        我是会议记录描述我是会议记录描述我是会议记录描述我是会议记录描述我是会议记录描述我是会议记录描述我是会议记录描述我是会议记录描述。
+                      </div>
+                    </div>
+                  </div>
+                </template>
+              </div>
+              <div class="meeting-content-right-content-bottom">
+                <n-input
+                  type="text"
+                  placeholder="请输入内容"
+                />
+                <n-button type="info">
+                  发送
+                </n-button>
+              </div>
+            </div>
+          </n-collapse-item>
+        </n-collapse>
+      </div>
+    </div>
+  </n-modal>
+</template>
+<script setup lang="ts">
+import { onMounted } from 'vue'
+import MeetingService from './meeting.service'
+
+const meetingService = new MeetingService()
+const props = defineProps<{
+  modelValue: boolean,
+  userList:any[]
+}>()
+const emit = defineEmits([ 'update:modelValue' ])
+
+function close() {
+  meetingService.exitRoom()
+  emit('update:modelValue', false)
+}
+
+onMounted(() => {
+  meetingService._init()
+})
+</script>
+<style lang="less" scoped>
+.meeting {
+  div {
+    box-sizing: border-box;
+    user-select: none;
+  }
+
+  &-header {
+    font-weight: 400;
+    font-size: 14px;
+    color: #FFFFFF;
+  }
+
+  &-content {
+    display: flex;
+    justify-content: space-between;
+    padding-top: 10px;
+    height: 100%;
+
+    &-left {
+      flex: 1;
+      margin-right: 20px;
+
+      &-top {
+        display: flex;
+        flex-wrap: wrap;
+        overflow-y: auto;
+        overflow-x: hidden;
+        height: calc(100% - 96px);
+        margin-bottom: 20px;
+
+        &-item {
+          width: 320px;
+          height: 288px;
+          background: #202020;
+          margin-left: 20px;
+          margin-bottom: 20px;
+          position: relative;
+          border-radius: 5px;
+          overflow: hidden;
+
+          &:nth-last-child(-n+4) {
+            margin-bottom: 0;
+          }
+
+          &-info {
+            position: absolute;
+            bottom: 0;
+            left: 0;
+            width: 100%;
+            font-weight: 400;
+            font-size: 14px;
+            color: #FFFFFF;
+            padding: 0 10px 8px 10px;
+
+            &>div {
+              display: flex;
+              align-items: center;
+              justify-content: space-between;
+
+              &>svg {
+                cursor: pointer;
+              }
+            }
+          }
+
+          &-names {
+            position: absolute;
+            left: 50%;
+            top: 50%;
+            transform: translate(-50%, -50%);
+            background: #2791FE;
+            width: 110px;
+            height: 110px;
+            border-radius: 50%;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            font-size: 20px;
+            color: #FFFFFF;
+            overflow: hidden;
+          }
+        }
+      }
+
+      &-bottom {
+        flex: 1;
+        height: 76px;
+        background: #202020;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        position: relative;
+
+        &-item {
+          margin-left: 20px;
+          text-align: center;
+        }
+
+        &-btn {
+          position: absolute;
+          right: 50px;
+        }
+      }
+    }
+
+    &-right {
+      width: 420px;
+      background: #2d2d2d;
+      height: 100%;
+      overflow-y: auto;
+      overflow-x: hidden;
+
+      .n-collapse .n-collapse-item:not(:first-child) {
+        border: none;
+        margin-top: 10px;
+      }
+
+      :deep(.n-collapse .n-collapse-item .n-collapse-item__header) {
+        padding: 10px 15px;
+      }
+
+      :deep(.n-collapse .n-collapse-item .n-collapse-item__header .n-collapse-item-arrow) {
+        color: white;
+        font-size: 23px;
+      }
+
+      &-header {
+        width: calc(100% - 20px);
+        font-weight: 500;
+        font-size: 15px;
+        color: #FFFFFF;
+      }
+
+      &-content {
+        padding: 10px 15px;
+        max-height: 672px;
+        overflow-y: auto;
+        overflow-x: hidden;
+        background: #202020;
+
+        &-search {
+          :deep(.n-input .n-input-wrapper) {
+            background: #1A1A1A;
+          }
+
+          :deep(.n-input .n-input__border, .n-input .n-input__state-border) {
+            border: 1px solid rgba(220, 222, 226, 0.2);
+          }
+        }
+
+        &-item {
+          display: flex;
+          align-items: center;
+          margin-top: 15px;
+
+          &-icon {
+            width: 55px;
+            height: 55px;
+            background: #2791FE;
+            border-radius: 50%;
+            overflow: hidden;
+            margin-right: 15px;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            color: white;
+          }
+
+          &-info {
+            font-weight: 400;
+            font-size: 16px;
+            color: #FFFFFF;
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            flex: 1;
+
+            &>div:first-child {
+              &>p:last-child {
+                font-size: 14px;
+                color: #C4C8CE;
+              }
+
+              .isActive {
+                background: #2791FE;
+                border-radius: 2px;
+                font-size: 14px;
+                padding: 1px 5px;
+                margin-left: 10px;
+              }
+            }
+
+            &>div:last-child {
+              display: flex;
+              align-items: center;
+              font-weight: 400;
+              font-size: 14px;
+              color: #2791FE;
+
+              svg {
+                margin-left: 5px;
+              }
+            }
+
+            svg {
+              cursor: pointer;
+            }
+          }
+        }
+
+        &-file {
+          border-bottom: solid 1px rgba(255, 255, 255, 0.2);
+          margin: 0;
+          padding: 10px 0;
+
+          &>div:first-child {
+            border-radius: 5px;
+          }
+        }
+
+        &-top {
+          height: 604px;
+          overflow-y: auto;
+          overflow-x: hidden;
+
+          &-item {
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            margin-bottom: 10px;
+
+            &>div:first-child {
+              width: 55px;
+              height: 55px;
+              border-radius: 4px;
+              background: #2791FE;
+              color: white;
+              display: flex;
+              align-items: center;
+              justify-content: center;
+            }
+
+            &>div:last-child {
+              width: calc(100% - 70px);
+              font-weight: 400;
+              font-size: 14px;
+              color: #FFFFFF;
+
+              &>div:first-child {
+                display: flex;
+                align-items: center;
+                justify-content: space-between;
+
+                &>span:first-child {
+                  font-size: 16px;
+                }
+
+                &>span:last-child {
+                  color: #C4C8CE;
+                }
+              }
+            }
+          }
+
+        }
+
+        &-bottom {
+          display: flex;
+          align-items: center;
+          justify-content: space-between;
+          border-top: solid 1px rgba(255, 255, 255, 0.2);
+          padding-top: 13px;
+
+          button {
+            margin-left: 10px;
+          }
+
+          :deep(.n-input) {
+            background: #1A1A1A;
+          }
+
+          :deep(.n-input .n-input__border) {
+            border: 1px solid rgba(220, 222, 226, 0.2);
+          }
+
+          :deep(.n-input .n-input__placeholder) {
+            color: var(--n-text-color);
+          }
+        }
+      }
+    }
+  }
+}
+</style>
+<style lang="less">
+.meeting {
+  width: 1800px !important;
+  height: 900px;
+  background: black;
+  padding: 0;
+
+}
+
+.n-dialog .n-dialog__close {
+  margin: 10px 10px 0 0;
+}
+
+.n-dialog .n-dialog__title {
+  background: #202020;
+  padding: 10px;
+}
+
+.n-dialog .n-dialog__content {
+  height: calc(100% - 50px);
+}
+</style>

+ 130 - 0
meeting.service.ts

@@ -0,0 +1,130 @@
+import TRTC from 'trtc-sdk-v5'
+import { useNotification } from 'naive-ui'
+class meetingService {
+  private trtc: any = null
+
+  private notification = useNotification()
+
+  // 监测设备
+  private async detectionDevice() {
+    let status = true
+    const micList = await TRTC.getMicrophoneList()
+    const speakerList = await TRTC.getSpeakerList()
+    const hasMicrophoneDevice = micList.length > 0
+    const hasSpeakerDevice = speakerList.length > 0
+    if (!hasMicrophoneDevice || !hasSpeakerDevice) {
+      status = false
+    }
+    if (hasMicrophoneDevice) {
+      try {
+        await this.trtc.startLocalAudio({ publish: false })
+        await this.trtc.enableAudioVolumeEvaluation(250)
+        await this.trtc.stopLocalAudio()
+      } catch (error) {
+        status = false
+      }
+    }
+    return status
+  }
+
+  // 加入房间+发布local video|audio
+  public async joinRoom(options: { userId: string, roomId: number }, videoDomID: string) {
+    if (!this.trtc) return
+    await this.trtc.enterRoom(options)
+    // 推送音视频
+    await this.trtc.startLocalVideo({
+      view: document.getElementById(videoDomID) // 在 DOM 中的 elementId 为 localVideo 的标签上预览视频。
+    })
+    // 采集默认麦克风并发布
+    await this.trtc.startLocalAudio()
+  }
+
+  // 退出房间
+  public async exitRoom() {
+    if (!this.trtc) return
+    await this.trtc.exitRoom()
+    await this.trtc.stopLocalVideo()
+    await this.trtc.stopLocalAudio()
+    this.destroy()
+  }
+
+  // 是否静音远程用户
+  public async stopRomoteAudio(userId:string, mute:boolean) {
+    if (!this.trtc) return
+    await this.trtc.muteRemoteAudio(userId, mute)
+  }
+
+  // 关闭本地视频推送
+  public async stopLocalVideo() {
+    if (!this.trtc) return
+    await this.trtc.stopLocalVideo()
+  }
+
+  // 初始化
+  public async _init() {
+    if (this.trtc) this.destroy()
+    // creta
+    this.trtc = TRTC.create()
+    // 检测设备
+    const status = await this.detectionDevice()
+    if (!status) return this.notification.error({ content: '请检查你的麦克风或扬声器是否正常' })
+    // 加入房间
+
+    // 监听房间事件
+    this.trtc.on(TRTC.EVENT.ERROR, this.handleError.bind(this))
+    // this.trtc.on(TRTC.EVENT.KICKED_OUT, this.handleKickedOut.bind(this))
+    this.trtc.on(TRTC.EVENT.REMOTE_USER_ENTER, this.handleRemoteUserEnter.bind(this))
+    this.trtc.on(TRTC.EVENT.REMOTE_USER_EXIT, this.handleRemoteUserExit.bind(this))
+    this.trtc.on(TRTC.EVENT.REMOTE_AUDIO_UNAVAILABLE, this.handleRemoteAudioUnavailable.bind(this))
+    this.trtc.on(TRTC.EVENT.REMOTE_AUDIO_AVAILABLE, this.handleRemoteAudioAvailable.bind(this))
+    this.trtc.on(TRTC.EVENT.REMOTE_VIDEO_AVAILABLE, this.handleRemoteVideoAvailable.bind(this))
+    this.trtc.on(TRTC.EVENT.REMOTE_VIDEO_UNAVAILABLE, this.handleRemoteVideoUnavailable.bind(this))
+    // this.trtc.on(TRTC.EVENT.AUDIO_VOLUME, this.handleAudioVolume.bind(this))
+    return true
+  }
+
+  // 错误信息
+  private handleError(error: { message: string }) {
+    this.notification.error({
+      content: error.message,
+      duration: 2000
+    })
+  }
+
+  // 远程用户加入
+  private handleRemoteUserEnter(event: any) {
+    console.log('远程用户加入', event)
+  }
+
+  // 远程用户离开
+  private handleRemoteUserExit(event: any) {
+    console.log('远程用户离开', event)
+  }
+
+  // 远程用户音频不可用
+  private handleRemoteAudioUnavailable(event: any) {
+    console.log('远程用户音频不可用', event)
+  }
+
+  // 远程用户音频可用
+  private handleRemoteAudioAvailable(event: any) {
+    console.log('远程用户音频可用', event)
+  }
+
+  // 远程用户视频不可用
+  private handleRemoteVideoUnavailable(event: any) {
+    console.log('远程用户视频不可用', event)
+  }
+
+  // 远程用户视频可用
+  private handleRemoteVideoAvailable(event: any) {
+    console.log('远程用户视频可用', event)
+  }
+
+  private destroy() {
+    if (this.trtc) this.trtc.destroy()
+    this.trtc = null
+  }
+}
+
+export default meetingService

+ 18 - 0
package.json

@@ -0,0 +1,18 @@
+{
+  "name": "trtc-meeting-demo",
+  "version": "1.0.0",
+  "description": "腾讯视频会议模版",
+  "main": "index.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "repository": {
+    "type": "git",
+    "url": "https://git.caner.top/Caner/TRTC-Meeting-Demo.git"
+  },
+  "author": "",
+  "license": "ISC",
+  "dependencies": {
+    "trtc-sdk-v5": "^5.4.3"
+  }
+}

+ 22 - 0
yarn.lock

@@ -0,0 +1,22 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+sdp@^3.2.0:
+  version "3.2.0"
+  resolved "https://registry.npmmirror.com/sdp/-/sdp-3.2.0.tgz#8961420552b36663b4d13ddba6f478d1461896a5"
+  integrity sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw==
+
+trtc-sdk-v5@^5.4.3:
+  version "5.4.3"
+  resolved "https://registry.npmmirror.com/trtc-sdk-v5/-/trtc-sdk-v5-5.4.3.tgz#58e7879e086bb68dc4eb5dcf987fd678dadd5fe6"
+  integrity sha512-DghxF2moIt3aiiM+KIHktqZGXxh31TXTpAhVRiOPdOWWajI/F5wmYwt5/EcyyBdsQU4QOA9q9tDdvG4dNX30qQ==
+  dependencies:
+    webrtc-adapter "^8.2.3"
+
+webrtc-adapter@^8.2.3:
+  version "8.2.3"
+  resolved "https://registry.npmmirror.com/webrtc-adapter/-/webrtc-adapter-8.2.3.tgz#85e5e52ea68e808be8d6db85e338aa5c95e80022"
+  integrity sha512-gnmRz++suzmvxtp3ehQts6s2JtAGPuDPjA1F3a9ckNpG1kYdYuHWYpazoAnL9FS5/B21tKlhkorbdCXat0+4xQ==
+  dependencies:
+    sdp "^3.2.0"