乐趣区

关于uni-app:环信uniappdemo-升级改造计划单人多人音视频通话三

前序文章:

环信 uni-app Demo 降级革新打算——Vue2 迁徙到 Vue3(一)

环信即时通讯 SDK 集成——环信 uni-app-demo 降级革新打算——整体代码重构优化(二)

概述

在将声网 uni-app 音视频插件正式集成进入环信的 uni-app-demo 中,标记着本次降级革新至此根本告一段落。在第三期的降级革新中,次要工作为在 Demo 层造成一个较为容易拆分的无关音视频相干组件,力求第一:代码是否可读、第二:能够对参考源码的同学提供实例、第三:可能不便在脱离其余 IM 性能时,实现对音视频性能的复用。

同时也棘手针对 emChat 组件进行小范畴重构,解决了 uni-app 在 App 以及小程序端,软键盘弹起音讯列表不滚动以及软键盘遮挡性能栏问题。

上面我将尽可能详细描述一下本次针对音视频性能、以及音讯列表重写的心路历程。

性能背景以及目标

有越来越多的用户在 IM 性能实现中未免向相似微信聊天的性能靠齐,除了日常 IM 性能中,也离不开音视频通话性能,因而须要在环信 uni-app-demo 中减少实现音视频通话的示例代码,可能对想要实现音视频性能的用户造成可参考的 demo 代码,以及可复用的音视频功能模块组件。

前置筹备

  • 确认实现性能范畴

    接听呼叫(单聊一对一、群组多人音视频通话)且只反对 uni-app 原生端应用。

  • 浏览声网音视频 uni-app 端相干文档,相熟大抵流程以及相熟局部外围 API,跑通示例 Demo。
  • 相熟环信其余端PCWeb 端、安卓、iOS 端callKit 信令交互相干逻辑,确保实现 uni-app 所实现的音视频性能可能与其余端 Demo 进行互通。
  • 理解 nvue 组件相干语法布局款式等与 vue 的差别,推拉流视频容器仅反对在 nvue 组件中进行应用。

实际见真章

Tip: 以下展现代码因篇幅所限,均做了不同水平的删减保留了外围逻辑展现,具体代码文末会给出源码地址。

step1:在我的项目中集成音视频相干插件

Agora(声网)Demo 示例中有两个插件是必须要进行集成的,别离为Native 原生插件,Js 插件

Agora-Demo 示例插件下载地址以及性能简介详见下方提供的链接。

  • Quick-start-demo
  • Agora 原生插件地址
  • Agora JS 插件地址

具体插件的导入形式就不在本篇中具体介绍,上方插件下载地址中有提到插件导入形式,能够进行参阅。

特地留神:Agora-Uni-App JS 插件导入之后会在目录下生成一个 package.json 文件,这个文件会与通过 npm 导入的 easemob-websdkpackage.json重合,因而 Demo 中只保留了 easemob-demopackage.json

step2: 设计搭建 emCallKit(音视频组件)逻辑构造

主体大抵构造如下:

graph TD
CallKit --> emCallkit
emCallkit --> callKitManage
emCallkit --> config
emCallkit --> contants
emCallkit --> stores
emCallkit --> utils
emCallkit --> index.js
CallKit --> emCallkitPages
emCallkitPages --> alertScreen.vue
emCallkitPages --> inviteMembers.vue
emCallkitPages --> multiCall.nvue
emCallkitPages --> singleCall.nvue

其中 components/emCallKit 次要为外围 emCallKit 逻辑层代码,callKitManage文件中次要蕴含对外公布订阅频道内工夫逻辑代码,以及频道内信令发送代码。config声网 AppId 配置。contants文件夹音视频频道内常量、stores频道内外围逻辑在此,利用 pinia 进行频道内状态治理。utils工具办法,index.jsemCallkit 入口文件,该文件内挂载信令监听初始化频道内 IM Client。

