从零搭建音视频通话太痛苦?这个 Vue3 CallKit 让你 5 分钟搞定 1v1 + 群聊通话

1 阅读8分钟

📌 声明:本篇文章基于 Easemob Chat CallKit Vue3 开源项目由 AI 辅助生成。

如果你正在 Vue3 项目中集成音视频通话功能,却被信令协议、状态管理、多端同步、UI 布局搞得焦头烂额——Easemob Chat CallKit Vue3 可能是你一直在找的答案。基于环信 IM + 声网 RTC,内置完整的呼叫、接听、挂断、群聊网格、媒体控制能力,真正的开箱即用


😫 我们先聊聊:自建音视频通话,到底难在哪?

做过实时音视频的同学应该都有体会,这玩意儿看起来只是"接个 SDK",真动手才发现坑一个接一个:

1. 信令层 = 自己造轮子

IM 消息和 RTC 是完全两套系统。呼叫、接听、拒绝、挂断、超时、占线……每一个动作都要自己定义信令格式、处理发送失败、重连补偿、离线消息过滤。稍不留神,两端状态就对不上——我这边显示"通话中",对方那边已经挂了。

2. 状态管理是灾难

单聊还勉强能搞个 isCalling flag,群聊直接懵圈:谁加入了、谁拒绝了、谁掉线了、谁在响铃中、视频轨道谁发布了……状态一多,Vue 的响应式系统开始疯狂重渲染,内存泄漏和竞态条件接踵而至。

3. UI 实现成本被严重低估

视频通话的 UI 不是简单放个 <video> 标签。单聊要有悬浮窗、最小化、拖拽、画中画;群聊要有九宫格、主视频模式、说话者高亮、成员管理。这些交互写起来没半个月下不来。

4. 单聊和群聊像是两个世界

单聊是二元状态机(呼叫方 ↔ 被叫方),群聊是分布式参与者集合。两者的信令协议、状态模型、UI 布局完全不同,很多团队最后不得不维护两套代码。


🎯 CallKit 解决什么问题?一句话:把上面这些坑全部填平

Easemob Chat CallKit Vue3 是环信官方推出的音视频通话 UI 组件库,基于 Vue 3 + 环信 IM SDK + 声网 RTC SDK,把信令、状态、UI 全部封装好,开发者只需要关心三件事:

  1. 我已经登录了环信 IM(你本来就要做聊天功能对吧?)
  2. 我要呼叫谁(传一个 userId 或 groupId)
  3. 我要监听什么事件(通话结束记个时长、写条消息)

其他的一切——信令收发、RTC 频道管理、视频渲染、邀请弹窗、通话计时、静音/摄像头切换——全部内置


🚀 5 分钟接入:从安装到打通第一通电话

安装依赖

# 安装 CallKit(以及你项目里已有的 IM 和 RTC SDK)
pnpm add easemob-chat-callkit-vue3 easemob-websdk agora-rtc-sdk-ng

Step 1:注册插件

// main.ts
import { createApp } from 'vue'
import EasemobChatCallKit from 'easemob-chat-callkit-vue3'
import 'easemob-chat-callkit-vue3/style.css'
import App from './App.vue'

const app = createApp(App)
app.use(EasemobChatCallKit)
app.mount('#app')

Step 2:在根组件放置 Provider + 通话组件

<template>
  <EasemobChatCallKitProvider :chat-client="chatClient" :init-config="{ logLevel: 2 }">
    <!-- 你的应用内容 -->
    <router-view />

    <!-- 被叫邀请弹窗(自动弹出,无需 v-if) -->
    <InvitationNotification />

    <!-- 单人通话组件(自动显隐) -->
    <EasemobChatSingleCall />

    <!-- 群组通话组件(自动显隐) -->
    <EasemobChatMultiCall :group-id="currentGroupId" />
  </EasemobChatCallKitProvider>
</template>

<script setup lang="ts">
import {
  EasemobChatCallKitProvider,
  InvitationNotification,
  EasemobChatSingleCall,
  EasemobChatMultiCall,
} from 'easemob-chat-callkit-vue3'

