-added jssip and session manager

-added typescript support
This commit is contained in:
ajukes
2023-07-03 13:33:29 +01:00
parent abf80082e3
commit b1f1d09464
12 changed files with 930 additions and 130 deletions

45
package-lock.json generated
View File

@@ -11,6 +11,7 @@
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"jssip": "3.10.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
@@ -20,11 +21,14 @@
"@rollup/plugin-commonjs": "^25.0.2",
"@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-node-resolve": "^15.1.0",
"@typescript-eslint/eslint-plugin": "^5.25.0",
"@typescript-eslint/parser": "^5.25.0",
"lit": "^2.7.5",
"rollup": "^2.79.1",
"rollup-plugin-lit-css": "^4.0.1",
"rollup-plugin-node-polyfills": "^0.2.1",
"rollup-plugin-terser": "^7.0.2"
"rollup-plugin-terser": "^7.0.2",
"typescript": "~4.9.5"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@@ -4201,6 +4205,14 @@
"@types/node": "*"
}
},
"node_modules/@types/debug": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz",
"integrity": "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==",
"dependencies": {
"@types/ms": "*"
}
},
"node_modules/@types/eslint": {
"version": "8.40.2",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.40.2.tgz",
@@ -4224,6 +4236,11 @@
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz",
"integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA=="
},
"node_modules/@types/events": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",
"integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g=="
},
"node_modules/@types/express": {
"version": "4.17.17",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz",
@@ -4544,6 +4561,11 @@
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
"integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw=="
},
"node_modules/@types/ms": {
"version": "0.7.31",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
"integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA=="
},
"node_modules/@types/node": {
"version": "20.3.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.3.tgz",
@@ -12056,6 +12078,18 @@
"node": ">=0.10.0"
}
},
"node_modules/jssip": {
"version": "3.10.0",
"resolved": "https://registry.npmjs.org/jssip/-/jssip-3.10.0.tgz",
"integrity": "sha512-iJj+bhnNl0S296sUDc2ZjIbAetnelzZ92aWARyW01oKZ0X8t+5aGrYfJdMFliLFm8hxMcnkep3vmSRGe/yRjsA==",
"dependencies": {
"@types/debug": "^4.1.7",
"@types/events": "^3.0.0",
"debug": "^4.3.1",
"events": "^3.3.0",
"sdp-transform": "^2.14.1"
}
},
"node_modules/jsx-ast-utils": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.4.tgz",
@@ -15478,6 +15512,14 @@
"url": "https://opencollective.com/webpack"
}
},
"node_modules/sdp-transform": {
"version": "2.14.1",
"resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.14.1.tgz",
"integrity": "sha512-RjZyX3nVwJyCuTo5tGPx+PZWkDMCg7oOLpSlhjDdZfwUoNqG1mM8nyj31IGHyaPWXhjbP7cdK3qZ2bmkJ1GzRw==",
"bin": {
"sdp-verify": "checker.js"
}
},
"node_modules/select-hose": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
@@ -16673,7 +16715,6 @@
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"

View File

