/*
* noVNC: HTML5 VNC client
* Audio streaming module for remote desktop audio playback
*
* 音频流模块 - 用于播放远程桌面的音频输出
*
* 功能:
* - 通过 WebSocket 接收 Opus 编码的音频数据
* - 使用 Web Audio API 解码并播放音频
* - 支持音量控制和连接状态管理
*/
import * as Log from '../core/util/logging.js';
// 音频管理器类
class AudioManager {
constructor() {
// WebSocket 连接
this.ws = null;
this.connected = false;
// Web Audio API 组件
this.audioContext = null;
this.gainNode = null;
this.decoder = null;
// 播放调度
this.nextPlayTime = 0;
// 配置
this.volume = 0.8; // 默认音量 80%
this.sampleRate = 48000;
this.channels = 2;
// 重试配置
this.maxRetries = 5; // 最大重试次数
this.retryCount = 0; // 当前重试次数
this.retryDelay = 2000; // 重试间隔 (ms)
this.retryTimer = null; // 重试定时器
this.lastWsUrl = null; // 上次连接的URL(用于重试)
this.autoRetryEnabled = true; // 是否启用自动重试
// 回调函数
this.onStatusChange = null;
this.onMaxRetriesReached = null; // 达到最大重试次数的回调
// OpusDecoder 加载状态
this.opusDecoderReady = false;
this._checkOpusDecoder();
}
/**
* 检查 OpusDecoder 是否已加载
* @private
*/
_checkOpusDecoder() {
// opus-decoder 库使用 UMD 模式,导出为 window["opus-decoder"].OpusDecoder
const opusDecoderLib = window["opus-decoder"];
if (opusDecoderLib && opusDecoderLib.OpusDecoder) {
window.OpusDecoder = opusDecoderLib.OpusDecoder;
this.opusDecoderReady = true;
Log.Info('[Audio] OpusDecoder 加载成功');
} else {
Log.Warn('[Audio] OpusDecoder 未加载,音频功能可能无法使用');
this.opusDecoderReady = false;
}
}
/**
* 构建音频 WebSocket URL
* @param {string} baseUrl - 基础 URL (如 http://127.0.0.1:8088)
* @param {string} projectId - 项目 ID
* @param {string} userId - 用户 ID (可选,调试模式使用)
* @param {boolean} debugMode - 是否调试模式
* @returns {string} WebSocket URL
*/
buildWsUrl(baseUrl, projectId, userId = null, debugMode = false) {
// 将 http/https 转换为 ws/wss
const wsUrl = baseUrl
.replace(/^http/, 'ws')
.replace(/\/+$/, '');
// 调试模式:包含 user_id
// 正式模式:不包含 user_id
if (debugMode && userId) {
return `ws://192.168.1.34:8088/computer/audio/${userId}/${projectId}/ws`;
} else {
return `${wsUrl}/computer/audio/${projectId}/ws`;
}
}
/**
* 连接音频流
* @param {string} wsUrl - WebSocket URL
* @returns {Promise}
*/
async connect(wsUrl) {
if (this.connected) {
Log.Warn('[Audio] 已经连接,请先断开');
return;
}
// 检查 OpusDecoder
if (!this.opusDecoderReady) {
this._checkOpusDecoder();
if (!this.opusDecoderReady) {
Log.Error('[Audio] OpusDecoder 未加载,无法启动音频');
this._updateStatus('error', 'OpusDecoder 未加载');
return;
}
}
try {
this._updateStatus('connecting', '音频连接中...');
Log.Info('[Audio] 连接音频流: ' + wsUrl);
// 初始化 Web Audio API
await this._initAudioContext();
// 保存URL用于重试
this.lastWsUrl = wsUrl;
// 连接 WebSocket
this.ws = new WebSocket(wsUrl);
this.ws.binaryType = 'arraybuffer';
this.ws.onopen = () => {
Log.Info('[Audio] WebSocket 已连接');
this.connected = true;
this.retryCount = 0; // 连接成功,重置重试计数
this._updateStatus('connected', '音频已连接');
};
this.ws.onmessage = async (event) => {
try {
const data = new Uint8Array(event.data);
// 检查协议头 (0x01 表示 Opus 音频)
if (data[0] === 0x01) {
const opusData = data.slice(1);
await this._playAudioChunk(opusData);
}
} catch (err) {
Log.Error('[Audio] 处理音频数据失败: ' + err);
}
};
this.ws.onerror = (err) => {
Log.Error('[Audio] WebSocket 错误: ' + err);
this._updateStatus('error', '音频连接错误');
};
this.ws.onclose = () => {
Log.Info('[Audio] WebSocket 已关闭');
this.connected = false;
this._cleanup();
// 自动重试逻辑
if (this.autoRetryEnabled && this.retryCount < this.maxRetries) {
this.retryCount++;
Log.Info('[Audio] 将在 ' + (this.retryDelay / 1000) + ' 秒后重试 (' + this.retryCount + '/' + this.maxRetries + ')');
this._updateStatus('connecting', '重试中 (' + this.retryCount + '/' + this.maxRetries + ')');
this.retryTimer = setTimeout(() => {
if (this.lastWsUrl && this.autoRetryEnabled) {
this.connect(this.lastWsUrl);
}
}, this.retryDelay);
} else if (this.retryCount >= this.maxRetries) {
Log.Warn('[Audio] 已达到最大重试次数 (' + this.maxRetries + ')');
this._updateStatus('error', '连接失败,请手动重试');
if (this.onMaxRetriesReached) {
this.onMaxRetriesReached();
}
} else {
this._updateStatus('disconnected', '音频已断开');
}
};
} catch (err) {
Log.Error('[Audio] 连接失败: ' + err);
this._updateStatus('error', '音频启动失败');
throw err;
}
}
/**
* 断开音频流
*/
disconnect() {
// 禁用自动重试
this.autoRetryEnabled = false;
// 清除重试定时器
if (this.retryTimer) {
clearTimeout(this.retryTimer);
this.retryTimer = null;
}
if (this.ws) {
this.ws.close();
this.ws = null;
}
this.connected = false;
this.retryCount = 0;
this._cleanup();
this._updateStatus('disconnected', '音频已停止');
Log.Info('[Audio] 音频流已断开');
}
/**
* 设置音量
* @param {number} volume - 音量值 (0-1)
*/
setVolume(volume) {
this.volume = Math.max(0, Math.min(1, volume));
if (this.gainNode) {
this.gainNode.gain.value = this.volume;
}
Log.Debug('[Audio] 音量设置为: ' + Math.round(this.volume * 100) + '%');
}
/**
* 获取当前音量
* @returns {number} 音量值 (0-1)
*/
getVolume() {
return this.volume;
}
/**
* 初始化 Web Audio API
* @private
*/
async _initAudioContext() {
if (!this.audioContext) {
this.audioContext = new (window.AudioContext || window.webkitAudioContext)({
sampleRate: this.sampleRate,
latencyHint: 'interactive'
});
this.gainNode = this.audioContext.createGain();
this.gainNode.gain.value = this.volume;
this.gainNode.connect(this.audioContext.destination);
this.nextPlayTime = this.audioContext.currentTime + 0.1;
}
// 恢复 AudioContext(浏览器安全策略)
if (this.audioContext.state === 'suspended') {
try {
await this.audioContext.resume();
Log.Info('[Audio] AudioContext 已恢复');
} catch (e) {
Log.Warn('[Audio] AudioContext 自动恢复失败,等待用户交互: ' + e);
}
}
}
/**
* 尝试恢复 AudioContext
*/
async resume() {
if (this.audioContext && this.audioContext.state === 'suspended') {
try {
await this.audioContext.resume();
Log.Info('[Audio] AudioContext 已恢复');
} catch (e) {
Log.Error('[Audio] 恢复失败: ' + e);
}
}
}
/**
* 播放音频数据块
* @param {Uint8Array} opusData - Opus 编码的音频数据
* @private
*/
async _playAudioChunk(opusData) {
try {
// 初始化或复用 Opus 解码器
if (!this.decoder) {
Log.Debug('[Audio] 初始化 OpusDecoder...');
this.decoder = new window.OpusDecoder({
sampleRate: this.sampleRate,
channels: this.channels
});
await this.decoder.ready;
Log.Debug('[Audio] OpusDecoder 初始化完成');
}
// 解码 Opus 数据
const decoded = this.decoder.decodeFrame(opusData);
const channels = decoded.channelData;
const samplesDecoded = decoded.samplesDecoded;
if (!channels || channels.length < 2) {
Log.Warn('[Audio] 输出声道数据无效');
return;
}
const leftChannelData = channels[0];
const rightChannelData = channels[1];
// 创建 AudioBuffer
const audioBuffer = this.audioContext.createBuffer(
2, samplesDecoded, this.sampleRate
);
// 填充声道数据
audioBuffer.getChannelData(0).set(leftChannelData);
audioBuffer.getChannelData(1).set(rightChannelData);
// 调度播放
const source = this.audioContext.createBufferSource();
source.buffer = audioBuffer;
source.connect(this.gainNode);
// 计算播放时间
const currentTime = this.audioContext.currentTime;
if (this.nextPlayTime < currentTime) {
this.nextPlayTime = currentTime + 0.05; // 50ms 缓冲
}
source.start(this.nextPlayTime);
// 计算下一个播放时间点
const duration = samplesDecoded / this.sampleRate;
this.nextPlayTime += duration;
} catch (err) {
Log.Error('[Audio] 解码或播放失败: ' + err);
// 解码失败时重置解码器
if (this.decoder) {
try {
this.decoder.free();
} catch (e) {
// 忽略释放错误
}
this.decoder = null;
}
}
}
/**
* 清理资源
* @private
*/
_cleanup() {
// 重置播放调度
this.nextPlayTime = 0;
// 释放解码器
if (this.decoder) {
try {
this.decoder.free();
} catch (e) {
// 忽略释放错误
}
this.decoder = null;
}
}
/**
* 更新状态并触发回调
* @param {string} status - 状态 (connecting/connected/disconnected/error)
* @param {string} message - 状态消息
* @private
*/
_updateStatus(status, message) {
if (this.onStatusChange) {
this.onStatusChange(status, message);
}
}
/**
* 销毁音频管理器
*/
destroy() {
this.disconnect();
if (this.audioContext) {
this.audioContext.close();
this.audioContext = null;
}
this.gainNode = null;
}
}
// 导出单例
const audioManager = new AudioManager();
export default audioManager;