音视频-mrtc

近期由于项目需要,本来集成的TRTC切换成MRTC
现在就MRTC集成做个简单整理,这里主要介绍web和H5的集成

1、MRTC简介

官网介绍
音视频通话组件( Mobile Real-Time Communication,简称 MRTC)是 mPaaS 提供的音频、视频通话组件。该组件功能丰富,提供纯语音通话和视频通话功能,支持 PC、移动端、IoT 设备等多终端接入。音视频通话可实现一对一通话及多人会议,通话过程中支持屏幕录制、屏幕共享、截图等功能,同时支持即时文字消息和文件传输。此外,支持实时语音识别,能够识别对端的语音确认,辅助本端判断对端的意向;点播功能可实现在视频通话过程中,播放视频、PPT 等多种提示画面。

多种参与模式:支持一对一视频通话及多人视频通话。

多平台:支持 iOS、Android、PC Web、H5 以及小程序。

多端互通:支持手机、PC、IoT 设备之间互联互通。

会话保持:网络短暂异常、网络切换时,业务流程不中断,保持会话的持续性。

自定义视频规格、自适应视频规格:支持自定义宽、高、最大帧率、最大码率,并能在上限范围内根据网络状况自适应调整视频规格。

2、MRTC的集成

官网接入文档

在官网的集成上在详细介绍下,做个二次封装,相关接口可查看官方文档

官方流程图
p432419.jpeg

思路:
1、封装集成JS方法 (可使用mixins方式)
2、封装UI组件(呼叫组件,视频通话组件,接听组件)
3、在业务的基础上封装相关业务音视频SDK

3、具体实现(vue版)

1、下载SDK,引入项目

下载 artvc-web-sdk,把lib文件引入到项目中

项目按需引入对应的js(在index.html里面)

  <script src="./lib/adapter.js"></script>
  <script src="./lib/meeting_api.js"></script>
  <script src="./lib/mcu.js"></script>
  <script src="./lib/meeting_camera_stream.js"></script>
  <script src="./lib/meeting_invite.js"></script>
2、实例化 SDK
 const test_controller = new McuController() // 实例化 SDK
    this.test_controller = test_controller
3、建立连接
init() {
      const test_controller = this.test_controller
      const config_param = {}

      config_param.uid = '6189'
      config_param.biz_name = 'demo'
      config_param.sub_biz = 'default'
      config_param.workspaceId = 'default'
      config_param.room_server_url = 'wss://服务地址'
      config_param.sign = this. getSign()
      // 允许最大断网时间 (超过未重连, 直接关闭)
      config_param.network_check_timeout = 120 * 1000
      test_controller.Connect(config_param)
    }

// 注意:签名应该是后台返回的,这是demo可写死
 // 签名(通道建连/创建房间/加入房间需要)
    getSign(uid, isRecord = false) {
      const test_controller = this.test_controller
      test_controller.trace(`GetSign uid=${uid}`)
      return 'signature'
    },
4、初始化回调方法

所有的回调方法都在这里监听

