基于 LiveKit 实现一对一视频通话:从信令到媒体进房的完整实践

适用读者:有 WebRTC / IM 基础,正在做私聊音视频或从 Mesh 迁移到 SFU 的前端 / 后端同学。

技术栈示例:Spring Boot + Redis + IM 信令;uni-app(H5 / App)+ LiveKit Client SDK。


一、写在前面

一对一视频通话看似"两端 PeerConnection 连一下就完了",真正做成可上线的 IM 能力时,至少要拆成两件事:

  1. 信令(Signaling):谁在呼叫、谁振铃、谁接听/拒绝/取消、何时挂断
  2. 媒体(Media):音视频怎么进房、怎么发布与订阅、怎么开关麦和摄像头

本文基于一套真实业务实现,介绍如何用 LiveKit SFU 承载媒体,用 现有 IM 私聊消息 + REST 承载信令,完成完整的一对一视频通话。


二、总体架构:信令与媒体分离

设计原则:

层次 职责 技术
业务信令 呼叫、振铃、接听、拒接、取消、失败、挂断、开关音视频状态同步 IM 消息类型 + /webrtc/private/*
媒体传输 进房、发布本地轨、订阅远端轨 LiveKit Room SDK
会话状态 房间名、主被叫终端、忙线 Redis

LiveKit 不负责振铃 UI,只负责"拿到 token 之后怎么传音视频"。


三、核心概念:房间、Token、忙线

3.1 房间命名

每次呼叫创建新房间,避免复用脏房间:

p_{min(userA,userB)}{max(userA,userB)}{timestamp}

例如用户 u1u2

p_u1_u2_1723000000000

ID 字典序排序,保证同一对用户房间名稳定可推导;时间戳保证每次呼叫唯一。

3.2 Token 签发(后端)

后端用 LiveKit API Key / Secret 签发短时 JWT,客户端永不持有密钥。

关键 grant:

3.3 Redis 会话与忙线

Key 含义 TTL
im:webrtc:private:session:{a}:{b} 私聊 RTC 会话 24h(心跳续期)
im:user:state:{userId} 忙线标记 60s(心跳续期)

会话查找兼容双向:先查 A:B,没有再查 B:A


四、信令消息类型(私聊)

一对一私聊 没有 群聊那种 RTC_GROUP_SETUP,发起信令是 RTC_CALL_*

code 常量 含义 典型方向
100 RTC_CALL_VOICE 语音呼叫 主叫 → 被叫全终端
101 RTC_CALL_VIDEO 视频呼叫 主叫 → 被叫全终端
102 RTC_ACCEPT_VIDEO 视频接听 被叫 → 主叫指定终端
111 RTC_ACCEPT_VOICE 语音接听 同上
103 RTC_REJECT 拒接 被叫 → 主叫
104 RTC_CANCEL 主叫取消 主叫 → 被叫
105 RTC_FAILED 呼叫失败(超时等) 主叫 → 被叫
106 RTC_HANDUP 挂断 任一方 → 对方
110 RTC_CHANGE_VIDEO_AUDIO 开关麦/摄像头状态 任一方 → 对方
40/41 ACT_RT_VOICE/VIDEO 通话结束后的聊天记录 落库展示

多端互斥靠两件事:

  1. recvTerminals:只打到"正在参与通话"的那个终端
  2. sendToSelf:接听/拒接时通知自己其他终端关闭振铃页

五、REST API 一览

Controller 前缀:/api/v1/webrtc/private

接口 作用
POST /call 创建会话、忙线、推呼叫信令,返回主叫凭证
POST /accept 写被叫终端、推接听信令,返回被叫凭证
POST /token 按当前用户重新签发进房 JWT(媒体页真正用这个)
POST /reject 拒接
POST /cancel 主叫取消
POST /failed 超时失败等
POST /handup 挂断(历史拼写,前后端统一)
POST /heartbeat 续会话 + 忙线(主叫定时调用)
POST /changeVideoAudio 同步对方 UI 的音视频开关状态

实现细节:/call 虽会返回 LiveKit 凭证,但当前前端媒体页统一走 /token 再进房;拨号页只负责呼叫信令,不立刻进房。


六、完整时序:从拨号到挂断


七、前端页面流转

7.1 发起侧

  1. 聊天页申请摄像头/麦克风权限
  2. 进入拨号页:/sendvideocall?mode=video&currentCallId={friendId}
  3. 拨号页立即调用 POST /call,播放等待音
  4. 不进 LiveKit,等待 RTC_ACCEPT_*
  5. 全局 App.vue 收到接听信令后 redirectTo 媒体页,并带上 isCaller=true

7.2 被叫侧

  1. App.vue 收到 RTC_CALL_VIDEO/VOICE(或离线推送点击)
  2. 打开振铃页:/videocallrev?mode=...&currentCallId={主叫ID}
  3. 接听:POST /acceptredirectTo 媒体页 isCaller=false
  4. 拒绝:POST /reject → 回聊天

7.3 媒体页参数

参数 含义
mode video / voice
currentCallId 对端用户 ID
isCaller 是否主叫(主叫开心跳)

平台分流:

  • H5 → videocallh5.vue
  • App → videocall.vue(web-view 壳)+ hybrid/html/videocall.html(真实 LiveKit)

八、H5 媒体进房实现要点

8.1 封装 connectLiveKitRoom

核心步骤:

说明:

  • autoSubscribe: true:远端发布后自动订阅
  • 进房后再扫一遍 remoteParticipants,避免"对方已在房内但错过事件"
  • 摄像头失败可降级为仅语音,避免直接进房失败

8.2 页面侧

渲染建议:

  • 用原生 DOM / track.attach() 挂载视频,比 uni-app <video> + srcObject 更稳
  • 画面元素静音,远端音频单独挂 <audio>,避免浏览器拦截自动播放导致黑屏

8.3 开关麦 / 摄像头

媒体层改轨 + 信令同步 UI:

对方收到 RTC_CHANGE_VIDEO_AUDIO 后只更新头像区静音/关摄像头状态,不必重进房。

8.4 挂断

// 回聊天页

后端清理会话、解忙线,并给对方推 RTC_HANDUP,同时落一条"通话时长 mm:ss"的会话消息。


九、App 端:web-view + Hybrid HTML

App 不便直接跑完整 LiveKit 时,常见做法是:

  1. Vue 壳页负责路由参数、心跳、把 IM 事件 evalJS 进 web-view
  2. Hybrid HTML 加载 livekit-client UMD,真正 connect/publish/subscribe
  3. 挂断后 uni.postMessage({ type: 'back' }) 通知壳页返回

壳页拼 URL 示例:

HTML 内:

信令桥接:


十、后端关键业务逻辑(摘要)

10.1 call

  1. requireEnabled():LiveKit 未配置直接失败,不做无效 Mesh 降级
  2. 生成 WebrtcPrivateSession + 房间名
  3. 校验对端在线 / 非忙线
  4. Redis 存会话,双方 setBusy
  5. 推送 RTC_CALL_VIDEO/VOICE
  6. 返回主叫凭证

10.2 accept

  1. 找到会话,记录被叫终端
  2. 推送 RTC_ACCEPT_* 到主叫终端,并 sendToSelf 关自己其他端振铃
  3. 返回被叫凭证

10.3 handup

  1. 计算通话时长
  2. 删会话、双方解忙
  3. RTC_HANDUP
  4. 落库 ACT_RT_VIDEO/VOICE

10.4 heartbeat

主叫约每 50s 调用一次:

  • 会话 TTL 续 24h
  • 忙线 TTL 续 60s

否则长时间通话可能被误判空闲,或忙线提前过期导致可被再次呼叫。


十一、异常分支怎么收口

场景 API / 信令 结果
被叫拒接 /reject + 103 主叫结束等待
主叫取消 /cancel + 104 被叫关振铃
30s 无应答 主叫 /failed + 105 双方结束,记录"未接通"
对方正忙 /call 直接失败 提示忙线
对方离线 /call 失败 提示不在线
自己其他端已接 ACCEPT 的 self 分支 本端关振铃,不进房
媒体中对方挂断 106 本端 disconnect 并退出

产品上建议:拨号页等 ACCEPT 再进房,避免主叫先进空房间空转。


十二、工程上值得注意的点

  1. 终端类型要传准

    App / H5 / 小程序终端码不同,后端靠它把信令打到"正在通话的那一端"。

  2. 自动播放策略

    远端 <video> 若混入音轨且未静音,移动浏览器常拦截播放 → 黑屏。画面静音 + 音频独立是稳妥做法。

  3. 挂断接口拼写

    历史接口是 /handup,前后端保持一致即可,不必强行改成 hangup 制造分裂。

  4. SDK 版本对齐

    H5 npm 与 App CDN 建议锁定同一主版本(如 livekit-client@2.21.x),减少双端行为差异。

  5. 小程序

    若业务暂不支持,IM 分发里对 MP 直接 return,避免半吊子体验。

  6. 安全

    API Secret 只放服务端;Token 短时有效;房间名不可被客户端随意伪造进房(无有效 JWT 进不去)。


十三、和"纯 P2P / Mesh"对比,为什么这样划界

维度 纯 P2P/Mesh 本文方案(IM 信令 + LiveKit SFU)
两人通话 简单直接 多一跳,但模型统一
后续扩展到群通话 连接数爆炸 同一套进房模型可复用
振铃/多端 要自己造 复用现有 IM
密钥与权限 难统一 JWT Grant 清晰
排障 端到端黑盒 信令问题 / 媒体问题可分离

一句话:

IM 解决"通话业务状态",LiveKit 解决"音视频传输"。


十四、总结

基于 LiveKit 做一对一视频通话,推荐落地路径:

  1. 后端创建会话 + 忙线 + 签发房间 Token
  2. IM 推送呼叫 / 接听 / 挂断等信令驱动页面跳转
  3. 双方媒体页再 POST /tokenRoom.connect → publish/subscribe
  4. 主叫心跳续期,挂断清理会话并写聊天记录

这条路径的好处是:业务状态机仍在你自己手里,媒体层可独立升级、监控和扩容;同时为后续群通话、录制等能力留下同一套 SFU 底座。

如果你正在从 Mesh 迁移,建议先把私聊链路按本文拆成"信令页 + 媒体页 + Token 服务"三块,再复用到群通话,迁移成本会小很多。

相关推荐
Afans_fire6 小时前
全媒体广告投放的5大技术框架
媒体·运营干货
程序员老陆7 小时前
M3U8(HLS 索引协议)深度详解:它不存一帧画面,却指挥了整个互联网视频
音视频·hls·m3u8
雪的季节8 小时前
音视频(51-52)
音视频
hz567898 小时前
视频会议终端品牌如何选择?主流产品特点与选型标准
安全·音视频·实时音视频·信息与通信
NutShell Wang11 小时前
国产全模态开源潮:从语言模型到视频模型,中国开源生态再升级
人工智能·语言模型·开源·大模型·音视频·多模态·vibe coding
民乐团扒谱机13 小时前
【读论文】复调音乐信号的主旋律提取:方法、应用与挑战
开发语言·人工智能·算法·音视频·音乐
byte轻骑兵13 小时前
【AVDTP】规范精讲[8-3]: 流配置核心三步:从参数下发到动态重配置全拆解
音视频·avrcp·蓝牙耳机·蓝牙车机·蓝牙音频控制
微帧Visionular16 小时前
GenOpt像素智绘引擎|AI生成视频需要怎样的视觉优化算法?
人工智能·aigc·音视频
杀生丸学AI1 天前
【世界模型】MotionForesight:视频模型应用于未来的三维场景流预测
音视频