KI-AGENT: FEDEO-Call-Erlebnis ausbauen
This commit is contained in:
@@ -24,6 +24,10 @@ const matrixCallTiles = ref([])
|
|||||||
const matrixCallMicEnabled = ref(true)
|
const matrixCallMicEnabled = ref(true)
|
||||||
const matrixCallCameraEnabled = ref(true)
|
const matrixCallCameraEnabled = ref(true)
|
||||||
const matrixCallAudioContainer = ref(null)
|
const matrixCallAudioContainer = ref(null)
|
||||||
|
const matrixCallScreenShareEnabled = ref(false)
|
||||||
|
const matrixCallStartedAt = ref(null)
|
||||||
|
const matrixCallDurationNow = ref(Date.now())
|
||||||
|
const matrixCallAnnouncementSent = ref(false)
|
||||||
const roomCreateForm = ref({
|
const roomCreateForm = ref({
|
||||||
name: "",
|
name: "",
|
||||||
key: "",
|
key: "",
|
||||||
@@ -39,6 +43,7 @@ const matrixMessageSending = ref(false)
|
|||||||
const matrixAutoRefreshActive = ref(false)
|
const matrixAutoRefreshActive = ref(false)
|
||||||
const lastUpdated = ref(null)
|
const lastUpdated = ref(null)
|
||||||
let matrixRefreshInterval = null
|
let matrixRefreshInterval = null
|
||||||
|
let matrixCallDurationInterval = null
|
||||||
let matrixMessagesRequestActive = false
|
let matrixMessagesRequestActive = false
|
||||||
let matrixMembersRequestActive = false
|
let matrixMembersRequestActive = false
|
||||||
let matrixLiveKitRoom = null
|
let matrixLiveKitRoom = null
|
||||||
@@ -75,6 +80,29 @@ const matrixCallTitle = computed(() =>
|
|||||||
matrixCallMode.value === "audio" ? "Audioanruf" : "Videokonferenz"
|
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(() =>
|
const roomCreateKeyPreview = computed(() =>
|
||||||
normalizeRoomKey(roomCreateForm.value.key || roomCreateForm.value.name)
|
normalizeRoomKey(roomCreateForm.value.key || roomCreateForm.value.name)
|
||||||
)
|
)
|
||||||
@@ -343,6 +371,11 @@ const syncRoomMembers = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const openMatrixCall = async (mode = "video") => {
|
const openMatrixCall = async (mode = "video") => {
|
||||||
|
if (matrixLiveKitRoom && matrixCallConnected.value) {
|
||||||
|
matrixCallOpen.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!canStartMatrixCall.value) {
|
if (!canStartMatrixCall.value) {
|
||||||
toast.add({
|
toast.add({
|
||||||
title: "Besprechung kann noch nicht gestartet werden",
|
title: "Besprechung kann noch nicht gestartet werden",
|
||||||
@@ -355,6 +388,7 @@ const openMatrixCall = async (mode = "video") => {
|
|||||||
matrixCallLoading.value = true
|
matrixCallLoading.value = true
|
||||||
matrixCallError.value = ""
|
matrixCallError.value = ""
|
||||||
matrixCallOpen.value = true
|
matrixCallOpen.value = true
|
||||||
|
matrixCallAnnouncementSent.value = false
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await leaveMatrixCall()
|
await leaveMatrixCall()
|
||||||
@@ -363,6 +397,7 @@ const openMatrixCall = async (mode = "video") => {
|
|||||||
})
|
})
|
||||||
matrixCallSession.value = session
|
matrixCallSession.value = session
|
||||||
await connectMatrixCall(session, { video: mode === "video" })
|
await connectMatrixCall(session, { video: mode === "video" })
|
||||||
|
await announceMatrixCallStarted(mode)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
matrixCallError.value = error?.data?.error || error?.message || "Verbindung fehlgeschlagen"
|
matrixCallError.value = error?.data?.error || error?.message || "Verbindung fehlgeschlagen"
|
||||||
toast.add({
|
toast.add({
|
||||||
@@ -377,12 +412,20 @@ const openMatrixCall = async (mode = "video") => {
|
|||||||
|
|
||||||
const getParticipantTile = (participant, local = false) => {
|
const getParticipantTile = (participant, local = false) => {
|
||||||
const publications = participant.getTrackPublications?.() || []
|
const publications = participant.getTrackPublications?.() || []
|
||||||
const videoPublication = publications.find((publication) =>
|
const screenSharePublication = publications.find((publication) =>
|
||||||
publication.kind === Track.Kind.Video && publication.track
|
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) =>
|
const audioPublication = publications.find((publication) =>
|
||||||
publication.kind === Track.Kind.Audio && publication.track
|
publication.kind === Track.Kind.Audio && publication.track
|
||||||
)
|
)
|
||||||
|
const videoPublication = screenSharePublication || cameraPublication
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: local ? "local" : participant.sid || participant.identity,
|
id: local ? "local" : participant.sid || participant.identity,
|
||||||
@@ -390,6 +433,7 @@ const getParticipantTile = (participant, local = false) => {
|
|||||||
name: local ? "Du" : participant.name || participant.identity,
|
name: local ? "Du" : participant.name || participant.identity,
|
||||||
local,
|
local,
|
||||||
speaking: participant.isSpeaking,
|
speaking: participant.isSpeaking,
|
||||||
|
screenSharing: Boolean(screenSharePublication?.track),
|
||||||
videoTrack: videoPublication?.track || null,
|
videoTrack: videoPublication?.track || null,
|
||||||
audioTrack: audioPublication?.track || null,
|
audioTrack: audioPublication?.track || null,
|
||||||
cameraEnabled: participant.isCameraEnabled,
|
cameraEnabled: participant.isCameraEnabled,
|
||||||
@@ -428,11 +472,54 @@ const refreshMatrixCallTiles = async () => {
|
|||||||
...Array.from(matrixLiveKitRoom.remoteParticipants.values()).map((participant) =>
|
...Array.from(matrixLiveKitRoom.remoteParticipants.values()).map((participant) =>
|
||||||
getParticipantTile(participant)
|
getParticipantTile(participant)
|
||||||
)
|
)
|
||||||
]
|
].sort((a, b) => Number(b.screenSharing) - Number(a.screenSharing))
|
||||||
|
|
||||||
await attachMatrixCallMedia()
|
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 connectMatrixCall = async (session, { video = true } = {}) => {
|
||||||
const room = new Room({
|
const room = new Room({
|
||||||
adaptiveStream: true,
|
adaptiveStream: true,
|
||||||
@@ -452,6 +539,7 @@ const connectMatrixCall = async (session, { video = true } = {}) => {
|
|||||||
RoomEvent.TrackMuted,
|
RoomEvent.TrackMuted,
|
||||||
RoomEvent.TrackUnmuted,
|
RoomEvent.TrackUnmuted,
|
||||||
RoomEvent.ActiveSpeakersChanged,
|
RoomEvent.ActiveSpeakersChanged,
|
||||||
|
RoomEvent.LocalTrackSubscribed,
|
||||||
]
|
]
|
||||||
|
|
||||||
for (const eventName of refreshEvents) {
|
for (const eventName of refreshEvents) {
|
||||||
@@ -461,11 +549,16 @@ const connectMatrixCall = async (session, { video = true } = {}) => {
|
|||||||
room.on(RoomEvent.ConnectionStateChanged, (state) => {
|
room.on(RoomEvent.ConnectionStateChanged, (state) => {
|
||||||
matrixCallState.value = state
|
matrixCallState.value = state
|
||||||
matrixCallConnected.value = state === ConnectionState.Connected
|
matrixCallConnected.value = state === ConnectionState.Connected
|
||||||
|
if (state === ConnectionState.Connected && !matrixCallStartedAt.value) {
|
||||||
|
startMatrixCallDurationTimer()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
room.on(RoomEvent.Disconnected, () => {
|
room.on(RoomEvent.Disconnected, () => {
|
||||||
matrixCallConnected.value = false
|
matrixCallConnected.value = false
|
||||||
matrixCallState.value = ConnectionState.Disconnected
|
matrixCallState.value = ConnectionState.Disconnected
|
||||||
|
matrixCallScreenShareEnabled.value = false
|
||||||
|
stopMatrixCallDurationTimer()
|
||||||
})
|
})
|
||||||
|
|
||||||
await room.connect(session.liveKitUrl, session.liveKitToken)
|
await room.connect(session.liveKitUrl, session.liveKitToken)
|
||||||
@@ -473,6 +566,10 @@ const connectMatrixCall = async (session, { video = true } = {}) => {
|
|||||||
await room.localParticipant.setCameraEnabled(video)
|
await room.localParticipant.setCameraEnabled(video)
|
||||||
matrixCallMicEnabled.value = true
|
matrixCallMicEnabled.value = true
|
||||||
matrixCallCameraEnabled.value = video
|
matrixCallCameraEnabled.value = video
|
||||||
|
matrixCallScreenShareEnabled.value = false
|
||||||
|
if (!matrixCallStartedAt.value) {
|
||||||
|
startMatrixCallDurationTimer()
|
||||||
|
}
|
||||||
await refreshMatrixCallTiles()
|
await refreshMatrixCallTiles()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -487,6 +584,9 @@ const leaveMatrixCall = async () => {
|
|||||||
matrixCallTiles.value = []
|
matrixCallTiles.value = []
|
||||||
matrixCallConnected.value = false
|
matrixCallConnected.value = false
|
||||||
matrixCallState.value = "disconnected"
|
matrixCallState.value = "disconnected"
|
||||||
|
matrixCallScreenShareEnabled.value = false
|
||||||
|
matrixCallAnnouncementSent.value = false
|
||||||
|
stopMatrixCallDurationTimer()
|
||||||
}
|
}
|
||||||
|
|
||||||
const closeMatrixCall = async () => {
|
const closeMatrixCall = async () => {
|
||||||
@@ -494,6 +594,10 @@ const closeMatrixCall = async () => {
|
|||||||
matrixCallOpen.value = false
|
matrixCallOpen.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const minimizeMatrixCall = () => {
|
||||||
|
matrixCallOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
const setMatrixCallVideoElement = (tileId, element) => {
|
const setMatrixCallVideoElement = (tileId, element) => {
|
||||||
if (element) {
|
if (element) {
|
||||||
matrixCallVideoElements.set(tileId, element)
|
matrixCallVideoElements.set(tileId, element)
|
||||||
@@ -522,6 +626,22 @@ const toggleMatrixCallCamera = async () => {
|
|||||||
await refreshMatrixCallTiles()
|
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 } = {}) => {
|
const loadRoomChat = async ({ silent = false, includeMembers = false } = {}) => {
|
||||||
await loadRoomMessages({ silent })
|
await loadRoomMessages({ silent })
|
||||||
|
|
||||||
@@ -625,6 +745,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
stopMatrixAutoRefresh()
|
stopMatrixAutoRefresh()
|
||||||
|
stopMatrixCallDurationTimer()
|
||||||
leaveMatrixCall()
|
leaveMatrixCall()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -929,6 +1050,40 @@ onBeforeUnmount(() => {
|
|||||||
</UAlert>
|
</UAlert>
|
||||||
</div>
|
</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
|
<div
|
||||||
ref="matrixMessagesViewport"
|
ref="matrixMessagesViewport"
|
||||||
class="min-h-0 flex-1 space-y-3 overflow-y-auto p-4 sm:p-5"
|
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" }}
|
{{ matrixCallCameraEnabled ? "Kamera an" : "Kamera aus" }}
|
||||||
</UButton>
|
</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
|
<UButton
|
||||||
icon="i-heroicons-phone-x-mark"
|
icon="i-heroicons-phone-x-mark"
|
||||||
color="error"
|
color="error"
|
||||||
@@ -1174,14 +1347,15 @@ onBeforeUnmount(() => {
|
|||||||
class="flex h-full flex-col"
|
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">
|
<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>{{ matrixCallParticipantCount }} Teilnehmer · {{ matrixCallDurationLabel }}</span>
|
||||||
<span>{{ matrixCallState }}</span>
|
<span>{{ matrixCallStateLabel }}</span>
|
||||||
</div>
|
</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 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
|
<div
|
||||||
v-for="tile in matrixCallTiles"
|
v-for="tile in matrixCallTiles"
|
||||||
:key="tile.id"
|
:key="tile.id"
|
||||||
class="relative min-h-52 overflow-hidden rounded-lg bg-inverted text-inverted"
|
class="relative min-h-52 overflow-hidden rounded-lg bg-inverted text-inverted"
|
||||||
|
:class="tile.speaking ? 'ring-2 ring-primary' : ''"
|
||||||
>
|
>
|
||||||
<video
|
<video
|
||||||
v-if="tile.videoTrack"
|
v-if="tile.videoTrack"
|
||||||
@@ -1200,7 +1374,10 @@ onBeforeUnmount(() => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</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">
|
<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">
|
<div class="flex items-center gap-2">
|
||||||
<UIcon
|
<UIcon
|
||||||
:name="tile.microphoneEnabled ? 'i-heroicons-microphone' : 'i-heroicons-microphone'"
|
:name="tile.microphoneEnabled ? 'i-heroicons-microphone' : 'i-heroicons-microphone'"
|
||||||
|
|||||||
Reference in New Issue
Block a user