From d4c44a1ce14b3841ad29cda4fa300aad9836b57f Mon Sep 17 00:00:00 2001 From: Anton Alexeyev Date: Tue, 2 Sep 2025 13:53:01 +0700 Subject: [PATCH 01/25] Create separate love client Signed-off-by: Anton Alexeyev --- plugins/love-resources/src/loveClient.ts | 127 +++++++++++++++++++++++ plugins/love-resources/src/utils.ts | 99 +----------------- 2 files changed, 132 insertions(+), 94 deletions(-) create mode 100644 plugins/love-resources/src/loveClient.ts diff --git a/plugins/love-resources/src/loveClient.ts b/plugins/love-resources/src/loveClient.ts new file mode 100644 index 00000000000..fcd567ec9d5 --- /dev/null +++ b/plugins/love-resources/src/loveClient.ts @@ -0,0 +1,127 @@ +import { concatLink, type Ref } from "@hcengineering/core" +import love, { type Room } from "@hcengineering/love" +import { getMetadata } from "@hcengineering/platform" +import presentation from "@hcengineering/presentation" +import { getPlatformToken, lk } from "./utils" +import { getCurrentEmployee } from "@hcengineering/contact" +import { getPersonByPersonRef } from "@hcengineering/contact-resources" +import { Analytics } from "@hcengineering/analytics" +import { currentMeetingMinutes } from "./stores" +import { get } from "svelte/store" + +interface RoomToken { + issuedOn: number + token: string +} +export function getLoveClient() { + return new LoveClient() +} + +export class LoveClient { + private tokens: Map, RoomToken> + + constructor() { + this.tokens = new Map, RoomToken>() + + } + + async getRoomToken (room: Room): Promise { + const currentTime = Date.now() + let roomToken: RoomToken | undefined = this.tokens.get(room._id) + // refresh token after 8 minutes, server sets token ttl to 10 minutes + if (roomToken === undefined || currentTime - roomToken.issuedOn >= 8 * 60 * 1000) { + roomToken = { issuedOn: currentTime, token: await this.refreshRoomToken(room) } + this.tokens.set(room._id, roomToken) + } + return roomToken.token + } + + async updateSessionLanguage (room: Room): Promise { + try { + const endpoint = this.getLoveEndpoint() + const token = getPlatformToken() + const roomName = this.getTokenRoomName(room) + + await fetch(concatLink(endpoint, '/language'), { + method: 'POST', + headers: { + Authorization: 'Bearer ' + token, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ roomName, room: room.name, language: room.language }) + }) + } catch (err: any) { + Analytics.handleError(err) + console.error(err) + } + } + + async record (room: Room): Promise { + try { + const endpoint = this.getLoveEndpoint() + const token = getPlatformToken() + const roomName = this.getTokenRoomName(room) + if (lk.isRecording) { + await fetch(concatLink(endpoint, '/stopRecord'), { + method: 'POST', + headers: { + Authorization: 'Bearer ' + token, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ roomName, room: room.name }) + }) + } else { + await fetch(concatLink(endpoint, '/startRecord'), { + method: 'POST', + headers: { + Authorization: 'Bearer ' + token, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ roomName, room: room.name, meetingMinutes: get(currentMeetingMinutes)?._id }) + }) + } + } catch (err: any) { + Analytics.handleError(err) + console.error(err) + } + } + + private getLoveEndpoint (): string { + const endpoint = getMetadata(love.metadata.ServiceEnpdoint) + if (endpoint === undefined) { + throw new Error('Love service endpoint not found') + } + + return endpoint + } + + private async refreshRoomToken(room: Room): Promise { + const sessionName = this.getTokenRoomName(room) + const endpoint = this.getLoveEndpoint() + if (endpoint === undefined) { + throw new Error('Love service endpoint not found') + } + const myPerson = await getPersonByPersonRef(getCurrentEmployee()) + if (myPerson == null) { + throw new Error('Cannot find current person') + } + const platformToken = getPlatformToken() + const res = await fetch(concatLink(endpoint, '/getToken'), { + method: 'POST', + headers: { + Authorization: `Bearer ${platformToken}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ roomName: sessionName, _id: myPerson._id, participantName: myPerson.name }) + }) + return await res.text() + } + + private getTokenRoomName (room: Room): string { + const currentWorkspaceUuid = getMetadata(presentation.metadata.WorkspaceUuid) + if (currentWorkspaceUuid === undefined) { + throw new Error('Current workspace not found') + } + return `${currentWorkspaceUuid}_${room.name}_${room._id}` + } +} diff --git a/plugins/love-resources/src/utils.ts b/plugins/love-resources/src/utils.ts index 064b9e7abb2..48bf943f5cf 100644 --- a/plugins/love-resources/src/utils.ts +++ b/plugins/love-resources/src/utils.ts @@ -75,40 +75,13 @@ import RoomSettingsPopup from './components/RoomSettingsPopup.svelte' import love from './plugin' import { $myPreferences, currentMeetingMinutes, currentRoom, myOffice, selectedRoomPlace } from './stores' import { getLiveKitClient } from './liveKitClient' - -export async function getToken ( - roomName: string, - roomId: Ref, - userId: string, - participantName: string -): Promise { - const endpoint = getMetadata(love.metadata.ServiceEnpdoint) - if (endpoint === undefined) { - throw new Error('Love service endpoint not found') - } - const token = getPlatformToken() - const res = await fetch(concatLink(endpoint, '/getToken'), { - method: 'POST', - headers: { - Authorization: `Bearer ${token}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ roomName: getTokenRoomName(roomName, roomId), _id: userId, participantName }) - }) - return await res.text() -} - -function getTokenRoomName (roomName: string, roomId: Ref): string { - const currentWorkspaceUuid = getMetadata(presentation.metadata.WorkspaceUuid) - if (currentWorkspaceUuid === undefined) { - throw new Error('Current workspace not found') - } - return `${currentWorkspaceUuid}_${roomName}_${roomId}` -} +import { getLoveClient } from './loveClient' export const liveKitClient = getLiveKitClient() export const lk: LKRoom = liveKitClient.liveKitRoom +const loveClient = getLoveClient() + export function setCustomCreateScreenTracks (value: () => Promise>>): void { lk.localParticipant.createScreenTracks = value } @@ -357,7 +330,7 @@ async function initRoomMetadata (metadata: string | undefined): Promise { } if (get(isRecordingAvailable) && data.recording == null && room?.startWithRecording === true && !get(isRecording)) { - await record(room) + await loveClient.record(room) } } @@ -534,7 +507,7 @@ export async function connectRoom ( return } await disconnect() - const token = await getToken(room.name, room._id, currentPerson._id, currentPerson.name) + const token = await loveClient.getRoomToken(room) try { await withRetries( async () => { @@ -700,36 +673,6 @@ export async function invite (person: Ref, room: Ref | undefined): }) } -export async function record (room: Room): Promise { - try { - const endpoint = getLoveEndpoint() - const token = getPlatformToken() - const roomName = getTokenRoomName(room.name, room._id) - if (lk.isRecording) { - await fetch(concatLink(endpoint, '/stopRecord'), { - method: 'POST', - headers: { - Authorization: 'Bearer ' + token, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ roomName, room: room.name }) - }) - } else { - await fetch(concatLink(endpoint, '/startRecord'), { - method: 'POST', - headers: { - Authorization: 'Bearer ' + token, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ roomName, room: room.name, meetingMinutes: get(currentMeetingMinutes)?._id }) - }) - } - } catch (err: any) { - Analytics.handleError(err) - console.error(err) - } -} - async function checkRecordAvailable (): Promise { try { const endpoint = getMetadata(love.metadata.ServiceEnpdoint) @@ -810,15 +753,6 @@ export function getLiveKitEndpoint (): string { return endpoint } -export function getLoveEndpoint (): string { - const endpoint = getMetadata(love.metadata.ServiceEnpdoint) - if (endpoint === undefined) { - throw new Error('Love service endpoint not found') - } - - return endpoint -} - export function getPlatformToken (): string { const token = getMetadata(presentation.metadata.Token) if (token === undefined) { @@ -842,29 +776,6 @@ export async function stopTranscription (room: Room): Promise { await disconnectMeeting(room._id) } -export async function updateSessionLanguage (room: Room): Promise { - const current = get(currentRoom) - if (current === undefined || room._id !== current._id || !get(isTranscription)) return - - try { - const endpoint = getLoveEndpoint() - const token = getPlatformToken() - const roomName = getTokenRoomName(room.name, room._id) - - await fetch(concatLink(endpoint, '/language'), { - method: 'POST', - headers: { - Authorization: 'Bearer ' + token, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ roomName, room: room.name, language: room.language }) - }) - } catch (err: any) { - Analytics.handleError(err) - console.error(err) - } -} - export async function showRoomSettings (room?: Room): Promise { if (room === undefined) return From e34da1831f3c1cc6534672a204dcc8b14795b2ea Mon Sep 17 00:00:00 2001 From: Anton Alexeyev Date: Tue, 2 Sep 2025 16:26:48 +0700 Subject: [PATCH 02/25] Add prepare connection Signed-off-by: Anton Alexeyev --- plugins/love-resources/src/components/EditRoom.svelte | 3 ++- plugins/love-resources/src/components/RoomPopup.svelte | 8 ++++++-- plugins/love-resources/src/liveKitClient.ts | 10 ++++++---- plugins/love-resources/src/utils.ts | 9 ++++++++- 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/plugins/love-resources/src/components/EditRoom.svelte b/plugins/love-resources/src/components/EditRoom.svelte index b5a941e7e06..0f1dc59ea9c 100644 --- a/plugins/love-resources/src/components/EditRoom.svelte +++ b/plugins/love-resources/src/components/EditRoom.svelte @@ -19,7 +19,7 @@ import { IntlString } from '@hcengineering/platform' import love from '../plugin' - import { getRoomName, tryConnect } from '../utils' + import { getRoomName, prepareRoomConnection, tryConnect } from '../utils' import { infos, invites, myInfo, myRequests, selectedRoomPlace, myOffice, currentRoom } from '../stores' import { lkSessionConnected } from '../liveKitClient' @@ -35,6 +35,7 @@ let connecting = false onMount(() => { + prepareRoomConnection(object) dispatch('open', { ignoreKeys: ['name'] }) }) diff --git a/plugins/love-resources/src/components/RoomPopup.svelte b/plugins/love-resources/src/components/RoomPopup.svelte index a791d3ae9ab..bb1c6d8223f 100644 --- a/plugins/love-resources/src/components/RoomPopup.svelte +++ b/plugins/love-resources/src/components/RoomPopup.svelte @@ -23,10 +23,10 @@ import { getClient } from '@hcengineering/presentation' import view from '@hcengineering/view' import { getObjectLinkFragment } from '@hcengineering/view-resources' - import { createEventDispatcher } from 'svelte' + import { createEventDispatcher, onMount } from 'svelte' import love from '../plugin' import { currentMeetingMinutes, infos, invites, myInfo, myRequests } from '../stores' - import { tryConnect } from '../utils' + import { prepareRoomConnection, tryConnect } from '../utils' import { lkSessionConnected } from '../liveKitClient' import MicrophoneButton from './meeting/controls/MicrophoneButton.svelte' import CameraButton from './meeting/controls/CameraButton.svelte' @@ -45,6 +45,10 @@ return await getPersonByPersonRef(info.person) } + onMount(() => { + prepareRoomConnection(room) + }) + let joined: boolean = false $: joined = $myInfo?.room === room._id diff --git a/plugins/love-resources/src/liveKitClient.ts b/plugins/love-resources/src/liveKitClient.ts index 680f2850a09..a98b6e03f87 100644 --- a/plugins/love-resources/src/liveKitClient.ts +++ b/plugins/love-resources/src/liveKitClient.ts @@ -24,8 +24,7 @@ const LAST_PARTICIPANT_NOTIFICATION_DELAY_MS = 2 * 60 * 1000 const AUTO_DISCONNECT_DELAY_MS = 60 * 1000 export function getLiveKitClient (): LiveKitClient { - const wsURL = getMetadata(love.metadata.WebSocketURL) - return new LiveKitClient(wsURL ?? '') + return new LiveKitClient() } const defaultCaptureOptions: VideoCaptureOptions = { @@ -45,7 +44,7 @@ export class LiveKitClient { private lastParticipantNotificationTimeout: number = -1 private lastParticipantDisconnectTimeout: number = -1 - constructor (wsUrl: string) { + constructor () { const lkRoom = new LKRoom({ adaptiveStream: true, dynacast: true, @@ -67,12 +66,15 @@ export class LiveKitClient { }, videoCaptureDefaults: defaultCaptureOptions }) - void lkRoom.prepareConnection(wsUrl) lkRoom.on(RoomEvent.Connected, this.onConnected) lkRoom.on(RoomEvent.Disconnected, this.onDisconnected) this.liveKitRoom = lkRoom } + async prepareConnection (wsUrl: string, token:string): Promise { + await this.liveKitRoom.prepareConnection(wsUrl, token) + } + async connect (wsURL: string, token: string, withVideo: boolean): Promise { this.currentSessionSupportsVideo = withVideo try { diff --git a/plugins/love-resources/src/utils.ts b/plugins/love-resources/src/utils.ts index b81e668ac5b..0bd7498fdd5 100644 --- a/plugins/love-resources/src/utils.ts +++ b/plugins/love-resources/src/utils.ts @@ -449,7 +449,9 @@ async function moveToRoom ( async function connectLK (token: string, room: Room): Promise { const wsURL = getLiveKitEndpoint() - await liveKitClient.connect(wsURL, token, room.type === RoomType.Video) + // await liveKitClient.connect(wsURL, token, room.type === RoomType.Video) + liveKitClient.liveKitRoom.simulateParticipants( { publish: {audio: true, video: true, useRealTracks: true}, participants: {count: 20, audio: true, video: true}} + ) } async function navigateToOfficeDoc (hierarchy: Hierarchy, object: Doc): Promise { @@ -478,6 +480,11 @@ async function initMeetingMinutes (room: Room): Promise { } } +export async function prepareRoomConnection(room: Room): Promise { + const roomToken = await loveClient.getRoomToken(room) + liveKitClient.prepareConnection(getLiveKitEndpoint(), roomToken) +} + export async function connectRoom ( x: number, y: number, From c5b8adcb100433f3e1ca54c5c757c45605a5be91 Mon Sep 17 00:00:00 2001 From: Anton Alexeyev Date: Fri, 5 Sep 2025 12:48:13 +0700 Subject: [PATCH 03/25] Move track handlers to the livekit client Signed-off-by: Anton Alexeyev --- .../components/SharingStateIndicator.svelte | 12 +- .../src/components/meeting/ControlBar.svelte | 6 +- .../meeting/controls/ShareScreenButton.svelte | 16 ++- plugins/love-resources/src/liveKitClient.ts | 135 +++++++++++++++--- plugins/love-resources/src/loveClient.ts | 29 ++-- plugins/love-resources/src/utils.ts | 113 +-------------- 6 files changed, 151 insertions(+), 160 deletions(-) diff --git a/plugins/love-resources/src/components/SharingStateIndicator.svelte b/plugins/love-resources/src/components/SharingStateIndicator.svelte index 5aab12e21ee..c6051609541 100644 --- a/plugins/love-resources/src/components/SharingStateIndicator.svelte +++ b/plugins/love-resources/src/components/SharingStateIndicator.svelte @@ -16,13 +16,12 @@ import { tooltip, eventToHTMLElement, showPopup } from '@hcengineering/ui' import love from '../plugin' - import { isSharingEnabled, isShareWithSound, screenSharing, liveKitClient } from '../utils' + import { isShareWithSound, liveKitClient } from '../utils' import SharingStatePopup from './SharingStatePopup.svelte' import IconShare from './icons/Share.svelte' - import { lkSessionConnected } from '../liveKitClient' + import { lkSessionConnected, ScreenSharingState, screenSharingState } from '../liveKitClient' - let disabled: boolean = false let pressed: boolean = false function handleShowPopup (ev: MouseEvent): void { @@ -33,15 +32,13 @@ } function handleShare (): void { - if (disabled) return + if ($screenSharingState !== ScreenSharingState.Inactive) return void liveKitClient.setScreenShareEnabled(true, $isShareWithSound) } - - $: disabled = !$screenSharing && !$lkSessionConnected {#if $lkSessionConnected} - {#if $isSharingEnabled} + {#if $screenSharingState === ScreenSharingState.Local}