UNPKG

agents

Version:

A home for your AI agents

157 lines (156 loc) 5.42 kB
//#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