KI-AGENT: Anrufsignalisierung im Softphone ergänzen
This commit is contained in:
@@ -24,6 +24,15 @@ const incomingCall = ref(null)
|
||||
const callState = ref("idle")
|
||||
const registererState = ref("Initial")
|
||||
const sipEvents = ref([])
|
||||
const ringtoneTimer = shallowRef(null)
|
||||
const ringtoneAudioContext = shallowRef(null)
|
||||
const callSignalStatus = ref("Bereit")
|
||||
const originalDocumentTitle = ref("")
|
||||
|
||||
const incomingCaller = computed(() => {
|
||||
const identity = incomingCall.value?.remoteIdentity
|
||||
return identity?.displayName || identity?.uri?.user || "Unbekannter Anrufer"
|
||||
})
|
||||
|
||||
const addSipEvent = (message) => {
|
||||
sipEvents.value = [
|
||||
@@ -35,6 +44,72 @@ const addSipEvent = (message) => {
|
||||
].slice(0, 8)
|
||||
}
|
||||
|
||||
const playRingtoneTick = async () => {
|
||||
if (!window.AudioContext && !window.webkitAudioContext) return
|
||||
|
||||
const AudioContextConstructor = window.AudioContext || window.webkitAudioContext
|
||||
const context = ringtoneAudioContext.value || new AudioContextConstructor()
|
||||
ringtoneAudioContext.value = context
|
||||
|
||||
if (context.state === "suspended") {
|
||||
await context.resume()
|
||||
}
|
||||
|
||||
const oscillator = context.createOscillator()
|
||||
const gain = context.createGain()
|
||||
oscillator.type = "sine"
|
||||
oscillator.frequency.value = 880
|
||||
gain.gain.setValueAtTime(0.0001, context.currentTime)
|
||||
gain.gain.exponentialRampToValueAtTime(0.12, context.currentTime + 0.03)
|
||||
gain.gain.exponentialRampToValueAtTime(0.0001, context.currentTime + 0.35)
|
||||
oscillator.connect(gain)
|
||||
gain.connect(context.destination)
|
||||
oscillator.start()
|
||||
oscillator.stop(context.currentTime + 0.38)
|
||||
}
|
||||
|
||||
const startCallSignaling = async () => {
|
||||
stopCallSignaling()
|
||||
callSignalStatus.value = "Signalisiert"
|
||||
originalDocumentTitle.value = document.title
|
||||
document.title = "Eingehender Anruf - FEDEO"
|
||||
|
||||
if (navigator.vibrate) {
|
||||
navigator.vibrate([180, 90, 180])
|
||||
}
|
||||
|
||||
try {
|
||||
await playRingtoneTick()
|
||||
ringtoneTimer.value = window.setInterval(() => {
|
||||
playRingtoneTick().catch((error) => {
|
||||
callSignalStatus.value = "Klingelton blockiert"
|
||||
addSipEvent(`Klingelton blockiert: ${error?.message || "Browser-Autoplay"}`)
|
||||
})
|
||||
}, 1400)
|
||||
} catch (error) {
|
||||
callSignalStatus.value = "Klingelton blockiert"
|
||||
addSipEvent(`Klingelton blockiert: ${error?.message || "Browser-Autoplay"}`)
|
||||
}
|
||||
}
|
||||
|
||||
const stopCallSignaling = () => {
|
||||
if (ringtoneTimer.value) {
|
||||
window.clearInterval(ringtoneTimer.value)
|
||||
ringtoneTimer.value = null
|
||||
}
|
||||
|
||||
if (originalDocumentTitle.value) {
|
||||
document.title = originalDocumentTitle.value
|
||||
originalDocumentTitle.value = ""
|
||||
}
|
||||
|
||||
if (navigator.vibrate) {
|
||||
navigator.vibrate(0)
|
||||
}
|
||||
|
||||
callSignalStatus.value = "Bereit"
|
||||
}
|
||||
|
||||
const selectedAccount = computed(() =>
|
||||
(config.value?.testAccounts || []).find((account) => account.extension === selectedExtension.value)
|
||||
|| config.value?.testAccounts?.[0]
|
||||
@@ -203,6 +278,7 @@ const setupSession = (session, direction = "outgoing") => {
|
||||
}
|
||||
|
||||
if (state === SessionState.Established) {
|
||||
stopCallSignaling()
|
||||
callState.value = "active"
|
||||
sipStatus.value = "Im Gespräch"
|
||||
incomingCall.value = null
|
||||
@@ -210,6 +286,7 @@ const setupSession = (session, direction = "outgoing") => {
|
||||
}
|
||||
|
||||
if (state === SessionState.Terminated) {
|
||||
stopCallSignaling()
|
||||
callState.value = "terminated"
|
||||
sipStatus.value = sipRegistered.value ? "Registriert" : "Nicht verbunden"
|
||||
activeSession.value = null
|
||||
@@ -268,6 +345,7 @@ const registerSip = async () => {
|
||||
addSipEvent(`INVITE von ${invitation.remoteIdentity?.uri?.user || "unbekannt"} empfangen`)
|
||||
incomingCall.value = invitation
|
||||
setupSession(invitation, "incoming")
|
||||
startCallSignaling()
|
||||
|
||||
toast.add({
|
||||
title: "Eingehender Anruf",
|
||||
@@ -411,6 +489,7 @@ const acceptIncomingCall = async () => {
|
||||
|
||||
sipLoading.value = true
|
||||
sipError.value = null
|
||||
stopCallSignaling()
|
||||
|
||||
try {
|
||||
await incomingCall.value.accept({
|
||||
@@ -432,6 +511,7 @@ const rejectIncomingCall = async () => {
|
||||
if (!incomingCall.value) return
|
||||
|
||||
try {
|
||||
stopCallSignaling()
|
||||
await incomingCall.value.reject()
|
||||
} finally {
|
||||
incomingCall.value = null
|
||||
@@ -457,6 +537,7 @@ const hangupCall = async () => {
|
||||
session.dispose()
|
||||
}
|
||||
} finally {
|
||||
stopCallSignaling()
|
||||
activeSession.value = null
|
||||
incomingCall.value = null
|
||||
callState.value = "idle"
|
||||
@@ -474,6 +555,7 @@ watch(config, (nextConfig) => {
|
||||
onMounted(loadTelephony)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopCallSignaling()
|
||||
stopSip()
|
||||
})
|
||||
</script>
|
||||
@@ -481,6 +563,60 @@ onBeforeUnmount(() => {
|
||||
<template>
|
||||
<div class="min-h-0 flex-1 overflow-y-auto bg-gray-50 px-4 py-6 sm:px-6 lg:px-8">
|
||||
<div class="mx-auto flex max-w-7xl flex-col gap-6">
|
||||
<div
|
||||
v-if="incomingCall"
|
||||
class="sticky top-0 z-30 rounded-lg border border-primary-200 bg-white p-4 shadow-lg ring-1 ring-primary-100"
|
||||
>
|
||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div class="flex min-w-0 items-center gap-3">
|
||||
<div class="flex h-12 w-12 shrink-0 items-center justify-center rounded-full bg-primary-50 text-primary-600 ring-8 ring-primary-100">
|
||||
<UIcon name="i-heroicons-phone-arrow-down-left" class="h-6 w-6" />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<p class="text-xs font-medium uppercase text-primary-600">
|
||||
Eingehender Anruf
|
||||
</p>
|
||||
<h2 class="truncate text-lg font-semibold text-gray-950">
|
||||
{{ incomingCaller }}
|
||||
</h2>
|
||||
<div class="mt-1 flex flex-wrap items-center gap-2 text-xs text-gray-500">
|
||||
<span>Nebenstelle {{ selectedExtension }}</span>
|
||||
<UBadge color="primary" variant="soft">
|
||||
{{ callSignalStatus }}
|
||||
</UBadge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2 sm:grid-cols-2 lg:min-w-72">
|
||||
<UButton
|
||||
color="success"
|
||||
icon="i-heroicons-phone"
|
||||
size="lg"
|
||||
block
|
||||
:loading="sipLoading"
|
||||
@click="acceptIncomingCall"
|
||||
>
|
||||
Annehmen
|
||||
</UButton>
|
||||
<UButton
|
||||
color="error"
|
||||
variant="soft"
|
||||
icon="i-heroicons-phone-x-mark"
|
||||
size="lg"
|
||||
block
|
||||
@click="rejectIncomingCall"
|
||||
>
|
||||
Ablehnen
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 rounded-md bg-gray-50 px-3 py-2 text-xs text-gray-500">
|
||||
Debug: INVITE empfangen, Session aktiv, Signalisierung {{ callSignalStatus }}.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4 border-b border-gray-200 pb-5 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-primary-600">
|
||||
|
||||
Reference in New Issue
Block a user