KI-AGENT: FEDEO-Call-Erlebnis ausbauen
This commit is contained in:
@@ -24,6 +24,10 @@ const matrixCallTiles = ref([])
|
||||
const matrixCallMicEnabled = ref(true)
|
||||
const matrixCallCameraEnabled = ref(true)
|
||||
const matrixCallAudioContainer = ref(null)
|
||||
const matrixCallScreenShareEnabled = ref(false)
|
||||
const matrixCallStartedAt = ref(null)
|
||||
const matrixCallDurationNow = ref(Date.now())
|
||||
const matrixCallAnnouncementSent = ref(false)
|
||||
const roomCreateForm = ref({
|
||||
name: "",
|
||||
key: "",
|
||||
@@ -39,6 +43,7 @@ const matrixMessageSending = ref(false)
|
||||
const matrixAutoRefreshActive = ref(false)
|
||||
const lastUpdated = ref(null)
|
||||
let matrixRefreshInterval = null
|
||||
let matrixCallDurationInterval = null
|
||||
let matrixMessagesRequestActive = false
|
||||
let matrixMembersRequestActive = false
|
||||
let matrixLiveKitRoom = null
|
||||
@@ -75,6 +80,29 @@ const matrixCallTitle = computed(() =>
|
||||
matrixCallMode.value === "audio" ? "Audioanruf" : "Videokonferenz"
|
||||
)
|
||||
|
||||
const matrixCallStateLabel = computed(() => {
|
||||
const labels = {
|
||||
connected: "Verbunden",
|
||||
connecting: "Verbindet",
|
||||
reconnecting: "Verbindung wird wiederhergestellt",
|
||||
disconnected: "Getrennt"
|
||||
}
|
||||
|
||||
return labels[String(matrixCallState.value).toLowerCase()] || matrixCallState.value
|
||||
})
|
||||
|
||||
const matrixCallParticipantCount = computed(() => matrixCallTiles.value.length)
|
||||
|
||||
const matrixCallDurationLabel = computed(() => {
|
||||
if (!matrixCallStartedAt.value) return "00:00"
|
||||
|
||||
const durationSeconds = Math.max(0, Math.floor((matrixCallDurationNow.value - matrixCallStartedAt.value) / 1000))
|
||||
const minutes = String(Math.floor(durationSeconds / 60)).padStart(2, "0")
|
||||
const seconds = String(durationSeconds % 60).padStart(2, "0")
|
||||
|
||||
return `${minutes}:${seconds}`
|
||||
})
|
||||
|
||||
const roomCreateKeyPreview = computed(() =>
|
||||
normalizeRoomKey(roomCreateForm.value.key || roomCreateForm.value.name)
|
||||
)
|
||||
@@ -343,6 +371,11 @@ const syncRoomMembers = async () => {
|
||||
}
|
||||
|
||||
const openMatrixCall = async (mode = "video") => {
|
||||
if (matrixLiveKitRoom && matrixCallConnected.value) {
|
||||
matrixCallOpen.value = true
|
||||
return
|
||||
}
|
||||
|
||||
if (!canStartMatrixCall.value) {
|
||||
toast.add({
|
||||
title: "Besprechung kann noch nicht gestartet werden",
|
||||
@@ -355,6 +388,7 @@ const openMatrixCall = async (mode = "video") => {
|
||||
matrixCallLoading.value = true
|
||||
matrixCallError.value = ""
|
||||
matrixCallOpen.value = true
|
||||
matrixCallAnnouncementSent.value = false
|
||||
|
||||
try {
|
||||
await leaveMatrixCall()
|
||||
@@ -363,6 +397,7 @@ const openMatrixCall = async (mode = "video") => {
|
||||
})
|
||||
matrixCallSession.value = session
|
||||
await connectMatrixCall(session, { video: mode === "video" })
|
||||
await announceMatrixCallStarted(mode)
|
||||
} catch (error) {
|
||||
matrixCallError.value = error?.data?.error || error?.message || "Verbindung fehlgeschlagen"
|
||||
toast.add({
|
||||
@@ -377,12 +412,20 @@ const openMatrixCall = async (mode = "video") => {
|
||||
|
||||
const getParticipantTile = (participant, local = false) => {
|
||||
const publications = participant.getTrackPublications?.() || []
|
||||
const videoPublication = publications.find((publication) =>
|
||||
publication.kind === Track.Kind.Video && publication.track
|
||||
const screenSharePublication = publications.find((publication) =>
|
||||
publication.kind === Track.Kind.Video &&
|
||||
publication.source === Track.Source.ScreenShare &&
|
||||
publication.track
|
||||
)
|
||||
const cameraPublication = publications.find((publication) =>
|
||||
publication.kind === Track.Kind.Video &&
|
||||
publication.source !== Track.Source.ScreenShare &&
|
||||
publication.track
|
||||
)
|
||||
const audioPublication = publications.find((publication) =>
|
||||
publication.kind === Track.Kind.Audio && publication.track
|
||||
)
|
||||
const videoPublication = screenSharePublication || cameraPublication
|
||||
|
||||
return {
|
||||
id: local ? "local" : participant.sid || participant.identity,
|
||||
@@ -390,6 +433,7 @@ const getParticipantTile = (participant, local = false) => {
|
||||
name: local ? "Du" : participant.name || participant.identity,
|
||||
local,
|
||||
speaking: participant.isSpeaking,
|
||||
screenSharing: Boolean(screenSharePublication?.track),
|
||||
videoTrack: videoPublication?.track || null,
|
||||
audioTrack: audioPublication?.track || null,
|
||||
cameraEnabled: participant.isCameraEnabled,
|
||||
@@ -428,11 +472,54 @@ const refreshMatrixCallTiles = async () => {
|
||||
...Array.from(matrixLiveKitRoom.remoteParticipants.values()).map((participant) =>
|
||||
getParticipantTile(participant)
|
||||
)
|
||||
]
|
||||
].sort((a, b) => Number(b.screenSharing) - Number(a.screenSharing))
|
||||
|
||||
await attachMatrixCallMedia()
|
||||
}
|
||||
|
||||
const startMatrixCallDurationTimer = () => {
|
||||
matrixCallStartedAt.value = Date.now()
|
||||
matrixCallDurationNow.value = Date.now()
|
||||
|
||||
if (matrixCallDurationInterval) {
|
||||
window.clearInterval(matrixCallDurationInterval)
|
||||
}
|
||||
|
||||
matrixCallDurationInterval = window.setInterval(() => {
|
||||
matrixCallDurationNow.value = Date.now()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const stopMatrixCallDurationTimer = () => {
|
||||
if (matrixCallDurationInterval) {
|
||||
window.clearInterval(matrixCallDurationInterval)
|
||||
matrixCallDurationInterval = null
|
||||
}
|
||||
|
||||
matrixCallStartedAt.value = null
|
||||
matrixCallDurationNow.value = Date.now()
|
||||
}
|
||||
|
||||
const announceMatrixCallStarted = async (mode) => {
|
||||
if (matrixCallAnnouncementSent.value || !activeRoom.value?.exists) return
|
||||
|
||||
matrixCallAnnouncementSent.value = true
|
||||
|
||||
try {
|
||||
const message = await $api(`${activeRoomEndpoint.value}/messages`, {
|
||||
method: "POST",
|
||||
body: {
|
||||
text: `${mode === "audio" ? "Audioanruf" : "Videokonferenz"} gestartet.`
|
||||
}
|
||||
})
|
||||
|
||||
mergeMatrixMessages([message])
|
||||
await scrollMessagesToBottom()
|
||||
} catch (error) {
|
||||
matrixCallAnnouncementSent.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const connectMatrixCall = async (session, { video = true } = {}) => {
|
||||
const room = new Room({
|
||||
adaptiveStream: true,
|
||||
@@ -452,6 +539,7 @@ const connectMatrixCall = async (session, { video = true } = {}) => {
|
||||
RoomEvent.TrackMuted,
|
||||
RoomEvent.TrackUnmuted,
|
||||
RoomEvent.ActiveSpeakersChanged,
|
||||
RoomEvent.LocalTrackSubscribed,
|
||||
]
|
||||
|
||||
for (const eventName of refreshEvents) {
|
||||
@@ -461,11 +549,16 @@ const connectMatrixCall = async (session, { video = true } = {}) => {
|
||||
room.on(RoomEvent.ConnectionStateChanged, (state) => {
|
||||
matrixCallState.value = state
|
||||
matrixCallConnected.value = state === ConnectionState.Connected
|
||||
if (state === ConnectionState.Connected && !matrixCallStartedAt.value) {
|
||||
startMatrixCallDurationTimer()
|
||||
}
|
||||
})
|
||||
|
||||
room.on(RoomEvent.Disconnected, () => {
|
||||
matrixCallConnected.value = false
|
||||
matrixCallState.value = ConnectionState.Disconnected
|
||||
matrixCallScreenShareEnabled.value = false
|
||||
stopMatrixCallDurationTimer()
|
||||
})
|
||||
|
||||
await room.connect(session.liveKitUrl, session.liveKitToken)
|
||||
@@ -473,6 +566,10 @@ const connectMatrixCall = async (session, { video = true } = {}) => {
|
||||
await room.localParticipant.setCameraEnabled(video)
|
||||
matrixCallMicEnabled.value = true
|
||||
matrixCallCameraEnabled.value = video
|
||||
matrixCallScreenShareEnabled.value = false
|
||||
if (!matrixCallStartedAt.value) {
|
||||
startMatrixCallDurationTimer()
|
||||
}
|
||||
await refreshMatrixCallTiles()
|
||||
}
|
||||
|
||||
@@ -487,6 +584,9 @@ const leaveMatrixCall = async () => {
|
||||
matrixCallTiles.value = []
|
||||
matrixCallConnected.value = false
|
||||
matrixCallState.value = "disconnected"
|
||||
matrixCallScreenShareEnabled.value = false
|
||||
matrixCallAnnouncementSent.value = false
|
||||
stopMatrixCallDurationTimer()
|
||||
}
|
||||
|
||||
const closeMatrixCall = async () => {
|
||||
@@ -494,6 +594,10 @@ const closeMatrixCall = async () => {
|
||||
matrixCallOpen.value = false
|
||||
}
|
||||
|
||||
const minimizeMatrixCall = () => {
|
||||
matrixCallOpen.value = false
|
||||
}
|
||||
|
||||
const setMatrixCallVideoElement = (tileId, element) => {
|
||||
if (element) {
|
||||
matrixCallVideoElements.set(tileId, element)
|
||||
@@ -522,6 +626,22 @@ const toggleMatrixCallCamera = async () => {
|
||||
await refreshMatrixCallTiles()
|
||||
}
|
||||
|
||||
const toggleMatrixCallScreenShare = async () => {
|
||||
if (!matrixLiveKitRoom) return
|
||||
|
||||
const enabled = !matrixCallScreenShareEnabled.value
|
||||
try {
|
||||
await matrixLiveKitRoom.localParticipant.setScreenShareEnabled(enabled)
|
||||
matrixCallScreenShareEnabled.value = enabled
|
||||
await refreshMatrixCallTiles()
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
title: "Bildschirmfreigabe konnte nicht geändert werden",
|
||||
color: "error"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const loadRoomChat = async ({ silent = false, includeMembers = false } = {}) => {
|
||||
await loadRoomMessages({ silent })
|
||||
|
||||
@@ -625,6 +745,7 @@ onMounted(async () => {
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopMatrixAutoRefresh()
|
||||
stopMatrixCallDurationTimer()
|
||||
leaveMatrixCall()
|
||||
})
|
||||
</script>
|
||||
@@ -929,6 +1050,40 @@ onBeforeUnmount(() => {
|
||||
</UAlert>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="matrixLiveKitRoom && matrixCallConnected"
|
||||
class="flex shrink-0 flex-wrap items-center justify-between gap-3 border-b border-primary/20 bg-primary/10 px-4 py-3"
|
||||
>
|
||||
<div class="min-w-0">
|
||||
<p class="truncate text-sm font-medium text-highlighted">
|
||||
{{ matrixCallTitle }} läuft in {{ activeRoom.name }}
|
||||
</p>
|
||||
<p class="truncate text-xs text-muted">
|
||||
{{ matrixCallParticipantCount }} Teilnehmer · {{ matrixCallDurationLabel }} · {{ matrixCallStateLabel }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<UButton
|
||||
icon="i-heroicons-arrow-top-right-on-square"
|
||||
color="primary"
|
||||
variant="soft"
|
||||
size="sm"
|
||||
@click="matrixCallOpen = true"
|
||||
>
|
||||
Öffnen
|
||||
</UButton>
|
||||
<UButton
|
||||
icon="i-heroicons-phone-x-mark"
|
||||
color="error"
|
||||
variant="soft"
|
||||
size="sm"
|
||||
@click="closeMatrixCall"
|
||||
>
|
||||
Auflegen
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref="matrixMessagesViewport"
|
||||
class="min-h-0 flex-1 space-y-3 overflow-y-auto p-4 sm:p-5"
|
||||
@@ -1133,6 +1288,24 @@ onBeforeUnmount(() => {
|
||||
>
|
||||
{{ matrixCallCameraEnabled ? "Kamera an" : "Kamera aus" }}
|
||||
</UButton>
|
||||
<UButton
|
||||
icon="i-heroicons-computer-desktop"
|
||||
color="neutral"
|
||||
:variant="matrixCallScreenShareEnabled ? 'soft' : 'outline'"
|
||||
:disabled="!matrixLiveKitRoom"
|
||||
@click="toggleMatrixCallScreenShare"
|
||||
>
|
||||
{{ matrixCallScreenShareEnabled ? "Freigabe an" : "Freigeben" }}
|
||||
</UButton>
|
||||
<UButton
|
||||
icon="i-heroicons-arrows-pointing-in"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
:disabled="!matrixLiveKitRoom"
|
||||
@click="minimizeMatrixCall"
|
||||
>
|
||||
Minimieren
|
||||
</UButton>
|
||||
<UButton
|
||||
icon="i-heroicons-phone-x-mark"
|
||||
color="error"
|
||||
@@ -1174,14 +1347,15 @@ onBeforeUnmount(() => {
|
||||
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>
|
||||
<span>{{ matrixCallParticipantCount }} Teilnehmer · {{ matrixCallDurationLabel }}</span>
|
||||
<span>{{ matrixCallStateLabel }}</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"
|
||||
:class="tile.speaking ? 'ring-2 ring-primary' : ''"
|
||||
>
|
||||
<video
|
||||
v-if="tile.videoTrack"
|
||||
@@ -1200,7 +1374,10 @@ onBeforeUnmount(() => {
|
||||
</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>
|
||||
<span class="truncate">
|
||||
{{ tile.name }}
|
||||
<span v-if="tile.screenSharing" class="ml-1 text-xs opacity-80">teilt Bildschirm</span>
|
||||
</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<UIcon
|
||||
:name="tile.microphoneEnabled ? 'i-heroicons-microphone' : 'i-heroicons-microphone'"
|
||||
|
||||
Reference in New Issue
Block a user