2021年1月1日,祝大家新年快乐!大吉大利!2020年疫情原因,在线直播、实时音视频技术也更加火热起来。公司项目业务中也有需要音视频的地方。技术选型使用了腾讯云TRTCCalling,以下展示在Vue 3.0 项目中如何配置使用。

在线demo地址:https://wkl007.github.io/trtc-calling-web

GitHub地址:https://github.com/wkl007/trtc-calling-web

1. 安装依赖

yarn add tim-js-sdk trtc-calling-js trtc-js-sdk tsignaling

2. Typescript项目中配置

trtc-calling-js目前暂无TypeScript类型声明文件,在shimes-vue.d.ts中添加:

2021年01月15日,本人已PR@types/trtc-calling-js类型声明文件。

yarn add --dev @types/trtc-calling-js

3. Vuex封装

有关TRTC相关的实例,通话状态等信息保存在Vuex中。

state.ts

const state: State = {
  ...
  trtcCalling: undefined, // trtc实例
  trtcInfo: {
    callStatus: 'idle', // 通话状态, idle, calling, connected
    isInviter: false, // 邀请者
    meetingUserIdList: [], // 会话用户ID列表
    muteVideoUserIdList: [], // 关闭摄像头用户ID列表
    muteAudioUserIdList: [] // 关闭麦克风用户ID列表
  },
  ...  
}

4. useTRTC hooks封装

新建hooks/index.ts文件

import TRTCCalling, { InvitedInfo, UserAudioInfo, UserInfo, UserVideoInfo } from 'trtc-calling-js'
// @ts-ignore
import * as LibGenerateTestUserSig from '../../public/js/lib-generate-test-usersig.min'
import { message, Modal } from 'ant-design-vue'
import store from '@/store'
import router from '@/router'

interface UseTRTC {
  getUserSig: () => { userSig: string },
  initTRTC: () => TRTCCalling,
  initListener: (trtcCalling: TRTCCalling) => void,
  removeListener: (trtcCalling: TRTCCalling) => void,
  handleLogin: (trtcCalling: TRTCCalling) => void
}

