mirror of
https://github.com/jambonz/chrome-extension-dialer.git
synced 2026-05-06 08:47:01 +00:00
Merge pull request #42 from jambonz/fix/noise_issolation
add noise isolation configuration and fix layout issue
This commit is contained in:
Generated
+31
-24
@@ -14,12 +14,12 @@
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@jambonz/client-sdk-web": "^0.1.4",
|
||||
"buffer": "^6.0.3",
|
||||
"dayjs": "^1.11.10",
|
||||
"framer-motion": "^10.16.4",
|
||||
"fuse.js": "^6.6.2",
|
||||
"google-libphonenumber": "^3.2.33",
|
||||
"jssip": "^3.10.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"uuid": "^9.0.1",
|
||||
@@ -4038,6 +4038,32 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@jambonz/client-sdk-core": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/client-sdk-core/-/client-sdk-core-0.1.4.tgz",
|
||||
"integrity": "sha512-D8IomTUJMboCGjqgFrun4OSZJffFP1Z4B6ggXe8HemcZTnownTJSaZpMyTzQyf9AL1C7vd+jb3LHXe1Sb4qtxw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jssip": "^3.10.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@jambonz/client-sdk-web": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/client-sdk-web/-/client-sdk-web-0.1.4.tgz",
|
||||
"integrity": "sha512-vKM/WcXr1VCNg/RFiF+idMgYAmmntzf7c7xoR/q3ANdhB2NQxZvitbmUBFbgevblX+hIPwUeWCXRr9Ncw6zwnA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jambonz/client-sdk-core": "0.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@jest/console": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@jest/console/-/console-27.5.1.tgz",
|
||||
@@ -5638,14 +5664,6 @@
|
||||
"@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.44.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.2.tgz",
|
||||
@@ -5672,11 +5690,6 @@
|
||||
"integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==",
|
||||
"dev": true
|
||||
},
|
||||
"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",
|
||||
@@ -5823,11 +5836,6 @@
|
||||
"integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==",
|
||||
"dev": true
|
||||
},
|
||||
"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": "16.18.53",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.53.tgz",
|
||||
@@ -14938,12 +14946,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jssip": {
|
||||
"version": "3.10.0",
|
||||
"resolved": "https://registry.npmjs.org/jssip/-/jssip-3.10.0.tgz",
|
||||
"integrity": "sha512-iJj+bhnNl0S296sUDc2ZjIbAetnelzZ92aWARyW01oKZ0X8t+5aGrYfJdMFliLFm8hxMcnkep3vmSRGe/yRjsA==",
|
||||
"version": "3.13.6",
|
||||
"resolved": "https://registry.npmjs.org/jssip/-/jssip-3.13.6.tgz",
|
||||
"integrity": "sha512-Bf1ndrSuqpO87/AG56WACR7kKcCvKOzaIQROu7JUMh0qFaGOV4NuR+wsnaXa7f3/d6xhwVczczFyt1ywJmTjPg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/events": "^3.0.0",
|
||||
"debug": "^4.3.1",
|
||||
"events": "^3.3.0",
|
||||
"sdp-transform": "^2.14.1"
|
||||
|
||||
+1
-1
@@ -9,12 +9,12 @@
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@jambonz/client-sdk-web": "^0.1.4",
|
||||
"buffer": "^6.0.3",
|
||||
"dayjs": "^1.11.10",
|
||||
"framer-motion": "^10.16.4",
|
||||
"fuse.js": "^6.6.2",
|
||||
"google-libphonenumber": "^3.2.33",
|
||||
"jssip": "^3.10.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"uuid": "^9.0.1",
|
||||
|
||||
Binary file not shown.
Binary file not shown.
+7
-9
@@ -1,4 +1,5 @@
|
||||
import { ConferenceModes } from "src/api/types";
|
||||
import type { ClientState } from "@jambonz/client-sdk-web";
|
||||
|
||||
export interface LoginCredential {
|
||||
name?: string;
|
||||
@@ -61,6 +62,10 @@ export interface AppSettings {
|
||||
accountSid?: string;
|
||||
apiKey?: string;
|
||||
apiServer?: string;
|
||||
|
||||
noiseIsolationVendor?: string;
|
||||
noiseIsolationLevel?: number;
|
||||
noiseIsolationModel?: string;
|
||||
}
|
||||
|
||||
export interface IAppSettings {
|
||||
@@ -97,12 +102,5 @@ export interface CallHistory {
|
||||
isSaved?: boolean;
|
||||
}
|
||||
|
||||
export type SipClientStatus =
|
||||
| "start"
|
||||
| "stop"
|
||||
| "connecting"
|
||||
| "connected"
|
||||
| "disconnected"
|
||||
| "registered"
|
||||
| "unregistered";
|
||||
export type SipCallDirection = "" | "outgoing" | "incoming";
|
||||
export type SipClientStatus = ClientState;
|
||||
export type SipCallDirection = "" | "outbound" | "inbound";
|
||||
|
||||
@@ -2,8 +2,6 @@ export default class SipAudioElements {
|
||||
#ringing: HTMLAudioElement;
|
||||
#ringBack: HTMLAudioElement;
|
||||
#failed: HTMLAudioElement;
|
||||
#busy: HTMLAudioElement;
|
||||
#remote: HTMLAudioElement;
|
||||
#hungup: HTMLAudioElement;
|
||||
#localHungup: HTMLAudioElement;
|
||||
|
||||
@@ -16,13 +14,10 @@ export default class SipAudioElements {
|
||||
this.#ringBack.volume = 0.8;
|
||||
this.#failed = this.getAudio("audios/call-failed.mp3");
|
||||
this.#failed.volume = 0.3;
|
||||
this.#busy = this.getAudio("audios/us-busy-signal.mp3");
|
||||
this.#busy.volume = 0.3;
|
||||
this.#hungup = this.getAudio("audios/remote-party-hungup-tone.mp3");
|
||||
this.#hungup.volume = 0.3;
|
||||
this.#localHungup = this.getAudio("audios/local-party-hungup-tone.mp3");
|
||||
this.#localHungup.volume = 0.3;
|
||||
this.#remote = new Audio();
|
||||
}
|
||||
|
||||
private getAudio(path: string) {
|
||||
@@ -43,19 +38,13 @@ export default class SipAudioElements {
|
||||
return new Audio(audioURL);
|
||||
}
|
||||
|
||||
playLocalHungup(volume: number | undefined) {
|
||||
playLocalHungup() {
|
||||
this.pauseRingback();
|
||||
this.pauseRinging();
|
||||
if (volume) {
|
||||
this.#localHungup.volume = volume;
|
||||
}
|
||||
this.#localHungup.play();
|
||||
}
|
||||
|
||||
playRinging(volume: number | undefined): void {
|
||||
if (volume) {
|
||||
this.#ringing.volume = volume;
|
||||
}
|
||||
playRinging(): void {
|
||||
this.#ringing.play();
|
||||
}
|
||||
|
||||
@@ -65,10 +54,7 @@ export default class SipAudioElements {
|
||||
}
|
||||
}
|
||||
|
||||
playRingback(volume: number | undefined): void {
|
||||
if (volume) {
|
||||
this.#ringBack.volume = volume;
|
||||
}
|
||||
playRingback(): void {
|
||||
this.#ringBack.play();
|
||||
}
|
||||
|
||||
@@ -78,47 +64,25 @@ export default class SipAudioElements {
|
||||
}
|
||||
}
|
||||
|
||||
playFailed(volume: number | undefined): void {
|
||||
playFailed(): void {
|
||||
this.pauseRinging();
|
||||
this.pauseRingback();
|
||||
if (volume) {
|
||||
this.#failed.volume = volume;
|
||||
}
|
||||
this.#failed.play();
|
||||
}
|
||||
|
||||
playRemotePartyHungup(volume: number | undefined): void {
|
||||
if (volume) {
|
||||
this.#hungup.volume = volume;
|
||||
}
|
||||
playRemotePartyHungup(): void {
|
||||
this.#hungup.play();
|
||||
}
|
||||
|
||||
playAnswer(volume: number | undefined): void {
|
||||
playAnswer(): void {
|
||||
this.pauseRinging();
|
||||
this.pauseRingback();
|
||||
}
|
||||
|
||||
isRemoteAudioPaused(): boolean {
|
||||
return this.#remote.paused;
|
||||
}
|
||||
|
||||
playRemote(stream: MediaStream) {
|
||||
this.#remote.srcObject = stream;
|
||||
this.#remote.play();
|
||||
}
|
||||
|
||||
stopAll() {
|
||||
this.pauseRinging();
|
||||
this.pauseRingback();
|
||||
}
|
||||
|
||||
isPLaying(audio: HTMLAudioElement) {
|
||||
return (
|
||||
audio.currentTime > 0 &&
|
||||
!audio.paused &&
|
||||
!audio.ended &&
|
||||
audio.readyState > audio.HAVE_CURRENT_DATA
|
||||
);
|
||||
this.#ringing.currentTime = 0;
|
||||
this.#ringBack.currentTime = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,311 +0,0 @@
|
||||
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);
|
||||
} else {
|
||||
this.#audio.playRingback(undefined);
|
||||
}
|
||||
});
|
||||
|
||||
this.#rtcSession.on(
|
||||
"accepted",
|
||||
({ response }: { response: IncomingResponse }) => {
|
||||
this.emit(SipConstants.SESSION_ANSWERED, {
|
||||
status: SipConstants.SESSION_ANSWERED,
|
||||
callSid: response?.hasHeader("X-Call-Sid")
|
||||
? response.getHeader("X-Call-Sid")
|
||||
: null,
|
||||
});
|
||||
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);
|
||||
} else {
|
||||
this.#audio.pauseRinging();
|
||||
this.#audio.pauseRingback();
|
||||
}
|
||||
});
|
||||
|
||||
this.#rtcSession.on("ended", (data: EndEvent): void => {
|
||||
const { originator, cause, message } = data;
|
||||
let description;
|
||||
if (originator === "remote") {
|
||||
this.#audio.playRemotePartyHungup(undefined);
|
||||
} else {
|
||||
this.#audio.playLocalHungup(undefined);
|
||||
}
|
||||
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 direction() {
|
||||
return this.#rtcSession.direction;
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
isMuted(): boolean {
|
||||
return this.#rtcSession.isMuted().audio?.valueOf() || false;
|
||||
}
|
||||
|
||||
mute(): void {
|
||||
this.#rtcSession.mute({ audio: true, video: true });
|
||||
}
|
||||
|
||||
unmute(): void {
|
||||
this.#rtcSession.unmute({ audio: true, video: true });
|
||||
}
|
||||
|
||||
isHolded(): boolean {
|
||||
return this.#rtcSession.isOnHold().local.valueOf() || false;
|
||||
}
|
||||
|
||||
hold(): void {
|
||||
this.#rtcSession.hold();
|
||||
}
|
||||
|
||||
unhold(): void {
|
||||
this.#rtcSession.unhold();
|
||||
}
|
||||
|
||||
sendDtmf(tone: number | string): void {
|
||||
this.#rtcSession.sendDTMF(tone, { transportType: DTMF_TRANSPORT.RFC2833 });
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
import { SipSession, SipModel, SipConstants } from "./index";
|
||||
|
||||
export default class SipSessionManager {
|
||||
#sessions: Map<string, SipModel.SipSessionState>;
|
||||
|
||||
constructor() {
|
||||
this.#sessions = new Map();
|
||||
}
|
||||
|
||||
activate(session: SipSession) {
|
||||
this.#sessions.forEach((v, k) => {
|
||||
if (k !== session.id) {
|
||||
v.active = false;
|
||||
session.setActive(false);
|
||||
} else {
|
||||
v.active = true;
|
||||
session.setActive(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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:
|
||||
case SipConstants.SESSION_ENDED:
|
||||
state.status = args.status;
|
||||
state.endState = {
|
||||
cause: args.cause,
|
||||
status: args.status,
|
||||
originator: args.endState,
|
||||
description: args.description,
|
||||
};
|
||||
this.#sessions.delete(session.id);
|
||||
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.get(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.set(session.id, {
|
||||
id: session.id,
|
||||
sipSession: session,
|
||||
startDateTime: new Date(),
|
||||
active: true,
|
||||
status: "init",
|
||||
});
|
||||
}
|
||||
|
||||
get activeSession(): SipSession {
|
||||
if (this.#sessions.size === 0) {
|
||||
throw new Error("No sessions");
|
||||
}
|
||||
|
||||
const state = [...this.#sessions.values()].filter((s) => s.active);
|
||||
if (state.length) {
|
||||
return state[0].sipSession;
|
||||
} else {
|
||||
throw new Error("No Active sessions");
|
||||
}
|
||||
}
|
||||
|
||||
get count() {
|
||||
return this.#sessions.size;
|
||||
}
|
||||
}
|
||||
@@ -1,219 +0,0 @@
|
||||
import * as events from "events";
|
||||
import { UA, WebSocketInterface, debug } 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();
|
||||
debug.enable("JsSIP:*");
|
||||
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: settings.register,
|
||||
register_expires: 600,
|
||||
});
|
||||
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("registrationFailed", (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()
|
||||
);
|
||||
this.#sessionManager.newSession(session);
|
||||
session.on(SipConstants.SESSION_RINGING, (args) =>
|
||||
this.updateSession(SipConstants.SESSION_RINGING, session, args, client)
|
||||
);
|
||||
session.on(SipConstants.SESSION_ANSWERED, (args) =>
|
||||
this.updateSession(SipConstants.SESSION_ANSWERED, session, args, client)
|
||||
);
|
||||
session.on(SipConstants.SESSION_FAILED, (args) =>
|
||||
this.updateSession(SipConstants.SESSION_FAILED, session, args, client)
|
||||
);
|
||||
session.on(SipConstants.SESSION_ENDED, (args) =>
|
||||
this.updateSession(SipConstants.SESSION_ENDED, session, args, client)
|
||||
);
|
||||
session.on(SipConstants.SESSION_MUTED, (args) =>
|
||||
this.updateSession(SipConstants.SESSION_MUTED, session, args, client)
|
||||
);
|
||||
session.on(SipConstants.SESSION_HOLD, (args) =>
|
||||
this.updateSession(SipConstants.SESSION_HOLD, session, args, client)
|
||||
);
|
||||
session.on(SipConstants.SESSION_UNHOLD, (args) =>
|
||||
this.updateSession(SipConstants.SESSION_UNHOLD, session, args, client)
|
||||
);
|
||||
session.on(SipConstants.SESSION_ICE_READY, (args) =>
|
||||
this.updateSession(
|
||||
SipConstants.SESSION_ICE_READY,
|
||||
session,
|
||||
args,
|
||||
client
|
||||
)
|
||||
);
|
||||
session.on(SipConstants.SESSION_ACTIVE, (args) => {
|
||||
this.updateSession(SipConstants.SESSION_ACTIVE, session, args, client);
|
||||
});
|
||||
session.setActive(true);
|
||||
});
|
||||
}
|
||||
|
||||
updateSession(
|
||||
field: string,
|
||||
session: SipSession,
|
||||
args: any,
|
||||
client: SipModel.ClientAuth
|
||||
) {
|
||||
this.emit(field, { ...args, client, session });
|
||||
this.#sessionManager.updateSession(field, session, args);
|
||||
}
|
||||
|
||||
start(): void {
|
||||
this.#ua.start();
|
||||
this.emit(SipConstants.UA_START);
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
this.#ua.stop();
|
||||
this.emit(SipConstants.UA_STOP);
|
||||
}
|
||||
|
||||
call(number: string, customHeaders: string[] = []): void {
|
||||
let normalizedNumber: string = normalizeNumber(number);
|
||||
this.#ua.call(normalizedNumber, {
|
||||
extraHeaders: [`X-Original-Number:${number}`].concat(customHeaders),
|
||||
mediaConstraints: { audio: true, video: false },
|
||||
pcConfig: this.#rtcConfig,
|
||||
});
|
||||
}
|
||||
|
||||
isMuted(id: string | undefined): boolean {
|
||||
if (id) {
|
||||
return this.#sessionManager.getSession(id).isMuted();
|
||||
} else {
|
||||
return this.#sessionManager.activeSession.isMuted();
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
isHolded(id: string | undefined): boolean {
|
||||
if (id) {
|
||||
return this.#sessionManager.getSession(id).isHolded();
|
||||
} else {
|
||||
return this.#sessionManager.activeSession.isHolded();
|
||||
}
|
||||
}
|
||||
|
||||
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 | string, 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);
|
||||
}
|
||||
|
||||
isConnected() {
|
||||
return this.#ua.isConnected();
|
||||
}
|
||||
}
|
||||
+1
-16
@@ -1,18 +1,3 @@
|
||||
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
|
||||
}
|
||||
export { SipAudioElements };
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
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_UNHOLD: string = "unhold";
|
||||
export const SESSION_REFER: string = "refer";
|
||||
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";
|
||||
@@ -1,45 +0,0 @@
|
||||
import SipSession from "./SipSession";
|
||||
|
||||
export interface SipSessionState {
|
||||
id: string;
|
||||
sipSession: SipSession;
|
||||
startDateTime: Date;
|
||||
active: boolean;
|
||||
status: string;
|
||||
muteStatus?: string;
|
||||
iceReady?: boolean;
|
||||
endState?: EndState;
|
||||
holdState?: HoldState;
|
||||
}
|
||||
|
||||
export interface EndState {
|
||||
cause: string;
|
||||
status: string;
|
||||
originator: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface HoldState {
|
||||
status: string;
|
||||
originator: string;
|
||||
}
|
||||
|
||||
export interface ClientAuth {
|
||||
username: string;
|
||||
password: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ClientOptions {
|
||||
pcConfig: PeerConnectionConfig;
|
||||
wsUri: string;
|
||||
register: boolean;
|
||||
}
|
||||
|
||||
export interface PeerConnectionConfig {
|
||||
iceServers: IceServer[];
|
||||
}
|
||||
|
||||
export interface IceServer {
|
||||
urls: string[];
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
function normalizeNumber(number: string): string {
|
||||
if (/^(sips?|tel):/i.test(number)) {
|
||||
return number;
|
||||
} else if (/@/i.test(number)) {
|
||||
return number;
|
||||
} else if (
|
||||
number.startsWith("app-") ||
|
||||
number.startsWith("queue-") ||
|
||||
number.startsWith("conference-")
|
||||
) {
|
||||
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 };
|
||||
+7
-10
@@ -1,4 +1,4 @@
|
||||
import { SipConstants } from "./../lib";
|
||||
import { CallState } from "@jambonz/client-sdk-web";
|
||||
import { deleteWindowIdKey, getWindowIdKey, saveWindowIdKey } from "./storage";
|
||||
import { PhoneNumberFormat, PhoneNumberUtil } from "google-libphonenumber";
|
||||
|
||||
@@ -44,19 +44,16 @@ const initiateNewPhonePopup = (callback: (v: unknown) => void) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const isSipClientRinging = (callStatus: string) => {
|
||||
return callStatus === SipConstants.SESSION_RINGING;
|
||||
export const isSipClientRinging = (callState: CallState | null) => {
|
||||
return callState === CallState.Ringing || callState === CallState.Connecting;
|
||||
};
|
||||
|
||||
export const isSipClientAnswered = (callStatus: string) => {
|
||||
return callStatus === SipConstants.SESSION_ANSWERED;
|
||||
export const isSipClientAnswered = (callState: CallState | null) => {
|
||||
return callState === CallState.Connected;
|
||||
};
|
||||
|
||||
export const isSipClientIdle = (callStatus: string) => {
|
||||
return (
|
||||
callStatus === SipConstants.SESSION_ENDED ||
|
||||
callStatus === SipConstants.SESSION_FAILED
|
||||
);
|
||||
export const isSipClientIdle = (callState: CallState | null) => {
|
||||
return callState === null || callState === CallState.Idle || callState === CallState.Ended;
|
||||
};
|
||||
|
||||
export const normalizeUrl = (input: string): string => {
|
||||
|
||||
+17
-20
@@ -10,11 +10,12 @@ import {
|
||||
import Phone from "./phone";
|
||||
import Settings from "./settings";
|
||||
import { DEFAULT_COLOR_SCHEME } from "src/common/constants";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { getActiveSettings, getCallHistories, getSettings } from "src/storage";
|
||||
|
||||
import CallHistories from "./history";
|
||||
import { CallHistory, IAppSettings, SipClientStatus } from "src/common/types";
|
||||
import { ClientState } from "@jambonz/client-sdk-web";
|
||||
import Footer from "./footer/footer";
|
||||
|
||||
export const WindowApp = () => {
|
||||
@@ -27,33 +28,25 @@ export const WindowApp = () => {
|
||||
const [calledNumber, setCalledNumber] = useState("");
|
||||
const [calledName, setCalledName] = useState("");
|
||||
const [tabIndex, setTabIndex] = useState(0);
|
||||
const [status, setStatus] = useState<SipClientStatus>("stop");
|
||||
const [status, setStatus] = useState<SipClientStatus>(ClientState.Disconnected);
|
||||
const [allSettings, setAllSettings] = useState<IAppSettings[]>([]);
|
||||
const [advancedSettings, setAdvancedSettings] = useState<IAppSettings | null>(
|
||||
null
|
||||
);
|
||||
const [isSwitchingUserStatus, setIsSwitchingUserStatus] = useState(false);
|
||||
const [isOnline, setIsOnline] = useState(false);
|
||||
const phoneSipAschildRef = useRef<{
|
||||
updateGoOffline: (x: string) => void;
|
||||
} | null>(null);
|
||||
const [isUserOffline, setIsUserOffline] = useState(false);
|
||||
|
||||
const handleGoOffline = (s: SipClientStatus) => {
|
||||
if (s === status) {
|
||||
return;
|
||||
}
|
||||
if (phoneSipAschildRef.current) {
|
||||
if (s === "unregistered") {
|
||||
phoneSipAschildRef.current.updateGoOffline("stop");
|
||||
} else {
|
||||
phoneSipAschildRef.current.updateGoOffline("start");
|
||||
}
|
||||
if (s === ClientState.Unregistered) {
|
||||
setIsUserOffline(true);
|
||||
} else {
|
||||
setIsUserOffline(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadSettings = () => {
|
||||
const settings = getSettings();
|
||||
|
||||
const activeSettings = settings.find((el) => el.active);
|
||||
|
||||
setAllSettings(getSettings());
|
||||
@@ -70,7 +63,6 @@ export const WindowApp = () => {
|
||||
title: "Dialer",
|
||||
content: (
|
||||
<Phone
|
||||
ref={phoneSipAschildRef}
|
||||
sipUsername={sipUsername}
|
||||
sipPassword={sipPassword}
|
||||
sipDomain={sipDomain}
|
||||
@@ -84,6 +76,7 @@ export const WindowApp = () => {
|
||||
reload={loadSettings}
|
||||
setIsSwitchingUserStatus={setIsSwitchingUserStatus}
|
||||
setIsOnline={setIsOnline}
|
||||
isUserOffline={isUserOffline}
|
||||
/>
|
||||
),
|
||||
},
|
||||
@@ -117,16 +110,20 @@ export const WindowApp = () => {
|
||||
setCallHistories(getCallHistories(sipUsername));
|
||||
};
|
||||
return (
|
||||
<Grid h="100vh" templateRows="1fr auto">
|
||||
<Box p={2}>
|
||||
<Grid h="100vh" templateRows="1fr auto" overflow="hidden">
|
||||
<Box p={2} minH={0} display="flex" flexDirection="column" overflow="hidden">
|
||||
<Tabs
|
||||
isFitted
|
||||
variant="enclosed"
|
||||
colorScheme={DEFAULT_COLOR_SCHEME}
|
||||
onChange={onTabsChange}
|
||||
index={tabIndex}
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
flex="1"
|
||||
minH={0}
|
||||
>
|
||||
<TabList mb="1em" gap={1}>
|
||||
<TabList mb="1em" gap={1} flexShrink={0}>
|
||||
{tabsSettings.map((s, i) => (
|
||||
<Tab
|
||||
_selected={{ color: "white", bg: "jambonz.500" }}
|
||||
@@ -138,7 +135,7 @@ export const WindowApp = () => {
|
||||
))}
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
<TabPanels flex="1" minH={0} overflowY="auto">
|
||||
{tabsSettings.map((s, i) => (
|
||||
<TabPanel key={i}>{s.content}</TabPanel>
|
||||
))}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { HStack, Image, Text } from "@chakra-ui/react";
|
||||
import jambonz from "src/imgs/jambonz.svg";
|
||||
import { Dispatch, SetStateAction, useEffect, useState } from "react";
|
||||
import { SipClientStatus } from "src/common/types";
|
||||
import { ClientState } from "@jambonz/client-sdk-web";
|
||||
import JambonzSwitch from "src/components/switch";
|
||||
import "./styles.scss";
|
||||
|
||||
@@ -35,9 +36,12 @@ function Footer({
|
||||
const [isConfigured, setIsConfigured] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (status === "registered" || status === "disconnected") {
|
||||
if (
|
||||
status === ClientState.Registered ||
|
||||
status === ClientState.Disconnected
|
||||
) {
|
||||
setIsSwitchingUserStatus(false);
|
||||
setIsOnline(status === "registered");
|
||||
setIsOnline(status === ClientState.Registered);
|
||||
}
|
||||
}, [status, setIsSwitchingUserStatus, setIsOnline]);
|
||||
|
||||
@@ -63,7 +67,9 @@ function Footer({
|
||||
checked={[isOnline, setIsOnline]}
|
||||
onChange={(v) => {
|
||||
setIsSwitchingUserStatus(true);
|
||||
onHandleGoOffline(v ? "registered" : "unregistered");
|
||||
onHandleGoOffline(
|
||||
v ? ClientState.Registered : ClientState.Unregistered
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Text>You are {isOnline ? "online" : "offline"}</Text>
|
||||
|
||||
@@ -35,9 +35,9 @@ export const CallHistoryItem = ({
|
||||
}: CallHistoryItemProbs) => {
|
||||
const [callEnable, setCallEnable] = useState(false);
|
||||
const getDirectionIcon = (direction: SipCallDirection) => {
|
||||
if (direction === "outgoing") {
|
||||
if (direction === "outbound") {
|
||||
return faArrowRightFromBracket;
|
||||
} else if (direction === "incoming") {
|
||||
} else if (direction === "inbound") {
|
||||
return faArrowRightToBracket;
|
||||
} else {
|
||||
return faPhone;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Text, UnorderedList, VStack } from "@chakra-ui/react";
|
||||
import CallHistoryItem from "./call-history-item";
|
||||
import { CallHistory } from "src/common/types";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import Fuse from "fuse.js";
|
||||
|
||||
type RecentsProbs = {
|
||||
@@ -19,35 +19,28 @@ export const Recents = ({
|
||||
onDataChange,
|
||||
onCallNumber,
|
||||
}: RecentsProbs) => {
|
||||
const [callHistories, setCallHistories] = useState<CallHistory[]>(calls);
|
||||
const baseCalls = useMemo(
|
||||
() => (isSaved ? calls.filter((c) => c.isSaved === true) : calls),
|
||||
[calls, isSaved]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const fuseInstance = useMemo(
|
||||
() => new Fuse(baseCalls, { keys: ["number"] }),
|
||||
[baseCalls]
|
||||
);
|
||||
|
||||
const callHistories = useMemo(() => {
|
||||
if (search) {
|
||||
setCallHistories((prev) =>
|
||||
new Fuse(prev, {
|
||||
keys: ["number"],
|
||||
})
|
||||
.search(search)
|
||||
.map(({ item }) => item)
|
||||
);
|
||||
} else {
|
||||
setCallHistories(
|
||||
isSaved ? calls.filter((c) => c.isSaved === true) : calls
|
||||
);
|
||||
return fuseInstance.search(search).map(({ item }) => item);
|
||||
}
|
||||
}, [search]);
|
||||
|
||||
useEffect(() => {
|
||||
setCallHistories(isSaved ? calls.filter((c) => c.isSaved === true) : calls);
|
||||
}, [calls]);
|
||||
return baseCalls;
|
||||
}, [search, fuseInstance, baseCalls]);
|
||||
|
||||
return (
|
||||
<VStack spacing={2}>
|
||||
{callHistories.length > 0 ? (
|
||||
<UnorderedList
|
||||
w="full"
|
||||
maxH="calc(100vh - 21em)"
|
||||
overflowY="auto"
|
||||
spacing={2}
|
||||
mt={2}
|
||||
>
|
||||
|
||||
@@ -2,8 +2,14 @@ export default class DialPadAudioElements {
|
||||
private keySounds: { [key: string]: HTMLAudioElement | undefined } = {};
|
||||
|
||||
constructor() {
|
||||
const arr = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "*", "#"];
|
||||
for (const i of arr) {
|
||||
const keys = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "*", "#"];
|
||||
const fileMap: Record<string, string> = {
|
||||
"*": "star",
|
||||
"#": "hash",
|
||||
};
|
||||
|
||||
for (const key of keys) {
|
||||
const fileName = fileMap[key] || key;
|
||||
let audioURL;
|
||||
|
||||
// Check if we're in a Chrome extension
|
||||
@@ -12,16 +18,14 @@ export default class DialPadAudioElements {
|
||||
chrome.runtime &&
|
||||
chrome.runtime.getURL
|
||||
) {
|
||||
audioURL = chrome.runtime.getURL(
|
||||
`audios/dtmf-${encodeURIComponent(i)}.mp3`
|
||||
);
|
||||
audioURL = chrome.runtime.getURL(`audios/dtmf-${fileName}.mp3`);
|
||||
} else {
|
||||
// We're in a web context, adjust this path as necessary
|
||||
audioURL = `/audios/dtmf-${encodeURIComponent(i)}.mp3`;
|
||||
audioURL = `/audios/dtmf-${fileName}.mp3`;
|
||||
}
|
||||
|
||||
this.keySounds[i] = new Audio(audioURL);
|
||||
const audio = this.keySounds[i];
|
||||
this.keySounds[key] = new Audio(audioURL);
|
||||
const audio = this.keySounds[key];
|
||||
if (audio) {
|
||||
audio.volume = 0.5;
|
||||
}
|
||||
|
||||
@@ -16,12 +16,12 @@ import { updateConferenceParticipantAction } from "src/api";
|
||||
import { ConferenceModes } from "src/api/types";
|
||||
import { DEFAULT_TOAST_DURATION } from "src/common/constants";
|
||||
import OutlineBox from "src/components/outline-box";
|
||||
import { SipConstants } from "src/lib";
|
||||
import {
|
||||
deleteConferenceSettings,
|
||||
getConferenceSettings,
|
||||
saveConferenceSettings,
|
||||
} from "src/storage";
|
||||
import { CallState } from "@jambonz/client-sdk-web";
|
||||
|
||||
type JoinConferenceProbs = {
|
||||
conferenceId?: string;
|
||||
@@ -60,9 +60,21 @@ export const JoinConference = ({
|
||||
);
|
||||
const [participantState, setParticipantState] = useState("Join as");
|
||||
|
||||
const updateSettings = (updates: Partial<{ mode: ConferenceModes; speakOnlyTo: string; tags: string }>) => {
|
||||
const next = {
|
||||
mode: updates.mode ?? mode,
|
||||
speakOnlyTo: updates.speakOnlyTo ?? speakOnlyTo,
|
||||
tags: updates.tags ?? tags,
|
||||
};
|
||||
if (updates.mode !== undefined) setMode(updates.mode);
|
||||
if (updates.speakOnlyTo !== undefined) setSpeakOnlyTo(updates.speakOnlyTo);
|
||||
if (updates.tags !== undefined) setTags(updates.tags);
|
||||
saveConferenceSettings(next);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
switch (callStatus) {
|
||||
case SipConstants.SESSION_ANSWERED:
|
||||
case CallState.Connected:
|
||||
setAppTitle("Conference");
|
||||
setSubmitTitle("Update");
|
||||
setCancelTitle("Hangup");
|
||||
@@ -70,8 +82,8 @@ export const JoinConference = ({
|
||||
setIsLoading(false);
|
||||
configureConferenceSession();
|
||||
break;
|
||||
case SipConstants.SESSION_ENDED:
|
||||
case SipConstants.SESSION_FAILED:
|
||||
case CallState.Ended:
|
||||
case CallState.Idle:
|
||||
setIsLoading(false);
|
||||
deleteConferenceSettings();
|
||||
break;
|
||||
@@ -80,7 +92,7 @@ export const JoinConference = ({
|
||||
|
||||
const handleSubmit = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (callStatus !== SipConstants.SESSION_ANSWERED) {
|
||||
if (callStatus !== CallState.Connected) {
|
||||
call(conferenceName);
|
||||
if (!callSid) {
|
||||
setIsLoading(true);
|
||||
@@ -145,7 +157,7 @@ export const JoinConference = ({
|
||||
</Text>
|
||||
{callDuration > 0 && (
|
||||
<Text fontSize="15px">
|
||||
{new Date(callDuration * 1000).toISOString().substr(11, 8)}
|
||||
{new Date(callDuration * 1000).toISOString().substring(11, 19)}
|
||||
</Text>
|
||||
)}
|
||||
<FormControl id="conference_name">
|
||||
@@ -163,12 +175,7 @@ export const JoinConference = ({
|
||||
<OutlineBox title={participantState}>
|
||||
<RadioGroup
|
||||
onChange={(e) => {
|
||||
setMode(e as ConferenceModes);
|
||||
saveConferenceSettings({
|
||||
mode: e as ConferenceModes,
|
||||
speakOnlyTo,
|
||||
tags,
|
||||
});
|
||||
updateSettings({ mode: e as ConferenceModes });
|
||||
}}
|
||||
value={mode}
|
||||
colorScheme="jambonz"
|
||||
@@ -189,12 +196,7 @@ export const JoinConference = ({
|
||||
placeholder="tag"
|
||||
value={speakOnlyTo}
|
||||
onChange={(e) => {
|
||||
setSpeakOnlyTo(e.target.value);
|
||||
saveConferenceSettings({
|
||||
mode,
|
||||
speakOnlyTo: e.target.value,
|
||||
tags,
|
||||
});
|
||||
updateSettings({ speakOnlyTo: e.target.value });
|
||||
}}
|
||||
disabled={mode !== "coach"}
|
||||
required={mode === "coach"}
|
||||
@@ -208,12 +210,7 @@ export const JoinConference = ({
|
||||
placeholder="tag"
|
||||
value={tags}
|
||||
onChange={(e) => {
|
||||
setTags(e.target.value);
|
||||
saveConferenceSettings({
|
||||
mode,
|
||||
speakOnlyTo,
|
||||
tags: e.target.value,
|
||||
});
|
||||
updateSettings({ tags: e.target.value });
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Box, Button, HStack, VStack } from "@chakra-ui/react";
|
||||
import DialPadAudioElements from "./DialPadSoundElement";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
|
||||
type DialPadProbs = {
|
||||
handleDigitPress: (digit: string, fromKeyboard: boolean) => void;
|
||||
@@ -8,17 +8,24 @@ type DialPadProbs = {
|
||||
|
||||
const keySounds = new DialPadAudioElements();
|
||||
|
||||
const buttons = [
|
||||
["1", "2", "3"],
|
||||
["4", "5", "6"],
|
||||
["7", "8", "9"],
|
||||
["*", "0", "#"],
|
||||
];
|
||||
|
||||
export const DialPad = ({ handleDigitPress }: DialPadProbs) => {
|
||||
const selfRef = useRef<HTMLDivElement | null>(null);
|
||||
const isVisibleRef = useRef(false);
|
||||
const buttons = [
|
||||
["1", "2", "3"],
|
||||
["4", "5", "6"],
|
||||
["7", "8", "9"],
|
||||
["*", "0", "#"],
|
||||
];
|
||||
const handleDigitPressRef = useRef(handleDigitPress);
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
// Keep ref in sync so the keydown listener always uses the latest callback
|
||||
useEffect(() => {
|
||||
handleDigitPressRef.current = handleDigitPress;
|
||||
}, [handleDigitPress]);
|
||||
|
||||
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
||||
if (
|
||||
["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "*", "#"].includes(
|
||||
e.key
|
||||
@@ -26,10 +33,10 @@ export const DialPad = ({ handleDigitPress }: DialPadProbs) => {
|
||||
) {
|
||||
if (isVisibleRef.current) {
|
||||
keySounds?.playKeyTone(e.key);
|
||||
handleDigitPress(e.key, true);
|
||||
handleDigitPressRef.current(e.key, true);
|
||||
}
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
@@ -50,7 +57,7 @@ export const DialPad = ({ handleDigitPress }: DialPadProbs) => {
|
||||
observer.unobserve(selfRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
}, [handleKeyDown]);
|
||||
|
||||
return (
|
||||
<Box p={2} w="full" h="280px" ref={selfRef}>
|
||||
|
||||
+793
-790
File diff suppressed because it is too large
Load Diff
@@ -48,6 +48,9 @@ function AccountForm({
|
||||
const [accountSid, setAccountSid] = useState<string | undefined>("");
|
||||
const [isCredentialOk, setIsCredentialOk] = useState<boolean>(false);
|
||||
const [isAdvancedMode, setIsAdvancedMode] = useState<boolean>(false);
|
||||
const [noiseIsolationVendor, setNoiseIsolationVendor] = useState("krisp");
|
||||
const [noiseIsolationLevel, setNoiseIsolationLevel] = useState("0.3");
|
||||
const [noiseIsolationModel, setNoiseIsolationModel] = useState("");
|
||||
const toast = useToast();
|
||||
|
||||
useEffect(
|
||||
@@ -62,6 +65,13 @@ function AccountForm({
|
||||
setAccountSid(formData.decoded.accountSid);
|
||||
setApiKey(formData.decoded.apiKey || "");
|
||||
setApiServer(formData.decoded.apiServer);
|
||||
setNoiseIsolationVendor(formData.decoded.noiseIsolationVendor || "krisp");
|
||||
setNoiseIsolationLevel(
|
||||
formData.decoded.noiseIsolationLevel !== undefined
|
||||
? String(formData.decoded.noiseIsolationLevel)
|
||||
: "0.3"
|
||||
);
|
||||
setNoiseIsolationModel(formData.decoded.noiseIsolationModel || "");
|
||||
|
||||
if (
|
||||
formData.decoded.accountSid ||
|
||||
@@ -101,6 +111,9 @@ function AccountForm({
|
||||
accountSid,
|
||||
apiKey,
|
||||
apiServer: apiServer ? normalizeUrl(apiServer) : "",
|
||||
noiseIsolationVendor: noiseIsolationVendor || "krisp",
|
||||
noiseIsolationLevel: parseFloat(noiseIsolationLevel) || 0.3,
|
||||
noiseIsolationModel: noiseIsolationModel || undefined,
|
||||
};
|
||||
|
||||
formData ? editSettings(settings, formData.id) : saveSettings(settings);
|
||||
@@ -146,6 +159,9 @@ function AccountForm({
|
||||
setApiServer("");
|
||||
setAccountSid("");
|
||||
setIsAdvancedMode(false);
|
||||
setNoiseIsolationVendor("krisp");
|
||||
setNoiseIsolationLevel("0.3");
|
||||
setNoiseIsolationModel("");
|
||||
|
||||
if (formData) {
|
||||
handleClose && handleClose();
|
||||
@@ -282,6 +298,40 @@ function AccountForm({
|
||||
)}
|
||||
</HStack>
|
||||
)}
|
||||
|
||||
<Text fontSize="13px" fontWeight="bold" mt={2}>
|
||||
Noise Isolation
|
||||
</Text>
|
||||
<FormControl id={`noise_vendor${inputUniqueId}`}>
|
||||
<FormLabel>Vendor</FormLabel>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="krisp"
|
||||
value={noiseIsolationVendor}
|
||||
onChange={(e) => setNoiseIsolationVendor(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl id={`noise_level${inputUniqueId}`}>
|
||||
<FormLabel>Level</FormLabel>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.1"
|
||||
min="0"
|
||||
max="1"
|
||||
placeholder="0.3"
|
||||
value={noiseIsolationLevel}
|
||||
onChange={(e) => setNoiseIsolationLevel(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl id={`noise_model${inputUniqueId}`}>
|
||||
<FormLabel>Model (Optional)</FormLabel>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Model name"
|
||||
value={noiseIsolationModel}
|
||||
onChange={(e) => setNoiseIsolationModel(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
</VStack>
|
||||
</AnimateOnShow>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user