KI-AGENT: LiveKit-Calls nativ in FEDEO integrieren
This commit is contained in:
@@ -5,6 +5,7 @@ import { FastifyInstance } from "fastify"
|
|||||||
import { authProfiles, authTenantUsers, authUsers, communicationRooms, tenants } from "../../db/schema"
|
import { authProfiles, authTenantUsers, authUsers, communicationRooms, tenants } from "../../db/schema"
|
||||||
import { and, eq } from "drizzle-orm"
|
import { and, eq } from "drizzle-orm"
|
||||||
import { secrets } from "../utils/secrets"
|
import { secrets } from "../utils/secrets"
|
||||||
|
import jwt from "jsonwebtoken"
|
||||||
|
|
||||||
type MatrixErrorResponse = {
|
type MatrixErrorResponse = {
|
||||||
errcode?: string
|
errcode?: string
|
||||||
@@ -40,6 +41,13 @@ type MatrixLoginTokenResponse = {
|
|||||||
expires_in_ms: number
|
expires_in_ms: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LiveKitGrant = {
|
||||||
|
roomJoin: boolean
|
||||||
|
room: string
|
||||||
|
canPublish: boolean
|
||||||
|
canSubscribe: boolean
|
||||||
|
}
|
||||||
|
|
||||||
type MatrixTenantRoomOptions = {
|
type MatrixTenantRoomOptions = {
|
||||||
key?: string
|
key?: string
|
||||||
name?: string
|
name?: string
|
||||||
@@ -153,6 +161,16 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
? `wss://${rtcHost()}/livekit/sfu`
|
? `wss://${rtcHost()}/livekit/sfu`
|
||||||
: `ws://localhost:${process.env.MATRIX_DEV_LIVEKIT_PORT || "7880"}`)
|
: `ws://localhost:${process.env.MATRIX_DEV_LIVEKIT_PORT || "7880"}`)
|
||||||
|
|
||||||
|
const livekitKey = () =>
|
||||||
|
process.env.LIVEKIT_KEY ||
|
||||||
|
secrets.LIVEKIT_KEY ||
|
||||||
|
(process.env.NODE_ENV === "production" ? "" : "devkey")
|
||||||
|
|
||||||
|
const livekitSecret = () =>
|
||||||
|
process.env.LIVEKIT_SECRET ||
|
||||||
|
secrets.LIVEKIT_SECRET ||
|
||||||
|
(process.env.NODE_ENV === "production" ? "" : "devsecret-local-matrix-stack-32-chars")
|
||||||
|
|
||||||
const serviceUserLocalpart = () =>
|
const serviceUserLocalpart = () =>
|
||||||
process.env.MATRIX_SERVICE_USER_LOCALPART ||
|
process.env.MATRIX_SERVICE_USER_LOCALPART ||
|
||||||
secrets.MATRIX_SERVICE_USER_LOCALPART ||
|
secrets.MATRIX_SERVICE_USER_LOCALPART ||
|
||||||
@@ -1185,6 +1203,62 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const createLiveKitRoomSession = async (
|
||||||
|
userId: string,
|
||||||
|
tenantId: number | null,
|
||||||
|
options: MatrixTenantRoomOptions = {}
|
||||||
|
) => {
|
||||||
|
if (!livekitKey() || !livekitSecret()) {
|
||||||
|
throw Object.assign(
|
||||||
|
new Error("LIVEKIT_KEY and LIVEKIT_SECRET are not configured"),
|
||||||
|
{ statusCode: 503 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const room = await provisionTenantRoom(userId, tenantId, options)
|
||||||
|
const session = await ensureCurrentUserJoinedRoom(userId, tenantId, {
|
||||||
|
roomId: room.roomId,
|
||||||
|
alias: room.alias,
|
||||||
|
})
|
||||||
|
const displayName = await getCurrentUserDisplayName(userId, tenantId)
|
||||||
|
const liveKitRoomName = `fedeo-${tenantId || "global"}-${room.key}`.replace(/[^a-zA-Z0-9_-]/g, "_")
|
||||||
|
const now = Math.floor(Date.now() / 1000)
|
||||||
|
const expiresInSeconds = 60 * 60
|
||||||
|
const video: LiveKitGrant = {
|
||||||
|
roomJoin: true,
|
||||||
|
room: liveKitRoomName,
|
||||||
|
canPublish: true,
|
||||||
|
canSubscribe: true,
|
||||||
|
}
|
||||||
|
const token = jwt.sign(
|
||||||
|
{
|
||||||
|
sub: session.matrixUserId,
|
||||||
|
name: displayName,
|
||||||
|
video,
|
||||||
|
nbf: now - 10,
|
||||||
|
exp: now + expiresInSeconds,
|
||||||
|
},
|
||||||
|
livekitSecret(),
|
||||||
|
{
|
||||||
|
algorithm: "HS256",
|
||||||
|
issuer: livekitKey(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
roomId: room.roomId,
|
||||||
|
alias: room.alias,
|
||||||
|
key: room.key,
|
||||||
|
name: room.name,
|
||||||
|
matrixUserId: session.matrixUserId,
|
||||||
|
displayName,
|
||||||
|
liveKitUrl: livekitUrl(),
|
||||||
|
liveKitRoomName,
|
||||||
|
liveKitToken: token,
|
||||||
|
expiresInMs: expiresInSeconds * 1000,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const listTenantCommunicationUsers = async (tenantId: number | null) => {
|
const listTenantCommunicationUsers = async (tenantId: number | null) => {
|
||||||
const tenant = await getCurrentTenant(tenantId)
|
const tenant = await getCurrentTenant(tenantId)
|
||||||
const rows = await server.db
|
const rows = await server.db
|
||||||
@@ -1342,6 +1416,7 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
getTenantRoomMembers,
|
getTenantRoomMembers,
|
||||||
sendTenantRoomMessage,
|
sendTenantRoomMessage,
|
||||||
createElementRoomSession,
|
createElementRoomSession,
|
||||||
|
createLiveKitRoomSession,
|
||||||
syncTenantRoomMembers,
|
syncTenantRoomMembers,
|
||||||
getGeneralRoomMessages,
|
getGeneralRoomMessages,
|
||||||
getGeneralRoomMembers,
|
getGeneralRoomMembers,
|
||||||
|
|||||||
@@ -136,6 +136,17 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
server.post("/communication/matrix/rooms/general/call-session", async (req, reply) => {
|
||||||
|
try {
|
||||||
|
return await matrix.createLiveKitRoomSession(req.user.user_id, req.user.tenant_id, {
|
||||||
|
key: "allgemein",
|
||||||
|
name: "Allgemeiner Chat",
|
||||||
|
})
|
||||||
|
} catch (err: any) {
|
||||||
|
return handleMatrixError(req, reply, err, "Matrix call session failed")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
server.post("/communication/matrix/rooms/general/members/sync", async (req, reply) => {
|
server.post("/communication/matrix/rooms/general/members/sync", async (req, reply) => {
|
||||||
try {
|
try {
|
||||||
return await matrix.syncTenantRoomMembers(req.user.user_id, req.user.tenant_id, {
|
return await matrix.syncTenantRoomMembers(req.user.user_id, req.user.tenant_id, {
|
||||||
@@ -213,6 +224,18 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
server.post("/communication/matrix/rooms/:roomKey/call-session", async (req, reply) => {
|
||||||
|
try {
|
||||||
|
return await matrix.createLiveKitRoomSession(
|
||||||
|
req.user.user_id,
|
||||||
|
req.user.tenant_id,
|
||||||
|
roomOptionsFromRequest(req)
|
||||||
|
)
|
||||||
|
} catch (err: any) {
|
||||||
|
return handleMatrixError(req, reply, err, "Matrix call session failed")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
server.post("/communication/matrix/rooms/:roomKey/members/sync", async (req, reply) => {
|
server.post("/communication/matrix/rooms/:roomKey/members/sync", async (req, reply) => {
|
||||||
try {
|
try {
|
||||||
return await matrix.syncTenantRoomMembers(
|
return await matrix.syncTenantRoomMembers(
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ export let secrets = {
|
|||||||
MATRIX_LIVEKIT_URL?: string
|
MATRIX_LIVEKIT_URL?: string
|
||||||
MATRIX_REGISTRATION_SHARED_SECRET?: string
|
MATRIX_REGISTRATION_SHARED_SECRET?: string
|
||||||
MATRIX_SERVICE_USER_LOCALPART?: string
|
MATRIX_SERVICE_USER_LOCALPART?: string
|
||||||
|
LIVEKIT_KEY?: string
|
||||||
|
LIVEKIT_SECRET?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const secretKeys = [
|
const secretKeys = [
|
||||||
@@ -84,6 +86,8 @@ const secretKeys = [
|
|||||||
"MATRIX_LIVEKIT_URL",
|
"MATRIX_LIVEKIT_URL",
|
||||||
"MATRIX_REGISTRATION_SHARED_SECRET",
|
"MATRIX_REGISTRATION_SHARED_SECRET",
|
||||||
"MATRIX_SERVICE_USER_LOCALPART",
|
"MATRIX_SERVICE_USER_LOCALPART",
|
||||||
|
"LIVEKIT_KEY",
|
||||||
|
"LIVEKIT_SECRET",
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
const numberKeys = new Set(["PORT", "MAILER_SMTP_PORT", "DOKUBOX_IMAP_PORT"])
|
const numberKeys = new Set(["PORT", "MAILER_SMTP_PORT", "DOKUBOX_IMAP_PORT"])
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ services:
|
|||||||
keys:
|
keys:
|
||||||
${LIVEKIT_KEY:-fedeo-livekit}: ${LIVEKIT_SECRET:-change-this-livekit-secret-please-replace}
|
${LIVEKIT_KEY:-fedeo-livekit}: ${LIVEKIT_SECRET:-change-this-livekit-secret-please-replace}
|
||||||
room:
|
room:
|
||||||
auto_create: false
|
auto_create: true
|
||||||
EOF
|
EOF
|
||||||
exec /livekit-server --config /tmp/livekit.yaml
|
exec /livekit-server --config /tmp/livekit.yaml
|
||||||
ports:
|
ports:
|
||||||
@@ -334,7 +334,7 @@ services:
|
|||||||
keys:
|
keys:
|
||||||
devkey: devsecret-local-matrix-stack-32-chars
|
devkey: devsecret-local-matrix-stack-32-chars
|
||||||
room:
|
room:
|
||||||
auto_create: false
|
auto_create: true
|
||||||
EOF
|
EOF
|
||||||
exec /livekit-server --config /tmp/livekit.yaml
|
exec /livekit-server --config /tmp/livekit.yaml
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
119
frontend/package-lock.json
generated
119
frontend/package-lock.json
generated
@@ -74,6 +74,7 @@
|
|||||||
"image-js": "^1.1.0",
|
"image-js": "^1.1.0",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"license-checker": "^25.0.1",
|
"license-checker": "^25.0.1",
|
||||||
|
"livekit-client": "^2.19.0",
|
||||||
"maplibre-gl": "^4.7.0",
|
"maplibre-gl": "^4.7.0",
|
||||||
"nuxt-editorjs": "^1.0.4",
|
"nuxt-editorjs": "^1.0.4",
|
||||||
"nuxt-viewport": "^2.0.6",
|
"nuxt-viewport": "^2.0.6",
|
||||||
@@ -1865,6 +1866,12 @@
|
|||||||
"node": ">=6.9.0"
|
"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": {
|
"node_modules/@capacitor-community/bluetooth-le": {
|
||||||
"version": "7.3.0",
|
"version": "7.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@capacitor-community/bluetooth-le/-/bluetooth-le-7.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@capacitor-community/bluetooth-le/-/bluetooth-le-7.3.0.tgz",
|
||||||
@@ -3159,6 +3166,21 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@mapbox/geojson-rewind": {
|
||||||
"version": "0.5.2",
|
"version": "0.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz",
|
||||||
@@ -8696,6 +8718,13 @@
|
|||||||
"tslib": "^2.4.0"
|
"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": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||||
@@ -12191,7 +12220,6 @@
|
|||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||||
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.8.x"
|
"node": ">=0.8.x"
|
||||||
@@ -14209,6 +14237,15 @@
|
|||||||
"jiti": "lib/jiti-cli.mjs"
|
"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": {
|
"node_modules/jpeg-js": {
|
||||||
"version": "0.4.4",
|
"version": "0.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz",
|
||||||
@@ -14784,6 +14821,26 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/local-pkg": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz",
|
||||||
@@ -14858,6 +14915,19 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/lru-cache": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||||
@@ -18452,6 +18522,16 @@
|
|||||||
"integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
|
"integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
|
||||||
"license": "BSD-3-Clause"
|
"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": {
|
"node_modules/safe-array-concat": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
|
"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==",
|
"integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/secure-json-parse": {
|
||||||
"version": "2.7.0",
|
"version": "2.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz",
|
"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"
|
"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": {
|
"node_modules/typescript": {
|
||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
@@ -21718,6 +21822,19 @@
|
|||||||
"integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
|
"integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/whatwg-url": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||||
|
|||||||
@@ -87,6 +87,7 @@
|
|||||||
"image-js": "^1.1.0",
|
"image-js": "^1.1.0",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"license-checker": "^25.0.1",
|
"license-checker": "^25.0.1",
|
||||||
|
"livekit-client": "^2.19.0",
|
||||||
"maplibre-gl": "^4.7.0",
|
"maplibre-gl": "^4.7.0",
|
||||||
"nuxt-editorjs": "^1.0.4",
|
"nuxt-editorjs": "^1.0.4",
|
||||||
"nuxt-viewport": "^2.0.6",
|
"nuxt-viewport": "^2.0.6",
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { ConnectionState, Room, RoomEvent, Track } from "livekit-client"
|
||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const { $api } = useNuxtApp()
|
const { $api } = useNuxtApp()
|
||||||
const runtimeConfig = useRuntimeConfig()
|
|
||||||
|
|
||||||
const status = ref(null)
|
const status = ref(null)
|
||||||
const identity = ref(null)
|
const identity = ref(null)
|
||||||
@@ -15,7 +16,14 @@ const roomCreateOpen = ref(false)
|
|||||||
const matrixCallOpen = ref(false)
|
const matrixCallOpen = ref(false)
|
||||||
const matrixCallMode = ref("video")
|
const matrixCallMode = ref("video")
|
||||||
const matrixCallLoading = ref(false)
|
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({
|
const roomCreateForm = ref({
|
||||||
name: "",
|
name: "",
|
||||||
key: "",
|
key: "",
|
||||||
@@ -33,6 +41,8 @@ const lastUpdated = ref(null)
|
|||||||
let matrixRefreshInterval = null
|
let matrixRefreshInterval = null
|
||||||
let matrixMessagesRequestActive = false
|
let matrixMessagesRequestActive = false
|
||||||
let matrixMembersRequestActive = false
|
let matrixMembersRequestActive = false
|
||||||
|
let matrixLiveKitRoom = null
|
||||||
|
const matrixCallVideoElements = new Map()
|
||||||
|
|
||||||
const canUseMatrixChat = computed(() =>
|
const canUseMatrixChat = computed(() =>
|
||||||
Boolean(status.value?.reachable && status.value?.provisioningConfigured)
|
Boolean(status.value?.reachable && status.value?.provisioningConfigured)
|
||||||
@@ -53,32 +63,18 @@ const activeRoomEndpoint = computed(() =>
|
|||||||
`/api/communication/matrix/rooms/${encodeURIComponent(activeRoomKey.value)}`
|
`/api/communication/matrix/rooms/${encodeURIComponent(activeRoomKey.value)}`
|
||||||
)
|
)
|
||||||
|
|
||||||
const matrixElementUrl = computed(() =>
|
|
||||||
String(runtimeConfig.public?.matrixElementUrl || "").replace(/\/+$/, "")
|
|
||||||
)
|
|
||||||
|
|
||||||
const activeRoomMatrixAddress = computed(() =>
|
const activeRoomMatrixAddress = computed(() =>
|
||||||
activeRoom.value?.roomId || activeRoom.value?.alias || ""
|
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(() =>
|
const canStartMatrixCall = computed(() =>
|
||||||
Boolean(canUseMatrixChat.value && activeRoom.value?.exists && activeRoomElementUrl.value)
|
Boolean(canUseMatrixChat.value && activeRoom.value?.exists)
|
||||||
)
|
)
|
||||||
|
|
||||||
const matrixCallTitle = computed(() =>
|
const matrixCallTitle = computed(() =>
|
||||||
matrixCallMode.value === "audio" ? "Audioanruf" : "Videokonferenz"
|
matrixCallMode.value === "audio" ? "Audioanruf" : "Videokonferenz"
|
||||||
)
|
)
|
||||||
|
|
||||||
const activeMatrixCallUrl = computed(() =>
|
|
||||||
matrixCallUrl.value || activeRoomElementUrl.value
|
|
||||||
)
|
|
||||||
|
|
||||||
const roomCreateKeyPreview = computed(() =>
|
const roomCreateKeyPreview = computed(() =>
|
||||||
normalizeRoomKey(roomCreateForm.value.key || roomCreateForm.value.name)
|
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") => {
|
const openMatrixCall = async (mode = "video") => {
|
||||||
if (!canStartMatrixCall.value) {
|
if (!canStartMatrixCall.value) {
|
||||||
toast.add({
|
toast.add({
|
||||||
@@ -368,25 +353,175 @@ const openMatrixCall = async (mode = "video") => {
|
|||||||
|
|
||||||
matrixCallMode.value = mode
|
matrixCallMode.value = mode
|
||||||
matrixCallLoading.value = true
|
matrixCallLoading.value = true
|
||||||
|
matrixCallError.value = ""
|
||||||
matrixCallOpen.value = true
|
matrixCallOpen.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const session = await $api(`${activeRoomEndpoint.value}/session`, {
|
await leaveMatrixCall()
|
||||||
|
const session = await $api(`${activeRoomEndpoint.value}/call-session`, {
|
||||||
method: "POST"
|
method: "POST"
|
||||||
})
|
})
|
||||||
matrixCallUrl.value = buildElementRoomSessionUrl(session) || activeRoomElementUrl.value
|
matrixCallSession.value = session
|
||||||
|
await connectMatrixCall(session, { video: mode === "video" })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
matrixCallUrl.value = activeRoomElementUrl.value
|
matrixCallError.value = error?.data?.error || error?.message || "Verbindung fehlgeschlagen"
|
||||||
toast.add({
|
toast.add({
|
||||||
title: "Matrix-Anmeldung konnte nicht vorbereitet werden",
|
title: "Besprechung konnte nicht gestartet werden",
|
||||||
description: "Der Raum wird ohne automatische Anmeldung geöffnet.",
|
description: matrixCallError.value,
|
||||||
color: "warning"
|
color: "error"
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
matrixCallLoading.value = false
|
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 } = {}) => {
|
const loadRoomChat = async ({ silent = false, includeMembers = false } = {}) => {
|
||||||
await loadRoomMessages({ silent })
|
await loadRoomMessages({ silent })
|
||||||
|
|
||||||
@@ -488,7 +623,10 @@ onMounted(async () => {
|
|||||||
startMatrixAutoRefresh()
|
startMatrixAutoRefresh()
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(stopMatrixAutoRefresh)
|
onBeforeUnmount(() => {
|
||||||
|
stopMatrixAutoRefresh()
|
||||||
|
leaveMatrixCall()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -978,20 +1116,36 @@ onBeforeUnmount(stopMatrixAutoRefresh)
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<UButton
|
<UButton
|
||||||
v-if="activeMatrixCallUrl"
|
icon="i-heroicons-microphone"
|
||||||
:to="activeMatrixCallUrl"
|
|
||||||
target="_blank"
|
|
||||||
icon="i-heroicons-arrow-top-right-on-square"
|
|
||||||
color="neutral"
|
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>
|
||||||
<UButton
|
<UButton
|
||||||
icon="i-heroicons-x-mark"
|
icon="i-heroicons-x-mark"
|
||||||
color="neutral"
|
color="neutral"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@click="matrixCallOpen = false"
|
@click="closeMatrixCall"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -1001,16 +1155,68 @@ onBeforeUnmount(stopMatrixAutoRefresh)
|
|||||||
v-if="matrixCallLoading"
|
v-if="matrixCallLoading"
|
||||||
class="flex h-full items-center justify-center text-sm text-muted"
|
class="flex h-full items-center justify-center text-sm text-muted"
|
||||||
>
|
>
|
||||||
Matrix wird geladen...
|
Besprechung wird gestartet...
|
||||||
</div>
|
</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"
|
v-else-if="canStartMatrixCall"
|
||||||
:key="`${activeRoomKey}-${matrixCallMode}`"
|
class="flex h-full flex-col"
|
||||||
:src="activeMatrixCallUrl"
|
>
|
||||||
class="h-full w-full border-0"
|
<div class="flex shrink-0 items-center justify-between border-b border-default bg-default px-4 py-2 text-xs text-muted">
|
||||||
allow="camera; microphone; display-capture; clipboard-read; clipboard-write; fullscreen; autoplay"
|
<span>{{ matrixCallSession?.liveKitRoomName || activeRoom.key }}</span>
|
||||||
referrerpolicy="no-referrer"
|
<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
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="flex h-full items-center justify-center p-6"
|
class="flex h-full items-center justify-center p-6"
|
||||||
@@ -1020,7 +1226,7 @@ onBeforeUnmount(stopMatrixAutoRefresh)
|
|||||||
color="warning"
|
color="warning"
|
||||||
variant="soft"
|
variant="soft"
|
||||||
title="Besprechung nicht verfügbar"
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user