vue集成zego即构实现实时音视频

主播端
image.png
观众端
image.png

第一步,肯定是要先实现这样的一个三分屏直播间样式,直接贴代码了

<template>
  <div class="main">
    <div class="row">
      <div class="left" :style="obj">
        <!-- top -->
        <div class="top">
          <span>频道号:2066909 <i>无直播</i> </span>
          <span>经典儿童言语康复案例解析</span>
          <span>
            <i>网络延时:10ms</i>
            <i style="padding: 0 2px;">丢包率:{{videoPacketsLostRate}}%</i>
            <i> <span></span><span class="span"></span><span></span>网络状态</i>
          </span>
        </div>
        <!-- bottom -->
        <div class="bottom">
          <div class="bottom_l">
            <div class="t">
              <!-- tab -->
              <div class="baiban">
                <i class="el-icon-edit-outline" style="font-size:24px;"></i>
                <p>白板</p>
              </div>
              <div class="baiban">
                <i class="el-icon-document" style="font-size:24px;"></i>
                <p>课件</p>
              </div>
              <div class="baiban">
                <i class="el-icon-video-camera" style="font-size:24px;"></i>
                <p>多媒体</p>
              </div>
              <div class="baiban">
                <i class="el-icon-monitor" style="font-size:24px;"></i>
                <p>屏幕共享</p>
              </div>
              <div class="baiban yingyong" @mouseenter="onMousehoverYing($event)" @mouseleave="onMouseleaveYing($event)">
                <i class="el-icon-copy-document" style="font-size:24px;"></i>
                <p>应用</p>
              </div>
            </div>
            <div class="b">
              <span>
                <el-button class="oneBtn" type="primary" @click="startPublishingStream" circle>开始</el-button>
              </span>
              <span>
                <el-button type="primary" class="btn two" size="small" icon="el-icon-share" circle></el-button>
              </span>
              <span>
                <el-button type="primary" class="btn" size="small" icon="el-icon-setting" circle></el-button>
              </span>
            </div>
          </div>
          <div class="bottom_r">
            <!-- 应用bar -->
            <div class="bar" v-show="showBar" @mouseenter="onMousehoverYing($event)" @mouseleave="onMouseleaveYing($event)" style="display: none;">
              <!-- tab -->
              <div class="baiban">
                <i class="el-icon-location-outline" style="font-size:24px;"></i>
                <p>签到</p>
              </div>
              <div class="baiban">
                <i class="el-icon-date" style="font-size:24px;"></i>
                <p>公告</p>
              </div>
              <div class="baiban">
                <i class="el-icon-document-copy" style="font-size:24px;"></i>
                <p>问卷</p>
              </div>
              <div class="baiban">
                <i class="el-icon-document-checked" style="font-size:24px;"></i>
                <p>答题卡</p>
              </div>
              <div class="baiban">
                <i class="el-icon-trophy" style="font-size:24px;"></i>
                <p>抽奖</p>
              </div>
            </div>
            <!-- 白板区域 -->
            白板区域 <br>
            白板区域 <br>
            白板区域 <br>
            白板区域 <br>
            白板区域 <br>
            白板区域 <br>

          </div>
        </div>
      </div>
      <div class="right" :style="obj">
        <!-- avatar -->
        <div class="avatar" id="local_stream" @mouseenter="onMousehoverEnv($event)" @mouseleave="onMouseleaveEnv($event)">
          <!-- toobar -->
          <div class="toobar" style="display: none;">
            <span>
              <img :src="getItemIcon1()" alt="" v-show='showTagVideo' @click="closeVideo" class="imgIcon" width="30">
              <img :src="getItemIcon2()" alt="" v-show='!showTagVideo' @click="openVideo" class="imgIcon" width="30">
            </span>
            <span style="margin:0 15px;">
              <img :src="getItemIcon3()" class="imgIcon" alt="" width="30">
            </span>
            <span>
              <img :src="getItemIcon4()" class="imgIcon" alt="" width="30">
            </span>
          </div>
          <!-- 底部状态栏 -->
          <div class="b_toobar">
            <span>(我)2076282</span>
            <span>
              <i class="el-icon-bell" style="font-size:20px;margin-right:5px;"></i>
              <i class="el-icon-microphone" style="font-size:20px;"></i>
            </span>
          </div>
          <!-- avatar_live.png -->
          <div class="live" v-show="!showTagVideo">
            <img src="http://erkong.ybc365.com/b4576202012231731231009.png" alt="">
          </div>
          <!-- 视频区域 -->
          <video v-show="showTagVideo" autoplay muted id='video' :src-object.prop="stream" width="100%" height="100%"></video>
        </div>
        <!-- tabs -->
        <ul class="tabs">
          <li class="li-tab" v-for="(item,index) in tabsParam" @click="toggleTabs(index)" :class="{active:index==nowIndex}">
            {{item =='成员'?item+`(${index})`:item}}</li>
        </ul>
        <div class="divTab1" v-show="nowIndex===0">
          <div class="content">
            content
          </div>
          <div class="send">
            <div class="check">
              <span>
                <i class="el-icon-picture-outline-round biaoqingicon"></i>
              </span>
              <span>
                <el-checkbox v-model="checked">屏蔽打赏</el-checkbox>
              </span>
            </div>
            <div class="input">
              <el-input class="textarea" type="textarea" placeholder="我也来参与一下互动" v-model="textarea" maxlength="200" show-word-limit rows="3"
                        resize='none'>
              </el-input>
              <div class="sendBtn">
                <div>聊天室已开启</div>
                <div>
                  <span style="margin-right:15px;">0/200</span>
                  <span class="senBtntxt">
                    发送
                  </span>
                </div>
              </div>
            </div>
          </div>
        </div>
        <div class="divTab2" v-show="nowIndex===1">
          22
        </div>
      </div>
    </div>
  </div>