// 你已经有的环信 IM 实例
const chatClient = /* 你的 easemob-websdk Connection */
const currentGroupId = /* 当前群组 ID */
</script>

Step 3:发起通话

<script setup lang="ts">
import { useCallKit } from 'easemob-chat-callkit-vue3'

const { call, groupCall, hangup, accept, reject } = useCallKit()

// ── 发起 1v1 视频通话 ──
await call({
  targetId: 'user123',
  type: 'video',
  userInfo: {
    nickname: '张三',
    avatarURL: 'https://example.com/avatar.png'
  }
})

// ── 发起群组视频会议 ──
await groupCall({
  groupId: 'group001',
  members: ['user1', 'user2', 'user3'],
  type: 'video',
  groupName: '产品周会',
})

// ── 挂断 ──
await hangup()
</script>

就这三步。 不需要手动创建 RTC 频道,不需要处理信令消息,不需要写 v-if 控制组件显示隐藏。EasemobChatSingleCall 会根据内部状态自动出现和消失。


🎧 监听通话生命周期,集成到你的业务

通话结束后想记个时长?收到来电想响个铃?用 useCallKitEvents() 一站式订阅:

import { useCallKitEvents, HANGUP_REASON } from 'easemob-chat-callkit-vue3'
import { onUnmounted } from 'vue'

const {
  onCallStarted,
  onCallEnded,
  onIncomingCall,
  onCallRefused,
  getCallRecord,   // ← 自动生成标准化通话记录
} = useCallKitEvents()

// 通话接通
const unbind1 = onCallStarted((e) => {
  console.log('通话开始', e.callId, '频道:', e.channel)
})

// 通话结束 → 自动获取通话记录,插入消息列表
const unbind2 = onCallEnded((e) => {
  const sec = Math.round(e.duration / 1000)
  console.log('通话结束,时长:', sec, '秒,原因:', e.reason)

  // 直接拿到标准化记录,无需自己拼凑字段
  const record = getCallRecord()
  // record = { callId, conversationId, chatType, from, to, status, duration, timestamp, endedBy }
  // 可以直接插入本地消息或发送 custom 消息
})

// 组件卸载时解绑,防止内存泄漏
onUnmounted(() => { unbind1(); unbind2() })

所有事件都携带 conversationIdisLocallocalUserRole 字段——你再也不用自己推断"这是单聊还是群聊""这是本端触发还是对端触发"。


🏗️ 设计思路:为什么 CallKit 能做得这么薄?

很多开发者会担心:"封装得这么彻底,灵活性会不会很差?" 不会。CallKit 的架构设计核心就一句话:"该隔离的隔离,该共享的共享"

四层架构模型

┌─────────────────────────────────────────────┐
│              UI 层(完全隔离)                │
│   SingleCall (1v1 悬浮窗) │ GroupCallShell   │
│   (自动显隐/拖拽/画中画)   │ (九宫格/主视频)   │
└─────────────────────────────────────────────┘
                     ↕
┌─────────────────────────────────────────────┐
│           状态层(领域隔离 + 共享)            │
│  SingleCallStore  │  GroupCallStore          │
│  (二元状态机)      │  (分布式参与者集合)       │
│                   ↕                          │
│         GlobalCallStore(跨域共享)           │
│         userInfoMap / isMinimized            │
└─────────────────────────────────────────────┘
                     ↕
┌─────────────────────────────────────────────┐
│           服务层(无状态,纯原子能力)          │
│   SignalingService    │   RtcChannelService  │
│   (发/收信令)          │   (join/leave/track) │
└─────────────────────────────────────────────┘
                     ↕
┌─────────────────────────────────────────────┐
│           基础设施层(外部 SDK)               │
│        环信 IM SDK  │  声网 RTC SDK          │
└─────────────────────────────────────────────┘

关键设计决策

