@cf-platform/chat
v1.1.0
Published
聊天相关库
Downloads
32
Readme
@cf-platform/chat
可扩展音视频会议功能库,支持语音通话、视频通话、多点会议、屏幕共享。
架构
┌──────────────────────────────────────┐
│ ConferenceClient │ ← 用户侧主入口(统一 API)
├──────────────────────────────────────┤
│ LiveKitEngine │ ← 引擎层(可扩展为其他引擎)
├──────────────────────────────────────┤
│ livekit-client │ ← 底层 SDK
└──────────────────────────────────────┘- ConferenceClient — 面向用户的统一 API,封装引擎、设备管理、本地轨道生命周期
- LiveKitEngine — 基于 livekit-client v1.15.13 的引擎实现,提供房间连接、轨道管理等功能
- MediaDeviceManager — 来自
@mm-custom/method,提供设备枚举、热插拔监听等能力
快速开始
安装
npm install @cf-platform/chat前置依赖:
@mm-custom/method >= 2.2.11(peerDependency)
基础用法
import { ConferenceClient, LiveKitEngine } from '@cf-platform/chat'
import { MediaDeviceManager } from '@mm-custom/method'
// 1. 创建引擎和设备管理器
const engine = new LiveKitEngine({ url: 'wss://your-server.com' })
const deviceManager = new MediaDeviceManager()
// 2. 创建会议客户端(deviceManager 可选,不传则内部自动创建)
const client = new ConferenceClient({ engine, deviceManager })
// 3. 监听事件
client.on('participantJoined', (p) => console.log(p.id, '加入会议'))
client.on('participantLeft', (p) => console.log(p.id, '离开会议'))
client.on('remoteTrackAdded', (info) => {
const el = document.getElementById(`video-${info.participantId}`)
if (el) client.attachTrack(info.track, el as HTMLVideoElement)
})
client.on('remoteTrackRemoved', (info) => {
const el = document.getElementById(`video-${info.participantId}`)
if (el) client.detachTrack(el as HTMLVideoElement)
})
// 4. 初始化引擎并加入房间
await engine.init()
try {
await client.join('room-name', 'access-token')
} catch (e) {
console.error('加入房间失败', e)
}
// 5. 开启音视频
await client.startAudio() // 麦克风
await client.startVideo() // 摄像头
// 6. 离开
await client.leave()
// 7. 销毁(异步,需 await)
await client.destroy()更多示例
语音通话(纯音频)
const engine = new LiveKitEngine({ url: 'wss://your-server.com' })
const client = new ConferenceClient({ engine })
await engine.init()
await client.join('voice-room', 'token')
await client.startAudio()
// 静音/取消静音
await client.muteAudio()
await client.unmuteAudio()屏幕共享
await client.startScreenShare()
// 用户通过浏览器 UI 停止共享时,会自动触发 stopScreenShare
// 也可以手动停止
await client.stopScreenShare()设备切换
// 获取可用设备
const cameras = client.getCameras()
const microphones = client.getMicrophones()
// 切换到指定设备
if (cameras.length > 1) {
await client.switchCamera(cameras[1].deviceId)
}E2EE 端到端加密
const engine = new LiveKitEngine({
url: 'wss://your-server.com',
e2ee: true,
cryptoKey: 'shared-secret-key',
})
const client = new ConferenceClient({ engine })
await engine.init()
await client.join('secure-room', 'token')[!WARNING] E2EE 依赖
new Worker(new URL(..., import.meta.url))加载加密 worker,仅在 ESM 消费方式下可用。 以 UMD(<script>)方式引入时import.meta会被替换为空对象,worker 路径失效。 此时若配置了e2ee: true,引擎会在初始化时抛出明确错误(提示改用 ESM 引入),而非静默失败。 如需 E2EE,请使用import方式引入本库。
强制 TURN 中继
// 适用于限制性网络环境(如仅允许 HTTP 出站的防火墙)
const engine = new LiveKitEngine({
url: 'wss://your-server.com',
forceUseTURN: true,
})自定义房间配置
import { VideoPresets } from 'livekit-client'
const engine = new LiveKitEngine({
url: 'wss://your-server.com',
roomOptions: {
adaptiveStream: false,
dynacast: false,
publishDefaults: {
videoSimulcastLayers: [VideoPresets.h180, VideoPresets.h360],
},
},
})开启日志
const engine = new LiveKitEngine({
url: 'wss://your-server.com',
enableLog: true,
})
// 或通过 ConferenceClient 开启(同时会覆盖引擎的 log 方法)
const client = new ConferenceClient({ engine, enableLog: true })错误处理
client.on('error', (err) => {
console.error('[Conference]', err)
})
try {
await client.join('room', 'token')
} catch (e) {
// join 失败会抛出异常
console.error('连接失败', e)
}完整的 React 组件示例
import { useEffect, useRef, useState } from 'react'
import { ConferenceClient, LiveKitEngine } from '@cf-platform/chat'
function VideoCall({ roomName, token }: { roomName: string; token: string }) {
const clientRef = useRef<ConferenceClient | null>(null)
const localVideoRef = useRef<HTMLVideoElement>(null)
const remoteVideoRef = useRef<HTMLVideoElement>(null)
const [connected, setConnected] = useState(false)
useEffect(() => {
const engine = new LiveKitEngine({ url: 'wss://your-server.com' })
const client = new ConferenceClient({ engine })
clientRef.current = client
client.on('connected', () => setConnected(true))
client.on('disconnected', () => setConnected(false))
client.on('remoteTrackAdded', (info) => {
if (info.kind === 'video' && remoteVideoRef.current) {
client.attachTrack(info.track, remoteVideoRef.current)
}
})
engine.init()
client.join(roomName, token).then(() => {
client.startAudio()
client.startVideo().then(() => {
// 将本地视频渲染到预览元素
const track = (client as any)._localVideoTrack
if (track && localVideoRef.current) {
const stream = new MediaStream([track])
localVideoRef.current.srcObject = stream
}
})
})
return () => { client.destroy() }
}, [roomName, token])
return (
<div>
<video ref={localVideoRef} autoPlay playsInline muted />
<video ref={remoteVideoRef} autoPlay playsInline />
<p>{connected ? '已连接' : '未连接'}</p>
</div>
)
}API
ConferenceClient
连接
| 方法 | 说明 |
|---|---|
| join(roomName: string, token: string): Promise<void> | 加入房间(连接失败会抛出异常) |
| leave(): Promise<void> | 离开房间,自动关闭所有本地轨道(任一 stop 失败不影响其他清理和断开) |
语音通话
| 方法 | 说明 |
|---|---|
| startAudio(deviceId?: string): Promise<void> | 开启麦克风。采集失败或未拿到音频轨道时抛出错误 |
| stopAudio(): Promise<void> | 关闭麦克风 |
| muteAudio(): Promise<void> | 静音(保持发布,发送静音数据,触发 localAudioChanged(false)) |
| unmuteAudio(): Promise<void> | 取消静音(触发 localAudioChanged(true)) |
视频通话
| 方法 | 说明 |
|---|---|
| startVideo(deviceId?: string): Promise<void> | 开启摄像头。采集失败或未拿到视频轨道时抛出错误 |
| stopVideo(): Promise<void> | 关闭摄像头 |
| muteVideo(): Promise<void> | 静画(保持发布,发送黑屏,触发 localVideoChanged(false)) |
| unmuteVideo(): Promise<void> | 取消静画(触发 localVideoChanged(true)) |
屏幕共享
| 方法 | 说明 |
|---|---|
| startScreenShare(): Promise<void> | 开始屏幕共享。采集失败或未拿到屏幕轨道时抛出错误 |
| stopScreenShare(): Promise<void> | 停止屏幕共享(同时解绑 ended 监听器) |
设备管理
| 方法 | 说明 |
|---|---|
| switchCamera(deviceId: string): Promise<void> | 切换摄像头。先采集新轨道成功再停旧轨道,采集失败时旧轨道保持不变并抛出错误 |
| switchMicrophone(deviceId: string): Promise<void> | 切换麦克风。同上,失败时旧轨道保持不变并抛出错误 |
| getDevices(): GroupedDevices | 获取当前设备列表(按类别分组) |
| getCameras(): DeviceInfo[] | 获取摄像头列表 |
| getMicrophones(): DeviceInfo[] | 获取麦克风列表 |
| getSpeakers(): DeviceInfo[] | 获取扬声器列表 |
设备热插拔时自动迁移到可用设备,无可用设备时自动停止轨道。
轨道渲染
| 方法 | 说明 |
|---|---|
| attachTrack(track: MediaStreamTrack, element: HTMLMediaElement): void | 将轨道附加到 DOM 元素(创建 MediaStream 并设置 srcObject) |
| detachTrack(element: HTMLMediaElement): void | 从 DOM 元素移除轨道绑定(仅清空 srcObject,不停止轨道本身,轨道生命周期由引擎管理) |
参与者
| 方法 | 说明 |
|---|---|
| getParticipants(): ParticipantInfo[] | 获取当前房间所有远程参与者 |
状态
| 属性 | 类型 | 说明 |
|---|---|---|
| connectionState | ConnectionState | 当前连接状态 |
| isAudioEnabled | boolean | 麦克风是否已开启 |
| isVideoEnabled | boolean | 摄像头是否已开启 |
| isScreenSharing | boolean | 是否正在屏幕共享 |
| isAudioMuted | boolean | 音频是否被静音 |
| isVideoMuted | boolean | 视频是否被静音 |
事件
| 事件 | 参数 | 说明 |
|---|---|---|
| connected | room: any | 连接成功(透传) |
| disconnected | reason?: DisconnectReason | 连接断开(透传) |
| connectionStateChanged | state: ConnectionState | 连接状态变更(透传) |
| participantJoined | ParticipantInfo | 参与者加入(透传) |
| participantLeft | { id: string } | 参与者离开(透传) |
| remoteTrackAdded | RemoteTrackInfo | 远程轨道添加(透传),调用 attachTrack 渲染。kind 为 screen 时表示对方屏幕共享轨道 |
| remoteTrackRemoved | RemoteTrackInfo | 远程轨道移除(透传),调用 detachTrack 清理 |
| trackMuted | TrackMuteInfo | 轨道静音(透传) |
| trackUnmuted | TrackMuteInfo | 轨道取消静音(透传) |
| activeSpeakersChanged | ParticipantInfo[] | 活跃发言人变更(透传) |
| localAudioChanged | enabled: boolean | 本地音频状态变更(自产),true=开启/取消静音,false=关闭/静音 |
| localVideoChanged | enabled: boolean | 本地视频状态变更(自产),true=开启/取消静音,false=关闭/静音 |
| screenShareChanged | enabled: boolean | 屏幕共享状态变更(自产) |
| mediaDevicesChanged | room: any | 设备列表变更(透传) |
| activeDeviceChanged | ActiveDeviceChange | 活跃设备切换(透传) |
| error | Error | 错误(透传) |
生命周期
| 方法 | 说明 |
|---|---|
| destroy(): Promise<void> | 销毁客户端,释放所有资源(异步方法,需 await) |
LiveKitEngine
基于 livekit-client v1.15.13 的引擎实现。通常作为 ConferenceClient 的底层引擎使用,不要直接调用其 @internal 方法。
配置
interface LiveKitEngineConfig {
url: string // 服务器地址(必填,格式如 wss://your-server.com)
enableLog?: boolean // 是否开启日志(默认 false)
e2ee?: boolean // 是否启用端到端加密(需同时提供 cryptoKey)
cryptoKey?: string // E2EE 加密密钥(e2ee 为 true 时必填)
forceUseTURN?: boolean // 是否强制使用 TURN 中继(默认 false)
roomOptions?: RoomOptions // 房间配置选项(深度合并到默认配置)
roomConnectOptions?: RoomConnectOptions // 连接配置选项(深度合并到默认配置)
}默认房间选项
| 选项 | 默认值 | 说明 |
|---|---|---|
| adaptiveStream | true | 自动管理订阅视频质量 |
| dynacast | true | 动态暂停无人消费的视频层 |
| publishDefaults.videoSimulcastLayers | [h90, h216] | 视频联播层 |
| publishDefaults.screenShareEncoding | h1080fps30 | 屏幕共享编码 |
| disconnectOnPageLeave | false | 离开页面不自动断开 |
公开方法
| 方法 | 说明 |
|---|---|
| init(config?) | 初始化引擎,创建 Room 实例并绑定事件。可在构造后再次调用以更新配置 |
| initConfig(config) | 更新引擎配置(唯一的配置入口,构造函数内部也调用此方法) |
| setVideoQuality(quality) | 设置远程视频质量(LOW/MEDIUM/HIGH),需关闭 adaptiveStream 才生效 |
| setVideoFPS(fps) | 设置远程视频帧率 |
| getRemoteParticipants() | 获取当前房间所有远程参与者 |
| log(...logs) | 输出日志(仅在 enableLog 为 true 时生效) |
| ~~handleTrackSubscribed(participant, track, elementId)~~ | 已废弃 — 请使用 ConferenceClient.attachTrack() 替代 |
[!NOTE] 以下引擎层方法标记为 @internal,由 ConferenceClient 内部调度,请勿直接调用:
connect()/disconnect()→ 通过client.join()/client.leave()publishTrack()/unpublishTrack()→ 通过client.startAudio/Video()等方法直接调用会绕过本地轨道生命周期管理(如离开时自动停止轨道)。
扩展引擎
当前 ConferenceClient 直接依赖 LiveKitEngine。如需接入其他媒体引擎(如 Agora、TRTC),
可参考 LiveKitEngine 的实现,创建一个继承 EventManager 并提供相同公开方法的新引擎类:
import { EventManager } from '@mm-custom/method'
import type { ConnectionState, LocalTrackInfo } from '@cf-platform/chat'
class AgoraEngine extends EventManager {
// 必须实现以下方法(与 LiveKitEngine 保持一致的接口)
get connectionState(): ConnectionState { /* ... */ }
init(config?: any): void { /* ... */ }
async connect(roomName: string, token: string): Promise<void> { /* ... */ } // @internal
async disconnect(): Promise<void> { /* ... */ } // @internal
async publishTrack(track: LocalTrackInfo): Promise<void> { /* ... */ } // @internal
async unpublishTrack(track: LocalTrackInfo): Promise<void> { /* ... */ } // @internal
getRemoteParticipants(): Record<string, any> { /* ... */ }
destroy(): void { /* ... */ }
}
// 使用方式与 LiveKit 完全一致
const client = new ConferenceClient({ engine: new AgoraEngine() as any })[!NOTE]
connect、disconnect、publishTrack、unpublishTrack标记为@internal, 由 ConferenceClient 统一调用,外部不应直接使用。
类型参考
// 连接状态
type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'reconnecting'
// 轨道来源
type TrackSource = 'camera' | 'microphone' | 'screen' | 'unknown'
// 本地轨道信息
interface LocalTrackInfo {
track: MediaStreamTrack
kind: 'audio' | 'video' | 'screen'
source?: TrackSource
deviceId?: string
label?: string
}
// 远程轨道信息
interface RemoteTrackInfo {
participantId: string
participantName?: string
track: MediaStreamTrack
kind: 'audio' | 'video' | 'screen'
isLocal: false
}
// 参与者信息
interface ParticipantInfo {
id: string
name?: string
isLocal?: boolean
}
// 轨道静音状态
interface TrackMuteInfo {
participantId: string
kind: 'audio' | 'video' | 'screen'
isMuted: boolean
}
// 设备信息
interface DeviceInfo {
deviceId: string
groupId: string
kind: MediaDeviceKind
label: string
}
// 分组设备列表
interface GroupedDevices {
audioinput: DeviceInfo[]
audiooutput: DeviceInfo[]
videoinput: DeviceInfo[]
}
// 活跃设备变更
interface ActiveDeviceChange {
kind: MediaDeviceKind
deviceId: string
}
// 会议客户端选项
interface ConferenceOptions {
engine: LiveKitEngine
deviceManager?: MediaDeviceManager // 可选,不传则内部自动创建
enableLog?: boolean
}
// LiveKit 引擎配置
interface LiveKitEngineConfig {
url: string
enableLog?: boolean
e2ee?: boolean
cryptoKey?: string
forceUseTURN?: boolean
roomOptions?: RoomOptions
roomConnectOptions?: RoomConnectOptions
}依赖
| 包 | 版本 | 类型 | 说明 |
|---|---|---|---|
| livekit-client | ^1.15.13 | dependency | LiveKit Web SDK |
| @mm-custom/method | ^2.2.11 | peerDependency | 事件管理器 & 设备管理器 |
版本
当前版本:1.1.0
更新日志
v1.1.0 (修复 + 打包对齐)
打包配置(对齐 cim/mq)
- 🔧 统一打包配置 —
vite.config.ts按 cim/mq 模板重写:显式formats: ['es', 'umd']、补rollupOptions.input、sourcemap: false,新增cleanup(去注释)+terser(压缩、mangle_前缀属性)两个 rollup 插件 - 🔧 dts 配置补齐 —
dts插件补include/exclude/insertTypesEntry/removeComments,与 cim/mq 一致 - ⚠️ 产物文件名变更 —
chat.lib.es.js/chat.lib.umd.js→chat.es.js/chat.umd.js(与 cim/mq 的<pkg>.<format>.js命名统一)。package.json的main/module/exports同步更新,按包名引入的消费方不受影响;files补README.md
逻辑修复
- 🐛 修复
unpublishTrack永远无法取消发布 — 原先用trackInfo.track.id(MediaStreamTrack 的 UUID)当作Track.Source枚举调getTrack,永远返回undefined,导致本地轨道在服务端永不取消发布。改为直接用MediaStreamTrack调LocalParticipant.unpublishTrack(track)(livekit 原生支持) - 🐛 修复屏幕共享远程轨道
kind错标为video— livekit 的track.kind只有audio/video/unknown,屏幕共享视频轨道 kind 仍为video,消费者按kind === 'screen'判断永远拿不到。新增_mapTrackKind,通过track.source === ScreenShare映射为'screen',订阅/退订/静音三处统一使用 - 🛡️ 本地轨道采集判空 —
startAudio/startVideo/startScreenShare采集后检查是否拿到轨道,失败时释放已申请轨道并抛出错误(原先会把undefined传给引擎) - 🛡️ 设备切换失败回滚 —
switchCamera/switchMicrophone改为"先采集新轨道成功再停旧轨道",采集失败时旧轨道保持不变并抛出错误(原先是"先停旧再采新",失败会导致新旧轨道同时丢失) - 🛡️ 屏幕共享
ended监听器不再泄漏 — 改用具名 handler 存为字段,stopScreenShare中removeEventListener(原先用匿名函数无法解绑,重复 start/stop 会累积监听器) - 🛡️ E2EE worker 加载失败保护 — UMD 引入下
import.meta.url失效,new Worker(new URL(...))会抛错。现用 try/catch 捕获并抛出明确错误(提示改用 ESM),而非静默失败
代码质量
- 🧹
unmuteAudio去掉多余的await this.emit(...),与其他 emit 调用一致 - 🧹
setVideoFPS去掉(trackPub as any)强转(RemoteTrackPublication.setVideoFPS本就是公开方法) - 🧹
log方法去掉非对象日志里多余的™符号
文档
- 📝 方法表补充
@throws说明、switchCamera/Microphone的新行为、remoteTrackAdded屏幕共享识别说明 - 📝 E2EE 章节增加 WARNING:UMD 下 E2EE 不可用,会抛明确错误
v1.0.1
- 完善会议库,补全注释与文档
v1.0.0
- 基于 livekit-client 的可扩展音视频会议库
- ConferenceClient 统一 API(语音/视频/多点会议/屏幕共享)
- LiveKitEngine 引擎层(房间连接、轨道管理、设备管理、E2EE、TURN)
- 设备热插拔自动迁移轨道
- 支持自定义房间配置深度合并
License
ISC
