agents
Version:
A home for your AI agents
157 lines (156 loc) • 5.42 kB
JavaScript
//#region src/voice/sfu.ts
/**
* Pure utility functions for the Cloudflare Realtime SFU integration.
*
* Extracted from sfu.ts for testability. These handle:
* - Protobuf varint encoding/decoding
* - SFU WebSocket adapter protobuf packet encoding/decoding
* - Audio format conversion (48kHz stereo ↔ 16kHz mono)
*/
function decodeVarint(buf, offset) {
let value = 0;
let shift = 0;
let bytesRead = 0;
while (offset + bytesRead < buf.length) {
const byte = buf[offset + bytesRead];
value |= (byte & 127) << shift;
bytesRead++;
if ((byte & 128) === 0) break;
shift += 7;
}
return {
value,
bytesRead
};
}
function encodeVarint(value) {
const bytes = [];
while (value > 127) {
bytes.push(value & 127 | 128);
value >>>= 7;
}
bytes.push(value & 127);
return new Uint8Array(bytes);
}
/** Extract the PCM payload from a protobuf Packet message. */
function extractPayloadFromProtobuf(data) {
const buf = new Uint8Array(data);
let offset = 0;
while (offset < buf.length) {
const { value: tag, bytesRead: tagBytes } = decodeVarint(buf, offset);
offset += tagBytes;
const fieldNumber = tag >>> 3;
const wireType = tag & 7;
if (wireType === 0) {
const { bytesRead } = decodeVarint(buf, offset);
offset += bytesRead;
} else if (wireType === 2) {
const { value: length, bytesRead: lenBytes } = decodeVarint(buf, offset);
offset += lenBytes;
if (fieldNumber === 5) return buf.slice(offset, offset + length);
offset += length;
} else break;
}
return null;
}
/** Encode PCM payload into a protobuf Packet message (for ingest/buffer mode — just payload). */
function encodePayloadToProtobuf(payload) {
const tagBytes = encodeVarint(42);
const lengthBytes = encodeVarint(payload.length);
const result = new Uint8Array(tagBytes.length + lengthBytes.length + payload.length);
result.set(tagBytes, 0);
result.set(lengthBytes, tagBytes.length);
result.set(payload, tagBytes.length + lengthBytes.length);
return result.buffer;
}
/** Downsample 48kHz stereo interleaved PCM to 16kHz mono PCM (both 16-bit LE). */
function downsample48kStereoTo16kMono(stereo48k) {
const inputView = new DataView(stereo48k.buffer, stereo48k.byteOffset, stereo48k.byteLength);
const inputSamples = stereo48k.byteLength / 4;
const outputSamples = Math.floor(inputSamples / 3);
const output = /* @__PURE__ */ new ArrayBuffer(outputSamples * 2);
const outputView = new DataView(output);
for (let i = 0; i < outputSamples; i++) {
const srcOffset = i * 3 * 4;
if (srcOffset + 3 >= stereo48k.byteLength) break;
const left = inputView.getInt16(srcOffset, true);
const right = inputView.getInt16(srcOffset + 2, true);
const mono = Math.round((left + right) / 2);
outputView.setInt16(i * 2, mono, true);
}
return output;
}
/** Upsample 16kHz mono PCM to 48kHz stereo interleaved PCM (both 16-bit LE). */
function upsample16kMonoTo48kStereo(mono16k) {
const inputView = new DataView(mono16k);
const inputSamples = mono16k.byteLength / 2;
const outputSamples = inputSamples * 3;
const output = /* @__PURE__ */ new ArrayBuffer(outputSamples * 4);
const outputView = new DataView(output);
for (let i = 0; i < inputSamples; i++) {
const sample = inputView.getInt16(i * 2, true);
for (let j = 0; j < 3; j++) {
const outOffset = (i * 3 + j) * 4;
outputView.setInt16(outOffset, sample, true);
outputView.setInt16(outOffset + 2, sample, true);
}
}
return new Uint8Array(output);
}
const SFU_API_BASE = "https://rtc.live.cloudflare.com/v1";
async function sfuFetch(config, path, body) {
const url = `${SFU_API_BASE}/apps/${config.appId}${path}`;
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${config.apiToken}`,
"Content-Type": "application/json"
},
body: JSON.stringify(body)
});
if (!response.ok) {
const text = await response.text();
throw new Error(`SFU API error ${response.status}: ${text}`);
}
return response.json();
}
async function createSFUSession(config) {
const url = `${SFU_API_BASE}/apps/${config.appId}/sessions/new`;
const response = await fetch(url, {
method: "POST",
headers: { Authorization: `Bearer ${config.apiToken}` }
});
if (!response.ok) {
const text = await response.text();
throw new Error(`SFU API error ${response.status}: ${text}`);
}
return response.json();
}
async function addSFUTracks(config, sessionId, body) {
return sfuFetch(config, `/sessions/${sessionId}/tracks/new`, body);
}
async function renegotiateSFUSession(config, sessionId, sdp) {
const url = `${SFU_API_BASE}/apps/${config.appId}/sessions/${sessionId}/renegotiate`;
const response = await fetch(url, {
method: "PUT",
headers: {
Authorization: `Bearer ${config.apiToken}`,
"Content-Type": "application/json"
},
body: JSON.stringify({ sessionDescription: {
type: "answer",
sdp
} })
});
if (!response.ok) {
const text = await response.text();
throw new Error(`SFU renegotiate error ${response.status}: ${text}`);
}
return response.json();
}
async function createSFUWebSocketAdapter(config, tracks) {
return sfuFetch(config, "/adapters/websocket/new", { tracks });
}
//#endregion
export { addSFUTracks, createSFUSession, createSFUWebSocketAdapter, decodeVarint, downsample48kStereoTo16kMono, encodePayloadToProtobuf, encodeVarint, extractPayloadFromProtobuf, renegotiateSFUSession, sfuFetch, upsample16kMonoTo48kStereo };
//# sourceMappingURL=sfu.js.map