实现音视频通话

更新时间: 2024/05/10 11:33:26

自 IM UIKit v9.4.0 开始,会话消息模块(chat-kit)支持音视频通话功能。该功能基于云信呼叫组件实现。

本文介绍如何引入和初始化呼叫组件,进而在您的 IM 应用中实现一对一音视频通话。

功能介绍

实现音视频通话功能后,用户在会话界面的输入区域点击 即可快速发起语音通话或视频通话。

前提条件

实现流程

步骤1:引入呼叫组件

  1. 通过 NPM 方式将 IM UIKit 安装到您的 Web 项目中。

    示例代码如下:

    npm install @xkit-yx/call-kit-react-ui
    npm install @xkit-yx/call-kit
    

    目前仅支持 npm 方式集成,暂不支持 CDN 方式集成。

  2. CallViewProviderCallViewProviderRef 组件以及样式文件导入到您的 React 项目中。

    示例代码如下:

    import { CallViewProvider, CallViewProviderRef } from '@xkit-yx/call-kit-react-ui'
    import '@xkit-yx/call-kit-react-ui/es/style/index'
    

步骤2:初始化呼叫组件

在发起音视频通话前,需先初始化呼叫组件。

  1. 在函数组件内,为了引用 CallViewProvider 组件,需要先创建一个 callViewProviderRef (类型为 CallViewProviderRef)。

  2. 使用 CallViewProvider 组件时,将 IM SDK 实例、AppKey 和当前用户账号等参数传递给 neCallConfig 配置对象。

  3. 使用 CallViewProvider 包裹 IMUIKit 根组件。

示例代码如下:

import { useStateContext } from '@xkit-yx/im-kit-ui/src'
const callViewProviderRef = useRef<CallViewProviderRef>(null)
// store实例和im sdk实例
const { store,nim } = useStateContext()
<CallViewProvider
  ref={callViewProviderRef}
  neCallConfig={{
    nim: nim.nim, //IM SDK实例
    appKey: appkey, //您的App Key
    debug: true,
  }}
  position={{
    x: 400,
    y: 10,
  }}
>
  <IMApp>
</CallViewProvider>

步骤3:监听音视频通话相关事件

useEffect 中,使用 callViewProviderRef.current 可以获取到 neCall 实例,可对呼叫过程中事件进行监听,例如注册呼叫结束事件 onMessageSent,以及设置呼叫超时时间 setTimeout

示例代码如下:

useEffect(() => {
    if (callViewProviderRef.current?.neCall) {
      //注册呼叫结束事件监听
      callViewProviderRef.current?.neCall?.on('onRecordSend', (options) => {
         const sessionId = store.uiStore.selectedSession
         // @ts-ignore 消息列表增加话单消息
         store.msgStore.addMsg(sessionId, [options])
         // @ts-ignore 使增加的消息出现在视野可见区域
         document.getElementById(options.idClient)?.scrollIntoView()
      })
      // 设置呼叫超时时间
      callViewProviderRef.current?.neCall?.setTimeout(30)
    }
  }, [callViewProviderRef.current?.neCall])

步骤4:发起音视频通话

云信已将呼叫组件的语音通话和视频通话能力绑定在 handleCall 中。因此您可直接调用 callViewProviderRef.current.call 方法,发起一对一语音通话或视频通话。

示例代码如下:

javaconst handleCall = useCallback(
  //当前选中会话场景 scene: p2p | team 会话接受方 to
  const {scene,to} = parseSessionId(store.uiStore.selectedSession)
  // callType '1' 为语音通话  '2'为视频通话
  async (callType) => {
    try {
      await callViewProviderRef.current?.call?.({ accId: to, callType })
      setCallingVisible(false)
    } catch (error) {
      console.log('=========error======', error)
      switch (error.code) {
        case '105':
          message.error(t('inCallText'))
          break
        case 'Error_Internet_Disconnected':
          message.error(t('networkDisconnectText'))
          break
        default:
          message.error(t('callFailed'))
          break
      }
    }
  },
  [to]
)

<div onClick={() => handleCall('1')}>发起呼叫</div>

