mirror of
https://github.com/hcengineering/platform.git
synced 2026-08-17 18:05:42 +02:00
Office: refactor livekit logic (#9679)
This commit is contained in:
Generated
+469
-505
File diff suppressed because it is too large
Load Diff
@@ -267,7 +267,7 @@
|
||||
"electron-store": "^8.2.0",
|
||||
"electron-log": "^5.1.7",
|
||||
"electron-updater": "^6.3.4",
|
||||
"livekit-client": "^2.13.3",
|
||||
"livekit-client": "^2.15.5",
|
||||
"@hcengineering/server-backup": "^0.6.0",
|
||||
"@hcengineering/communication-types": "^0.1.0",
|
||||
"ws": "^8.18.2"
|
||||
|
||||
Binary file not shown.
@@ -90,6 +90,9 @@
|
||||
"Sharing": "Sdílení...",
|
||||
"Browser": "Prohlížeč",
|
||||
"Monitor": "Monitor",
|
||||
"Window": "Okno"
|
||||
"Window": "Okno",
|
||||
"MeetingEmptyTitle": "Schůzka ukončena?",
|
||||
"MeetingEmptyMessage": "Všichni účastníci odešli. Během 1 minuty budete odpojeni. Chcete zůstat?",
|
||||
"StayInRoom": "Zůstat v místnosti"
|
||||
}
|
||||
}
|
||||
@@ -88,6 +88,9 @@
|
||||
"Sharing": "Teilen...",
|
||||
"Browser": "Browser",
|
||||
"Monitor": "Monitor",
|
||||
"Window": "Fenster"
|
||||
"Window": "Fenster",
|
||||
"MeetingEmptyTitle": "Besprechung beendet?",
|
||||
"MeetingEmptyMessage": "Alle Teilnehmer haben die Besprechung verlassen. Sie werden in 1 Minute automatisch getrennt. Möchten Sie bleiben?",
|
||||
"StayInRoom": "Im Raum bleiben"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +90,9 @@
|
||||
"Sharing": "Sharing...",
|
||||
"Browser": "Browser",
|
||||
"Monitor": "Monitor",
|
||||
"Window": "Window"
|
||||
"Window": "Window",
|
||||
"MeetingEmptyTitle": "Meeting ended?",
|
||||
"MeetingEmptyMessage": "All participants have left. You'll be disconnected in 1 minute. Would you like to stay?",
|
||||
"StayInRoom": "Stay in room"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +90,9 @@
|
||||
"Sharing": "Compartiendo...",
|
||||
"Browser": "Navegador",
|
||||
"Monitor": "Monitor",
|
||||
"Window": "Ventana"
|
||||
"Window": "Ventana",
|
||||
"MeetingEmptyTitle": "¿Reunión finalizada?",
|
||||
"MeetingEmptyMessage": "Todos los participantes han salido. Serás desconectado en 1 minuto. ¿Deseas permanecer?",
|
||||
"StayInRoom": "Permanecer en sala"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +90,9 @@
|
||||
"Sharing": "Partage...",
|
||||
"Browser": "Navigateur",
|
||||
"Monitor": "Moniteur",
|
||||
"Window": "Fenêtre"
|
||||
"Window": "Fenêtre",
|
||||
"MeetingEmptyTitle": "Réunion terminée ?",
|
||||
"MeetingEmptyMessage": "Tous les participants sont partis. Vous serez déconnecté dans 1 minute. Souhaitez-vous rester ?",
|
||||
"StayInRoom": "Rester dans la salle"
|
||||
}
|
||||
}
|
||||
@@ -90,6 +90,9 @@
|
||||
"Sharing": "Condivisione...",
|
||||
"Browser": "Browser",
|
||||
"Monitor": "Monitor",
|
||||
"Window": "Finestra"
|
||||
"Window": "Finestra",
|
||||
"MeetingEmptyTitle": "Riunione terminata?",
|
||||
"MeetingEmptyMessage": "Tutti i partecipanti hanno abbandonato. Sarai disconnesso tra 1 minuto. Vuoi rimanere?",
|
||||
"StayInRoom": "Rimani nella stanza"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +90,9 @@
|
||||
"Sharing": "共有中...",
|
||||
"Browser": "ブラウザ",
|
||||
"Monitor": "モニター",
|
||||
"Window": "ウィンドウ"
|
||||
"Window": "ウィンドウ",
|
||||
"MeetingEmptyTitle": "ミーティング終了?",
|
||||
"MeetingEmptyMessage": "全参加者が退出しました。1分後に自動切断されます。ミーティングに残りますか?",
|
||||
"StayInRoom": "ルームに残る"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +90,9 @@
|
||||
"Sharing": "A partilhar...",
|
||||
"Browser": "Navegador",
|
||||
"Monitor": "Monitor",
|
||||
"Window": "Janela"
|
||||
"Window": "Janela",
|
||||
"MeetingEmptyTitle": "Reunião encerrada?",
|
||||
"MeetingEmptyMessage": "Todos os participantes saíram. Você será desconectado em 1 minuto. Deseja permanecer?",
|
||||
"StayInRoom": "Permanecer na sala"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +90,9 @@
|
||||
"Sharing": "Показ...",
|
||||
"Browser": "Браузер",
|
||||
"Monitor": "Монитор",
|
||||
"Window": "Окно"
|
||||
"Window": "Окно",
|
||||
"MeetingEmptyTitle": "Совещание завершено?",
|
||||
"MeetingEmptyMessage": "Все участники вышли. Вы будете отключены через 1 минуту. Хотите остаться?",
|
||||
"StayInRoom": "Остаться в комнате"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +90,9 @@
|
||||
"Sharing": "共享中...",
|
||||
"Browser": "浏览器",
|
||||
"Monitor": "监视器",
|
||||
"Window": "窗口"
|
||||
"Window": "窗口",
|
||||
"MeetingEmptyTitle": "会议已结束?",
|
||||
"MeetingEmptyMessage": "所有参与者已离开。您将在1分钟后断开连接。是否要留在会议中?",
|
||||
"StayInRoom": "留在房间"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,5 +41,6 @@ loadMetadata(love.icon, {
|
||||
MeetingMinutes: `${icons}#meeting-minutes`
|
||||
})
|
||||
loadMetadata(love.sound, {
|
||||
Knock: require('../assets/knock.wav')
|
||||
Knock: require('../assets/knock.wav'),
|
||||
MeetingEndNotification: require('../assets/meeting-end-notification.wav')
|
||||
})
|
||||
|
||||
@@ -61,9 +61,11 @@
|
||||
"@hcengineering/view-resources": "^0.6.0",
|
||||
"@hcengineering/workbench": "^0.6.16",
|
||||
"@hcengineering/workbench-resources": "^0.6.1",
|
||||
"@hcengineering/emoji": "^0.6.0",
|
||||
"@hcengineering/theme": "^0.6.5",
|
||||
"@livekit/krisp-noise-filter": "^0.3.0",
|
||||
"@livekit/track-processors": "^0.5.6",
|
||||
"livekit-client": "^2.13.3",
|
||||
"livekit-client": "^2.15.5",
|
||||
"svelte": "^4.2.19"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,6 @@
|
||||
import { get } from 'svelte/store'
|
||||
import {
|
||||
isCameraEnabled,
|
||||
isConnected,
|
||||
$isCurrentInstanceConnected,
|
||||
isMicEnabled,
|
||||
isSharingEnabled,
|
||||
setCam,
|
||||
setMic,
|
||||
setShare
|
||||
} from './utils'
|
||||
import { getCurrentLocation, location } from '@hcengineering/ui'
|
||||
import { location } from '@hcengineering/ui'
|
||||
|
||||
let key = 'love' + getCurrentLocation().path?.[2]
|
||||
let key = ''
|
||||
|
||||
let bc: BroadcastChannel | undefined
|
||||
|
||||
@@ -20,7 +9,10 @@ location.subscribe((newLocation) => {
|
||||
const newKey = 'love' + newLocation.path[2]
|
||||
if (key !== newKey) {
|
||||
key = newKey
|
||||
bc?.close()
|
||||
bc = new BroadcastChannel(key)
|
||||
bc.onmessage = onMessage
|
||||
sendMessage({ type: 'status_ask' })
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -55,50 +47,48 @@ interface BroadcastStatusMessage {
|
||||
value: boolean
|
||||
}
|
||||
|
||||
export function sendMessage (req: BroadcastMessage): void {
|
||||
function sendMessage (req: BroadcastMessage): void {
|
||||
bc?.postMessage(req)
|
||||
}
|
||||
|
||||
if (bc !== undefined) {
|
||||
bc.onmessage = async (e: MessageEvent<BroadcastMessage>) => {
|
||||
if (e.data.type === 'set_mic') {
|
||||
if ($isCurrentInstanceConnected) {
|
||||
await setMic(e.data.value)
|
||||
}
|
||||
}
|
||||
if (e.data.type === 'set_cam') {
|
||||
if ($isCurrentInstanceConnected) {
|
||||
await setCam(e.data.value)
|
||||
}
|
||||
}
|
||||
if (e.data.type === 'set_share') {
|
||||
if ($isCurrentInstanceConnected) {
|
||||
await setShare(e.data.value)
|
||||
}
|
||||
}
|
||||
if (e.data.type === 'share') {
|
||||
isSharingEnabled.set(e.data.value)
|
||||
}
|
||||
if (e.data.type === 'mic') {
|
||||
isMicEnabled.set(e.data.value)
|
||||
}
|
||||
if (e.data.type === 'cam') {
|
||||
isCameraEnabled.set(e.data.value)
|
||||
}
|
||||
if (e.data.type === 'status_ask') {
|
||||
if ($isCurrentInstanceConnected) {
|
||||
sendMessage({ type: 'connect', value: true })
|
||||
sendMessage({ type: 'mic', value: get(isMicEnabled) })
|
||||
sendMessage({ type: 'cam', value: get(isCameraEnabled) })
|
||||
sendMessage({ type: 'share', value: get(isSharingEnabled) })
|
||||
}
|
||||
}
|
||||
if (e.data.type === 'connect') {
|
||||
if (!$isCurrentInstanceConnected) {
|
||||
isConnected.set(e.data.value)
|
||||
}
|
||||
async function onMessage (e: MessageEvent<BroadcastMessage>): Promise<void> {
|
||||
/*
|
||||
if (e.data.type === 'set_mic') {
|
||||
if ($isCurrentInstanceConnected) {
|
||||
await setMic(e.data.value)
|
||||
}
|
||||
}
|
||||
if (e.data.type === 'set_cam') {
|
||||
if ($isCurrentInstanceConnected) {
|
||||
await setCam(e.data.value)
|
||||
}
|
||||
}
|
||||
if (e.data.type === 'set_share') {
|
||||
if ($isCurrentInstanceConnected) {
|
||||
await setShare(e.data.value)
|
||||
}
|
||||
}
|
||||
if (e.data.type === 'share') {
|
||||
isSharingEnabled.set(e.data.value)
|
||||
}
|
||||
if (e.data.type === 'mic') {
|
||||
isMicEnabled.set(e.data.value)
|
||||
}
|
||||
if (e.data.type === 'cam') {
|
||||
isCameraEnabled.set(e.data.value)
|
||||
}
|
||||
if (e.data.type === 'status_ask') {
|
||||
if ($isCurrentInstanceConnected) {
|
||||
sendMessage({ type: 'connect', value: true })
|
||||
sendMessage({ type: 'mic', value: get(isMicEnabled) })
|
||||
sendMessage({ type: 'cam', value: get(isCameraEnabled) })
|
||||
sendMessage({ type: 'share', value: get(isSharingEnabled) })
|
||||
}
|
||||
}
|
||||
if (e.data.type === 'connect') {
|
||||
if (!$isCurrentInstanceConnected) {
|
||||
isConnected.set(e.data.value)
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
sendMessage({ type: 'status_ask' })
|
||||
|
||||
@@ -33,15 +33,12 @@
|
||||
} from '@hcengineering/ui'
|
||||
import view, { Action } from '@hcengineering/view'
|
||||
import { getActions } from '@hcengineering/view-resources'
|
||||
import { toggleCamState, toggleMicState } from '@hcengineering/media-resources'
|
||||
import { toggleCamState, toggleMicState, state } from '@hcengineering/media-resources'
|
||||
|
||||
import love from '../plugin'
|
||||
import { currentRoom, myInfo, myOffice } from '../stores'
|
||||
import {
|
||||
isCameraEnabled,
|
||||
isConnected,
|
||||
isFullScreen,
|
||||
isMicEnabled,
|
||||
isRecording,
|
||||
isRecordingAvailable,
|
||||
isShareWithSound,
|
||||
@@ -53,7 +50,8 @@
|
||||
screenSharing,
|
||||
setShare,
|
||||
startTranscription,
|
||||
stopTranscription
|
||||
stopTranscription,
|
||||
lk
|
||||
} from '../utils'
|
||||
import CamSettingPopup from './meeting/CamSettingPopup.svelte'
|
||||
import ControlBarContainer from './ControlBarContainer.svelte'
|
||||
@@ -61,6 +59,8 @@
|
||||
import RoomAccessPopup from './RoomAccessPopup.svelte'
|
||||
import RoomModal from './RoomModal.svelte'
|
||||
import ShareSettingPopup from './ShareSettingPopup.svelte'
|
||||
import { lkSessionConnected } from '../liveKitClient'
|
||||
import emojiPlugin from '@hcengineering/emoji'
|
||||
|
||||
export let room: Room
|
||||
export let canMaximize: boolean = true
|
||||
@@ -74,6 +74,8 @@
|
||||
|
||||
$: allowCam = $currentRoom?.type === RoomType.Video
|
||||
$: allowLeave = $myInfo?.room !== ($myOffice?._id ?? love.ids.Reception)
|
||||
$: isMicEnabled = $state.microphone?.enabled === true
|
||||
$: isCamEnabled = $state.camera?.enabled === true
|
||||
|
||||
async function changeShare (): Promise<void> {
|
||||
const newValue = !$isSharingEnabled
|
||||
@@ -137,16 +139,25 @@
|
||||
function maximize (): void {
|
||||
showPopup(RoomModal, { room }, 'full-centered')
|
||||
}
|
||||
|
||||
function addReaction (event: MouseEvent): void {
|
||||
showPopup(
|
||||
emojiPlugin.component.EmojiPopup,
|
||||
{},
|
||||
event?.target as HTMLElement,
|
||||
async (result) => {
|
||||
const emoji = result?.text
|
||||
if (emoji == null) return
|
||||
void lk.localParticipant.sendChatMessage(emoji, { topic: 'reaction' })
|
||||
},
|
||||
() => {}
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<ControlBarContainer bind:noLabel>
|
||||
<!-- <svelte:fragment slot="right">
|
||||
{#if $isConnected && isTranscriptionAllowed() && $isTranscription}
|
||||
<RoomLanguageSelector {room} kind="icon" />
|
||||
{/if}
|
||||
</svelte:fragment> -->
|
||||
<svelte:fragment slot="center">
|
||||
{#if room._id !== love.ids.Reception}
|
||||
<svelte:fragment slot="right">
|
||||
{#if room._id !== love.ids.Reception && $lkSessionConnected}
|
||||
<ModernButton
|
||||
icon={roomAccessIcon[room.access]}
|
||||
iconProps={{
|
||||
@@ -164,12 +175,15 @@
|
||||
on:click={setAccess}
|
||||
/>
|
||||
{/if}
|
||||
{#if $isConnected}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="center">
|
||||
{#if $lkSessionConnected}
|
||||
<ModernButton icon={emojiPlugin.icon.Emoji} kind={'secondary'} size={'large'} on:click={addReaction} />
|
||||
<SplitButton
|
||||
size={'large'}
|
||||
icon={$isMicEnabled ? love.icon.MicEnabled : love.icon.MicDisabled}
|
||||
icon={isMicEnabled ? love.icon.MicEnabled : love.icon.MicDisabled}
|
||||
showTooltip={{
|
||||
label: $isMicEnabled ? love.string.Mute : love.string.UnMute,
|
||||
label: isMicEnabled ? love.string.Mute : love.string.UnMute,
|
||||
keys: micKeys
|
||||
}}
|
||||
action={toggleMicState}
|
||||
@@ -180,9 +194,9 @@
|
||||
{#if allowCam}
|
||||
<SplitButton
|
||||
size={'large'}
|
||||
icon={$isCameraEnabled ? love.icon.CamEnabled : love.icon.CamDisabled}
|
||||
icon={isCamEnabled ? love.icon.CamEnabled : love.icon.CamDisabled}
|
||||
showTooltip={{
|
||||
label: $isCameraEnabled ? love.string.StopVideo : love.string.StartVideo,
|
||||
label: isCamEnabled ? love.string.StopVideo : love.string.StartVideo,
|
||||
keys: camKeys
|
||||
}}
|
||||
action={toggleCamState}
|
||||
@@ -199,7 +213,7 @@
|
||||
fill: $isSharingEnabled ? 'var(--bg-negative-default)' : 'var(--bg-positive-default)'
|
||||
}}
|
||||
showTooltip={{ label: $isSharingEnabled ? love.string.StopShare : love.string.Share }}
|
||||
disabled={($screenSharing && !$isSharingEnabled) || !$isConnected}
|
||||
disabled={($screenSharing && !$isSharingEnabled) || !$lkSessionConnected}
|
||||
action={changeShare}
|
||||
secondIcon={IconUpOutline}
|
||||
secondAction={shareSettings}
|
||||
@@ -210,13 +224,13 @@
|
||||
<ModernButton
|
||||
icon={$isRecording ? love.icon.StopRecord : love.icon.Record}
|
||||
tooltip={{ label: $isRecording ? love.string.StopRecord : love.string.Record }}
|
||||
disabled={!$isConnected}
|
||||
disabled={!$lkSessionConnected}
|
||||
kind={'secondary'}
|
||||
size={'large'}
|
||||
on:click={() => record(room)}
|
||||
/>
|
||||
{/if}
|
||||
{#if hasAccountRole(getCurrentAccount(), AccountRole.User) && isTranscriptionAllowed() && $isConnected}
|
||||
{#if hasAccountRole(getCurrentAccount(), AccountRole.User) && isTranscriptionAllowed() && $lkSessionConnected}
|
||||
<ModernButton
|
||||
icon={view.icon.Feather}
|
||||
iconProps={$isTranscription ? { fill: 'var(--button-negative-BackgroundColor)' } : {}}
|
||||
@@ -232,10 +246,27 @@
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{:else}
|
||||
<ModernButton
|
||||
icon={roomAccessIcon[room.access]}
|
||||
iconProps={{
|
||||
fill:
|
||||
room.access === RoomAccess.Open
|
||||
? 'var(--bg-positive-default)'
|
||||
: room.access === RoomAccess.DND
|
||||
? 'var(--bg-negative-default)'
|
||||
: 'currentColor'
|
||||
}}
|
||||
tooltip={{ label: love.string.ChangeAccess }}
|
||||
kind={'secondary'}
|
||||
size={'large'}
|
||||
disabled={isOffice(room) && room.person !== me}
|
||||
on:click={setAccess}
|
||||
/>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="left">
|
||||
{#if $isConnected && withVideo && onFullScreen}
|
||||
{#if $lkSessionConnected && withVideo && onFullScreen}
|
||||
<ModernButton
|
||||
icon={$isFullScreen ? love.icon.ExitFullScreen : love.icon.FullScreen}
|
||||
tooltip={{
|
||||
@@ -250,7 +281,7 @@
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if ($screenSharing || room.type === RoomType.Video) && $isConnected && canMaximize}
|
||||
{#if ($screenSharing || room.type === RoomType.Video) && $lkSessionConnected && canMaximize}
|
||||
<ModernButton
|
||||
icon={IconMaximize}
|
||||
tooltip={{
|
||||
@@ -263,7 +294,7 @@
|
||||
on:click={maximize}
|
||||
/>
|
||||
{/if}
|
||||
{#if $isConnected && moreItems.length > 0}
|
||||
{#if $lkSessionConnected && moreItems.length > 0}
|
||||
<ButtonMenu
|
||||
items={moreItems}
|
||||
icon={IconMoreV}
|
||||
|
||||
@@ -48,7 +48,6 @@
|
||||
disconnect,
|
||||
endMeeting,
|
||||
getRoomName,
|
||||
isCurrentInstanceConnected,
|
||||
leaveRoom,
|
||||
screenSharing
|
||||
} from '../utils'
|
||||
@@ -60,6 +59,7 @@
|
||||
import RoomPopup from './RoomPopup.svelte'
|
||||
import RoomButton from './RoomButton.svelte'
|
||||
import { getPersonByPersonRef } from '@hcengineering/contact-resources'
|
||||
import { lkSessionConnected } from '../liveKitClient'
|
||||
|
||||
const client = getClient()
|
||||
|
||||
@@ -194,7 +194,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: checkOwnRoomConnection($infos, $myInfo, $myOffice, $isCurrentInstanceConnected)
|
||||
$: checkOwnRoomConnection($infos, $myInfo, $myOffice, $lkSessionConnected)
|
||||
|
||||
const myInvitesCategory = 'myInvites'
|
||||
|
||||
@@ -232,7 +232,7 @@
|
||||
return
|
||||
}
|
||||
|
||||
if ($isCurrentInstanceConnected) {
|
||||
if ($lkSessionConnected) {
|
||||
const widget = client.getModel().findAllSync(workbench.class.Widget, { _id: love.ids.MeetingWidget })[0]
|
||||
if (widget === undefined) return
|
||||
|
||||
@@ -252,7 +252,7 @@
|
||||
|
||||
$: checkActiveVideo(
|
||||
$location,
|
||||
$isCurrentInstanceConnected && ($currentRoom?.type === RoomType.Video || $screenSharing),
|
||||
$lkSessionConnected && ($currentRoom?.type === RoomType.Video || $screenSharing),
|
||||
$currentRoom?._id
|
||||
)
|
||||
|
||||
@@ -283,7 +283,7 @@
|
||||
})
|
||||
|
||||
const beforeUnloadListener = () => {
|
||||
if ($myInfo !== undefined && $isCurrentInstanceConnected) {
|
||||
if ($myInfo !== undefined && $lkSessionConnected) {
|
||||
if ($myOffice !== undefined && $myInfo.room === $myOffice._id) {
|
||||
endMeeting($myOffice, $rooms, $infos, $myInfo)
|
||||
} else {
|
||||
|
||||
@@ -19,8 +19,9 @@
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
|
||||
import love from '../plugin'
|
||||
import { getRoomName, tryConnect, isConnected } from '../utils'
|
||||
import { getRoomName, tryConnect } from '../utils'
|
||||
import { infos, invites, myInfo, myRequests, selectedRoomPlace, myOffice, currentRoom } from '../stores'
|
||||
import { lkSessionConnected } from '../liveKitClient'
|
||||
|
||||
export let object: Room
|
||||
|
||||
@@ -54,7 +55,7 @@
|
||||
selectedRoomPlace.set(undefined)
|
||||
}
|
||||
|
||||
$: connecting = tryConnecting || ($currentRoom?._id === object._id && !$isConnected)
|
||||
$: connecting = tryConnecting || ($currentRoom?._id === object._id && !$lkSessionConnected)
|
||||
|
||||
let connectLabel: IntlString = $infos.some(({ room }) => room === object._id)
|
||||
? love.string.JoinMeeting
|
||||
@@ -100,7 +101,7 @@
|
||||
<div class="name">
|
||||
<EditBox disabled={true} placeholder={love.string.Room} bind:value={roomName} focusIndex={1} />
|
||||
</div>
|
||||
{#if showConnectionButton(object, connecting, $isConnected, $infos, $myOffice, $currentRoom)}
|
||||
{#if showConnectionButton(object, connecting, $lkSessionConnected, $infos, $myOffice, $currentRoom)}
|
||||
<ModernButton label={connectLabel} size="large" kind={'primary'} on:click={connect} loading={connecting} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -21,8 +21,9 @@
|
||||
import Hall from './Hall.svelte'
|
||||
import { getMetadata } from '@hcengineering/platform'
|
||||
import love from '../plugin'
|
||||
import { tryConnect, isConnected, isCurrentInstanceConnected, screenSharing } from '../utils'
|
||||
import { tryConnect, screenSharing } from '../utils'
|
||||
import { infos, invites, myInfo, myRequests, waitForOfficeLoaded, currentRoom } from '../stores'
|
||||
import { lkSessionConnected } from '../liveKitClient'
|
||||
|
||||
const localNav: boolean = $deviceInfo.navigator.visible
|
||||
const savedNav = localStorage.getItem('love-visibleNav')
|
||||
@@ -46,8 +47,7 @@
|
||||
if (room === undefined) return
|
||||
|
||||
if (
|
||||
!$isConnected &&
|
||||
!$isCurrentInstanceConnected &&
|
||||
!$lkSessionConnected &&
|
||||
(room.type === RoomType.Video || $screenSharing) &&
|
||||
$myInfo?.sessionId &&
|
||||
$myInfo.sessionId === getMetadata(presentation.metadata.SessionId)
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import love from '../plugin'
|
||||
import { currentRoom, infos, myInfo, myOffice, rooms, currentMeetingMinutes } from '../stores'
|
||||
import { endMeeting, isCurrentInstanceConnected, leaveRoom } from '../utils'
|
||||
import { endMeeting, leaveRoom } from '../utils'
|
||||
import { lkSessionConnected } from '../liveKitClient'
|
||||
|
||||
export let limit: number = 4
|
||||
|
||||
@@ -60,7 +61,7 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if $isCurrentInstanceConnected && $currentRoom != null}
|
||||
{#if $lkSessionConnected && $currentRoom != null}
|
||||
{@const overLimit = participants.length > limit}
|
||||
|
||||
<div class="m-1 p-2">
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Person, formatName } from '@hcengineering/contact'
|
||||
import { Avatar, getPersonByPersonRefStore } from '@hcengineering/contact-resources'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { Loading } from '@hcengineering/ui'
|
||||
|
||||
import { currentRoomAudioLevels } from '../utils'
|
||||
import MicDisabled from './icons/MicDisabled.svelte'
|
||||
|
||||
export let _id: string
|
||||
export let name: string
|
||||
export let muted: boolean
|
||||
export let mirror: boolean
|
||||
export let connecting: boolean = false
|
||||
|
||||
let parent: HTMLDivElement
|
||||
let activeTrack: boolean = false
|
||||
|
||||
export function appendChild (track: HTMLMediaElement, enabled: boolean = true): void {
|
||||
const video = parent.querySelector('.video')
|
||||
if (video != null) {
|
||||
video.remove()
|
||||
}
|
||||
|
||||
track.classList.add('video')
|
||||
if (!enabled) track.classList.add('hidden')
|
||||
parent.appendChild(track)
|
||||
activeTrack = enabled
|
||||
}
|
||||
|
||||
export function setTrackMuted (value: boolean): void {
|
||||
const video = parent.querySelector('video')
|
||||
if (video != null) {
|
||||
if (value) {
|
||||
video.classList.add('hidden')
|
||||
} else {
|
||||
video.classList.remove('hidden')
|
||||
}
|
||||
}
|
||||
activeTrack = !value
|
||||
}
|
||||
|
||||
$: personByRefStore = getPersonByPersonRefStore([_id as Ref<Person>])
|
||||
$: user = $personByRefStore.get(_id as Ref<Person>)
|
||||
|
||||
$: speach = $currentRoomAudioLevels.get(_id as Ref<Person>) ?? 0
|
||||
</script>
|
||||
|
||||
<div id={_id} class="parent" class:speach={speach > 0}>
|
||||
<div bind:this={parent} class="cover" class:active={activeTrack} class:mirror={mirror && activeTrack} />
|
||||
<div class="ava">
|
||||
<Avatar size={'full'} {name} person={user} showStatus={false} />
|
||||
</div>
|
||||
<div class="label" class:withIcon={muted || connecting}>
|
||||
{#if connecting}<Loading size={'small'} shrink />{/if}
|
||||
{#if muted}<MicDisabled fill={'var(--bg-negative-default)'} size={'small'} />{/if}
|
||||
<span class="overflow-label">{formatName(name)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
:global(.video) {
|
||||
object-fit: cover;
|
||||
border-radius: 0.75rem;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
:global(.hidden) {
|
||||
display: none;
|
||||
}
|
||||
.cover {
|
||||
object-fit: cover;
|
||||
border-radius: 0.75rem;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
aspect-ratio: 1280 / 720;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&.mirror {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
&.active + .ava {
|
||||
display: none;
|
||||
}
|
||||
&:not(.active) {
|
||||
background-color: black;
|
||||
}
|
||||
}
|
||||
.ava {
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
height: 50%;
|
||||
aspect-ratio: 1;
|
||||
border-radius: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
.parent {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
height: max-content;
|
||||
min-height: 0;
|
||||
max-height: 100%;
|
||||
background-color: black;
|
||||
border-radius: 0.75rem;
|
||||
|
||||
.label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
top: 0.25rem;
|
||||
left: 0.25rem;
|
||||
max-width: 12rem;
|
||||
font-weight: 500;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1rem;
|
||||
color: var(--white-color);
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 0.5rem;
|
||||
backdrop-filter: blur(3px);
|
||||
|
||||
&.withIcon {
|
||||
padding-left: 0.25rem;
|
||||
}
|
||||
}
|
||||
&.speach::before,
|
||||
&.speach::after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
inset: 0;
|
||||
border-radius: 0.75rem;
|
||||
z-index: 1;
|
||||
}
|
||||
&.speach::before {
|
||||
border: 3px solid var(--border-talk-indication-secondary);
|
||||
}
|
||||
&.speach::after {
|
||||
border: 2px solid var(--border-talk-indication-primary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -20,8 +20,9 @@
|
||||
import { JoinRequest, RequestStatus } from '@hcengineering/love'
|
||||
import love from '../plugin'
|
||||
import { myInfo, myOffice } from '../stores'
|
||||
import { connectRoom, isConnected } from '../utils'
|
||||
import { connectRoom } from '../utils'
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import { lkSessionConnected } from '../liveKitClient'
|
||||
|
||||
export let request: JoinRequest
|
||||
|
||||
@@ -33,7 +34,7 @@
|
||||
|
||||
async function accept (): Promise<void> {
|
||||
await client.update(request, { status: RequestStatus.Approved })
|
||||
if (request.room === $myOffice?._id && !$isConnected) {
|
||||
if (request.room === $myOffice?._id && !$lkSessionConnected) {
|
||||
const person = await getPersonByPersonRef(getCurrentEmployee())
|
||||
if (person == null) return
|
||||
await connectRoom(0, 0, $myInfo, person, $myOffice)
|
||||
|
||||
@@ -13,223 +13,30 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
import presentation, { ActionContext } from '@hcengineering/presentation'
|
||||
import { getPersonRefByPersonIdCb } from '@hcengineering/contact-resources'
|
||||
import { Room as TypeRoom } from '@hcengineering/love'
|
||||
import { getMetadata } from '@hcengineering/platform'
|
||||
import { Label, Loading, resizeObserver, deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
|
||||
import {
|
||||
LocalParticipant,
|
||||
LocalTrackPublication,
|
||||
Participant,
|
||||
RemoteParticipant,
|
||||
RemoteTrack,
|
||||
RemoteTrackPublication,
|
||||
RoomEvent,
|
||||
Track,
|
||||
TrackPublication
|
||||
} from 'livekit-client'
|
||||
import { onDestroy, onMount, tick } from 'svelte'
|
||||
import { aiBotSocialIdentityStore } from '@hcengineering/ai-bot-resources'
|
||||
import { Label, Loading, deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
|
||||
import love from '../plugin'
|
||||
import { waitForOfficeLoaded, currentRoom, infos, invites, myInfo, myRequests } from '../stores'
|
||||
import {
|
||||
awaitConnect,
|
||||
isConnected,
|
||||
isCurrentInstanceConnected,
|
||||
isFullScreen,
|
||||
lk,
|
||||
screenSharing,
|
||||
tryConnect
|
||||
} from '../utils'
|
||||
import { isFullScreen, tryConnect } from '../utils'
|
||||
import ControlBar from './ControlBar.svelte'
|
||||
import ParticipantView from './ParticipantView.svelte'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { Person } from '@hcengineering/contact'
|
||||
import { lkSessionConnected } from '../liveKitClient'
|
||||
import ParticipantsListView from './meeting/ParticipantsListView.svelte'
|
||||
import ScreenSharingView from './meeting/ScreenSharingView.svelte'
|
||||
|
||||
export let withVideo: boolean
|
||||
export let canMaximize: boolean = true
|
||||
export let room: TypeRoom
|
||||
|
||||
interface ParticipantData {
|
||||
_id: string
|
||||
name: string
|
||||
connecting: boolean
|
||||
muted: boolean
|
||||
mirror: boolean
|
||||
isAgent: boolean
|
||||
}
|
||||
|
||||
let participants: ParticipantData[] = []
|
||||
const participantElements: ParticipantView[] = []
|
||||
let screen: HTMLVideoElement
|
||||
let roomEl: HTMLDivElement
|
||||
|
||||
let aiPersonRef: Ref<Person> | undefined
|
||||
$: if ($aiBotSocialIdentityStore != null) {
|
||||
getPersonRefByPersonIdCb($aiBotSocialIdentityStore?._id, (ref) => {
|
||||
if (ref != null) {
|
||||
aiPersonRef = ref
|
||||
}
|
||||
})
|
||||
} else {
|
||||
aiPersonRef = undefined
|
||||
}
|
||||
|
||||
function handleTrackSubscribed (
|
||||
track: RemoteTrack,
|
||||
publication: RemoteTrackPublication,
|
||||
participant: RemoteParticipant
|
||||
): void {
|
||||
if (track.kind === Track.Kind.Video) {
|
||||
if (track.source === Track.Source.ScreenShare) {
|
||||
track.attach(screen)
|
||||
} else {
|
||||
const element = track.attach()
|
||||
attachTrack(element, participant)
|
||||
}
|
||||
updateStyle(getActiveParticipants(participants).length, $screenSharing)
|
||||
} else {
|
||||
const part = participants.find((p) => p._id === participant.identity)
|
||||
if (part !== undefined) {
|
||||
part.muted = publication.isMuted
|
||||
participants = participants
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleTrackUnsubscribed (
|
||||
track: RemoteTrack,
|
||||
publication: RemoteTrackPublication,
|
||||
participant: RemoteParticipant
|
||||
): void {
|
||||
if (track.kind === Track.Kind.Video) {
|
||||
if (track.source !== Track.Source.ScreenShare) {
|
||||
const index = participants.findIndex((p) => p._id === participant.identity)
|
||||
if (index !== -1) {
|
||||
participants.splice(index, 1)
|
||||
participants = participants
|
||||
}
|
||||
} else {
|
||||
track.detach(screen)
|
||||
}
|
||||
updateStyle(getActiveParticipants(participants).length, $screenSharing)
|
||||
}
|
||||
}
|
||||
|
||||
function handleLocalTrack (publication: LocalTrackPublication, participant: LocalParticipant): void {
|
||||
if (publication.track?.kind === Track.Kind.Video) {
|
||||
if (publication.track.source === Track.Source.ScreenShare) {
|
||||
publication.track.attach(screen)
|
||||
} else {
|
||||
const element = publication.track.attach()
|
||||
void attachTrack(element, participant)
|
||||
}
|
||||
updateStyle(getActiveParticipants(participants).length, $screenSharing)
|
||||
} else {
|
||||
const part = participants.find((p) => p._id === participant.identity)
|
||||
if (part !== undefined) {
|
||||
part.muted = publication.isMuted
|
||||
participants = participants
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function attachTrack (element: HTMLMediaElement, participant: Participant): Promise<void> {
|
||||
let index = participants.findIndex((p) => p._id === participant.identity)
|
||||
if (index === -1) {
|
||||
index = participants.push({
|
||||
_id: participant.identity,
|
||||
name: participant.name ?? '',
|
||||
muted: !participant.isMicrophoneEnabled,
|
||||
mirror: participant.isLocal,
|
||||
connecting: false,
|
||||
isAgent: participant.isAgent
|
||||
})
|
||||
}
|
||||
participants = participants
|
||||
for (let attempt = 0; attempt < 10; attempt++) {
|
||||
await tick()
|
||||
index = participants.findIndex((p) => p._id === participant.identity)
|
||||
const el = participantElements[index]
|
||||
if (el != null) {
|
||||
el.appendChild(element)
|
||||
return
|
||||
}
|
||||
}
|
||||
console.error('Failed to attach track after 10 attempts')
|
||||
Analytics.handleError(new Error(`Failed to attach track after 10 attempts, participant: ${participant.identity}`))
|
||||
}
|
||||
|
||||
function attachParticipant (participant: Participant): void {
|
||||
const current = participants.find((p) => p._id === participant.identity)
|
||||
if (current !== undefined) {
|
||||
current.connecting = false
|
||||
current.muted = !participant.isMicrophoneEnabled
|
||||
current.mirror = participant.isLocal
|
||||
participants = participants
|
||||
return
|
||||
}
|
||||
const value: ParticipantData = {
|
||||
_id: participant.identity,
|
||||
name: participant.name ?? '',
|
||||
muted: !participant.isMicrophoneEnabled,
|
||||
mirror: participant.isLocal,
|
||||
connecting: false,
|
||||
isAgent: participant.isAgent
|
||||
}
|
||||
participants.push(value)
|
||||
participants = participants
|
||||
updateStyle(getActiveParticipants(participants).length, $screenSharing)
|
||||
}
|
||||
|
||||
function handleParticipantDisconnected (participant: RemoteParticipant): void {
|
||||
const index = participants.findIndex((p) => p._id === participant.identity)
|
||||
if (index !== -1) {
|
||||
participants.splice(index, 1)
|
||||
participants = participants
|
||||
}
|
||||
updateStyle(getActiveParticipants(participants).length, $screenSharing)
|
||||
}
|
||||
|
||||
function muteHandler (publication: TrackPublication, participant: Participant): void {
|
||||
if (publication.kind === Track.Kind.Video) {
|
||||
if (publication.source === Track.Source.ScreenShare) {
|
||||
return
|
||||
}
|
||||
const index = participants.findIndex((p) => p._id === participant.identity)
|
||||
if (index !== -1 && participantElements[index] != null) {
|
||||
participantElements[index].setTrackMuted(publication.isMuted)
|
||||
}
|
||||
} else {
|
||||
const part = participants.find((p) => p._id === participant.identity)
|
||||
if (part !== undefined) {
|
||||
part.muted = publication.isMuted
|
||||
participants = participants
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let withScreenSharing: boolean = false
|
||||
let loading: boolean = false
|
||||
let configured: boolean = false
|
||||
|
||||
function handleLocalTrackUnsubscribed (publication: LocalTrackPublication, participant: LocalParticipant): void {
|
||||
if (publication?.track?.kind === Track.Kind.Video) {
|
||||
if (publication.track.source === Track.Source.ScreenShare) {
|
||||
publication.track.detach(screen)
|
||||
updateStyle(getActiveParticipants(participants).length, $screenSharing)
|
||||
} else {
|
||||
const index = participants.findIndex((p) => p._id === participant.identity)
|
||||
if (index !== -1) {
|
||||
participants.splice(index, 1)
|
||||
participants = participants
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
loading = true
|
||||
|
||||
@@ -243,48 +50,10 @@
|
||||
|
||||
await waitForOfficeLoaded()
|
||||
|
||||
if (
|
||||
!$isConnected &&
|
||||
!$isCurrentInstanceConnected &&
|
||||
$myInfo?.sessionId === getMetadata(presentation.metadata.SessionId)
|
||||
) {
|
||||
if (!$lkSessionConnected && $myInfo?.sessionId === getMetadata(presentation.metadata.SessionId)) {
|
||||
const info = $infos.filter((p) => p.room === room._id)
|
||||
await tryConnect($myInfo, room, info, $myRequests, $invites)
|
||||
}
|
||||
|
||||
await awaitConnect()
|
||||
for (const participant of lk.remoteParticipants.values()) {
|
||||
attachParticipant(participant)
|
||||
for (const publication of participant.trackPublications.values()) {
|
||||
if (publication.track !== undefined && publication.track.kind === Track.Kind.Video) {
|
||||
if (publication.track.source === Track.Source.ScreenShare) {
|
||||
publication.track.attach(screen)
|
||||
} else {
|
||||
const element = publication.track.attach()
|
||||
await attachTrack(element, participant)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
attachParticipant(lk.localParticipant)
|
||||
for (const publication of lk.localParticipant.trackPublications.values()) {
|
||||
if (publication.track !== undefined && publication.track.kind === Track.Kind.Video) {
|
||||
if (publication.track.source === Track.Source.ScreenShare) {
|
||||
publication.track.attach(screen)
|
||||
} else {
|
||||
const element = publication.track.attach()
|
||||
await attachTrack(element, lk.localParticipant)
|
||||
}
|
||||
}
|
||||
}
|
||||
lk.on(RoomEvent.ParticipantConnected, attachParticipant)
|
||||
lk.on(RoomEvent.ParticipantDisconnected, handleParticipantDisconnected)
|
||||
lk.on(RoomEvent.TrackMuted, muteHandler)
|
||||
lk.on(RoomEvent.TrackUnmuted, muteHandler)
|
||||
lk.on(RoomEvent.TrackSubscribed, handleTrackSubscribed)
|
||||
lk.on(RoomEvent.TrackUnsubscribed, handleTrackUnsubscribed)
|
||||
lk.on(RoomEvent.LocalTrackPublished, handleLocalTrack)
|
||||
lk.on(RoomEvent.LocalTrackUnpublished, handleLocalTrackUnsubscribed)
|
||||
roomEl && roomEl.addEventListener('fullscreenchange', handleFullScreen)
|
||||
loading = false
|
||||
})
|
||||
@@ -292,39 +61,8 @@
|
||||
let gridStyle = ''
|
||||
let columns: number = 0
|
||||
let rows: number = 0
|
||||
let roomWidth: number
|
||||
let roomHeight: number
|
||||
|
||||
onDestroy(
|
||||
infos.subscribe((data) => {
|
||||
for (const info of data) {
|
||||
if (info.room !== room._id) continue
|
||||
const current = participants.find((p) => p._id === info.person)
|
||||
if (current !== undefined) continue
|
||||
const value: ParticipantData = {
|
||||
_id: info.person,
|
||||
name: info.name,
|
||||
muted: true,
|
||||
mirror: false,
|
||||
connecting: true,
|
||||
isAgent: aiPersonRef === info.person
|
||||
}
|
||||
participants.push(value)
|
||||
}
|
||||
participants = participants
|
||||
updateStyle(getActiveParticipants(participants).length, $screenSharing)
|
||||
})
|
||||
)
|
||||
|
||||
onDestroy(() => {
|
||||
lk.off(RoomEvent.ParticipantConnected, attachParticipant)
|
||||
lk.off(RoomEvent.ParticipantDisconnected, handleParticipantDisconnected)
|
||||
lk.off(RoomEvent.TrackSubscribed, handleTrackSubscribed)
|
||||
lk.off(RoomEvent.TrackUnsubscribed, handleTrackUnsubscribed)
|
||||
lk.off(RoomEvent.LocalTrackPublished, handleLocalTrack)
|
||||
lk.off(RoomEvent.TrackMuted, muteHandler)
|
||||
lk.off(RoomEvent.TrackUnmuted, muteHandler)
|
||||
lk.off(RoomEvent.LocalTrackUnpublished, handleLocalTrackUnsubscribed)
|
||||
roomEl.removeEventListener('fullscreenchange', handleFullScreen)
|
||||
})
|
||||
|
||||
@@ -387,21 +125,11 @@
|
||||
}
|
||||
|
||||
$: if (((document.fullscreenElement && !$isFullScreen) || $isFullScreen) && roomEl) checkFullscreen()
|
||||
|
||||
function getActiveParticipants (participants: ParticipantData[]): ParticipantData[] {
|
||||
return participants.filter((p) => !p.isAgent || $infos.some(({ person }) => person === p._id))
|
||||
}
|
||||
|
||||
$: activeParticipants = getActiveParticipants(participants)
|
||||
</script>
|
||||
|
||||
<div bind:this={roomEl} class="flex-col-center w-full h-full" class:theme-dark={$isFullScreen}>
|
||||
<ActionContext context={{ mode: 'workbench' }} />
|
||||
{#if $isConnected && !$isCurrentInstanceConnected}
|
||||
<div class="flex justify-center error h-full w-full clear-mins">
|
||||
<Label label={love.string.AnotherWindowError} />
|
||||
</div>
|
||||
{:else if !configured}
|
||||
{#if !configured}
|
||||
<div class="flex justify-center error h-full w-full clear-mins">
|
||||
<Label label={love.string.ServiceNotConfigured} />
|
||||
</div>
|
||||
@@ -410,27 +138,22 @@
|
||||
{/if}
|
||||
<div
|
||||
class="room-container"
|
||||
class:sharing={$screenSharing}
|
||||
class:sharing={withScreenSharing}
|
||||
class:many={columns > 3}
|
||||
class:hidden={loading}
|
||||
class:mobile={$deviceInfo.isMobile}
|
||||
>
|
||||
<div class="screenContainer">
|
||||
<video class="screen" bind:this={screen}></video>
|
||||
<ScreenSharingView bind:hasActiveTrack={withScreenSharing} />
|
||||
</div>
|
||||
{#if withVideo}
|
||||
<div
|
||||
use:resizeObserver={(element) => {
|
||||
roomWidth = element.clientWidth
|
||||
roomHeight = element.clientHeight
|
||||
}}
|
||||
class="videoGrid"
|
||||
style={$screenSharing ? '' : gridStyle}
|
||||
class:scroll-m-0={$screenSharing}
|
||||
>
|
||||
{#each activeParticipants as participant, i (participant._id)}
|
||||
<ParticipantView bind:this={participantElements[i]} {...participant} />
|
||||
{/each}
|
||||
<div class="videoGrid" style={withScreenSharing ? '' : gridStyle} class:scroll-m-0={withScreenSharing}>
|
||||
<ParticipantsListView
|
||||
room={room._id}
|
||||
on:participantsCount={(evt) => {
|
||||
updateStyle(evt.detail, withScreenSharing)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -45,16 +45,13 @@
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import view from '@hcengineering/view'
|
||||
import { getObjectLinkFragment } from '@hcengineering/view-resources'
|
||||
import { toggleCamState, toggleMicState } from '@hcengineering/media-resources'
|
||||
import { toggleCamState, toggleMicState, state } from '@hcengineering/media-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import love from '../plugin'
|
||||
import { currentMeetingMinutes, currentRoom, infos, invites, myInfo, myOffice, myRequests, rooms } from '../stores'
|
||||
import {
|
||||
endMeeting,
|
||||
getRoomName,
|
||||
isCameraEnabled,
|
||||
isConnected,
|
||||
isMicEnabled,
|
||||
isShareWithSound,
|
||||
isSharingEnabled,
|
||||
leaveRoom,
|
||||
@@ -62,6 +59,7 @@
|
||||
setShare,
|
||||
tryConnect
|
||||
} from '../utils'
|
||||
import { lkSessionConnected } from '../liveKitClient'
|
||||
import CamSettingPopup from './meeting/CamSettingPopup.svelte'
|
||||
import MicSettingPopup from './meeting/MicSettingPopup.svelte'
|
||||
import RoomAccessPopup from './RoomAccessPopup.svelte'
|
||||
@@ -92,6 +90,8 @@
|
||||
const allowShare: boolean = true
|
||||
|
||||
$: allowCam = $currentRoom?.type === RoomType.Video
|
||||
$: isMicEnabled = $state.microphone?.enabled === true
|
||||
$: isCamEnabled = $state.camera?.enabled === true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -188,14 +188,14 @@
|
||||
</div>
|
||||
</Scroller>
|
||||
</div>
|
||||
{#if joined && $isConnected}
|
||||
{#if joined && $lkSessionConnected}
|
||||
<div class="room-btns" class:no-video={!allowCam}>
|
||||
<SplitButton
|
||||
size={'large'}
|
||||
icon={$isMicEnabled ? love.icon.MicEnabled : love.icon.MicDisabled}
|
||||
label={$isMicEnabled ? love.string.Mute : love.string.UnMute}
|
||||
icon={isMicEnabled ? love.icon.MicEnabled : love.icon.MicDisabled}
|
||||
label={isMicEnabled ? love.string.Mute : love.string.UnMute}
|
||||
showTooltip={{
|
||||
label: $isMicEnabled ? love.string.Mute : love.string.UnMute
|
||||
label: isMicEnabled ? love.string.Mute : love.string.UnMute
|
||||
}}
|
||||
action={toggleMicState}
|
||||
secondIcon={IconUpOutline}
|
||||
@@ -205,10 +205,10 @@
|
||||
{#if allowCam}
|
||||
<SplitButton
|
||||
size={'large'}
|
||||
icon={$isCameraEnabled ? love.icon.CamEnabled : love.icon.CamDisabled}
|
||||
label={$isCameraEnabled ? love.string.StopVideo : love.string.StartVideo}
|
||||
icon={isCamEnabled ? love.icon.CamEnabled : love.icon.CamDisabled}
|
||||
label={isCamEnabled ? love.string.StopVideo : love.string.StartVideo}
|
||||
showTooltip={{
|
||||
label: $isCameraEnabled ? love.string.StopVideo : love.string.StartVideo
|
||||
label: isCamEnabled ? love.string.StopVideo : love.string.StartVideo
|
||||
}}
|
||||
action={toggleCamState}
|
||||
secondIcon={IconUpOutline}
|
||||
@@ -221,7 +221,7 @@
|
||||
size={'large'}
|
||||
icon={$isSharingEnabled ? love.icon.SharingEnabled : love.icon.SharingDisabled}
|
||||
showTooltip={{ label: $isSharingEnabled ? love.string.StopShare : love.string.Share }}
|
||||
disabled={($screenSharing && !$isSharingEnabled) || !$isConnected}
|
||||
disabled={($screenSharing && !$isSharingEnabled) || !$lkSessionConnected}
|
||||
action={changeShare}
|
||||
secondIcon={IconUpOutline}
|
||||
secondAction={shareSettings}
|
||||
|
||||
@@ -23,9 +23,10 @@
|
||||
|
||||
import love from '../plugin'
|
||||
import { myInfo, selectedRoomPlace, currentRoom, currentMeetingMinutes } from '../stores'
|
||||
import { getRoomLabel, isConnected } from '../utils'
|
||||
import { getRoomLabel } from '../utils'
|
||||
import PersonActionPopup from './PersonActionPopup.svelte'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { lkSessionConnected } from '../liveKitClient'
|
||||
|
||||
export let room: Room
|
||||
export let info: ParticipantInfo[]
|
||||
@@ -71,7 +72,7 @@
|
||||
async function openRoom (x: number, y: number): Promise<void> {
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
if ($isConnected && $currentRoom?._id === room._id) {
|
||||
if ($lkSessionConnected && $currentRoom?._id === room._id) {
|
||||
let meeting = $currentMeetingMinutes
|
||||
if (meeting?.attachedTo !== room._id || meeting?.status !== MeetingStatus.Active) {
|
||||
meeting = await client.findOne(love.class.MeetingMinutes, {
|
||||
|
||||
@@ -16,10 +16,11 @@
|
||||
import { tooltip, eventToHTMLElement, showPopup } from '@hcengineering/ui'
|
||||
|
||||
import love from '../plugin'
|
||||
import { isConnected, isSharingEnabled, isShareWithSound, screenSharing, setShare } from '../utils'
|
||||
import { isSharingEnabled, isShareWithSound, screenSharing, setShare } from '../utils'
|
||||
|
||||
import SharingStatePopup from './SharingStatePopup.svelte'
|
||||
import IconShare from './icons/Share.svelte'
|
||||
import { lkSessionConnected } from '../liveKitClient'
|
||||
|
||||
let disabled: boolean = false
|
||||
let pressed: boolean = false
|
||||
@@ -38,10 +39,10 @@
|
||||
void setShare(true, audio)
|
||||
}
|
||||
|
||||
$: disabled = !$screenSharing && !$isConnected
|
||||
$: disabled = !$screenSharing && !$lkSessionConnected
|
||||
</script>
|
||||
|
||||
{#if $isConnected}
|
||||
{#if $lkSessionConnected}
|
||||
{#if $isSharingEnabled}
|
||||
<button
|
||||
class="hulyStatusBarButton mini positive positiveContent"
|
||||
|
||||
@@ -13,249 +13,22 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { aiBotSocialIdentityStore } from '@hcengineering/ai-bot-resources'
|
||||
import { getPersonRefByPersonIdCb } from '@hcengineering/contact-resources'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { Room as TypeRoom } from '@hcengineering/love'
|
||||
import { Scroller } from '@hcengineering/ui'
|
||||
import {
|
||||
LocalParticipant,
|
||||
LocalTrackPublication,
|
||||
Participant,
|
||||
RemoteParticipant,
|
||||
RemoteTrack,
|
||||
RemoteTrackPublication,
|
||||
RoomEvent,
|
||||
Track,
|
||||
TrackPublication
|
||||
} from 'livekit-client'
|
||||
import { createEventDispatcher, onDestroy, onMount, tick } from 'svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
import { infos } from '../stores'
|
||||
import { awaitConnect, isSharingEnabled, lk, screenSharing } from '../utils'
|
||||
import ParticipantView from './ParticipantView.svelte'
|
||||
import { Person } from '@hcengineering/contact'
|
||||
import ParticipantsListView from './meeting/ParticipantsListView.svelte'
|
||||
import ScreenSharingView from './meeting/ScreenSharingView.svelte'
|
||||
|
||||
export let isDock: boolean = false
|
||||
export let room: Ref<TypeRoom>
|
||||
|
||||
interface ParticipantData {
|
||||
_id: string
|
||||
name: string
|
||||
muted: boolean
|
||||
mirror: boolean
|
||||
connecting: boolean
|
||||
isAgent: boolean
|
||||
}
|
||||
|
||||
let aiPersonRef: Ref<Person> | undefined
|
||||
$: if ($aiBotSocialIdentityStore != null) {
|
||||
getPersonRefByPersonIdCb($aiBotSocialIdentityStore?._id, (ref) => {
|
||||
if (ref != null) {
|
||||
aiPersonRef = ref
|
||||
}
|
||||
})
|
||||
} else {
|
||||
aiPersonRef = undefined
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let participants: ParticipantData[] = []
|
||||
const participantElements: ParticipantView[] = []
|
||||
let screen: HTMLVideoElement
|
||||
|
||||
function handleTrackSubscribed (
|
||||
track: RemoteTrack,
|
||||
publication: RemoteTrackPublication,
|
||||
participant: RemoteParticipant
|
||||
): void {
|
||||
if (track.kind === Track.Kind.Video) {
|
||||
if (track.source === Track.Source.ScreenShare) {
|
||||
track.attach(screen)
|
||||
} else {
|
||||
const element = track.attach()
|
||||
attachTrack(element, participant)
|
||||
}
|
||||
} else {
|
||||
const part = participants.find((p) => p._id === participant.identity)
|
||||
if (part !== undefined) {
|
||||
part.muted = publication.isMuted
|
||||
participants = participants
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleTrackUnsubscribed (
|
||||
track: RemoteTrack,
|
||||
publication: RemoteTrackPublication,
|
||||
participant: RemoteParticipant
|
||||
): void {
|
||||
if (track.kind === Track.Kind.Video) {
|
||||
if (track.source !== Track.Source.ScreenShare) {
|
||||
const part = participants.find((p) => p._id === participant.identity)
|
||||
if (part !== undefined) {
|
||||
participants = participants
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleLocalTrack (publication: LocalTrackPublication, participant: LocalParticipant): void {
|
||||
if (publication.track?.kind === Track.Kind.Video) {
|
||||
if (publication.track.source === Track.Source.ScreenShare) {
|
||||
publication.track.attach(screen)
|
||||
} else {
|
||||
const element = publication.track.attach()
|
||||
void attachTrack(element, participant)
|
||||
}
|
||||
} else {
|
||||
const part = participants.find((p) => p._id === participant.identity)
|
||||
if (part !== undefined) {
|
||||
part.muted = publication.isMuted
|
||||
participants = participants
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function attachTrack (element: HTMLMediaElement, participant: Participant): Promise<void> {
|
||||
let index = participants.findIndex((p) => p._id === participant.identity)
|
||||
if (index === -1) {
|
||||
index = participants.push({
|
||||
_id: participant.identity,
|
||||
name: participant.name ?? '',
|
||||
muted: !participant.isMicrophoneEnabled,
|
||||
mirror: participant.isLocal,
|
||||
connecting: false,
|
||||
isAgent: participant.isAgent
|
||||
})
|
||||
}
|
||||
participants = participants
|
||||
participantElements.length = participants.length
|
||||
await tick()
|
||||
participantElements[index]?.appendChild(element, participant.isCameraEnabled)
|
||||
}
|
||||
|
||||
function attachParticipant (participant: Participant): void {
|
||||
const current = participants.find((p) => p._id === participant.identity)
|
||||
if (current !== undefined) {
|
||||
current.connecting = false
|
||||
current.muted = !participant.isMicrophoneEnabled
|
||||
current.mirror = participant.isLocal
|
||||
participants = participants
|
||||
return
|
||||
}
|
||||
const value: ParticipantData = {
|
||||
_id: participant.identity,
|
||||
name: participant.name ?? '',
|
||||
muted: !participant.isMicrophoneEnabled,
|
||||
mirror: participant.isLocal,
|
||||
connecting: false,
|
||||
isAgent: participant.isAgent
|
||||
}
|
||||
participants.push(value)
|
||||
participants = participants
|
||||
}
|
||||
|
||||
function handleParticipantDisconnected (participant: RemoteParticipant): void {
|
||||
const index = participants.findIndex((p) => p._id === participant.identity)
|
||||
if (index !== -1) {
|
||||
participants.splice(index, 1)
|
||||
participants = participants
|
||||
}
|
||||
}
|
||||
|
||||
function muteHandler (publication: TrackPublication, participant: Participant): void {
|
||||
if (publication.kind === Track.Kind.Video) {
|
||||
if (publication.source === Track.Source.ScreenShare) {
|
||||
return
|
||||
}
|
||||
const index = participants.findIndex((p) => p._id === participant.identity)
|
||||
if (index !== -1 && participantElements[index] != null) {
|
||||
participantElements[index].setTrackMuted(publication.isMuted)
|
||||
}
|
||||
} else {
|
||||
const part = participants.find((p) => p._id === participant.identity)
|
||||
if (part !== undefined) {
|
||||
part.muted = publication.isMuted
|
||||
participants = participants
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await awaitConnect()
|
||||
for (const participant of lk.remoteParticipants.values()) {
|
||||
attachParticipant(participant)
|
||||
for (const publication of participant.trackPublications.values()) {
|
||||
if (publication.track !== undefined && publication.track.kind === Track.Kind.Video) {
|
||||
if (publication.track.source === Track.Source.ScreenShare) {
|
||||
publication.track.attach(screen)
|
||||
} else {
|
||||
const element = publication.track.attach()
|
||||
await attachTrack(element, participant)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
attachParticipant(lk.localParticipant)
|
||||
for (const publication of lk.localParticipant.trackPublications.values()) {
|
||||
if (publication.track !== undefined && publication.track.kind === Track.Kind.Video) {
|
||||
if (publication.track.source === Track.Source.ScreenShare) {
|
||||
publication.track.attach(screen)
|
||||
} else {
|
||||
const element = publication.track.attach()
|
||||
await attachTrack(element, lk.localParticipant)
|
||||
}
|
||||
}
|
||||
}
|
||||
lk.on(RoomEvent.ParticipantConnected, attachParticipant)
|
||||
lk.on(RoomEvent.ParticipantDisconnected, handleParticipantDisconnected)
|
||||
lk.on(RoomEvent.TrackMuted, muteHandler)
|
||||
lk.on(RoomEvent.TrackUnmuted, muteHandler)
|
||||
lk.on(RoomEvent.TrackSubscribed, handleTrackSubscribed)
|
||||
lk.on(RoomEvent.TrackUnsubscribed, handleTrackUnsubscribed)
|
||||
lk.on(RoomEvent.LocalTrackPublished, handleLocalTrack)
|
||||
})
|
||||
|
||||
onDestroy(
|
||||
infos.subscribe((data) => {
|
||||
for (const info of data) {
|
||||
if (info.room !== room) continue
|
||||
const current = participants.find((p) => p._id === info.person)
|
||||
if (current !== undefined) continue
|
||||
const value: ParticipantData = {
|
||||
_id: info.person,
|
||||
name: info.name,
|
||||
muted: true,
|
||||
mirror: false,
|
||||
connecting: true,
|
||||
isAgent: info.person === aiPersonRef
|
||||
}
|
||||
participants.push(value)
|
||||
}
|
||||
participants = participants
|
||||
})
|
||||
)
|
||||
|
||||
onDestroy(() => {
|
||||
lk.off(RoomEvent.ParticipantConnected, attachParticipant)
|
||||
lk.off(RoomEvent.ParticipantDisconnected, handleParticipantDisconnected)
|
||||
lk.off(RoomEvent.TrackSubscribed, handleTrackSubscribed)
|
||||
lk.off(RoomEvent.TrackUnsubscribed, handleTrackUnsubscribed)
|
||||
lk.off(RoomEvent.LocalTrackPublished, handleLocalTrack)
|
||||
lk.off(RoomEvent.TrackMuted, muteHandler)
|
||||
lk.off(RoomEvent.TrackUnmuted, muteHandler)
|
||||
})
|
||||
|
||||
let withScreenSharing: boolean = false
|
||||
let divScroll: HTMLElement
|
||||
|
||||
export function canClose (): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
$: dispatchFit($isSharingEnabled)
|
||||
$: dispatchFit(participants.length > 0)
|
||||
const dispatchFit = (_?: boolean): void => {
|
||||
setTimeout(() => {
|
||||
if (divScroll) {
|
||||
@@ -265,16 +38,12 @@
|
||||
}, 10)
|
||||
}
|
||||
|
||||
function getActiveParticipants (participants: ParticipantData[]): ParticipantData[] {
|
||||
return participants.filter((p) => !p.isAgent || $infos.some(({ person }) => person === p._id))
|
||||
}
|
||||
|
||||
$: activeParticipants = getActiveParticipants(participants)
|
||||
$: dispatchFit(withScreenSharing)
|
||||
</script>
|
||||
|
||||
<div class="antiPopup videoPopup-container" class:isDock>
|
||||
<div class="screenContainer" class:hidden={!$screenSharing || $isSharingEnabled}>
|
||||
<video class="screen" bind:this={screen}></video>
|
||||
<div class="screenContainer" class:hidden={!withScreenSharing}>
|
||||
<ScreenSharingView showLocalTrack={false} bind:hasActiveTrack={withScreenSharing} />
|
||||
</div>
|
||||
<Scroller
|
||||
bind:divScroll
|
||||
@@ -285,11 +54,12 @@
|
||||
stickedScrollBars
|
||||
>
|
||||
<div class="videoGrid">
|
||||
{#each activeParticipants as participant, i (participant._id)}
|
||||
<div class="video">
|
||||
<ParticipantView bind:this={participantElements[i]} {...participant} />
|
||||
</div>
|
||||
{/each}
|
||||
<ParticipantsListView
|
||||
{room}
|
||||
on:participantsCount={(evt) => {
|
||||
dispatchFit(evt.detail > 0)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Scroller>
|
||||
<div class="antiNav-space" />
|
||||
@@ -313,14 +83,6 @@
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-shrink: 0;
|
||||
margin: 0.5rem 1rem;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.screenContainer {
|
||||
@@ -337,12 +99,6 @@
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
.screen {
|
||||
object-fit: contain;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.videoGrid {
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
import { RemoteParticipant, RemoteTrack, RemoteTrackPublication, RoomEvent, Track } from 'livekit-client'
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import love from '../plugin'
|
||||
import { disconnect, isCurrentInstanceConnected, lk } from '../utils'
|
||||
import { disconnect, lk } from '../utils'
|
||||
import { lkSessionConnected } from '../liveKitClient'
|
||||
|
||||
let parentElement: HTMLDivElement
|
||||
|
||||
@@ -41,7 +42,7 @@
|
||||
onDestroy(async () => {
|
||||
lk.off(RoomEvent.TrackSubscribed, handleTrackSubscribed)
|
||||
lk.off(RoomEvent.TrackUnsubscribed, handleTrackUnsubscribed)
|
||||
if ($isCurrentInstanceConnected) {
|
||||
if ($lkSessionConnected) {
|
||||
await disconnect()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<!--
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { IconSize } from '@hcengineering/ui'
|
||||
|
||||
export let size: IconSize = 'small'
|
||||
export let fill: string = 'currentColor'
|
||||
</script>
|
||||
|
||||
<svg class="svg-{size}" {fill} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
|
||||
<path
|
||||
d="M24,25a15.1,15.1,0,0,0-11.7,5.5A2,2,0,0,0,15.4,33,11.5,11.5,0,0,1,24,29a11.5,11.5,0,0,1,8.6,4,2,2,0,1,0,3.1-2.5A15.1,15.1,0,0,0,24,25Z"
|
||||
/>
|
||||
<path
|
||||
d="M24,17A22.9,22.9,0,0,0,7.5,23.9a2,2,0,0,0-.2,2.6,1.9,1.9,0,0,0,3,.2,19.3,19.3,0,0,1,27.4,0,1.9,1.9,0,0,0,3-.2,2,2,0,0,0-.2-2.6A22.9,22.9,0,0,0,24,17Z"
|
||||
/>
|
||||
<path
|
||||
d="M45.4,17.4a31.5,31.5,0,0,0-42.8,0,2.1,2.1,0,0,0-.2,2.7h0a1.9,1.9,0,0,0,3,.2,27.3,27.3,0,0,1,37.2,0,1.9,1.9,0,0,0,3-.2h0A2.1,2.1,0,0,0,45.4,17.4Z"
|
||||
/>
|
||||
<circle cx="24" cy="38" r="5" />
|
||||
</svg>
|
||||
@@ -0,0 +1,37 @@
|
||||
<script lang="ts">
|
||||
import { Button, Notification, NotificationToast } from '@hcengineering/ui'
|
||||
import { myInfo, myOffice } from '../../stores'
|
||||
import { leaveRoom } from '../../utils'
|
||||
import love from '../../plugin'
|
||||
import { onMount } from 'svelte'
|
||||
import { playSound } from '@hcengineering/presentation'
|
||||
|
||||
export let onRemove: () => void
|
||||
export let notification: Notification
|
||||
|
||||
function stayInMeeting (e: MouseEvent): void {
|
||||
onRemove()
|
||||
}
|
||||
|
||||
async function leaveMeeting (e: MouseEvent): Promise<void> {
|
||||
await leaveRoom($myInfo, $myOffice)
|
||||
onRemove()
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await playSound(love.sound.MeetingEndNotification)
|
||||
})
|
||||
</script>
|
||||
|
||||
<NotificationToast title={notification.title} severity={notification.severity} onClose={onRemove}>
|
||||
<svelte:fragment slot="content">
|
||||
{notification.subTitle}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="buttons">
|
||||
<div style="width: auto" />
|
||||
<div class="flex-between gap-2">
|
||||
<Button label={love.string.LeaveRoom} stopPropagation={false} kind="negative" on:click={leaveMeeting} />
|
||||
<Button label={love.string.StayInRoom} stopPropagation={false} kind="primary" on:click={stayInMeeting} />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</NotificationToast>
|
||||
@@ -0,0 +1,325 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { formatName, Person } from '@hcengineering/contact'
|
||||
import { Avatar, getPersonByPersonRefStore } from '@hcengineering/contact-resources'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { Loading } from '@hcengineering/ui'
|
||||
import MicDisabled from '../icons/MicDisabled.svelte'
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import {
|
||||
ChatMessage,
|
||||
ConnectionQuality,
|
||||
LocalParticipant,
|
||||
type LocalTrackPublication,
|
||||
Participant,
|
||||
ParticipantEvent,
|
||||
RemoteParticipant,
|
||||
type RemoteTrack,
|
||||
type RemoteTrackPublication,
|
||||
RoomEvent,
|
||||
Track,
|
||||
TrackPublication
|
||||
} from 'livekit-client'
|
||||
import BadConnection from '../icons/BadConnection.svelte'
|
||||
import { lk } from '../../utils'
|
||||
import Reaction from './Reaction.svelte'
|
||||
|
||||
export let _id: string
|
||||
export let participant: Participant | undefined = undefined
|
||||
|
||||
let parent: HTMLDivElement
|
||||
let mirror: boolean
|
||||
|
||||
let activeParticipant: Participant | undefined = undefined
|
||||
let videoTrack: Track | undefined = undefined
|
||||
let videoTrackElement: HTMLMediaElement | undefined = undefined
|
||||
let videoMuted: boolean = true
|
||||
let microphoneMuted: boolean = true
|
||||
let isSpeaking: boolean = false
|
||||
let isBadConnection: boolean = false
|
||||
|
||||
$: personByRefStore = getPersonByPersonRefStore([_id as Ref<Person>])
|
||||
$: user = $personByRefStore.get(_id as Ref<Person>)
|
||||
$: userName = (user !== undefined ? user.name : activeParticipant?.name) ?? ''
|
||||
|
||||
function attachTrack (track: Track): void {
|
||||
if (parent == null) return
|
||||
detachCurrentTrack()
|
||||
|
||||
videoTrack = track
|
||||
videoTrackElement = track.attach()
|
||||
const enabled = activeParticipant?.isCameraEnabled ?? false
|
||||
|
||||
videoTrackElement.classList.add('video')
|
||||
if (!enabled) videoTrackElement.classList.add('hidden')
|
||||
parent.appendChild(videoTrackElement)
|
||||
videoMuted = !enabled
|
||||
}
|
||||
|
||||
function detachCurrentTrack (): void {
|
||||
if (videoTrackElement !== undefined) {
|
||||
videoTrack?.detach(videoTrackElement)
|
||||
videoTrackElement.remove()
|
||||
}
|
||||
}
|
||||
|
||||
function setTrackMuted (value: boolean): void {
|
||||
if (videoTrackElement !== undefined) {
|
||||
if (value) {
|
||||
videoTrackElement.classList.add('hidden')
|
||||
} else {
|
||||
videoTrackElement.classList.remove('hidden')
|
||||
}
|
||||
}
|
||||
videoMuted = value
|
||||
}
|
||||
|
||||
function speachHandler (speaking: boolean): void {
|
||||
isSpeaking = speaking
|
||||
}
|
||||
|
||||
function muteHandler (publication: TrackPublication): void {
|
||||
if (publication.kind === Track.Kind.Audio) {
|
||||
microphoneMuted = publication.isMuted
|
||||
} else if (publication.kind === Track.Kind.Video && publication.source !== Track.Source.ScreenShare) {
|
||||
setTrackMuted(publication.isMuted)
|
||||
}
|
||||
}
|
||||
|
||||
function onConnectionQualityChanged (quality: ConnectionQuality): void {
|
||||
isBadConnection = quality === ConnectionQuality.Lost || quality === ConnectionQuality.Poor
|
||||
}
|
||||
|
||||
function onLocalTrackPublished (publication: LocalTrackPublication): void {
|
||||
if (publication.track === undefined) return
|
||||
if (publication.kind === Track.Kind.Audio) {
|
||||
microphoneMuted = publication.isMuted
|
||||
} else if (publication.kind === Track.Kind.Video && publication.source !== Track.Source.ScreenShare) {
|
||||
attachTrack(publication.track)
|
||||
}
|
||||
}
|
||||
|
||||
function onLocalTrackUnpublished (publication: LocalTrackPublication): void {
|
||||
if (videoTrack === publication.track) {
|
||||
detachCurrentTrack()
|
||||
}
|
||||
}
|
||||
|
||||
function onTrackSubscribed (track: RemoteTrack, publication: RemoteTrackPublication): void {
|
||||
if (track.kind === Track.Kind.Audio) {
|
||||
microphoneMuted = track.isMuted
|
||||
} else if (track.kind === Track.Kind.Video && track.source !== Track.Source.ScreenShare) {
|
||||
attachTrack(track)
|
||||
}
|
||||
}
|
||||
|
||||
function onTrackUnsubscribed (track: RemoteTrack, publication: RemoteTrackPublication): void {
|
||||
if (videoTrack === track) {
|
||||
detachCurrentTrack()
|
||||
}
|
||||
}
|
||||
|
||||
function detachParticipant (): void {
|
||||
if (activeParticipant === undefined) return
|
||||
activeParticipant.off(ParticipantEvent.TrackMuted, muteHandler)
|
||||
activeParticipant.off(ParticipantEvent.TrackUnmuted, muteHandler)
|
||||
activeParticipant.off(ParticipantEvent.IsSpeakingChanged, speachHandler)
|
||||
if (activeParticipant.isLocal) {
|
||||
activeParticipant.off(ParticipantEvent.LocalTrackPublished, onLocalTrackPublished)
|
||||
activeParticipant.off(ParticipantEvent.LocalTrackUnpublished, onLocalTrackUnpublished)
|
||||
} else {
|
||||
activeParticipant.off(ParticipantEvent.TrackSubscribed, onTrackSubscribed)
|
||||
activeParticipant.off(ParticipantEvent.TrackUnsubscribed, onTrackUnsubscribed)
|
||||
}
|
||||
activeParticipant = undefined
|
||||
}
|
||||
|
||||
function onChatMessage (message: ChatMessage, participant?: RemoteParticipant | LocalParticipant | undefined): void {
|
||||
if (activeParticipant !== participant) return
|
||||
reactions = [
|
||||
...reactions,
|
||||
{
|
||||
id: message.id,
|
||||
emoji: message.message,
|
||||
width: parent.offsetWidth,
|
||||
height: parent.offsetHeight
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
let reactions: Array<{ id: string, emoji: string, height: number, width: number }> = []
|
||||
|
||||
function setParticipant (p: Participant | undefined): void {
|
||||
if (parent == null) return
|
||||
if (activeParticipant === p) return
|
||||
|
||||
detachParticipant()
|
||||
detachCurrentTrack()
|
||||
|
||||
activeParticipant = p
|
||||
if (p === undefined) return
|
||||
|
||||
for (const publication of p.trackPublications.values()) {
|
||||
if (
|
||||
publication.track !== undefined &&
|
||||
publication.track.kind === Track.Kind.Video &&
|
||||
publication.track.source !== Track.Source.ScreenShare
|
||||
) {
|
||||
attachTrack(publication.track)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
mirror = p.isLocal
|
||||
microphoneMuted = !p.isMicrophoneEnabled
|
||||
|
||||
p.on(ParticipantEvent.TrackMuted, muteHandler)
|
||||
p.on(ParticipantEvent.TrackUnmuted, muteHandler)
|
||||
p.on(ParticipantEvent.IsSpeakingChanged, speachHandler)
|
||||
p.on(ParticipantEvent.ConnectionQualityChanged, onConnectionQualityChanged)
|
||||
|
||||
if (p.isLocal) {
|
||||
p.on(ParticipantEvent.LocalTrackPublished, onLocalTrackPublished)
|
||||
p.on(ParticipantEvent.LocalTrackUnpublished, onLocalTrackUnpublished)
|
||||
} else {
|
||||
p.on(ParticipantEvent.TrackSubscribed, onTrackSubscribed)
|
||||
p.on(ParticipantEvent.TrackUnsubscribed, onTrackUnsubscribed)
|
||||
}
|
||||
}
|
||||
|
||||
$: setParticipant(participant)
|
||||
|
||||
onDestroy(() => {
|
||||
lk.off(RoomEvent.ChatMessage, onChatMessage)
|
||||
detachCurrentTrack()
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
lk.on(RoomEvent.ChatMessage, onChatMessage)
|
||||
setParticipant(participant)
|
||||
})
|
||||
</script>
|
||||
|
||||
<div id={_id} class="parent" class:speach={isSpeaking}>
|
||||
<div bind:this={parent} class="cover" class:active={!videoMuted} class:mirror={mirror && !videoMuted} />
|
||||
<div class="ava">
|
||||
<Avatar size={'full'} name={userName} person={user} showStatus={false} />
|
||||
</div>
|
||||
<div class="label" class:withIcon={microphoneMuted || isBadConnection || participant === undefined}>
|
||||
{#if participant === undefined}<Loading size={'small'} shrink />{/if}
|
||||
{#if isBadConnection}<BadConnection fill={'var(--bg-negative-default)'} size={'small'} />{/if}
|
||||
{#if microphoneMuted}<MicDisabled fill={'var(--bg-negative-default)'} size={'small'} />{/if}
|
||||
<span class="overflow-label">{formatName(userName)}</span>
|
||||
</div>
|
||||
{#each reactions as reaction (reaction.id)}
|
||||
<Reaction
|
||||
{...reaction}
|
||||
on:complete={() => {
|
||||
reactions = reactions.filter((it) => it !== reaction)
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
:global(.video) {
|
||||
object-fit: cover;
|
||||
border-radius: 0.75rem;
|
||||
width: 100%;
|
||||
}
|
||||
:global(.hidden) {
|
||||
display: none;
|
||||
}
|
||||
.cover {
|
||||
object-fit: cover;
|
||||
border-radius: 0.75rem;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
aspect-ratio: 1280 / 720;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&.mirror {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
&.active + .ava {
|
||||
display: none;
|
||||
}
|
||||
&:not(.active) {
|
||||
background-color: black;
|
||||
}
|
||||
}
|
||||
.ava {
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
height: 50%;
|
||||
aspect-ratio: 1;
|
||||
border-radius: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
.parent {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
height: max-content;
|
||||
min-height: 0;
|
||||
max-height: 100%;
|
||||
background-color: black;
|
||||
border-radius: 0.75rem;
|
||||
|
||||
.label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
top: 0.25rem;
|
||||
left: 0.25rem;
|
||||
max-width: 12rem;
|
||||
font-weight: 500;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1rem;
|
||||
color: var(--white-color);
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 0.5rem;
|
||||
backdrop-filter: blur(3px);
|
||||
|
||||
&.withIcon {
|
||||
padding-left: 0.25rem;
|
||||
}
|
||||
}
|
||||
&.speach::before,
|
||||
&.speach::after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
inset: 0;
|
||||
border-radius: 0.75rem;
|
||||
z-index: 1;
|
||||
}
|
||||
&.speach::before {
|
||||
border: 3px solid var(--border-talk-indication-secondary);
|
||||
}
|
||||
&.speach::after {
|
||||
border: 2px solid var(--border-talk-indication-primary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,105 @@
|
||||
<script lang="ts">
|
||||
import { aiBotSocialIdentityStore } from '@hcengineering/ai-bot-resources'
|
||||
import ParticipantView from './ParticipantView.svelte'
|
||||
import { Participant, RemoteParticipant, RoomEvent } from 'livekit-client'
|
||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
|
||||
import { liveKitClient, lk } from '../../utils'
|
||||
import { infos } from '../../stores'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { Person } from '@hcengineering/contact'
|
||||
import { getPersonRefByPersonIdCb } from '@hcengineering/contact-resources'
|
||||
import { Room as TypeRoom } from '@hcengineering/love'
|
||||
|
||||
export let room: Ref<TypeRoom>
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
interface ParticipantData {
|
||||
_id: string
|
||||
participant: Participant | undefined
|
||||
isAgent: boolean
|
||||
}
|
||||
|
||||
let aiPersonRef: Ref<Person> | undefined
|
||||
$: if ($aiBotSocialIdentityStore != null) {
|
||||
getPersonRefByPersonIdCb($aiBotSocialIdentityStore?._id, (ref) => {
|
||||
if (ref != null) {
|
||||
aiPersonRef = ref
|
||||
}
|
||||
})
|
||||
} else {
|
||||
aiPersonRef = undefined
|
||||
}
|
||||
|
||||
let participants: ParticipantData[] = []
|
||||
|
||||
function attachParticipant (participant: Participant): void {
|
||||
const current = participants.find((p) => p._id === participant.identity)
|
||||
if (current !== undefined) {
|
||||
current.participant = participant
|
||||
participants = participants
|
||||
return
|
||||
}
|
||||
const value: ParticipantData = {
|
||||
_id: participant.identity,
|
||||
participant,
|
||||
isAgent: participant.isAgent
|
||||
}
|
||||
participants.push(value)
|
||||
participants = participants
|
||||
}
|
||||
|
||||
function handleParticipantDisconnected (participant: RemoteParticipant): void {
|
||||
const index = participants.findIndex((p) => p._id === participant.identity)
|
||||
if (index !== -1) {
|
||||
participants.splice(index, 1)
|
||||
participants = participants
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await liveKitClient.awaitConnect()
|
||||
for (const participant of lk.remoteParticipants.values()) {
|
||||
attachParticipant(participant)
|
||||
}
|
||||
attachParticipant(lk.localParticipant)
|
||||
lk.on(RoomEvent.ParticipantConnected, attachParticipant)
|
||||
lk.on(RoomEvent.ParticipantDisconnected, handleParticipantDisconnected)
|
||||
})
|
||||
|
||||
onDestroy(
|
||||
infos.subscribe((data) => {
|
||||
for (const info of data) {
|
||||
if (info.room !== room) continue
|
||||
const current = participants.find((p) => p._id === info.person)
|
||||
if (current !== undefined) continue
|
||||
const value: ParticipantData = {
|
||||
_id: info.person,
|
||||
participant: undefined,
|
||||
isAgent: info.person === aiPersonRef
|
||||
}
|
||||
participants.push(value)
|
||||
}
|
||||
participants = participants
|
||||
})
|
||||
)
|
||||
|
||||
onDestroy(() => {
|
||||
lk.off(RoomEvent.ParticipantConnected, attachParticipant)
|
||||
lk.off(RoomEvent.ParticipantDisconnected, handleParticipantDisconnected)
|
||||
})
|
||||
|
||||
function getActiveParticipants (participants: ParticipantData[]): ParticipantData[] {
|
||||
const result = participants.filter((p) => !p.isAgent || $infos.some(({ person }) => person === p._id))
|
||||
dispatch('participantsCount', result.length)
|
||||
return result
|
||||
}
|
||||
|
||||
$: activeParticipants = getActiveParticipants(participants)
|
||||
</script>
|
||||
|
||||
{#each activeParticipants as participant, i (participant._id)}
|
||||
<div class="video">
|
||||
<ParticipantView {...participant} />
|
||||
</div>
|
||||
{/each}
|
||||
@@ -0,0 +1,64 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
|
||||
export let emoji: string = ''
|
||||
export let width: number = 0
|
||||
export let height: number = 0
|
||||
export let duration: number = 2000
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let offsetX: number = 0
|
||||
let isVisible: boolean = false
|
||||
let x: number = 0
|
||||
let size: number = 0
|
||||
|
||||
onMount(() => {
|
||||
offsetX = (Math.random() * width) / 4 - width / 8
|
||||
size = height / 4
|
||||
x = width / 2 - size / 2
|
||||
isVisible = true
|
||||
setTimeout(() => {
|
||||
isVisible = false
|
||||
dispatch('complete')
|
||||
}, duration)
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if isVisible}
|
||||
<div
|
||||
class="reaction"
|
||||
style="
|
||||
left: {x}px;
|
||||
font-size: {size}px;
|
||||
--x-offset: {offsetX}px;
|
||||
--duration: {duration}ms;
|
||||
"
|
||||
>
|
||||
{emoji}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.reaction {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
pointer-events: none;
|
||||
z-index: 100;
|
||||
opacity: 1;
|
||||
animation: floatUp var(--duration) ease-out forwards;
|
||||
will-change: transform, opacity;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
@keyframes floatUp {
|
||||
0% {
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translate3d(var(--x-offset), -100px, 0) scale(1.5);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,102 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import { liveKitClient, lk } from '../../utils'
|
||||
import {
|
||||
LocalParticipant,
|
||||
LocalTrackPublication,
|
||||
RemoteParticipant,
|
||||
RemoteTrack,
|
||||
RemoteTrackPublication,
|
||||
RoomEvent,
|
||||
Track
|
||||
} from 'livekit-client'
|
||||
|
||||
export let hasActiveTrack: boolean = false
|
||||
export let showLocalTrack: boolean = true
|
||||
|
||||
let activeTrack: Track | null = null
|
||||
let screen: HTMLVideoElement
|
||||
|
||||
function trySetActiveTrack (track: Track | undefined): boolean {
|
||||
if (track === undefined) return false
|
||||
if (track.kind !== Track.Kind.Video || track.source !== Track.Source.ScreenShare) return false
|
||||
hasActiveTrack = true
|
||||
activeTrack = track
|
||||
track.attach(screen)
|
||||
return true
|
||||
}
|
||||
|
||||
function clearActiveTrack (track: Track | undefined): void {
|
||||
if (track !== activeTrack) return
|
||||
hasActiveTrack = false
|
||||
activeTrack?.detach()
|
||||
activeTrack = null
|
||||
}
|
||||
|
||||
function onTrackSubscribed (
|
||||
track: RemoteTrack,
|
||||
publication: RemoteTrackPublication,
|
||||
participant: RemoteParticipant
|
||||
): void {
|
||||
trySetActiveTrack(track)
|
||||
}
|
||||
|
||||
function onTrackUnsubscribed (
|
||||
track: RemoteTrack,
|
||||
publication: RemoteTrackPublication,
|
||||
participant: RemoteParticipant
|
||||
): void {
|
||||
clearActiveTrack(track)
|
||||
}
|
||||
|
||||
function onLocalTrackPublished (publication: LocalTrackPublication, participant: LocalParticipant): void {
|
||||
trySetActiveTrack(publication.track)
|
||||
}
|
||||
|
||||
function onLocalTrackUnpublished (publication: LocalTrackPublication, participant: LocalParticipant): void {
|
||||
clearActiveTrack(publication.track)
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await liveKitClient.awaitConnect()
|
||||
|
||||
lk.on(RoomEvent.TrackSubscribed, onTrackSubscribed)
|
||||
lk.on(RoomEvent.TrackUnsubscribed, onTrackUnsubscribed)
|
||||
for (const participant of lk.remoteParticipants.values()) {
|
||||
for (const publication of participant.trackPublications.values()) {
|
||||
if (trySetActiveTrack(publication.track)) break
|
||||
}
|
||||
}
|
||||
|
||||
if (showLocalTrack) {
|
||||
lk.on(RoomEvent.LocalTrackPublished, onLocalTrackPublished)
|
||||
lk.on(RoomEvent.LocalTrackUnpublished, onLocalTrackUnpublished)
|
||||
for (const publication of lk.localParticipant.trackPublications.values()) {
|
||||
if (trySetActiveTrack(publication.track)) break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
activeTrack?.detach(screen)
|
||||
lk.off(RoomEvent.TrackSubscribed, onTrackSubscribed)
|
||||
lk.off(RoomEvent.TrackUnsubscribed, onTrackUnsubscribed)
|
||||
if (showLocalTrack) {
|
||||
lk.off(RoomEvent.LocalTrackPublished, onLocalTrackPublished)
|
||||
lk.off(RoomEvent.LocalTrackUnpublished, onLocalTrackUnpublished)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<video class="screen" bind:this={screen}></video>
|
||||
|
||||
<style lang="scss">
|
||||
.screen {
|
||||
object-fit: contain;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
@@ -20,10 +20,10 @@
|
||||
|
||||
import love from '../../plugin'
|
||||
import VideoTab from './VideoTab.svelte'
|
||||
import { isCurrentInstanceConnected } from '../../utils'
|
||||
import { currentMeetingMinutes, currentRoom } from '../../stores'
|
||||
import ChatTab from './ChatTab.svelte'
|
||||
import TranscriptionTab from './TranscriptionTab.svelte'
|
||||
import { lkSessionConnected } from '../../liveKitClient'
|
||||
|
||||
export let widgetState: WidgetState | undefined
|
||||
export let height: string
|
||||
@@ -39,7 +39,7 @@
|
||||
$: room = $currentRoom
|
||||
|
||||
$: if (
|
||||
!$isCurrentInstanceConnected ||
|
||||
!$lkSessionConnected ||
|
||||
widgetState?.data?.room === undefined ||
|
||||
$currentRoom === undefined ||
|
||||
$currentRoom._id !== widgetState?.data?.room
|
||||
|
||||
@@ -18,8 +18,10 @@
|
||||
import { AppItem } from '@hcengineering/workbench-resources'
|
||||
import { RoomType } from '@hcengineering/love'
|
||||
import { currentRoom } from '../../stores'
|
||||
import { isConnected, isSharingEnabled, isCameraEnabled, isMicEnabled } from '../../utils'
|
||||
import { isSharingEnabled } from '../../utils'
|
||||
import { state } from '@hcengineering/media-resources'
|
||||
import love from '../../plugin'
|
||||
import { lkSessionConnected } from '../../liveKitClient'
|
||||
|
||||
export let label: IntlString
|
||||
export let icon: Asset | AnySvelteComponent
|
||||
@@ -27,28 +29,30 @@
|
||||
export let size: 'small' | 'medium' | 'large' = 'small'
|
||||
|
||||
$: allowCam = $currentRoom?.type === RoomType.Video
|
||||
$: isMicEnabled = $state.microphone?.enabled === true
|
||||
$: isCamEnabled = $state.camera?.enabled === true
|
||||
</script>
|
||||
|
||||
<AppItem
|
||||
{label}
|
||||
icon={$isSharingEnabled
|
||||
? love.icon.SharingDisabled
|
||||
: $isConnected && allowCam && !$isCameraEnabled && !$isMicEnabled
|
||||
: $lkSessionConnected && allowCam && !isCamEnabled && !isMicEnabled
|
||||
? love.icon.CamDisabled
|
||||
: $isConnected && !allowCam && $isMicEnabled
|
||||
: $lkSessionConnected && !allowCam && isMicEnabled
|
||||
? love.icon.Mic
|
||||
: !allowCam || (!$isCameraEnabled && $isMicEnabled)
|
||||
: !allowCam || (!isCamEnabled && isMicEnabled)
|
||||
? love.icon.Mic
|
||||
: icon}
|
||||
{selected}
|
||||
{size}
|
||||
kind={$isSharingEnabled
|
||||
? 'negative'
|
||||
: $isConnected && $isCameraEnabled
|
||||
: $lkSessionConnected && isCamEnabled
|
||||
? 'positive'
|
||||
: $isConnected && $isMicEnabled
|
||||
: $lkSessionConnected && isMicEnabled
|
||||
? 'warning'
|
||||
: $isConnected && !($isCameraEnabled && $isMicEnabled)
|
||||
: $lkSessionConnected && !(isCamEnabled && isMicEnabled)
|
||||
? 'accented'
|
||||
: 'default'}
|
||||
on:click
|
||||
|
||||
@@ -36,11 +36,10 @@ import {
|
||||
showRoomSettings,
|
||||
startTranscription,
|
||||
stopTranscription,
|
||||
toggleMic,
|
||||
toggleVideo,
|
||||
getMeetingMinutesTitle,
|
||||
queryMeetingMinutes
|
||||
} from './utils'
|
||||
import { toggleMicState, toggleCamState } from '@hcengineering/media-resources'
|
||||
|
||||
export { setCustomCreateScreenTracks } from './utils'
|
||||
|
||||
@@ -90,8 +89,8 @@ export default async (): Promise<Resources> => ({
|
||||
MeetingMinutesTitleProvider: getMeetingMinutesTitle
|
||||
},
|
||||
actionImpl: {
|
||||
ToggleMic: toggleMic,
|
||||
ToggleVideo: toggleVideo,
|
||||
ToggleMic: toggleMicState,
|
||||
ToggleVideo: toggleCamState,
|
||||
StartTranscribing: startTranscription,
|
||||
StopTranscribing: stopTranscription,
|
||||
ShowRoomSettings: showRoomSettings,
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
import { ConnectionState, type RemoteParticipant, Room as LKRoom, RoomEvent } from 'livekit-client'
|
||||
import { getMetadata, translate } from '@hcengineering/platform'
|
||||
import { getSelectedSpeakerId, type MediaSession } from '@hcengineering/media'
|
||||
import { LoveEvents } from '@hcengineering/love'
|
||||
import { useMedia } from '@hcengineering/media-resources'
|
||||
import { get, writable } from 'svelte/store'
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
import { addNotification, NotificationSeverity } from '@hcengineering/ui'
|
||||
import { getCurrentLanguage } from '@hcengineering/theme'
|
||||
import LastParticipantNotification from './components/meeting/LastParticipantNotification.svelte'
|
||||
import love from './plugin'
|
||||
import { leaveRoom } from './utils'
|
||||
import { myInfo, myOffice } from './stores'
|
||||
|
||||
export const lkSessionConnected = writable<boolean>(false)
|
||||
|
||||
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 ?? '')
|
||||
}
|
||||
|
||||
export class LiveKitClient {
|
||||
public readonly liveKitRoom: LKRoom
|
||||
|
||||
public currentMediaSession: MediaSession | undefined = undefined
|
||||
private currentSessionSupportsVideo: boolean = false
|
||||
private lastParticipantNotificationTimeout: number = -1
|
||||
private lastParticipantDisconnectTimeout: number = -1
|
||||
|
||||
private readonly wsUrl: string
|
||||
|
||||
constructor (wsUrl: string) {
|
||||
this.wsUrl = wsUrl
|
||||
const lkRoom = new LKRoom({
|
||||
adaptiveStream: true,
|
||||
dynacast: true,
|
||||
publishDefaults: {
|
||||
videoCodec: 'vp9',
|
||||
screenShareEncoding: {
|
||||
maxBitrate: 7_000_000,
|
||||
maxFramerate: 15,
|
||||
priority: 'high'
|
||||
}
|
||||
},
|
||||
audioCaptureDefaults: {
|
||||
autoGainControl: true,
|
||||
echoCancellation: true,
|
||||
noiseSuppression: true
|
||||
},
|
||||
audioOutput: {
|
||||
deviceId: getSelectedSpeakerId()
|
||||
},
|
||||
videoCaptureDefaults: {
|
||||
facingMode: 'user',
|
||||
resolution: {
|
||||
width: 1280,
|
||||
height: 720,
|
||||
frameRate: 30
|
||||
}
|
||||
}
|
||||
})
|
||||
void lkRoom.prepareConnection(this.wsUrl)
|
||||
lkRoom.on(RoomEvent.Connected, this.onConnected)
|
||||
lkRoom.on(RoomEvent.Disconnected, this.onDisconnected)
|
||||
this.liveKitRoom = lkRoom
|
||||
}
|
||||
|
||||
async connect (token: string, withVideo: boolean): Promise<void> {
|
||||
this.currentSessionSupportsVideo = withVideo
|
||||
await this.liveKitRoom.connect(this.wsUrl, token)
|
||||
}
|
||||
|
||||
async disconnect (): Promise<void> {
|
||||
clearTimeout(this.lastParticipantNotificationTimeout)
|
||||
const me = this.liveKitRoom.localParticipant
|
||||
await Promise.all([me.setScreenShareEnabled(false), me.setCameraEnabled(false), me.setMicrophoneEnabled(false)])
|
||||
await this.liveKitRoom.disconnect()
|
||||
this.currentSessionSupportsVideo = false
|
||||
}
|
||||
|
||||
async awaitConnect (): Promise<void> {
|
||||
await new Promise<void>((resolve) => {
|
||||
if (this.liveKitRoom.state === ConnectionState.Connected) {
|
||||
resolve()
|
||||
}
|
||||
this.liveKitRoom.once(RoomEvent.Connected, () => {
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
onConnected = (): void => {
|
||||
const session = useMedia({
|
||||
state: {
|
||||
camera: this.currentSessionSupportsVideo ? { enabled: false } : undefined,
|
||||
microphone: { enabled: false }
|
||||
},
|
||||
autoDestroy: false
|
||||
})
|
||||
lkSessionConnected.set(true)
|
||||
this.currentMediaSession = session
|
||||
this.liveKitRoom.on(RoomEvent.ParticipantConnected, this.onParticipantConnected)
|
||||
this.liveKitRoom.on(RoomEvent.ParticipantDisconnected, this.onParticipantDisconnected)
|
||||
}
|
||||
|
||||
onDisconnected = (): void => {
|
||||
this.currentMediaSession?.close()
|
||||
this.currentMediaSession?.removeAllListeners()
|
||||
this.currentMediaSession = undefined
|
||||
lkSessionConnected.set(false)
|
||||
this.liveKitRoom.off(RoomEvent.ParticipantConnected, this.onParticipantConnected)
|
||||
this.liveKitRoom.off(RoomEvent.ParticipantDisconnected, this.onParticipantDisconnected)
|
||||
Analytics.handleEvent(LoveEvents.DisconnectedFromRoom)
|
||||
}
|
||||
|
||||
onParticipantConnected = (participant: RemoteParticipant): void => {
|
||||
clearTimeout(this.lastParticipantDisconnectTimeout)
|
||||
clearTimeout(this.lastParticipantNotificationTimeout)
|
||||
}
|
||||
|
||||
onParticipantDisconnected = (participant: RemoteParticipant): void => {
|
||||
if (this.liveKitRoom.remoteParticipants.size === 0) {
|
||||
clearTimeout(this.lastParticipantDisconnectTimeout)
|
||||
clearTimeout(this.lastParticipantNotificationTimeout)
|
||||
this.lastParticipantNotificationTimeout = window.setTimeout(() => {
|
||||
void this.showLastParticipantNotification()
|
||||
}, LAST_PARTICIPANT_NOTIFICATION_DELAY_MS)
|
||||
}
|
||||
}
|
||||
|
||||
async showLastParticipantNotification (): Promise<void> {
|
||||
addNotification(
|
||||
await translate(love.string.MeetingEmptyTitle, {}, getCurrentLanguage()),
|
||||
await translate(love.string.MeetingEmptyMessage, {}, getCurrentLanguage()),
|
||||
LastParticipantNotification,
|
||||
undefined,
|
||||
NotificationSeverity.Info,
|
||||
'love'
|
||||
)
|
||||
this.lastParticipantDisconnectTimeout = window.setTimeout(() => {
|
||||
void leaveRoom(get(myInfo), get(myOffice))
|
||||
}, AUTO_DISCONNECT_DELAY_MS)
|
||||
}
|
||||
}
|
||||
@@ -105,6 +105,9 @@ export default mergeIds(loveId, love, {
|
||||
Sharing: '' as IntlString,
|
||||
Browser: '' as IntlString,
|
||||
Monitor: '' as IntlString,
|
||||
Window: '' as IntlString
|
||||
Window: '' as IntlString,
|
||||
MeetingEmptyTitle: '' as IntlString,
|
||||
MeetingEmptyMessage: '' as IntlString,
|
||||
StayInRoom: '' as IntlString
|
||||
}
|
||||
})
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
type ParticipantInfo,
|
||||
type Room
|
||||
} from '@hcengineering/love'
|
||||
import { type MediaSession } from '@hcengineering/media'
|
||||
import { createQuery, onClient } from '@hcengineering/presentation'
|
||||
import { derived, get, writable } from 'svelte/store'
|
||||
|
||||
@@ -60,7 +59,6 @@ export let $myPreferences: DevicesPreference | undefined
|
||||
|
||||
export const currentMeetingMinutes = writable<MeetingMinutes | undefined>(undefined)
|
||||
export const selectedRoomPlace = writable<{ _id: Ref<Room>, x: number, y: number } | undefined>(undefined)
|
||||
export const currentSession = writable<MediaSession | undefined>(undefined)
|
||||
|
||||
async function filterParticipantInfo (value: ParticipantInfo[]): Promise<ParticipantInfo[]> {
|
||||
const map = new Map<string, ParticipantInfo>()
|
||||
|
||||
@@ -40,8 +40,7 @@ import {
|
||||
RoomType,
|
||||
TranscriptionStatus
|
||||
} from '@hcengineering/love'
|
||||
import { getSelectedSpeakerId } from '@hcengineering/media'
|
||||
import { getSelectedCam, getSelectedMic, getSelectedSpeaker, useMedia } from '@hcengineering/media-resources'
|
||||
import { getSelectedCam, getSelectedMic, getSelectedSpeaker } from '@hcengineering/media-resources'
|
||||
import { getEmbeddedLabel, getMetadata, getResource, type IntlString } from '@hcengineering/platform'
|
||||
import presentation, {
|
||||
copyTextToClipboard,
|
||||
@@ -65,16 +64,14 @@ import { isKrispNoiseFilterSupported, KrispNoiseFilter } from '@livekit/krisp-no
|
||||
import { BackgroundBlur, type BackgroundOptions, type ProcessorWrapper } from '@livekit/track-processors'
|
||||
import {
|
||||
type AudioCaptureOptions,
|
||||
ConnectionState,
|
||||
Room as LKRoom,
|
||||
LocalAudioTrack,
|
||||
type LocalTrack,
|
||||
type LocalTrackPublication,
|
||||
LocalVideoTrack,
|
||||
type Participant,
|
||||
type RemoteParticipant,
|
||||
type RemoteTrack,
|
||||
type RemoteTrackPublication,
|
||||
type Room as LKRoom,
|
||||
RoomEvent,
|
||||
type ScreenShareCaptureOptions,
|
||||
Track,
|
||||
@@ -83,18 +80,11 @@ import {
|
||||
import { get, writable } from 'svelte/store'
|
||||
|
||||
import { getPersonByPersonRef } from '@hcengineering/contact-resources'
|
||||
import { sendMessage } from './broadcast'
|
||||
import MeetingMinutesSearchItem from './components/MeetingMinutesSearchItem.svelte'
|
||||
import RoomSettingsPopup from './components/RoomSettingsPopup.svelte'
|
||||
import love from './plugin'
|
||||
import {
|
||||
$myPreferences,
|
||||
currentMeetingMinutes,
|
||||
currentRoom,
|
||||
currentSession,
|
||||
myOffice,
|
||||
selectedRoomPlace
|
||||
} from './stores'
|
||||
import { $myPreferences, currentMeetingMinutes, currentRoom, myOffice, selectedRoomPlace } from './stores'
|
||||
import { getLiveKitClient } from './liveKitClient'
|
||||
|
||||
export async function getToken (
|
||||
roomName: string,
|
||||
@@ -128,66 +118,21 @@ function getTokenRoomName (roomName: string, roomId: Ref<Room>): string {
|
||||
return `${currentWorkspace.uuid}_${roomName}_${roomId}`
|
||||
}
|
||||
|
||||
export const lk: LKRoom = new LKRoom({
|
||||
adaptiveStream: true,
|
||||
dynacast: true,
|
||||
publishDefaults: {
|
||||
videoCodec: 'vp9',
|
||||
screenShareEncoding: {
|
||||
maxBitrate: 7_000_000,
|
||||
maxFramerate: 15,
|
||||
priority: 'high'
|
||||
}
|
||||
},
|
||||
audioCaptureDefaults: {
|
||||
autoGainControl: true,
|
||||
echoCancellation: true,
|
||||
noiseSuppression: true
|
||||
},
|
||||
audioOutput: {
|
||||
deviceId: getSelectedSpeakerId()
|
||||
},
|
||||
videoCaptureDefaults: {
|
||||
facingMode: 'user',
|
||||
resolution: {
|
||||
width: 1280,
|
||||
height: 720,
|
||||
frameRate: 30
|
||||
}
|
||||
}
|
||||
})
|
||||
export const liveKitClient = getLiveKitClient()
|
||||
export const lk: LKRoom = liveKitClient.liveKitRoom
|
||||
|
||||
export function setCustomCreateScreenTracks (value: () => Promise<Array<LocalTrack<Track.Kind>>>): void {
|
||||
lk.localParticipant.createScreenTracks = value
|
||||
}
|
||||
|
||||
async function prepare (): Promise<void> {
|
||||
const wsURL = getMetadata(love.metadata.WebSocketURL)
|
||||
if (wsURL !== undefined) {
|
||||
await lk.prepareConnection(wsURL)
|
||||
}
|
||||
}
|
||||
|
||||
void prepare()
|
||||
|
||||
export const isConnected = writable<boolean>(false)
|
||||
export const isCurrentInstanceConnected = writable<boolean>(false)
|
||||
export let $isCurrentInstanceConnected: boolean = false
|
||||
isCurrentInstanceConnected.subscribe((value) => {
|
||||
$isCurrentInstanceConnected = value
|
||||
})
|
||||
export const screenSharing = writable<boolean>(false)
|
||||
export const isRecording = writable<boolean>(false)
|
||||
export const isTranscription = writable<boolean>(false)
|
||||
export const isRecordingAvailable = writable<boolean>(false)
|
||||
export const isMicEnabled = writable<boolean>(false)
|
||||
export const isCameraEnabled = writable<boolean>(false)
|
||||
export const isSharingEnabled = writable<boolean>(false)
|
||||
export const isFullScreen = writable<boolean>(false)
|
||||
export const isShareWithSound = writable<boolean>(false)
|
||||
|
||||
export const currentRoomAudioLevels = writable<Map<Ref<Person>, number>>(new Map())
|
||||
|
||||
function handleTrackSubscribed (
|
||||
track: RemoteTrack,
|
||||
publication: RemoteTrackPublication,
|
||||
@@ -210,7 +155,7 @@ function handleTrackUnsubscribed (
|
||||
|
||||
lk.on(RoomEvent.TrackSubscribed, handleTrackSubscribed)
|
||||
lk.on(RoomEvent.LocalTrackPublished, (pub, part) => {
|
||||
const session = get(currentSession)
|
||||
const session = liveKitClient.currentMediaSession
|
||||
const track = pub.track?.mediaStreamTrack
|
||||
const deviceId = track?.getSettings().deviceId
|
||||
if (pub.track?.kind === Track.Kind.Video) {
|
||||
@@ -218,62 +163,47 @@ lk.on(RoomEvent.LocalTrackPublished, (pub, part) => {
|
||||
session?.setFeature('sharing', { enabled: true, track, deviceId })
|
||||
screenSharing.set(true)
|
||||
isSharingEnabled.set(true)
|
||||
sendMessage({ type: 'share', value: true })
|
||||
} else {
|
||||
session?.setCamera({ enabled: true, track, deviceId })
|
||||
isCameraEnabled.set(true)
|
||||
sendMessage({ type: 'cam', value: true })
|
||||
}
|
||||
} else if (pub.track?.kind === Track.Kind.Audio) {
|
||||
session?.setMicrophone({ enabled: true, track, deviceId })
|
||||
isMicEnabled.set(!pub.track?.isMuted)
|
||||
sendMessage({ type: 'mic', value: !pub.track?.isMuted })
|
||||
}
|
||||
})
|
||||
lk.on(RoomEvent.LocalTrackUnpublished, (pub, part) => {
|
||||
const session = get(currentSession)
|
||||
const session = liveKitClient.currentMediaSession
|
||||
if (pub.track?.kind === Track.Kind.Video) {
|
||||
if (pub.track.source === Track.Source.ScreenShare) {
|
||||
session?.setFeature('sharing', { enabled: false })
|
||||
screenSharing.set(false)
|
||||
isSharingEnabled.set(false)
|
||||
sendMessage({ type: 'share', value: false })
|
||||
} else {
|
||||
session?.setCamera({ enabled: false })
|
||||
isCameraEnabled.set(false)
|
||||
sendMessage({ type: 'cam', value: false })
|
||||
}
|
||||
} else if (pub.track?.kind === Track.Kind.Audio) {
|
||||
session?.setMicrophone({ enabled: false })
|
||||
isMicEnabled.set(false)
|
||||
sendMessage({ type: 'mic', value: false })
|
||||
}
|
||||
})
|
||||
lk.on(RoomEvent.TrackUnsubscribed, handleTrackUnsubscribed)
|
||||
lk.on(RoomEvent.TrackMuted, (pub, participant) => {
|
||||
if (participant.isLocal) {
|
||||
const session = get(currentSession)
|
||||
const session = liveKitClient.currentMediaSession
|
||||
if (pub.track?.kind === Track.Kind.Video) {
|
||||
if (pub.track.source === Track.Source.ScreenShare) {
|
||||
session?.setFeature('sharing', { enabled: false })
|
||||
screenSharing.set(false)
|
||||
isSharingEnabled.set(false)
|
||||
sendMessage({ type: 'share', value: false })
|
||||
} else {
|
||||
session?.setCamera({ enabled: false })
|
||||
isCameraEnabled.set(false)
|
||||
sendMessage({ type: 'cam', value: false })
|
||||
}
|
||||
} else if (pub.track?.kind === Track.Kind.Audio) {
|
||||
session?.setMicrophone({ enabled: false })
|
||||
isMicEnabled.set(false)
|
||||
sendMessage({ type: 'mic', value: false })
|
||||
}
|
||||
}
|
||||
})
|
||||
lk.on(RoomEvent.TrackUnmuted, (pub, participant) => {
|
||||
if (participant.isLocal) {
|
||||
const session = get(currentSession)
|
||||
const session = liveKitClient.currentMediaSession
|
||||
const track = pub.track?.mediaStreamTrack
|
||||
const deviceId = track?.getSettings().deviceId
|
||||
if (pub.track?.kind === Track.Kind.Video) {
|
||||
@@ -281,16 +211,11 @@ lk.on(RoomEvent.TrackUnmuted, (pub, participant) => {
|
||||
session?.setFeature('sharing', { enabled: true })
|
||||
screenSharing.set(true)
|
||||
isSharingEnabled.set(true)
|
||||
sendMessage({ type: 'share', value: true })
|
||||
} else {
|
||||
session?.setCamera({ enabled: true, track, deviceId })
|
||||
isCameraEnabled.set(true)
|
||||
sendMessage({ type: 'cam', value: true })
|
||||
}
|
||||
} else if (pub.track?.kind === Track.Kind.Audio) {
|
||||
session?.setMicrophone({ enabled: true, track, deviceId })
|
||||
isMicEnabled.set(true)
|
||||
sendMessage({ type: 'mic', value: true })
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -381,7 +306,6 @@ lk.on(RoomEvent.LocalTrackPublished, (pub) => {
|
||||
if (pub.track?.kind === Track.Kind.Video && pub.track.source === Track.Source.ScreenShare) {
|
||||
screenSharing.set(true)
|
||||
isSharingEnabled.set(true)
|
||||
sendMessage({ type: 'share', value: true })
|
||||
}
|
||||
|
||||
if (pub.source === Track.Source.Microphone) {
|
||||
@@ -397,7 +321,6 @@ lk.on(RoomEvent.LocalTrackUnpublished, (pub) => {
|
||||
if (pub.track.source === Track.Source.ScreenShare) {
|
||||
screenSharing.set(false)
|
||||
isSharingEnabled.set(false)
|
||||
sendMessage({ type: 'share', value: false })
|
||||
} else if (pub.track.source === Track.Source.Camera) {
|
||||
if (localVideo !== undefined) {
|
||||
localVideo = undefined
|
||||
@@ -418,30 +341,18 @@ lk.on(RoomEvent.RoomMetadataChanged, (metadata) => {
|
||||
}
|
||||
})
|
||||
|
||||
lk.on(RoomEvent.ActiveSpeakersChanged, (speakers: Participant[]) => {
|
||||
currentRoomAudioLevels.set(new Map(speakers.map((it) => [it.identity as Ref<Person>, it.audioLevel])))
|
||||
})
|
||||
|
||||
lk.on(RoomEvent.Connected, () => {
|
||||
const current = get(currentRoom)
|
||||
const session = liveKitClient.currentMediaSession
|
||||
|
||||
const session = useMedia({
|
||||
state: {
|
||||
camera: current?.type === RoomType.Video ? { enabled: false } : undefined,
|
||||
microphone: { enabled: false }
|
||||
},
|
||||
autoDestroy: false
|
||||
})
|
||||
|
||||
session.on('camera', (enabled) => {
|
||||
session?.on('camera', (enabled) => {
|
||||
void setCam(enabled)
|
||||
})
|
||||
|
||||
session.on('microphone', (enabled) => {
|
||||
session?.on('microphone', (enabled) => {
|
||||
void setMic(enabled)
|
||||
})
|
||||
|
||||
session.on('selected-camera', (deviceId) => {
|
||||
session?.on('selected-camera', (deviceId) => {
|
||||
if (deviceId === null) {
|
||||
if (lk.localParticipant.isCameraEnabled) {
|
||||
void setCam(false)
|
||||
@@ -455,7 +366,7 @@ lk.on(RoomEvent.Connected, () => {
|
||||
}
|
||||
})
|
||||
|
||||
session.on('selected-microphone', (deviceId) => {
|
||||
session?.on('selected-microphone', (deviceId) => {
|
||||
if (lk.localParticipant.isMicrophoneEnabled) {
|
||||
void lk.switchActiveDevice('audioinput', deviceId)
|
||||
} else {
|
||||
@@ -463,35 +374,19 @@ lk.on(RoomEvent.Connected, () => {
|
||||
}
|
||||
})
|
||||
|
||||
session.on('selected-speaker', (deviceId) => {
|
||||
session?.on('selected-speaker', (deviceId) => {
|
||||
void lk.switchActiveDevice('audiooutput', deviceId)
|
||||
})
|
||||
|
||||
session.on('feature', (feature, enabled) => {
|
||||
session?.on('feature', (feature, enabled) => {
|
||||
if (feature === 'sharing') {
|
||||
void setShare(enabled)
|
||||
}
|
||||
})
|
||||
|
||||
currentSession.set(session)
|
||||
isConnected.set(true)
|
||||
currentRoomAudioLevels.set(new Map())
|
||||
sendMessage({ type: 'connect', value: true })
|
||||
isCurrentInstanceConnected.set(true)
|
||||
isRecording.set(lk.isRecording)
|
||||
void initRoom()
|
||||
Analytics.handleEvent(LoveEvents.ConnectedToRoom)
|
||||
})
|
||||
lk.on(RoomEvent.Disconnected, () => {
|
||||
const session = get(currentSession)
|
||||
session?.close()
|
||||
session?.removeAllListeners()
|
||||
currentSession.set(undefined)
|
||||
isConnected.set(false)
|
||||
sendMessage({ type: 'connect', value: true })
|
||||
isCurrentInstanceConnected.set(false)
|
||||
Analytics.handleEvent(LoveEvents.DisconnectedFromRoom)
|
||||
})
|
||||
|
||||
async function initRoom (): Promise<void> {
|
||||
const room = get(currentRoom)
|
||||
@@ -544,47 +439,19 @@ async function withRetries (fn: () => Promise<void>, retries: number, delay: num
|
||||
}
|
||||
}
|
||||
|
||||
async function connect (name: string, room: Room, _id: string): Promise<void> {
|
||||
const wsURL = getLiveKitEndpoint()
|
||||
if (getCurrentAccount().role === AccountRole.ReadOnlyGuest) {
|
||||
return
|
||||
}
|
||||
|
||||
const token = await getToken(room.name, room._id, _id, name)
|
||||
await lk.connect(wsURL, token)
|
||||
sendMessage({ type: 'connect', value: true })
|
||||
}
|
||||
|
||||
export async function awaitConnect (): Promise<void> {
|
||||
await new Promise<void>((resolve) => {
|
||||
if (lk.state === ConnectionState.Connected) {
|
||||
resolve()
|
||||
}
|
||||
lk.once(RoomEvent.Connected, () => {
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export async function disconnect (): Promise<void> {
|
||||
const me = lk.localParticipant
|
||||
await Promise.all([me.setScreenShareEnabled(false), me.setCameraEnabled(false), me.setMicrophoneEnabled(false)])
|
||||
await lk.disconnect()
|
||||
await liveKitClient.disconnect()
|
||||
screenSharing.set(false)
|
||||
isMicEnabled.set(false)
|
||||
isCameraEnabled.set(false)
|
||||
isSharingEnabled.set(false)
|
||||
currentRoomAudioLevels.set(new Map())
|
||||
sendMessage({ type: 'mic', value: false })
|
||||
sendMessage({ type: 'cam', value: false })
|
||||
sendMessage({ type: 'share', value: false })
|
||||
sendMessage({ type: 'connect', value: false })
|
||||
get(currentSession)?.close()
|
||||
}
|
||||
|
||||
export async function leaveRoom (ownInfo: ParticipantInfo | undefined, ownOffice: Office | undefined): Promise<void> {
|
||||
const me = lk.localParticipant
|
||||
await Promise.all([me.setScreenShareEnabled(false), me.setCameraEnabled(false), me.setMicrophoneEnabled(false)])
|
||||
try {
|
||||
await Promise.all([me.setScreenShareEnabled(false), me.setCameraEnabled(false), me.setMicrophoneEnabled(false)])
|
||||
} catch (err: any) {
|
||||
console.error(err)
|
||||
}
|
||||
if (ownInfo !== undefined) {
|
||||
const client = getClient()
|
||||
if (ownOffice !== undefined && ownInfo.room !== ownOffice._id) {
|
||||
@@ -626,67 +493,55 @@ function isRoomOpened (room: Room): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
export async function setCam (value: boolean): Promise<void> {
|
||||
async function setCam (value: boolean): Promise<void> {
|
||||
if (value && get(currentRoom)?.type !== RoomType.Video) return
|
||||
if ($isCurrentInstanceConnected) {
|
||||
try {
|
||||
const opt: VideoCaptureOptions = {}
|
||||
const selectedDevice = await getSelectedCam()
|
||||
if (selectedDevice !== null && selectedDevice !== undefined) {
|
||||
// We need to use exact device id to avoid issues with Firefox
|
||||
// that can switch to another device when using just deviceId
|
||||
opt.deviceId = { exact: selectedDevice.deviceId }
|
||||
await lk.switchActiveDevice('videoinput', selectedDevice.deviceId)
|
||||
}
|
||||
await lk.localParticipant.setCameraEnabled(value, opt)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
try {
|
||||
const opt: VideoCaptureOptions = {}
|
||||
const selectedDevice = await getSelectedCam()
|
||||
if (selectedDevice !== null && selectedDevice !== undefined) {
|
||||
// We need to use exact device id to avoid issues with Firefox
|
||||
// that can switch to another device when using just deviceId
|
||||
opt.deviceId = { exact: selectedDevice.deviceId }
|
||||
await lk.switchActiveDevice('videoinput', selectedDevice.deviceId)
|
||||
}
|
||||
} else {
|
||||
sendMessage({ type: 'set_cam', value })
|
||||
await lk.localParticipant.setCameraEnabled(value, opt)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
export async function setMic (value: boolean): Promise<void> {
|
||||
if ($isCurrentInstanceConnected) {
|
||||
try {
|
||||
const speaker = await getSelectedSpeaker()
|
||||
if (speaker !== null && speaker !== undefined) {
|
||||
await lk.switchActiveDevice('audiooutput', speaker.deviceId)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
async function setMic (value: boolean): Promise<void> {
|
||||
try {
|
||||
const speaker = await getSelectedSpeaker()
|
||||
if (speaker !== null && speaker !== undefined) {
|
||||
await lk.switchActiveDevice('audiooutput', speaker.deviceId)
|
||||
}
|
||||
try {
|
||||
const opt: AudioCaptureOptions = {}
|
||||
const selectedDevice = await getSelectedMic()
|
||||
if (selectedDevice !== null && selectedDevice !== undefined) {
|
||||
// We need to use exact device id to avoid issues with Firefox
|
||||
// that can switch to another device when using just deviceId
|
||||
opt.deviceId = { exact: selectedDevice.deviceId }
|
||||
}
|
||||
await lk.localParticipant.setMicrophoneEnabled(value, opt)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
try {
|
||||
const opt: AudioCaptureOptions = {}
|
||||
const selectedDevice = await getSelectedMic()
|
||||
if (selectedDevice !== null && selectedDevice !== undefined) {
|
||||
// We need to use exact device id to avoid issues with Firefox
|
||||
// that can switch to another device when using just deviceId
|
||||
opt.deviceId = { exact: selectedDevice.deviceId }
|
||||
}
|
||||
} else {
|
||||
sendMessage({ type: 'set_mic', value })
|
||||
await lk.localParticipant.setMicrophoneEnabled(value, opt)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
export async function setShare (value: boolean, withAudio: boolean = false): Promise<void> {
|
||||
if ($isCurrentInstanceConnected) {
|
||||
try {
|
||||
const options: ScreenShareCaptureOptions = {}
|
||||
if (withAudio) {
|
||||
options.audio = true
|
||||
}
|
||||
await lk.localParticipant.setScreenShareEnabled(value, options)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
try {
|
||||
const options: ScreenShareCaptureOptions = {}
|
||||
if (withAudio) {
|
||||
options.audio = true
|
||||
}
|
||||
} else {
|
||||
sendMessage({ type: 'set_share', value })
|
||||
await lk.localParticipant.setScreenShareEnabled(value, options)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -740,8 +595,8 @@ async function moveToRoom (
|
||||
}
|
||||
}
|
||||
|
||||
async function connectLK (currentPerson: Person, room: Room): Promise<void> {
|
||||
await connect(currentPerson.name, room, currentPerson._id)
|
||||
async function connectLK (token: string, room: Room): Promise<void> {
|
||||
await liveKitClient.connect(token, room.type === RoomType.Video)
|
||||
await Promise.all([
|
||||
setMic($myPreferences?.micEnabled ?? lk.remoteParticipants.size < 16),
|
||||
setCam(room.type === RoomType.Video && ($myPreferences?.camEnabled ?? true))
|
||||
@@ -781,13 +636,17 @@ export async function connectRoom (
|
||||
currentPerson: Person,
|
||||
room: Room
|
||||
): Promise<void> {
|
||||
await disconnect()
|
||||
await moveToRoom(x, y, currentInfo, currentPerson, room, getMetadata(presentation.metadata.SessionId) ?? null)
|
||||
selectedRoomPlace.set(undefined)
|
||||
if (getCurrentAccount().role === AccountRole.ReadOnlyGuest) {
|
||||
return
|
||||
}
|
||||
await disconnect()
|
||||
const token = await getToken(room.name, room._id, currentPerson._id, currentPerson.name)
|
||||
try {
|
||||
await withRetries(
|
||||
async () => {
|
||||
await connectLK(currentPerson, room)
|
||||
await connectLK(token, room)
|
||||
},
|
||||
3,
|
||||
1000
|
||||
@@ -949,14 +808,6 @@ export async function invite (person: Ref<Person>, room: Ref<Room> | undefined):
|
||||
})
|
||||
}
|
||||
|
||||
export async function toggleMic (): Promise<void> {
|
||||
await setMic(!get(isMicEnabled))
|
||||
}
|
||||
|
||||
export async function toggleVideo (): Promise<void> {
|
||||
await setCam(!get(isCameraEnabled))
|
||||
}
|
||||
|
||||
export async function record (room: Room): Promise<void> {
|
||||
try {
|
||||
const endpoint = getLoveEndpoint()
|
||||
|
||||
@@ -107,7 +107,8 @@ const love = plugin(loveId, {
|
||||
MeetingMinutes: '' as Asset
|
||||
},
|
||||
sound: {
|
||||
Knock: '' as Asset
|
||||
Knock: '' as Asset,
|
||||
MeetingEndNotification: '' as Asset
|
||||
},
|
||||
metadata: {
|
||||
WebSocketURL: '' as Metadata<string>,
|
||||
|
||||
Reference in New Issue
Block a user