@@ -9,9 +9,12 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
"web-vitals": "^2.1.4",
"jssip": "3.10.0"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^5.25.0",
"@typescript-eslint/parser": "^5.25.0",
"@rollup/plugin-commonjs": "^25.0.2",
"@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-node-resolve": "^15.1.0",
@@ -19,7 +22,8 @@
"rollup-plugin-lit-css": "^4.0.1",
"rollup-plugin-node-polyfills": "^0.2.1",
"rollup-plugin-terser": "^7.0.2",
"lit": "^2.7.5"
"lit": "^2.7.5",
"typescript": "~4.9.5"
},
"scripts": {
"start": "react-scripts start",

View File

@@ -0,0 +1,63 @@
// @ts-ignore
const ringingURL: string = "https://vibe-public.s3.eu-west-1.amazonaws.com/Tone-Telephone-UK-Ring+Tone-Loop.mp3";
const failedURL: string = "https://vibe-public.s3.eu-west-1.amazonaws.com/windows-error-sound-effect-35894.mp3";
const answeredURL: string = "https://vibe-public.s3.eu-west-1.amazonaws.com/lazer-96499.mp3";
export default class SipAudioElements {
#ringing: HTMLAudioElement;
#failed: HTMLAudioElement;
#answer: HTMLAudioElement;
#remote: HTMLAudioElement;
constructor() {
this.#ringing = new Audio();
this.#ringing.loop = true;
this.#ringing.src = ringingURL;
this.#ringing.volume = 0.8;
this.#failed = new Audio();
this.#failed.src = failedURL;
this.#failed.volume = 0.3;
this.#answer = new Audio();
this.#answer.src = answeredURL;
this.#answer.volume = 0.3;
this.#remote = new Audio();
}
playRinging(volume: number | undefined): void {
if (volume) {
this.#ringing.volume = volume;
}
this.#ringing.play();
}
pauseRinging(): void {
this.#ringing.pause();
}
playFailed(volume: number | undefined): void {
this.pauseRinging();
if (volume) {
this.#failed.volume = volume;
}
this.#failed.play();
}
playAnswer(volume: number | undefined): void {
this.pauseRinging();
if (volume) {
this.#answer.volume = volume;
}
this.#answer.play();
}
isRemoteAudioPaused(): boolean {
return this.#remote.paused;
}
playRemote(stream: MediaStream) {
this.#remote.srcObject = stream;
this.#remote.play();
}
}

270
src/lib/SipSession.ts Normal file
View File

@@ -0,0 +1,270 @@
import {
EndEvent,
HoldEvent,
IceCandidateEvent,
PeerConnectionEvent,
ReferEvent,
RTCPeerConnectionDeprecated,
RTCSession
} from "jssip/lib/RTCSession";
import {SipConstants, SipAudioElements, randomId} from "./index";
import {DTMF_TRANSPORT} from "jssip/lib/Constants";
import {IncomingResponse} from "jssip/lib/SIPMessage";
import * as events from "events";
import {C, Grammar} from "jssip";
export default class SipSession extends events.EventEmitter {
#id: string;
#rtcOptions: any;
#audio: SipAudioElements;
#rtcSession: RTCSession;
#active: boolean;
constructor(rtcSession: RTCSession, rtcConfig: RTCConfiguration, audio: SipAudioElements) {
super();
this.setMaxListeners(Infinity);
this.#id = randomId("");
this.#rtcOptions = {
mediaConstraints: {audio: true, video: false},
pcConfig: rtcConfig
};
this.#rtcSession = rtcSession;
this.#audio = audio;
this.#active = false;
this.addListeners();
}
addListeners(): void {
if (this.#rtcSession.connection) {
this.addPeerConnectionListener(this.#rtcSession.connection);
} else {
this.#rtcSession.on('peerconnection', (data: PeerConnectionEvent): void => {
let pc: RTCPeerConnectionDeprecated = data.peerconnection;
this.addPeerConnectionListener(pc);
});
}
this.#rtcSession.on('progress', (): void => {
this.emit(SipConstants.SESSION_RINGING, {
status: SipConstants.SESSION_RINGING,
});
if (this.#audio.isRemoteAudioPaused() && !this.replaces) {
this.#audio.playRinging(undefined);
}
});
this.#rtcSession.on('accepted', () => {
this.emit(SipConstants.SESSION_ANSWERED, {
status: SipConstants.SESSION_ANSWERED
});
this.#audio.playAnswer(undefined);
});
this.#rtcSession.on('failed', (data: EndEvent): void => {
let {originator, cause, message} = data;
let description;
if (message && originator === 'remote' && message instanceof IncomingResponse && message.status_code) {
description = `${message.status_code}`.trim();
}
if (originator === 'local' && cause === C.causes.CANCELED) {
description = 'Cancelled by user';
}
if (originator === 'local' && cause === C.causes.REJECTED) {
description = 'Rejected by user';
}
this.emit(SipConstants.SESSION_FAILED, {
cause: cause,
status: SipConstants.SESSION_FAILED,
originator: originator,
description: description
});
if (originator === 'remote') {
this.#audio.playFailed(undefined);
}
});
this.#rtcSession.on("ended", (data: EndEvent): void => {
const {originator, cause, message} = data;
let description;
if (message && originator === 'remote' && message.hasHeader('Reason')) {
const reason = Grammar.parse(message.getHeader('Reason'), 'Reason');
if (reason) {
description = `${reason.cause}`.trim();
}
}
this.emit(SipConstants.SESSION_ENDED, {
cause: cause,
status: SipConstants.SESSION_ENDED,
originator: originator,
description: description
});
});
this.#rtcSession.on("muted", (): void => {
this.emit(SipConstants.SESSION_MUTED, {
status: "muted"
});
});
this.#rtcSession.on("unmuted", (): void => {
this.emit(SipConstants.SESSION_MUTED, {
status: "unmuted"
});
});
this.#rtcSession.on("hold", (data: HoldEvent): void => {
this.emit(SipConstants.SESSION_HOLD, {
status: "hold",
originator: data.originator
});
});
this.#rtcSession.on("unhold", (data: HoldEvent): void => {
this.emit(SipConstants.SESSION_HOLD, {
status: "unhold",
originator: data.originator
});
});
this.#rtcSession.on("refer", (data: ReferEvent): void => {
let {accept} = data;
accept((rtcSession: RTCSession): void => {
rtcSession.data.replaces = true;
this.emit(SipConstants.SESSION_REFER, {
session: rtcSession,
type: 'refer'
});
}, this.#rtcOptions);
});
this.#rtcSession.on("replaces", (data: ReferEvent): void => {
data.accept((rtcSession: RTCSession): void => {
rtcSession.data.replaces = true;
if (!rtcSession.isEstablished()) {
rtcSession.answer(this.#rtcOptions);
this.emit(SipConstants.SESSION_REPLACES, {
session: rtcSession,
type: 'replaces'
});
}
});
});
this.#rtcSession.on('icecandidate', (evt: IceCandidateEvent): void => {
let type: string[] = evt.candidate.candidate.split(' ');
let candidate: string = type[7];
if (['srflx', 'relay'].indexOf(candidate) > -1) {
evt.ready();
this.emit(SipConstants.SESSION_ICE_READY, {
candidate: candidate,
status: 'ready'
});
}
});
}
addPeerConnectionListener(pc: RTCPeerConnection): void {
pc.addEventListener('addstream', (event: any): void => {
if (this.#rtcSession.direction === 'outgoing') {
this.#audio.pauseRinging();
}
this.#audio.playRemote(event.stream);
this.emit(SipConstants.SESSION_ADD_STREAM, {
direction: this.#rtcSession.direction
})
});
pc.addEventListener('track', (event: RTCPeerConnectionEventMap["track"]): void => {
const stream: MediaStream = new MediaStream([event.track])
if (this.#rtcSession.direction === 'outgoing') {
this.#audio.pauseRinging();
}
this.#audio.playRemote(stream);
this.emit(SipConstants.SESSION_TRACK, {
direction: this.#rtcSession.direction
});
});
};
get rtcSession() {
return this.#rtcSession;
}
get id() {
return this.#id;
}
get user() {
return this.#rtcSession.remote_identity.uri.user;
}
get active() {
return this.#active;
}
get answerTime(): Date {
return this.#rtcSession.start_time;
}
get duration() {
if (!this.answerTime) {
return 0;
}
let now: number = new Date().getUTCMilliseconds();
return Math.floor((now - this.answerTime.getUTCMilliseconds()) / 1000);
}
get replaces() {
return Boolean(this.#rtcSession.data.replaces);
}
setActive(flag: boolean): void {
let wasActive: boolean = this.#active;
this.#active = flag;
if (this.#rtcSession.isEstablished()) {
if (this.replaces) {
return;
}
if (this.#active) {
this.unhold();
} else {
this.hold();
}
}
if (this.#active && !wasActive) {
this.emit(SipConstants.SESSION_ACTIVE);
}
}
answer() {
this.#rtcSession.answer(this.#rtcOptions);
}
terminate(sipCode: number, sipReason: string): void {
this.#rtcSession.terminate({
status_code: sipCode,
reason_phrase: sipReason
});
}
mute(): void {
this.#rtcSession.mute({audio: true, video: true});
}
unmute(): void {
this.#rtcSession.unmute({audio: true, video: true});
}
hold(): void {
this.#rtcSession.hold();
}
unhold(): void {
this.#rtcSession.unhold();
}
sendDtmf(tone: number): void {
this.#rtcSession.sendDTMF(tone, {transportType: DTMF_TRANSPORT.RFC2833});
}
}

