rn-verto-typescript
Version:
Verto Free Switch with React Native Type Script
562 lines (478 loc) • 15.6 kB
text/typescript
import VertoRTC from '../webrtc/VertoRTC';
import { generateGUID, ENUM } from './utils';
import BackgroundTimer from 'react-native-background-timer';
import VertinhoClient from './VertoClient';
import CallStateItem from '../models/Call/CallStateItem';
import CallParams from '../models/Call/CallParams';
import VertoRTCMediaHandlers from '../models/VertoRTC/VertoRTCMediaHandlers';
import { CallActions } from '../enums/CallActions.enum';
BackgroundTimer.start();
export default class Call {
private direction: CallStateItem;
private verto: VertinhoClient;
public rtc: VertoRTC;
private params: CallParams;
private mediaHandlers: VertoRTCMediaHandlers;
private answered: boolean;
private lastState: CallStateItem;
private state: CallStateItem;
// private causeCode;
private cause: string;
private gotEarly: boolean;
private gotAnswer: boolean;
constructor(direction: CallStateItem, verto: VertinhoClient, params: CallParams, mediaHandlers?: VertoRTCMediaHandlers) {
this.direction = direction;
this.verto = verto;
this.params = {
callID: generateGUID(),
useVideo: verto.options.useVideo,
useStereo: verto.options.useStereo,
screenShare: false,
useCamera: false,
useMic: verto.options.deviceParams.useMic,
useSpeak: verto.options.deviceParams.useSpeak,
remoteVideo: verto.options.remoteVideo,
remoteAudioId: verto.options.remoteAudioId,
localVideo: verto.options.localVideo,
login: verto.options.login,
videoParams: verto.options.videoParams,
...params,
};
this.mediaHandlers = mediaHandlers;
if (!this.params.screenShare) {
this.params.useCamera = verto.options.deviceParams.useCamera;
}
this.answered = false;
this.lastState = ENUM.state.new;
this.state = this.lastState;
this.verto.calls[this.params.callID] = this;
if (this.direction === ENUM.direction.inbound) {
if (this.params.display_direction === 'outbound') {
this.params.remote_caller_id_name =
this.params.caller_id_name || 'NOBODY';
this.params.remote_caller_id_number =
this.params.caller_id_number || 'UNKNOWN';
} else {
this.params.remote_caller_id_name =
this.params.callee_id_name || 'NOBODY';
this.params.remote_caller_id_number =
this.params.callee_id_number || 'UNKNOWN';
}
} else {
this.params.remote_caller_id_name = 'OUTBOUND CALL';
this.params.remote_caller_id_number = this.params.destination_number;
}
this.bootstrapRealtimeConnection();
if (this.direction === ENUM.direction.inbound) {
if (this.params.attach) {
this.answer();
} else {
// this.ring(); // TODO call ring method
}
}
}
private bootstrapRealtimeConnection(): void {
const callbacks = {
onICESDP: () => {
const { requesting, answering, active } = ENUM.state;
if ([requesting, answering, active].includes(this.state)) {
console.log(
'This ICE SDP should not being received, reload your page!',
);
return;
}
let firstTime = true;
let newsdp = this.rtc.mediaData.SDP.split('\r\n');
newsdp = newsdp.concat([undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]);
newsdp = newsdp.map((line, index) => {
if (line && line.indexOf('a=rtcp:9 IN IP4 0.0.0.0') === 0) {
if (firstTime) {
const audioCandidates = this.rtc.mediaData.candidateList
.filter(val => val.sdpMid === 'audio')
.map(val => `a=${val.candidate}`);
// audioCandidates.unshift(line);
newsdp.splice(
index + 1,
0,
audioCandidates[0],
audioCandidates[1],
audioCandidates[2],
audioCandidates[3],
audioCandidates[4],
audioCandidates[5],
audioCandidates[6],
);
} else {
const videoCandidates = this.rtc.mediaData.candidateList
.filter(val => val.sdpMid === 'video')
.map(val => `a=${val.candidate}`);
// videoCandidates.unshift(line);
newsdp.splice(
index + 1,
0,
videoCandidates[0],
videoCandidates[1],
videoCandidates[2],
videoCandidates[3],
videoCandidates[4],
videoCandidates[5],
videoCandidates[6],
);
}
firstTime = false;
}
return line;
});
const isActivelyCalling = this.rtc.type === 'offer';
const options = { sdp: newsdp.join('\r\n') };
if (isActivelyCalling) {
if (this.state === active) {
this.setState(requesting);
this.broadcastMethod(CallActions.Attach, options);
} else {
this.setState(requesting);
this.broadcastMethod(CallActions.Invite, options);
}
} else {
this.setState(answering);
this.broadcastMethod(
this.params.attach ? CallActions.Attach : CallActions.Answer,
options,
);
}
},
onPeerStreaming: stream => {
this.verto.options.onPeerStreaming(stream);
},
//(reason: any) => PromiseLike<never>
onPeerStreamingError: (reason: any) => Promise.reject((error: any) => {
this.verto.options.onPeerStreamingError(error);
this.hangup({ cause: 'Device or Permission Error' });
return Promise.reject(error);
}),
onNewCall: (call: Call) => {
console.log('onNewCall 1');
if(this.verto && this.verto.callbacks && this.verto.callbacks.onNewCall) {
console.log('onNewCall 2');
this.verto.callbacks.onNewCall(call);
}
}
};
this.rtc = new VertoRTC({
callbacks,
mediaHandlers: this.mediaHandlers,
localVideo: this.params.screenShare ? null : (this.params.localVideo ? true : false),
useVideo: (this.params.remoteVideo ? true : false),
useAudio: (this.params.remoteAudioId ? true : false),
videoParams: this.params.videoParams || {},
audioParams: this.verto.options.audioParams || {},
iceServers: this.verto.options.iceServers,
screenShare: (this.params.screenShare ? true : false),
useCamera: this.params.useCamera,
useMic: this.params.useMic,
useSpeak: this.params.useSpeak,
verto: this.verto,
userData: null
});
}
private broadcastMethod(method: CallActions, options): void {
const { noDialogParams, ...methodParams } = options;
const dialogParams = Object.keys(this.params).reduce(
(accumulator, currentKey) => {
if (
currentKey === 'sdp' &&
method !== CallActions.Invite &&
method !== CallActions.Attach
) {
return accumulator;
}
if (currentKey === 'callID' && noDialogParams === true) {
return accumulator;
}
return { ...accumulator, [currentKey]: this.params[currentKey] };
},
{},
);
const handleMethodResponseFn = success => x =>
this.handleMethodResponse(method, success, x);
this.verto.publish(
method,
{
...methodParams,
dialogParams,
},
handleMethodResponseFn(true),
handleMethodResponseFn(false),
);
}
public setState(state: CallStateItem): boolean {
if (this.state === ENUM.state.ringing) {
// this.stopRinging(); // TODO set a ringer
}
const checkStateChange =
state === ENUM.state.purge || ENUM.states[this.state.name][state.name];
if (this.state === state || !checkStateChange) {
console.log(
`Invalid call state change from ${this.state.name} to ${state.name
}. ${this}`,
);
this.hangup();
return false;
}
this.lastState = this.state;
this.state = state;
this.verto.callbacks.onCallStateChange({
previous: this.lastState,
current: this.state,
});
const speaker = this.params.useSpeak;
const useCustomSpeaker = speaker && speaker !== 'any' && speaker !== 'none';
const isAfterRequesting = this.lastState.val > ENUM.state.requesting.val;
const isBeforeHangup = this.lastState.val < ENUM.state.hangup.val;
switch (this.state) {
case ENUM.state.early:
case ENUM.state.active:
if (useCustomSpeaker) {
console.log('Custom speaker not supported, ignoring.');
}
break;
case ENUM.state.trying:
// setTimeout(() => {
// if (this.state === ENUM.state.trying) {
// console.log(`Turning off after 3s of trying. ${this}`);
// this.setState(ENUM.state.hangup);
// }
// }, 3000);
break;
case ENUM.state.purge:
this.setState(ENUM.state.destroy);
break;
case ENUM.state.hangup:
if (isAfterRequesting && isBeforeHangup) {
this.broadcastMethod(CallActions.Bye, {});
}
this.setState(ENUM.state.destroy);
break;
case ENUM.state.destroy:
delete this.verto.calls[this.params.callID];
if (this.params.screenShare) {
this.rtc.stopPeer();
} else {
this.rtc.stop();
}
break;
default:
break;
}
return true;
}
private handleMethodResponse(method: CallActions, success, response): void {
switch (method) {
case CallActions.Answer:
case CallActions.Attach:
if (success) {
this.setState(ENUM.state.active);
} else {
this.hangup();
}
break;
case CallActions.Invite:
if (success) {
this.setState(ENUM.state.trying);
} else {
this.setState(ENUM.state.destroy);
}
break;
case CallActions.Bye:
this.hangup();
break;
case CallActions.Modify:
if (response.holdState === 'held' && this.state !== ENUM.state.held) {
this.setState(ENUM.state.held);
}
if (
response.holdState === 'active' &&
this.state !== ENUM.state.active
) {
this.setState(ENUM.state.active);
}
break;
default:
break;
}
}
public hangup(params?) {
if (params) {
this.cause = params.cause;
}
if (!this.cause) {
this.cause = 'NORMAL_CLEARING';
}
const isNotNew = this.state.val >= ENUM.state.new.val;
const didntHangupYet = this.state.val < ENUM.state.hangup.val;
if (isNotNew && didntHangupYet) {
this.setState(ENUM.state.hangup);
}
// TODO Check => converted to destroy.val from destroy
const didntDestroyYet = this.state.val < ENUM.state.destroy.val;
if (didntDestroyYet) {
this.setState(ENUM.state.destroy);
}
}
// stopRinging() {
// if (!this.verto.ringer) {
// return;
// }
// this.verto.ringer.getTracks().forEach(ringer => ringer.stop());
// }
// indicateRing() {
// if (!this.verto.ringer) {
// console.log(`Call is ringing, but no ringer set. ${this}`);
// return;
// }
// if (!this.verto.ringer.src && this.verto.options.ringFile) {
// this.verto.ringer.src = this.verto.options.ringFile;
// }
// this.verto.ringer.play();
// BackgroundTimer.setTimeout(() => {
// this.stopRinging();
// if (this.state === ENUM.state.ringing) {
// this.indicateRing();
// } else {
// console.log(`Call stopped ringing, but no ringer set. ${this}`);
// }
// }, this.verto.options.ringSleep);
// }
// ring() {
// this.setState(ENUM.state.ringing);
// this.indicateRing();
// }
sendTouchtone(digit) {
this.broadcastMethod(CallActions.Info, { dtmf: digit });
}
sendRealTimeText({ code, chars }) {
this.broadcastMethod(CallActions.Info, {
txt: { code, chars },
noDialogParams: true,
});
}
transferTo(destination) {
this.broadcastMethod(CallActions.Modify, { action: 'transfer', destination });
}
hold() {
this.broadcastMethod(CallActions.Modify, { action: 'hold' });
}
unhold() {
this.broadcastMethod(CallActions.Modify, { action: 'unhold' });
}
toggleHold() {
this.broadcastMethod(CallActions.Modify, { action: 'toggleHold' });
}
sendMessageTo(to, body) {
this.broadcastMethod(CallActions.Info, {
msg: { from: this.params.login, to, body },
});
}
answer() {
if (this.answered) {
return;
}
this.rtc.createAnswer(this.params);
this.answered = true;
}
handleAnswer(sdp) {
this.gotAnswer = true;
if (this.state.val >= ENUM.state.active.val) {
return;
}
const afterOrAtEarly = this.state.val >= ENUM.state.early.val;
if (afterOrAtEarly) {
this.setState(ENUM.state.active);
return;
}
const shouldDelayForNow = this.gotEarly;
if (shouldDelayForNow) {
return;
}
this.rtc.answer(
sdp,
() => {
this.setState(ENUM.state.active);
},
error => Promise.reject(error).then(value => {
console.log('Error while answering', value);
this.hangup();
return Promise.reject();
})
);
}
getDestinationNumber() {
return this.params.destination_number;
}
getId() {
return this.params.callID;
}
getCallerIdentification({ useCaracterEntities }) {
return [
this.params.remote_caller_id_name,
' ',
useCaracterEntities ? '<' : '<',
this.params.remote_caller_id_number,
useCaracterEntities ? '>' : '>',
].join('');
}
handleInfo(params) {
this.verto.callbacks.onInfo(params);
}
handleDisplay(displayName, displayNumber) {
if (displayName !== undefined) {
this.params.remote_caller_id_name = displayName;
}
if (displayNumber !== undefined) {
this.params.remote_caller_id_number = displayNumber;
}
this.verto.callbacks.onDisplay({
name: displayName,
number: displayNumber,
});
}
handleMedia(sdp) {
if (this.state.val >= ENUM.state.early.val) {
return;
}
this.gotEarly = true;
this.rtc.answer(
sdp,
() => {
this.setState(ENUM.state.early);
if (this.gotAnswer) {
this.setState(ENUM.state.active);
}
},
reason => Promise.reject(reason).then(error => {
console.log('Error on answering early', error);
this.hangup();
return Promise.reject();
}),
);
}
public toString(): string {
const {
callID: id,
destination_number: destination,
caller_id_name: callerName,
caller_id_number: callerNumber,
remote_caller_id_name: calleeName,
remote_caller_id_number: calleeNumber,
} = this.params;
const attributes = [
{ key: 'id', value: id },
{ key: 'destination', value: destination },
{ key: 'from', value: `${callerName} (${callerNumber})` },
{ key: 'to', value: `${calleeName} (${calleeNumber})` },
]
.map(({ key, value }) => `${key}: "${value}"`)
.join(', ');
return `Call<${attributes}>`;
}
}