@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 class="web-phone-media" :class="{ 'has-video': webPhoneForm.video && webPhone.callState.value === 'active' }">
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 v-if="!webPhoneForm.video || webPhone.callState.value !== 'active'" class="audio-call-state">
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="webPhone.toggleMute"
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, any>;
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, any>;
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, any>, pjsip: Record<string, any>, status: string): IntercomRow {
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 || "HID Reader",
1023
- location: reader.location || "N/A",
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 || "N/A",
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, any>[]) {
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, any> | undefined): Record<string, any> | undefined {
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.configuration?.pjsip,
1066
- source.result?.pjsip,
1067
- source.result?.configuration?.pjsip,
1068
- source.data?.pjsip,
1069
- source.data?.configuration?.pjsip,
1070
- source.data?.result?.pjsip,
1071
- source.data?.result?.configuration?.pjsip,
1072
- source.data?.data?.pjsip,
1073
- source.data?.data?.configuration?.pjsip,
1074
- source.configuration,
1075
- source.data?.configuration,
1076
- source.data?.data?.configuration,
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, any> {
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, any>, keys: 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, any>, pjsip: Record<string, any>) {
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: any) {
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: any) {
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: any) {
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: any) {
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: (error) => {
63
- connectionState.value = error ? "error" : "disconnected";
64
- if (error) errorMessage.value = error.message;
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
- if (muted.value) {
163
- user.unmute();
164
- } else {
165
- user.mute();
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
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@7365admin1/layer-common",
3
3
  "license": "MIT",
4
4
  "type": "module",
5
- "version": "3.1.4-staging.57",
5
+ "version": "3.1.4-staging.58",
6
6
  "author": "7365admin1",
7
7
  "main": "./nuxt.config.ts",
8
8
  "publishConfig": {