@coze/realtime-api
Version:
A powerful real-time communication SDK for voice interactions with Coze AI bots | 扣子官方实时通信 SDK,用于与 Coze AI bots 进行语音交互
251 lines (250 loc) • 9.4 kB
JavaScript
import * as __WEBPACK_EXTERNAL_MODULE__coze_api__ from "@coze/api";
// WTN服务基础URL
const WTN_BASE_URL = 'https://wtn.volcvideo.com';
/**
* WebRTC资源状态
*/ var live_ResourceStatus = /*#__PURE__*/ function(ResourceStatus) {
ResourceStatus["IDLE"] = "idle";
ResourceStatus["CONNECTING"] = "connecting";
ResourceStatus["CONNECTED"] = "connected";
ResourceStatus["FAILED"] = "failed";
ResourceStatus["CLOSING"] = "closing";
ResourceStatus["CLOSED"] = "closed";
return ResourceStatus;
}({});
/**
* 同声传译客户端
*/ class WebLiveClient {
/**
* 获取当前连接状态
*/ getStatus() {
return this.status;
}
/**
* 添加状态变化监听器
* @param callback 状态变化回调函数
*/ onStatusChange(callback) {
this.statusListeners.push(callback);
}
/**
* 移除状态变化监听器
* @param callback 要移除的回调函数
*/ offStatusChange(callback) {
this.removeStatusListener(callback);
}
/**
* 移除状态变化监听器
* @param callback 要移除的回调函数
*/ removeStatusListener(callback) {
const index = this.statusListeners.indexOf(callback);
if (-1 !== index) this.statusListeners.splice(index, 1);
}
/**
* 订阅音频资源
* @param appId 应用ID
* @param streamId 流ID
* @param clientId 客户端ID
*/ async subscribe(appId, streamId) {
let clientId = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : '';
try {
var _pc_localDescription;
// 先清理现有连接
if (this.peerConnection) {
this.peerConnection.close();
this.peerConnection = null;
}
this.setStatus("connecting");
// 1. 创建RTCPeerConnection
const rtcConfig = {};
const pc = new RTCPeerConnection(rtcConfig);
pc.ontrack = (event)=>{
// 音频流
this.player.onloadeddata = ()=>{
this.player.play();
};
this.player.srcObject = event.streams[0];
};
this.peerConnection = pc;
this.setupPeerConnectionListeners();
pc.addTransceiver('audio', {
direction: 'recvonly'
});
// 2. 创建Offer (SDP)
const offer = await pc.createOffer();
// 设置本地描述
await pc.setLocalDescription(offer);
// 等待ICE收集完成再继续
await this.waitForIceGathering(pc);
if (!(null === (_pc_localDescription = pc.localDescription) || void 0 === _pc_localDescription ? void 0 : _pc_localDescription.sdp)) throw new Error('Failed to create SDP offer');
// 3. 发送Offer到WTN服务订阅资源
let subscribeUrl = `${WTN_BASE_URL}/sub/${appId}/${streamId}?MuteVideo=true`;
if (clientId) subscribeUrl += `&clientid=${clientId}`;
const response = await fetch(subscribeUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/sdp'
},
body: offer.sdp
});
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
// 4. 保存资源URL (用于销毁资源)
this.resourceUrl = response.headers.get('location') || '';
// 5. 设置远程SDP (Answer)
// 直接获取文本响应,因为服务器返回的是application/sdp格式而非json
const answerSdp = await response.text();
const answer = new RTCSessionDescription({
type: 'answer',
sdp: answerSdp
});
await this.peerConnection.setRemoteDescription(answer);
// 7. 返回结果
return {
status: this.status,
peerConnection: this.peerConnection
};
} catch (error) {
this.status = "failed";
console.error('Failed to subscribe WebRTC stream:', error);
return Promise.reject(error);
}
}
/**
* 销毁订阅资源
*/ async unsubscribe() {
try {
// 销毁订阅资源
if (!this.resourceUrl) throw new Error('No valid subscription resource URL to unsubscribe');
this.setStatus("closing");
const response = await fetch(this.resourceUrl, {
method: 'DELETE'
});
if (!response.ok) throw new Error(`Failed to unsubscribe: ${response.status} ${response.statusText}`);
// 关闭RTC连接
if (this.peerConnection) {
this.peerConnection.close();
this.peerConnection = null;
}
this.resourceUrl = '';
this.status = "closed";
return true;
} catch (error) {
console.error('Error unsubscribing resource:', error);
this.status = "failed";
return Promise.reject(error);
}
}
/**
* 静音/取消静音
* @param muted 是否静音
*/ setMuted(muted) {
this.player.muted = muted;
}
/**
* 关闭并清理资源
*/ close() {
// 关闭PeerConnection
this.closePeerConnection();
// Clean up audio element
if (this.player) {
this.player.pause();
this.player.srcObject = null;
this.player.remove();
}
// 重置状态
this.resourceUrl = '';
this.setStatus("idle");
}
/**
* 等待ICE收集完成
* @param pc RTCPeerConnection实例
*/ waitForIceGathering(pc) {
return new Promise((resolve)=>{
// 如果已经收集完成,直接返回
if ('complete' === pc.iceGatheringState) {
resolve();
return;
}
// 设置收集完成时的回调
const checkState = ()=>{
if ('complete' === pc.iceGatheringState) {
pc.removeEventListener('icegatheringstatechange', checkState);
resolve();
}
};
// 监听收集状态变化
pc.addEventListener('icegatheringstatechange', checkState);
// 添加超时处理,防止永远等待
setTimeout(()=>resolve(), 5000);
});
}
setupPeerConnectionListeners() {
if (!this.peerConnection) return;
this.peerConnection.oniceconnectionstatechange = ()=>{
var _this_peerConnection, _this_peerConnection1;
console.log('ICE connection state changed:', null === (_this_peerConnection = this.peerConnection) || void 0 === _this_peerConnection ? void 0 : _this_peerConnection.iceConnectionState);
switch(null === (_this_peerConnection1 = this.peerConnection) || void 0 === _this_peerConnection1 ? void 0 : _this_peerConnection1.iceConnectionState){
case 'connected':
case 'completed':
this.setStatus("connected");
break;
case 'failed':
case 'disconnected':
this.setStatus("failed");
break;
case 'closed':
this.setStatus("closed");
break;
default:
var _this_peerConnection2;
console.log('ICE connection state changed:', null === (_this_peerConnection2 = this.peerConnection) || void 0 === _this_peerConnection2 ? void 0 : _this_peerConnection2.iceConnectionState);
break;
}
};
this.peerConnection.onicecandidate = (event)=>{
if (event.candidate) console.log('New ICE candidate:', event.candidate);
};
}
/**
* 关闭PeerConnection
*/ closePeerConnection() {
if (this.peerConnection) {
this.peerConnection.close();
this.peerConnection = null;
}
}
/**
* 设置状态并触发监听回调
* @param newStatus 新状态
* @private 私有方法,仅内部使用
*/ setStatus(newStatus) {
const oldStatus = this.status;
if (oldStatus !== newStatus) {
this.status = newStatus;
// 触发所有监听器
for (const listener of this.statusListeners)try {
listener(newStatus);
} catch (error) {
console.error('Error in status listener:', error);
}
}
}
constructor(liveId){
this.peerConnection = null;
this.resourceUrl = '';
this.status = "idle";
this.statusListeners = [];
/**
* 获取直播信息
*/ this.getLiveData = async ()=>{
const api = new __WEBPACK_EXTERNAL_MODULE__coze_api__.CozeAPI({
baseURL: __WEBPACK_EXTERNAL_MODULE__coze_api__.COZE_CN_BASE_URL,
token: ''
});
return await api.audio.live.retrieve(this.liveId);
};
this.setupPeerConnectionListeners();
this.player = document.createElement('audio');
this.liveId = liveId;
}
}
export { live_ResourceStatus as ResourceStatus, WebLiveClient };