pages/emCallKitPages 则是频道内各个页面在此结构,alertScreen.vue单人多人收到邀请弹出该页面,单人呼叫也应用该页面。inviteMembers.vue多人邀请页面。multiCall.nvue多人通话中页面。singleCall.nvue单人通话中页面。

step3: 实现单人音视频信令接管以及发送

在思考实现单人音视频拨打之前须要理解其余端曾经实现的音视频时序,
以单人音视频呼叫为例:

Alice 为呼叫方 John 为接管方

sequenceDiagram
Alice->>John: invite message(邀请您进行单人音视频通话)John-->>Alice: alerting
Alice-)John: confirmRing
John-->>Alice: answerCall
Alice-)John: confirmCallee

能够看到与 http 的”握手“过程类似,须要通过几次确认,这样频繁的确认意义在于,是否保障通话状态的准确性,且无效避免在离线的状况下,上线无端触发曾经生效的邀请弹窗。而下面的除了邀请的音讯为一条一般文本音讯,整个过程都是通过环信 IM 的 CMD 命令音讯 实现,且每条音讯信令中都有携带一些声网频道信息,比方频道名称,呼叫的类型等都是基于 CMD 命令音讯 实现。

为了可能独立于 IM 性能之外去应用音视频插件,因而在书写时尽可能的与外层 IM Demo 中的逻辑分来到,比方 callKit 中有用到音讯监听用来监听音讯以及发送 im 音讯,因而将实例化后的 websdk(暂称:EMClient)传入到 emCallKit 中,并利用 websdk 反对多处挂载监听回调的个性,通过拿到传入 EMClient.send 进行音讯发送,并应用 EMClient.addEventHandler 进行监听的挂载,便造成了如下缩减后的代码:

/* 频道信令发送 */
import useSendSignalMsgs from './callKitManage/useSendSignalMsgs';
let CallKitEMClient = null;
let CallKitCreateMsgFun = null;
export const useInitCallKit = () => {
  // 初始化 EMClient 之 Callkit 内
  const setCallKitClient = (EMClient, CreateMsgFun) => {
    CallKitEMClient = EMClient;
    CallKitCreateMsgFun = CreateMsgFun;
    mountSignallingListener();};
  // 挂载 Callkit 信令相干监听
  const mountSignallingListener = () => {console.log('>>>>>>>callkit 监听已挂载');
    CallKitEMClient.addEventHandler('callkitSignal', {onTextMessage: (message) => {const { ext} = message;
        if (ext && ext?.action === CALL_ACTIONS_TYPE.INVITE)
          handleCallKitInvite(message);
        console.log('>>>>> 收到文本信令音讯', message);
      },
      onCmdMessage: (msg) => {console.log('>>>>> 收到命令信令音讯', msg);
        if (msg && msg?.action === CALL_ACTIONS_TYPE.RTC_CALL)
          handleCallKitCommand(msg);
      },
    });
    // 解决收到为文本的邀请信息
    const handleCallKitInvite = (msgBody) => {console.log('>>>>> 开始解决被邀请音讯');
      const {from, ext} = msgBody || {};
      // 邀请音讯发送者为本人则疏忽
      if (from === CallKitEMClient.user) return;
    };
    // 解决接管到通话交互过程的 CMD 命令音讯
    const handleCallKitCommand = (msgBody) => {
      // 多端状态下信令音讯发送者为本人则疏忽
      if (msgBody.from === CallKitEMClient.user) return;
    };
  };

  };
  return {
    CallKitEMClient,
    CallKitCreateMsgFun,
    setCallKitClient,
  };
};
// 外层调用初始化 callKit 频道
import {EMClient, EaseSDK} from './EaseIM';
/* callKit */
import {useInitCallKit} from '@/components/emCallKit';
const {setCallKitClient} = useInitCallKit();
setCallKitClient(EMClient, EaseSDK.message);

