mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-30 13:35:04 +02:00
Office: refactor livekit logic (#9679)
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user