</template>
<script>
const controlBtnList = [
  {
    name: 'camera',
    cnName: '摄像头',
    imgSrc: {
      open: require('../assets/icons/room/mumber_camer.svg'),
      close: require('../assets/icons/room/mumber_camer_close.svg')
    }
  },
  {
    name: 'mic',
    cnName: '麦克风',
    imgSrc: {
      open: require('../assets/icons/room/mumber_micophone.svg'),
      close: require('../assets/icons/room/mumber_micophone_close.svg')
    }
  },

]
export default {
  data() {
    return {
      showTagVideo: true,
      showBar: false,
      checked: false,
      textarea: "",
      tabsParam: ['聊天', '成员'],
      nowIndex: 0,//默认第一个tab为激活状态
      obj: {
        height: (document.documentElement.clientHeight || document.body.clientHeight) + 'px',
      },
      controlBtnList,
      videoPacketsLostRate: 0
    }
  },
  methods: {
    getItemIcon1() {
      return this.controlBtnList[0].imgSrc.open
    },
    getItemIcon2() {
      return this.controlBtnList[0].imgSrc.close
    },
    getItemIcon3() {
      return this.controlBtnList[1].imgSrc.open
    },
    getItemIcon4() {
      return this.controlBtnList[1].imgSrc.close
    },
    onMousehoverEnv(e) {
      e.target.parentElement.querySelector(".toobar").style.display = "block"
    },
    onMouseleaveEnv(e) {
      e.target.parentElement.querySelector(".toobar").style.display = "none"
    },
    onMousehoverYing(e) {
      this.showBar = true
    },
    onMouseleaveYing(e) {
      this.showBar = false
    },
    toggleTabs(index) {
      this.nowIndex = index;
    },
    // 关闭摄像头
    closeVideo() {
      this.showTagVideo = !this.showTagVideo
      this.$message.error('摄像头已关闭,观众将看不到您的画面')
    },
    // 启用摄像头
    openVideo() {
      this.showTagVideo = !this.showTagVideo
      this.$message.success('摄像头已打开')
    },

  },
  mounted() {
    window.onresize = () => {
      return (() => {
        this.obj.height = (document.documentElement.clientHeight
          || document.body.clientHeight) + 'px'
      })()
    }
  },
  watch: {
    showBar() {
      if (this.showBar) {
        document.querySelector(".yingyong").style.color = "#3595fb"
        document.querySelector(".yingyong").style.background = "#363644"
      } else {
        document.querySelector(".yingyong").style.color = ""
        document.querySelector(".yingyong").style.background = ""
      }
    }
  }
}
</script>
<style lang="scss" scoped>
.main {
  .row {
    display: flex;
    .left {
      background: #eee;
      min-height: 650px;
      flex: 1;
      min-width: 1000px;
      .top {
        height: 49px;
        background: #191a1c;
        display: flex;
        justify-content: space-between;
        align-items: center;
        color: #adadc0;
        font-size: 14px;
        padding: 0 15px;
        span {
          &:first-child {
            i {
              font-style: normal;
              border: 1px solid #adadc0;
              border-radius: 2px;
              padding: 0 8px;
              margin-left: 16px;
              color: #fff;
            }
          }
          &:nth-child(2) {
            color: #fff;
            font-size: 16px;
          }
          &:nth-child(3) {
            i {
              font-style: normal;
              &:nth-child(2) {
                margin: 0 12px;
              }
              &:nth-child(3) {
                span {
                  display: inline-block;
                  width: 1px;
                  background: #5fb430;
                  &:first-child {
                    height: 4px;
                  }
                  &:nth-child(2) {
                    height: 7px;
                  }
                  &:nth-child(3) {
                    height: 10px;
                    margin-right: 5px;
                  }
                }
                .span {
                  margin: 0 3px;
                }
              }
            }
          }
        }
      }
      .bottom {
        display: flex;
        height: calc(100% - 49px);
        width: 100%;
        font-size: 13.5px;
        .bottom_l {
          background: #191a1c;
          height: 100%;
          width: 80px;
          color: #adadc0;
          display: flex;
          flex-direction: column;
          justify-content: space-between;
          text-align: center;
          .t {
            width: 100%;
            height: 50%;
            p {
              margin: 0;
            }
            .baiban {
              width: 100%;
              height: 75px;
              display: flex;
              justify-content: center;
              align-items: center;
              flex-direction: column;
              p {
                margin-top: 6px;
              }
              &:hover {
                background: #363644;
                cursor: pointer;
                color: #3595fb;
              }
            }
          }
          .b {
            width: 100%;
            display: flex;
            flex-direction: column;
            justify-content: space-around;
            padding: 15px 0;
            .oneBtn {
              width: 55px;
              height: 55px;
            }
            .btn {
              background: #26272e;
              border-color: #26272e;
              font-size: 16px;
            }
            .two {
              margin: 15px 0;
            }
          }
        }
        .bottom_r {
          flex: 1;
          background: #f5f7ff;
          height: 100%;
          position: relative;
          .bar {
            width: 80px;
            background: #363644;
            height: 100%;
            color: #adadc0;
            position: absolute;
            top: 0px;
            p {
              margin: 0;
            }
            .baiban {
              width: 100%;
              height: 75px;
              display: flex;
              justify-content: center;
              align-items: center;
              flex-direction: column;
              p {
                margin-top: 6px;
              }
              &:hover {
                background: #363644;
                cursor: pointer;
                color: #fff;
              }
            }
          }
        }
      }
    }
    .right {
      background: #191a1c;
      min-height: 650px;
      min-width: 256px;
      width: 256px;
      .avatar {
        width: 100%;
        height: 168px;
        background: #363636;
        color: #fff;
        position: relative;
        box-shadow: inset 0px -6px 18px -10px #000;
        .toobar {
          position: absolute;
          bottom: 0;
          top: 0;
          left: 0;
          right: 0;
          background: rgba(0, 0, 0, 0.5);
          text-align: center;
          line-height: 150px;
          z-index: 99;
          .imgIcon {
            cursor: pointer;
          }
        }
        .b_toobar {
          width: 100%;
          position: absolute;
          bottom: 0;
          font-size: 12px;
          padding: 2px 4px;
          display: flex;
          align-items: center;
          justify-content: space-between;
          z-index: 98;
        }
        .live {
          position: absolute;
          bottom: 0;
          top: 25%;
          left: 0;
          right: 0;
          text-align: center;
          img {
            width: 66px;
          }
        }
      }

      .tabs {
        padding: 0;
        margin: 0;
        width: 100%;
        display: flex;
        justify-content: space-around;
        background: #363644;
        color: #fff;
        font-size: 13px;
        .li-tab {
          height: 100%;
          display: inline-block;
          text-align: center;
          padding: 10px 0 5px 0;
          &:hover {
            cursor: pointer;
          }
        }

        .active {
          border-bottom: 2px solid #fff;
        }
      }
      .divTab1 {
        color: #fff;
        font-size: 12px;
        height: calc(100% - 341px);
        .content {
          padding: 10px 10px 0 10px;
          height: 100%;
        }
        .send {
          height: 142px;
          // background: red;
          .check {
            height: 30px;
            background: #363644;
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 0 10px;
            .biaoqingicon {
              cursor: pointer;
              font-size: 20px;
            }
            ::v-deep .el-checkbox__input.is-checked + .el-checkbox__label {
              color: #fff;
            }
            ::v-deep .el-checkbox {
              color: #fff;
            }
            ::v-deep .el-checkbox__label {
              margin-top: 1px;
            }
          }
          .input {
            height: calc(100% - 30px);
            position: relative;
            .textarea {
              height: 85%;
              ::v-deep .el-textarea__inner {
                border: none;
                background-color: transparent;
                color: #fff;
                font-size: 12px;
                &::placeholder {
                  font-size: 12px;
                }
              }
            }
            .sendBtn {
              position: absolute;
              bottom: 0;
              background: #333;
              width: 100%;
              padding: 12px 10px;
              display: flex;
              justify-content: space-between;
              align-items: center;
              .senBtntxt {
                background: #3595fb;
                padding: 4px 14px;
                border-radius: 30px;
                cursor: pointer;
              }
            }
          }
        }
      }
    }
    #video {
      object-fit: cover;
    }
  }
}
</style>