至此就能够做到了,在初始化的时候实现针对 callKit 监听的挂载,可能做到在 callKit 中独自接管 im 相干邀请音讯以及信令。
上面解决 im 信令发的问题
如下面形容的 callKit 我的项目构造统一,在 callKitManage 文件夹下新建 useSendSignalMsgs.js 文件次要解决无关信令发送外围代码,从而解决信令的发送问题。

/* 用来发送所有频道内信令应用 */
import {CALL_ACTIONS_TYPE, MSG_TYPE} from '../contants';
import {useInitCallKit} from '../index.js';

const action = 'rtcCall';
const useSendSignalMsgs = () => {const { CallKitEMClient, CallKitCreateMsgFun} = useInitCallKit();
  // 发送告诉弹出待接听窗口信令
  const sendAlertMsg = (payload) => {const { from, ext} = payload;
    const option = {
      type: 'cmd',
      chatType: 'singleChat',
      to: from,
      action: action,
      ext: {
        action: CALL_ACTIONS_TYPE.ALERT,
        calleeDevId: CallKitEMClient.context.jid.clientResource,
        callerDevId: ext.callerDevId,
        callId: ext.callId,
        ts: Date.now(),
        msgType: MSG_TYPE,
      },
    };
    console.log('>>>>>>>option', option);
    const msg = CallKitCreateMsgFun.create(option);
    // 调用 `send` 办法发送该透传音讯。CallKitEMClient.send(msg)
      .then((res) => {
        // 音讯胜利发送回调。console.log('answer Success', res);
      })
      .catch((e) => {
        // 音讯发送失败回调。console.log('anser Fail', e);
      });
  };
  return {sendAlertMsg,};
};
export default useSendSignalMsgs;
// 发送时调用
import useSendSignalMsgs from '../callKitManage/useSendSignalMsgs';
const {sendAnswerMsg} = useSendSignalMsgs();
const payload = {
  targetId: from,
  sendBody: ext,
};
sendAnswerMsg(payload, ANSWER_TYPE.BUSY);

到这里,对于 callKit 组件内的无关信令局部的外围代码的设计就此结束。

step4: 搭建频道内治理相干代码

频道治理是必须要做的,试想一个小场景,张三正在与李四进行音视频通话,此时王五呼叫过去,如果不做什么状态的治理,收到王五的视频邀请就立马弹出了一个邀请弹窗,然而此时张三却曾经在通话中了,那么从代码的角度讲这个曾经算是一个较为重大的 Bug 了,因而咱们必须要在频道中引入 状态治理 这个概念,这个概念的实现即不是 环信 IM层面,也不是 声网 RTC, 而是咱们本人须要实现的一个状态,比方闲暇、呼叫中、邀请中、通话中等等,咱们须要形象进去一个频道状态从而映射出用户在应用音视频通话性能中不同期间的状况,并且做出不同的逻辑层解决。

在引入状态治理的状况下,再去套用方才的场景:
张三在收到李四的通话邀请时,张三自身为闲暇状态,此时就能够回复给李四状态闲暇能够通话,李四收到张三的回复后能够调起通话待接听界面,直到张三接听后单方可进入到频道中,失常进行通话性能的应用,此时王五呼叫张三,引领收回后,张三收到邀请信令,获取以后状态为通话中,则间接依据获取的状态判断间接回复 BUSY 繁忙中,从而回绝了王五的通话邀请。

能够看到引入了频道中的状态治理概念咱们解决了音视频通话时防止状态凌乱导致的一系列问题,上面能够看下示例代码。

