diff --git a/package.json b/package.json
index 2a4eb14..183b2bc 100644
--- a/package.json
+++ b/package.json
@@ -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": {
diff --git a/src/App.tsx b/src/App.tsx
index eb55739..6a3ef60 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -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 (
-
- );
+ const { startDevice, answer, hangup, voiceState } = useVoice();
+
+ window.setTimeout(() => startDevice(voiceToken), 5000);
+
+ return (
+
+ );
}
export default App;
diff --git a/src/Hooks/voice-state.ts b/src/Hooks/voice-state.ts
new file mode 100644
index 0000000..4aa27de
--- /dev/null
+++ b/src/Hooks/voice-state.ts
@@ -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("Offline");
+ const [callCreatedTime, setCallCreatedime] = useState(0);
+ const [callEstablishedTime, setCallEstablishedTime] = useState(0);
+ const [callPhoneNumber, setCallPhoneNumber] = useState("");
+
+ 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
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Wrappers/default-config.ts b/src/Wrappers/default-config.ts
new file mode 100644
index 0000000..6362fae
--- /dev/null
+++ b/src/Wrappers/default-config.ts
@@ -0,0 +1 @@
+export const voiceToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6InNjb3BlOmNsaWVudDppbmNvbWluZz9jbGllbnROYW1lPXNwZW5jZXIgc2NvcGU6Y2xpZW50Om91dGdvaW5nP2FwcFNpZD1BUGJmNzMyOTg4YjcxYTMzZDFiOGY1ZjY1MmI1NDU4MmQ0JmNsaWVudE5hbWU9c3BlbmNlciIsImlzcyI6IkFDOWQyYzcxOTc0NmIxZTg5MWU2ZjVlZmQwMmE5Yjg0MzAiLCJleHAiOjE1ODA5NjA0NTAsImlhdCI6MTU4MDk1Njg1MH0.LOlWmq52LjPLM4ONlMdnUybZf6NDk1Xeye-qjVRI_O4";
diff --git a/src/Wrappers/voice.ts b/src/Wrappers/voice.ts
new file mode 100644
index 0000000..ad7ae97
--- /dev/null
+++ b/src/Wrappers/voice.ts
@@ -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,
+ }
+}
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
index f2850b7..2fa5bec 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -20,6 +20,7 @@
"jsx": "react"
},
"include": [
- "src"
+ "src",
+ "types"
]
}
diff --git a/types/twilio-client.d.ts b/types/twilio-client.d.ts
new file mode 100644
index 0000000..840e9dd
--- /dev/null
+++ b/types/twilio-client.d.ts
@@ -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;
+ 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;
+ 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;
+ availableInputDevices: Map;
+ 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;
+}
\ No newline at end of file