fix test bug
All checks were successful
Build and Deploy Backend / build-and-deploy (push) Successful in 3m38s
All checks were successful
Build and Deploy Backend / build-and-deploy (push) Successful in 3m38s
This commit is contained in:
parent
5e3f0653c9
commit
0bf556018e
@ -39,7 +39,7 @@ spec:
|
||||
value: "8888"
|
||||
- name: HW_RTC_BACKEND_URL
|
||||
# 集群内部直接访问 rtc-backend Service,不走公网
|
||||
value: "http://rtc-backend-svc:8000"
|
||||
value: "http://rtc-backend:8000"
|
||||
|
||||
lifecycle:
|
||||
preStop:
|
||||
|
||||
266
hw_service_go/test/libopus.js
Normal file
266
hw_service_go/test/libopus.js
Normal file
File diff suppressed because one or more lines are too long
650
hw_service_go/test/test.html
Normal file
650
hw_service_go/test/test.html
Normal file
@ -0,0 +1,650 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>hw_service_go 硬件通讯测试</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #f5f5f5;
|
||||
color: #333;
|
||||
padding: 20px;
|
||||
}
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #fff;
|
||||
padding: 20px 24px;
|
||||
}
|
||||
.header h1 { font-size: 20px; font-weight: 600; }
|
||||
.header p { font-size: 13px; opacity: 0.8; margin-top: 4px; }
|
||||
|
||||
.section {
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.section:last-child { border-bottom: none; }
|
||||
.section-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #888;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.form-row:last-child { margin-bottom: 0; }
|
||||
.form-row label {
|
||||
min-width: 80px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
}
|
||||
.form-row input {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-family: 'SF Mono', Monaco, monospace;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
.form-row input:focus { border-color: #667eea; }
|
||||
|
||||
.btn {
|
||||
padding: 8px 18px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.btn-primary { background: #667eea; color: #fff; }
|
||||
.btn-primary:hover:not(:disabled) { background: #5a6fd6; }
|
||||
.btn-danger { background: #e74c3c; color: #fff; }
|
||||
.btn-danger:hover:not(:disabled) { background: #c0392b; }
|
||||
.btn-success { background: #27ae60; color: #fff; }
|
||||
.btn-success:hover:not(:disabled) { background: #219a52; }
|
||||
.btn-secondary { background: #95a5a6; color: #fff; }
|
||||
.btn-secondary:hover:not(:disabled) { background: #7f8c8d; }
|
||||
.btn-small { padding: 4px 10px; font-size: 12px; }
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.status-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: #bdc3c7;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
.status-dot.connected { background: #27ae60; }
|
||||
.status-dot.connecting { background: #f39c12; animation: pulse 1s infinite; }
|
||||
.status-dot.error { background: #e74c3c; }
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.4; }
|
||||
}
|
||||
|
||||
.log-container {
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
height: 400px;
|
||||
overflow-y: auto;
|
||||
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.log-container::-webkit-scrollbar { width: 6px; }
|
||||
.log-container::-webkit-scrollbar-track { background: transparent; }
|
||||
.log-container::-webkit-scrollbar-thumb { background: #555; border-radius: 3px; }
|
||||
.log-entry { white-space: pre-wrap; word-break: break-all; }
|
||||
.log-time { color: #858585; }
|
||||
.log-send { color: #dcdcaa; }
|
||||
.log-recv { color: #9cdcfe; }
|
||||
.log-binary { color: #ce9178; }
|
||||
.log-audio { color: #c586c0; }
|
||||
.log-error { color: #f44747; }
|
||||
.log-success { color: #6a9955; }
|
||||
.log-warning { color: #d7ba7d; }
|
||||
|
||||
.stats-bar {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
padding: 12px 24px;
|
||||
background: #fafafa;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
.stats-bar span { font-weight: 600; color: #333; }
|
||||
|
||||
.log-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
</style>
|
||||
<!-- Opus WASM 解码库 -->
|
||||
<script src="libopus.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- 标题 -->
|
||||
<div class="header">
|
||||
<h1>hw_service_go 硬件通讯测试</h1>
|
||||
<p>模拟 ESP32 硬件,测试 WebSocket 故事推送与 Opus 音频播放</p>
|
||||
</div>
|
||||
|
||||
<!-- 连接配置 -->
|
||||
<div class="section">
|
||||
<div class="section-title">连接配置</div>
|
||||
<div class="form-row">
|
||||
<label>服务地址</label>
|
||||
<input type="text" id="wsUrl" value="ws://localhost:8888/xiaozhi/v1/">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label>device-id</label>
|
||||
<input type="text" id="deviceId" placeholder="AA:BB:CC:DD:EE:FF">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label>client-id</label>
|
||||
<input type="text" id="clientId" value="">
|
||||
<button class="btn btn-secondary btn-small" onclick="generateClientId()">随机生成</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 控制面板 -->
|
||||
<div class="section">
|
||||
<div class="controls">
|
||||
<button class="btn btn-primary" id="btnConnect" onclick="connect()">连接</button>
|
||||
<button class="btn btn-danger" id="btnDisconnect" onclick="disconnect()" disabled>断开</button>
|
||||
<div style="width: 1px; height: 24px; background: #ddd;"></div>
|
||||
<button class="btn btn-success" id="btnStory" onclick="triggerStory()" disabled>▶ 触发故事播放</button>
|
||||
<button class="btn btn-danger" id="btnStop" onclick="stopPlayback()" disabled>■ 停止</button>
|
||||
<div style="flex:1"></div>
|
||||
<div class="status-indicator">
|
||||
<div class="status-dot" id="statusDot"></div>
|
||||
<span id="statusText">未连接</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计栏 -->
|
||||
<div class="stats-bar">
|
||||
<div>Opus 帧: <span id="statFrames">0</span></div>
|
||||
<div>音频时长: <span id="statDuration">0.0s</span></div>
|
||||
<div>Opus 库: <span id="statOpus">加载中...</span></div>
|
||||
</div>
|
||||
|
||||
<!-- 消息日志 -->
|
||||
<div class="section">
|
||||
<div class="log-header">
|
||||
<div class="section-title" style="margin-bottom:0">消息日志</div>
|
||||
<button class="btn btn-secondary btn-small" onclick="clearLog()">清空</button>
|
||||
</div>
|
||||
<div class="log-container" id="logContainer"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ============================================================
|
||||
// 全局状态
|
||||
// ============================================================
|
||||
let ws = null;
|
||||
let audioCtx = null;
|
||||
let opusDecoder = null;
|
||||
let opusReady = false;
|
||||
let handshaked = false; // hello 握手是否完成
|
||||
|
||||
// 播放状态
|
||||
let opusFrameCount = 0;
|
||||
let pcmBufferQueue = []; // Float32Array 队列
|
||||
let isPlaying = false;
|
||||
let nextPlayTime = 0;
|
||||
|
||||
// ============================================================
|
||||
// 工具函数
|
||||
// ============================================================
|
||||
function $(id) { return document.getElementById(id); }
|
||||
|
||||
function log(msg, type = 'info') {
|
||||
const container = $('logContainer');
|
||||
const now = new Date();
|
||||
const ts = `${now.toLocaleTimeString()}.${String(now.getMilliseconds()).padStart(3, '0')}`;
|
||||
const entry = document.createElement('div');
|
||||
entry.className = 'log-entry';
|
||||
|
||||
const typeClass = {
|
||||
send: 'log-send',
|
||||
recv: 'log-recv',
|
||||
binary: 'log-binary',
|
||||
audio: 'log-audio',
|
||||
error: 'log-error',
|
||||
success: 'log-success',
|
||||
warning: 'log-warning',
|
||||
}[type] || '';
|
||||
|
||||
const arrow = type === 'send' ? '→ ' : type === 'recv' ? '← ' : type === 'binary' ? '← ' : '';
|
||||
entry.innerHTML = `<span class="log-time">[${ts}]</span> <span class="${typeClass}">${arrow}${escapeHtml(msg)}</span>`;
|
||||
container.appendChild(entry);
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function updateStatus(state, text) {
|
||||
const dot = $('statusDot');
|
||||
dot.className = 'status-dot';
|
||||
if (state) dot.classList.add(state);
|
||||
$('statusText').textContent = text;
|
||||
}
|
||||
|
||||
function updateStats() {
|
||||
$('statFrames').textContent = opusFrameCount;
|
||||
const duration = (opusFrameCount * 60 / 1000).toFixed(1);
|
||||
$('statDuration').textContent = `${duration}s`;
|
||||
}
|
||||
|
||||
function generateClientId() {
|
||||
const id = 'test-' + Math.random().toString(36).substring(2, 10);
|
||||
$('clientId').value = id;
|
||||
}
|
||||
|
||||
function clearLog() {
|
||||
$('logContainer').innerHTML = '';
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Opus 解码器初始化
|
||||
// ============================================================
|
||||
function initOpusDecoder() {
|
||||
try {
|
||||
let mod = null;
|
||||
|
||||
// 检查 Module.instance 或全局 Module
|
||||
if (typeof Module !== 'undefined') {
|
||||
if (Module.instance && typeof Module.instance._opus_decoder_get_size === 'function') {
|
||||
mod = Module.instance;
|
||||
} else if (typeof Module._opus_decoder_get_size === 'function') {
|
||||
mod = Module;
|
||||
}
|
||||
}
|
||||
|
||||
if (!mod) {
|
||||
log('Opus 库未就绪,等待加载...', 'warning');
|
||||
$('statOpus').textContent = '加载失败';
|
||||
return false;
|
||||
}
|
||||
|
||||
const SAMPLE_RATE = 16000;
|
||||
const CHANNELS = 1;
|
||||
const FRAME_SIZE = 960; // 60ms @ 16kHz
|
||||
|
||||
// 获取解码器大小并分配内存
|
||||
const decoderSize = mod._opus_decoder_get_size(CHANNELS);
|
||||
const decoderPtr = mod._malloc(decoderSize);
|
||||
if (!decoderPtr) throw new Error('无法分配解码器内存');
|
||||
|
||||
// 初始化解码器
|
||||
const err = mod._opus_decoder_init(decoderPtr, SAMPLE_RATE, CHANNELS);
|
||||
if (err < 0) throw new Error(`Opus 解码器初始化失败: ${err}`);
|
||||
|
||||
opusDecoder = {
|
||||
mod,
|
||||
decoderPtr,
|
||||
frameSize: FRAME_SIZE,
|
||||
|
||||
decode(opusData) {
|
||||
// 为 Opus 数据分配内存
|
||||
const opusPtr = mod._malloc(opusData.length);
|
||||
mod.HEAPU8.set(opusData, opusPtr);
|
||||
|
||||
// 为 PCM 输出分配内存 (Int16 = 2 bytes)
|
||||
const pcmPtr = mod._malloc(FRAME_SIZE * 2);
|
||||
|
||||
// 解码
|
||||
const decodedSamples = mod._opus_decode(
|
||||
decoderPtr, opusPtr, opusData.length,
|
||||
pcmPtr, FRAME_SIZE, 0
|
||||
);
|
||||
|
||||
if (decodedSamples < 0) {
|
||||
mod._free(opusPtr);
|
||||
mod._free(pcmPtr);
|
||||
throw new Error(`Opus 解码失败: ${decodedSamples}`);
|
||||
}
|
||||
|
||||
// 读取 Int16 并转为 Float32
|
||||
const float32 = new Float32Array(decodedSamples);
|
||||
for (let i = 0; i < decodedSamples; i++) {
|
||||
const sample = mod.HEAP16[(pcmPtr >> 1) + i];
|
||||
float32[i] = sample / (sample < 0 ? 0x8000 : 0x7FFF);
|
||||
}
|
||||
|
||||
mod._free(opusPtr);
|
||||
mod._free(pcmPtr);
|
||||
return float32;
|
||||
},
|
||||
|
||||
destroy() {
|
||||
if (decoderPtr) mod._free(decoderPtr);
|
||||
}
|
||||
};
|
||||
|
||||
opusReady = true;
|
||||
log('Opus 解码器初始化成功 (16kHz, 单声道, 60ms/帧)', 'success');
|
||||
$('statOpus').textContent = '就绪';
|
||||
$('statOpus').style.color = '#27ae60';
|
||||
return true;
|
||||
} catch (e) {
|
||||
log(`Opus 初始化失败: ${e.message}`, 'error');
|
||||
$('statOpus').textContent = '失败';
|
||||
$('statOpus').style.color = '#e74c3c';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// WebSocket 连接
|
||||
// ============================================================
|
||||
function connect() {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
log('已经连接,请先断开', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const baseUrl = $('wsUrl').value.trim();
|
||||
const deviceId = $('deviceId').value.trim();
|
||||
const clientId = $('clientId').value.trim();
|
||||
|
||||
if (!deviceId) { log('请输入 device-id (MAC 地址)', 'error'); return; }
|
||||
if (!clientId) { log('请输入 client-id', 'error'); return; }
|
||||
|
||||
// 确保 AudioContext 存在(需要用户交互后创建)
|
||||
if (!audioCtx) {
|
||||
audioCtx = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 16000 });
|
||||
}
|
||||
|
||||
// 确保 Opus 解码器已初始化
|
||||
if (!opusReady) {
|
||||
if (!initOpusDecoder()) {
|
||||
log('Opus 解码器未就绪,无法连接', 'error');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 构建 URL
|
||||
const url = new URL(baseUrl);
|
||||
url.searchParams.set('device-id', deviceId);
|
||||
url.searchParams.set('client-id', clientId);
|
||||
const connUrl = url.toString();
|
||||
|
||||
log(`正在连接: ${connUrl}`, 'info');
|
||||
updateStatus('connecting', '连接中...');
|
||||
|
||||
$('btnConnect').disabled = true;
|
||||
|
||||
ws = new WebSocket(connUrl);
|
||||
ws.binaryType = 'arraybuffer';
|
||||
|
||||
ws.onopen = () => {
|
||||
log('WebSocket 连接成功,发送 hello 握手...', 'success');
|
||||
updateStatus('connecting', '握手中...');
|
||||
$('btnConnect').disabled = true;
|
||||
$('btnDisconnect').disabled = false;
|
||||
handshaked = false;
|
||||
|
||||
// 发送 hello 握手消息
|
||||
const helloMsg = JSON.stringify({ type: 'hello', mac: deviceId });
|
||||
ws.send(helloMsg);
|
||||
log(`发送: ${helloMsg}`, 'send');
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
if (event.data instanceof ArrayBuffer) {
|
||||
handleBinaryMessage(event.data);
|
||||
} else {
|
||||
handleTextMessage(event.data);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (err) => {
|
||||
log('WebSocket 错误', 'error');
|
||||
updateStatus('error', '连接错误');
|
||||
};
|
||||
|
||||
ws.onclose = (event) => {
|
||||
log(`WebSocket 已关闭 (code=${event.code}, reason=${event.reason || '无'})`, 'warning');
|
||||
updateStatus(null, '未连接');
|
||||
$('btnConnect').disabled = false;
|
||||
$('btnDisconnect').disabled = true;
|
||||
$('btnStory').disabled = true;
|
||||
$('btnStop').disabled = true;
|
||||
ws = null;
|
||||
};
|
||||
}
|
||||
|
||||
function disconnect() {
|
||||
if (ws) {
|
||||
ws.close();
|
||||
log('主动断开连接', 'info');
|
||||
}
|
||||
handshaked = false;
|
||||
stopPlayback();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 消息处理
|
||||
// ============================================================
|
||||
function handleTextMessage(data) {
|
||||
try {
|
||||
const msg = JSON.parse(data);
|
||||
log(`收到: ${JSON.stringify(msg)}`, 'recv');
|
||||
|
||||
// 处理 hello 握手响应
|
||||
if (msg.type === 'hello' && msg.status === 'ok') {
|
||||
handshaked = true;
|
||||
log(`握手成功,session_id=${msg.session_id}`, 'success');
|
||||
updateStatus('connected', '已连接');
|
||||
$('btnStory').disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.type === 'tts') {
|
||||
switch (msg.state) {
|
||||
case 'start':
|
||||
log('故事推送开始', 'audio');
|
||||
resetPlaybackState();
|
||||
$('btnStop').disabled = false;
|
||||
break;
|
||||
case 'sentence_start':
|
||||
if (msg.text) {
|
||||
log(`故事标题: ${msg.text}`, 'audio');
|
||||
}
|
||||
break;
|
||||
case 'stop':
|
||||
log('故事推送结束', 'audio');
|
||||
$('btnStop').disabled = true;
|
||||
// 标记流结束,等待播放完成
|
||||
log(`共接收 ${opusFrameCount} 个 Opus 帧,约 ${(opusFrameCount * 60 / 1000).toFixed(1)}s 音频`, 'success');
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
log(`收到文本: ${data}`, 'recv');
|
||||
}
|
||||
}
|
||||
|
||||
function handleBinaryMessage(data) {
|
||||
const frame = new Uint8Array(data);
|
||||
opusFrameCount++;
|
||||
updateStats();
|
||||
|
||||
// 每 20 帧打印一次,避免刷屏
|
||||
if (opusFrameCount <= 3 || opusFrameCount % 20 === 0) {
|
||||
log(`[Binary] Opus 帧 #${opusFrameCount} (${frame.length} bytes)`, 'binary');
|
||||
}
|
||||
|
||||
if (!opusDecoder) {
|
||||
log('Opus 解码器未初始化,丢弃帧', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const pcmFloat32 = opusDecoder.decode(frame);
|
||||
if (pcmFloat32 && pcmFloat32.length > 0) {
|
||||
pcmBufferQueue.push(pcmFloat32);
|
||||
schedulePlayback();
|
||||
}
|
||||
} catch (e) {
|
||||
log(`解码帧 #${opusFrameCount} 失败: ${e.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 音频播放(按时序排队)
|
||||
// ============================================================
|
||||
function resetPlaybackState() {
|
||||
opusFrameCount = 0;
|
||||
pcmBufferQueue = [];
|
||||
isPlaying = false;
|
||||
nextPlayTime = 0;
|
||||
updateStats();
|
||||
}
|
||||
|
||||
function schedulePlayback() {
|
||||
// 预缓冲:等待至少 3 帧再开始播放
|
||||
if (!isPlaying && pcmBufferQueue.length < 3) return;
|
||||
|
||||
if (!isPlaying) {
|
||||
isPlaying = true;
|
||||
log('开始音频播放...', 'audio');
|
||||
}
|
||||
|
||||
// 如果 AudioContext 被暂停(浏览器策略),恢复它
|
||||
if (audioCtx && audioCtx.state === 'suspended') {
|
||||
audioCtx.resume();
|
||||
}
|
||||
|
||||
// 直接把队列中所有帧排入播放时间线
|
||||
while (pcmBufferQueue.length > 0) {
|
||||
playPcmChunk(pcmBufferQueue.shift());
|
||||
}
|
||||
}
|
||||
|
||||
function playPcmChunk(pcmFloat32) {
|
||||
const buffer = audioCtx.createBuffer(1, pcmFloat32.length, 16000);
|
||||
buffer.copyToChannel(pcmFloat32, 0);
|
||||
|
||||
const source = audioCtx.createBufferSource();
|
||||
source.buffer = buffer;
|
||||
|
||||
const now = audioCtx.currentTime;
|
||||
const startTime = Math.max(now, nextPlayTime);
|
||||
|
||||
source.connect(audioCtx.destination);
|
||||
source.start(startTime);
|
||||
|
||||
// 下一帧紧接当前帧播放
|
||||
nextPlayTime = startTime + buffer.duration;
|
||||
}
|
||||
|
||||
function stopPlayback() {
|
||||
pcmBufferQueue = [];
|
||||
isPlaying = false;
|
||||
nextPlayTime = 0;
|
||||
if (audioCtx) {
|
||||
// 创建新的 AudioContext 来停止所有播放
|
||||
audioCtx.close();
|
||||
audioCtx = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 16000 });
|
||||
}
|
||||
log('播放已停止', 'audio');
|
||||
$('btnStop').disabled = true;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 触发故事
|
||||
// ============================================================
|
||||
function triggerStory() {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||
log('WebSocket 未连接', 'error');
|
||||
return;
|
||||
}
|
||||
if (!handshaked) {
|
||||
log('握手尚未完成,请等待', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const msg = JSON.stringify({ type: 'story' });
|
||||
ws.send(msg);
|
||||
log(`发送: ${msg}`, 'send');
|
||||
|
||||
// 重置统计
|
||||
resetPlaybackState();
|
||||
$('btnStop').disabled = false;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 页面初始化
|
||||
// ============================================================
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
// 生成默认 client-id
|
||||
generateClientId();
|
||||
|
||||
// 延迟初始化 Opus(等 WASM 加载完)
|
||||
const checkOpus = () => {
|
||||
if (typeof Module !== 'undefined' &&
|
||||
((Module.instance && typeof Module.instance._opus_decoder_get_size === 'function') ||
|
||||
typeof Module._opus_decoder_get_size === 'function')) {
|
||||
initOpusDecoder();
|
||||
} else {
|
||||
setTimeout(checkOpus, 200);
|
||||
}
|
||||
};
|
||||
setTimeout(checkOpus, 500);
|
||||
|
||||
log('页面加载完成,等待 Opus 库初始化...', 'info');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
x
Reference in New Issue
Block a user