import {defineStore} from 'pinia';
import useSendSignalMsgs from '../callKitManage/useSendSignalMsgs';
import createUid from '../utils/createUid';
const useAgoraChannelStore = defineStore('agoraChannelStore', {state: () => ({
    emClientInfos: {
      apiUrl: '',
      appKey: '',
      loginUserId: '',
      clientResource: '',
      accessToken: '',
    },
    callKitStatus: {
      localClientStatus: CALLSTATUS.idle, //callkit 状态
      channelInfos: {
        channelName: '', // 频道名
        agoraChannelToken: '', // 频道 token
        agoraUserId: '', // 频道用户 id,
        callType: CALL_TYPES.SINGLE_VOICE, //0 语音 1 视频 2 多人音视频
        callId: null, // 会议 ID
        channelUsers: {}, // 频道内用户
        callerDevId: '', // 主叫方设施 ID
        calleeDevId: '', // 被叫方设施 ID
        callerIMName: '', // 主叫方环信 ID
        calleeIMName: '', // 被叫方环信 ID
        groupId: '', // 群组 ID
      },
      // 被邀请对象 单人为 string 多人为 array
      inviteTarget: null,
    },
  }),
  actions: {
    /* emClient */
    initEmClientInfos(emClient) {console.log('initEmClientInfos', emClient);
      if (!emClient) return;
      this.emClientInfos.apiUrl = emClient.apiUrl;
      this.emClientInfos.appKey = emClient.appKey;
      this.emClientInfos.loginUserId = emClient.user;
      this.emClientInfos.accessToken = emClient.token;
      this.emClientInfos.clientResource = emClient.clientResource;
    },
    /* CallKit status 治理 */
    // 初始化频道信息
    initChannelInfos() {
      this.callKitStatus.localClientStatus = CALLSTATUS.idle;
      this.callKitStatus.channelInfos = {
        channelName: '', // 频道名
        agoraChannelToken: '', // 频道 token
        agoraUid: '', // 频道用户 id
        callType: CALL_TYPES.SINGLE_VOICE, //0 语音 1 视频 2 多人音视频
        callId: null, // 会议 ID
        channelUsers: {}, // 频道内用户
        callerDevId: '', // 主叫方设施 ID
        calleeDevId: '', // 被叫方设施 ID
        confrontId: '', // 要解决的指标 ID
        callerIMName: '', // 主叫方环信 ID
        calleeIMName: '', // 被叫方环信 ID
        groupId: '', // 群组 ID
      };
      this.callKitStatus.inviteTarget = null;
      this.callKitTimer && clearTimeout(this.callKitTimer);
    },
    // 更新 localStatus
    updateLocalStatus(typeCode) {console.log('>>>>> 开始变更本地状态为 typeCode', typeCode);
      this.callKitStatus.localClientStatus = typeCode;
    },
    // 更新频道信息
    updateChannelInfos(msgBody) {console.log('触发更新频道信息', msgBody);
      const {from, to, ext} = msgBody || {};
      const params = {
        channelName:
          ext.channelName || this.callKitStatus.channelInfos.channelName,
        callId: ext.callId || this.callKitStatus.channelInfos.callId,
        callType:
          CALL_TYPE[ext.type] || this.callKitStatus.channelInfos.callType,
        callerDevId: ext.callerDevId || 0,
        calleeDevId: ext.calleeDevId,
        callerIMName: from,
        calleeIMName: to,
        groupId: ext?.ext?.groupId ? ext.ext.groupId : '',
      };
      console.log('%c 将要更新的信息内容为', 'color:red', params);
      Object.assign(this.callKitStatus.channelInfos, params);
    },
  },
});
export default useAgoraChannelStore;
// 频道状态应用以及变更示例代码
import useAgoraChannelStore from './stores/channelManger';
const {updateChannelInfos, updateLocalStatus} = agoraChannelStore;
const callKitStatus = computed(() => agoraChannelStore.callKitStatus);

下面示例代码,是针对频道内的状态治理演示代码,用到了 pinia 去进行状态存储以及治理,pinia 也反对在 nvue 页面中很不便的应用。

step5: 对于 callKit 可视页面的解决