附上svg图片

//mumber_camer_close.svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18">
<rect class="cls-1" width="18" height="18"/>
<path class="hover-fill" fill="#737680" d="M9,1A5.9,5.9,0,0,0,5.52,2.12l1,1A4.53,4.53,0,0,1,9,2.4,4.6,4.6,0,0,1,13.6,7a4.53,4.53,0,0,1-.73,2.47l1,1A5.9,5.9,0,0,0,15,7,6,6,0,0,0,9,1Zm4.8,14.2-.6-.6H4.62l1.8-1.2h5.16l1.26.84-2.77-2.77A4.18,4.18,0,0,1,9,11.6,4.6,4.6,0,0,1,4.4,7a4.18,4.18,0,0,1,.13-1.07L3.41,4.81A6.13,6.13,0,0,0,3,7a6,6,0,0,0,2.85,5.1L3,14v2H15v-.3A1.7,1.7,0,0,1,13.8,15.2Z"/>
<path class="hover-fill" fill="#737680" d="M6,7.44A3,3,0,0,0,8.56,10ZM9,4a3,3,0,0,0-1.3.3L8.82,5.42A.55.55,0,0,1,9,5.4,1.6,1.6,0,0,1,10.6,7a.55.55,0,0,1,0,.18L11.7,8.3A3,3,0,0,0,12,7,3,3,0,0,0,9,4Z"/>
<path class="hover-fill" fill="#737680" d="M15,14.7a.74.74,0,0,1-.5-.2L3,3A.71.71,0,0,1,4,2L15.5,13.5a.72.72,0,0,1,0,1A.74.74,0,0,1,15,14.7Z"/>
</svg>
//mumber_camer.svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18">
<rect class="hover-fill" fill="none" width="18" height="18"/>
<path class="hover-fill" fill="#0044ff" d="M9,2.4A4.6,4.6,0,1,1,4.4,7,4.6,4.6,0,0,1,9,2.4M9,1a6,6,0,1,0,6,6A6,6,0,0,0,9,1Z"/>
<path class="hover-fill" fill="#0044ff" d="M9,5.4A1.6,1.6,0,1,1,7.4,7,1.6,1.6,0,0,1,9,5.4M9,4a3,3,0,1,0,3,3A3,3,0,0,0,9,4Z"/>
<path class="hover-fill" fill="#0044ff" d="M11.58,13.4l1.8,1.2H4.62l1.8-1.2h5.16M12,12H6L3,14v2H15V14l-3-2Z"/>
</svg>
//mumber_micophone_close.svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18">
<rect class="cls-1" width="18" height="18"/>
<path class="hover-fill" fill="#737680" d="M9,2A3,3,0,0,0,6.6,3.2l1,1A1.55,1.55,0,0,1,9,3.4,1.6,1.6,0,0,1,10.6,5V7.2L12,8.55A3.31,3.31,0,0,0,12,8V5A3,3,0,0,0,9,2ZM6,8a3,3,0,0,0,3,3A3.31,3.31,0,0,0,9.55,11L6,7.4Z"/>
<path class="hover-fill" fill="#737680" d="M10.58,12l1.05,1A5.46,5.46,0,0,1,3.55,8.25a.7.7,0,0,1,1.4,0A4.06,4.06,0,0,0,9,12.3,4.17,4.17,0,0,0,10.58,12Z"/>
<path class="hover-fill" fill="#737680" d="M14.45,8.25a5.3,5.3,0,0,1-.51,2.29L12.86,9.46a3.74,3.74,0,0,0,.19-1.21.7.7,0,1,1,1.4,0Z"/>
<path class="hover-fill" fill="#737680" d="M11.5,14.8h-5a.7.7,0,0,0,0,1.4h5a.7.7,0,0,0,0-1.4Z"/>
<path class="hover-fill" fill="#737680" d="M15,14.7a.74.74,0,0,1-.5-.2L3,3A.71.71,0,0,1,4,2L15.5,13.5a.72.72,0,0,1,0,1A.74.74,0,0,1,15,14.7Z"/>
</svg>
//mumber_micophone.svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18">
<rect class="cls-1" width="18" height="18"/>
<path class="hover-fill" fill="#0044ff" d="M9,3.4A1.6,1.6,0,0,1,10.6,5V8A1.6,1.6,0,0,1,7.4,8V5A1.6,1.6,0,0,1,9,3.4M9,2A3,3,0,0,0,6,5V8a3,3,0,0,0,6,0V5A3,3,0,0,0,9,2Z"/>
<path class="hover-stroke" fill="none" stroke="#0044ff" d="M13.75,8.25A4.75,4.75,0,0,1,9,13H9A4.75,4.75,0,0,1,4.25,8.25"/>
<line class="hover-stroke" fill="none" stroke="#0044ff" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" x1="6.5" y1="15.5" x2="11.5" y2="15.5"/>
</svg>

