feat: Implement WebRTC messaging and audio handling in the WebRTC service

This commit is contained in:
Marco Beretta 2024-12-21 16:18:23 +01:00
parent cf4b73b5e3
commit 9a33292f88
No known key found for this signature in database
GPG key ID: D918033D8E74CC11
8 changed files with 674 additions and 137 deletions

View file

@ -111,6 +111,7 @@
"winston": "^3.11.0",
"winston-daily-rotate-file": "^4.7.1",
"youtube-transcript": "^1.2.1",
"wrtc": "^0.4.7",
"ws": "^8.18.0",
"zod": "^3.22.4"
},

View file

@ -1,12 +1,15 @@
const { WebSocketServer } = require('ws');
const fs = require('fs');
const path = require('path');
const { RTCPeerConnection } = require('wrtc');
module.exports.WebSocketService = class {
constructor(server) {
this.wss = new WebSocketServer({ server, path: '/ws' });
this.log('Server initialized');
this.clientAudioBuffers = new Map();
this.activeClients = new Map();
this.iceServers = [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' },
];
this.setupHandlers();
}
@ -17,7 +20,13 @@ module.exports.WebSocketService = class {
setupHandlers() {
this.wss.on('connection', (ws) => {
const clientId = Date.now().toString();
this.clientAudioBuffers.set(clientId, []);
this.activeClients.set(clientId, {
ws,
state: 'idle',
audioBuffer: [],
currentTranscription: '',
isProcessing: false,
});
this.log(`Client connected: ${clientId}`);
@ -29,42 +38,175 @@ module.exports.WebSocketService = class {
return;
}
if (message.type === 'audio-chunk') {
if (!this.clientAudioBuffers.has(clientId)) {
this.clientAudioBuffers.set(clientId, []);
}
this.clientAudioBuffers.get(clientId).push(message.data);
}
switch (message.type) {
case 'call-start':
this.handleCallStart(clientId);
break;
if (message.type === 'request-response') {
const filePath = path.join(__dirname, './assets/response.mp3');
const audioFile = fs.readFileSync(filePath);
ws.send(JSON.stringify({ type: 'audio-response', data: audioFile.toString('base64') }));
}
case 'audio-chunk':
await this.handleAudioChunk(clientId, message.data);
break;
if (message.type === 'call-ended') {
const allChunks = this.clientAudioBuffers.get(clientId);
this.writeAudioFile(clientId, allChunks);
this.clientAudioBuffers.delete(clientId);
case 'processing-start':
await this.processAudioStream(clientId);
break;
case 'audio-received':
this.confirmAudioReceived(clientId);
break;
case 'call-ended':
this.handleCallEnd(clientId);
break;
}
});
ws.on('close', () => {
this.handleCallEnd(clientId);
this.activeClients.delete(clientId);
this.log(`Client disconnected: ${clientId}`);
this.clientAudioBuffers.delete(clientId);
});
ws.on('error', (error) => {
this.log(`Error for client ${clientId}: ${error.message}`);
this.handleCallEnd(clientId);
});
});
}
writeAudioFile(clientId, base64Chunks) {
if (!base64Chunks || base64Chunks.length === 0) {
async handleCallStart(clientId) {
const client = this.activeClients.get(clientId);
if (!client) {
return;
}
const filePath = path.join(__dirname, `recorded_${clientId}.webm`);
const buffer = Buffer.concat(
base64Chunks.map((chunk) => Buffer.from(chunk.split(',')[1], 'base64')),
try {
client.state = 'active';
client.audioBuffer = [];
client.currentTranscription = '';
client.isProcessing = false;
const peerConnection = new RTCPeerConnection({
iceServers: this.iceServers,
sdpSemantics: 'unified-plan',
});
client.peerConnection = peerConnection;
client.dataChannel = peerConnection.createDataChannel('audio', {
ordered: true,
maxRetransmits: 3,
});
client.dataChannel.onopen = () => this.log(`Data channel opened for ${clientId}`);
client.dataChannel.onmessage = async (event) => {
await this.handleAudioChunk(clientId, event.data);
};
peerConnection.onicecandidate = (event) => {
if (event.candidate) {
client.ws.send(
JSON.stringify({
type: 'ice-candidate',
candidate: event.candidate,
}),
);
}
};
peerConnection.onnegotiationneeded = async () => {
try {
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
client.ws.send(
JSON.stringify({
type: 'webrtc-offer',
sdp: peerConnection.localDescription,
}),
);
} catch (error) {
this.log(`Negotiation failed for ${clientId}: ${error}`);
}
};
this.log(`Call started for client ${clientId}`);
} catch (error) {
this.log(`Error starting call for ${clientId}: ${error.message}`);
this.handleCallEnd(clientId);
}
}
async handleAudioChunk(clientId, data) {
const client = this.activeClients.get(clientId);
if (!client || client.state !== 'active') {
return;
}
client.audioBuffer.push(data);
client.ws.send(JSON.stringify({ type: 'audio-received' }));
}
async processAudioStream(clientId) {
const client = this.activeClients.get(clientId);
if (!client || client.state !== 'active' || client.isProcessing) {
return;
}
client.isProcessing = true;
try {
// Process transcription
client.ws.send(
JSON.stringify({
type: 'transcription',
data: 'Processing audio...',
}),
);
// Stream LLM response
client.ws.send(
JSON.stringify({
type: 'llm-response',
data: 'Processing response...',
}),
);
// Stream TTS chunks
client.ws.send(
JSON.stringify({
type: 'tts-chunk',
data: 'audio_data_here',
}),
);
} catch (error) {
this.log(`Processing error for client ${clientId}: ${error.message}`);
} finally {
client.isProcessing = false;
client.audioBuffer = [];
}
}
confirmAudioReceived(clientId) {
const client = this.activeClients.get(clientId);
if (!client) {
return;
}
client.ws.send(
JSON.stringify({
type: 'audio-received',
data: null,
}),
);
fs.writeFileSync(filePath, buffer);
this.log(`Saved audio to ${filePath}`);
}
handleCallEnd(clientId) {
const client = this.activeClients.get(clientId);
if (!client) {
return;
}
client.state = 'idle';
client.audioBuffer = [];
client.currentTranscription = '';
}
};

View file

@ -56,6 +56,17 @@ export type BadgeItem = {
isAvailable: boolean;
};
export interface RTCMessage {
type:
| 'audio-chunk'
| 'audio-received'
| 'transcription'
| 'llm-response'
| 'tts-chunk'
| 'call-ended';
data?: string | ArrayBuffer | null;
}
export type AssistantListItem = {
id: string;
name: string;

View file

@ -1,67 +1,106 @@
import { useState, useRef, useCallback } from 'react';
import useWebSocket from './useWebSocket';
import { WebRTCService } from '../services/WebRTC/WebRTCService';
import type { RTCMessage } from '~/common';
import useWebSocket from './useWebSocket';
const SILENCE_THRESHOLD = -50;
const SILENCE_DURATION = 1000;
const useCall = () => {
const { sendMessage } = useWebSocket();
const { sendMessage: wsMessage } = useWebSocket();
const [isCalling, setIsCalling] = useState(false);
const [isProcessing, setIsProcessing] = useState(false);
const audioContextRef = useRef<AudioContext | null>(null);
const analyserRef = useRef<AnalyserNode | null>(null);
const audioChunksRef = useRef<Blob[]>([]);
const silenceStartRef = useRef<number | null>(null);
const intervalRef = useRef<number | null>(null);
const webrtcServiceRef = useRef<WebRTCService | null>(null);
const checkSilence = useCallback(() => {
if (!analyserRef.current || !isCalling) {
const sendAudioChunk = useCallback(() => {
if (audioChunksRef.current.length === 0) {
return;
}
const data = new Float32Array(analyserRef.current.frequencyBinCount);
analyserRef.current.getFloatFrequencyData(data);
const avg = data.reduce((a, b) => a + b) / data.length;
if (avg < SILENCE_THRESHOLD) {
if (!silenceStartRef.current) {
silenceStartRef.current = Date.now();
} else if (Date.now() - silenceStartRef.current > SILENCE_DURATION) {
sendMessage({ type: 'request-response' });
silenceStartRef.current = null;
}
} else {
silenceStartRef.current = null;
const audioBlob = new Blob(audioChunksRef.current, { type: 'audio/webm' });
// Send audio through WebRTC data channel
webrtcServiceRef.current?.sendAudioChunk(audioBlob);
// Signal processing start via WebSocket
wsMessage({ type: 'processing-start' });
audioChunksRef.current = [];
setIsProcessing(true);
}, [wsMessage]);
const handleRTCMessage = useCallback((message: RTCMessage) => {
if (message.type === 'audio-received') {
// Backend confirmed audio receipt
setIsProcessing(true);
}
}, [isCalling, sendMessage]);
}, []);
const startCall = useCallback(async () => {
webrtcServiceRef.current = new WebRTCService(sendMessage);
// Initialize WebRTC with message handler
webrtcServiceRef.current = new WebRTCService(handleRTCMessage);
await webrtcServiceRef.current.initializeCall();
// Signal call start via WebSocket
wsMessage({ type: 'call-start' });
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
audioContextRef.current = new AudioContext();
const source = audioContextRef.current.createMediaStreamSource(stream);
analyserRef.current = audioContextRef.current.createAnalyser();
source.connect(analyserRef.current);
intervalRef.current = window.setInterval(checkSilence, 100);
// Start VAD monitoring
intervalRef.current = window.setInterval(() => {
if (!analyserRef.current || !isCalling) {
return;
}
const data = new Float32Array(analyserRef.current.frequencyBinCount);
analyserRef.current.getFloatFrequencyData(data);
const avg = data.reduce((a, b) => a + b) / data.length;
if (avg < SILENCE_THRESHOLD) {
if (silenceStartRef.current === null) {
silenceStartRef.current = Date.now();
} else if (Date.now() - silenceStartRef.current > SILENCE_DURATION) {
sendAudioChunk();
silenceStartRef.current = null;
}
} else {
silenceStartRef.current = null;
}
}, 100);
setIsCalling(true);
}, [checkSilence, sendMessage]);
}, [handleRTCMessage, wsMessage, sendAudioChunk]);
const hangUp = useCallback(async () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
analyserRef.current = null;
audioContextRef.current?.close();
audioContextRef.current = null;
await webrtcServiceRef.current?.endCall();
webrtcServiceRef.current = null;
setIsCalling(false);
sendMessage({ type: 'call-ended' });
}, [sendMessage]);
return { isCalling, startCall, hangUp };
setIsCalling(false);
setIsProcessing(false);
wsMessage({ type: 'call-ended' });
}, [wsMessage]);
return {
isCalling,
isProcessing,
startCall,
hangUp,
};
};
export default useCall;

View file

@ -1,75 +1,44 @@
import { useRef, useCallback } from 'react';
import { WebRTCService } from '../services/WebRTC/WebRTCService';
import type { RTCMessage } from '~/common';
import useWebSocket from './useWebSocket';
const SILENCE_THRESHOLD = -50;
const SILENCE_DURATION = 1000;
const useWebRTC = () => {
const { sendMessage } = useWebSocket();
const localStreamRef = useRef<MediaStream | null>(null);
const audioContextRef = useRef<AudioContext | null>(null);
const analyserRef = useRef<AnalyserNode | null>(null);
const silenceStartTime = useRef<number | null>(null);
const isProcessingRef = useRef(false);
const webrtcServiceRef = useRef<WebRTCService | null>(null);
const log = (msg: string) => console.log(`[WebRTC ${new Date().toISOString()}] ${msg}`);
const processAudioLevel = () => {
if (!analyserRef.current || !isProcessingRef.current) {
return;
}
const dataArray = new Float32Array(analyserRef.current.frequencyBinCount);
analyserRef.current.getFloatFrequencyData(dataArray);
const average = dataArray.reduce((a, b) => a + b) / dataArray.length;
if (average < SILENCE_THRESHOLD) {
if (!silenceStartTime.current) {
silenceStartTime.current = Date.now();
log(`Silence started: ${average}dB`);
} else if (Date.now() - silenceStartTime.current > SILENCE_DURATION) {
log('Silence threshold reached - requesting response');
sendMessage({ type: 'request-response' });
silenceStartTime.current = null;
const handleRTCMessage = useCallback(
(message: RTCMessage) => {
switch (message.type) {
case 'audio-chunk':
sendMessage({ type: 'processing-start' });
break;
case 'transcription':
case 'llm-response':
case 'tts-chunk':
// TODO: Handle streaming responses
break;
}
} else {
silenceStartTime.current = null;
}
requestAnimationFrame(processAudioLevel);
};
},
[sendMessage],
);
const startLocalStream = async () => {
try {
log('Starting audio capture');
localStreamRef.current = await navigator.mediaDevices.getUserMedia({ audio: true });
audioContextRef.current = new AudioContext();
const source = audioContextRef.current.createMediaStreamSource(localStreamRef.current);
analyserRef.current = audioContextRef.current.createAnalyser();
source.connect(analyserRef.current);
isProcessingRef.current = true;
processAudioLevel();
log('Audio capture started');
webrtcServiceRef.current = new WebRTCService(handleRTCMessage);
await webrtcServiceRef.current.initializeCall();
sendMessage({ type: 'call-start' });
} catch (error) {
log(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
console.error(error);
throw error;
}
};
const stopLocalStream = useCallback(() => {
log('Stopping audio capture');
isProcessingRef.current = false;
audioContextRef.current?.close();
localStreamRef.current?.getTracks().forEach((track) => track.stop());
localStreamRef.current = null;
audioContextRef.current = null;
analyserRef.current = null;
silenceStartTime.current = null;
}, []);
webrtcServiceRef.current?.endCall();
webrtcServiceRef.current = null;
sendMessage({ type: 'call-ended' });
}, [sendMessage]);
return { startLocalStream, stopLocalStream };
};

View file

@ -1,39 +1,47 @@
import { useEffect, useRef, useState, useCallback } from 'react';
import { useGetWebsocketUrlQuery } from 'librechat-data-provider/react-query';
import type { RTCMessage } from '~/common';
const useWebSocket = () => {
const { data: url } = useGetWebsocketUrlQuery();
const { data: data } = useGetWebsocketUrlQuery();
const [isConnected, setIsConnected] = useState(false);
const wsRef = useRef<WebSocket | null>(null);
console.log('wsConfig:', url?.url);
const connect = useCallback(() => {
if (!url?.url) {
if (!data || !data.url) {
return;
}
wsRef.current = new WebSocket(url?.url);
wsRef.current = new WebSocket(data.url);
wsRef.current.onopen = () => setIsConnected(true);
wsRef.current.onclose = () => setIsConnected(false);
wsRef.current.onerror = (err) => console.error('WebSocket error:', err);
wsRef.current.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === 'audio-response') {
const audioData = msg.data;
const audio = new Audio(`data:audio/mp3;base64,${audioData}`);
audio.play().catch(console.error);
const msg: RTCMessage = JSON.parse(event.data);
switch (msg.type) {
case 'transcription':
// TODO: Handle transcription update
break;
case 'llm-response':
// TODO: Handle LLM streaming response
break;
case 'tts-chunk':
if (typeof msg.data === 'string') {
const audio = new Audio(`data:audio/mp3;base64,${msg.data}`);
audio.play().catch(console.error);
}
break;
}
};
}, [url?.url]);
}, [data?.url]);
useEffect(() => {
connect();
return () => wsRef.current?.close();
}, [connect]);
const sendMessage = useCallback((message: any) => {
const sendMessage = useCallback((message: Record<string, unknown>) => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(JSON.stringify(message));
}

View file

@ -1,36 +1,55 @@
import type { RTCMessage } from '~/common';
export class WebRTCService {
private peerConnection: RTCPeerConnection | null = null;
private dataChannel: RTCDataChannel | null = null;
private mediaRecorder: MediaRecorder | null = null;
private sendMessage: (msg: any) => void;
private onMessage: (msg: RTCMessage) => void;
constructor(sendMessage: (msg: any) => void) {
this.sendMessage = sendMessage;
constructor(onMessage: (msg: RTCMessage) => void) {
this.onMessage = onMessage;
}
async initializeCall() {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
this.peerConnection = new RTCPeerConnection();
stream.getTracks().forEach((track) => this.peerConnection?.addTrack(track, stream));
this.dataChannel = this.peerConnection.createDataChannel('audio');
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
this.mediaRecorder = new MediaRecorder(stream);
this.mediaRecorder.ondataavailable = (e) => {
if (e.data.size > 0) {
const reader = new FileReader();
reader.onload = () => {
this.sendMessage({
type: 'audio-chunk',
data: reader.result,
});
};
reader.readAsDataURL(e.data);
if (e.data.size > 0 && this.dataChannel?.readyState === 'open') {
e.data.arrayBuffer().then((buffer) => {
this.dataChannel?.send(buffer);
});
}
};
this.mediaRecorder.start();
this.mediaRecorder.start(100);
this.setupDataChannel();
}
private setupDataChannel() {
if (!this.dataChannel) {
return;
}
this.dataChannel.onmessage = (event) => {
this.onMessage({
type: 'audio-chunk',
data: event.data,
});
};
}
public async sendAudioChunk(audioBlob: Blob) {
if (this.dataChannel && this.dataChannel.readyState === 'open') {
this.dataChannel.send(await audioBlob.arrayBuffer());
}
}
async endCall() {
this.mediaRecorder?.stop();
this.dataChannel?.close();
this.peerConnection?.close();
this.peerConnection = null;
}
}

352
package-lock.json generated
View file

@ -127,6 +127,7 @@
"winston": "^3.11.0",
"winston-daily-rotate-file": "^4.7.1",
"youtube-transcript": "^1.2.1",
"wrtc": "^0.4.7",
"ws": "^8.18.0",
"zod": "^3.22.4"
},
@ -24421,8 +24422,7 @@
"node_modules/abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
"dev": true
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
},
"node_modules/abort-controller": {
"version": "3.0.0",
@ -26036,6 +26036,15 @@
"node": ">= 0.12.0"
}
},
"node_modules/code-point-at": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
"integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/cohere-ai": {
"version": "7.9.1",
"resolved": "https://registry.npmjs.org/cohere-ai/-/cohere-ai-7.9.1.tgz",
@ -26286,6 +26295,12 @@
"integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==",
"dev": true
},
"node_modules/console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
"integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
"license": "ISC"
},
"node_modules/constants-browserify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz",
@ -26977,6 +26992,12 @@
"node": ">=0.4.0"
}
},
"node_modules/delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
"integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
"license": "MIT"
},
"node_modules/denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
@ -29196,6 +29217,25 @@
"universalify": "^0.1.0"
}
},
"node_modules/fs-minipass": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz",
"integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==",
"license": "ISC",
"dependencies": {
"minipass": "^2.6.0"
}
},
"node_modules/fs-minipass/node_modules/minipass": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz",
"integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==",
"license": "ISC",
"dependencies": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -29253,6 +29293,70 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gauge": {
"version": "2.7.4",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
"integrity": "sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg==",
"deprecated": "This package is no longer supported.",
"license": "ISC",
"dependencies": {
"aproba": "^1.0.3",
"console-control-strings": "^1.0.0",
"has-unicode": "^2.0.0",
"object-assign": "^4.1.0",
"signal-exit": "^3.0.0",
"string-width": "^1.0.1",
"strip-ansi": "^3.0.1",
"wide-align": "^1.1.0"
}
},
"node_modules/gauge/node_modules/ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
"integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/gauge/node_modules/is-fullwidth-code-point": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
"integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==",
"license": "MIT",
"dependencies": {
"number-is-nan": "^1.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/gauge/node_modules/string-width": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
"integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==",
"license": "MIT",
"dependencies": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
"strip-ansi": "^3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/gauge/node_modules/strip-ansi": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^2.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/gaxios": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.2.0.tgz",
@ -29797,6 +29901,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
"license": "ISC"
},
"node_modules/hash-base": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz",
@ -34877,6 +34987,25 @@
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/minizlib": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz",
"integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==",
"license": "MIT",
"dependencies": {
"minipass": "^2.9.0"
}
},
"node_modules/minizlib/node_modules/minipass": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz",
"integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==",
"license": "ISC",
"dependencies": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
}
},
"node_modules/mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
@ -35023,6 +35152,32 @@
"node": ">=18"
}
},
"node_modules/needle": {
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/needle/-/needle-2.9.1.tgz",
"integrity": "sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==",
"license": "MIT",
"dependencies": {
"debug": "^3.2.6",
"iconv-lite": "^0.4.4",
"sax": "^1.2.4"
},
"bin": {
"needle": "bin/needle"
},
"engines": {
"node": ">= 4.4.x"
}
},
"node_modules/needle/node_modules/debug": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.1"
}
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@ -35115,6 +35270,108 @@
"integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==",
"dev": true
},
"node_modules/node-pre-gyp": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.17.0.tgz",
"integrity": "sha512-abzZt1hmOjkZez29ppg+5gGqdPLUuJeAEwVPtHYEJgx0qzttCbcKFpxrCQn2HYbwCv2c+7JwH4BgEzFkUGpn4A==",
"deprecated": "Please upgrade to @mapbox/node-pre-gyp: the non-scoped node-pre-gyp package is deprecated and only the @mapbox scoped package will recieve updates in the future",
"license": "BSD-3-Clause",
"dependencies": {
"detect-libc": "^1.0.3",
"mkdirp": "^0.5.5",
"needle": "^2.5.2",
"nopt": "^4.0.3",
"npm-packlist": "^1.4.8",
"npmlog": "^4.1.2",
"rc": "^1.2.8",
"rimraf": "^2.7.1",
"semver": "^5.7.1",
"tar": "^4.4.13"
},
"bin": {
"node-pre-gyp": "bin/node-pre-gyp"
}
},
"node_modules/node-pre-gyp/node_modules/detect-libc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
"license": "Apache-2.0",
"bin": {
"detect-libc": "bin/detect-libc.js"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/node-pre-gyp/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/node-pre-gyp/node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"license": "MIT",
"dependencies": {
"minimist": "^1.2.6"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/node-pre-gyp/node_modules/nopt": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz",
"integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==",
"license": "ISC",
"dependencies": {
"abbrev": "1",
"osenv": "^0.1.4"
},
"bin": {
"nopt": "bin/nopt.js"
}
},
"node_modules/node-pre-gyp/node_modules/rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"deprecated": "Rimraf versions prior to v4 are no longer supported",
"license": "ISC",
"dependencies": {
"glob": "^7.1.3"
},
"bin": {
"rimraf": "bin.js"
}
},
"node_modules/node-pre-gyp/node_modules/semver": {
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"license": "ISC",
"bin": {
"semver": "bin/semver"
}
},
"node_modules/node-releases": {
"version": "2.0.18",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
@ -35324,6 +35581,32 @@
"node": ">=0.10.0"
}
},
"node_modules/npm-bundled": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.2.tgz",
"integrity": "sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ==",
"license": "ISC",
"dependencies": {
"npm-normalize-package-bin": "^1.0.1"
}
},
"node_modules/npm-normalize-package-bin": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz",
"integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==",
"license": "ISC"
},
"node_modules/npm-packlist": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz",
"integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==",
"license": "ISC",
"dependencies": {
"ignore-walk": "^3.0.1",
"npm-bundled": "^1.0.1",
"npm-normalize-package-bin": "^1.0.1"
}
},
"node_modules/npm-run-path": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
@ -39756,6 +40039,12 @@
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/sax": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
"integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==",
"license": "ISC"
},
"node_modules/saxes": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
@ -40996,6 +41285,28 @@
"streamx": "^2.15.0"
}
},
"node_modules/tar/node_modules/minipass": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz",
"integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==",
"license": "ISC",
"dependencies": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
}
},
"node_modules/tar/node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"license": "MIT",
"dependencies": {
"minimist": "^1.2.6"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/temp-dir": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz",
@ -43535,6 +43846,43 @@
"node": ">=6"
}
},
"node_modules/wrtc": {
"version": "0.4.7",
"resolved": "https://registry.npmjs.org/wrtc/-/wrtc-0.4.7.tgz",
"integrity": "sha512-P6Hn7VT4lfSH49HxLHcHhDq+aFf/jd9dPY7lDHeFhZ22N3858EKuwm2jmnlPzpsRGEPaoF6XwkcxY5SYnt4f/g==",
"bundleDependencies": [
"node-pre-gyp"
],
"hasInstallScript": true,
"license": "BSD-2-Clause",
"dependencies": {
"node-pre-gyp": "^0.13.0"
},
"engines": {
"node": "^8.11.2 || >=10.0.0"
},
"optionalDependencies": {
"domexception": "^1.0.1"
}
},
"node_modules/wrtc/node_modules/domexception": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz",
"integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==",
"deprecated": "Use your platform's native DOMException instead",
"license": "MIT",
"optional": true,
"dependencies": {
"webidl-conversions": "^4.0.2"
}
},
"node_modules/wrtc/node_modules/webidl-conversions": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
"integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==",
"license": "BSD-2-Clause",
"optional": true
},
"node_modules/ws": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",