对于可视组件的解决是指的是,比方在收到邀请信息时须要弹出 待接听页面 ,那么咱们就须要跳转至待接听页面,多人通话时咱们须要邀请更多人退出会议,那么咱们则须要弹出 邀请页面 ,单人以及多人通话中咱们则须要跳转至理论须要显示通话单方 音视频流的组件页面, 下面提到的几个页面就别离对应了:alertScreen.vueinviteMembers.vuemultiCall.nvuesingleCall.nvue

这些组件因为是页面级别的,因而在须要跳转至对应的页面时,未免须要进行 router 路由映射关系配置,因而咱们须要在 pages.json 中进行对应的页面地址配置,这里拿其中 alertScreen.vue 做代码演示。

pages.json 配置

{
  "path": "pages/emCallKitPages/alertScreen",
  "style": {
    "app-plus": {"titleNView": false}
  }
}

跳转至待接听页面

import useCallKitEvent from '@/components/emCallKit/callKitManage/useCallKitEvent';
const {EVENT_NAME, CALLKIT_EVENT_CODE, SUB_CHANNEL_EVENT} = useCallKitEvent();
SUB_CHANNEL_EVENT(EVENT_NAME, (params) => {const { type, ext, callType, eventHxId} = params;
  console.log('>>>>>> 订阅到 callkit 事件公布', params);
  // 弹出待接听事件
  switch (type.code) {
    case CALLKIT_EVENT_CODE.ALERT_SCREEN:
      {
        // 跳转至待接听页面
        uni.navigateTo({url: '../emCallKitPages/alertScreen',});
      }
      break;
    default:
      break;
  }
});

从待接听页面抉择接听后的跳转

在待接听页面,点击接听后,应该是怎么的逻辑解决?

const agreeJoinChannel = () => {handleSendAnswerMsg(ANSWER_TYPE.ACCPET);
  if (channelInfos.value.callType === CALL_TYPES.MULTI_VIDEO) {
    uni.redirectTo({url: '/pages/emCallKitPages/multiCall',});
  } else {enterSingleCallPage();
  }
};
const enterSingleCallPage = () => {
  uni.redirectTo({url: '/pages/emCallKitPages/singleCall',});
};

能够看到下面的演示代码做了两种通话大类(单人、多人)不同的页面跳转。

上面咱们看下通话中的视图页面是怎么的(singleCall 为例),同样代码做了一部分的删减。

