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 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">
|
||||||
|
|||||||
Reference in New Issue
Block a user