View File

@@ -0,0 +1,99 @@
import {
SipSession, SipModel, SipConstants
} from "./index";
export default class SipSessionManager {
#sessions: SipModel.SipSessionState[];
constructor() {
this.#sessions = [];
}
activate(session: SipSession) {
this.#sessions.forEach(state => {
if (session.id !== state.id) {
state.active = false;
session.setActive(false);
}
});
}
updateSession(field: string, session: SipSession, args: any): void {
const state: SipModel.SipSessionState = this.getSessionState(session.id);
if (state) {
switch (field) {
case SipConstants.SESSION_RINGING:
state.status = args.status;
break;
case SipConstants.SESSION_ANSWERED:
state.status = args.status;
break;
case SipConstants.SESSION_FAILED || SipConstants.SESSION_ENDED:
state.status = args.status;
state.endState = {
cause: args.cause,
status: args.status,
originator: args.endState,
description: args.description
}
break;
case SipConstants.SESSION_MUTED:
state.muteStatus = args.status;
break;
case SipConstants.SESSION_HOLD:
state.holdState = {
originator: args.originator,
status: args.status
}
break;
case SipConstants.SESSION_ICE_READY:
state.iceReady = true;
break;
case SipConstants.SESSION_ACTIVE:
state.active = true;
break;
}
}
}
getSessionState(id: string): SipModel.SipSessionState {
const state = this.#sessions.find(value => value.id === id);
if (!state) {
throw new Error("Session not found");
}
return state;
}
getSession(id: string): SipSession {
return this.getSessionState(id).sipSession;
}
newSession(session: SipSession): void {
this.#sessions.push({
id: session.id,
sipSession: session,
startDateTime: new Date(),
active: true,
status: 'init',
});
}
get activeSession(): SipSession {
const state = this.#sessions.find(value => value.active);
if (state) {
return state.sipSession;
}
if (this.#sessions.length === 0) {
throw new Error("No sessions");
}
return this.#sessions[0].sipSession;
}
get count() {
return this.#sessions.length;
}
}

