Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(CallPopUp): Enable call pop up when user is out of active call chat #855

Merged
merged 18 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
c5612ad
feat(calling): enhance video preview functionality and improve stream…
lgmarchi Nov 14, 2024
a8090e8
feat(calling): enhance video preview layout and add user name and mut…
lgmarchi Nov 14, 2024
928b749
feat(calling): implement dynamic user grid layout in video preview an…
lgmarchi Nov 14, 2024
d5d9687
feat(calling): implement user sorting logic based on audio playback s…
lgmarchi Nov 14, 2024
7baca8f
feat(calling): update user filtering logic and limit displayed users …
lgmarchi Nov 14, 2024
9344ab2
feat(calling): update user tracking logic and add loading state for v…
lgmarchi Nov 15, 2024
37ef4d3
fix(calling): update border color in video preview to use muted color…
lgmarchi Nov 16, 2024
23da66c
feat(calling): add user count display in video preview for better use…
lgmarchi Nov 16, 2024
4eafee4
Merge branch 'dev' into Bug/SAT-854/call-video-popout-not-showing
lgmarchi Nov 18, 2024
01914e7
Merge branch 'dev' into Bug/SAT-854/call-video-popout-not-showing
phillsatellite Nov 18, 2024
4cbaa40
Merge branch 'dev' into Bug/SAT-854/call-video-popout-not-showing
stavares843 Nov 19, 2024
a8e09e1
Merge branch 'dev' into Bug/SAT-854/call-video-popout-not-showing
phillsatellite Nov 19, 2024
7c27beb
Merge branch 'dev' into Bug/SAT-854/call-video-popout-not-showing
lgmarchi Nov 21, 2024
83846cb
Merge remote-tracking branch 'origin/Bug/SAT-854/call-video-popout-no…
lgmarchi Nov 21, 2024
6b3ffbe
feat(call): implement showCallPopUp state management in CallScreen an…
lgmarchi Nov 22, 2024
f4be161
fix(video-preview): update video container display logic for screen s…
lgmarchi Nov 22, 2024
c9882f7
Merge remote-tracking branch 'origin/dev' into Bug/SAT-854/call-video…
lgmarchi Nov 22, 2024
9b30d49
feat(call): create LiveLabel component for screen sharing indication …
lgmarchi Nov 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
287 changes: 241 additions & 46 deletions src/lib/components/calling/VideoPreview.svelte
Original file line number Diff line number Diff line change
@@ -1,54 +1,108 @@
<script lang="ts">
import { VoiceRTCInstance } from "$lib/media/Voice"
import { callScreenVisible, usersDidInActiveCall } from "$lib/media/Voice"
import { Store } from "$lib/state/Store"
import { onMount } from "svelte"
import { page } from "$app/stores"
import { Route } from "$lib/enums"
import { Shape } from "$lib/enums"
import { get } from "svelte/store"
import Participant from "./Participant.svelte"
import { UIStore } from "$lib/state/ui"
import { log } from "$lib/utils/Logger"
import Icon from "$lib/elements/Icon.svelte"
import { _ } from "svelte-i18n"

export let show: boolean = false
let showVideoPreview = false
let previewVideo: HTMLDivElement
let remoteVideoElement: HTMLVideoElement