第二步,装zego即构的SDK,npm install zego-express-engine-webrtc --save
然后在页面script内引入
import { ZegoExpressEngine } from 'zego-express-engine-webrtc'

第三步,在data内新增几个值

userID: '888',
AppID: 1974122008,
AppSign: '52db995b730066c45a659f8f4217676eb56346d621f98f97b50dca32882cd98a',
zg: {},
token1: "",
stream: {},

第四步,在mounted初始化实例

// 初始化实例
    this.zg = new ZegoExpressEngine(this.AppID, 'wss://webliveroom-test.zego.im/ws')
// 监听zg连接状态
    this.zg.on('roomStateUpdate', (roomID, state, errorCode, extendedData) => {
      if (state == 'DISCONNECTED') {
        // 与房间断开了连接
        console.log('与房间断开了连接');
      }

      if (state == 'CONNECTING') {
        // 与房间尝试连接中 
        console.log('与房间尝试连接中');
      }

      if (state == 'CONNECTED') {
        // 与房间连接成功
        this.$message.success('登陆成功')
      }
    })
    // 监听推流状态
    this.zg.on('publisherStateUpdate', result => {
      // 推流状态变更通知
      this.$message.success('推流成功')
    })
    this.zg.on('publishQualityUpdate', (streamID, stats) => {
      // 推流质量
      console.log(stats);
      this.videoPacketsLostRate = (stats.video.videoTransferFPS).toFixed(2)
    })