149
src/lib/SipUA.ts Normal file
View File

@@ -0,0 +1,149 @@
import * as events from "events";
import {UA, WebSocketInterface} from "jssip";
import {DisconnectEvent} from "jssip/lib/WebSocketInterface";
import {RTCSession} from "jssip/lib/RTCSession";
import {
ConnectedEvent,
IncomingRTCSessionEvent,
RegisteredEvent,
UAConnectingEvent,
UnRegisteredEvent,
} from "jssip/lib/UA";
import {
SessionManager,
SipAudioElements,
SipSession,
SipConstants,
SipModel,
normalizeNumber,
} from "./index";
export default class SipUA extends events.EventEmitter {
#ua: UA;
#rtcConfig: RTCConfiguration;
#sessionManager: SessionManager;
constructor(client: SipModel.ClientAuth, settings: SipModel.ClientOptions) {
super();
this.#sessionManager = new SessionManager();
this.#rtcConfig = settings.pcConfig;
this.#ua = new UA({
uri: `sip:${client.username}`,
password: client.password,
display_name: client.name,
sockets: [new WebSocketInterface(settings.wsUri)],
register: true,
});
this.#ua.on("connecting", (data: UAConnectingEvent) => this.emit(SipConstants.UA_CONNECTING, {...data, client}))
this.#ua.on("connected", (data: ConnectedEvent) => this.emit(SipConstants.UA_CONNECTED, {...data, client}))
this.#ua.on("disconnected", (data: DisconnectEvent) => this.emit(SipConstants.UA_DISCONNECTED, {
...data,
client
}))
this.#ua.on("registered", (data: RegisteredEvent) => this.emit(SipConstants.UA_REGISTERED, {...data, client}))
this.#ua.on("unregistered", (data: UnRegisteredEvent) => this.emit(SipConstants.UA_UNREGISTERED, {
...data,
client
}))
this.#ua.on('newRTCSession', (data: IncomingRTCSessionEvent) => {
const rtcSession: RTCSession = data.session;
const session: SipSession = new SipSession(rtcSession, this.#rtcConfig, new SipAudioElements());
session.on(SipConstants.SESSION_RINGING, args => this.#sessionManager.updateSession(SipConstants.SESSION_RINGING, session, args));
session.on(SipConstants.SESSION_ANSWERED, args => this.#sessionManager.updateSession(SipConstants.SESSION_ANSWERED, session, args));
session.on(SipConstants.SESSION_FAILED, args => this.#sessionManager.updateSession(SipConstants.SESSION_FAILED, session, args));
session.on(SipConstants.SESSION_ENDED, args => this.#sessionManager.updateSession(SipConstants.SESSION_ENDED, session, args));
session.on(SipConstants.SESSION_MUTED, args => this.#sessionManager.updateSession(SipConstants.SESSION_MUTED, session, args));
session.on(SipConstants.SESSION_HOLD, args => this.#sessionManager.updateSession(SipConstants.SESSION_HOLD, session, args));
session.on(SipConstants.SESSION_ICE_READY, args => this.#sessionManager.updateSession(SipConstants.SESSION_ICE_READY, session, args));
session.on(SipConstants.SESSION_ACTIVE, args => {
this.#sessionManager.updateSession(SipConstants.SESSION_ACTIVE, session, args);
});
session.setActive(true);
this.#sessionManager.newSession(session);
});
}
start(): void {
this.#ua.start();
this.emit(SipConstants.UA_START);
}
stop(): void {
this.#ua.stop();
this.emit(SipConstants.UA_STOP);
}
call(number: string):
void {
let normalizedNumber: string = normalizeNumber(number);
this.#ua.call(normalizedNumber, {
extraHeaders: [`X-Original-Number:${number}`],
mediaConstraints: {audio: true, video: false},
pcConfig: this.#rtcConfig,
});
}
mute(id: string | undefined): void {
if (id) {
this.#sessionManager.getSession(id).mute();
} else {
this.#sessionManager.activeSession.mute();
}
}
unmute(id: string | undefined): void {
if (id) {
this.#sessionManager.getSession(id).unmute();
} else {
this.#sessionManager.activeSession.unmute();
}
}
hold(id: string | undefined): void {
if (id) {
this.#sessionManager.getSession(id).hold();
} else {
this.#sessionManager.activeSession.hold();
}
}
unhold(id: string | undefined): void {
if (id) {
this.#sessionManager.getSession(id).unhold();
} else {
this.#sessionManager.activeSession.unhold();
}
}
dtmf(tone: number, id: string | undefined): void {
if (id) {
this.#sessionManager.getSession(id).sendDtmf(tone);
} else {
this.#sessionManager.activeSession.sendDtmf(tone);
}
}
terminate(sipCode: number, reason: string, id: string | undefined): void {
if (id) {
this.#sessionManager.getSession(id).terminate(sipCode, reason);
} else {
this.#sessionManager.activeSession.terminate(sipCode, reason);
}
}
answer(id: string | undefined): void {
if (id) {
this.#sessionManager.getSession(id).answer();
} else {
this.#sessionManager.activeSession.answer();
}
}
activate(id: string) {
const session: SipSession = this.#sessionManager.getSession(id);
session.setActive(true);
}
}

18
src/lib/index.ts Normal file
View File

@@ -0,0 +1,18 @@
import SipSession from "./SipSession";
import SessionManager from "./SipSessionManager";
import SipAudioElements from "./SipAudioElements";
import SipUA from "./SipUA";
import {normalizeNumber, randomId} from "./sip-utils";
import * as SipModel from "./sip-models";
import * as SipConstants from "./sip-constants";
export {
SipUA,
SipSession,
SessionManager,
SipAudioElements,
randomId,
normalizeNumber,
SipConstants,
SipModel
}

19
src/lib/sip-constants.ts Normal file
View File

@@ -0,0 +1,19 @@
export const SESSION_RINGING :string = "ringing";
export const SESSION_ANSWERED :string = "answered";
export const SESSION_FAILED :string = "failed";
export const SESSION_ENDED :string = "ended";
export const SESSION_MUTED :string = "muted";
export const SESSION_HOLD :string = "hold";
export const SESSION_REFER :string = "hold";
export const SESSION_REPLACES :string = "replaces";
export const SESSION_ICE_READY :string = "ice-ready";
export const SESSION_ADD_STREAM :string = "add-stream";
export const SESSION_TRACK :string = "track";
export const SESSION_ACTIVE :string = "active";
export const UA_CONNECTING :string = "connecting";
export const UA_CONNECTED :string = "connected";
export const UA_DISCONNECTED :string = "disconnected";
export const UA_REGISTERED :string = "registered";
export const UA_UNREGISTERED :string = "unregistered";
export const UA_START :string = "start";
export const UA_STOP :string = "stop";

54
src/lib/sip-models.ts Normal file
View File

@@ -0,0 +1,54 @@
import SipSession from "./SipSession";
interface SipSessionState {
id: string;
sipSession: SipSession;
startDateTime: Date;
active: boolean;
status: string;
muteStatus?: string;
iceReady?: boolean;
endState?: EndState;
holdState?: HoldState;
}
interface EndState {
cause: string,
status: string,
originator: string,
description: string
}
interface HoldState {
status: string,
originator: string
}
interface ClientAuth {
username:string;
password:string;
name: string;
}
interface ClientOptions {
pcConfig:PeerConnectionConfig;
wsUri:string;
}
interface PeerConnectionConfig {
iceServers: IceServer[];
}
interface IceServer {
urls : string[];
}
export {
SipSessionState,
EndState,
HoldState,
ClientAuth,
ClientOptions,
PeerConnectionConfig,
IceServer
}

23
src/lib/sip-utils.ts Normal file
View File

@@ -0,0 +1,23 @@
function normalizeNumber(number: string): string {
if (/^(sips?|tel):/i.test(number)) {
return number;
} else if (/@/i.test(number)) {
return number;
} else {
return number.replace(/[()\-. ]*/g, '');
}
}
function randomId(prefix: string): string {
const id: string = [...Array(16)].map(() => Math.floor(Math.random() * 16).toString(16)).join('');
if (prefix) {
return `${prefix}-${id}`;
} else {
return id;
}
}
export {
normalizeNumber,
randomId
}

View File

@@ -1,142 +1,169 @@
import { LitElement, html, css } from 'lit-element';
import {LitElement, html, css} from 'lit-element';
import {SipUA} from "../lib";
import {SipConstants} from "../lib";
class Phone extends LitElement {
static styles = css`
.number-display {
margin: auto;
width: 200px;
padding: 20px;
border: solid 1px #333;
border-radius: 5px;
text-align: center;
font-size: 20px;
background-color: #f1f1f1;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
}
static styles = css`
.number-display {
margin: auto;
width: 200px;
padding: 20px;
border: solid 1px #333;
border-radius: 5px;
text-align: center;
font-size: 20px;
background-color: #f1f1f1;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
}
.number-input-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
}
.number-input-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
}
.number-label {
width: 20%;
text-align: right;
padding-right: 10px;
font-weight: bold;
}
.number-label {
width: 20%;
text-align: right;
padding-right: 10px;
font-weight: bold;
}
.number-input {
width: 70%;
padding: 5px;
border: none;
border-radius: 3px;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2);
}
.number-input {
width: 70%;
padding: 5px;
border: none;
border-radius: 3px;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2);
}
.dial-pad {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
max-width: 200px;
margin: auto;
margin-top: 20px;
}
.dial-pad {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
max-width: 200px;
margin: auto;
margin-top: 20px;
}
.num-button,
.call-button {
padding: 10px;
background-color: #fafafa;
border: none;
border-radius: 5px;
text-align: center;
font-size: 20px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.num-button,
.call-button {
padding: 10px;
background-color: #fafafa;
border: none;
border-radius: 5px;
text-align: center;
font-size: 20px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.num-button:hover,
.call-button:hover {
background-color: #e1e1e1;
transform: scale(1.05);
}
.num-button:hover,
.call-button:hover {
background-color: #e1e1e1;
transform: scale(1.05);
}
.call-button {
margin-top: 20px;
background-color: green;
color: white;
}
`;
static get properties() {
return {
props: { type: Object },
toNumber: { type: String }
};
}
constructor() {
super();
this.props = {};
this.toNumber = '';
this.sipClient = this._createSipClient();
}
render() {
return html`
<div class="number-display">
<div class="number-input-container">
<label class="number-label">From</label>
<input type="text" class="number-input" placeholder="Enter number here" disabled .value="${this.props.caller}"/>
</div>
<div class="number-input-container">
<label class="number-label">To</label>
<input type="text" class="number-input" placeholder="Enter number here" .value="${this.toNumber}" @input="${this._handleInput}"/>
</div>
</div>
<div class="dial-pad">
${Array(12).fill(0).map((_, index) => {
let displayNum;
if (index === 9) displayNum = "*";
else if (index === 10) displayNum = 0;
else if (index === 11) displayNum = "#";
else displayNum = index + 1; // 1-9
return html`
<button
class="num-button"
@click="${() => this._handleClick(displayNum)}"
>
${displayNum}
</button>
`;
})}
</div>
<button class="call-button" @click="${this._handleCall}">
Call
</button>
.call-button {
margin-top: 20px;
background-color: green;
color: white;
}
`;
}
_createSipClient () {
}
_handleClick(num) {
this.toNumber += num;
}
static get properties() {
return {
props: {type: Object},
toNumber: {type: String}
};
}
_handleCall() {
console.log(`Calling...`);
this.sipClient.call(this.toNumber, '121241231');
}
constructor() {
super();
this.props = {};
this.toNumber = '';
this.sipClient = this._createSipClient();
}
_handleInput(event) {
this.toNumber = event.target.value;
}
render() {
return html`
<div class="number-display">
<div class="number-input-container">
<label class="number-label">From</label>
<input type="text" class="number-input" placeholder="Enter number here" disabled
.value="${this.props.caller}"/>
</div>
<div class="number-input-container">
<label class="number-label">To</label>
<input type="text" class="number-input" placeholder="Enter number here" .value="${this.toNumber}"
@input="${this._handleInput}"/>
</div>
</div>
<div class="dial-pad">
${Array(12).fill(0).map((_, index) => {
let displayNum;
if (index === 9) displayNum = "*";
else if (index === 10) displayNum = 0;
else if (index === 11) displayNum = "#";
else displayNum = index + 1; // 1-9
return html`
<button
class="num-button"
@click="${() => this._handleClick(displayNum)}"
>
${displayNum}
</button>
`;
})}
</div>
<button class="call-button" @click="${this._handleCall}">
Call
</button>
`;
}
_createSipClient() {
const client = {
username: "xxx@jambonz.org",
password: "1234",
name: "Antony Jukes"
}
const settings = {
pcConfig: {
iceServers: [{urls: ['stun:stun.l.google.com:19302']}],
},
wsUri: "wss://foo.jambonz.org:8443",
};
const sipUA = new SipUA(client, settings);
sipUA.on(SipConstants.UA_CONNECTING, args => {
console.log(SipConstants.UA_CONNECTING, args);
});
sipUA.on(SipConstants.UA_REGISTERED, args => {
console.log(SipConstants.UA_REGISTERED, args);
});
sipUA.on(SipConstants.UA_UNREGISTERED, args => {
console.log(SipConstants.UA_UNREGISTERED, args);
});
sipUA.start();
}
_handleClick(num) {
this.toNumber += num;
}
_handleCall() {
console.log(`Calling...`);
this.sipClient.call(this.toNumber, '121241231');
}
_handleInput(event) {
this.toNumber = event.target.value;
}
}
customElements.define('phone-element', Phone);

33
tsconfig.json Normal file
View File

@@ -0,0 +1,33 @@
{
"compilerOptions": {
"target": "es2019",
"module": "es2020",
"lib": ["es2020", "DOM", "DOM.Iterable"],
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"inlineSources": true,
"outDir": "./",
"rootDir": "./src",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"noImplicitThis": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"forceConsistentCasingInFileNames": true,
"noImplicitOverride": true,
"plugins": [
{
"name": "ts-lit-plugin",
"strict": true
}
]
},
"include": ["src/**/*.ts"],
"exclude": []
}