Store.state.activeChat.subscribe(async activeChat => {
if (activeChat.id !== VoiceRTCInstance.channel && get(Store.state.activeCall)) {
show = true
if (VoiceRTCInstance.remoteVideoElement) {
remoteVideoElement.srcObject = VoiceRTCInstance.activeCall?.remoteStream!
remoteVideoElement.play()
$: remoteStreams = Store.state.activeCallMeta
$: gridTemplateColumns = `repeat(${Math.min(filteredUsers.length, 3)}, 1fr)`

$: chat = get(Store.state.activeCall)?.chat
let ownUserKey = get(Store.state.user).key

function sortUsers(users: string[]) {
return users.sort((a, b) => {
const userA = $userCache[a]
const userB = $userCache[b]
if (userA.media.is_playing_audio && !userB.media.is_playing_audio) return -1
if (!userA.media.is_playing_audio && userB.media.is_playing_audio) return 1
return 0
})
}

$: filteredUsers = sortUsers($usersDidInActiveCall.filter(user => $userCache[user] && $userCache[user].key !== ownUserKey && $remoteStreams[user]) || [])
let lastSortedUsers: string[] = []

$: {
if ($usersDidInActiveCall.length > 1) {
const usersPlayingAudio = filteredUsers.filter(user => $userCache[user].media.is_playing_audio)
if (usersPlayingAudio.length > 0 && !arraysEqual(usersPlayingAudio, lastSortedUsers)) {
filteredUsers = sortUsers($usersDidInActiveCall)
usersDidInActiveCall.set(filteredUsers)
lastSortedUsers = [...usersPlayingAudio]
}
}
})
}

$: chat = get(Store.state.activeCall)?.chat
function arraysEqual(a: any[], b: any[]) {
if (a === b) return true
if (a == null || b == null) return false
if (a.length !== b.length) return false

for (let i = 0; i < a.length; ++i) {
if (a[i] !== b[i]) return false
}
return true
}

Store.state.activeCall.subscribe(async activeCall => {
log.debug(`VideoPreview: Page: ${$page.route.id}. activeCall: ${activeCall}`)
if ($page.route.id !== Route.Chat && activeCall != null) {
show = true
remoteVideoElement.srcObject = VoiceRTCInstance.activeCall?.remoteStream!
remoteVideoElement.play()
} else if (!activeCall && remoteVideoElement) {
show = false
remoteVideoElement.pause()
remoteVideoElement.srcObject = null
} else if ($page.route.id === Route.Chat && get(Store.state.activeChat).id === VoiceRTCInstance.channel) {
show = false
remoteVideoElement.pause()
remoteVideoElement.srcObject = null
if (activeCall) {
chat = activeCall.chat
} else {
chat = undefined
showVideoPreview = false
}
})

callScreenVisible.subscribe(visible => {
if (visible === true) {
showVideoPreview = false
return
}
if (visible === false && get(Store.state.activeCall) !== null) {
setTimeout(() => {
log.warn("Changing video preview visibility")
showVideoPreview = true
}, 100)
}
otherUserSettingsInCall = VoiceRTCInstance.remoteVoiceUser
chat = activeCall?.chat
})

$: otherUserSettingsInCall = VoiceRTCInstance.remoteVoiceUser
$: ownUser = get(Store.state.user)
$: chats = UIStore.state.chats
$: userCache = Store.getUsersLookup($chats.map(c => c.users).flat())

function attachStream(node: HTMLMediaElement, user: string) {
const stream = $remoteStreams[user]?.stream

if (stream) {
node.srcObject = stream
stream.onremovetrack = () => {
log.dev("Stream removed: ", user)
}
}

return {
update(newUser: string) {
const newStream = $remoteStreams[newUser]?.stream
if (newStream && node.srcObject !== newStream) {
node.srcObject = newStream
}
},
destroy() {
node.srcObject = null
},
}
}

onMount(() => {
const video = previewVideo

Expand Down Expand Up @@ -107,22 +161,45 @@
})
</script>

<div id="video-preview" class={show ? "video-preview" : "hidden"}>
<div id="video-preview" class={showVideoPreview ? "video-preview" : "hidden"}>
<div id="preview-video" bind:this={previewVideo}>
<video id="remote-user-float-video" bind:this={remoteVideoElement} width={otherUserSettingsInCall?.videoEnabled ? 400 : 0} height={otherUserSettingsInCall?.videoEnabled ? 400 : 0} autoplay>
<track kind="captions" src="" />
</video>
{#if !otherUserSettingsInCall?.videoEnabled && chat !== undefined}
{#each chat.users as user}
{#if user !== ownUser.key}
<Participant
participant={$userCache[user]}
hasVideo={$userCache[user].media.is_streaming_video}
isMuted={$userCache[user].media.is_muted}
isDeafened={$userCache[user].media.is_deafened}
isTalking={$userCache[user].media.is_playing_audio} />
{/if}
{/each}
<div class="users-in-call">
<Icon icon={Shape.Users}></Icon>
{filteredUsers.length}
</div>
{#if chat !== undefined && get(Store.state.activeCall) !== null && filteredUsers.length > 0}
<div class="video-grid" style="grid-template-columns: {gridTemplateColumns};">
{#each filteredUsers.slice(0, 3) as user}
{#if $remoteStreams[user]}
<div class="video-container {$userCache[user].media.is_playing_audio ? 'talking' : ''}" style={!$remoteStreams[user].user.videoEnabled ? "display: none" : ""} role="none">
<video data-cy="remote-user-video" id="remote-user-video-{user}" class={$remoteStreams[user].user.videoEnabled ? "" : "disabled"} autoplay muted={false} use:attachStream={user}>
<track kind="captions" src="" />
</video>
<div class="user-name">{$userCache[user].name}</div>
{#if !$remoteStreams[user].user.audioEnabled}
<div class="mute-status">
<Icon icon={Shape.MicrophoneSlash}></Icon>
</div>
{/if}
</div>

{#if !$remoteStreams[user].stream || !$remoteStreams[user].user.videoEnabled}
<Participant
participant={$userCache[user]}
hasVideo={$userCache[user].media.is_streaming_video}
isMuted={$remoteStreams[user] && !$remoteStreams[user].user.audioEnabled}
isDeafened={$remoteStreams[user] && $remoteStreams[user].user.isDeafened}
isTalking={$userCache[user].media.is_playing_audio}
on:click={_ => {}} />
{/if}
{/if}
{/each}
</div>
{:else if chat !== undefined && get(Store.state.activeCall) !== null && filteredUsers.length === 0}
<div class="loading-when-no-answer">
<div class="spinner"></div>
<p>{$_("settings.calling.waitingOthersToJoin")}</p>
</div>
{/if}
</div>
</div>
Expand All @@ -138,6 +215,7 @@
overflow: hidden;
pointer-events: none;
}

#video-preview {
position: fixed;
top: 0;
Expand All @@ -156,18 +234,135 @@
height: 225px;
background: var(--background-alt);
border-radius: var(--border-radius);
border: var(--border-width) solid var(--border-color);
border: var(--border-width) solid var(--color-muted);
position: absolute;
top: 0;
right: 0;
cursor: grab;
pointer-events: all;
transition: transform 0.3s ease;
margin: var(--padding);
display: inline-flex;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: var(--padding);
box-sizing: border-box;
overflow: hidden;
align-items: center;

.users-in-call {
position: absolute;
display: flex;
gap: 4px;
bottom: 8px;
right: 12px;
align-items: center;
justify-content: center;
background-color: rgba(14, 13, 13, 0.5);
color: white;
padding: 4px 8px;
border-radius: 8px;
font-size: 14px;
z-index: 1;
}

.video-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
justify-content: center;
align-items: center;
justify-items: center;
gap: 10px;
width: 100%;
height: 100%;
}

.video-container {
position: relative;
display: inline-block;
border-radius: 12px;
overflow: hidden;
border: 2px solid var(--color-muted);
cursor: pointer;
&.talking {
border: 2px solid var(--success-color);
}
width: 100%;
height: 100%;
aspect-ratio: 4 / 3;

.user-name {
position: absolute;
bottom: 8px;
left: 12px;
background-color: rgba(0, 0, 0, 0.6);
color: white;
padding: 4px 8px;
border-radius: 8px;
font-size: 14px;
z-index: 1;
}

.mute-status {
position: absolute;
display: flex;
bottom: 8px;
right: 12px;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.6);
color: white;
padding: 4px 8px;
border-radius: 8px;
font-size: 14px;
z-index: 1;
}

video {
object-fit: cover;
border-radius: 12px;
background-color: var(--black);
width: 100%;
height: 100%;

&.disabled {
width: 0;
height: 0;
}
}
}
}
}

.spinner {
width: 48px;
height: 48px;
border: 8px solid #f3f3f3;
border-top: 8px solid #3498db;
border-radius: 50%;
animation: spin 2s linear infinite;
}

@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

.loading-when-no-answer {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
text-align: center;
margin: 32px;
}

.loading-when-no-answer p {
margin-top: 16px;
}
</style>
1 change: 1 addition & 0 deletions src/lib/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,7 @@
"hasCancelled": "Has cancelled the call.",
"disconnecting": "Disconnecting...",
"userInviteToAGroupCall": "{user} has started a group call in {group}",
"waitingOthersToJoin": "Waiting for others to join...",
"noAnswer": "No answer, leaving the call...",
"everybodyDeniedTheCall": "Everybody Denied the call. Disconnecting...",
"acceptedCall": "Joined, loading...",
Expand Down
Loading