Add Twilio client wrapper and types

Add voice state manager
This commit is contained in:
2020-02-06 00:11:48 -05:00
parent adf9e17884
commit 827978d18d
7 changed files with 229 additions and 19 deletions

View File

@@ -13,6 +13,7 @@
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-scripts": "3.3.1",
"twilio-client": "^1.9.7",
"typescript": "^3.7.5"
},
"scripts": {

View File

@@ -1,26 +1,24 @@
import React from 'react';
import logo from './logo.svg';
import './App.css';
import useVoice from './Wrappers/voice';
import { voiceToken } from './Wrappers/default-config';
const App = () => {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
const { startDevice, answer, hangup, voiceState } = useVoice();
window.setTimeout(() => startDevice(voiceToken), 5000);
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Device is currently {voiceState.deviceState}.
</p>
</header>
</div>
);
}
export default App;

53
src/Hooks/voice-state.ts Normal file
View File

@@ -0,0 +1,53 @@
import { useState } from 'react'
export type DeviceState = "Ringing" | "Connected" | "Muted" | "Disconnected" | "Offline";
export interface VoiceState
{
deviceState: DeviceState;
callCreatedTime: number;
callEstablishedTime: number;
callPhoneNumber: string;
}
export interface VoiceStateManager
{
onDeviceStateUpdate(state: DeviceState, phoneNumber?: string): void;
voiceState: VoiceState;
}
export default function useVoiceState(): VoiceStateManager
{
const [deviceState, setDeviceState] = useState<DeviceState>("Offline");
const [callCreatedTime, setCallCreatedime] = useState<number>(0);
const [callEstablishedTime, setCallEstablishedTime] = useState<number>(0);
const [callPhoneNumber, setCallPhoneNumber] = useState<string>("");
const onDeviceStateUpdate = (state: DeviceState, phoneNumber?: string) =>
{
if (state === "Ringing" || (state === "Connected" && deviceState === "Disconnected"))
{
setCallCreatedime(Date.now());
}
if (state === "Connected" && (deviceState === "Ringing" || deviceState === "Disconnected"))
{
setCallEstablishedTime(Date.now());
}
setDeviceState(state);
if (phoneNumber) { setCallPhoneNumber(phoneNumber); }
}
return {
onDeviceStateUpdate,
voiceState: {
deviceState,
callCreatedTime,
callEstablishedTime,
callPhoneNumber
}
}
}

View File

@@ -0,0 +1 @@
export const voiceToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6InNjb3BlOmNsaWVudDppbmNvbWluZz9jbGllbnROYW1lPXNwZW5jZXIgc2NvcGU6Y2xpZW50Om91dGdvaW5nP2FwcFNpZD1BUGJmNzMyOTg4YjcxYTMzZDFiOGY1ZjY1MmI1NDU4MmQ0JmNsaWVudE5hbWU9c3BlbmNlciIsImlzcyI6IkFDOWQyYzcxOTc0NmIxZTg5MWU2ZjVlZmQwMmE5Yjg0MzAiLCJleHAiOjE1ODA5NjA0NTAsImlhdCI6MTU4MDk1Njg1MH0.LOlWmq52LjPLM4ONlMdnUybZf6NDk1Xeye-qjVRI_O4";

53
src/Wrappers/voice.ts Normal file
View File

