├── .gitignore ├── README.md ├── dist ├── index.d.ts ├── index.js ├── index.m.js ├── index.modern.mjs └── index.umd.js ├── package-lock.json ├── package.json ├── src └── index.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Check out 2 | [Retell AI Web Call Guide](https://docs.retellai.com/make-calls/web-call) for 3 | how to use this SDK. 4 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from "eventemitter3"; 2 | export interface StartCallConfig { 3 | accessToken: string; 4 | sampleRate?: number; 5 | captureDeviceId?: string; 6 | playbackDeviceId?: string; 7 | emitRawAudioSamples?: boolean; 8 | } 9 | export declare class RetellWebClient extends EventEmitter { 10 | private room; 11 | private connected; 12 | isAgentTalking: boolean; 13 | analyzerComponent: { 14 | calculateVolume: () => number; 15 | analyser: AnalyserNode; 16 | cleanup: () => Promise; 17 | }; 18 | private captureAudioFrame; 19 | constructor(); 20 | startCall(startCallConfig: StartCallConfig): Promise; 21 | startAudioPlayback(): Promise; 22 | stopCall(): void; 23 | mute(): void; 24 | unmute(): void; 25 | private captureAudioSamples; 26 | private handleRoomEvents; 27 | private handleAudioEvents; 28 | private handleDataEvents; 29 | } 30 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | var t=require("eventemitter3"),e=require("livekit-client");function n(t,e){return n=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(t,e){return t.__proto__=e,t},n(t,e)}var o=new TextDecoder;exports.RetellWebClient=/*#__PURE__*/function(t){var i,a;function r(){var e;return(e=t.call(this)||this).room=void 0,e.connected=!1,e.isAgentTalking=!1,e.analyzerComponent=void 0,e.captureAudioFrame=void 0,e}a=t,(i=r).prototype=Object.create(a.prototype),i.prototype.constructor=i,n(i,a);var c=r.prototype;return c.startCall=function(t){try{var n=this,o=function(o,i){try{var a=(n.room=new e.Room({audioCaptureDefaults:{autoGainControl:!0,echoCancellation:!0,noiseSuppression:!0,channelCount:1,deviceId:t.captureDeviceId,sampleRate:t.sampleRate},audioOutput:{deviceId:t.playbackDeviceId}}),n.handleRoomEvents(),n.handleAudioEvents(t),n.handleDataEvents(),Promise.resolve(n.room.connect("wss://retell-ai-4ihahnq7.livekit.cloud",t.accessToken)).then(function(){console.log("connected to room",n.room.name),n.room.localParticipant.setMicrophoneEnabled(!0),n.connected=!0,n.emit("call_started")}))}catch(t){return i(t)}return a&&a.then?a.then(void 0,i):a}(0,function(t){n.emit("error","Error starting call"),console.error("Error starting call",t),n.stopCall()});return Promise.resolve(o&&o.then?o.then(function(){}):void 0)}catch(t){return Promise.reject(t)}},c.startAudioPlayback=function(){try{return Promise.resolve(this.room.startAudio()).then(function(){})}catch(t){return Promise.reject(t)}},c.stopCall=function(){var t;this.connected&&(this.connected=!1,this.emit("call_ended"),null==(t=this.room)||t.disconnect(),this.isAgentTalking=!1,delete this.room,this.analyzerComponent&&(this.analyzerComponent.cleanup(),delete this.analyzerComponent),this.captureAudioFrame&&(window.cancelAnimationFrame(this.captureAudioFrame),delete this.captureAudioFrame))},c.mute=function(){this.connected&&this.room.localParticipant.setMicrophoneEnabled(!1)},c.unmute=function(){this.connected&&this.room.localParticipant.setMicrophoneEnabled(!0)},c.captureAudioSamples=function(){var t=this;if(this.connected&&this.analyzerComponent){var e=new Float32Array(this.analyzerComponent.analyser.fftSize);this.analyzerComponent.analyser.getFloatTimeDomainData(e),this.emit("audio",e),this.captureAudioFrame=window.requestAnimationFrame(function(){return t.captureAudioSamples()})}},c.handleRoomEvents=function(){var t=this;this.room.on(e.RoomEvent.ParticipantDisconnected,function(e){"server"===(null==e?void 0:e.identity)&&setTimeout(function(){t.stopCall()},500)}),this.room.on(e.RoomEvent.Disconnected,function(){t.stopCall()})},c.handleAudioEvents=function(t){var n=this;this.room.on(e.RoomEvent.TrackSubscribed,function(o,i,a){o.kind===e.Track.Kind.Audio&&o instanceof e.RemoteAudioTrack&&("agent_audio"===i.trackName&&(n.emit("call_ready"),t.emitRawAudioSamples&&(n.analyzerComponent=e.createAudioAnalyser(o),n.captureAudioFrame=window.requestAnimationFrame(function(){return n.captureAudioSamples()}))),o.attach())})},c.handleDataEvents=function(){var t=this;this.room.on(e.RoomEvent.DataReceived,function(e,n,i,a){try{if("server"!==(null==n?void 0:n.identity))return;var r=o.decode(e),c=JSON.parse(r);"update"===c.event_type?t.emit("update",c):"metadata"===c.event_type?t.emit("metadata",c):"agent_start_talking"===c.event_type?(t.isAgentTalking=!0,t.emit("agent_start_talking")):"agent_stop_talking"===c.event_type?(t.isAgentTalking=!1,t.emit("agent_stop_talking")):"node_transition"===c.event_type&&t.emit("node_transition",c)}catch(t){console.error("Error decoding data received",t)}})},r}(t.EventEmitter); 2 | -------------------------------------------------------------------------------- /dist/index.m.js: -------------------------------------------------------------------------------- 1 | import{EventEmitter as t}from"eventemitter3";import{Room as e,RoomEvent as n,Track as o,RemoteAudioTrack as i,createAudioAnalyser as a}from"livekit-client";function r(t,e){return r=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(t,e){return t.__proto__=e,t},r(t,e)}var c=new TextDecoder,s=/*#__PURE__*/function(t){var s,l;function u(){var e;return(e=t.call(this)||this).room=void 0,e.connected=!1,e.isAgentTalking=!1,e.analyzerComponent=void 0,e.captureAudioFrame=void 0,e}l=t,(s=u).prototype=Object.create(l.prototype),s.prototype.constructor=s,r(s,l);var d=u.prototype;return d.startCall=function(t){try{var n=this,o=function(o,i){try{var a=(n.room=new e({audioCaptureDefaults:{autoGainControl:!0,echoCancellation:!0,noiseSuppression:!0,channelCount:1,deviceId:t.captureDeviceId,sampleRate:t.sampleRate},audioOutput:{deviceId:t.playbackDeviceId}}),n.handleRoomEvents(),n.handleAudioEvents(t),n.handleDataEvents(),Promise.resolve(n.room.connect("wss://retell-ai-4ihahnq7.livekit.cloud",t.accessToken)).then(function(){console.log("connected to room",n.room.name),n.room.localParticipant.setMicrophoneEnabled(!0),n.connected=!0,n.emit("call_started")}))}catch(t){return i(t)}return a&&a.then?a.then(void 0,i):a}(0,function(t){n.emit("error","Error starting call"),console.error("Error starting call",t),n.stopCall()});return Promise.resolve(o&&o.then?o.then(function(){}):void 0)}catch(t){return Promise.reject(t)}},d.startAudioPlayback=function(){try{return Promise.resolve(this.room.startAudio()).then(function(){})}catch(t){return Promise.reject(t)}},d.stopCall=function(){var t;this.connected&&(this.connected=!1,this.emit("call_ended"),null==(t=this.room)||t.disconnect(),this.isAgentTalking=!1,delete this.room,this.analyzerComponent&&(this.analyzerComponent.cleanup(),delete this.analyzerComponent),this.captureAudioFrame&&(window.cancelAnimationFrame(this.captureAudioFrame),delete this.captureAudioFrame))},d.mute=function(){this.connected&&this.room.localParticipant.setMicrophoneEnabled(!1)},d.unmute=function(){this.connected&&this.room.localParticipant.setMicrophoneEnabled(!0)},d.captureAudioSamples=function(){var t=this;if(this.connected&&this.analyzerComponent){var e=new Float32Array(this.analyzerComponent.analyser.fftSize);this.analyzerComponent.analyser.getFloatTimeDomainData(e),this.emit("audio",e),this.captureAudioFrame=window.requestAnimationFrame(function(){return t.captureAudioSamples()})}},d.handleRoomEvents=function(){var t=this;this.room.on(n.ParticipantDisconnected,function(e){"server"===(null==e?void 0:e.identity)&&setTimeout(function(){t.stopCall()},500)}),this.room.on(n.Disconnected,function(){t.stopCall()})},d.handleAudioEvents=function(t){var e=this;this.room.on(n.TrackSubscribed,function(n,r,c){n.kind===o.Kind.Audio&&n instanceof i&&("agent_audio"===r.trackName&&(e.emit("call_ready"),t.emitRawAudioSamples&&(e.analyzerComponent=a(n),e.captureAudioFrame=window.requestAnimationFrame(function(){return e.captureAudioSamples()}))),n.attach())})},d.handleDataEvents=function(){var t=this;this.room.on(n.DataReceived,function(e,n,o,i){try{if("server"!==(null==n?void 0:n.identity))return;var a=c.decode(e),r=JSON.parse(a);"update"===r.event_type?t.emit("update",r):"metadata"===r.event_type?t.emit("metadata",r):"agent_start_talking"===r.event_type?(t.isAgentTalking=!0,t.emit("agent_start_talking")):"agent_stop_talking"===r.event_type?(t.isAgentTalking=!1,t.emit("agent_stop_talking")):"node_transition"===r.event_type&&t.emit("node_transition",r)}catch(t){console.error("Error decoding data received",t)}})},u}(t);export{s as RetellWebClient}; 2 | -------------------------------------------------------------------------------- /dist/index.modern.mjs: -------------------------------------------------------------------------------- 1 | import{EventEmitter as t}from"eventemitter3";import{Room as e,RoomEvent as i,Track as a,RemoteAudioTrack as n,createAudioAnalyser as o}from"livekit-client";const s=new TextDecoder;class r extends t{constructor(){super(),this.room=void 0,this.connected=!1,this.isAgentTalking=!1,this.analyzerComponent=void 0,this.captureAudioFrame=void 0}async startCall(t){try{this.room=new e({audioCaptureDefaults:{autoGainControl:!0,echoCancellation:!0,noiseSuppression:!0,channelCount:1,deviceId:t.captureDeviceId,sampleRate:t.sampleRate},audioOutput:{deviceId:t.playbackDeviceId}}),this.handleRoomEvents(),this.handleAudioEvents(t),this.handleDataEvents(),await this.room.connect("wss://retell-ai-4ihahnq7.livekit.cloud",t.accessToken),console.log("connected to room",this.room.name),this.room.localParticipant.setMicrophoneEnabled(!0),this.connected=!0,this.emit("call_started")}catch(t){this.emit("error","Error starting call"),console.error("Error starting call",t),this.stopCall()}}async startAudioPlayback(){await this.room.startAudio()}stopCall(){var t;this.connected&&(this.connected=!1,this.emit("call_ended"),null==(t=this.room)||t.disconnect(),this.isAgentTalking=!1,delete this.room,this.analyzerComponent&&(this.analyzerComponent.cleanup(),delete this.analyzerComponent),this.captureAudioFrame&&(window.cancelAnimationFrame(this.captureAudioFrame),delete this.captureAudioFrame))}mute(){this.connected&&this.room.localParticipant.setMicrophoneEnabled(!1)}unmute(){this.connected&&this.room.localParticipant.setMicrophoneEnabled(!0)}captureAudioSamples(){if(!this.connected||!this.analyzerComponent)return;let t=new Float32Array(this.analyzerComponent.analyser.fftSize);this.analyzerComponent.analyser.getFloatTimeDomainData(t),this.emit("audio",t),this.captureAudioFrame=window.requestAnimationFrame(()=>this.captureAudioSamples())}handleRoomEvents(){this.room.on(i.ParticipantDisconnected,t=>{"server"===(null==t?void 0:t.identity)&&setTimeout(()=>{this.stopCall()},500)}),this.room.on(i.Disconnected,()=>{this.stopCall()})}handleAudioEvents(t){this.room.on(i.TrackSubscribed,(e,i,s)=>{e.kind===a.Kind.Audio&&e instanceof n&&("agent_audio"===i.trackName&&(this.emit("call_ready"),t.emitRawAudioSamples&&(this.analyzerComponent=o(e),this.captureAudioFrame=window.requestAnimationFrame(()=>this.captureAudioSamples()))),e.attach())})}handleDataEvents(){this.room.on(i.DataReceived,(t,e,i,a)=>{try{if("server"!==(null==e?void 0:e.identity))return;let i=s.decode(t),a=JSON.parse(i);"update"===a.event_type?this.emit("update",a):"metadata"===a.event_type?this.emit("metadata",a):"agent_start_talking"===a.event_type?(this.isAgentTalking=!0,this.emit("agent_start_talking")):"agent_stop_talking"===a.event_type?(this.isAgentTalking=!1,this.emit("agent_stop_talking")):"node_transition"===a.event_type&&this.emit("node_transition",a)}catch(t){console.error("Error decoding data received",t)}})}}export{r as RetellWebClient}; 2 | -------------------------------------------------------------------------------- /dist/index.umd.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("eventemitter3"),require("livekit-client")):"function"==typeof define&&define.amd?define(["exports","eventemitter3","livekit-client"],t):t((e||self).retellClientJsSdk={},e.eventemitter3,e.livekitClient)}(this,function(e,t,n){function o(e,t){return o=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(e,t){return e.__proto__=t,e},o(e,t)}var i=new TextDecoder;e.RetellWebClient=/*#__PURE__*/function(e){var t,a;function r(){var t;return(t=e.call(this)||this).room=void 0,t.connected=!1,t.isAgentTalking=!1,t.analyzerComponent=void 0,t.captureAudioFrame=void 0,t}a=e,(t=r).prototype=Object.create(a.prototype),t.prototype.constructor=t,o(t,a);var c=r.prototype;return c.startCall=function(e){try{var t=this,o=function(o,i){try{var a=(t.room=new n.Room({audioCaptureDefaults:{autoGainControl:!0,echoCancellation:!0,noiseSuppression:!0,channelCount:1,deviceId:e.captureDeviceId,sampleRate:e.sampleRate},audioOutput:{deviceId:e.playbackDeviceId}}),t.handleRoomEvents(),t.handleAudioEvents(e),t.handleDataEvents(),Promise.resolve(t.room.connect("wss://retell-ai-4ihahnq7.livekit.cloud",e.accessToken)).then(function(){console.log("connected to room",t.room.name),t.room.localParticipant.setMicrophoneEnabled(!0),t.connected=!0,t.emit("call_started")}))}catch(e){return i(e)}return a&&a.then?a.then(void 0,i):a}(0,function(e){t.emit("error","Error starting call"),console.error("Error starting call",e),t.stopCall()});return Promise.resolve(o&&o.then?o.then(function(){}):void 0)}catch(e){return Promise.reject(e)}},c.startAudioPlayback=function(){try{return Promise.resolve(this.room.startAudio()).then(function(){})}catch(e){return Promise.reject(e)}},c.stopCall=function(){var e;this.connected&&(this.connected=!1,this.emit("call_ended"),null==(e=this.room)||e.disconnect(),this.isAgentTalking=!1,delete this.room,this.analyzerComponent&&(this.analyzerComponent.cleanup(),delete this.analyzerComponent),this.captureAudioFrame&&(window.cancelAnimationFrame(this.captureAudioFrame),delete this.captureAudioFrame))},c.mute=function(){this.connected&&this.room.localParticipant.setMicrophoneEnabled(!1)},c.unmute=function(){this.connected&&this.room.localParticipant.setMicrophoneEnabled(!0)},c.captureAudioSamples=function(){var e=this;if(this.connected&&this.analyzerComponent){var t=new Float32Array(this.analyzerComponent.analyser.fftSize);this.analyzerComponent.analyser.getFloatTimeDomainData(t),this.emit("audio",t),this.captureAudioFrame=window.requestAnimationFrame(function(){return e.captureAudioSamples()})}},c.handleRoomEvents=function(){var e=this;this.room.on(n.RoomEvent.ParticipantDisconnected,function(t){"server"===(null==t?void 0:t.identity)&&setTimeout(function(){e.stopCall()},500)}),this.room.on(n.RoomEvent.Disconnected,function(){e.stopCall()})},c.handleAudioEvents=function(e){var t=this;this.room.on(n.RoomEvent.TrackSubscribed,function(o,i,a){o.kind===n.Track.Kind.Audio&&o instanceof n.RemoteAudioTrack&&("agent_audio"===i.trackName&&(t.emit("call_ready"),e.emitRawAudioSamples&&(t.analyzerComponent=n.createAudioAnalyser(o),t.captureAudioFrame=window.requestAnimationFrame(function(){return t.captureAudioSamples()}))),o.attach())})},c.handleDataEvents=function(){var e=this;this.room.on(n.RoomEvent.DataReceived,function(t,n,o,a){try{if("server"!==(null==n?void 0:n.identity))return;var r=i.decode(t),c=JSON.parse(r);"update"===c.event_type?e.emit("update",c):"metadata"===c.event_type?e.emit("metadata",c):"agent_start_talking"===c.event_type?(e.isAgentTalking=!0,e.emit("agent_start_talking")):"agent_stop_talking"===c.event_type?(e.isAgentTalking=!1,e.emit("agent_stop_talking")):"node_transition"===c.event_type&&e.emit("node_transition",c)}catch(e){console.error("Error decoding data received",e)}})},r}(t.EventEmitter)}); 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "retell-client-js-sdk", 3 | "version": "2.0.7", 4 | "description": "", 5 | "main": "dist/index.js", 6 | "module": "dist/index.m.js", 7 | "unpkg": "dist/index.umd.js", 8 | "types": "dist/index.d.ts", 9 | "scripts": { 10 | "build": "rm -rf dist && microbundle --tsconfig tsconfig.json --no-sourcemap" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "devDependencies": { 16 | "microbundle": "^0.15.1" 17 | }, 18 | "dependencies": { 19 | "eventemitter3": "^5.0.1", 20 | "livekit-client": "^2.5.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from "eventemitter3"; 2 | import { 3 | DataPacket_Kind, 4 | RemoteParticipant, 5 | RemoteTrack, 6 | RemoteAudioTrack, 7 | RemoteTrackPublication, 8 | Room, 9 | RoomEvent, 10 | Track, 11 | createAudioAnalyser, 12 | } from "livekit-client"; 13 | 14 | const hostUrl = "wss://retell-ai-4ihahnq7.livekit.cloud"; 15 | const decoder = new TextDecoder(); 16 | 17 | export interface StartCallConfig { 18 | accessToken: string; 19 | sampleRate?: number; 20 | captureDeviceId?: string; // specific sink id for audio capture device 21 | playbackDeviceId?: string; // specific sink id for audio playback device 22 | emitRawAudioSamples?: boolean; // receive raw float32 audio samples (ex. for animation). Default to false. 23 | } 24 | 25 | export class RetellWebClient extends EventEmitter { 26 | // Room related 27 | private room: Room; 28 | private connected: boolean = false; 29 | 30 | // Helper nodes and variables to analyze and animate based on audio 31 | public isAgentTalking: boolean = false; 32 | 33 | // Analyser node for agent audio, only available when 34 | // emitRawAudioSamples is true. Can directly use / modify this for visualization. 35 | // contains a calculateVolume helper method to get the current volume. 36 | public analyzerComponent: { 37 | calculateVolume: () => number; 38 | analyser: AnalyserNode; 39 | cleanup: () => Promise; 40 | }; 41 | private captureAudioFrame: number; 42 | 43 | constructor() { 44 | super(); 45 | } 46 | 47 | public async startCall(startCallConfig: StartCallConfig): Promise { 48 | try { 49 | // Room options 50 | this.room = new Room({ 51 | audioCaptureDefaults: { 52 | autoGainControl: true, 53 | echoCancellation: true, 54 | noiseSuppression: true, 55 | channelCount: 1, // always mono for input 56 | deviceId: startCallConfig.captureDeviceId, 57 | sampleRate: startCallConfig.sampleRate, 58 | }, 59 | audioOutput: { 60 | deviceId: startCallConfig.playbackDeviceId, 61 | }, 62 | }); 63 | 64 | // Register handlers 65 | this.handleRoomEvents(); 66 | this.handleAudioEvents(startCallConfig); 67 | this.handleDataEvents(); 68 | 69 | // Connect to room 70 | await this.room.connect(hostUrl, startCallConfig.accessToken); 71 | console.log("connected to room", this.room.name); 72 | 73 | // Turns microphone track on 74 | this.room.localParticipant.setMicrophoneEnabled(true); 75 | this.connected = true; 76 | this.emit("call_started"); 77 | } catch (err) { 78 | this.emit("error", "Error starting call"); 79 | console.error("Error starting call", err); 80 | // Cleanup 81 | this.stopCall(); 82 | } 83 | } 84 | 85 | // Optional. 86 | // Some browser does not support audio playback without user interaction 87 | // Call this function inside a click/tap handler to start audio playback 88 | public async startAudioPlayback() { 89 | await this.room.startAudio(); 90 | } 91 | 92 | public stopCall(): void { 93 | if (!this.connected) return; 94 | // Cleanup variables and disconnect from room 95 | this.connected = false; 96 | this.emit("call_ended"); 97 | this.room?.disconnect(); 98 | 99 | this.isAgentTalking = false; 100 | delete this.room; 101 | 102 | if (this.analyzerComponent) { 103 | this.analyzerComponent.cleanup(); 104 | delete this.analyzerComponent; 105 | } 106 | 107 | if (this.captureAudioFrame) { 108 | window.cancelAnimationFrame(this.captureAudioFrame); 109 | delete this.captureAudioFrame; 110 | } 111 | } 112 | 113 | public mute(): void { 114 | if (this.connected) this.room.localParticipant.setMicrophoneEnabled(false); 115 | } 116 | 117 | public unmute(): void { 118 | if (this.connected) this.room.localParticipant.setMicrophoneEnabled(true); 119 | } 120 | 121 | private captureAudioSamples() { 122 | if (!this.connected || !this.analyzerComponent) return; 123 | let bufferLength = this.analyzerComponent.analyser.fftSize; 124 | let dataArray = new Float32Array(bufferLength); 125 | this.analyzerComponent.analyser.getFloatTimeDomainData(dataArray); 126 | this.emit("audio", dataArray); 127 | this.captureAudioFrame = window.requestAnimationFrame(() => 128 | this.captureAudioSamples(), 129 | ); 130 | } 131 | 132 | private handleRoomEvents(): void { 133 | this.room.on( 134 | RoomEvent.ParticipantDisconnected, 135 | (participant: RemoteParticipant) => { 136 | if (participant?.identity === "server") { 137 | // Agent hang up, wait 500ms to hangup call to avoid cutoff last bit of audio 138 | setTimeout(() => { 139 | this.stopCall(); 140 | }, 500); 141 | } 142 | }, 143 | ); 144 | 145 | this.room.on(RoomEvent.Disconnected, () => { 146 | // room disconnected 147 | this.stopCall(); 148 | }); 149 | } 150 | 151 | private handleAudioEvents(startCallConfig: StartCallConfig): void { 152 | this.room.on( 153 | RoomEvent.TrackSubscribed, 154 | ( 155 | track: RemoteTrack, 156 | publication: RemoteTrackPublication, 157 | participant: RemoteParticipant, 158 | ) => { 159 | if ( 160 | track.kind === Track.Kind.Audio && 161 | track instanceof RemoteAudioTrack 162 | ) { 163 | if (publication.trackName === "agent_audio") { 164 | // this is where the agent can start playback 165 | // can be used to stop loading animation 166 | this.emit("call_ready"); 167 | 168 | if (startCallConfig.emitRawAudioSamples) { 169 | this.analyzerComponent = createAudioAnalyser(track); 170 | this.captureAudioFrame = window.requestAnimationFrame(() => 171 | this.captureAudioSamples(), 172 | ); 173 | } 174 | } 175 | 176 | // Start playing audio for subscribed tracks 177 | track.attach(); 178 | } 179 | }, 180 | ); 181 | } 182 | 183 | private handleDataEvents(): void { 184 | this.room.on( 185 | RoomEvent.DataReceived, 186 | ( 187 | payload: Uint8Array, 188 | participant?: RemoteParticipant, 189 | kind?: DataPacket_Kind, 190 | topic?: string, 191 | ) => { 192 | try { 193 | // parse server data 194 | if (participant?.identity !== "server") return; 195 | 196 | let decodedData = decoder.decode(payload); 197 | let event = JSON.parse(decodedData); 198 | if (event.event_type === "update") { 199 | this.emit("update", event); 200 | } else if (event.event_type === "metadata") { 201 | this.emit("metadata", event); 202 | } else if (event.event_type === "agent_start_talking") { 203 | this.isAgentTalking = true; 204 | this.emit("agent_start_talking"); 205 | } else if (event.event_type === "agent_stop_talking") { 206 | this.isAgentTalking = false; 207 | this.emit("agent_stop_talking"); 208 | } else if (event.event_type === "node_transition") { 209 | this.emit("node_transition", event); 210 | } 211 | } catch (err) { 212 | console.error("Error decoding data received", err); 213 | } 214 | }, 215 | ); 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "allowSyntheticDefaultImports": true, 7 | "target": "es2017", 8 | "sourceMap": false, 9 | "outDir": "./dist", 10 | "baseUrl": "./", 11 | "incremental": true 12 | }, 13 | "include": ["src/**/*"], 14 | "exclude": ["node_modules", "test", "lib", "**/*spec.ts"] 15 | } --------------------------------------------------------------------------------