Skip to content

Commit

Permalink
feat: sip.js implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
farhat-ha committed Mar 20, 2024
1 parent f4129f8 commit 65cc900
Show file tree
Hide file tree
Showing 10 changed files with 1,185 additions and 191 deletions.
8 changes: 6 additions & 2 deletions packages/js-sip/package.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
{
"name": "@telnyx/sip",
"version": "1.0.0",
"main": "dist/js-sip/src/index.js",
"main": "dist/index.js",
"license": "MIT",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "rm -rf ./dist && mkdir ./dist && tsc",
"release": "release-it"
},
"dependencies": {
"sip.js": "^0.15.4"
"eventemitter3": "^5.0.1",
"sip.js": "^0.21.2"
},
"devDependencies": {
"typescript": "^5.4.2"
}
}
127 changes: 127 additions & 0 deletions packages/js-sip/src/call.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { Session } from "sip.js";
import {
SessionDescriptionHandler,
SessionManager,
} from "sip.js/lib/platform/web";
import { SwEvent } from "./constant";
import { eventBus } from "./events";
import { CallState, ICall } from "./types";

export class Call implements ICall {
private _session: Session;
private _sessionManager: SessionManager;

constructor(
session: Session,
manager: SessionManager,
direction: "inbound" | "outbound"
) {
session.dialog?.id;
this._session = session;
this._sessionManager = manager;
this.direction = direction;
}

public direction: "inbound" | "outbound";
public prevState: CallState;
public state: CallState;

public get id() {
return this._session.id;
}
public get localStream(): MediaStream | null {
return this._sessionManager.getLocalMediaStream(this._session) ?? null;
}

public get remoteStream(): MediaStream | null {
return this._sessionManager.getRemoteMediaStream(this._session) ?? null;
}

public telnyxIDs: {};

public hangup(): Promise<void> {
return this._sessionManager.hangup(this._session);
}

public setState(nextState: CallState) {
this.prevState = this.state;
this.state = nextState;

eventBus.emit(SwEvent.Notification, { call: this, type: "callUpdate" });
}

public answer(): Promise<void> {
return this._sessionManager.answer(this._session).then(() => {
this.setState("answering");
});
}
public deaf(): void {
const handler = this._session.sessionDescriptionHandler;
if (!(handler instanceof SessionDescriptionHandler)) {
return;
}
handler.enableReceiverTracks(false);
}
dtmf(dtmf: any): Promise<void> {
throw new Error("Method not implemented.");
}
hold(): Promise<void> {
return this._sessionManager.hold(this._session);
}
muteAudio(): Promise<void> {
throw new Error("Method not implemented.");
}
muteVideo(): Promise<void> {
throw new Error("Method not implemented.");
}
setAudioInDevice(deviceId: string): Promise<void> {
throw new Error("Method not implemented.");
}
setAudioOutDevice(deviceId: string): Promise<void> {
throw new Error("Method not implemented.");
}
setVideoDevice(deviceId: any): Promise<void> {
throw new Error("Method not implemented.");
}
toggleAudioMute(): void {
throw new Error("Method not implemented.");
}
toggleDeaf(): void {
throw new Error("Method not implemented.");
}
toggleHold(): Promise<void> {
throw new Error("Method not implemented.");
}
toggleVideoMute(): void {
throw new Error("Method not implemented.");
}
undeaf(): void {
const handler = this._session.sessionDescriptionHandler;
if (!(handler instanceof SessionDescriptionHandler)) {
return;
}
handler.enableReceiverTracks(true);
}
unhold(): Promise<void> {
return this._sessionManager.unhold(this._session);
}

unmuteAudio(): void {
const handler = this._session.sessionDescriptionHandler;
if (!(handler instanceof SessionDescriptionHandler)) {
return;
}
handler.enableSenderTracks(true);
}
unmuteVideo(): void {
const handler = this._session.sessionDescriptionHandler;
if (!(handler instanceof SessionDescriptionHandler)) {
return;
}
handler.enableSenderTracks(true);
}

getStats() {
console.log("getStats");
}
}
213 changes: 213 additions & 0 deletions packages/js-sip/src/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import { Session, UserAgent } from "sip.js";
import { IncomingResponse } from "sip.js/lib/core";
import { SessionManager } from "sip.js/lib/platform/web";
import { Call } from "./call";
import { SwEvent } from "./constant";
import { eventBus } from "./events";
import {
AnyFunction,
ICall,
ICallOptions,
IClient,
IClientOptions,
IWebRTCInfo,
IWebRTCSupportedBrowser,
} from "./types";