export function useTRTC (): UseTRTC {
  // 前端生成签名
  function getUserSig () {
    const EXPIRE_TIME = 604800 // 签名过期时间
    const { sdkInfo: { sdkAppId, secretKey }, userInfo: { username } } = store.getters
    // eslint-disable-next-line new-cap
    const generator = new LibGenerateTestUserSig.default(Number(sdkAppId), secretKey, EXPIRE_TIME)
    return generator.genTestUserSig(username)
  }

  // 初始化TRTC实例
  function initTRTC () {
    const { sdkInfo: { sdkAppId } } = store.getters
    const options = {
      SDKAppID: sdkAppId
    }
    return new TRTCCalling(options)
  }

  // 初始化监听
  function initListener (trtcCalling: TRTCCalling) {
    if (!trtcCalling) return
    trtcCalling.on(TRTCCalling.EVENT.ERROR, handleError)
    trtcCalling.on(TRTCCalling.EVENT.INVITED, (e: InvitedInfo) => handleInvited(trtcCalling, e))
    trtcCalling.on(TRTCCalling.EVENT.USER_ENTER, (e: UserInfo) => handleUserEnter(trtcCalling, e))
    trtcCalling.on(TRTCCalling.EVENT.USER_LEAVE, handleUserLeave)
    trtcCalling.on(TRTCCalling.EVENT.REJECT, handleEject)
    trtcCalling.on(TRTCCalling.EVENT.LINE_BUSY, handleLineBusy)
    trtcCalling.on(TRTCCalling.EVENT.CALLING_CANCEL, handleCallingCancel)
    trtcCalling.on(TRTCCalling.EVENT.KICKED_OUT, () => handleKickedOut(trtcCalling))
    trtcCalling.on(TRTCCalling.EVENT.CALLING_TIMEOUT, handleCallingTimeout)
    trtcCalling.on(TRTCCalling.EVENT.NO_RESP, handleNoResp)
    trtcCalling.on(TRTCCalling.EVENT.CALL_END, () => handleCallEnd(trtcCalling))
    trtcCalling.on(TRTCCalling.EVENT.USER_VIDEO_AVAILABLE, handleUserVideoChange)
    trtcCalling.on(TRTCCalling.EVENT.USER_AUDIO_AVAILABLE, handleUserAudioChange)
  }

  // 移除监听
  function removeListener (trtcCalling: TRTCCalling) {
    if (!trtcCalling) return
    trtcCalling.off(TRTCCalling.EVENT.ERROR, handleError)
    trtcCalling.off(TRTCCalling.EVENT.INVITED, (e: InvitedInfo) => handleInvited(trtcCalling, e))
    trtcCalling.off(TRTCCalling.EVENT.USER_ENTER, (e: UserInfo) => handleUserEnter(trtcCalling, e))
    trtcCalling.off(TRTCCalling.EVENT.USER_LEAVE, handleUserLeave)
    trtcCalling.off(TRTCCalling.EVENT.REJECT, handleEject)
    trtcCalling.off(TRTCCalling.EVENT.LINE_BUSY, handleLineBusy)
    trtcCalling.off(TRTCCalling.EVENT.CALLING_CANCEL, handleCallingCancel)
    trtcCalling.off(TRTCCalling.EVENT.KICKED_OUT, () => handleKickedOut(trtcCalling))
    trtcCalling.off(TRTCCalling.EVENT.CALLING_TIMEOUT, handleCallingTimeout)
    trtcCalling.off(TRTCCalling.EVENT.NO_RESP, handleNoResp)
    trtcCalling.off(TRTCCalling.EVENT.CALL_END, () => handleCallEnd(trtcCalling))
    trtcCalling.off(TRTCCalling.EVENT.USER_VIDEO_AVAILABLE, handleUserVideoChange)
    trtcCalling.off(TRTCCalling.EVENT.USER_AUDIO_AVAILABLE, handleUserAudioChange)
  }

  // 登录
  async function handleLogin (trtcCalling: TRTCCalling) {
    try {
      const { userInfo: { username } } = store.getters
      await trtcCalling.login({
        userID: username,
        userSig: getUserSig()
      })
    } catch (e) {}
  }

  // 退出登录
  async function handleLogout (trtcCalling: TRTCCalling) {
    try {
      await trtcCalling.logout()
      await store.dispatch('setLoginStatus', 0)
      await store.dispatch('setUserInfo', { username: '' })
      await router.push({ path: '/login' })
    } catch (e) {}
  }

  function handleError (err: any) {
    console.log(err)
  }

  // 被邀用户收到了邀请通知
  async function handleInvited (trtcCalling: TRTCCalling, {
    sponsor,
    userIDList,
    isFromGroup,
    inviteData,
    inviteID
  }: InvitedInfo) {
    try {
      console.log('被邀用户收到了邀请通知')
      const { trtcInfo, userInfo: { username } } = store.getters
      // 最后一个人发送 invite 进行挂断
      if (inviteData.callEnd) {
        trtcInfo.callStatus = 'idle'
        await store.dispatch('setTrtcInfo', trtcInfo)
        return
      }
      // 邀请人是自己, 同一个账号有可能在多端登录
      if (sponsor === username) return
      // 考虑忙线的情况
      if (trtcInfo.callStatus === 'calling' || trtcInfo.callStatus === 'connected') {
        await trtcCalling.reject({ inviteID, isBusy: true, callType: inviteData.callType })
        return
      }
      // 接通会话
      const { callType, roomID } = inviteData // 1:语音通话,2:视频通话
      trtcInfo.callStatus = 'calling'
      trtcInfo.isInviter = false
      const callTypeDisplayName = callType === TRTCCalling.CALL_TYPE.AUDIO_CALL ? '语音通话' : '视频通话'
      await store.dispatch('setTrtcInfo', trtcInfo)
      Modal.confirm({
        content: `来自${sponsor}${callTypeDisplayName}`,
        okText: '接听',
        cancelText: '拒绝',
        onOk: async () => {
          if (trtcInfo.meetingUserIdList.indexOf(username) < 0) trtcInfo.meetingUserIdList.push(username)
          await store.dispatch('setTrtcInfo', trtcInfo)
          if (roomID) {
            await trtcCalling.accept({
              inviteID,
              roomID,
              callType
            })
          }
          if (callType === TRTCCalling.CALL_TYPE.AUDIO_CALL) {
            await router.push({ path: '/audioCall' })
          }
          if (callType === TRTCCalling.CALL_TYPE.VIDEO_CALL) {
            await router.push({ path: '/videoCall' })
          }
        },
        onCancel: async () => {
          trtcCalling.reject({
            inviteID,
            isBusy: false,
            callType
          })
          await dissolveMeeting()
        }
      })
    } catch (e) {}
  }

  // 用户进入通话
  async function handleUserEnter (trtcCalling: TRTCCalling, { userID }: UserInfo) {
    console.log('用户进入通话')
    const { trtcInfo } = store.getters
    if (trtcInfo.meetingUserIdList.indexOf(userID) < 0) trtcInfo.meetingUserIdList.push(userID)
    if (trtcInfo.callStatus === 'calling') {
      // 如果是邀请者, 则建立连接
      // TODO
      trtcInfo.callStatus = 'connected'
    } else {
      // 第n (n >= 3)个人被邀请入会, 并且他不是第 n 个人的邀请人
      // 需要先等远程用户 id 的节点渲染到 dom 上
      // TODO
      await trtcCalling.startRemoteView({
        userID,
        videoViewDomID: `video-${userID}`
      })
    }
    await store.dispatch('setTrtcInfo', trtcInfo)
  }

  // 用户离开会话
  async function handleUserLeave ({ userID }: UserInfo) {
    console.log('用户离开会话')
    const { trtcInfo } = store.getters
    if (trtcInfo.meetingUserIdList.length === 2) trtcInfo.callStatus = 'idle'
    const index = trtcInfo.meetingUserIdList.findIndex((item: string) => item === userID)
    if (index >= 0) trtcInfo.meetingUserIdList.splice(index, 1)
    await store.dispatch('setTrtcInfo', trtcInfo)
  }

  // 被邀用户拒绝通话
  async function handleEject ({ userID }: UserInfo) {
    console.log('被邀用户拒绝通话')
    message.info(`${userID}拒绝通话`)
    await dissolveMeeting()
  }

  // 被邀用户正在通话中,忙线
  async function handleLineBusy ({ userID }: UserInfo) {
    console.log('被邀用户正在通话中,忙线')
    message.info(`${userID}忙线`)
    await dissolveMeeting()
  }

  // 本次通话被取消了
  async function handleCallingCancel () {
    console.log('本次通话被取消了')
    message.info('通话已取消')
    await dissolveMeeting()
  }

  // 重复登录,被踢出
  async function handleKickedOut (trtcCalling: TRTCCalling) {
    console.log('重复登录,被踢出')
    message.info('重复登录,被踢出')
    // await handleLogout(trtcCalling)
  }

  // 本次通话超时未应答
  async function handleCallingTimeout () {
    console.log('本次通话超时未应答')
    message.info('通话超时未应答')
    await dissolveMeeting()
  }

  // 被邀用户超时无应答
  async function handleNoResp ({ userID }: UserInfo) {
    console.log('被邀用户超时无应答')
    message.info(`${userID || '被邀用户'}无应答`)
    await dissolveMeeting()
  }

  // 本次通话结束
  async function handleCallEnd (trtcCalling: TRTCCalling) {
    console.log('通话已结束')
    message.info('通话已结束')
    trtcCalling.hangup()
    await dissolveMeeting()
  }

  // 远端用户开启/关闭了摄像头
  async function handleUserVideoChange ({ userID, isVideoAvailable }: UserVideoInfo) {
    console.log('远端用户开启/关闭了摄像头')
    const { trtcInfo } = store.getters
    if (isVideoAvailable) {
      trtcInfo.muteVideoUserIdList = trtcInfo.muteVideoUserIdList.filter((item: string) => item !== userID)
    } else {
      trtcInfo.muteVideoUserIdList = [...trtcInfo.muteVideoUserIdList, userID]
    }
    await store.dispatch('setTrtcInfo', trtcInfo)
  }

  // 远端用户开启/关闭了麦克风
  async function handleUserAudioChange ({ userID, isAudioAvailable }: UserAudioInfo) {
    console.log('远端用户开启/关闭了麦克风')
    const { trtcInfo } = store.getters
    if (isAudioAvailable) {
      trtcInfo.muteAudioUserIdList = trtcInfo.muteAudioUserIdList.filter((item: string) => item !== userID)
    } else {
      trtcInfo.muteAudioUserIdList = [...trtcInfo.muteAudioUserIdList, userID]
    }
    await store.dispatch('setTrtcInfo', trtcInfo)
  }

  // 解散会议
  async function dissolveMeeting () {
    const { trtcInfo } = store.getters
    trtcInfo.callStatus = 'idle'
    if (trtcInfo.meetingUserIdList.length < 2) {
      trtcInfo.meetingUserIdList = []
      trtcInfo.muteVideoUserIdList = []
      trtcInfo.muteAudioUserIdList = []
    }
    await store.dispatch('setTrtcInfo', trtcInfo)
  }

  return {
    getUserSig,
    initTRTC,
    initListener,
    removeListener,
    handleLogin
  }
}

