UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

352 lines (351 loc) • 13.1 kB
import { u as toErrorObject } from "./error-coercion-1P2One0X.mjs"; import "./error-runtime-C9z8-Tyo.mjs"; import { o as createStreamingPcmResampler } from "./audio-energy-fMmNST7n.mjs"; import "./realtime-voice-provider-D_Fp61Jy.mjs"; import { a as OpenAIQuicksilverPendingAudio, i as OpenAIQuicksilverAudioClock } from "./realtime-quicksilver-audio-buffer-Djyp8pva.mjs"; import { randomInt } from "node:crypto"; //#region extensions/openai/realtime-quicksilver-media.runtime.ts const QUICKSILVER_SAMPLE_RATE = 48e3; const RELAY_SAMPLE_RATE = 24e3; const QUICKSILVER_CHANNELS = 2; const OPUS_FRAME_SAMPLES = 960; const INBOUND_REORDER_DEPTH = 4; const INBOUND_MAX_LATE_PACKETS = 100; const RTP_SEQUENCE_MODULUS = 65536; const RTP_SEQUENCE_HALF_RANGE = RTP_SEQUENCE_MODULUS / 2; function duplicateMonoToStereo(pcm48kMono) { const frameCount = Math.floor(pcm48kMono.length / 2); const stereo48k = new Int16Array(frameCount * QUICKSILVER_CHANNELS); for (let index = 0; index < frameCount; index += 1) { const sample = pcm48kMono.readInt16LE(index * 2); stereo48k[index * 2] = sample; stereo48k[index * 2 + 1] = sample; } return stereo48k; } function forwardSequenceDistance(expected, sequenceNumber) { return sequenceNumber - expected + RTP_SEQUENCE_MODULUS & 65535; } /** Pure-TypeScript WebRTC media peer with a WASM-only Opus codec. */ var OpenAIQuicksilverAudioPeer = class OpenAIQuicksilverAudioPeer { static async create(params) { const [werift, libopus] = await Promise.all([import("./lib-BKnG84XZ.mjs"), import("./dist-CoyYG0nB.mjs")]); params.signal?.throwIfAborted(); const peer = new werift.RTCPeerConnection({ codecs: { audio: [werift.useOPUS({ payloadType: 111 })], video: [] }, ...params.iceServers ? { iceServers: params.iceServers } : {} }); const transceiver = peer.addTransceiver("audio", { direction: "sendrecv" }); let encoder; let decoder; let encoderFreed = false; let decoderFreed = false; let peerClosed = false; const cleanup = async () => { if (encoder && !encoderFreed) { encoderFreed = true; encoder.free(); } if (decoder && !decoderFreed) { decoderFreed = true; decoder.free(); } if (!peerClosed) { peerClosed = true; await peer.close().catch(() => void 0); } }; const onAbort = () => void cleanup(); params.signal?.addEventListener("abort", onAbort, { once: true }); try { encoder = await libopus.createEncoder({ application: libopus.Application.Voip, channels: QUICKSILVER_CHANNELS, sampleRate: QUICKSILVER_SAMPLE_RATE, frameSize: OPUS_FRAME_SAMPLES }); params.signal?.throwIfAborted(); decoder = await libopus.createDecoder({ channels: QUICKSILVER_CHANNELS, sampleRate: QUICKSILVER_SAMPLE_RATE }); params.signal?.throwIfAborted(); params.signal?.removeEventListener("abort", onAbort); return new OpenAIQuicksilverAudioPeer({ callbacks: params.callbacks, decoder, encoder, libopus, peer, transceiver, werift }); } catch (error) { params.signal?.removeEventListener("abort", onAbort); await cleanup(); throw error; } } constructor(state) { this.state = state; this.connected = false; this.closed = false; this.outboundPacketFailed = false; this.discardingInboundAudio = false; this.inboundRtpState = { pendingPackets: /* @__PURE__ */ new Map() }; this.audioClock = new OpenAIQuicksilverAudioClock((skippedFrames) => { this.timestamp = this.timestamp + skippedFrames * OPUS_FRAME_SAMPLES >>> 0; this.sendNextAudioFrame(); }); this.pendingAudio = new OpenAIQuicksilverPendingAudio(); this.pendingResampledAudio = Buffer.alloc(60); this.inboundResampler = createStreamingPcmResampler(QUICKSILVER_SAMPLE_RATE, RELAY_SAMPLE_RATE); this.outboundResampler = createStreamingPcmResampler(RELAY_SAMPLE_RATE, QUICKSILVER_SAMPLE_RATE); this.sequenceNumber = randomInt(65536); this.subscribedTracks = /* @__PURE__ */ new Set(); this.timestamp = randomInt(4294967296); state.peer.onTrack.subscribe((track) => this.attachInboundTrack(track)); state.peer.connectionStateChange.subscribe((connectionState) => { if (this.closed) return; if (connectionState === "connected") { this.connected = true; this.audioClock.start(); } else if ([ "failed", "disconnected", "closed" ].includes(connectionState)) { this.connected = false; this.audioClock.stop(); this.state.callbacks.onError(/* @__PURE__ */ new Error(`GPT-Live WebRTC media connection ${connectionState}`)); } }); } async createOffer() { const offer = await this.state.peer.createOffer(); await this.state.peer.setLocalDescription(offer); const sdp = this.state.peer.localDescription?.sdp; if (!sdp?.trim()) throw new Error("werift did not produce a GPT-Live SDP offer"); return sdp; } async applyAnswer(answerSdp) { await this.state.peer.setRemoteDescription({ type: "answer", sdp: answerSdp }); this.attachInboundTrack(this.state.transceiver.receiver.track); } adoptPendingAudio(pendingAudio) { if (this.closed) { pendingAudio.clear(); return; } if (this.pendingAudio.length > 0) { pendingAudio.clear(); throw new Error("GPT-Live WebRTC peer already owns pending audio"); } this.pendingAudio = pendingAudio; } sendAudio(audio) { if (this.closed || audio.length < 2) return; this.pendingAudio.append(audio); } clearOutputAudio() { if (this.closed) return; const state = this.inboundRtpState; this.clearInboundFlushTimer(state); this.discardingInboundAudio = true; try { while (!this.closed && state.pendingPackets.size > 0) this.flushInboundReorderWindow(state, true); } catch (error) { this.fail(error); } finally { this.discardingInboundAudio = false; this.inboundResampler = createStreamingPcmResampler(QUICKSILVER_SAMPLE_RATE, RELAY_SAMPLE_RATE); } } close() { if (this.closed) return; this.closed = true; this.audioClock.stop(); this.pendingAudio.clear(); this.pendingResampledAudio = Buffer.alloc(0); this.inboundResampler.flush(); this.outboundResampler.flush(); this.resetInboundRtpState(); this.state.encoder.free(); this.state.decoder.free(); this.state.peer.close().catch(() => void 0); } attachInboundTrack(track) { if (track.kind !== "audio" || this.subscribedTracks.has(track.uuid)) return; this.subscribedTracks.add(track.uuid); track.onReceiveRtp.subscribe((packet) => this.handleInboundRtp(packet)); } reportMediaError(error) { (this.state.callbacks.onMediaError ?? this.state.callbacks.onError)(toErrorObject(error, "OpenAI GPT-Live WebRTC media failed")); } fail(error) { this.audioClock.stop(); this.state.callbacks.onError(toErrorObject(error, "OpenAI GPT-Live WebRTC media failed")); } handleInboundRtp(packet) { if (this.closed) return; try { this.state.callbacks.onRtpPacket?.(); const sequenceNumber = packet.header.sequenceNumber; if (this.activeInboundSsrc === void 0) this.activeInboundSsrc = packet.header.ssrc; else if (packet.header.ssrc !== this.activeInboundSsrc) throw new Error("GPT-Live WebRTC audio source changed unexpectedly"); const state = this.inboundRtpState; if (state.nextSequence === void 0) { state.nextSequence = sequenceNumber + 1 & 65535; this.decodeInboundPacket(packet); return; } const distance = forwardSequenceDistance(state.nextSequence, sequenceNumber); if (distance >= RTP_SEQUENCE_HALF_RANGE) { if (forwardSequenceDistance(sequenceNumber, state.nextSequence) <= INBOUND_MAX_LATE_PACKETS) return; throw new Error("GPT-Live WebRTC RTP sequence changed unexpectedly"); } if (state.pendingPackets.has(sequenceNumber)) return; if (distance === 0) { state.nextSequence = state.nextSequence + 1 & 65535; this.decodeInboundPacket(packet); this.clearInboundFlushTimer(state); this.drainInboundPackets(state); return; } state.pendingPackets.set(sequenceNumber, packet); this.flushInboundReorderWindow(state); this.scheduleInboundFlush(state); } catch (error) { this.fail(error); } } resetInboundRtpState() { this.clearInboundFlushTimer(this.inboundRtpState); this.inboundRtpState.nextSequence = void 0; this.inboundRtpState.pendingPackets.clear(); } flushInboundReorderWindow(state, force = false) { const expected = state.nextSequence; if (expected === void 0 || state.pendingPackets.size === 0) return; const pending = [...state.pendingPackets.keys()].map((sequenceNumber) => ({ sequenceNumber, distance: forwardSequenceDistance(expected, sequenceNumber) })).filter(({ distance }) => distance < RTP_SEQUENCE_HALF_RANGE).toSorted((left, right) => left.distance - right.distance); const nearest = pending[0]; const farthest = pending.at(-1); if (!nearest || !farthest || !force && farthest.distance < INBOUND_REORDER_DEPTH) return; this.clearInboundFlushTimer(state); const concealCount = Math.min(nearest.distance, INBOUND_REORDER_DEPTH); for (let index = 0; index < concealCount; index += 1) { state.nextSequence = (state.nextSequence ?? 0) + 1 & 65535; this.decodeInboundPacketLoss(); } if (nearest.distance > INBOUND_REORDER_DEPTH) state.nextSequence = nearest.sequenceNumber; this.drainInboundPackets(state); } drainInboundPackets(state) { while (state.nextSequence !== void 0) { const packet = state.pendingPackets.get(state.nextSequence); if (!packet) break; state.pendingPackets.delete(state.nextSequence); state.nextSequence = state.nextSequence + 1 & 65535; this.decodeInboundPacket(packet); } if (state.pendingPackets.size === 0) this.clearInboundFlushTimer(state); else this.scheduleInboundFlush(state); } scheduleInboundFlush(state) { if (this.closed || this.discardingInboundAudio || state.flushTimer || state.pendingPackets.size === 0) return; state.flushTimer = setTimeout(() => { state.flushTimer = void 0; if (this.closed) return; try { this.flushInboundReorderWindow(state, true); } catch (error) { this.fail(error); } }, 80); state.flushTimer.unref?.(); } clearInboundFlushTimer(state) { if (state.flushTimer) { clearTimeout(state.flushTimer); state.flushTimer = void 0; } } decodeInboundPacket(packet) { const opusPacket = this.state.werift.dePacketizeRtpPackets("opus", [packet]).data; let decoded; try { decoded = this.state.decoder.decode(opusPacket, { maxFrameSize: 5760 }); } catch (error) { if (!(error instanceof this.state.libopus.OpusError && error.code === this.state.libopus.OpusErrorCode.InvalidPacket || opusPacket.length === 0 && error instanceof RangeError) || !this.state.callbacks.onMediaError) throw error; this.reportMediaError(error); if (this.closed) return; decoded = this.state.decoder.decodePacketLoss(OPUS_FRAME_SAMPLES); } this.emitInboundPcm(decoded); } decodeInboundPacketLoss() { this.emitInboundPcm(this.state.decoder.decodePacketLoss(OPUS_FRAME_SAMPLES)); } emitInboundPcm(decoded) { if (this.closed || this.discardingInboundAudio) return; const frameCount = Math.floor(decoded.length / QUICKSILVER_CHANNELS); const mono48k = Buffer.alloc(frameCount * 2); for (let frame = 0; frame < frameCount; frame += 1) { const left = decoded[frame * 2] ?? 0; const right = decoded[frame * 2 + 1] ?? 0; mono48k.writeInt16LE(Math.round((left + right) / 2), frame * 2); } const relayPcm = this.inboundResampler.process(mono48k); if (relayPcm.length > 0) this.state.callbacks.onAudio(relayPcm); } sendNextAudioFrame() { if (!this.connected || this.closed) return; const frame = this.takeNextRelayFrame(); try { const resampled = this.outboundResampler.process(frame); this.pendingResampledAudio = Buffer.concat([this.pendingResampledAudio, resampled]); const monoFrameBytes = OPUS_FRAME_SAMPLES * 2; const monoFrame = Buffer.alloc(monoFrameBytes); this.pendingResampledAudio.copy(monoFrame, 0, 0, monoFrameBytes); this.pendingResampledAudio = Buffer.from(this.pendingResampledAudio.subarray(monoFrameBytes)); const opusPacket = this.state.encoder.encode(duplicateMonoToStereo(monoFrame), { frameSize: OPUS_FRAME_SAMPLES }); const rtp = new this.state.werift.RtpPacket(new this.state.werift.RtpHeader({ marker: false, payloadType: 111, sequenceNumber: this.sequenceNumber, timestamp: this.timestamp }), Buffer.from(opusPacket)); this.sequenceNumber = this.sequenceNumber + 1 & 65535; this.timestamp = this.timestamp + OPUS_FRAME_SAMPLES >>> 0; this.state.transceiver.sender.sendRtp(rtp).then(() => { this.outboundPacketFailed = false; }, (error) => { if (this.closed) return; if (this.outboundPacketFailed) { this.fail(error); return; } this.outboundPacketFailed = true; this.reportMediaError(error); }); } catch (error) { this.fail(error); } } takeNextRelayFrame() { const frame = Buffer.alloc(960); this.pendingAudio.readInto(frame); return frame; } }; //#endregion export { OpenAIQuicksilverAudioPeer as t };