<template>
  <div class="single_call_container">
    <!-- 视频视图 -->
    <view
      class="rtc_view_container"
      v-if="callKitStatus.channelInfos.callType === CALL_TYPES.SINGLE_VIDEO"
    >
      <view class="local_container">
        <rtc-surface-view
          v-if="state.engine"
          class="local_view_stream"
          :uid="0"
          :zOrderMediaOverlay="true"
        ></rtc-surface-view>
      </view>
      <view class="remote_container">
        <rtc-surface-view
          class="remote_view_stream"
          :uid="state.remoteUid"
        ></rtc-surface-view>
      </view>
    </view>
    <!-- 语音视图 -->
    <view
      class="rtc_voice_container"
      v-if="callKitStatus.channelInfos.callType === CALL_TYPES.SINGLE_VOICE"
    >
      <view class="circleBodyView">
        <image
          class="circleItemAvatar"
          src="/static/emCallKit/theme2x.png"
        ></image>
        <view class="circleCenter"
          ><text class="cenametext"
            >{{ callKitStatus.inviteTarget ||
            callKitStatus.channelInfos.callerIMName }}</text
          >
          <text class="centertext"> 正在语音通话…</text>
        </view>
      </view>
    </view>
    <!-- 页面管制 -->
    <view class="rtc_control">
      <view class="circleBoxView">
        <text class="hint">{{formatTime}}</text>
      </view>
      <view class="circleBoxView">
        <view class="circleBox" @click="onSwitchLocalMicPhone">
          <image
            class="circleImg"
            :src="
              state.isMuteLocalAudioStream
                ? '/static/emCallKit/icon_video_quiet.png'
                : '/static/emCallKit/icon_video_microphone.png'
            "
          ></image>
          <text class="hint"> 麦克风 </text>
        </view>
        <view class="circleBox" @click="onSwitchSperkerPhone">
          <image
            class="circleImg"
            :src="
              state.isSwitchSperkerPhone
                ? '/static/emCallKit/icon_video_speaker.png'
                : '/static/emCallKit/icon_video_speakerno.png'
            "
          ></image>
          <text class="hint"> 扬声器 </text>
        </view>
        <view
          v-if="callKitStatus.channelInfos.callType === CALL_TYPES.SINGLE_VIDEO"
          class="circleBox"
          @click="onSwitchLocalCameraOpened"
        >
          <image
            class="circleImg"
            :src="
              state.isSwitchLocalCameraOpened
                ? '/static/emCallKit/icon_video_speaker.png'
                : '/static/emCallKit/icon_video_speakerno.png'
            "
          ></image>
          <text class="hint"> 摄像头 </text>
        </view>
      </view>
      <view class="circleBoxView">
        <view class="circleBox" @click="leaveChannel">
          <image
            class="circleImg"
            src="/static/emCallKit/icon_video_cancel.png"
          ></image>
          <text class="hint"> 挂断 </text>
        </view>
      </view>
      <image
        v-if="callKitStatus.channelInfos.callType === CALL_TYPES.SINGLE_VIDEO"
        class="switchCamera"
        @click="onSwitchCamera"
        src="/static/emCallKit/iconxiangjifanzhuan.png"
      ></image>
    </view>
  </div>
</template>
<script setup>
import {ref, reactive, computed} from 'vue';
import {onLoad, onUnload} from '@dcloudio/uni-app';
import {AGORA_APP_ID} from '@/components/emCallKit/config/index.js';
import {CALLSTATUS, CALL_TYPES} from '@/components/emCallKit/contants';
import RtcEngine, {RtcChannel} from '@/components/Agora-RTC-JS/index';
import {
  ClientRole,
  ChannelProfile,
} from '@/components/Agora-RTC-JS/common/Enums';
import RtcSurfaceView from '@/components/Agora-RTC-JS/RtcSurfaceView';
import useAgoraChannelStore from '@/components/emCallKit/stores/channelManger';

// 获取挪动端受权权限
import permision from '@/js_sdk/wa-permission/permission';
//store
const agoraChannelStore = useAgoraChannelStore();
//channelInfos
const callKitStatus = computed(() => {return agoraChannelStore.callKitStatus;});
//channelName
const channelName = computed(() => agoraChannelStore.callKitStatus.channelInfos?.channelName
);
const state = reactive({
  engine: undefined,
  channelId: '',
  isJoined: false,
  remoteUid: '',
  isSwitchCamera: true,
  isSwitchSperkerPhone: true,
  isMuteLocalAudioStream: false,
  isSwitchLocalCameraOpened: true,
});
// 开启通话计时
const inChannelTimer = ref(null);
const timeCount = ref(0);
const startInChannelTimer = () => {inChannelTimer.value && clearInterval(inChannelTimer.value);
  inChannelTimer.value = setInterval(() => {
    timeCount.value++;
    // console.log('%c 通话计时开启中...', 'color:green', timeCount);
  }, 1000);
};
// 转换为可间接渲染的工夫
const formatTime = computed(() => {const m = Math.floor(timeCount.value / 60);
  const s = timeCount.value % 60;
  const h = Math.floor(m / 60);
  const remMin = m % 60;
  return `${h > 0 ? h + ':' : ''}${remMin < 10 ?'0' + remMin : remMin}:${s < 10 ? '0' + s : s}`;
});
// 频道监听
const addListeners = () => {state.engine.addListener('JoinChannelSuccess', (channel, uid, elapsed) => {console.info('JoinChannelSuccess', channel, uid, elapsed);
    state.isJoined = true;
  });
  state.engine.addListener('UserJoined', (uid, elapsed) => {console.info('UserJoined', uid, elapsed);
    state.remoteUid = uid;
  });
  state.engine.addListener('UserOffline', (uid, reason) => {console.info('UserOffline', uid, reason);
    state.remoteUid = '';
    state.isJoined = false;
    leaveChannel();});
  state.engine.addListener('LeaveChannel', (stats) => {console.info('LeaveChannel', stats);
    state.isJoined = false;
    state.remoteUid = '';
  });
};
// 放弃屏幕常亮
uni.setKeepScreenOn({keepScreenOn: true,});
// 初始化频道实例
const initEngine = async () => {console.log('>>>>>>> 初始化声网 RTC');

  state.engine = await RtcEngine.create(AGORA_APP_ID);
  addListeners();
  if (uni.getSystemInfoSync().platform === 'android') {await permision.requestAndroidPermission('android.permission.RECORD_AUDIO');
    await permision.requestAndroidPermission('android.permission.CAMERA');
  }
  await state.engine.enableVideo();
  await state.engine.startPreview();
  await state.engine.setChannelProfile(ChannelProfile.LiveBroadcasting);
  await state.engine.setClientRole(ClientRole.Broadcaster);
  // 设置频道麦克风为扬声器模式
  await state.engine.setDefaultAudioRoutetoSpeakerphone(true);
  await joinChannel();};

