@telnyx/react-native-voice-sdk
Version:
Telnyx React Native Voice SDK - A complete WebRTC voice calling solution
598 lines (561 loc) • 14.2 kB
text/typescript
import uuid from 'uuid-random';
import type { CallOptions } from '../call-options';
import { SDK_VERSION } from '../env';
import { TelnyxRTCMethod } from './methods';
type InviteACKMessage = {
id: string;
jsonrpc: '2.0';
result: {
callID: string;
message: 'CALL CREATED';
sessid: string;
};
voice_sdk_id: string;
};
type CreateInviteMessageParams = {
sessionId: string;
sdp: string;
attach: boolean;
callId: string;
callOptions: CallOptions;
};
export function createInviteMessage({
sdp,
sessionId,
attach,
callId,
callOptions,
}: CreateInviteMessageParams) {
return {
id: uuid(),
jsonrpc: '2.0',
method: TelnyxRTCMethod.INVITE,
params: {
sessid: sessionId,
sdp,
'User-Agent': `ReactNative-${SDK_VERSION}`,
dialogParams: {
attach,
callID: callId,
audio: callOptions.audio,
prefetchIceCandidates: callOptions.peerConnectionOptions?.prefetchIceCandidates,
useTrickleIce: callOptions.peerConnectionOptions?.useTrickleIce,
// Keep both keys for signaling compatibility. Android sends top-level
// `trickle`; existing RN consumers read `useTrickleIce` from dialogParams.
trickle: callOptions.peerConnectionOptions?.useTrickleIce,
destination_number: callOptions.destinationNumber,
remote_caller_id_name: callOptions.remoteCallerIdName,
remote_caller_id_number: callOptions.remoteCallerIdNumber,
caller_id_name: callOptions.callerIdName,
caller_id_number: callOptions.callerIdNumber,
custom_headers: callOptions.customHeaders,
clientState: callOptions.clientState,
},
},
};
}
export function isInviteACKMessage(msg: unknown): msg is InviteACKMessage {
if (!msg) {
return false;
}
const temp: Partial<InviteACKMessage> = msg;
return (
Boolean(temp.result?.callID) &&
Boolean(temp.result?.sessid) &&
temp.result?.message === 'CALL CREATED'
);
}
type CandidateMessage = {
jsonrpc: '2.0';
id: string;
method: 'telnyx_rtc.candidate';
params: {
sessid: string;
candidate: string;
sdpMid?: string | null;
sdpMLineIndex?: number | null;
dialogParams: {
callID: string;
};
};
};
type CreateCandidateMessageParams = {
sessionId: string;
callId: string;
candidate: string;
sdpMid?: string | null;
sdpMLineIndex?: number | null;
};
export function createCandidateMessage({
sessionId,
callId,
candidate,
sdpMid,
sdpMLineIndex,
}: CreateCandidateMessageParams): CandidateMessage {
return {
id: uuid(),
jsonrpc: '2.0',
method: TelnyxRTCMethod.CANDIDATE,
params: {
sessid: sessionId,
candidate,
sdpMid,
sdpMLineIndex,
dialogParams: {
callID: callId,
},
},
};
}
export type CandidateEvent = {
id?: string | number;
jsonrpc?: '2.0';
method: 'telnyx_rtc.candidate';
params: {
sessid?: string;
candidate: string;
sdpMid?: string | null;
sdpMLineIndex?: number | null;
dialogParams?: {
callID?: string;
callId?: string;
};
callID?: string;
callId?: string;
};
voice_sdk_id?: string;
};
export function getCallIdFromTrickleParams(params: {
dialogParams?: { callID?: string; callId?: string };
callID?: string;
callId?: string;
}): string | undefined {
return (
params.dialogParams?.callID ??
params.dialogParams?.callId ??
params.callID ??
params.callId
);
}
export function isCandidateEvent(msg: unknown): msg is CandidateEvent {
if (!msg) {
return false;
}
const temp = msg as Partial<CandidateEvent>;
const params = temp.params;
const callId = params ? getCallIdFromTrickleParams(params) : undefined;
return (
temp.method === TelnyxRTCMethod.CANDIDATE &&
typeof params?.candidate === 'string' &&
params.sdpMid != null &&
typeof params.sdpMLineIndex === 'number' &&
Boolean(callId)
);
}
type EndOfCandidatesMessage = {
jsonrpc: '2.0';
id: string;
method: 'telnyx_rtc.endOfCandidates';
params: {
sessid: string;
dialogParams: {
callID: string;
};
};
};
type CreateEndOfCandidatesMessageParams = {
sessionId: string;
callId: string;
};
export function createEndOfCandidatesMessage({
sessionId,
callId,
}: CreateEndOfCandidatesMessageParams): EndOfCandidatesMessage {
return {
id: uuid(),
jsonrpc: '2.0',
method: TelnyxRTCMethod.END_OF_CANDIDATES,
params: {
sessid: sessionId,
dialogParams: {
callID: callId,
},
},
};
}
export type EndOfCandidatesEvent = {
id?: string | number;
jsonrpc?: '2.0';
method: 'telnyx_rtc.endOfCandidates';
params: {
sessid?: string;
dialogParams?: {
callID?: string;
callId?: string;
};
callID?: string;
callId?: string;
};
voice_sdk_id?: string;
};
export function isEndOfCandidatesEvent(msg: unknown): msg is EndOfCandidatesEvent {
if (!msg) {
return false;
}
const temp = msg as Partial<EndOfCandidatesEvent>;
const params = temp.params;
const callId = params ? getCallIdFromTrickleParams(params) : undefined;
return temp.method === TelnyxRTCMethod.END_OF_CANDIDATES && Boolean(callId);
}
export type RingingEvent = {
id: number;
jsonrpc: '2.0';
method: 'telnyx_rtc.ringing';
params: {
callID: string;
callee_id_name: string;
callee_id_number: string;
caller_id_name: string;
caller_id_number: string;
dialogParams: {
custom_headers: { name: string; value: string }[];
};
display_direction: string;
telnyx_leg_id: string;
telnyx_session_id: string;
};
voice_sdk_id: string;
};
export function isRingingEvent(msg: unknown): msg is RingingEvent {
if (!msg) {
return false;
}
const temp: Partial<RingingEvent> = msg;
return (
temp.method === TelnyxRTCMethod.RINGING &&
Boolean(temp.params?.callID) &&
Boolean(temp.params?.telnyx_leg_id) &&
Boolean(temp.params?.telnyx_session_id)
);
}
export type AnswerEvent = {
id: number;
jsonrpc: '2.0';
method: 'telnyx_rtc.answer';
params: {
callID: string;
dialogParams: { custom_headers: { name: string; value: string }[] };
sdp?: string; // Made optional since SDP might come via media event instead
variables: {
'Core-UUID': string;
};
/** The Telnyx call control ID for outbound flows (parked & bridged) */
telnyx_call_control_id?: string;
/** The Telnyx leg ID */
telnyx_leg_id?: string;
/** The Telnyx session ID */
telnyx_session_id?: string;
};
voice_sdk_id: string;
};
export function isAnswerEvent(msg: unknown): msg is AnswerEvent {
if (!msg) {
return false;
}
const temp: Partial<AnswerEvent> = msg;
return temp.method === TelnyxRTCMethod.ANSWER && Boolean(temp.params?.callID);
}
export function isByeEvent(msg: unknown): msg is ByeEvent {
if (!msg) {
return false;
}
const temp: Partial<ByeEvent> = msg;
return (
temp.method === TelnyxRTCMethod.BYE && Boolean(temp.params?.callID) && Boolean(temp.params?.cause)
);
}
export type ByeEvent = {
id: number;
jsonrpc: '2.0';
method: 'telnyx_rtc.bye';
params: {
callID: string;
cause: string;
causeCode: number;
dialogParams: { custom_headers: { name: string; value: string }[] };
sipCode: number;
sipReason: string;
sip_call_id: string;
};
voice_sdk_id: string;
};
type HangupRequest = {
jsonrpc: '2.0';
id: string;
method: 'telnyx_rtc.bye';
params: {
sessid: string;
dialogParams: {
telnyxSessionId: string;
telnyxLegId: string;
callID: string;
custom_headers?: { name: string; value: string }[];
};
cause: string;
causeCode: number;
};
};
type CreateHangupRequestParams = {
sessionId: string;
cause: string;
causeCode: number;
telnyxSessionId: string;
telnyxLegId: string;
callId: string;
customHeaders?: { name: string; value: string }[];
};
export function createHangupRequest({
cause,
causeCode,
sessionId,
callId,
telnyxLegId,
telnyxSessionId,
customHeaders,
}: CreateHangupRequestParams): HangupRequest {
return {
id: uuid(),
jsonrpc: '2.0',
method: TelnyxRTCMethod.BYE,
params: {
sessid: sessionId,
cause: cause,
causeCode: causeCode,
dialogParams: {
telnyxSessionId,
telnyxLegId,
callID: callId,
custom_headers: customHeaders || [],
},
},
};
}
export type InviteEvent = {
id: number;
jsonrpc: '2.0';
method: 'telnyx_rtc.invite';
params: {
callID: string;
callee_id_name: string;
callee_id_number: string;
caller_id_name: string;
caller_id_number: string;
dialogParams: {
custom_headers: { name: string; value: string }[];
};
display_direction: string;
sdp?: string; // Made optional since SDP can come via media event
telnyx_leg_id: string;
telnyx_session_id: string;
};
voice_sdk_id: string;
};
export function isInviteEvent(msg: unknown): msg is InviteEvent {
if (!msg) {
return false;
}
const temp: Partial<InviteEvent> = msg;
return temp.method === TelnyxRTCMethod.INVITE && Boolean(temp.params?.callID);
}
type AnswerMessage = {
jsonrpc: '2.0';
id: string;
method: 'telnyx_rtc.answer';
params: {
sessid: string;
sdp: string;
dialogParams: {
telnyxSessionId: string;
telnyxLegId: string;
callID: string;
custom_headers?: { name: string; value: string }[];
};
'User-Agent': string;
};
};
type CreateAnswerMessageParams = {
sessionId: string;
sdp: string;
dialogParams: object;
callId: string;
telnyxSessionId: string;
telnyxLegId: string;
customHeaders?: { name: string; value: string }[];
};
export function createAnswerMessage({
dialogParams,
sdp,
sessionId,
callId,
telnyxLegId,
telnyxSessionId,
customHeaders,
}: CreateAnswerMessageParams): AnswerMessage {
return {
id: uuid(),
jsonrpc: '2.0',
method: TelnyxRTCMethod.ANSWER,
params: {
sessid: sessionId,
sdp,
dialogParams: {
callID: callId,
telnyxLegId,
telnyxSessionId,
custom_headers: customHeaders || [],
...dialogParams,
},
'User-Agent': `ReactNative-${SDK_VERSION}`,
},
};
}
type ModifyCallRequest = {
jsonrpc: '2.0';
id: string;
method: 'telnyx_rtc.modify';
params: {
sessid: string;
action: 'hold' | 'unhold';
dialogParams: object;
};
};
export function createModifyCallRequest({
sessionId,
action,
callId,
}: {
callId: string;
sessionId: string;
action: 'hold' | 'unhold';
}): ModifyCallRequest {
return {
jsonrpc: '2.0',
id: uuid(),
method: TelnyxRTCMethod.MODIFY,
params: {
sessid: sessionId,
action,
dialogParams: { callId },
},
};
}
type ModifyCallAnswer = {
id: string;
jsonrpc: '2.0';
result: {
action: 'hold' | 'unhold';
callID: string;
holdState: 'held' | 'unheld';
sessid: string;
};
voice_sdk_id: string;
};
export function isModifyCallAnswer(msg: unknown): msg is ModifyCallAnswer {
if (!msg) {
return false;
}
const temp: Partial<ModifyCallAnswer> = msg;
return (
Boolean(temp.result?.callID) &&
Boolean(temp.result?.sessid) &&
(temp.result?.holdState === 'held' || temp.result?.holdState === 'unheld')
);
}
/**
* Verto `telnyx_rtc.info` request used to send DTMF tones.
*
* Mirrors the shape produced by the Android Voice SDK so the gateway sees a
* consistent payload across platforms — including the full `dialogParams`
* object with empty-but-present defaults for fields we don't track.
*/
type DTMFRequest = {
jsonrpc: '2.0';
id: string;
method: 'telnyx_rtc.info';
params: {
sessid: string;
dtmf: string;
dialogParams: {
attach: boolean;
audio: boolean;
callID: string;
caller_id_name: string;
caller_id_number: string;
clientState: string;
custom_headers: { name: string; value: string }[];
destination_number: string;
remote_caller_id_name: string;
screenShare: boolean;
useStereo: boolean;
userVariables: unknown[];
video: boolean;
};
};
};
type CreateDTMFRequestParams = {
digits: string;
sessionId: string;
callId: string;
callerIdName?: string;
callerIdNumber?: string;
clientState?: string;
customHeaders?: { name: string; value: string }[];
destinationNumber?: string;
remoteCallerIdName?: string;
};
export function createDTMFRequest(params: CreateDTMFRequestParams): DTMFRequest {
return {
id: uuid(),
jsonrpc: '2.0',
method: TelnyxRTCMethod.INFO,
params: {
sessid: params.sessionId,
dtmf: params.digits,
dialogParams: {
attach: false,
audio: true,
callID: params.callId,
caller_id_name: params.callerIdName ?? '',
caller_id_number: params.callerIdNumber ?? '',
clientState: params.clientState ?? '',
custom_headers: params.customHeaders ?? [],
destination_number: params.destinationNumber ?? '',
remote_caller_id_name: params.remoteCallerIdName ?? '',
screenShare: false,
useStereo: false,
userVariables: [],
video: false,
},
},
};
}
type DTMFResponse = {
id: string;
jsonrpc: '2.0';
// `message` is legacy — the Telnyx gateway currently acks DTMF with just `sessid`.
// Kept optional so older servers that still emit `{ message: 'SENT', sessid }` still type-check.
result: { message?: string; sessid: string };
voice_sdk_id?: string;
};
export function isDTMFResponse(msg: unknown): msg is DTMFResponse {
if (!msg || typeof msg !== 'object') {
return false;
}
const temp = msg as Partial<DTMFResponse>;
// A DTMF ack is any JSON-RPC success response for a `telnyx_rtc.info` request
// that echoes back the session id. The Android SDK's `Call.dtmf()` doesn't
// validate the response at all (fire-and-forget); this check is just enough
// to distinguish the success ack from a JSON-RPC error frame.
return Boolean(temp.result?.sessid);
}