层级策略理由
UI 层单聊/群聊彻底隔离单聊是"一对一窗口+拖拽",群聊是"多方网格+主视频",交互模式差异巨大,强行复用只会增加复杂度
状态层领域隔离 + GlobalCallStore 共享单聊是二元状态机(IDLE → INVITING → IN_CALL → IDLE),群聊是分布式参与者集合;但 userInfoMap(头像/昵称)、isMinimized 需要跨域共享
服务层无状态共享sendInviteMessagejoinChannelcreateAudioTrack 是通用能力,不应绑定任何业务状态
基础设施单实例共享IM 连接和 RTC 客户端各一个实例,避免资源浪费

自动显隐:开发者不用写 v-if

单聊组件内置了状态驱动的显隐逻辑:

  • INVITING(呼叫中)→ 显示等待界面
  • ALERTING(被叫响铃)→ 不显示,由 InvitationNotification 接管弹窗
  • IN_CALL(通话中)→ 显示视频流界面
  • IDLE(空闲)→ 自动隐藏

这意味着你把组件往模板里一放,剩下的交给 CallKit。

类型安全的事件系统

不像传统 EventBus 的 any 类型,useCallKitEvents() 提供完全类型化的事件订阅:

onCallEnded((e) => {
  e.reason      // HANGUP_REASON 枚举,有代码提示
  e.duration    // number,毫秒
  e.conversationId // string,单聊=对方ID,群聊=groupId
  e.isLocal     // boolean,true=本端挂断
})

每个事件都返回解绑函数,组件卸载时自动清理,告别内存泄漏。

过期信令自动过滤

多端登录、离线重连时,过期的邀请/取消信令会堆积。CallKit 内置了时间戳过期判断(invite 40s 阈值,cmd 60s 阈值),自动丢弃过期消息,避免"幽灵弹窗"。


📦 更多能力一览

特性说明
📞 单人通话1v1 音频/视频,支持呼叫、接听、拒绝、挂断
👥 群组通话多人音视频,支持邀请成员、视频网格布局
🔔 邀请通知被叫方自动弹出接听/拒绝弹窗
🎛️ 媒体控制静音、开关摄像头、切换前后置
🖼️ 视频布局单聊悬浮窗+最小化;群聊九宫格/主视频模式
🎯 自动显隐根据通话状态自动显示/隐藏,无需手动 v-if
📊 通话记录getCallRecord() 自动生成标准化记录
🔧 源码调试支持 Vite alias 映射到源码,开发时热更新

📝 适用场景

  • 社交 App:1v1 语音/视频通话、多人语音房
  • 在线教育:1v1 答疑、小班课视频互动
  • 远程医疗:医患视频问诊
  • 企业协作:群组视频会议、远程面试
  • 任何已有环信 IM 的项目:聊天功能已经用了环信,直接叠加通话能力

😁效果图片

待接听

image.png

视频通话中

image.png

群组通话中

image.png

被叫待接听

image.png

🔗 相关链接

资源链接
📘 GitHub 仓库github.com/Easemob-Com…
📖 完整 API 文档USAGE.md
🚀 体验地址线上地址
🏢 环信官网注册www.easemob.com/
📦 npm 包easemob-chat-callkit-vue3

💡 写在最后

音视频通话是现代应用的标配,但自建成本极高。Easemob Chat CallKit Vue3 的思路是:把通用能力下沉到组件库,把业务逻辑留给开发者。你只需要关心"什么时候发起通话"和"通话结束后做什么",中间最复杂的信令协商、状态同步、视频渲染,全部封装在组件内部。

如果你正在用 Vue3 开发即时通讯应用,或者正为集成音视频通话头疼,不妨试试这个方案。注册环信账号、创建应用、获取 App Key,几分钟就能跑通第一通电话。

环信官网注册入口 👉 www.easemob.com/

注册后即可创建应用,免费体验完整的 IM + 音视频通话能力。


本文介绍的 Easemob Chat CallKit Vue3 基于 MIT 协议开源,欢迎 Star 和 PR。