第五步,在methods里新写一个获取token的方法

// 获取token的方法
    getTokenFun(appID, userID) {
      return new Promise((resolve, reject) => {
        const xmlhttp = new XMLHttpRequest();
        xmlhttp.onreadystatechange = e => {
          if (xmlhttp.readyState == 4) {
            if (xmlhttp.status == 200) {
              resolve(xmlhttp.response);
            } else {
              reject(e);
            }
          }
        };
        xmlhttp.open(
          "GET",
          `https://wsliveroom-alpha.zego.im:8282/token?app_id=${appID}&id_name=${userID}`,
          true
        );
        xmlhttp.send(null);
      });
    },

第六步,继续写方法,获取token,进入房间

// 获取token
    async getToken() {
      this.token1 = await this.getTokenFun(this.AppID, this.userID)
      this.loginRoom()
    },
    // 登陆房间
    async loginRoom() {
      const result = await this.zg.loginRoom('666', this.token1, { userID: this.userID, userName: 'aaa' });
      this.createStr()
    },
    // 创建流和渲染
    async createStr() {
      this.stream = await this.zg.createStream();
    },
    // 开始推流、开始直播
    startPublishingStream() {
      this.zg.startPublishingStream('123', this.stream)
    }

最后一步,在mounted里执行一个方法

