实现音视频通话
更新时间: 2024/07/26 13:39:23
自 IM UIKit v9.4.0 开始,会话消息模块(chat-kit
)支持音视频通话功能。该功能基于云信呼叫组件实现。
本文介绍如何引入和初始化呼叫组件,进而在您的 IM 应用中实现一对一音视频通话。
功能介绍
实现音视频通话功能后,用户在会话界面的输入区域点击 即可快速发起语音通话或视频通话。
前提条件
实现流程
步骤1:引入呼叫组件
-
通过 NPM 方式将 IM UIKit 安装到您的 Web 项目中。
示例代码如下:
npm install @xkit-yx/call-kit-react-ui npm install @xkit-yx/call-kit
目前仅支持 npm 方式集成,暂不支持 CDN 方式集成。
-
将
CallViewProvider
、CallViewProviderRef
组件以及样式文件导入到您的 React 项目中。示例代码如下:
import { CallViewProvider, CallViewProviderRef } from '@xkit-yx/call-kit-react-ui' import '@xkit-yx/call-kit-react-ui/es/style/index'
步骤2:初始化呼叫组件
在发起音视频通话前,需先初始化呼叫组件。
-
在函数组件内,为了引用
CallViewProvider
组件,需要先创建一个callViewProviderRef
(类型为CallViewProviderRef
)。 -
使用
CallViewProvider
组件时,将 IM SDK 实例、AppKey 和当前用户账号等参数传递给neCallConfig
配置对象。 -
使用
CallViewProvider
包裹 IMUIKit 根组件。
示例代码如下:
jsx/tsximport { 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
。
示例代码如下:
jsx/tsxuseEffect(() => {
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
方法,发起一对一语音通话或视频通话。
示例代码如下:
jsx/tsxconst 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
进行自定义渲染音视频消息内容。如下图所示:示例代码如下:
jsx/tsx
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('-'), } }
-
自定义渲染音视频消息发送按钮:
如下图所示:
示例代码如下:
jsx/tsx
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} />
相关信息
其他功能开通
如果需要实现“屏蔽黑名单用户发起的语音/视频通话请求”,需要在云信控制台开启该功能。 如未开通该功能,黑名单用户仍可以向将其拉黑的用户发起通话请求。
-
在控制台首页应用管理中选择应用,然后单击 IM 即时通讯下的功能配置按钮进入功能配置页。
-
在顶部选择基础功能页签,开启被拉黑时被拉黑者无法唤起呼叫。
错误码
呼叫组件相关错误码,请参见错误码。
常见问题
具体请参见呼叫组件常见问题。