步骤5:自定义渲染消息内容和发送按钮

  • 自定义渲染音视频消息内容:

    msg.type 为 g2,表示音视频消息,使用 renderP2pCustomMessage 进行自定义渲染音视频消息内容。如下图所示:

    示例代码如下:
    const renderP2pCustomMessage = useCallback(
        (msg) => {
          msg = msg.msg
          // msg.type 为 g2 代表的是话单消息 renderP2pCustomMessage 返回 null 就会按照组件默认的逻辑进行展示消息
          if (msg.type !== 'g2' || sdkVersion == 2) {
            return null
          }
          const { attach } = msg
          const duration = attach?.durations[0]?.duration
          const status = attach?.status
          const type = attach?.type
          const icon = type == 1 ? 'icon-yuyin8' : 'icon-shipin8'
          const myAccount = store.userStore.myUserInfo.account
          const isSelf = msg.from === myAccount
          const account = isSelf ? myAccount : to
          return (
            <div className={classNames('wrapper', { 'wrapper-self': isSelf })}>
              // 头像组件
              <ComplexAvatarContainer account={account} />
              <div
                className={classNames('g2-msg-wrapper', {
                  'g2-msg-wrapper-self': isSelf,
                })}
              >
                <div className="appellation">
                  // 用户昵称 优先级按照 备注 > 群昵称 > 好友昵称 > 好友账号 返回
                  {store.uiStore.getAppellation({ account })}
                </div>
                <div
                  className={classNames('g2-msg', { 'g2-msg-self': isSelf })}
                  onClick={() => handleCall(type.toString())}
                >
                  <i className={classNames('iconfont', 'g2-icon', icon)}></i>
                  <span>{g2StatusMap[status]}</span>
                  {duration && (
                    <span className="g2-time">
                      {convertSecondsToTime(duration)}
                    </span>
                  )}
                </div>
                <div className="time">{renderMsgDate(msg.time)}</div>
              </div>
            </div>
          )
        },
        [
          handleCall,
          sdkVersion,
          to,
          store.uiStore,
          store.userStore.myUserInfo.account,
        ]
      )
      //...
      <ChatContainer
          //...
          renderP2pCustomMessage={renderP2pCustomMessage}
        />
    
    //call.tsx
    const Call: FC<IProps> = ({ handleCall }) => {
      return (
        <div>
          <div
            onClick={() => handleCall(callTypeMap['audio'])}
            className="calling-item"
          >
            <i className="calling-item-icon iconfont icon-yuyin8" />
            <span>{t('voiceCallText')}</span>
          </div>
          <div
            onClick={() => handleCall(callTypeMap['vedio'])}
            className="calling-item"
          >
            <i className="calling-item-icon iconfont icon-shipin8" />
            <span>{t('vedioCallText')}</span>
          </div>
        </div>
      )
    }
    // 以下是一些Util 方法
    //话单类型
    const g2StatusMap = {
      1: t('callDurationText'),
      2: t('callCancelText'),
      3: t('callRejectedText'),
      4: t('callTimeoutText'),
      5: t('callBusyText'),
    }
    
    const callTypeMap = {
      audio: '1',
      vedio: '2',
    }
    /**
    * 格式化时间
    */
    const renderMsgDate = (time) => {
      const date = moment(time)
      const isCurrentDay = date.isSame(moment(), 'day')
      const isCurrentYear = date.isSame(moment(), 'year')
      return isCurrentDay
        ? date.format('HH:mm:ss')
        : isCurrentYear
        ? date.format('MM-DD HH:mm:ss')
        : date.format('YYYY-MM-DD HH:mm:ss')
    }
    
    /**
    * 秒转换为时分秒
    */
    const convertSecondsToTime = (seconds: number): string => {
      const hours: number = Math.floor(seconds / 3600)
      const minutes: number = Math.floor((seconds - hours * 3600) / 60)
      const remainingSeconds: number = seconds - hours * 3600 - minutes * 60
    
      let timeString = ''
      const includeHours = seconds >= 3600
      if (includeHours) {
        if (hours < 10) {
          timeString += '0'
        }
        timeString += hours.toString() + ':'
      }
      if (minutes < 10) {
        timeString += '0'
      }
      timeString += minutes.toString() + ':'
      if (remainingSeconds < 10) {
        timeString += '0'
      }
      timeString += remainingSeconds.toString()
      return timeString
    }
    
    /**
    * 解析 sessionId,形如 scene-accid
    */
    export const parseSessionId = (
      sessionId: string
    ): { scene: string; to: string } => {
      const [scene, ...to] = sessionId.split('-')
      return {
        scene,
        // 这样处理是为了防止有些用户 accid 中自带 -
        to: to.join('-'),
      }
    }
    
  • 自定义渲染音视频消息发送按钮:

    如下图所示:

    示例代码如下:
    const [callingVisible, setCallingVisible] = useState<boolean>(false)
      const actions = useMemo(
        () => [
          {
            action: 'emoji',
            visible: true,
          },
          {
            action: 'sendImg',
            visible: true,
          },
          {
            action: 'sendFile',
            visible: true,
          },
          {
            action: 'calling',
            visible: scene === 'team' || sdkVersion === 2 ? false : true,
            render: () => {
              return (
                <Button type="text" disabled={false}>
                  <Popover
                    trigger="click"
                    visible={callingVisible}
                    content={<Calling handleCall={handleCall} />}
                    onVisibleChange={(newVisible) => setCallingVisible(newVisible)}
                  >
                    <i className="calling-icon iconfont icon-shipinyuyin" />
                  </Popover>
                </Button>
              )
            },
          },
        ],
        [handleCall, callingVisible, scene, sdkVersion]
      )
      
      //...
      <ChatContainer
          //...
          actions={actions}
          renderP2pCustomMessage={renderP2pCustomMessage}
        />
    

相关信息

其他功能开通

如果需要实现“屏蔽黑名单用户发起的语音/视频通话请求”,需要在云信控制台开启该功能。 如未开通该功能,黑名单用户仍可以向将其拉黑的用户发起通话请求。

  1. 控制台首页应用管理选择应用进入应用配置页面,然后单击 IM即时通讯 专业版下的功能配置按钮进入 IM 即时通讯配置页。

  2. 在顶部选择基础功能页签,开启被拉黑时被拉黑者无法唤起呼叫

错误码

呼叫组件相关错误码,请参见错误码

常见问题

具体请参见呼叫组件常见问题

此文档是否对你有帮助?
有帮助
去反馈
  • 功能介绍
  • 前提条件
  • 实现流程
  • 步骤1:引入呼叫组件
  • 步骤2:初始化呼叫组件
  • 步骤3:监听音视频通话相关事件
  • 步骤4:发起音视频通话
  • 步骤5:自定义渲染消息内容和发送按钮
  • 相关信息
  • 其他功能开通
  • 错误码
  • 常见问题