// 退出频道
const joinChannel = async () => {let { accessToken, agoraUserId} =
    await agoraChannelStore.requestRtcChannelToken();
  console.log(
    '>>>>>> 频道 token 申请实现',
    accessToken,
    agoraUserId,
    channelName.value
  );
  (await state.engine) &&
    state.engine.joinChannel(accessToken, channelName.value, null, agoraUserId);
  startInChannelTimer();};
// 挂断
const leaveChannel = async () => {(await state.engine) && state.engine.leaveChannel();
  uni.navigateBack({delta: 1});
  // 设置本地状态为闲置
  agoraChannelStore.updateLocalStatus(CALLSTATUS.idle);
  uni.showToast({
    icon: 'none',
    title: ` 通话完结【${formatTime.value}】`,
  });
};
// 切换摄像头
const onSwitchCamera = () => {
  state.engine &&
    state.engine
      .switchCamera()
      .then(() => {state.isSwitchCamera = !state.isSwitchCamera;})
      .catch((err) => {console.warn('switchCamera', err);
      });
};
// 切换扬声器
const onSwitchSperkerPhone = async () => {
  try {(await state.engine) &&
      state.engine.setEnableSpeakerphone(!state.isSwitchSperkerPhone);
    state.isSwitchSperkerPhone = !state.isSwitchSperkerPhone;
  } catch (error) {uni.showToast({ icon: 'none', title: '扬声器切换失败!'});
  }
};
// 开启敞开本地麦克风采集
const onSwitchLocalMicPhone = async () => {
  try {(await state.engine) &&
      state.engine.muteLocalAudioStream(!state.isMuteLocalAudioStream);
    state.isMuteLocalAudioStream = !state.isMuteLocalAudioStream;
  } catch (error) {uni.showToast({ icon: 'none', title: '开关本地麦克风采集失败!'});
  }
};
// 开启敞开本地视频流采集
const onSwitchLocalCameraOpened = async () => {
  try {(await state.engine) &&
      state.engine.enableLocalVideo(!state.isSwitchLocalCameraOpened);
    state.isSwitchLocalCameraOpened = !state.isSwitchLocalCameraOpened;
  } catch (error) {uni.showToast({ icon: 'none', title: '开关本地摄像头采集失败!'});
  }
};

onLoad(() => {console.log('+++++++singleCall onLoad');
  initEngine();});