5. 绑定监听事件

大家可以结合实际的业务场景,在页面中或者全局绑定监听事件。

App.vue中添加:

setup () {
    const { initListener, removeListener } = useTRTC()
    const store = useStore()
    const loginStatus = computed(() => store.getters.loginStatus)
    const trtcCalling = computed(() => store.getters.trtcCalling)

    watch(loginStatus, (val, oldVal) => {
      if (val) {
        // 登录成功监听
        initListener(toRaw(trtcCalling.value))
      } else {
        // 取消登录移除监听
        removeListener(toRaw(trtcCalling.value))
      }
    })

    ....
}

6. 视频呼叫模块

VideoCall.vue中:

setup () {
    const store = useStore()
    const images = inject('images')
    const trtcInfo = computed(() => store.getters.trtcInfo)
    const userInfo = computed(() => store.getters.userInfo)
    const trtcCalling = computed(() => store.getters.trtcCalling)
    const showVideoCall = ref(false) // 显示视频区域
    const isVideoOn = ref(true) // 视频状态
    const isAudioOn = ref(true) // 麦克风状态

    // 呼叫用户
    async function handleCallUser (values: { username: string }) {
      const trtcInfoData = trtcInfo.value
      const { username } = userInfo.value
      await toRaw(trtcCalling.value).call({
        userID: values.username,
        type: TRTCCalling.CALL_TYPE.VIDEO_CALL, // 视频通话
        timeout: 30 // 超时30s
      })
      trtcInfoData.callStatus = 'calling'
      trtcInfoData.isInviter = true
      if (trtcInfoData.meetingUserIdList.indexOf(username) < 0) trtcInfoData.meetingUserIdList.push(username)
      await store.dispatch('setTrtcInfo', trtcInfoData)
    }

    // 取消呼叫
    async function handleCancelCallUser () {
      const trtcInfoData = trtcInfo.value
      await toRaw(trtcCalling.value).hangup()
      trtcInfoData.callStatus = 'idle'
      trtcInfoData.meetingUserIdList = []
      trtcInfoData.muteVideoUserIdList = []
      trtcInfoData.muteAudioUserIdList = []
      await store.dispatch('setTrtcInfo', trtcInfoData)
    }

    // 开始会议
    function startMeeting () {
      const trtcInfoData = trtcInfo.value
      const { username } = userInfo.value
      // 多人通话
      if (trtcInfoData.meetingUserIdList >= 3) {
        const lastJoinUser = trtcInfoData.meetingUserIdList[trtcInfoData.meetingUserIdList.length - 1]
        nextTick(() => {
          toRaw(trtcCalling.value).startRemoteView({
            userID: lastJoinUser,
            videoViewDomID: `video-${lastJoinUser}`
          })
        })
        return
      }
      showVideoCall.value = true
      nextTick(() => {
        toRaw(trtcCalling.value).startLocalView({
          userID: username,
          videoViewDomID: `video-${username}`
        })
        const otherParticipants = trtcInfoData.meetingUserIdList.filter((item: string) => item !== username)
        otherParticipants.forEach((userID: string) => {
          toRaw(trtcCalling.value).startRemoteView({
            userID,
            videoViewDomID: `video-${userID}`
          })
        })
      })
    }

    // 挂断会议
    async function handleHangup () {
      const trtcInfoData = trtcInfo.value
      await toRaw(trtcCalling.value).hangup()
      showVideoCall.value = false
      trtcInfoData.callStatus = 'idle'
      await store.dispatch('setTrtcInfo', trtcInfoData)
    }

    // 打开/关闭摄像头
    async function toggleVideoStatus () {
      const trtcInfoData = trtcInfo.value
      const { username } = userInfo.value
      isVideoOn.value = !isVideoOn.value
      if (isVideoOn.value) {
        await toRaw(trtcCalling.value).openCamera()
        trtcInfoData.muteVideoUserIdList = trtcInfoData.muteVideoUserIdList.filter((item: string) => item !== username)
      } else {
        await toRaw(trtcCalling.value).closeCamera()
        trtcInfoData.muteVideoUserIdList.push(username)
      }
      await store.dispatch('setTrtcInfo', trtcInfoData)
    }

    // 打开/关闭麦克风
    async function toggleAudioStatus () {
      const trtcInfoData = trtcInfo.value
      const { username } = userInfo.value
      isAudioOn.value = !isAudioOn.value
      toRaw(trtcCalling.value).setMicMute(!isAudioOn.value)
      if (isAudioOn.value) {
        trtcInfoData.muteAudioUserIdList = trtcInfoData.muteAudioUserIdList.filter((item: string) => item !== username)
      } else {
        trtcInfoData.muteAudioUserIdList.push(username)
      }
      await store.dispatch('setTrtcInfo', trtcInfoData)
    }

    // 判断是否关闭媒体
    function isUserMute (muteUserList: Array<string>, userId: string): boolean {
      return muteUserList.indexOf(userId) !== -1
    }

    watch(() => trtcInfo.value.callStatus, (val, oldVal) => {
      if (val !== oldVal && val === 'connected') {
        startMeeting()
      }
    })

    onMounted(() => {
      const trtcInfoData = trtcInfo.value
      if (trtcInfoData.callStatus === 'connected' && !trtcInfoData.isInviter) {
        startMeeting()
      }
    })

    onBeforeUnmount(() => {
      const trtcInfoData = trtcInfo.value
      trtcInfoData.muteVideoUserIdList = []
      trtcInfoData.muteAudioUserIdList = []
      if (trtcInfoData.callStatus === 'connected') {
        toRaw(trtcCalling.value).hangup()
        trtcInfoData.callStatus = 'idle'
      }
      store.dispatch('setTrtcInfo', trtcInfoData)
    })

    return {
      images,
      trtcInfo,
      userInfo,
      showVideoCall,
      isVideoOn,
      isAudioOn,
      handleCallUser,
      handleCancelCallUser,
      toggleVideoStatus,
      toggleAudioStatus,
      handleHangup,
      isUserMute
    }
  }

7. 效果展示

7.1 登录页

登录页

7.2 视频通话

视频通话

7.3 语音通话

语音通话

Logo

前往低代码交流专区

更多推荐