// 获取token执行登陆房间操作
    this.getToken()

主播端推流 就搞定了

下面是观众端拉流

比较简单,直接贴出来吧

<template>
  <div>
    <div id="remote_stream">
      <video autoplay muted id='video' :src-object.prop="stream" width="100%" height="100%"></video>
    </div>
  </div>
</template>

<script>
import { ZegoExpressEngine } from 'zego-express-engine-webrtc'
export default {
  data() {
    return {
      userID: '999',
      AppID: 1974122008,
      AppSign: '52db995b730066c45a659f8f4217676eb56346d621f98f97b50dca32882cd98a',
      zg: {},
      token1: "",
      stream: {},
    }
  },
  methods: {
    // 获取token的方法
    getTokenFun(appID, userID) {
      return new Promise((resolve, reject) => {
        const xmlhttp = new XMLHttpRequest();
        xmlhttp.onreadystatechange = e => {
          if (xmlhttp.readyState == 4) {
            if (xmlhttp.status == 200) {
              resolve(xmlhttp.response);
            } else {
              reject(e);
            }
          }
        };
        xmlhttp.open(
          "GET",
          `https://wsliveroom-alpha.zego.im:8282/token?app_id=${appID}&id_name=${userID}`,
          true
        );
        xmlhttp.send(null);
      });
    },
    // 获取token
    async getToken() {
      this.token1 = await this.getTokenFun(this.AppID, this.userID)
      this.loginRoom()
    },
    // 登陆房间
    async loginRoom() {
      const result = await this.zg.loginRoom('666', this.token1, { userID: this.userID, userName: 'bbb' });
    },
    // 开始 拉流
    async startPlayingStream() {

      this.stream = await this.zg.startPlayingStream('123');
      console.log(stream);
    }
  },
  mounted() {
    // 初始化实例
    this.zg = new ZegoExpressEngine(this.AppID, 'wss://webliveroom-test.zego.im/ws')
    // 获取token执行登陆房间操作
    this.getToken()
    // 监听zg连接状态
    this.zg.on('roomStateUpdate', (roomID, state, errorCode, extendedData) => {
      if (state == 'DISCONNECTED') {
        // 与房间断开了连接
        console.log('与房间断开了连接');
      }

      if (state == 'CONNECTING') {
        // 与房间尝试连接中 
        console.log('与房间尝试连接中');
      }

      if (state == 'CONNECTED') {
        // 与房间连接成功
        this.$message.success('登陆成功')
      }
    })
    this.zg.on('roomStreamUpdate', (roomID, updateType, streamList, extendedData) => {
      if (updateType == 'DELETE') {
        // 与房间断开了连接
        console.log('与房间断开了连接');
        this.$message.error('停止拉流!')
        this.zg.stopPlayingStream('123')
      }

      if (updateType == 'ADD') {
        // 与房间尝试连接中 
        this.startPlayingStream()
      }
    })
    // 监听拉流状态
    this.zg.on('playerStateUpdate', result => {
      // 处理拉流状态
      console.log(result);
    })
    this.zg.on('playQualityUpdate', (streamID, stats) => {
      // 拉流质量回调
    })
  }
}
</script>

<style lang="scss" scoped>
#remote_stream {
  width: 375px;
  height: 180px;
  background: #333;
}
</style>

推荐阅读更多精彩内容