initCallback() {
      const test_controller = this.test_controller
      // 建立连接成功回调
      test_controller.OnConnectOK = () => {
        // this.initRoom()
        console.log('建立连接成功')
      }
      // 建立连接失败回调
      test_controller.OnConnectFailed = function(code, msg) {
        console.log(code, msg)
        console.log('建立连接失败, 请尝试https修复')
      }
      // 房间初始化成功
      test_controller.OnInitRoomConfigOK = () => {
        console.log('房间初始化成功')
        if (this.role === 'created') {
          this.createRoom()
        } else if (this.role === 'join') {
          this.joinRoom()
        }
      }
      // 房间初始化失败
      test_controller.OnInitRoomConfigFail = function(err_code, err_msg) {
        console.log(err_code, err_msg)
        console.log('房间初始化失败')
      }
      // 创建房间成功回调
      test_controller.OnCreateRoomSucc = (room_id, rtoken) => {
        console.log( room_id, rtoken)

        this.isHiddenVideo = false
        this.typeState = '0'
        this.messageSend({
          toUserId: '9232131735',
          userId: '123',
          type: '1',
          roomNumber: room_id,
          passWord: rtoken
        })
        // test_controller.JoinRoom(room_id, rtoken, this.getSign())

        console.log('创建房间成功')
      }
      // 创建房间失败回调
      test_controller.OnCreateRoomFailed = function(err_code, err_msg) {
        console.log(err_code, err_msg)
        console.log('创建房间失败')
      }
      // 加入房间成功
      test_controller.OnJoinRoomSucc = () => {
        console.log('加入房间成功')
        this.isHiddenVideo = false
        this.typeState = '0'
      }
      // 加入房间失败
      test_controller.OnJoinRoomFailed = function(err_code, err_msg) {
        console.log(err_code, err_msg)
        console.log('加入房间失败')
      }
      test_controller.OnPublishSucc = (sid) => {
        this.timeStart()
        console.log('发布订阅')
      }
      // 订阅成功回调
      test_controller.OnSubscribeSucc = function(feedId, sid) {
        test_controller.trace(`~~~~~~~~~~~~~ OnSubscribeSuccess  Response  , sid=${sid},feedId=${feedId}`)
        console.log('订阅成功回调')
      }
      // 邀请成功
      test_controller.OnInviteOK = function() {
        console.log('邀请成功回调')
      }

      // 邀请失败
      test_controller.OnInviteFail = function(code, msg) {
        console.log('邀请失败回调')
      }
      test_controller.OnReplyInviteOK = () => {
        console.log('回复邀请回调')
      }
      // 退出房间回调
      test_controller.OnLeaveRoom = (leaveType) => {
        test_controller.warning(`~~~~~~~~~~~~~ leave room! leaveType = ${leaveType}`)
        console.log('退出房间成功')
        this.onTimeReset()
        this.isHiddenVideo = true
      }
      // 退出房间回调
      test_controller.OnParticipantLeaveRoom = (participant, exitType) => {
        test_controller.warning(`~~~~~~~~~~~~~ leave room! leaveType = ${participant}${exitType}`)
        console.log('对方退出房间成功')
        this.onQuit()
      }
    }
5、初始化房间
initRoom(type) {
    // type 是区分是加入房间还是创建房间
      if (type) {
        this.role = type
      }
      const test_controller = this.test_controller
      const config_param = {
        auto_publish_subscribe: 3,
        media_type: 1,
        publish_device: 1,
        initSubscribe: [
          {
            subscribe_video_id: 'video0',
            subscribe_audio_id: 'audio0',
            subscribe_streamId_id: 'subscribe_streamId0',
            feedId_id: 'feedId0'
          }, {
            subscribe_video_id: 'video4',
            subscribe_audio_id: 'audio4'
          }],
        initPublish: [
          {
            publish_video_id: 'publish_video1',
            publish_streamId_id: 'publish_streamId1',
            publish_tag: 'VIDEO_SOURCE_CAMERA_1'
          }
        ]
      }
      test_controller.InitRoomConfig(config_param)
    }
6、创建房间
createRoom() {
      const test_controller = this.test_controller
      test_controller.CreateRoom(this.getSign())
    },
7、发布订阅

注意:如果初始化的时候是自动发布订阅,则创建房间之后不需要手动发布订阅,否则需要手动发布订阅

onPublish() {
      const test_controller = this.test_controller
      const config_param = {
        'media_type': 1,
        'need_volume_analyser': true,
        'publish_video_id': 'publish_video1',
        'publish_streamId_id': 'publish_streamId1',
        'aspectRatioStrongDepend': false,
        'aspectRatio': '0',
        'video_profile_type': '2',
        'publish_tag': 'VIDEO_SOURCE_CAMERA',
        'enableVideo': true,
        'enableAudio': true,
        'publish_device': 1,
        'transport_': 'all',
        'defaultTurnServer': '',
        'degradationType': 1,
        'scalabilityMode': 'NONE'
      }
      test_controller.Publish(config_param)
    }
8、退出房间
 onLeaveRoom() {
      const test_controller = this.test_controller
      test_controller.LeaveRoom()
    },
9、视频UI组件
<template>
  <div id="videos" v-drag class="video-div" :class="isHiddenVideo?'display-none':'display-block'">
    <div class="publishVideo">
      <video
        id="publish_video1"
        autoplay
        muted="true"
        webkit-playsinline="true"
        playsinline="true"
        width="100%"
        height="100%"
        style="object-fit: cover;"
      />
      <div class="time">{
   
   { time }}</div>

      <div class="video-tool">
        <img class="img-gd" src="@/assets/images/jj.png" alt="" @click="onQuit">
    
    </div>
    <div class="subscribeVideo">
      <video
        id="video0"
        autoplay
        muted
        width="100%"
        height="100%"
        webkit-playsinline="true"
        playsinline="true"
        style="object-fit: cover;"
      >
        video
      </video>
      <audio id="audio0" autoplay>音频</audio>
      <video id="video99" autoplay muted="true" width="100%" height="480" hidden>
        video
      </video>
      <audio id="audio99" autoplay hidden>音频</audio>
      <br>
      <label id="subscribe_feedId_text0" type="text" class="hiddenForMobile" hidden>&nbsp;&nbsp;feedId:</label>
      <label id="feedId0" class="css-text-color hiddenForMobile" type="text" />
      <br>
      <label id="subscribe_streamId_text0" type="text" hidden class="hiddenForMobile">&nbsp;&nbsp;streamId:</label>
      <label id="subscribe_streamId0" class="css-text-color hiddenForMobile" type="text" />
    </div>
  </div>
