@7365admin1/layer-common 3.1.4-staging.57 → 3.1.4-staging.58
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -234,6 +234,13 @@
|
|
|
234
234
|
<v-radio label="UDP" value="UDP" />
|
|
235
235
|
<v-radio label="TCP" value="TCP" />
|
|
236
236
|
</v-radio-group>
|
|
237
|
+
<div class="sip-switch-row sip-section-switch">
|
|
238
|
+
<div>
|
|
239
|
+
<span>Enable video</span>
|
|
240
|
+
<small>Allow this reader to send and receive video during SIP calls.</small>
|
|
241
|
+
</div>
|
|
242
|
+
<v-switch v-model="sipForm.videoEnabled" :disabled="!sipForm.enabled" color="success" density="compact" hide-details />
|
|
243
|
+
</div>
|
|
237
244
|
</v-card-text>
|
|
238
245
|
<v-card-actions class="dialog-actions pa-0">
|
|
239
246
|
<v-btn class="text-none action-cancel" variant="flat" height="44" @click="sipDialog = false">Cancel</v-btn>
|
|
@@ -446,14 +453,27 @@
|
|
|
446
453
|
</div>
|
|
447
454
|
|
|
448
455
|
<div class="web-phone-console">
|
|
449
|
-
<div
|
|
456
|
+
<div
|
|
457
|
+
class="web-phone-media"
|
|
458
|
+
:class="{
|
|
459
|
+
'has-video': webPhoneForm.video && webPhone.callState.value === 'active',
|
|
460
|
+
'has-local-video': webPhone.localVideoAvailable.value,
|
|
461
|
+
'has-remote-video': webPhone.remoteVideoAvailable.value,
|
|
462
|
+
}"
|
|
463
|
+
>
|
|
450
464
|
<video ref="remoteVideoEl" autoplay playsinline class="remote-video" />
|
|
451
465
|
<video ref="localVideoEl" autoplay muted playsinline class="local-video" />
|
|
452
466
|
<audio ref="remoteAudioEl" autoplay />
|
|
453
|
-
<div
|
|
467
|
+
<div
|
|
468
|
+
v-if="!webPhoneForm.video || webPhone.callState.value !== 'active' || !webPhone.remoteVideoAvailable.value"
|
|
469
|
+
class="audio-call-state"
|
|
470
|
+
>
|
|
454
471
|
<v-icon size="54">{{ webPhoneCallIcon }}</v-icon>
|
|
455
472
|
<strong>{{ webPhoneCallLabel }}</strong>
|
|
456
473
|
<span v-if="webPhone.currentTarget.value">{{ webPhone.currentTarget.value }}</span>
|
|
474
|
+
<span v-if="webPhoneForm.video && webPhone.callState.value === 'active' && !webPhone.remoteVideoAvailable.value">
|
|
475
|
+
Waiting for video from the other device
|
|
476
|
+
</span>
|
|
457
477
|
</div>
|
|
458
478
|
</div>
|
|
459
479
|
|
|
@@ -507,7 +527,15 @@
|
|
|
507
527
|
variant="outlined"
|
|
508
528
|
:disabled="webPhone.callState.value !== 'active'"
|
|
509
529
|
:title="webPhone.muted.value ? 'Unmute' : 'Mute'"
|
|
510
|
-
@click="
|
|
530
|
+
@click="toggleWebMute"
|
|
531
|
+
/>
|
|
532
|
+
<v-btn
|
|
533
|
+
v-if="webPhoneForm.video"
|
|
534
|
+
:icon="webPhone.cameraEnabled.value ? 'mdi-video' : 'mdi-video-off'"
|
|
535
|
+
variant="outlined"
|
|
536
|
+
:disabled="webPhone.callState.value !== 'active' || !webPhone.localVideoAvailable.value"
|
|
537
|
+
:title="webPhone.cameraEnabled.value ? 'Turn camera off' : 'Turn camera on'"
|
|
538
|
+
@click="toggleWebCamera"
|
|
511
539
|
/>
|
|
512
540
|
<v-btn
|
|
513
541
|
icon="mdi-phone-hangup"
|
|
@@ -631,7 +659,7 @@ type DetailItem = {
|
|
|
631
659
|
};
|
|
632
660
|
|
|
633
661
|
type IntercomRow = {
|
|
634
|
-
reader: Record<string,
|
|
662
|
+
reader: Record<string, unknown>;
|
|
635
663
|
name: string;
|
|
636
664
|
location: string;
|
|
637
665
|
serverAddress: string;
|
|
@@ -647,6 +675,7 @@ type IntercomRow = {
|
|
|
647
675
|
password: string;
|
|
648
676
|
operatingMode: string;
|
|
649
677
|
protocol: string;
|
|
678
|
+
videoEnabled: boolean;
|
|
650
679
|
dialMode: string;
|
|
651
680
|
dialModeLabel: string;
|
|
652
681
|
dialCode: string;
|
|
@@ -659,7 +688,7 @@ type IntercomRow = {
|
|
|
659
688
|
externalDialButton: boolean;
|
|
660
689
|
unknownContact: boolean;
|
|
661
690
|
callAfter: string;
|
|
662
|
-
pjsip: Record<string,
|
|
691
|
+
pjsip: Record<string, unknown>;
|
|
663
692
|
};
|
|
664
693
|
|
|
665
694
|
const {
|
|
@@ -753,6 +782,7 @@ const sipForm = reactive({
|
|
|
753
782
|
password: "",
|
|
754
783
|
operatingMode: "sip-client",
|
|
755
784
|
protocol: "UDP",
|
|
785
|
+
videoEnabled: false,
|
|
756
786
|
});
|
|
757
787
|
|
|
758
788
|
const callForm = reactive({
|
|
@@ -863,6 +893,7 @@ const intercomDetailSections = computed(() => {
|
|
|
863
893
|
items: [
|
|
864
894
|
{ label: "Operating Mode", value: row?.operatingMode },
|
|
865
895
|
{ label: "Protocol", value: row?.protocol },
|
|
896
|
+
{ label: "Video", value: row?.videoEnabled ? "Enabled" : "Disabled" },
|
|
866
897
|
],
|
|
867
898
|
},
|
|
868
899
|
{
|
|
@@ -1005,7 +1036,7 @@ function paginate<T>(items: T[]) {
|
|
|
1005
1036
|
return items.slice(start, start + limit);
|
|
1006
1037
|
}
|
|
1007
1038
|
|
|
1008
|
-
function toIntercomRow(reader: Record<string,
|
|
1039
|
+
function toIntercomRow(reader: Record<string, unknown>, pjsip: Record<string, unknown>, status: string): IntercomRow {
|
|
1009
1040
|
const peerToPeer = toFlag(getPjsipValue(pjsip, ["peer_to_peer_enabled", "peerToPeerEnabled"])) === "1";
|
|
1010
1041
|
const dialMode = toText(getPjsipValue(pjsip, ["dialing_display_mode", "dialingDisplayMode"]), "0");
|
|
1011
1042
|
const initialRtpPort = toText(getPjsipValue(pjsip, ["server_outbound_port", "initialRtpPort", "initial_rtp_port"]), "N/A");
|
|
@@ -1019,13 +1050,13 @@ function toIntercomRow(reader: Record<string, any>, pjsip: Record<string, any>,
|
|
|
1019
1050
|
|
|
1020
1051
|
return {
|
|
1021
1052
|
reader,
|
|
1022
|
-
name: reader.name || reader.deviceId
|
|
1023
|
-
location: reader.location
|
|
1053
|
+
name: toText(reader.name || reader.deviceId, "HID Reader"),
|
|
1054
|
+
location: toText(reader.location, "N/A"),
|
|
1024
1055
|
serverAddress: toText(getPjsipValue(pjsip, ["server_ip", "serverAddress", "server_address", "serverIp"]), "N/A"),
|
|
1025
1056
|
serverPort: toText(getPjsipValue(pjsip, ["server_port", "serverPort", "port"]), "N/A"),
|
|
1026
1057
|
initialRtpPort,
|
|
1027
1058
|
finalRtpPort,
|
|
1028
|
-
monitorPath: reader.monitorPath
|
|
1059
|
+
monitorPath: toText(reader.monitorPath, "N/A"),
|
|
1029
1060
|
status,
|
|
1030
1061
|
sipEnabled: toFlag(getPjsipValue(pjsip, ["enabled"])) === "1",
|
|
1031
1062
|
numericExtension: toFlag(getPjsipValue(pjsip, ["numeric_branch_enabled", "numericExtension", "numericExtensionEnabled"])) === "1",
|
|
@@ -1034,6 +1065,7 @@ function toIntercomRow(reader: Record<string, any>, pjsip: Record<string, any>,
|
|
|
1034
1065
|
password: toText(getPjsipValue(pjsip, ["password"]), ""),
|
|
1035
1066
|
operatingMode: peerToPeer ? "Peer-to-Peer" : "SIP Client",
|
|
1036
1067
|
protocol: toText(getPjsipValue(pjsip, ["protocol"]), "UDP"),
|
|
1068
|
+
videoEnabled: toFlag(getPjsipValue(pjsip, ["video_enabled", "videoEnabled"])) === "1",
|
|
1037
1069
|
dialMode,
|
|
1038
1070
|
dialModeLabel: getDialModeLabel(dialMode),
|
|
1039
1071
|
dialCode: toText(getPjsipValue(pjsip, ["open_door_command", "dialCode"]), "N/A"),
|
|
@@ -1050,7 +1082,7 @@ function toIntercomRow(reader: Record<string, any>, pjsip: Record<string, any>,
|
|
|
1050
1082
|
};
|
|
1051
1083
|
}
|
|
1052
1084
|
|
|
1053
|
-
function normalizePjsip(...sources: Record<string,
|
|
1085
|
+
function normalizePjsip(...sources: Record<string, unknown>[]) {
|
|
1054
1086
|
for (const source of sources) {
|
|
1055
1087
|
const candidate = findPjsipCandidate(source);
|
|
1056
1088
|
if (candidate) return candidate;
|
|
@@ -1058,42 +1090,43 @@ function normalizePjsip(...sources: Record<string, any>[]) {
|
|
|
1058
1090
|
return {};
|
|
1059
1091
|
}
|
|
1060
1092
|
|
|
1061
|
-
function findPjsipCandidate(source: Record<string,
|
|
1093
|
+
function findPjsipCandidate(source: Record<string, unknown> | undefined): Record<string, unknown> | undefined {
|
|
1062
1094
|
if (!source || typeof source !== "object") return undefined;
|
|
1063
1095
|
const candidates = [
|
|
1064
1096
|
source.pjsip,
|
|
1065
|
-
source.
|
|
1066
|
-
source.
|
|
1067
|
-
source.result
|
|
1068
|
-
source.
|
|
1069
|
-
source.data
|
|
1070
|
-
source.data
|
|
1071
|
-
source.data
|
|
1072
|
-
source.data
|
|
1073
|
-
source.data
|
|
1074
|
-
source.configuration,
|
|
1075
|
-
source.
|
|
1076
|
-
source.data
|
|
1097
|
+
toRecord(source.pjsip),
|
|
1098
|
+
toRecord(toRecord(source.configuration).pjsip),
|
|
1099
|
+
toRecord(toRecord(source.result).pjsip),
|
|
1100
|
+
toRecord(toRecord(toRecord(source.result).configuration).pjsip),
|
|
1101
|
+
toRecord(toRecord(source.data).pjsip),
|
|
1102
|
+
toRecord(toRecord(toRecord(source.data).configuration).pjsip),
|
|
1103
|
+
toRecord(toRecord(toRecord(source.data).result).pjsip),
|
|
1104
|
+
toRecord(toRecord(toRecord(toRecord(source.data).result).configuration).pjsip),
|
|
1105
|
+
toRecord(toRecord(toRecord(source.data).data).pjsip),
|
|
1106
|
+
toRecord(toRecord(toRecord(toRecord(source.data).data).configuration).pjsip),
|
|
1107
|
+
toRecord(source.configuration),
|
|
1108
|
+
toRecord(toRecord(source.data).configuration),
|
|
1109
|
+
toRecord(toRecord(toRecord(source.data).data).configuration),
|
|
1077
1110
|
source,
|
|
1078
1111
|
];
|
|
1079
1112
|
|
|
1080
1113
|
return candidates.find((candidate) => isPjsipCandidate(candidate));
|
|
1081
1114
|
}
|
|
1082
1115
|
|
|
1083
|
-
function isPjsipCandidate(value: unknown): value is Record<string,
|
|
1116
|
+
function isPjsipCandidate(value: unknown): value is Record<string, unknown> {
|
|
1084
1117
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1085
1118
|
return pjsipKeys.some((key) => Object.prototype.hasOwnProperty.call(value, key)) ||
|
|
1086
1119
|
["serverAddress", "serverIp", "serverPort", "extension", "peerToPeerEnabled"].some((key) => Object.prototype.hasOwnProperty.call(value, key));
|
|
1087
1120
|
}
|
|
1088
1121
|
|
|
1089
|
-
function getPjsipValue(source: Record<string,
|
|
1122
|
+
function getPjsipValue(source: Record<string, unknown>, keys: string[]) {
|
|
1090
1123
|
for (const key of keys) {
|
|
1091
1124
|
if (source?.[key] !== undefined && source?.[key] !== null && source?.[key] !== "") return source[key];
|
|
1092
1125
|
}
|
|
1093
1126
|
return undefined;
|
|
1094
1127
|
}
|
|
1095
1128
|
|
|
1096
|
-
function normalizeStatus(response: Record<string,
|
|
1129
|
+
function normalizeStatus(response: Record<string, unknown>, pjsip: Record<string, unknown>) {
|
|
1097
1130
|
const status = Number(response?.status);
|
|
1098
1131
|
if (toFlag(pjsip.enabled) === "0" || status === -1) return "Disabled";
|
|
1099
1132
|
if (status === 200) return "Connected";
|
|
@@ -1133,6 +1166,7 @@ function openSipDialog(row: IntercomRow) {
|
|
|
1133
1166
|
sipForm.password = row.password;
|
|
1134
1167
|
sipForm.operatingMode = row.operatingMode === "Peer-to-Peer" ? "peer-to-peer" : "sip-client";
|
|
1135
1168
|
sipForm.protocol = row.protocol || "UDP";
|
|
1169
|
+
sipForm.videoEnabled = row.videoEnabled;
|
|
1136
1170
|
sipDialog.value = true;
|
|
1137
1171
|
}
|
|
1138
1172
|
|
|
@@ -1172,6 +1206,7 @@ function openWebPhoneDialog(row?: IntercomRow) {
|
|
|
1172
1206
|
selectedRow.value = activeRow;
|
|
1173
1207
|
if (!webPhone.isRegistered.value && activeRow.extension !== "N/A") {
|
|
1174
1208
|
webPhoneForm.destination = activeRow.extension;
|
|
1209
|
+
webPhoneForm.video = activeRow.videoEnabled;
|
|
1175
1210
|
}
|
|
1176
1211
|
}
|
|
1177
1212
|
webPhoneDialog.value = true;
|
|
@@ -1240,7 +1275,7 @@ async function startWebCall() {
|
|
|
1240
1275
|
webPhoneActionLoading.value = true;
|
|
1241
1276
|
try {
|
|
1242
1277
|
await webPhone.call(webPhoneForm.destination);
|
|
1243
|
-
} catch (error:
|
|
1278
|
+
} catch (error: unknown) {
|
|
1244
1279
|
showMessage(getErrorMessage(error), "error");
|
|
1245
1280
|
} finally {
|
|
1246
1281
|
webPhoneActionLoading.value = false;
|
|
@@ -1251,7 +1286,7 @@ async function answerWebCall() {
|
|
|
1251
1286
|
webPhoneActionLoading.value = true;
|
|
1252
1287
|
try {
|
|
1253
1288
|
await webPhone.answer();
|
|
1254
|
-
} catch (error:
|
|
1289
|
+
} catch (error: unknown) {
|
|
1255
1290
|
showMessage(getErrorMessage(error), "error");
|
|
1256
1291
|
} finally {
|
|
1257
1292
|
webPhoneActionLoading.value = false;
|
|
@@ -1262,7 +1297,7 @@ async function declineWebCall() {
|
|
|
1262
1297
|
webPhoneActionLoading.value = true;
|
|
1263
1298
|
try {
|
|
1264
1299
|
await webPhone.decline();
|
|
1265
|
-
} catch (error:
|
|
1300
|
+
} catch (error: unknown) {
|
|
1266
1301
|
showMessage(getErrorMessage(error), "error");
|
|
1267
1302
|
} finally {
|
|
1268
1303
|
webPhoneActionLoading.value = false;
|
|
@@ -1273,13 +1308,29 @@ async function hangupWebCall() {
|
|
|
1273
1308
|
webPhoneActionLoading.value = true;
|
|
1274
1309
|
try {
|
|
1275
1310
|
await webPhone.hangup();
|
|
1276
|
-
} catch (error:
|
|
1311
|
+
} catch (error: unknown) {
|
|
1277
1312
|
showMessage(getErrorMessage(error), "error");
|
|
1278
1313
|
} finally {
|
|
1279
1314
|
webPhoneActionLoading.value = false;
|
|
1280
1315
|
}
|
|
1281
1316
|
}
|
|
1282
1317
|
|
|
1318
|
+
function toggleWebCamera() {
|
|
1319
|
+
try {
|
|
1320
|
+
webPhone.toggleCamera();
|
|
1321
|
+
} catch (error: unknown) {
|
|
1322
|
+
showMessage(getErrorMessage(error), "error");
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
function toggleWebMute() {
|
|
1327
|
+
try {
|
|
1328
|
+
webPhone.toggleMute();
|
|
1329
|
+
} catch (error: unknown) {
|
|
1330
|
+
showMessage(getErrorMessage(error), "error");
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1283
1334
|
function openContactDialog(contact?: ContactRow) {
|
|
1284
1335
|
selectedContact.value = contact || null;
|
|
1285
1336
|
contactForm.id = contact?.id;
|
|
@@ -1328,6 +1379,7 @@ async function saveSipSettings() {
|
|
|
1328
1379
|
login: isPeerToPeer ? "" : sipForm.login,
|
|
1329
1380
|
password: isPeerToPeer ? "" : sipForm.password,
|
|
1330
1381
|
peer_to_peer_enabled: isPeerToPeer ? "1" : "0",
|
|
1382
|
+
video_enabled: sipForm.videoEnabled ? "1" : "0",
|
|
1331
1383
|
});
|
|
1332
1384
|
if (saved) {
|
|
1333
1385
|
sipDialog.value = false;
|
|
@@ -1852,6 +1904,20 @@ export default {
|
|
|
1852
1904
|
margin-bottom: 12px;
|
|
1853
1905
|
}
|
|
1854
1906
|
|
|
1907
|
+
.sip-section-switch > div {
|
|
1908
|
+
display: flex;
|
|
1909
|
+
min-width: 0;
|
|
1910
|
+
padding: 8px 12px 8px 0;
|
|
1911
|
+
flex-direction: column;
|
|
1912
|
+
gap: 2px;
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
.sip-section-switch small {
|
|
1916
|
+
color: #667085;
|
|
1917
|
+
font-size: 10px;
|
|
1918
|
+
line-height: 1.4;
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1855
1921
|
.sip-switch-row :deep(.v-switch) {
|
|
1856
1922
|
flex: 0 0 auto;
|
|
1857
1923
|
}
|
|
@@ -2392,7 +2458,7 @@ export default {
|
|
|
2392
2458
|
object-fit: cover;
|
|
2393
2459
|
}
|
|
2394
2460
|
|
|
2395
|
-
.web-phone-media.has-video .remote-video {
|
|
2461
|
+
.web-phone-media.has-remote-video .remote-video {
|
|
2396
2462
|
display: block;
|
|
2397
2463
|
}
|
|
2398
2464
|
|
|
@@ -2409,10 +2475,14 @@ export default {
|
|
|
2409
2475
|
object-fit: cover;
|
|
2410
2476
|
}
|
|
2411
2477
|
|
|
2412
|
-
.web-phone-media.has-video .local-video {
|
|
2478
|
+
.web-phone-media.has-local-video .local-video {
|
|
2413
2479
|
display: block;
|
|
2414
2480
|
}
|
|
2415
2481
|
|
|
2482
|
+
.web-phone-media.has-remote-video .audio-call-state {
|
|
2483
|
+
display: none;
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2416
2486
|
.audio-call-state {
|
|
2417
2487
|
display: flex;
|
|
2418
2488
|
align-items: center;
|
|
@@ -25,6 +25,9 @@ export default function useSipWebPhone() {
|
|
|
25
25
|
const callState = ref<CallState>("idle");
|
|
26
26
|
const errorMessage = ref("");
|
|
27
27
|
const muted = ref(false);
|
|
28
|
+
const cameraEnabled = ref(false);
|
|
29
|
+
const localVideoAvailable = ref(false);
|
|
30
|
+
const remoteVideoAvailable = ref(false);
|
|
28
31
|
const currentTarget = ref("");
|
|
29
32
|
const activeConfig = shallowRef<SipWebPhoneConfig>();
|
|
30
33
|
|
|
@@ -38,9 +41,30 @@ export default function useSipWebPhone() {
|
|
|
38
41
|
connectionState.value = "connecting";
|
|
39
42
|
errorMessage.value = "";
|
|
40
43
|
activeConfig.value = { ...config };
|
|
44
|
+
let clearRegistrationTimer = () => {};
|
|
41
45
|
|
|
42
46
|
try {
|
|
43
47
|
const { SimpleUser: SimpleUserConstructor } = await import("sip.js/lib/platform/web");
|
|
48
|
+
let registrationSettled = false;
|
|
49
|
+
let resolveRegistration: (() => void) | undefined;
|
|
50
|
+
let rejectRegistration: ((error: Error) => void) | undefined;
|
|
51
|
+
const registrationPromise = new Promise<void>((resolve, reject) => {
|
|
52
|
+
resolveRegistration = resolve;
|
|
53
|
+
rejectRegistration = reject;
|
|
54
|
+
});
|
|
55
|
+
const registrationTimeout = globalThis.setTimeout(() => {
|
|
56
|
+
if (registrationSettled) return;
|
|
57
|
+
registrationSettled = true;
|
|
58
|
+
rejectRegistration?.(new Error("SIP registration timed out. Check the WebSocket server and account credentials."));
|
|
59
|
+
}, 15000);
|
|
60
|
+
clearRegistrationTimer = () => globalThis.clearTimeout(registrationTimeout);
|
|
61
|
+
const settleRegistration = (error?: Error) => {
|
|
62
|
+
if (registrationSettled) return;
|
|
63
|
+
registrationSettled = true;
|
|
64
|
+
clearRegistrationTimer();
|
|
65
|
+
if (error) rejectRegistration?.(error);
|
|
66
|
+
else resolveRegistration?.();
|
|
67
|
+
};
|
|
44
68
|
const delegate: SimpleUserDelegate = {
|
|
45
69
|
onCallCreated: () => {
|
|
46
70
|
if (callState.value !== "incoming") callState.value = "calling";
|
|
@@ -51,17 +75,30 @@ export default function useSipWebPhone() {
|
|
|
51
75
|
},
|
|
52
76
|
onCallAnswered: () => {
|
|
53
77
|
callState.value = "active";
|
|
78
|
+
syncVideoState();
|
|
79
|
+
},
|
|
80
|
+
onCallHangup: () => {
|
|
81
|
+
const previousState = callState.value;
|
|
82
|
+
resetCall();
|
|
83
|
+
if (previousState === "calling") {
|
|
84
|
+
errorMessage.value = "The call was not answered or was rejected by the destination.";
|
|
85
|
+
}
|
|
54
86
|
},
|
|
55
|
-
onCallHangup: () => resetCall(),
|
|
56
87
|
onRegistered: () => {
|
|
57
88
|
connectionState.value = "registered";
|
|
89
|
+
settleRegistration();
|
|
58
90
|
},
|
|
59
91
|
onUnregistered: () => {
|
|
92
|
+
if (connectionState.value === "connecting") {
|
|
93
|
+
settleRegistration(new Error("SIP registration was rejected. Check the username and password."));
|
|
94
|
+
}
|
|
60
95
|
connectionState.value = "disconnected";
|
|
61
96
|
},
|
|
62
|
-
onServerDisconnect: (
|
|
63
|
-
|
|
64
|
-
|
|
97
|
+
onServerDisconnect: () => {
|
|
98
|
+
const error = new Error("The SIP WebSocket server disconnected during registration.");
|
|
99
|
+
connectionState.value = "error";
|
|
100
|
+
errorMessage.value = error.message;
|
|
101
|
+
settleRegistration(error);
|
|
65
102
|
resetCall();
|
|
66
103
|
},
|
|
67
104
|
};
|
|
@@ -87,7 +124,9 @@ export default function useSipWebPhone() {
|
|
|
87
124
|
simpleUser.value = user;
|
|
88
125
|
await user.connect();
|
|
89
126
|
await user.register();
|
|
127
|
+
await registrationPromise;
|
|
90
128
|
} catch (error) {
|
|
129
|
+
clearRegistrationTimer();
|
|
91
130
|
await disconnect(false);
|
|
92
131
|
connectionState.value = "error";
|
|
93
132
|
errorMessage.value = getErrorMessage(error);
|
|
@@ -159,18 +198,65 @@ export default function useSipWebPhone() {
|
|
|
159
198
|
|
|
160
199
|
function toggleMute() {
|
|
161
200
|
const user = requireUser();
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
201
|
+
const tracks = user.localMediaStream?.getAudioTracks() || [];
|
|
202
|
+
if (!tracks.length) throw new Error("No microphone track is available for this call.");
|
|
203
|
+
const enable = muted.value;
|
|
204
|
+
tracks.forEach((track) => {
|
|
205
|
+
track.enabled = enable;
|
|
206
|
+
});
|
|
207
|
+
muted.value = !enable;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function toggleCamera() {
|
|
211
|
+
const user = requireUser();
|
|
212
|
+
const tracks = user.localMediaStream?.getVideoTracks() || [];
|
|
213
|
+
if (!tracks.length) throw new Error("No camera track is available. Reconnect the web phone with video enabled.");
|
|
214
|
+
const enable = !cameraEnabled.value;
|
|
215
|
+
tracks.forEach((track) => {
|
|
216
|
+
track.enabled = enable;
|
|
217
|
+
});
|
|
218
|
+
cameraEnabled.value = enable;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function syncVideoState() {
|
|
222
|
+
const user = simpleUser.value;
|
|
223
|
+
const localStream = user?.localMediaStream;
|
|
224
|
+
const remoteStream = user?.remoteMediaStream;
|
|
225
|
+
const localTrack = localStream?.getVideoTracks()[0];
|
|
226
|
+
const remoteTrack = remoteStream?.getVideoTracks()[0];
|
|
227
|
+
|
|
228
|
+
const updateState = () => {
|
|
229
|
+
const currentLocalTrack = localStream?.getVideoTracks()[0];
|
|
230
|
+
const currentRemoteTrack = remoteStream?.getVideoTracks()[0];
|
|
231
|
+
localVideoAvailable.value = Boolean(currentLocalTrack && currentLocalTrack.readyState === "live");
|
|
232
|
+
remoteVideoAvailable.value = Boolean(currentRemoteTrack && currentRemoteTrack.readyState === "live");
|
|
233
|
+
cameraEnabled.value = Boolean(currentLocalTrack?.enabled);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
updateState();
|
|
237
|
+
if (localStream) localStream.onaddtrack = updateState;
|
|
238
|
+
if (remoteStream) remoteStream.onaddtrack = updateState;
|
|
239
|
+
|
|
240
|
+
if (localTrack) {
|
|
241
|
+
localTrack.onended = () => {
|
|
242
|
+
localVideoAvailable.value = false;
|
|
243
|
+
cameraEnabled.value = false;
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
if (remoteTrack) {
|
|
247
|
+
remoteTrack.onended = () => {
|
|
248
|
+
remoteVideoAvailable.value = false;
|
|
249
|
+
};
|
|
166
250
|
}
|
|
167
|
-
muted.value = user.isMuted();
|
|
168
251
|
}
|
|
169
252
|
|
|
170
253
|
function resetCall() {
|
|
171
254
|
callState.value = "idle";
|
|
172
255
|
currentTarget.value = "";
|
|
173
256
|
muted.value = false;
|
|
257
|
+
cameraEnabled.value = false;
|
|
258
|
+
localVideoAvailable.value = false;
|
|
259
|
+
remoteVideoAvailable.value = false;
|
|
174
260
|
}
|
|
175
261
|
|
|
176
262
|
function requireRegisteredUser() {
|
|
@@ -193,6 +279,9 @@ export default function useSipWebPhone() {
|
|
|
193
279
|
callState: readonly(callState),
|
|
194
280
|
errorMessage: readonly(errorMessage),
|
|
195
281
|
muted: readonly(muted),
|
|
282
|
+
cameraEnabled: readonly(cameraEnabled),
|
|
283
|
+
localVideoAvailable: readonly(localVideoAvailable),
|
|
284
|
+
remoteVideoAvailable: readonly(remoteVideoAvailable),
|
|
196
285
|
currentTarget: readonly(currentTarget),
|
|
197
286
|
isRegistered,
|
|
198
287
|
hasCall,
|
|
@@ -203,6 +292,7 @@ export default function useSipWebPhone() {
|
|
|
203
292
|
decline,
|
|
204
293
|
hangup,
|
|
205
294
|
toggleMute,
|
|
295
|
+
toggleCamera,
|
|
206
296
|
};
|
|
207
297
|
}
|
|
208
298
|
|
|
@@ -232,5 +322,11 @@ function normalizeDestination(target: string, aor: string) {
|
|
|
232
322
|
}
|
|
233
323
|
|
|
234
324
|
function getErrorMessage(error: unknown) {
|
|
325
|
+
if (error instanceof DOMException && error.name === "NotAllowedError") {
|
|
326
|
+
return "Camera or microphone permission was denied. Allow media access in the browser and reconnect the web phone.";
|
|
327
|
+
}
|
|
328
|
+
if (error instanceof DOMException && error.name === "NotFoundError") {
|
|
329
|
+
return "No camera or microphone was found for this video call.";
|
|
330
|
+
}
|
|
235
331
|
return error instanceof Error ? error.message : "SIP connection failed.";
|
|
236
332
|
}
|