UNPKG

@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
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 };