</template>

<script>
export default {
  name: 'Index',
  // 自定义指令
  directives: {
    drag: {
      // 指令的定义
      bind: function(el) {
        const oDiv = el // 获取当前元素
        oDiv.onmousedown = (e) => {
          console.log('onmousedown')
          // 算出鼠标相对元素的位置
          const disX = e.clientX - oDiv.offsetLeft
          const disY = e.clientY - oDiv.offsetTop

          document.onmousemove = (e) => {
            // 用鼠标的位置减去鼠标相对元素的位置,得到元素的位置
            const left = e.clientX - disX
            const top = e.clientY - disY

            oDiv.style.left = left + 'px'
            oDiv.style.top = top + 'px'
          }

          document.onmouseup = (e) => {
            document.onmousemove = null
            document.onmouseup = null
          }
        }
      }
    }
  },
  props: {
    isHiddenVideo: {
      type: Boolean,
      default: true
    },
    time: {
      type: String,
      default: '00:00:00'
    }
  },
  methods: {
    onQuit() {
      this.$emit('onQuit')
    },
    onInappropriate() {
      this.$emit('onInappropriate')
    },
    onLooks() {
      this.$emit('onLooks')
    },
    onOffer() {
      this.$emit('onOffer')
    }
  }
}
</script>

<style lang="scss" scoped>
.display-none {
  display: none;
}

.display-block {
  display: block;
}

.video-div {
  position: absolute;
  top: 40px;
  right: 10px;
  width: 500px;
  height: 600px;
  overflow: hidden;
  background: #001528;
  border-radius: 20px;

  .publishVideo {
    width: 100%;
    height: 100%;

    .time {
      position: absolute;
      top: 10px;
      right: 0;
      width: 100px;
      height: 40px;
      line-height: 40px;
      color: white;
    }

    .video-tool {
      position: absolute;
      bottom: 0;
      left: 0;
      width: 100%;
      height: 100px;
      background-color: rgba(0,0,0,.5);

      .img-gd {
        position: absolute;
        top: -20px;
        right: calc(50% - 20px);
        z-index: 99;
        width: 40px;
      }

      .btn-class {
        position: relative;
        width: 320px;
        height: 100px;
        margin: 0 auto;
        //background: red;
      }

      .looks {
        position: absolute;
        left: 120px;
      }

      .offer {
        position: absolute;
        left: 220px;
      }

      .inappropriate {
        position: absolute;
        left: 20px;
      }

      .btn-base {
        bottom: 25px;
        width: 90px;
        height: 40px;
        font-size: 12px;
        line-height: 40px;
        color: white;
        text-align: center;
        background: #000;
        border-radius: 20px;
      }
    }
  }

  .subscribeVideo {
    position: absolute;
    top: 50px;
    right: 10px;
    width: 200px;
    height: 200px;
    overflow: hidden;
    //background-color: #409eff;
    border-radius: 10px;
  }
}

</style>

视频组件

10、接听UI组件
<template>
  <div class="invite-video">
    <div class="user-info">
      <img class="img-head" :src="callUserInfo.avatar?callUserInfo.avatar:'/company/static/images/system/user_avatar_default.png'" alt="">
      <div class="info-name"><span class="name">{
   
   { callUserInfo.name }}</span><span> {
   
   {callUserInfo.attrs}} </span></div>
      <div class="info-text"><span>请求与你视频面试</span></div>
    </div>
    <img class="img-jj" src="../../assets/image/jj.png" alt="" @click="onAction('1')">
    <img class="img-splj" src="../../assets/image/splj.png" alt="" @click="onAction('2')">

  </div>
</template>

