import { Room, RoomEvent } from "livekit-client";
const CORE_SERVICE_BASE_URL = "https://live.convai.com";
const API_KEY = "<X-Api-Key>";
const CHARACTER_ID = "<your character_id>";
async function startVoiceSessionWithAudioData() {
// 1) 调用启用 audio_config 的 /connect
const connectResp = await fetch(`${CORE_SERVICE_BASE_URL}/connect`, {
method: "POST",
headers: {
"x-api-key": API_KEY,
"content-type": "application/json",
},
body: JSON.stringify({
character_id: CHARACTER_ID,
audio_config: {
output: {
audio_routing: "data_only",
max_chunk_duration_ms: 100,
add_wav_header: false,
},
},
}),
});
if (!connectResp.ok) {
throw new Error(`/connect failed: ${connectResp.status}`);
}
const connectData = await connectResp.json();
const { room_url, token, session_id } = connectData;
// 2) 使用 room_url + token 加入 WebRTC 房间
const room = new Room();
// 3) 设置音频上下文用于播放
const audioContext = new AudioContext({ sampleRate: 48000 });
const audioQueue = [];
room.on(RoomEvent.Connected, () => {
console.log("已连接到房间");
});
room.on(RoomEvent.DataReceived, (payload) => {
// LiveKit 数据载荷是字节 -> 解码 -> 解析 JSON
let msg;
try {
const text = new TextDecoder().decode(payload);
msg = JSON.parse(text);
} catch {
return;
}
// 4) 仅过滤 audio-data 消息
if (msg?.label !== "rtvi-ai") return;
if (msg?.type !== "server-message") return;
if (msg?.data?.type !== "audio-data") return;
const audioData = msg.data;
console.log("收到音频块:", {
sample_rate: audioData.sample_rate,
channels: audioData.channels,
includes_wav_header: audioData.includes_wav_header,
audio_length: audioData.audio.length,
});
// 5) 解码 base64 音频数据
const base64Audio = audioData.audio;
const binaryString = atob(base64Audio);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
// 6) 转换为 AudioBuffer 并加入播放队列
let pcmData = bytes;
if (audioData.includes_wav_header) {
// 跳过 WAV 头(前 44 字节)
pcmData = bytes.slice(44);
}
// 将字节转换为 16 位 PCM 采样
const samples = new Int16Array(
pcmData.buffer,
pcmData.byteOffset,
pcmData.byteLength / 2,
);
// 转换为用于 Web Audio API 的 Float32Array
const floatSamples = new Float32Array(samples.length);
for (let i = 0; i < samples.length; i++) {
floatSamples[i] = samples[i] / 32768.0; // 归一化到 [-1, 1]
}
// 创建 AudioBuffer
const audioBuffer = audioContext.createBuffer(
audioData.channels,
floatSamples.length / audioData.channels,
audioData.sample_rate,
);
audioBuffer.copyToChannel(floatSamples, 0);
// 加入播放队列
audioQueue.push(audioBuffer);
playNextChunk(); // 请自行实现播放队列管理
});
room.on(RoomEvent.Disconnected, () => {
console.log("已断开房间连接");
});
await room.connect(room_url, token);
// 返回对象,以便调用方稍后断开连接/清理
return { room, session_id, audioContext };
}
// 示例用法:
startVoiceSessionWithAudioData().catch(console.error);