@coze/realtime-api 1.3.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/README.zh-CN.md +3 -0
- package/dist/cjs/event-names/index.js +4 -0
- package/dist/cjs/index.js +10 -3
- package/dist/cjs/live/index.js +292 -0
- package/dist/esm/event-names/index.js +4 -0
- package/dist/esm/index.js +10 -3
- package/dist/esm/live/index.js +250 -0
- package/dist/types/event-names/event-names.d.ts +6 -1
- package/dist/types/event-names/index.d.ts +2 -1
- package/dist/types/event-names/live/index.d.ts +86 -0
- package/dist/types/event-names.d.ts +6 -1
- package/dist/types/index.d.ts +2 -1
- package/dist/types/live/client.d.ts +49 -0
- package/dist/types/live/error.d.ts +25 -0
- package/dist/types/live/event-handler.d.ts +12 -0
- package/dist/types/live/event-names.d.ts +213 -0
- package/dist/types/live/index.d.ts +86 -0
- package/dist/types/live/live/index.d.ts +86 -0
- package/dist/types/live/utils.d.ts +49 -0
- package/dist/umd/index.js +10 -3
- package/package.json +12 -2
package/README.md
CHANGED
package/README.zh-CN.md
CHANGED
@@ -200,6 +200,10 @@ var EventNames = /*#__PURE__*/ function(EventNames) {
|
|
200
200
|
* en: Mode updated
|
201
201
|
* zh: 更新房间模式成功
|
202
202
|
*/ EventNames["MODE_UPDATED"] = "server.mode.updated";
|
203
|
+
/**
|
204
|
+
* en: Live created
|
205
|
+
* zh: 直播创建
|
206
|
+
*/ EventNames["LIVE_CREATED"] = "server.live.created";
|
203
207
|
return EventNames;
|
204
208
|
}(EventNames || {});
|
205
209
|
/* ESM default export */ const __WEBPACK_DEFAULT_EXPORT__ = EventNames;
|
package/dist/cjs/index.js
CHANGED
@@ -313,6 +313,10 @@ var event_names_EventNames = /*#__PURE__*/ function(EventNames) {
|
|
313
313
|
* en: Mode updated
|
314
314
|
* zh: 更新房间模式成功
|
315
315
|
*/ EventNames["MODE_UPDATED"] = "server.mode.updated";
|
316
|
+
/**
|
317
|
+
* en: Live created
|
318
|
+
* zh: 直播创建
|
319
|
+
*/ EventNames["LIVE_CREATED"] = "server.live.created";
|
316
320
|
return EventNames;
|
317
321
|
}(event_names_EventNames || {});
|
318
322
|
/* ESM default export */ const event_names = event_names_EventNames;
|
@@ -654,6 +658,7 @@ class EngineClient extends RealtimeEventHandler {
|
|
654
658
|
if (isTestEnv) rtc_default().setParameter('ICE_CONFIG_REQUEST_URLS', [
|
655
659
|
'rtc-test.bytedance.com'
|
656
660
|
]);
|
661
|
+
else localStorage.removeItem('RTC_ACCESS_URLS-VolcEngine');
|
657
662
|
this.engine = rtc_default().createEngine(appId);
|
658
663
|
this.handleMessage = this.handleMessage.bind(this);
|
659
664
|
this.handleUserJoin = this.handleUserJoin.bind(this);
|
@@ -687,7 +692,7 @@ class RealtimeClient extends RealtimeEventHandler {
|
|
687
692
|
else {
|
688
693
|
const config = {};
|
689
694
|
if (this._config.prologueContent) config.prologue_content = this._config.prologueContent;
|
690
|
-
if (void 0 !== this._config.roomMode && null !== this._config.roomMode) config.room_mode = this._config.roomMode;
|
695
|
+
if (void 0 !== this._config.roomMode && null !== this._config.roomMode) config.room_mode = this._config.roomMode || api_namespaceObject.RoomMode.Default;
|
691
696
|
if (this._config.videoConfig) {
|
692
697
|
if (isScreenShareDevice(this._config.videoConfig.videoInputDeviceId)) config.video_config = {
|
693
698
|
stream_video_type: 'screen'
|
@@ -696,7 +701,8 @@ class RealtimeClient extends RealtimeEventHandler {
|
|
696
701
|
stream_video_type: 'main'
|
697
702
|
};
|
698
703
|
}
|
699
|
-
|
704
|
+
if (this._config.translateConfig) config.translate_config = this._config.translateConfig;
|
705
|
+
const params = {
|
700
706
|
bot_id: botId,
|
701
707
|
conversation_id: conversationId || void 0,
|
702
708
|
voice_id: voiceId && voiceId.length > 0 ? voiceId : void 0,
|
@@ -704,7 +710,8 @@ class RealtimeClient extends RealtimeEventHandler {
|
|
704
710
|
uid: this._config.userId || void 0,
|
705
711
|
workflow_id: this._config.workflowId || void 0,
|
706
712
|
config
|
707
|
-
}
|
713
|
+
};
|
714
|
+
roomInfo = await this._api.audio.rooms.create(params);
|
708
715
|
}
|
709
716
|
} catch (error) {
|
710
717
|
this.dispatch(event_names.ERROR, error);
|
@@ -0,0 +1,292 @@
|
|
1
|
+
"use strict";
|
2
|
+
// The require scope
|
3
|
+
var __webpack_require__ = {};
|
4
|
+
/************************************************************************/ // webpack/runtime/define_property_getters
|
5
|
+
(()=>{
|
6
|
+
__webpack_require__.d = function(exports1, definition) {
|
7
|
+
for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
|
8
|
+
enumerable: true,
|
9
|
+
get: definition[key]
|
10
|
+
});
|
11
|
+
};
|
12
|
+
})();
|
13
|
+
// webpack/runtime/has_own_property
|
14
|
+
(()=>{
|
15
|
+
__webpack_require__.o = function(obj, prop) {
|
16
|
+
return Object.prototype.hasOwnProperty.call(obj, prop);
|
17
|
+
};
|
18
|
+
})();
|
19
|
+
// webpack/runtime/make_namespace_object
|
20
|
+
(()=>{
|
21
|
+
// define __esModule on exports
|
22
|
+
__webpack_require__.r = function(exports1) {
|
23
|
+
if ('undefined' != typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
|
24
|
+
value: 'Module'
|
25
|
+
});
|
26
|
+
Object.defineProperty(exports1, '__esModule', {
|
27
|
+
value: true
|
28
|
+
});
|
29
|
+
};
|
30
|
+
})();
|
31
|
+
/************************************************************************/ var __webpack_exports__ = {};
|
32
|
+
// ESM COMPAT FLAG
|
33
|
+
__webpack_require__.r(__webpack_exports__);
|
34
|
+
// EXPORTS
|
35
|
+
__webpack_require__.d(__webpack_exports__, {
|
36
|
+
WebLiveClient: ()=>/* binding */ WebLiveClient,
|
37
|
+
ResourceStatus: ()=>/* binding */ live_ResourceStatus
|
38
|
+
});
|
39
|
+
const api_namespaceObject = require("@coze/api");
|
40
|
+
// WTN服务基础URL
|
41
|
+
const WTN_BASE_URL = 'https://wtn.volcvideo.com';
|
42
|
+
/**
|
43
|
+
* WebRTC资源状态
|
44
|
+
*/ var live_ResourceStatus = /*#__PURE__*/ function(ResourceStatus) {
|
45
|
+
ResourceStatus["IDLE"] = "idle";
|
46
|
+
ResourceStatus["CONNECTING"] = "connecting";
|
47
|
+
ResourceStatus["CONNECTED"] = "connected";
|
48
|
+
ResourceStatus["FAILED"] = "failed";
|
49
|
+
ResourceStatus["CLOSING"] = "closing";
|
50
|
+
ResourceStatus["CLOSED"] = "closed";
|
51
|
+
return ResourceStatus;
|
52
|
+
}({});
|
53
|
+
/**
|
54
|
+
* 同声传译客户端
|
55
|
+
*/ class WebLiveClient {
|
56
|
+
/**
|
57
|
+
* 获取当前连接状态
|
58
|
+
*/ getStatus() {
|
59
|
+
return this.status;
|
60
|
+
}
|
61
|
+
/**
|
62
|
+
* 添加状态变化监听器
|
63
|
+
* @param callback 状态变化回调函数
|
64
|
+
*/ onStatusChange(callback) {
|
65
|
+
this.statusListeners.push(callback);
|
66
|
+
}
|
67
|
+
/**
|
68
|
+
* 移除状态变化监听器
|
69
|
+
* @param callback 要移除的回调函数
|
70
|
+
*/ offStatusChange(callback) {
|
71
|
+
this.removeStatusListener(callback);
|
72
|
+
}
|
73
|
+
/**
|
74
|
+
* 移除状态变化监听器
|
75
|
+
* @param callback 要移除的回调函数
|
76
|
+
*/ removeStatusListener(callback) {
|
77
|
+
const index = this.statusListeners.indexOf(callback);
|
78
|
+
if (-1 !== index) this.statusListeners.splice(index, 1);
|
79
|
+
}
|
80
|
+
/**
|
81
|
+
* 订阅音频资源
|
82
|
+
* @param appId 应用ID
|
83
|
+
* @param streamId 流ID
|
84
|
+
* @param clientId 客户端ID
|
85
|
+
*/ async subscribe(appId, streamId) {
|
86
|
+
let clientId = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : '';
|
87
|
+
try {
|
88
|
+
var _pc_localDescription;
|
89
|
+
// 先清理现有连接
|
90
|
+
if (this.peerConnection) {
|
91
|
+
this.peerConnection.close();
|
92
|
+
this.peerConnection = null;
|
93
|
+
}
|
94
|
+
this.setStatus("connecting");
|
95
|
+
// 1. 创建RTCPeerConnection
|
96
|
+
const rtcConfig = {};
|
97
|
+
const pc = new RTCPeerConnection(rtcConfig);
|
98
|
+
pc.ontrack = (event)=>{
|
99
|
+
// 音频流
|
100
|
+
this.player.onloadeddata = ()=>{
|
101
|
+
this.player.play();
|
102
|
+
};
|
103
|
+
this.player.srcObject = event.streams[0];
|
104
|
+
};
|
105
|
+
this.peerConnection = pc;
|
106
|
+
this.setupPeerConnectionListeners();
|
107
|
+
pc.addTransceiver('audio', {
|
108
|
+
direction: 'recvonly'
|
109
|
+
});
|
110
|
+
// 2. 创建Offer (SDP)
|
111
|
+
const offer = await pc.createOffer();
|
112
|
+
// 设置本地描述
|
113
|
+
await pc.setLocalDescription(offer);
|
114
|
+
// 等待ICE收集完成再继续
|
115
|
+
await this.waitForIceGathering(pc);
|
116
|
+
if (!(null === (_pc_localDescription = pc.localDescription) || void 0 === _pc_localDescription ? void 0 : _pc_localDescription.sdp)) throw new Error('Failed to create SDP offer');
|
117
|
+
// 3. 发送Offer到WTN服务订阅资源
|
118
|
+
let subscribeUrl = `${WTN_BASE_URL}/sub/${appId}/${streamId}?MuteVideo=true`;
|
119
|
+
if (clientId) subscribeUrl += `&clientid=${clientId}`;
|
120
|
+
const response = await fetch(subscribeUrl, {
|
121
|
+
method: 'POST',
|
122
|
+
headers: {
|
123
|
+
'Content-Type': 'application/sdp'
|
124
|
+
},
|
125
|
+
body: offer.sdp
|
126
|
+
});
|
127
|
+
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
128
|
+
// 4. 保存资源URL (用于销毁资源)
|
129
|
+
this.resourceUrl = response.headers.get('location') || '';
|
130
|
+
// 5. 设置远程SDP (Answer)
|
131
|
+
// 直接获取文本响应,因为服务器返回的是application/sdp格式而非json
|
132
|
+
const answerSdp = await response.text();
|
133
|
+
const answer = new RTCSessionDescription({
|
134
|
+
type: 'answer',
|
135
|
+
sdp: answerSdp
|
136
|
+
});
|
137
|
+
await this.peerConnection.setRemoteDescription(answer);
|
138
|
+
// 7. 返回结果
|
139
|
+
return {
|
140
|
+
status: this.status,
|
141
|
+
peerConnection: this.peerConnection
|
142
|
+
};
|
143
|
+
} catch (error) {
|
144
|
+
this.status = "failed";
|
145
|
+
console.error('Failed to subscribe WebRTC stream:', error);
|
146
|
+
return Promise.reject(error);
|
147
|
+
}
|
148
|
+
}
|
149
|
+
/**
|
150
|
+
* 销毁订阅资源
|
151
|
+
*/ async unsubscribe() {
|
152
|
+
try {
|
153
|
+
// 销毁订阅资源
|
154
|
+
if (!this.resourceUrl) throw new Error('No valid subscription resource URL to unsubscribe');
|
155
|
+
this.setStatus("closing");
|
156
|
+
const response = await fetch(this.resourceUrl, {
|
157
|
+
method: 'DELETE'
|
158
|
+
});
|
159
|
+
if (!response.ok) throw new Error(`Failed to unsubscribe: ${response.status} ${response.statusText}`);
|
160
|
+
// 关闭RTC连接
|
161
|
+
if (this.peerConnection) {
|
162
|
+
this.peerConnection.close();
|
163
|
+
this.peerConnection = null;
|
164
|
+
}
|
165
|
+
this.resourceUrl = '';
|
166
|
+
this.status = "closed";
|
167
|
+
return true;
|
168
|
+
} catch (error) {
|
169
|
+
console.error('Error unsubscribing resource:', error);
|
170
|
+
this.status = "failed";
|
171
|
+
return Promise.reject(error);
|
172
|
+
}
|
173
|
+
}
|
174
|
+
/**
|
175
|
+
* 静音/取消静音
|
176
|
+
* @param muted 是否静音
|
177
|
+
*/ setMuted(muted) {
|
178
|
+
this.player.muted = muted;
|
179
|
+
}
|
180
|
+
/**
|
181
|
+
* 关闭并清理资源
|
182
|
+
*/ close() {
|
183
|
+
// 关闭PeerConnection
|
184
|
+
this.closePeerConnection();
|
185
|
+
// Clean up audio element
|
186
|
+
if (this.player) {
|
187
|
+
this.player.pause();
|
188
|
+
this.player.srcObject = null;
|
189
|
+
this.player.remove();
|
190
|
+
}
|
191
|
+
// 重置状态
|
192
|
+
this.resourceUrl = '';
|
193
|
+
this.setStatus("idle");
|
194
|
+
}
|
195
|
+
/**
|
196
|
+
* 等待ICE收集完成
|
197
|
+
* @param pc RTCPeerConnection实例
|
198
|
+
*/ waitForIceGathering(pc) {
|
199
|
+
return new Promise((resolve)=>{
|
200
|
+
// 如果已经收集完成,直接返回
|
201
|
+
if ('complete' === pc.iceGatheringState) {
|
202
|
+
resolve();
|
203
|
+
return;
|
204
|
+
}
|
205
|
+
// 设置收集完成时的回调
|
206
|
+
const checkState = ()=>{
|
207
|
+
if ('complete' === pc.iceGatheringState) {
|
208
|
+
pc.removeEventListener('icegatheringstatechange', checkState);
|
209
|
+
resolve();
|
210
|
+
}
|
211
|
+
};
|
212
|
+
// 监听收集状态变化
|
213
|
+
pc.addEventListener('icegatheringstatechange', checkState);
|
214
|
+
// 添加超时处理,防止永远等待
|
215
|
+
setTimeout(()=>resolve(), 5000);
|
216
|
+
});
|
217
|
+
}
|
218
|
+
setupPeerConnectionListeners() {
|
219
|
+
if (!this.peerConnection) return;
|
220
|
+
this.peerConnection.oniceconnectionstatechange = ()=>{
|
221
|
+
var _this_peerConnection, _this_peerConnection1;
|
222
|
+
console.log('ICE connection state changed:', null === (_this_peerConnection = this.peerConnection) || void 0 === _this_peerConnection ? void 0 : _this_peerConnection.iceConnectionState);
|
223
|
+
switch(null === (_this_peerConnection1 = this.peerConnection) || void 0 === _this_peerConnection1 ? void 0 : _this_peerConnection1.iceConnectionState){
|
224
|
+
case 'connected':
|
225
|
+
case 'completed':
|
226
|
+
this.setStatus("connected");
|
227
|
+
break;
|
228
|
+
case 'failed':
|
229
|
+
case 'disconnected':
|
230
|
+
this.setStatus("failed");
|
231
|
+
break;
|
232
|
+
case 'closed':
|
233
|
+
this.setStatus("closed");
|
234
|
+
break;
|
235
|
+
default:
|
236
|
+
var _this_peerConnection2;
|
237
|
+
console.log('ICE connection state changed:', null === (_this_peerConnection2 = this.peerConnection) || void 0 === _this_peerConnection2 ? void 0 : _this_peerConnection2.iceConnectionState);
|
238
|
+
break;
|
239
|
+
}
|
240
|
+
};
|
241
|
+
this.peerConnection.onicecandidate = (event)=>{
|
242
|
+
if (event.candidate) console.log('New ICE candidate:', event.candidate);
|
243
|
+
};
|
244
|
+
}
|
245
|
+
/**
|
246
|
+
* 关闭PeerConnection
|
247
|
+
*/ closePeerConnection() {
|
248
|
+
if (this.peerConnection) {
|
249
|
+
this.peerConnection.close();
|
250
|
+
this.peerConnection = null;
|
251
|
+
}
|
252
|
+
}
|
253
|
+
/**
|
254
|
+
* 设置状态并触发监听回调
|
255
|
+
* @param newStatus 新状态
|
256
|
+
* @private 私有方法,仅内部使用
|
257
|
+
*/ setStatus(newStatus) {
|
258
|
+
const oldStatus = this.status;
|
259
|
+
if (oldStatus !== newStatus) {
|
260
|
+
this.status = newStatus;
|
261
|
+
// 触发所有监听器
|
262
|
+
for (const listener of this.statusListeners)try {
|
263
|
+
listener(newStatus);
|
264
|
+
} catch (error) {
|
265
|
+
console.error('Error in status listener:', error);
|
266
|
+
}
|
267
|
+
}
|
268
|
+
}
|
269
|
+
constructor(liveId){
|
270
|
+
this.peerConnection = null;
|
271
|
+
this.resourceUrl = '';
|
272
|
+
this.status = "idle";
|
273
|
+
this.statusListeners = [];
|
274
|
+
/**
|
275
|
+
* 获取直播信息
|
276
|
+
*/ this.getLiveData = async ()=>{
|
277
|
+
const api = new api_namespaceObject.CozeAPI({
|
278
|
+
baseURL: api_namespaceObject.COZE_CN_BASE_URL,
|
279
|
+
token: ''
|
280
|
+
});
|
281
|
+
return await api.audio.live.retrieve(this.liveId);
|
282
|
+
};
|
283
|
+
this.setupPeerConnectionListeners();
|
284
|
+
this.player = document.createElement('audio');
|
285
|
+
this.liveId = liveId;
|
286
|
+
}
|
287
|
+
}
|
288
|
+
var __webpack_export_target__ = exports;
|
289
|
+
for(var i in __webpack_exports__)__webpack_export_target__[i] = __webpack_exports__[i];
|
290
|
+
if (__webpack_exports__.__esModule) Object.defineProperty(__webpack_export_target__, '__esModule', {
|
291
|
+
value: true
|
292
|
+
});
|
@@ -163,6 +163,10 @@ var event_names_EventNames = /*#__PURE__*/ function(EventNames) {
|
|
163
163
|
* en: Mode updated
|
164
164
|
* zh: 更新房间模式成功
|
165
165
|
*/ EventNames["MODE_UPDATED"] = "server.mode.updated";
|
166
|
+
/**
|
167
|
+
* en: Live created
|
168
|
+
* zh: 直播创建
|
169
|
+
*/ EventNames["LIVE_CREATED"] = "server.live.created";
|
166
170
|
return EventNames;
|
167
171
|
}(event_names_EventNames || {});
|
168
172
|
/* ESM default export */ const event_names = event_names_EventNames;
|
package/dist/esm/index.js
CHANGED
@@ -286,6 +286,10 @@ var event_names_EventNames = /*#__PURE__*/ function(EventNames) {
|
|
286
286
|
* en: Mode updated
|
287
287
|
* zh: 更新房间模式成功
|
288
288
|
*/ EventNames["MODE_UPDATED"] = "server.mode.updated";
|
289
|
+
/**
|
290
|
+
* en: Live created
|
291
|
+
* zh: 直播创建
|
292
|
+
*/ EventNames["LIVE_CREATED"] = "server.live.created";
|
289
293
|
return EventNames;
|
290
294
|
}(event_names_EventNames || {});
|
291
295
|
/* ESM default export */ const event_names = event_names_EventNames;
|
@@ -625,6 +629,7 @@ class EngineClient extends RealtimeEventHandler {
|
|
625
629
|
if (isTestEnv) __WEBPACK_EXTERNAL_MODULE__volcengine_rtc__["default"].setParameter('ICE_CONFIG_REQUEST_URLS', [
|
626
630
|
'rtc-test.bytedance.com'
|
627
631
|
]);
|
632
|
+
else localStorage.removeItem('RTC_ACCESS_URLS-VolcEngine');
|
628
633
|
this.engine = __WEBPACK_EXTERNAL_MODULE__volcengine_rtc__["default"].createEngine(appId);
|
629
634
|
this.handleMessage = this.handleMessage.bind(this);
|
630
635
|
this.handleUserJoin = this.handleUserJoin.bind(this);
|
@@ -658,7 +663,7 @@ class RealtimeClient extends RealtimeEventHandler {
|
|
658
663
|
else {
|
659
664
|
const config = {};
|
660
665
|
if (this._config.prologueContent) config.prologue_content = this._config.prologueContent;
|
661
|
-
if (void 0 !== this._config.roomMode && null !== this._config.roomMode) config.room_mode = this._config.roomMode;
|
666
|
+
if (void 0 !== this._config.roomMode && null !== this._config.roomMode) config.room_mode = this._config.roomMode || __WEBPACK_EXTERNAL_MODULE__coze_api__.RoomMode.Default;
|
662
667
|
if (this._config.videoConfig) {
|
663
668
|
if (isScreenShareDevice(this._config.videoConfig.videoInputDeviceId)) config.video_config = {
|
664
669
|
stream_video_type: 'screen'
|
@@ -667,7 +672,8 @@ class RealtimeClient extends RealtimeEventHandler {
|
|
667
672
|
stream_video_type: 'main'
|
668
673
|
};
|
669
674
|
}
|
670
|
-
|
675
|
+
if (this._config.translateConfig) config.translate_config = this._config.translateConfig;
|
676
|
+
const params = {
|
671
677
|
bot_id: botId,
|
672
678
|
conversation_id: conversationId || void 0,
|
673
679
|
voice_id: voiceId && voiceId.length > 0 ? voiceId : void 0,
|
@@ -675,7 +681,8 @@ class RealtimeClient extends RealtimeEventHandler {
|
|
675
681
|
uid: this._config.userId || void 0,
|
676
682
|
workflow_id: this._config.workflowId || void 0,
|
677
683
|
config
|
678
|
-
}
|
684
|
+
};
|
685
|
+
roomInfo = await this._api.audio.rooms.create(params);
|
679
686
|
}
|
680
687
|
} catch (error) {
|
681
688
|
this.dispatch(event_names.ERROR, error);
|
@@ -0,0 +1,250 @@
|
|
1
|
+
import * as __WEBPACK_EXTERNAL_MODULE__coze_api__ from "@coze/api";
|
2
|
+
// WTN服务基础URL
|
3
|
+
const WTN_BASE_URL = 'https://wtn.volcvideo.com';
|
4
|
+
/**
|
5
|
+
* WebRTC资源状态
|
6
|
+
*/ var live_ResourceStatus = /*#__PURE__*/ function(ResourceStatus) {
|
7
|
+
ResourceStatus["IDLE"] = "idle";
|
8
|
+
ResourceStatus["CONNECTING"] = "connecting";
|
9
|
+
ResourceStatus["CONNECTED"] = "connected";
|
10
|
+
ResourceStatus["FAILED"] = "failed";
|
11
|
+
ResourceStatus["CLOSING"] = "closing";
|
12
|
+
ResourceStatus["CLOSED"] = "closed";
|
13
|
+
return ResourceStatus;
|
14
|
+
}({});
|
15
|
+
/**
|
16
|
+
* 同声传译客户端
|
17
|
+
*/ class WebLiveClient {
|
18
|
+
/**
|
19
|
+
* 获取当前连接状态
|
20
|
+
*/ getStatus() {
|
21
|
+
return this.status;
|
22
|
+
}
|
23
|
+
/**
|
24
|
+
* 添加状态变化监听器
|
25
|
+
* @param callback 状态变化回调函数
|
26
|
+
*/ onStatusChange(callback) {
|
27
|
+
this.statusListeners.push(callback);
|
28
|
+
}
|
29
|
+
/**
|
30
|
+
* 移除状态变化监听器
|
31
|
+
* @param callback 要移除的回调函数
|
32
|
+
*/ offStatusChange(callback) {
|
33
|
+
this.removeStatusListener(callback);
|
34
|
+
}
|
35
|
+
/**
|
36
|
+
* 移除状态变化监听器
|
37
|
+
* @param callback 要移除的回调函数
|
38
|
+
*/ removeStatusListener(callback) {
|
39
|
+
const index = this.statusListeners.indexOf(callback);
|
40
|
+
if (-1 !== index) this.statusListeners.splice(index, 1);
|
41
|
+
}
|
42
|
+
/**
|
43
|
+
* 订阅音频资源
|
44
|
+
* @param appId 应用ID
|
45
|
+
* @param streamId 流ID
|
46
|
+
* @param clientId 客户端ID
|
47
|
+
*/ async subscribe(appId, streamId) {
|
48
|
+
let clientId = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : '';
|
49
|
+
try {
|
50
|
+
var _pc_localDescription;
|
51
|
+
// 先清理现有连接
|
52
|
+
if (this.peerConnection) {
|
53
|
+
this.peerConnection.close();
|
54
|
+
this.peerConnection = null;
|
55
|
+
}
|
56
|
+
this.setStatus("connecting");
|
57
|
+
// 1. 创建RTCPeerConnection
|
58
|
+
const rtcConfig = {};
|
59
|
+
const pc = new RTCPeerConnection(rtcConfig);
|
60
|
+
pc.ontrack = (event)=>{
|
61
|
+
// 音频流
|
62
|
+
this.player.onloadeddata = ()=>{
|
63
|
+
this.player.play();
|
64
|
+
};
|
65
|
+
this.player.srcObject = event.streams[0];
|
66
|
+
};
|
67
|
+
this.peerConnection = pc;
|
68
|
+
this.setupPeerConnectionListeners();
|
69
|
+
pc.addTransceiver('audio', {
|
70
|
+
direction: 'recvonly'
|
71
|
+
});
|
72
|
+
// 2. 创建Offer (SDP)
|
73
|
+
const offer = await pc.createOffer();
|
74
|
+
// 设置本地描述
|
75
|
+
await pc.setLocalDescription(offer);
|
76
|
+
// 等待ICE收集完成再继续
|
77
|
+
await this.waitForIceGathering(pc);
|
78
|
+
if (!(null === (_pc_localDescription = pc.localDescription) || void 0 === _pc_localDescription ? void 0 : _pc_localDescription.sdp)) throw new Error('Failed to create SDP offer');
|
79
|
+
// 3. 发送Offer到WTN服务订阅资源
|
80
|
+
let subscribeUrl = `${WTN_BASE_URL}/sub/${appId}/${streamId}?MuteVideo=true`;
|
81
|
+
if (clientId) subscribeUrl += `&clientid=${clientId}`;
|
82
|
+
const response = await fetch(subscribeUrl, {
|
83
|
+
method: 'POST',
|
84
|
+
headers: {
|
85
|
+
'Content-Type': 'application/sdp'
|
86
|
+
},
|
87
|
+
body: offer.sdp
|
88
|
+
});
|
89
|
+
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
90
|
+
// 4. 保存资源URL (用于销毁资源)
|
91
|
+
this.resourceUrl = response.headers.get('location') || '';
|
92
|
+
// 5. 设置远程SDP (Answer)
|
93
|
+
// 直接获取文本响应,因为服务器返回的是application/sdp格式而非json
|
94
|
+
const answerSdp = await response.text();
|
95
|
+
const answer = new RTCSessionDescription({
|
96
|
+
type: 'answer',
|
97
|
+
sdp: answerSdp
|
98
|
+
});
|
99
|
+
await this.peerConnection.setRemoteDescription(answer);
|
100
|
+
// 7. 返回结果
|
101
|
+
return {
|
102
|
+
status: this.status,
|
103
|
+
peerConnection: this.peerConnection
|
104
|
+
};
|
105
|
+
} catch (error) {
|
106
|
+
this.status = "failed";
|
107
|
+
console.error('Failed to subscribe WebRTC stream:', error);
|
108
|
+
return Promise.reject(error);
|
109
|
+
}
|
110
|
+
}
|
111
|
+
/**
|
112
|
+
* 销毁订阅资源
|
113
|
+
*/ async unsubscribe() {
|
114
|
+
try {
|
115
|
+
// 销毁订阅资源
|
116
|
+
if (!this.resourceUrl) throw new Error('No valid subscription resource URL to unsubscribe');
|
117
|
+
this.setStatus("closing");
|
118
|
+
const response = await fetch(this.resourceUrl, {
|
119
|
+
method: 'DELETE'
|
120
|
+
});
|
121
|
+
if (!response.ok) throw new Error(`Failed to unsubscribe: ${response.status} ${response.statusText}`);
|
122
|
+
// 关闭RTC连接
|
123
|
+
if (this.peerConnection) {
|
124
|
+
this.peerConnection.close();
|
125
|
+
this.peerConnection = null;
|
126
|
+
}
|
127
|
+
this.resourceUrl = '';
|
128
|
+
this.status = "closed";
|
129
|
+
return true;
|
130
|
+
} catch (error) {
|
131
|
+
console.error('Error unsubscribing resource:', error);
|
132
|
+
this.status = "failed";
|
133
|
+
return Promise.reject(error);
|
134
|
+
}
|
135
|
+
}
|
136
|
+
/**
|
137
|
+
* 静音/取消静音
|
138
|
+
* @param muted 是否静音
|
139
|
+
*/ setMuted(muted) {
|
140
|
+
this.player.muted = muted;
|
141
|
+
}
|
142
|
+
/**
|
143
|
+
* 关闭并清理资源
|
144
|
+
*/ close() {
|
145
|
+
// 关闭PeerConnection
|
146
|
+
this.closePeerConnection();
|
147
|
+
// Clean up audio element
|
148
|
+
if (this.player) {
|
149
|
+
this.player.pause();
|
150
|
+
this.player.srcObject = null;
|
151
|
+
this.player.remove();
|
152
|
+
}
|
153
|
+
// 重置状态
|
154
|
+
this.resourceUrl = '';
|
155
|
+
this.setStatus("idle");
|
156
|
+
}
|
157
|
+
/**
|
158
|
+
* 等待ICE收集完成
|
159
|
+
* @param pc RTCPeerConnection实例
|
160
|
+
*/ waitForIceGathering(pc) {
|
161
|
+
return new Promise((resolve)=>{
|
162
|
+
// 如果已经收集完成,直接返回
|
163
|
+
if ('complete' === pc.iceGatheringState) {
|
164
|
+
resolve();
|
165
|
+
return;
|
166
|
+
}
|
167
|
+
// 设置收集完成时的回调
|
168
|
+
const checkState = ()=>{
|
169
|
+
if ('complete' === pc.iceGatheringState) {
|
170
|
+
pc.removeEventListener('icegatheringstatechange', checkState);
|
171
|
+
resolve();
|
172
|
+
}
|
173
|
+
};
|
174
|
+
// 监听收集状态变化
|
175
|
+
pc.addEventListener('icegatheringstatechange', checkState);
|
176
|
+
// 添加超时处理,防止永远等待
|
177
|
+
setTimeout(()=>resolve(), 5000);
|
178
|
+
});
|
179
|
+
}
|
180
|
+
setupPeerConnectionListeners() {
|
181
|
+
if (!this.peerConnection) return;
|
182
|
+
this.peerConnection.oniceconnectionstatechange = ()=>{
|
183
|
+
var _this_peerConnection, _this_peerConnection1;
|
184
|
+
console.log('ICE connection state changed:', null === (_this_peerConnection = this.peerConnection) || void 0 === _this_peerConnection ? void 0 : _this_peerConnection.iceConnectionState);
|
185
|
+
switch(null === (_this_peerConnection1 = this.peerConnection) || void 0 === _this_peerConnection1 ? void 0 : _this_peerConnection1.iceConnectionState){
|
186
|
+
case 'connected':
|
187
|
+
case 'completed':
|
188
|
+
this.setStatus("connected");
|
189
|
+
break;
|
190
|
+
case 'failed':
|
191
|
+
case 'disconnected':
|
192
|
+
this.setStatus("failed");
|
193
|
+
break;
|
194
|
+
case 'closed':
|
195
|
+
this.setStatus("closed");
|
196
|
+
break;
|
197
|
+
default:
|
198
|
+
var _this_peerConnection2;
|
199
|
+
console.log('ICE connection state changed:', null === (_this_peerConnection2 = this.peerConnection) || void 0 === _this_peerConnection2 ? void 0 : _this_peerConnection2.iceConnectionState);
|
200
|
+
break;
|
201
|
+
}
|
202
|
+
};
|
203
|
+
this.peerConnection.onicecandidate = (event)=>{
|
204
|
+
if (event.candidate) console.log('New ICE candidate:', event.candidate);
|
205
|
+
};
|
206
|
+
}
|
207
|
+
/**
|
208
|
+
* 关闭PeerConnection
|
209
|
+
*/ closePeerConnection() {
|
210
|
+
if (this.peerConnection) {
|
211
|
+
this.peerConnection.close();
|
212
|
+
this.peerConnection = null;
|
213
|
+
}
|
214
|
+
}
|
215
|
+
/**
|
216
|
+
* 设置状态并触发监听回调
|
217
|
+
* @param newStatus 新状态
|
218
|
+
* @private 私有方法,仅内部使用
|
219
|
+
*/ setStatus(newStatus) {
|
220
|
+
const oldStatus = this.status;
|
221
|
+
if (oldStatus !== newStatus) {
|
222
|
+
this.status = newStatus;
|
223
|
+
// 触发所有监听器
|
224
|
+
for (const listener of this.statusListeners)try {
|
225
|
+
listener(newStatus);
|
226
|
+
} catch (error) {
|
227
|
+
console.error('Error in status listener:', error);
|
228
|
+
}
|
229
|
+
}
|
230
|
+
}
|
231
|
+
constructor(liveId){
|
232
|
+
this.peerConnection = null;
|
233
|
+
this.resourceUrl = '';
|
234
|
+
this.status = "idle";
|
235
|
+
this.statusListeners = [];
|
236
|
+
/**
|
237
|
+
* 获取直播信息
|
238
|
+
*/ this.getLiveData = async ()=>{
|
239
|
+
const api = new __WEBPACK_EXTERNAL_MODULE__coze_api__.CozeAPI({
|
240
|
+
baseURL: __WEBPACK_EXTERNAL_MODULE__coze_api__.COZE_CN_BASE_URL,
|
241
|
+
token: ''
|
242
|
+
});
|
243
|
+
return await api.audio.live.retrieve(this.liveId);
|
244
|
+
};
|
245
|
+
this.setupPeerConnectionListeners();
|
246
|
+
this.player = document.createElement('audio');
|
247
|
+
this.liveId = liveId;
|
248
|
+
}
|
249
|
+
}
|
250
|
+
export { live_ResourceStatus as ResourceStatus, WebLiveClient };
|