<script>
export default {
  name: 'Index',
  props:{
    callUserInfo:{
      type:Object,
      default:()=>{
        return {
          avatar:null,
          name:null,
          attrs:null
        }
      }
    }
  },
  methods: {
    onAction(type) {
      this.$emit('onAction', type)
    }
  }
}
</script>
<style lang="scss" scoped>
.invite-video {
  position: absolute;
  top: 20px;
  right: calc(50% - 200px);
  width: 400px;
  height: 150px;
  overflow: hidden;
  background: #001528;
  border-radius: 10px;

  .user-info {
    position: absolute;
    top: 0;
    right: 0;
    width: 400px;
    height: 80px;
    //background: red;
    overflow: hidden;

    .info-name {
      position: absolute;
      top: 15px;
      left: 90px;
      width: 300px;
      height: 20px;
      font-size: 14px;
      color: #666;

      .name {
        font-size: 20px;
        color: white
      }
    }

    .info-text {
      position: absolute;
      bottom: 15px;
      left: 90px;
      width: 300px;
      height: 20px;
      font-size: 14px;
      color: white;
    }

    .img-head {
      position: absolute;
      bottom: 15px;
      left: 20px;
      width: 50px;
      border-radius: 10px;
    }
  }

  .ckjl-btn {
    position: absolute;
    left: 110px;
  }

  .kshf-btn {
    position: absolute;
    left: 20px;
  }

  .btn-base {
    bottom: 25px;
    width: 80px;
    height: 30px;
    font-size: 12px;
    line-height: 30px;
    color: white;
    text-align: center;
    background: #000;
    border-radius: 17px;
  }

  .img-jj {
    position: absolute;
    right: 80px;
    bottom: 20px;
    width: 40px;
  }

  .img-splj {
    position: absolute;
    right: 20px;
    bottom: 20px;
    width: 40px;
  }
}
</style>

接听组件

11、呼叫UI组件
<template>
<div class="call-class">
  <img class="img-user" :src="userInfo.avatar?userInfo.avatar:'/company/static/images/system/user_avatar_default.png'" alt="">
  <div class="name">{
   
   { userInfo.userName }}</div>
  <div class="name-call">正在呼叫...</div>
  <img class="img-cancel" src="../../assets/image/cancel.png" alt="" @click="onCancel">
  <div class="name-cancel" @click="onCancel">取消</div>

</div>
</template>

<script>
export default {
  name: "index",
  props:{
    userInfo:{
      type:Object,
      default:()=>{
        return {
          avatar:null,
          userName:null
        }
      }
    }
  },
  methods:{
    onCancel(){
      this.$emit('onCancel')
    }

  }

}
</script>

<style scoped lang="scss">
.call-class{
  position: absolute;
  top: 100px;
  right: 0;
  width: 300px;
  height: 350px;
  background: rgba(0,0,0,0.8);
  color: white;
  border-radius: 10px 0 0 10px;
  z-index: 99999;
  .name{
    text-align: center;
    position: absolute;
    top: 120px;
    font-size: 20px;
    font-weight: bold;
    width: 100%;
  }
  .img-cancel{
    position: absolute;
    top: 230px;
    right: calc(50% - 20px);
    width: 40px;
    z-index: 99;
  }
  .name-cancel{
    text-align: center;
    position: absolute;
    top: 280px;
    font-size: 12px;
    width: 100%;
  }
  .name-call{
    text-align: center;
    position: absolute;
    top: 160px;
    font-size: 14px;
    width: 100%;
  }
}
.img-user{
  position: absolute;
  top: 30px;
  right: calc(50% - 40px);
  width: 80px;
  z-index: 99;
}

</style>

呼叫组件

12、app.vue 集成
<Video :is-hidden-video="isHiddenVideo" :time="timeStr" @onQuit="onQuit" />
    <InviteVideo v-if="typeState === '0'" @onAction="onAction" />
    <CallVideo v-if="isCallShow" @onCancel="onCancel"></CallVideo>


import InviteVideo from './components/InviteVideo'
import Video from './components/Video'
import mrtc from '@/mixins/mrtc'

components: {
    InviteVideo,
    Video
  },
  mixins: [mrtc, webSocket],

到这里基本上MRTC音视频集成完成了。

4、WebSocket 使用

由于需求场景是pc和小程序互通,但由于小程序的局限性,无法邀请好友加入房间,也无法监听加入房间事件。因此需要业务自行实现消息发送。

websocket封装,网上也有相应的教程