onUnload(() => {state.engine && state.engine.destroy();
  state.isJoined = false;
  // 卸载组件革除通话计时
  // 革除通话计时
  inChannelTimer.value && clearInterval(inChannelTimer.value);
});
</script>

外围的流展现则是 Agora-UniApp 原生插件提供的 RtcSurfaceView 组件通过该组件进行本地流和远端流的展现。

在 nvue 组件中提几个点,能够关注一下。

  • 安卓机型,在公布本地流之前须要拿到用户对于录音以及摄像头的受权,否则无奈失常的进行推流展现。具体的受权 js 调用插件,关注 wa-permission 这个插件。
  • 默认音视频通话会追随零碎息屏工夫主动息屏,不心愿息屏则能够调用 uni-app 提供的 apiuni.setKeepScreenOn({keepScreenOn: true,});
  • 引入原生插件后必须打包为自定义调试基座才能够看到具体的成果,否则不会展现画面。

到这里可视页面的相干代码以及所需配置介绍临时告一段落。
上面再看下邀请相干逻辑。

step6: 对于 callKit 邀请相干逻辑的介绍。

如果作为邀请方也就是音视频性能的发起方,咱们如何应用 callKit 内的代码实现这一动作?

<template>
  <view>
    <uv-popup ref="invitePopup" mode="bottom" round="10">
      <view class="invite_btn_box">
        <text
          class="invite_func_btn"
          @click="sendAvCallMessage(CALL_TYPES.SINGLE_VIDEO)"
          > 视频通话 </text
        >
        <text
          class="invite_func_btn"
          @click="sendAvCallMessage(CALL_TYPES.SINGLE_VOICE)"
          > 语音通话 </text
        >

        <text class="invite_func_btn invite_func_btn_cannel" @click="onCannel"
          > 勾销 </text
        >
      </view>
    </uv-popup>
  </view>
</template>
<script setup>
import {ref, inject} from 'vue';
import useAgoraChannelStore from '@/components/emCallKit/stores/channelManger';
import {CALL_TYPES} from '@/components/emCallKit/contants';
import onFeedTap from '@/utils/feedTap';
const agoraChannelStore = useAgoraChannelStore();
const injectTargetId = inject('targetId');
const invitePopup = ref(null);
const openInvitePopup = () => {invitePopup.value.open();
};
const closeInvitePopup = () => {invitePopup.value.close();
};
const onCannel = () => {onFeedTap && onFeedTap();
  closeInvitePopup();};
const sendAvCallMessage = async (callType) => {onFeedTap && onFeedTap();
  try {await agoraChannelStore.sendInviteMessage(injectTargetId.value, callType);
    uni.navigateTo({url: '/pages/emCallKitPages/alertScreen',});
  } catch (error) {console.log('>>>> 通话邀请发动失败', error);
    uni.showToast({
      icon: 'none',
      title: '通话发动失败',
    });
  } finally {closeInvitePopup();
  }
};

defineExpose({openInvitePopup,});
</script>

在理论的 Demo 中减少了一个 inviteAvcall.vue 组件在外层点击某个 icon 时展现该 Popup 组件,弹出视频邀请或音频邀请的选项。
成果如下:

点击时传入对应的类型邀请信令发送给要邀请的指标一条文本邀请信息。

而多人音视频模式下,邀请下则不须要弹出待接听页面,而是进入勾选要发送邀请信息的成员页面,发送邀请并创立频道并退出即可,就像这样。

const inviteAvcallComp = ref(null);
const selectAvcallType = () => {closeAllModal();
  if (injectChatType.value === 'groupChat') {
    uni.navigateTo({url: `/pages/emCallKitPages/inviteMembers?groupId=${injectTargetId.value}`,
    });
  } else {inviteAvcallComp.value && inviteAvcallComp.value.openInvitePopup();
  }
};

页面成果展现

相干链接

环信 uni-app 文档地址

本文源码地址

声网音视频插件材料相干地址

退出移动版