@@ -0,0 +1,53 @@
import { Device, Connection } from "twilio-client";
import useVoiceState, { VoiceState } from "../Hooks/voice-state";
export interface Voice
{
startDevice(token: string): void;
answer(): void;
hangup(): void;
voiceState: VoiceState;
}
export default function useVoice()
{
const state = useVoiceState();
let device: Device;
let connection: Connection;
const startDevice = (token: string) =>
{
device = new Device(token);
device.on("ready", () => state.onDeviceStateUpdate("Disconnected"));
device.on("offline", () => state.onDeviceStateUpdate("Offline"));
device.on("disconnect", () => state.onDeviceStateUpdate("Disconnected"));
device.on("cancel", () => state.onDeviceStateUpdate("Disconnected"));
device.on("incoming", (incomingConnection: Connection) =>
{
state.onDeviceStateUpdate("Ringing", incomingConnection.parameters.From);
incomingConnection.on("accept", () => state.onDeviceStateUpdate("Connected"));
incomingConnection.on("mute", muted => state.onDeviceStateUpdate(muted ? "Muted" : "Connected"));
connection = incomingConnection;
});
}
const answer = () =>
{
connection.accept();
}
const hangup = () =>
{
device.disconnectAll();
}
return {
startDevice,
answer,
hangup,
voiceState: state.voiceState,
}
}

View File

@@ -20,6 +20,7 @@
"jsx": "react"
},
"include": [
"src"
"src",
"types"
]
}

103
types/twilio-client.d.ts vendored Normal file
View File

@@ -0,0 +1,103 @@
declare module "twilio-client"
{
interface SetupParams
{
allowIncomingWhileBusy: boolean;
audioConstraints: boolean;
backoffMaxMs: number;
codecPreferences: ("pcmu" | "opus")[];
closeProtection: boolean;
debug: boolean;
dscp: boolean;
enableRingingState: boolean;
fakeLocalDTMF: boolean;
iceServers: string[];
region: "au1" | "br1" | "ie1" | "de1" | "jp1" | "sg1" | "us1" | "us2" | "gll";
rtcConfiguration: object;
sounds: object;
warnings: boolean;
}
interface ConnectionParameters
{
CallSid: string;
AccountSid: string;
From: string;
To: string;
ApiVersion: string;
}
type ConnectionStatus = "pending" | "connecting" | "ringing" | "open" | "closed";
type ConnectionEvent = "accept" | "disconnect" | "error" | "mute" | "ringing" | "sample" | "volume" | "warning" | "warning-cleared";
interface Connection
{
parameters: ConnectionParameters;
customParameters: Map<string, string>;
options: object;
accept(audioConstraints?: any): void;
reject(): void;
ignore(): void;
disconnect(): void;
mute(mute: boolean): void;
isMuted(): boolean;
getRemoteStream(): any;
sendDigits(digits: string): void;
status(): ConnectionStatus;
on(event: ConnectionEvent, handler: (...args: any[]) => void): void;
on(event: "mute", handler: (muted: boolean, connection: Connection) => void): void;
}
interface MediaDevicesInfo
{
deviceId: string;
groupId: string;
kind: string;
label: string;
}
interface AudioOutputCollection
{
get(): Set<MediaDevicesInfo>;
set(deviceId: string | string[]): void;
test(soundUrl: string): void;
}
type AudioDeviceEvent = "deviceChange" | "inputVolume";
interface DeviceAudio
{
setAudioConstraints(audioConstraints: object): void;
setInputDevice(id: string): void;
unsetAudioContraints(): void;
unsetInputDevice(id: string): void;
on(event: AudioDeviceEvent, callback: (data: any) => void): void;
audioContraits: object;
availableOutputDevices: Map<string, MediaDevicesInfo>;
availableInputDevices: Map<string, MediaDevicesInfo>;
inputDevice: MediaDevicesInfo;
speakerDevices: AudioOutputCollection;
ringtoneDevices: AudioOutputCollection;
isOutputSelectionSupported: boolean;
}
type DeviceStatus = "ready" | "offline" | "busy";
type DeviceEvent = "cancel" | "connect" | "disconnect" | "error" | "incoming" | "offline" | "ready";
class Device
{
constructor(token: string, params?: SetupParams);
public setup(token: string, params?: SetupParams): void;
public connect(params: any, audioConstraints?: any): Connection;
public activeConnection(): Connection;
public destroy(): void;
public disconnectAll(): void;
public status(): DeviceStatus;
public on(event: DeviceEvent, handler: (...args: any[]) => void): void;
public audio: DeviceAudio;
}
exports.Device = Device;
}