KI-AGENT: LiveKit-Calls nativ in FEDEO integrieren
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
<script setup>
|
||||
import { ConnectionState, Room, RoomEvent, Track } from "livekit-client"
|
||||
|
||||
const toast = useToast()
|
||||
const { $api } = useNuxtApp()
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
|
||||
const status = ref(null)
|
||||
const identity = ref(null)
|
||||
@@ -15,7 +16,14 @@ const roomCreateOpen = ref(false)
|
||||
const matrixCallOpen = ref(false)
|
||||
const matrixCallMode = ref("video")
|
||||
const matrixCallLoading = ref(false)
|
||||
const matrixCallUrl = ref("")
|
||||
const matrixCallConnected = ref(false)
|
||||
const matrixCallState = ref("disconnected")
|
||||
const matrixCallError = ref("")
|
||||
const matrixCallSession = ref(null)
|
||||
const matrixCallTiles = ref([])
|
||||
const matrixCallMicEnabled = ref(true)
|
||||
const matrixCallCameraEnabled = ref(true)
|
||||
const matrixCallAudioContainer = ref(null)
|
||||
const roomCreateForm = ref({
|
||||
name: "",
|
||||
key: "",
|
||||
@@ -33,6 +41,8 @@ const lastUpdated = ref(null)
|
||||
let matrixRefreshInterval = null
|
||||
let matrixMessagesRequestActive = false
|
||||
let matrixMembersRequestActive = false
|
||||
let matrixLiveKitRoom = null
|
||||
const matrixCallVideoElements = new Map()
|
||||
|
||||
const canUseMatrixChat = computed(() =>
|
||||
Boolean(status.value?.reachable && status.value?.provisioningConfigured)
|
||||
@@ -53,32 +63,18 @@ const activeRoomEndpoint = computed(() =>
|
||||
`/api/communication/matrix/rooms/${encodeURIComponent(activeRoomKey.value)}`
|
||||
)
|
||||
|
||||
const matrixElementUrl = computed(() =>
|
||||
String(runtimeConfig.public?.matrixElementUrl || "").replace(/\/+$/, "")
|
||||
)
|
||||
|
||||
const activeRoomMatrixAddress = computed(() =>
|
||||
activeRoom.value?.roomId || activeRoom.value?.alias || ""
|
||||
)
|
||||
|
||||
const activeRoomElementUrl = computed(() => {
|
||||
if (!matrixElementUrl.value || !activeRoomMatrixAddress.value) return ""
|
||||
|
||||
return `${matrixElementUrl.value}/#/room/${encodeURIComponent(activeRoomMatrixAddress.value)}`
|
||||
})
|
||||
|
||||
const canStartMatrixCall = computed(() =>
|
||||
Boolean(canUseMatrixChat.value && activeRoom.value?.exists && activeRoomElementUrl.value)
|
||||
Boolean(canUseMatrixChat.value && activeRoom.value?.exists)
|
||||
)
|
||||
|
||||
const matrixCallTitle = computed(() =>
|
||||
matrixCallMode.value === "audio" ? "Audioanruf" : "Videokonferenz"
|
||||
)
|
||||
|
||||
const activeMatrixCallUrl = computed(() =>
|
||||
matrixCallUrl.value || activeRoomElementUrl.value
|
||||
)
|
||||
|
||||
const roomCreateKeyPreview = computed(() =>
|
||||
normalizeRoomKey(roomCreateForm.value.key || roomCreateForm.value.name)
|
||||
)
|
||||
@@ -346,17 +342,6 @@ const syncRoomMembers = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const buildElementRoomSessionUrl = (session) => {
|
||||
const roomAddress = session.roomId || session.alias || activeRoomMatrixAddress.value
|
||||
if (!matrixElementUrl.value || !roomAddress) return ""
|
||||
|
||||
const params = new URLSearchParams({
|
||||
loginToken: session.loginToken
|
||||
})
|
||||
|
||||
return `${matrixElementUrl.value}/?${params.toString()}#/room/${encodeURIComponent(roomAddress)}`
|
||||
}
|
||||
|
||||
const openMatrixCall = async (mode = "video") => {
|
||||
if (!canStartMatrixCall.value) {
|
||||
toast.add({
|
||||
@@ -368,25 +353,175 @@ const openMatrixCall = async (mode = "video") => {
|
||||
|
||||
matrixCallMode.value = mode
|
||||
matrixCallLoading.value = true
|
||||
matrixCallError.value = ""
|
||||
matrixCallOpen.value = true
|
||||
|
||||
try {
|
||||
const session = await $api(`${activeRoomEndpoint.value}/session`, {
|
||||
await leaveMatrixCall()
|
||||
const session = await $api(`${activeRoomEndpoint.value}/call-session`, {
|
||||
method: "POST"
|
||||
})
|
||||
matrixCallUrl.value = buildElementRoomSessionUrl(session) || activeRoomElementUrl.value
|
||||
matrixCallSession.value = session
|
||||
await connectMatrixCall(session, { video: mode === "video" })
|
||||
} catch (error) {
|
||||
matrixCallUrl.value = activeRoomElementUrl.value
|
||||
matrixCallError.value = error?.data?.error || error?.message || "Verbindung fehlgeschlagen"
|
||||
toast.add({
|
||||
title: "Matrix-Anmeldung konnte nicht vorbereitet werden",
|
||||
description: "Der Raum wird ohne automatische Anmeldung geöffnet.",
|
||||
color: "warning"
|
||||
title: "Besprechung konnte nicht gestartet werden",
|
||||
description: matrixCallError.value,
|
||||
color: "error"
|
||||
})
|
||||
} finally {
|
||||
matrixCallLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const getParticipantTile = (participant, local = false) => {
|
||||
const publications = participant.getTrackPublications?.() || []
|
||||
const videoPublication = publications.find((publication) =>
|
||||
publication.kind === Track.Kind.Video && publication.track
|
||||
)
|
||||
const audioPublication = publications.find((publication) =>
|
||||
publication.kind === Track.Kind.Audio && publication.track
|
||||
)
|
||||
|
||||
return {
|
||||
id: local ? "local" : participant.sid || participant.identity,
|
||||
identity: participant.identity,
|
||||
name: local ? "Du" : participant.name || participant.identity,
|
||||
local,
|
||||
speaking: participant.isSpeaking,
|
||||
videoTrack: videoPublication?.track || null,
|
||||
audioTrack: audioPublication?.track || null,
|
||||
cameraEnabled: participant.isCameraEnabled,
|
||||
microphoneEnabled: participant.isMicrophoneEnabled,
|
||||
}
|
||||
}
|
||||
|
||||
const attachMatrixCallMedia = async () => {
|
||||
await nextTick()
|
||||
|
||||
for (const tile of matrixCallTiles.value) {
|
||||
const videoElement = matrixCallVideoElements.get(tile.id)
|
||||
|
||||
if (videoElement && tile.videoTrack) {
|
||||
tile.videoTrack.attach(videoElement)
|
||||
}
|
||||
|
||||
if (!tile.local && tile.audioTrack && matrixCallAudioContainer.value) {
|
||||
const audioElement = tile.audioTrack.attach()
|
||||
audioElement.autoplay = true
|
||||
audioElement.dataset.participant = tile.id
|
||||
matrixCallAudioContainer.value.appendChild(audioElement)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const refreshMatrixCallTiles = async () => {
|
||||
if (!matrixLiveKitRoom) {
|
||||
matrixCallTiles.value = []
|
||||
return
|
||||
}
|
||||
|
||||
matrixCallAudioContainer.value?.replaceChildren()
|
||||
matrixCallTiles.value = [
|
||||
getParticipantTile(matrixLiveKitRoom.localParticipant, true),
|
||||
...Array.from(matrixLiveKitRoom.remoteParticipants.values()).map((participant) =>
|
||||
getParticipantTile(participant)
|
||||
)
|
||||
]
|
||||
|
||||
await attachMatrixCallMedia()
|
||||
}
|
||||
|
||||
const connectMatrixCall = async (session, { video = true } = {}) => {
|
||||
const room = new Room({
|
||||
adaptiveStream: true,
|
||||
dynacast: true,
|
||||
})
|
||||
|
||||
matrixLiveKitRoom = room
|
||||
matrixCallState.value = ConnectionState.Connecting
|
||||
|
||||
const refreshEvents = [
|
||||
RoomEvent.TrackSubscribed,
|
||||
RoomEvent.TrackUnsubscribed,
|
||||
RoomEvent.LocalTrackPublished,
|
||||
RoomEvent.LocalTrackUnpublished,
|
||||
RoomEvent.ParticipantConnected,
|
||||
RoomEvent.ParticipantDisconnected,
|
||||
RoomEvent.TrackMuted,
|
||||
RoomEvent.TrackUnmuted,
|
||||
RoomEvent.ActiveSpeakersChanged,
|
||||
]
|
||||
|
||||
for (const eventName of refreshEvents) {
|
||||
room.on(eventName, refreshMatrixCallTiles)
|
||||
}
|
||||
|
||||
room.on(RoomEvent.ConnectionStateChanged, (state) => {
|
||||
matrixCallState.value = state
|
||||
matrixCallConnected.value = state === ConnectionState.Connected
|
||||
})
|
||||
|
||||
room.on(RoomEvent.Disconnected, () => {
|
||||
matrixCallConnected.value = false
|
||||
matrixCallState.value = ConnectionState.Disconnected
|
||||
})
|
||||
|
||||
await room.connect(session.liveKitUrl, session.liveKitToken)
|
||||
await room.localParticipant.setMicrophoneEnabled(true)
|
||||
await room.localParticipant.setCameraEnabled(video)
|
||||
matrixCallMicEnabled.value = true
|
||||
matrixCallCameraEnabled.value = video
|
||||
await refreshMatrixCallTiles()
|
||||
}
|
||||
|
||||
const leaveMatrixCall = async () => {
|
||||
if (matrixLiveKitRoom) {
|
||||
matrixLiveKitRoom.disconnect()
|
||||
matrixLiveKitRoom = null
|
||||
}
|
||||
|
||||
matrixCallVideoElements.clear()
|
||||
matrixCallAudioContainer.value?.replaceChildren()
|
||||
matrixCallTiles.value = []
|
||||
matrixCallConnected.value = false
|
||||
matrixCallState.value = "disconnected"
|
||||
}
|
||||
|
||||
const closeMatrixCall = async () => {
|
||||
await leaveMatrixCall()
|
||||
matrixCallOpen.value = false
|
||||
}
|
||||
|
||||
const setMatrixCallVideoElement = (tileId, element) => {
|
||||
if (element) {
|
||||
matrixCallVideoElements.set(tileId, element)
|
||||
const tile = matrixCallTiles.value.find((item) => item.id === tileId)
|
||||
tile?.videoTrack?.attach(element)
|
||||
} else {
|
||||
matrixCallVideoElements.delete(tileId)
|
||||
}
|
||||
}
|
||||
|
||||
const toggleMatrixCallMic = async () => {
|
||||
if (!matrixLiveKitRoom) return
|
||||
|
||||
const enabled = !matrixCallMicEnabled.value
|
||||
await matrixLiveKitRoom.localParticipant.setMicrophoneEnabled(enabled)
|
||||
matrixCallMicEnabled.value = enabled
|
||||
await refreshMatrixCallTiles()
|
||||
}
|
||||
|
||||
const toggleMatrixCallCamera = async () => {
|
||||
if (!matrixLiveKitRoom) return
|
||||
|
||||
const enabled = !matrixCallCameraEnabled.value
|
||||
await matrixLiveKitRoom.localParticipant.setCameraEnabled(enabled)
|
||||
matrixCallCameraEnabled.value = enabled
|
||||
await refreshMatrixCallTiles()
|
||||
}
|
||||
|
||||
const loadRoomChat = async ({ silent = false, includeMembers = false } = {}) => {
|
||||
await loadRoomMessages({ silent })
|
||||
|
||||
@@ -488,7 +623,10 @@ onMounted(async () => {
|
||||
startMatrixAutoRefresh()
|
||||
})
|
||||
|
||||
onBeforeUnmount(stopMatrixAutoRefresh)
|
||||
onBeforeUnmount(() => {
|
||||
stopMatrixAutoRefresh()
|
||||
leaveMatrixCall()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -978,20 +1116,36 @@ onBeforeUnmount(stopMatrixAutoRefresh)
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<UButton
|
||||
v-if="activeMatrixCallUrl"
|
||||
:to="activeMatrixCallUrl"
|
||||
target="_blank"
|
||||
icon="i-heroicons-arrow-top-right-on-square"
|
||||
icon="i-heroicons-microphone"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
:variant="matrixCallMicEnabled ? 'soft' : 'outline'"
|
||||
:disabled="!matrixLiveKitRoom"
|
||||
@click="toggleMatrixCallMic"
|
||||
>
|
||||
Extern öffnen
|
||||
{{ matrixCallMicEnabled ? "Mikro an" : "Mikro aus" }}
|
||||
</UButton>
|
||||
<UButton
|
||||
icon="i-heroicons-video-camera"
|
||||
color="neutral"
|
||||
:variant="matrixCallCameraEnabled ? 'soft' : 'outline'"
|
||||
:disabled="!matrixLiveKitRoom"
|
||||
@click="toggleMatrixCallCamera"
|
||||
>
|
||||
{{ matrixCallCameraEnabled ? "Kamera an" : "Kamera aus" }}
|
||||
</UButton>
|
||||
<UButton
|
||||
icon="i-heroicons-phone-x-mark"
|
||||
color="error"
|
||||
variant="soft"
|
||||
@click="closeMatrixCall"
|
||||
>
|
||||
Auflegen
|
||||
</UButton>
|
||||
<UButton
|
||||
icon="i-heroicons-x-mark"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
@click="matrixCallOpen = false"
|
||||
@click="closeMatrixCall"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
@@ -1001,16 +1155,68 @@ onBeforeUnmount(stopMatrixAutoRefresh)
|
||||
v-if="matrixCallLoading"
|
||||
class="flex h-full items-center justify-center text-sm text-muted"
|
||||
>
|
||||
Matrix wird geladen...
|
||||
Besprechung wird gestartet...
|
||||
</div>
|
||||
<iframe
|
||||
<div
|
||||
v-else-if="matrixCallError"
|
||||
class="flex h-full items-center justify-center p-6"
|
||||
>
|
||||
<UAlert
|
||||
icon="i-heroicons-exclamation-triangle"
|
||||
color="error"
|
||||
variant="soft"
|
||||
title="Besprechung konnte nicht gestartet werden"
|
||||
:description="matrixCallError"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="canStartMatrixCall"
|
||||
:key="`${activeRoomKey}-${matrixCallMode}`"
|
||||
:src="activeMatrixCallUrl"
|
||||
class="h-full w-full border-0"
|
||||
allow="camera; microphone; display-capture; clipboard-read; clipboard-write; fullscreen; autoplay"
|
||||
referrerpolicy="no-referrer"
|
||||
/>
|
||||
class="flex h-full flex-col"
|
||||
>
|
||||
<div class="flex shrink-0 items-center justify-between border-b border-default bg-default px-4 py-2 text-xs text-muted">
|
||||
<span>{{ matrixCallSession?.liveKitRoomName || activeRoom.key }}</span>
|
||||
<span>{{ matrixCallState }}</span>
|
||||
</div>
|
||||
<div class="grid min-h-0 flex-1 auto-rows-fr gap-3 overflow-y-auto p-3 sm:grid-cols-2 xl:grid-cols-3">
|
||||
<div
|
||||
v-for="tile in matrixCallTiles"
|
||||
:key="tile.id"
|
||||
class="relative min-h-52 overflow-hidden rounded-lg bg-inverted text-inverted"
|
||||
>
|
||||
<video
|
||||
v-if="tile.videoTrack"
|
||||
:ref="(element) => setMatrixCallVideoElement(tile.id, element)"
|
||||
class="h-full w-full object-cover"
|
||||
autoplay
|
||||
playsinline
|
||||
:muted="tile.local"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="flex h-full min-h-52 items-center justify-center bg-elevated text-highlighted"
|
||||
>
|
||||
<span class="flex size-16 items-center justify-center rounded-lg bg-primary/10 text-xl font-semibold text-primary">
|
||||
{{ (tile.name || tile.identity || "?").slice(0, 1).toUpperCase() }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="absolute inset-x-0 bottom-0 flex items-center justify-between bg-black/55 px-3 py-2 text-sm text-white">
|
||||
<span class="truncate">{{ tile.name }}</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<UIcon
|
||||
:name="tile.microphoneEnabled ? 'i-heroicons-microphone' : 'i-heroicons-microphone'"
|
||||
class="size-4"
|
||||
:class="tile.microphoneEnabled ? 'opacity-100' : 'opacity-35'"
|
||||
/>
|
||||
<UIcon
|
||||
:name="tile.cameraEnabled ? 'i-heroicons-video-camera' : 'i-heroicons-video-camera-slash'"
|
||||
class="size-4"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="matrixCallAudioContainer" class="hidden" />
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="flex h-full items-center justify-center p-6"
|
||||
@@ -1020,7 +1226,7 @@ onBeforeUnmount(stopMatrixAutoRefresh)
|
||||
color="warning"
|
||||
variant="soft"
|
||||
title="Besprechung nicht verfügbar"
|
||||
description="Der Matrix-Raum oder die Element-Integration ist noch nicht bereit."
|
||||
description="Der Matrix-Raum oder LiveKit ist noch nicht bereit."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user