KI-AGENT: LiveKit-Calls nativ in FEDEO integrieren
This commit is contained in:
119
frontend/package-lock.json
generated
119
frontend/package-lock.json
generated
@@ -74,6 +74,7 @@
|
||||
"image-js": "^1.1.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"license-checker": "^25.0.1",
|
||||
"livekit-client": "^2.19.0",
|
||||
"maplibre-gl": "^4.7.0",
|
||||
"nuxt-editorjs": "^1.0.4",
|
||||
"nuxt-viewport": "^2.0.6",
|
||||
@@ -1865,6 +1866,12 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@bufbuild/protobuf": {
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.10.1.tgz",
|
||||
"integrity": "sha512-wJ8ReQbHxsAfXhrf9ixl0aYbZorRuOWpBNzm8pL8ftmSxQx/wnJD5Eg861NwJU/czy2VXFIebCeZnZrI9rktIQ==",
|
||||
"license": "(Apache-2.0 AND BSD-3-Clause)"
|
||||
},
|
||||
"node_modules/@capacitor-community/bluetooth-le": {
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor-community/bluetooth-le/-/bluetooth-le-7.3.0.tgz",
|
||||
@@ -3159,6 +3166,21 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@livekit/mutex": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@livekit/mutex/-/mutex-1.1.1.tgz",
|
||||
"integrity": "sha512-EsshAucklmpuUAfkABPxJNhzj9v2sG7JuzFDL4ML1oJQSV14sqrpTYnsaOudMAw9yOaW53NU3QQTlUQoRs4czw==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@livekit/protocol": {
|
||||
"version": "1.45.8",
|
||||
"resolved": "https://registry.npmjs.org/@livekit/protocol/-/protocol-1.45.8.tgz",
|
||||
"integrity": "sha512-Q+l57E7w/xxOBFVWzdX5rkAZO7ffyF+rlDzNUYq2SU114+5aTyCq+PK4unaEVDNd4952Af7wteKr3sOgasGuaA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "^1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/geojson-rewind": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz",
|
||||
@@ -8696,6 +8718,13 @@
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/dom-mediacapture-record": {
|
||||
"version": "1.0.22",
|
||||
"resolved": "https://registry.npmjs.org/@types/dom-mediacapture-record/-/dom-mediacapture-record-1.0.22.tgz",
|
||||
"integrity": "sha512-mUMZLK3NvwRLcAAT9qmcK+9p7tpU2FHdDsntR3YI4+GY88XrgG4XiE7u1Q2LAN2/FZOz/tdMDC3GQCR4T8nFuw==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
@@ -12191,7 +12220,6 @@
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8.x"
|
||||
@@ -14209,6 +14237,15 @@
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/jose": {
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz",
|
||||
"integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/jpeg-js": {
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz",
|
||||
@@ -14784,6 +14821,26 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/livekit-client": {
|
||||
"version": "2.19.0",
|
||||
"resolved": "https://registry.npmjs.org/livekit-client/-/livekit-client-2.19.0.tgz",
|
||||
"integrity": "sha512-aolY1XDAtx0nHKBNm29W9OhzBnSz1CP5kq3phvRhFfi1NbvMXs8tcACjAkZTnIKgihkp+BiJScZZ3tZv0Gz8sA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@livekit/mutex": "1.1.1",
|
||||
"@livekit/protocol": "1.45.8",
|
||||
"events": "^3.3.0",
|
||||
"jose": "^6.1.0",
|
||||
"loglevel": "^1.9.2",
|
||||
"sdp-transform": "^2.15.0",
|
||||
"tslib": "2.8.1",
|
||||
"typed-emitter": "^2.1.0",
|
||||
"webrtc-adapter": "9.0.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/dom-mediacapture-record": "^1"
|
||||
}
|
||||
},
|
||||
"node_modules/local-pkg": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz",
|
||||
@@ -14858,6 +14915,19 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/loglevel": {
|
||||
"version": "1.9.2",
|
||||
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz",
|
||||
"integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/loglevel"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||
@@ -18452,6 +18522,16 @@
|
||||
"integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/rxjs": {
|
||||
"version": "7.8.2",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
||||
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-array-concat": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
|
||||
@@ -18609,6 +18689,21 @@
|
||||
"integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sdp": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.2.tgz",
|
||||
"integrity": "sha512-xZocWwfyp4hkbN4hLWxMjmv2Q8aNa9MhmOZ7L9aCZPT+dZsgRr6wZRrSYE3HTdyk/2pZKPSgqI7ns7Een1xMSA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sdp-transform": {
|
||||
"version": "2.15.0",
|
||||
"resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.15.0.tgz",
|
||||
"integrity": "sha512-KrOH82c/W+GYQ0LHqtr3caRpM3ITglq3ljGUIb8LTki7ByacJZ9z+piSGiwZDsRyhQbYBOBJgr2k6X4BZXi3Kw==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"sdp-verify": "checker.js"
|
||||
}
|
||||
},
|
||||
"node_modules/secure-json-parse": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz",
|
||||
@@ -20164,6 +20259,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/typed-emitter": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/typed-emitter/-/typed-emitter-2.1.0.tgz",
|
||||
"integrity": "sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA==",
|
||||
"license": "MIT",
|
||||
"optionalDependencies": {
|
||||
"rxjs": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
@@ -21718,6 +21822,19 @@
|
||||
"integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/webrtc-adapter": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-9.0.5.tgz",
|
||||
"integrity": "sha512-U9vjByy/sK2OMXu5mmfuZFKTMIUQe34c0JXRO+oDrxJTsntdYT2iIFwYMOV7HhMTuktcZLGf2W1N/OcSf9ssWg==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"sdp": "^3.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0",
|
||||
"npm": ">=3.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
|
||||
@@ -87,6 +87,7 @@
|
||||
"image-js": "^1.1.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"license-checker": "^25.0.1",
|
||||
"livekit-client": "^2.19.0",
|
||||
"maplibre-gl": "^4.7.0",
|
||||
"nuxt-editorjs": "^1.0.4",
|
||||
"nuxt-viewport": "^2.0.6",
|
||||
|
||||
@@ -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