export class Client implements IClient {
public calls: Record<string, Call>;
public options: IClientOptions;
public mediaConstraints: MediaStreamConstraints;

/**
* @deprecated
*/
public static telnyxStateCall(call: ICall) {
return call;
}
private _sessionManager: SessionManager;

constructor(options: IClientOptions) {
this.calls = {};
this.options = options;
this.mediaConstraints = {
audio: true,
video: false,
};

this._sessionManager = new SessionManager("wss://sipdev.telnyx.com:7443", {
aor: `sip:${options.login}@sip.telnyx.com`,
delegate: {
onRegistered: this._onRegister,
onUnregistered: this._onUnregister,
onServerConnect: this._onSocketOpen,
onServerDisconnect: this._onSocketClose,
onCallAnswered: this._onCallAnswered,
onCallReceived: this._onCallReceived,
onCallHangup: this._onCallHangup,
},
userAgentOptions: {
logLevel: "warn",
authorizationUsername: options.login,
authorizationPassword: options.password,
uri: UserAgent.makeURI(`sip:${options.login}@sip.telnyx.com`),
displayName: options.login,
sessionDescriptionHandlerFactoryOptions: {
iceGatheringTimeout: 1000,
peerConnectionConfiguration: {
bundlePolicy: "max-compat",
sdpSemantics: "unified-plan",
rtcpMuxPolicy: "negotiate",
},
},
},
});
}

private _onCallHangup = (session: Session) => {
const call = this.calls[session.id];
if (!call) {
return;
}
call.setState("hangup");
delete this.calls[call.id];
};
private _onCallReceived = (session: Session) => {
const call = new Call(session, this._sessionManager, "inbound");
this.calls[session.id] = call;
call.setState("ringing");
};
async checkPermissions(audio: boolean, video: boolean): Promise<boolean> {
return true;
}
disableMicrophone(): void {
this.mediaConstraints.audio = false;
}
disableWebcam(): void {
this.mediaConstraints.video = false;
}
enableMicrophone(): void {
this.mediaConstraints.audio = true;
}
enableWebcam(): void {
this.mediaConstraints.video = true;
}

async logout(): Promise<void> {}

private _onCallTrying = (response: IncomingResponse) => {
const id = response.message.callId + response.message.fromTag;
const call = this.calls[id];
if (!call) {
return;
}
call.setState("trying");
};

private _onCallProgress = (response: IncomingResponse) => {
const id = response.message.callId + response.message.fromTag;
const call = this.calls[id];
if (!call) {
return;
}
if (response.message.statusCode === 180) {
call.setState("ringing");
}
};

async newCall(options: ICallOptions): Promise<Call> {
const extraHeaders = options.customHeaders
? options.customHeaders.map(({ name, value }) => `${name}: ${value}`)
: [];

const session = await this._sessionManager.call(
options.destinationNumber,
{
earlyMedia: true,
extraHeaders: extraHeaders,
},
{
requestDelegate: {
onTrying: this._onCallTrying,
onProgress: this._onCallProgress,
},
}
);
const call = new Call(session, this._sessionManager, "outbound");
return call;
}

private _onCallAnswered = (session: Session) => {
const call = this.calls[session.id];
if (!call) {
return;
}
call.setState("active");
};

off(eventName: string, callback: AnyFunction): void {
eventBus.removeListener(eventName, callback);
}
on(eventName: string, callback: AnyFunction): void {
eventBus.addListener(eventName, callback);
}

private _onRegister = () => {
eventBus.emit(SwEvent.Ready);
};
private _onUnregister = () => {
eventBus.emit(SwEvent.Closed);
};

private _onSocketOpen = () => {
eventBus.emit(SwEvent.SocketOpen);
};
private _onSocketClose = () => {
eventBus.emit(SwEvent.SocketClose);
};

public get connected() {
return this._sessionManager.isConnected();
}

public async connect() {
await this._sessionManager.connect();
await this._sessionManager.register();
}

public async disconnect() {
await this._sessionManager.unregister();
await this._sessionManager.disconnect();
}

// ----------

localElement: HTMLMediaElement | null;
remoteElement: HTMLMediaElement | null;
speaker: string | null;

webRTCInfo(): IWebRTCInfo {
return {} as IWebRTCInfo;
}
webRTCSupportedBrowserList(): IWebRTCSupportedBrowser[] {
return [];
}

async getAudioInDevices(): Promise<MediaDeviceInfo[]> {
return [];
}
async getAudioOutDevices(): Promise<MediaDeviceInfo[]> {
return [];
}
getDeviceResolutions(
deviceId: string
): { resolution: string; width: number; height: number }[] {
return [];
}
async getDevices(): Promise<MediaDeviceInfo[]> {
return [];
}
async getVideoDevices(): Promise<MediaDeviceInfo[]> {
return [];
}
}
34 changes: 34 additions & 0 deletions packages/js-sip/src/constant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
export const TELNYX_WS_URL_PROD: string = "wss://sip.telnyx.com";
export const TELNYX_WS_URL_DEV: string = "wss://sipdev.telnyx.com:7443";
export enum SwEvent {
// Socket Events
SocketOpen = "telnyx.socket.open",
SocketClose = "telnyx.socket.close",
SocketError = "telnyx.socket.error",
SocketMessage = "telnyx.socket.message",

// Internal events
SpeedTest = "telnyx.internal.speedtest",

// Global Events
Ready = "telnyx.ready",
Closed = "telnyx.closed",
Error = "telnyx.error",
Notification = "telnyx.notification",

// Blade Events
Messages = "telnyx.messages",
Calls = "telnyx.calls",

// RTC Events
MediaError = "telnyx.rtc.mediaError",
}

const stunServers = {
urls: ["stun:stun.telnyx.com:3478"],
};
const turnServers = {
urls: ["turn:turn.telnyx.com:3478?transport=tcp"],
username: "turnuser",
password: "turnpassword",
};
Loading

0 comments on commit 65cc900

Please sign in to comment.