ibi-ai-talk 1.0.0
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 +24 -0
- package/dist/demo.html +5 -0
- package/dist/index.common.js +2653 -0
- package/dist/index.common.js.map +1 -0
- package/dist/index.umd.js +2665 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/index.umd.min.js +2 -0
- package/dist/index.umd.min.js.map +1 -0
- package/index.js +8 -0
- package/package.json +18 -0
- package/src/index.vue +57 -0
- package/src/utils/blocking-queue.js +98 -0
- package/src/utils/controller.js +34 -0
- package/src/utils/manager.js +39 -0
- package/src/utils/opus-codec.js +169 -0
- package/src/utils/ota-connector.js +115 -0
- package/src/utils/player.js +262 -0
- package/src/utils/recorder.js +420 -0
- package/src/utils/stream-context.js +184 -0
- package/src/utils/tools.js +517 -0
- package/src/utils/websocket.js +565 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// 检查Opus库是否已加载
|
|
2
|
+
export function checkOpusLoaded() {
|
|
3
|
+
try {
|
|
4
|
+
// 检查Module是否存在(本地库导出的全局变量)
|
|
5
|
+
if (typeof Module === 'undefined') {
|
|
6
|
+
throw new Error('Opus库未加载,Module对象不存在');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// 尝试先使用Module.instance(libopus.js最后一行导出方式)
|
|
10
|
+
if (typeof Module.instance !== 'undefined' && typeof Module.instance._opus_decoder_get_size === 'function') {
|
|
11
|
+
// 使用Module.instance对象替换全局Module对象
|
|
12
|
+
window.ModuleInstance = Module.instance;
|
|
13
|
+
console.log('Opus库加载成功(使用Module.instance)', 'success');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// 如果没有Module.instance,检查全局Module函数
|
|
17
|
+
if (typeof Module._opus_decoder_get_size === 'function') {
|
|
18
|
+
window.ModuleInstance = Module;
|
|
19
|
+
console.log('Opus库加载成功(使用全局Module)', 'success');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
throw new Error('Opus解码函数未找到,可能Module结构不正确');
|
|
23
|
+
} catch (err) {
|
|
24
|
+
console.log(`Opus库加载失败,请检查libopus.js文件是否存在且正确: ${err.message}`, 'error');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
// 创建一个Opus编码器
|
|
30
|
+
let opusEncoder = null;
|
|
31
|
+
export function initOpusEncoder() {
|
|
32
|
+
try {
|
|
33
|
+
if (opusEncoder) {
|
|
34
|
+
return opusEncoder; // 已经初始化过
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!window.ModuleInstance) {
|
|
38
|
+
console.log('无法创建Opus编码器:ModuleInstance不可用', 'error');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 初始化一个Opus编码器
|
|
43
|
+
const mod = window.ModuleInstance;
|
|
44
|
+
const sampleRate = 16000; // 16kHz采样率
|
|
45
|
+
const channels = 1; // 单声道
|
|
46
|
+
const application = 2048; // OPUS_APPLICATION_VOIP = 2048
|
|
47
|
+
|
|
48
|
+
// 创建编码器
|
|
49
|
+
opusEncoder = {
|
|
50
|
+
channels: channels,
|
|
51
|
+
sampleRate: sampleRate,
|
|
52
|
+
frameSize: 960, // 60ms @ 16kHz = 60 * 16 = 960 samples
|
|
53
|
+
maxPacketSize: 4000, // 最大包大小
|
|
54
|
+
module: mod,
|
|
55
|
+
|
|
56
|
+
// 初始化编码器
|
|
57
|
+
init: function () {
|
|
58
|
+
try {
|
|
59
|
+
// 获取编码器大小
|
|
60
|
+
const encoderSize = mod._opus_encoder_get_size(this.channels);
|
|
61
|
+
console.log(`Opus编码器大小: ${encoderSize}字节`, 'info');
|
|
62
|
+
|
|
63
|
+
// 分配内存
|
|
64
|
+
this.encoderPtr = mod._malloc(encoderSize);
|
|
65
|
+
if (!this.encoderPtr) {
|
|
66
|
+
throw new Error("无法分配编码器内存");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 初始化编码器
|
|
70
|
+
const err = mod._opus_encoder_init(
|
|
71
|
+
this.encoderPtr,
|
|
72
|
+
this.sampleRate,
|
|
73
|
+
this.channels,
|
|
74
|
+
application
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
if (err < 0) {
|
|
78
|
+
throw new Error(`Opus编码器初始化失败: ${err}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 设置位率 (16kbps)
|
|
82
|
+
mod._opus_encoder_ctl(this.encoderPtr, 4002, 16000); // OPUS_SET_BITRATE
|
|
83
|
+
|
|
84
|
+
// 设置复杂度 (0-10, 越高质量越好但CPU使用越多)
|
|
85
|
+
mod._opus_encoder_ctl(this.encoderPtr, 4010, 5); // OPUS_SET_COMPLEXITY
|
|
86
|
+
|
|
87
|
+
// 设置使用DTX (不传输静音帧)
|
|
88
|
+
mod._opus_encoder_ctl(this.encoderPtr, 4016, 1); // OPUS_SET_DTX
|
|
89
|
+
|
|
90
|
+
console.log("Opus编码器初始化成功", 'success');
|
|
91
|
+
return true;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (this.encoderPtr) {
|
|
94
|
+
mod._free(this.encoderPtr);
|
|
95
|
+
this.encoderPtr = null;
|
|
96
|
+
}
|
|
97
|
+
console.log(`Opus编码器初始化失败: ${error.message}`, 'error');
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
// 编码PCM数据为Opus
|
|
103
|
+
encode: function (pcmData) {
|
|
104
|
+
if (!this.encoderPtr) {
|
|
105
|
+
if (!this.init()) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const mod = this.module;
|
|
112
|
+
|
|
113
|
+
// 为PCM数据分配内存
|
|
114
|
+
const pcmPtr = mod._malloc(pcmData.length * 2); // 2字节/int16
|
|
115
|
+
|
|
116
|
+
// 将PCM数据复制到HEAP
|
|
117
|
+
for (let i = 0; i < pcmData.length; i++) {
|
|
118
|
+
mod.HEAP16[(pcmPtr >> 1) + i] = pcmData[i];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 为输出分配内存
|
|
122
|
+
const outPtr = mod._malloc(this.maxPacketSize);
|
|
123
|
+
|
|
124
|
+
// 进行编码
|
|
125
|
+
const encodedLen = mod._opus_encode(
|
|
126
|
+
this.encoderPtr,
|
|
127
|
+
pcmPtr,
|
|
128
|
+
this.frameSize,
|
|
129
|
+
outPtr,
|
|
130
|
+
this.maxPacketSize
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
if (encodedLen < 0) {
|
|
134
|
+
throw new Error(`Opus编码失败: ${encodedLen}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 复制编码后的数据
|
|
138
|
+
const opusData = new Uint8Array(encodedLen);
|
|
139
|
+
for (let i = 0; i < encodedLen; i++) {
|
|
140
|
+
opusData[i] = mod.HEAPU8[outPtr + i];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 释放内存
|
|
144
|
+
mod._free(pcmPtr);
|
|
145
|
+
mod._free(outPtr);
|
|
146
|
+
|
|
147
|
+
return opusData;
|
|
148
|
+
} catch (error) {
|
|
149
|
+
console.log(`Opus编码出错: ${error.message}`, 'error');
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
// 销毁编码器
|
|
155
|
+
destroy: function () {
|
|
156
|
+
if (this.encoderPtr) {
|
|
157
|
+
this.module._free(this.encoderPtr);
|
|
158
|
+
this.encoderPtr = null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
opusEncoder.init();
|
|
164
|
+
return opusEncoder;
|
|
165
|
+
} catch (error) {
|
|
166
|
+
console.log(`创建Opus编码器失败: ${error.message}`, 'error');
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// WebSocket 连接
|
|
2
|
+
export async function webSocketConnect(otaUrl, config) {
|
|
3
|
+
|
|
4
|
+
if (!validateConfig(config)) {
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// 发送OTA请求并获取返回的websocket信息
|
|
9
|
+
const otaResult = await sendOTA(otaUrl, config);
|
|
10
|
+
if (!otaResult) {
|
|
11
|
+
console.log('无法从OTA服务器获取信息', 'error');
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// 从OTA响应中提取websocket信息
|
|
16
|
+
const { websocket } = otaResult;
|
|
17
|
+
if (!websocket || !websocket.url) {
|
|
18
|
+
console.log('OTA响应中缺少websocket信息', 'error');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 使用OTA返回的websocket URL
|
|
23
|
+
let connUrl = new URL(websocket.url);
|
|
24
|
+
|
|
25
|
+
// 添加token参数(从OTA响应中获取)
|
|
26
|
+
if (websocket.token) {
|
|
27
|
+
if (websocket.token.startsWith("Bearer ")) {
|
|
28
|
+
connUrl.searchParams.append('authorization', websocket.token);
|
|
29
|
+
} else {
|
|
30
|
+
connUrl.searchParams.append('authorization', 'Bearer ' + websocket.token);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 添加认证参数(保持原有逻辑)
|
|
35
|
+
connUrl.searchParams.append('device-id', config.deviceId);
|
|
36
|
+
connUrl.searchParams.append('client-id', config.clientId);
|
|
37
|
+
|
|
38
|
+
const wsurl = connUrl.toString()
|
|
39
|
+
|
|
40
|
+
console.log(`正在连接: ${wsurl}`, 'info');
|
|
41
|
+
|
|
42
|
+
return new WebSocket(connUrl.toString());
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 验证配置
|
|
46
|
+
function validateConfig(config) {
|
|
47
|
+
if (!config.deviceMac) {
|
|
48
|
+
console.log('设备MAC地址不能为空', 'error');
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
if (!config.clientId) {
|
|
52
|
+
console.log('客户端ID不能为空', 'error');
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 判断wsUrl路径是否存在错误
|
|
59
|
+
function validateWsUrl(wsUrl) {
|
|
60
|
+
if (wsUrl === '') return false;
|
|
61
|
+
// 检查URL格式
|
|
62
|
+
if (!wsUrl.startsWith('ws://') && !wsUrl.startsWith('wss://')) {
|
|
63
|
+
console.log('URL格式错误,必须以ws://或wss://开头', 'error');
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
return true
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
// OTA发送请求,验证状态,并返回响应数据
|
|
71
|
+
async function sendOTA(otaUrl, config) {
|
|
72
|
+
try {
|
|
73
|
+
const res = await fetch(otaUrl, {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: {
|
|
76
|
+
'Content-Type': 'application/json',
|
|
77
|
+
'Device-Id': config.deviceId,
|
|
78
|
+
'Client-Id': config.clientId
|
|
79
|
+
},
|
|
80
|
+
body: JSON.stringify({
|
|
81
|
+
version: 0,
|
|
82
|
+
uuid: '',
|
|
83
|
+
application: {
|
|
84
|
+
name: 'xiaozhi-web-test',
|
|
85
|
+
version: '1.0.0',
|
|
86
|
+
compile_time: '2025-04-16 10:00:00',
|
|
87
|
+
idf_version: '4.4.3',
|
|
88
|
+
elf_sha256: '1234567890abcdef1234567890abcdef1234567890abcdef'
|
|
89
|
+
},
|
|
90
|
+
ota: { label: 'xiaozhi-web-test' },
|
|
91
|
+
board: {
|
|
92
|
+
type: 'xiaozhi-web-test',
|
|
93
|
+
ssid: 'xiaozhi-web-test',
|
|
94
|
+
rssi: 0,
|
|
95
|
+
channel: 0,
|
|
96
|
+
ip: '192.168.1.1',
|
|
97
|
+
mac: config.deviceMac
|
|
98
|
+
},
|
|
99
|
+
flash_size: 0,
|
|
100
|
+
minimum_free_heap_size: 0,
|
|
101
|
+
mac_address: config.deviceMac,
|
|
102
|
+
chip_model_name: '',
|
|
103
|
+
chip_info: { model: 0, cores: 0, revision: 0, features: 0 },
|
|
104
|
+
partition_table: [{ label: '', type: 0, subtype: 0, address: 0, size: 0 }]
|
|
105
|
+
})
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
|
|
109
|
+
|
|
110
|
+
const result = await res.json();
|
|
111
|
+
return result; // 返回完整的响应数据
|
|
112
|
+
} catch (err) {
|
|
113
|
+
return null; // 失败返回null
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// 音频播放模块
|
|
2
|
+
import BlockingQueue from "./blocking-queue.js";
|
|
3
|
+
import { createStreamingContext } from "./stream-context.js";
|
|
4
|
+
|
|
5
|
+
// 音频播放器类
|
|
6
|
+
export class AudioPlayer {
|
|
7
|
+
constructor() {
|
|
8
|
+
// 音频参数
|
|
9
|
+
this.SAMPLE_RATE = 16000;
|
|
10
|
+
this.CHANNELS = 1;
|
|
11
|
+
this.FRAME_SIZE = 960;
|
|
12
|
+
this.MIN_AUDIO_DURATION = 0.12;
|
|
13
|
+
|
|
14
|
+
// 状态
|
|
15
|
+
this.audioContext = null;
|
|
16
|
+
this.opusDecoder = null;
|
|
17
|
+
this.streamingContext = null;
|
|
18
|
+
this.queue = new BlockingQueue();
|
|
19
|
+
this.isPlaying = false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 获取或创建AudioContext
|
|
23
|
+
getAudioContext() {
|
|
24
|
+
if (!this.audioContext) {
|
|
25
|
+
this.audioContext = new (window.AudioContext ||
|
|
26
|
+
window.webkitAudioContext)({
|
|
27
|
+
sampleRate: this.SAMPLE_RATE,
|
|
28
|
+
latencyHint: "interactive",
|
|
29
|
+
});
|
|
30
|
+
console.log(
|
|
31
|
+
"创建音频上下文,采样率: " + this.SAMPLE_RATE + "Hz",
|
|
32
|
+
"debug"
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
return this.audioContext;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 初始化Opus解码器
|
|
39
|
+
async initOpusDecoder() {
|
|
40
|
+
if (this.opusDecoder) return this.opusDecoder;
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
if (typeof window.ModuleInstance === "undefined") {
|
|
44
|
+
if (typeof Module !== "undefined") {
|
|
45
|
+
window.ModuleInstance = Module;
|
|
46
|
+
console.log("使用全局Module作为ModuleInstance", "info");
|
|
47
|
+
} else {
|
|
48
|
+
throw new Error("Opus库未加载,ModuleInstance和Module对象都不存在");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const mod = window.ModuleInstance;
|
|
53
|
+
|
|
54
|
+
this.opusDecoder = {
|
|
55
|
+
channels: this.CHANNELS,
|
|
56
|
+
rate: this.SAMPLE_RATE,
|
|
57
|
+
frameSize: this.FRAME_SIZE,
|
|
58
|
+
module: mod,
|
|
59
|
+
decoderPtr: null,
|
|
60
|
+
|
|
61
|
+
init: function () {
|
|
62
|
+
if (this.decoderPtr) return true;
|
|
63
|
+
|
|
64
|
+
const decoderSize = mod._opus_decoder_get_size(this.channels);
|
|
65
|
+
console.log(`Opus解码器大小: ${decoderSize}字节`, "debug");
|
|
66
|
+
|
|
67
|
+
this.decoderPtr = mod._malloc(decoderSize);
|
|
68
|
+
if (!this.decoderPtr) {
|
|
69
|
+
throw new Error("无法分配解码器内存");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const err = mod._opus_decoder_init(
|
|
73
|
+
this.decoderPtr,
|
|
74
|
+
this.rate,
|
|
75
|
+
this.channels
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
if (err < 0) {
|
|
79
|
+
this.destroy();
|
|
80
|
+
throw new Error(`Opus解码器初始化失败: ${err}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
console.log("Opus解码器初始化成功", "success");
|
|
84
|
+
return true;
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
decode: function (opusData) {
|
|
88
|
+
if (!this.decoderPtr) {
|
|
89
|
+
if (!this.init()) {
|
|
90
|
+
throw new Error("解码器未初始化且无法初始化");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const mod = this.module;
|
|
96
|
+
|
|
97
|
+
const opusPtr = mod._malloc(opusData.length);
|
|
98
|
+
mod.HEAPU8.set(opusData, opusPtr);
|
|
99
|
+
|
|
100
|
+
const pcmPtr = mod._malloc(this.frameSize * 2);
|
|
101
|
+
|
|
102
|
+
const decodedSamples = mod._opus_decode(
|
|
103
|
+
this.decoderPtr,
|
|
104
|
+
opusPtr,
|
|
105
|
+
opusData.length,
|
|
106
|
+
pcmPtr,
|
|
107
|
+
this.frameSize,
|
|
108
|
+
0
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
if (decodedSamples < 0) {
|
|
112
|
+
mod._free(opusPtr);
|
|
113
|
+
mod._free(pcmPtr);
|
|
114
|
+
throw new Error(`Opus解码失败: ${decodedSamples}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const decodedData = new Int16Array(decodedSamples);
|
|
118
|
+
for (let i = 0; i < decodedSamples; i++) {
|
|
119
|
+
decodedData[i] = mod.HEAP16[(pcmPtr >> 1) + i];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
mod._free(opusPtr);
|
|
123
|
+
mod._free(pcmPtr);
|
|
124
|
+
|
|
125
|
+
return decodedData;
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.log(`Opus解码错误: ${error.message}`, "error");
|
|
128
|
+
return new Int16Array(0);
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
destroy: function () {
|
|
133
|
+
if (this.decoderPtr) {
|
|
134
|
+
this.module._free(this.decoderPtr);
|
|
135
|
+
this.decoderPtr = null;
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
if (!this.opusDecoder.init()) {
|
|
141
|
+
throw new Error("Opus解码器初始化失败");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return this.opusDecoder;
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.log(`Opus解码器初始化失败: ${error.message}`, "error");
|
|
147
|
+
this.opusDecoder = null;
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 启动音频缓冲
|
|
153
|
+
async startAudioBuffering() {
|
|
154
|
+
console.log("开始音频缓冲...", "info");
|
|
155
|
+
|
|
156
|
+
this.initOpusDecoder().catch((error) => {
|
|
157
|
+
console.log(`预初始化Opus解码器失败: ${error.message}`, "warning");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const timeout = 400;
|
|
161
|
+
while (true) {
|
|
162
|
+
const packets = await this.queue.dequeue(6, timeout, (count) => {
|
|
163
|
+
console.log(`缓冲超时,当前缓冲包数: ${count},开始播放`, "info");
|
|
164
|
+
});
|
|
165
|
+
if (packets.length) {
|
|
166
|
+
console.log(`已缓冲 ${packets.length} 个音频包,开始播放`, "info");
|
|
167
|
+
this.streamingContext.pushAudioBuffer(packets);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
while (true) {
|
|
171
|
+
const data = await this.queue.dequeue(99, 30);
|
|
172
|
+
if (data.length) {
|
|
173
|
+
this.streamingContext.pushAudioBuffer(data);
|
|
174
|
+
} else {
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 播放已缓冲的音频
|
|
182
|
+
async playBufferedAudio() {
|
|
183
|
+
try {
|
|
184
|
+
this.audioContext = this.getAudioContext();
|
|
185
|
+
|
|
186
|
+
if (!this.opusDecoder) {
|
|
187
|
+
console.log("初始化Opus解码器...", "info");
|
|
188
|
+
try {
|
|
189
|
+
this.opusDecoder = await this.initOpusDecoder();
|
|
190
|
+
if (!this.opusDecoder) {
|
|
191
|
+
throw new Error("解码器初始化失败");
|
|
192
|
+
}
|
|
193
|
+
console.log("Opus解码器初始化成功", "success");
|
|
194
|
+
} catch (error) {
|
|
195
|
+
console.log("Opus解码器初始化失败: " + error.message, "error");
|
|
196
|
+
this.isPlaying = false;
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!this.streamingContext) {
|
|
202
|
+
this.streamingContext = createStreamingContext(
|
|
203
|
+
this.opusDecoder,
|
|
204
|
+
this.audioContext,
|
|
205
|
+
this.SAMPLE_RATE,
|
|
206
|
+
this.CHANNELS,
|
|
207
|
+
this.MIN_AUDIO_DURATION
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
this.streamingContext.decodeOpusFrames();
|
|
212
|
+
this.streamingContext.startPlaying();
|
|
213
|
+
} catch (error) {
|
|
214
|
+
console.log(`播放已缓冲的音频出错: ${error.message}`, "error");
|
|
215
|
+
this.isPlaying = false;
|
|
216
|
+
this.streamingContext = null;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// 添加音频数据到队列
|
|
221
|
+
enqueueAudioData(opusData) {
|
|
222
|
+
if (opusData.length > 0) {
|
|
223
|
+
this.queue.enqueue(opusData);
|
|
224
|
+
} else {
|
|
225
|
+
console.log("收到空音频数据帧,可能是结束标志", "warning");
|
|
226
|
+
if (this.isPlaying && this.streamingContext) {
|
|
227
|
+
this.streamingContext.endOfStream = true;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// 预加载解码器
|
|
233
|
+
async preload() {
|
|
234
|
+
console.log("预加载Opus解码器...", "info");
|
|
235
|
+
try {
|
|
236
|
+
await this.initOpusDecoder();
|
|
237
|
+
console.log("Opus解码器预加载成功", "success");
|
|
238
|
+
} catch (error) {
|
|
239
|
+
console.log(
|
|
240
|
+
`Opus解码器预加载失败: ${error.message},将在需要时重试`,
|
|
241
|
+
"warning"
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// 启动播放系统
|
|
247
|
+
async start() {
|
|
248
|
+
await this.preload();
|
|
249
|
+
this.playBufferedAudio();
|
|
250
|
+
this.startAudioBuffering();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// 创建单例
|
|
255
|
+
let audioPlayerInstance = null;
|
|
256
|
+
|
|
257
|
+
export function getAudioPlayer() {
|
|
258
|
+
if (!audioPlayerInstance) {
|
|
259
|
+
audioPlayerInstance = new AudioPlayer();
|
|
260
|
+
}
|
|
261
|
+
return audioPlayerInstance;
|
|
262
|
+
}
|