export default {
  components: { },
  data() {
    return {
      websock: '',
      lockReconnect: false, // 是否真正建立连接
      timeout: 58 * 1000, // 58秒一次心跳
      timeoutObj: null, // 心跳倒计时
      serverTimeoutObj: null, // 心跳倒计时
      timeoutnum: null, // 断开 重连倒计时
      typeState: '0',
      roomID: '6693563501',
      roomKey: '123',
      userID: ''
    }
  },
  created() {
  },
  destroyed() {
    this.websock.close() // 离开路由之后断开websocket连接
  },
  methods: {
    messageSend({ toUserId, userId, type, roomNumber, passWord }) {
      const actions = {
        toUserId: '9232131735487',
        userId: '123',
        type: type,
        roomNumber: roomNumber,
        passWord: passWord
      }
      this.websocketsend(JSON.stringify(actions))
    },
    currentTime() {
      setInterval(this.formatDate, 500)
    },
    initWebSocket() {
      // 初始化weosocket
      const wsuri = 'ws://域名'
      this.websock = new WebSocket(wsuri)
      // 客户端接收服务端数据时触发
      this.websock.onmessage = this.websocketonmessage
      // 连接建立时触发
      this.websock.onopen = this.websocketonopen
      // 通信发生错误时触发
      this.websock.onerror = this.websocketonerror
      // 连接关闭时触发
      this.websock.onclose = this.websocketclose
    },
    // 连接建立时触发
    websocketonopen() {
      // 开启心跳
      this.start()
      // 连接建立之后执行send方法发送数据

      // this.websocketsend(actions)
    },
    // 通信发生错误时触发
    websocketonerror() {
      console.log('出现错误')
      this.reconnect()
    },
    // 客户端接收服务端数据时触发
    websocketonmessage(e) {
      console.log(e.data)
      // 收到服务器信息,心跳重置
      //("1","呼叫"),
      //  ("2","被拒接"),
      //  ("3","不在线"),
      //  ("4","占线中"),
      //  ("999","心跳检测");
  // 业务逻辑自行处理
      const data = JSON.parse(e.data)
      this.typeState = data.type
      switch (data.type) {
        case '1':
          this.roomID = data.roomNumber
          this.roomKey = data.passWord
          this.userID = data.userId
          break
      }

      this.reset()
    },
    websocketsend(Data) {
      // 数据发送
      this.websock.send(Data)
    },
    // 连接关闭时触发
    websocketclose(e) {
      // 关闭
      console.log('断开连接', e)
      // 重连
      this.reconnect()
    },
    reconnect() {
      // 重新连接
      var that = this
      if (that.lockReconnect) {
        return
      }
      that.lockReconnect = true
      // 没连接上会一直重连,设置延迟避免请求过多
      that.timeoutnum && clearTimeout(that.timeoutnum)
      that.timeoutnum = setTimeout(function() {
        // 新连接
        that.initWebSocket()
        that.lockReconnect = false
      }, 5000)
    },
    reset() {
      // 重置心跳
      var that = this
      // 清除时间
      clearTimeout(that.timeoutObj)
      clearTimeout(that.serverTimeoutObj)
      // 重启心跳
      that.start()
    },
    start() {
      // 开启心跳
      console.log('开启心跳')
      var self = this
      self.timeoutObj && clearTimeout(self.timeoutObj)
      self.serverTimeoutObj && clearTimeout(self.serverTimeoutObj)
      self.timeoutObj = setTimeout(function() {
        // 这里发送一个心跳,后端收到后,返回一个心跳消息,
        if (self.websock.readyState && Number(self.websock.readyState) === 1) {
          // 如果连接正常
          const actions = {
            toUserId: '1592321317',
            userId: '123',
            type: '999',
            roomNumber: '123456',
            passWord: '123456'
          }
          self.websocketsend(JSON.stringify(actions)) // 这里可以自己跟后端约定
        } else {
          // 否则重连
          self.reconnect()
        }
        self.serverTimeoutObj = setTimeout(function() {
          // 超时关闭
          self.websock.close()
        }, self.timeout)
      }, self.timeout)
    }
  },
  mounted() {
    this.currentTime()
  },
  // 销毁定时器
  beforeDestroy() {
    if (this.formatDate) {
      clearInterval(this.formatDate) // 在Vue实例销毁前,清除时间定时器
    }
  }
}

到这MRTC就集成完成了,通信需要配置WebSocket实现。

附带简单集成demo,不带业务逻辑

猜你喜欢

转载自blog.csdn.net/wang15180138572/article/details/128122232