KI-AGENT: Anrufsignalisierung im Softphone ergänzen

This commit is contained in:
2026-05-21 13:38:01 +02:00
parent df32bf516b
commit d99cddf5b5

View File

@@ -24,6 +24,15 @@ const incomingCall = ref(null)
const callState = ref("idle") const callState = ref("idle")
const registererState = ref("Initial") const registererState = ref("Initial")
const sipEvents = ref([]) 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) => { const addSipEvent = (message) => {
sipEvents.value = [ sipEvents.value = [
@@ -35,6 +44,72 @@ const addSipEvent = (message) => {
].slice(0, 8) ].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(() => const selectedAccount = computed(() =>
(config.value?.testAccounts || []).find((account) => account.extension === selectedExtension.value) (config.value?.testAccounts || []).find((account) => account.extension === selectedExtension.value)
|| config.value?.testAccounts?.[0] || config.value?.testAccounts?.[0]
@@ -203,6 +278,7 @@ const setupSession = (session, direction = "outgoing") => {
} }
if (state === SessionState.Established) { if (state === SessionState.Established) {
stopCallSignaling()
callState.value = "active" callState.value = "active"
sipStatus.value = "Im Gespräch" sipStatus.value = "Im Gespräch"
incomingCall.value = null incomingCall.value = null
@@ -210,6 +286,7 @@ const setupSession = (session, direction = "outgoing") => {
} }
if (state === SessionState.Terminated) { if (state === SessionState.Terminated) {
stopCallSignaling()
callState.value = "terminated" callState.value = "terminated"
sipStatus.value = sipRegistered.value ? "Registriert" : "Nicht verbunden" sipStatus.value = sipRegistered.value ? "Registriert" : "Nicht verbunden"
activeSession.value = null activeSession.value = null
@@ -268,6 +345,7 @@ const registerSip = async () => {
addSipEvent(`INVITE von ${invitation.remoteIdentity?.uri?.user || "unbekannt"} empfangen`) addSipEvent(`INVITE von ${invitation.remoteIdentity?.uri?.user || "unbekannt"} empfangen`)
incomingCall.value = invitation incomingCall.value = invitation
setupSession(invitation, "incoming") setupSession(invitation, "incoming")
startCallSignaling()
toast.add({ toast.add({
title: "Eingehender Anruf", title: "Eingehender Anruf",
@@ -411,6 +489,7 @@ const acceptIncomingCall = async () => {
sipLoading.value = true sipLoading.value = true
sipError.value = null sipError.value = null
stopCallSignaling()
try { try {
await incomingCall.value.accept({ await incomingCall.value.accept({
@@ -432,6 +511,7 @@ const rejectIncomingCall = async () => {
if (!incomingCall.value) return if (!incomingCall.value) return
try { try {
stopCallSignaling()
await incomingCall.value.reject() await incomingCall.value.reject()
} finally { } finally {
incomingCall.value = null incomingCall.value = null
@@ -457,6 +537,7 @@ const hangupCall = async () => {
session.dispose() session.dispose()
} }
} finally { } finally {
stopCallSignaling()
activeSession.value = null activeSession.value = null
incomingCall.value = null incomingCall.value = null
callState.value = "idle" callState.value = "idle"
@@ -474,6 +555,7 @@ watch(config, (nextConfig) => {
onMounted(loadTelephony) onMounted(loadTelephony)
onBeforeUnmount(() => { onBeforeUnmount(() => {
stopCallSignaling()
stopSip() stopSip()
}) })
</script> </script>
@@ -481,6 +563,60 @@ onBeforeUnmount(() => {
<template> <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="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 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 class="flex flex-col gap-4 border-b border-gray-200 pb-5 lg:flex-row lg:items-end lg:justify-between">
<div> <div>
<p class="text-sm font-medium text-primary-600"> <p class="text-sm font-medium text-primary-600">