diff --git a/src/lib/components/calling/CallScreen.svelte b/src/lib/components/calling/CallScreen.svelte index e93467724..2cc87f2fe 100644 --- a/src/lib/components/calling/CallScreen.svelte +++ b/src/lib/components/calling/CallScreen.svelte @@ -13,11 +13,24 @@ import type { Chat } from "$lib/types" import VolumeMixer from "./VolumeMixer.svelte" import { createEventDispatcher, onDestroy, onMount } from "svelte" - import { callInProgress, callScreenVisible, callTimeout, makeCallSound, TIME_TO_SHOW_CONNECTING, TIME_TO_SHOW_END_CALL_FEEDBACK, timeCallStarted, usersAcceptedTheCall, usersDeniedTheCall, VoiceRTCInstance } from "$lib/media/Voice" + import { + callInProgress, + callScreenVisible, + callTimeout, + makeCallSound, + showCallPopUp, + TIME_TO_SHOW_CONNECTING, + TIME_TO_SHOW_END_CALL_FEEDBACK, + timeCallStarted, + usersAcceptedTheCall, + usersDeniedTheCall, + VoiceRTCInstance, + } from "$lib/media/Voice" import { log } from "$lib/utils/Logger" import { playSound, Sounds } from "../utils/SoundHandler" import { MultipassStoreInstance } from "$lib/wasm/MultipassStore" import { debounce } from "$lib/utils/Functions" + import LiveLabel from "./LiveLabel.svelte" const MIN_USER_SIZE = 250 export let expanded: boolean = false @@ -196,6 +209,7 @@ onMount(async () => { callScreenVisible.set(true) + showCallPopUp.set(false) if ($makeCallSound) { stopMakeCallSound() } @@ -269,6 +283,12 @@ if (get(Store.state.activeCall) === null && get(Store.state.devices.screenShare) === true) { Store.state.devices.screenShare.set(false) } + + if ($callInProgress !== null) { + showCallPopUp.set(true) + } else { + showCallPopUp.set(false) + } }) function updateUserListSplit() { @@ -494,12 +514,7 @@
{$userCache[user].name}
- {#if $remoteStreams[user].user.screenShareEnabled} -
- - {$_("settings.calling.live")} -
- {/if} + {#if !$remoteStreams[user].user.audioEnabled}
@@ -813,28 +828,6 @@ z-index: 1; } - .live-label { - position: absolute; - top: 8px; - left: 12px; - display: inline-flex; - align-items: center; - background-color: rgba(0, 0, 0, 0.6); - color: white; - padding: 4px 8px; - border-radius: 8px; - font-size: 14px; - z-index: 1; - } - - .red-dot { - width: 6px; - height: 6px; - background-color: red; - border-radius: 50%; - margin-right: 8px; - } - .mute-status { position: absolute; display: flex; diff --git a/src/lib/components/calling/LiveLabel.svelte b/src/lib/components/calling/LiveLabel.svelte new file mode 100644 index 000000000..2ea0f3d91 --- /dev/null +++ b/src/lib/components/calling/LiveLabel.svelte @@ -0,0 +1,36 @@ + + +{#if screenShareEnabled} +
+ + {$_("settings.calling.live")} +
+{/if} + + diff --git a/src/lib/components/calling/VideoPreview.svelte b/src/lib/components/calling/VideoPreview.svelte index 00af59c5c..ca2f6fcbd 100644 --- a/src/lib/components/calling/VideoPreview.svelte +++ b/src/lib/components/calling/VideoPreview.svelte @@ -1,54 +1,92 @@ -
+
- - {#if !otherUserSettingsInCall?.videoEnabled && chat !== undefined} - {#each chat.users as user} - {#if user !== ownUser.key} - - {/if} - {/each} +
+ + {filteredUsers.length} +
+ {#if chat !== undefined && get(Store.state.activeCall) !== null && filteredUsers.length > 0} +
+ {#each filteredUsers.slice(0, 3) as user} + {#if $remoteStreams[user]} +
+ +
{$userCache[user].name}
+ + {#if !$remoteStreams[user].user.audioEnabled} +
+ +
+ {/if} +
+ + {#if !$remoteStreams[user].stream || (!$remoteStreams[user].user.videoEnabled && !$remoteStreams[user].user.screenShareEnabled)} + {}} /> + {/if} + {/if} + {/each} +
+ {:else if chat !== undefined && get(Store.state.activeCall) !== null && filteredUsers.length === 0} +
+
+

{$_("settings.calling.waitingOthersToJoin")}

+
{/if}
@@ -138,6 +209,7 @@ overflow: hidden; pointer-events: none; } + #video-preview { position: fixed; top: 0; @@ -156,7 +228,7 @@ 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; @@ -164,10 +236,127 @@ 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; + } diff --git a/src/lib/lang/en.json b/src/lib/lang/en.json index ce8c4f671..f99a4c769 100644 --- a/src/lib/lang/en.json +++ b/src/lib/lang/en.json @@ -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...", diff --git a/src/lib/media/Voice.ts b/src/lib/media/Voice.ts index 600285271..0a1bd7c64 100644 --- a/src/lib/media/Voice.ts +++ b/src/lib/media/Voice.ts @@ -28,6 +28,8 @@ export const timeCallStarted: Writable = writable(null) export const callInProgress: Writable = writable(null) export const makeCallSound = writable(undefined) export const callScreenVisible = writable(false) +export const usersDidInActiveCall = writable([]) +export const showCallPopUp = writable(false) const relaysToTest = [ "wss://nostr-pub.wellorder.net", @@ -158,6 +160,7 @@ async function handleStreamMeta(did: string, stream: MediaStream): Promise("did_sync") did_rv((did, peer) => { + if (!get(usersDidInActiveCall).includes(did)) { + usersDidInActiveCall.update(u => [...u, did]) + } this.participants[did] = new Participant(did, peer) }) room.onPeerJoin(async peer => { @@ -253,13 +259,14 @@ export class CallRoom { this.start = new Date() } }) - room.onPeerTrack((stream, peer, _meta) => { + room.onPeerTrack((_, peer, _meta) => { log.debug(`Receiving track from ${peer}`) }) room.onPeerLeave(peer => { log.debug(`Peer ${peer} left the room`) let participant = Object.entries(this.participants).find(p => p[1].remotePeerId === peer) if (participant) { + usersDidInActiveCall.update(u => u.filter(did => did !== participant[0])) VoiceRTCInstance.remoteVideoCreator.delete(participant[0]) delete this.participants[participant[0]] } @@ -276,8 +283,6 @@ export class CallRoom { participant[1].handleRemoteStream(stream) } }) - room.onPeerTrack((stream, peer, _meta) => {}) - // room.onPeerTrack((stream, peer, meta) => {}) } toggleStreams(state: boolean, type: ToggleType) { @@ -809,9 +814,9 @@ export class VoiceRTC { } async leaveCall(sendEndCallMessage = false) { - callScreenVisible.set(false) callInProgress.set(null) timeCallStarted.set(null) + showCallPopUp.set(false) usersDeniedTheCall.set([]) callTimeout.set(false) connectionOpened.set(false) @@ -834,6 +839,7 @@ export class VoiceRTC { if (get(Store.state.activeCall)) { Store.endCall() } + callScreenVisible.set(false) if (get(Store.state.pendingCall)) { Store.denyCall() diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 652eb5af8..327a34af6 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -290,7 +290,7 @@ - + UIStore.toggleMarket()} />