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(Call): Fix problem that made new call when back to chat #701

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
220 changes: 168 additions & 52 deletions src/lib/components/calling/CallScreen.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,14 @@
import Participant from "./Participant.svelte"
import Text from "$lib/elements/Text.svelte"
import CallSettings from "./CallSettings.svelte"
import { get, writable, type Writable } from "svelte/store"
import { get } from "svelte/store"
import { Store } from "$lib/state/Store"
import { _ } from "svelte-i18n"
import type { Chat } from "$lib/types"
import { UIStore } from "$lib/state/ui"
import VolumeMixer from "./VolumeMixer.svelte"
import { onDestroy, onMount } from "svelte"
import { VoiceRTCInstance, type VoiceRTCUser } from "$lib/media/Voice"
import { callTimeout, VoiceRTCInstance } from "$lib/media/Voice"
import { log } from "$lib/utils/Logger"
import { debounce } from "$lib/utils/Functions"

export let expanded: boolean = false
function toggleExanded() {
Expand Down Expand Up @@ -92,29 +90,45 @@
}

function attachStream(node: HTMLMediaElement, user: string) {
if (!$remoteStreams[user]) return
let stream = $remoteStreams[user].stream
node.srcObject = stream
node.play()
const stream = $remoteStreams[user]?.stream

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

return {
update(_: MediaStream) {
debounce(() => {
if (node.srcObject) (node.srcObject as MediaStream).getTracks().forEach(t => t.stop())
node.srcObject = stream
node.play()
}, 3)
update(newUser: string) {
const newStream = $remoteStreams[newUser]?.stream
if (newStream && node.srcObject !== newStream) {
node.srcObject = newStream
}
},
destroy() {
node.srcObject = null
},
}
}

let showAnimation = true
let message = $_("settings.calling.connecting")

let timeout: NodeJS.Timeout | undefined

onMount(async () => {
document.addEventListener("mousedown", handleClickOutside)
await VoiceRTCInstance.setVideoElements(localVideoCurrentSrc)

/// HACK: To make sure the video elements are loaded before we start the call
if (VoiceRTCInstance.localVideoCurrentSrc && VoiceRTCInstance.remoteVideoCreator) {
if (VoiceRTCInstance.toCall && VoiceRTCInstance.toCall.find(did => did !== "") !== undefined) {
if (VoiceRTCInstance.toCall && VoiceRTCInstance.toCall.find(did => did !== "") !== undefined && (VoiceRTCInstance.call?.peersQuantityOnCall ?? 0) < 1) {
await VoiceRTCInstance.makeCall()
timeout = setTimeout(() => {
showAnimation = false
message = $_("settings.calling.noResponse")
}, 15000)
}
}

Expand All @@ -129,6 +143,9 @@
subscribeTwo()
subscribeThree()
subscribeFour()
if (timeout) {
clearTimeout(timeout)
}
})
</script>

Expand All @@ -141,44 +158,61 @@
</Text>
</svelte:fragment>
</Topbar>
<div id="participants">
<video
data-cy="local-user-video"
id="local-user-video"
bind:this={localVideoCurrentSrc}
width={userCallOptions.video.enabled ? (isFullScreen ? "calc(50% - var(--gap) * 2)" : 200) : 0}
height={userCallOptions.video.enabled ? (isFullScreen ? "50%" : 200) : 0}
muted
autoplay>
<track kind="captions" src="" />
</video>

{#each chat.users as user}
{#if user === get(Store.state.user).key && !userCallOptions.video.enabled}
<Participant participant={$userCache[user]} hasVideo={$userCache[user].media.is_streaming_video} isMuted={muted} isDeafened={userCallOptions.audio.deafened} isTalking={$userCache[user].media.is_playing_audio} />
{:else if $userCache[user] && $userCache[user].key !== get(Store.state.user).key && $remoteStreams[user]}
{#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} />
{/if}
{#if $remoteStreams[user].stream}
<video
data-cy="remote-user-video"
id="remote-user-video"
width={$remoteStreams[user].user.videoEnabled ? (isFullScreen ? "calc(50% - var(--gap) * 2)" : 400) : 0}
height={$remoteStreams[user].user.videoEnabled ? (isFullScreen ? "50%" : 400) : 0}
autoplay
use:attachStream={user}>
<track kind="captions" src="" />
</video>

{#if !$callTimeout}
<div id="participants">
<video
data-cy="local-user-video"
id="local-user-video"
bind:this={localVideoCurrentSrc}
width={userCallOptions.video.enabled ? (isFullScreen ? "calc(50% - var(--gap) * 2)" : 200) : 0}
height={userCallOptions.video.enabled ? (isFullScreen ? "50%" : 200) : 0}
muted
autoplay>
<track kind="captions" src="" />
</video>

{#each chat.users as user (user)}
{#if user === get(Store.state.user).key && !userCallOptions.video.enabled}
<Participant participant={$userCache[user]} hasVideo={$userCache[user].media.is_streaming_video} isMuted={muted} isDeafened={userCallOptions.audio.deafened} isTalking={$userCache[user].media.is_playing_audio} />
{:else if $userCache[user] && $userCache[user].key !== get(Store.state.user).key && VoiceRTCInstance.toCall && !$remoteStreams[user]}
{#if showAnimation}
<div class="calling-animation">
<div class="shaking-participant">
<Participant participant={$userCache[user]} hasVideo={false} isMuted={true} isDeafened={true} isTalking={false} />
<p>{message}</p>
</div>
</div>
{:else}
<div class="no-response">
<Participant participant={$userCache[user]} hasVideo={false} isMuted={true} isDeafened={true} isTalking={false} />
<p>{message}</p>
</div>
{/if}
{:else if $userCache[user] && $userCache[user].key !== get(Store.state.user).key && $remoteStreams[user]}
{#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} />
{/if}

{#if $remoteStreams[user] && $remoteStreams[user].stream && $remoteStreams[user].user.videoEnabled}
<video data-cy="remote-user-video" id="remote-user-video-{user}" width={isFullScreen ? "calc(50% - var(--gap) * 2)" : 400} height={isFullScreen ? "50%" : 400} autoplay use:attachStream={user}>
<track kind="captions" src="" />
</video>
{/if}
{/if}
{/if}
{/each}
</div>
{/each}
</div>
{:else}
<div class="loading-when-no-answer">
<div class="spinner"></div>
<p>{$_("settings.calling.noAnswer")}</p>
</div>
{/if}
{/if}
<div class="toolbar">
<Controls>
Expand Down Expand Up @@ -327,5 +361,87 @@
border-radius: 12px;
background-color: var(--black);
}

.calling-animation {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
text-align: center;
animation: shake 0.4s ease-in-out infinite;
}

.shaking-participant {
animation: shake 0.4s ease-in-out infinite;
}

@keyframes shake {
0%,
100% {
transform: translateX(0);
}
25% {
transform: translateX(-0.75px);
}
50% {
transform: translateX(0.75px);
}
75% {
transform: translateX(-0.75px);
}
}

.calling-animation p {
margin-top: 10px;
font-size: 1.2rem;
color: #666;
font-weight: bold;
}

.no-response {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
text-align: center;
}

.no-response p {
margin-top: 10px;
font-size: 1.2rem;
color: #666;
font-weight: bold;
}

.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>
60 changes: 45 additions & 15 deletions src/lib/components/calling/IncomingCall.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,44 @@
import ProfilePicture from "../profile/ProfilePicture.svelte"
import { playSound, SoundHandler, Sounds } from "../utils/SoundHandler"
import { Store } from "$lib/state/Store"
import { VoiceRTCInstance } from "$lib/media/Voice"
import { connectionOpened, VoiceRTCInstance } from "$lib/media/Voice"
import { goto } from "$app/navigation"
import { writable } from "svelte/store"
import { onDestroy, onMount } from "svelte"
import { _ } from "svelte-i18n"

let callSound: SoundHandler | undefined = undefined
let pending = false
let user = writable(defaultUser)
let timeOutToCancel: NodeJS.Timeout | undefined = undefined
$: cancelledCall = false

onMount(() => {
cancelledCall = false
clearTimeout(timeOutToCancel)
})

onDestroy(() => {
cancelledCall = false
clearTimeout(timeOutToCancel)
})

Store.state.pendingCall.subscribe(async _ => {
if (VoiceRTCInstance.incomingCallFrom && !VoiceRTCInstance.toCall) {
if (VoiceRTCInstance.incomingCallFrom && !VoiceRTCInstance.toCall && $connectionOpened) {
if (callSound === null || callSound === undefined) {
callSound = playSound(Sounds.IncomingCall)
callSound.play()
}
pending = true
user = Store.getUser(VoiceRTCInstance.incomingCallFrom[1].metadata.did)
} else if (!$connectionOpened) {
cancelledCall = true
timeOutToCancel = setTimeout(() => {
cancelledCall = false
pending = false
callSound?.stop()
callSound = undefined
}, 4000)
} else {
pending = false
callSound?.stop()
Expand Down Expand Up @@ -49,19 +71,27 @@
{#if pending}
<div id="incoming-call">
<div class="body">
<div class="content">
<ProfilePicture id={$user.key} hook="friend-profile-picture" size={Size.Large} image={$user.profile.photo.image} status={$user.profile.status} />
<Text>{$user.name}</Text>
<Text muted>{$user.profile.status_message}</Text>
<Spacer />
<Controls>
<Button appearance={Appearance.Success} text="Answer" on:click={answerCall}>
<Icon icon={Shape.PhoneCall} />
</Button>
<Button appearance={Appearance.Error} text="End" on:click={endCall}>
<Icon icon={Shape.PhoneXMark} />
</Button>
</Controls>
<div class="content" style={cancelledCall ? "border: var(--border-width) solid var(--warning-color);" : "border: var(--border-width) solid var(--success-color);"}>
{#if cancelledCall}
<ProfilePicture id={$user.key} hook="friend-profile-picture" size={Size.Large} image={$user.profile.photo.image} status={$user.profile.status} />
<Text>{$user.name}</Text>
<Text muted size={Size.Large}>{$_("settings.calling.hasCancelled")}</Text>
<Text muted size={Size.Large}>{$_("settings.calling.disconnecting")}</Text>
<Spacer />
{:else}
<ProfilePicture id={$user.key} hook="friend-profile-picture" size={Size.Large} image={$user.profile.photo.image} status={$user.profile.status} />
<Text>{$user.name}</Text>
<Text muted>{$user.profile.status_message}</Text>
<Spacer />
<Controls>
<Button appearance={Appearance.Success} text="Answer" on:click={answerCall}>
<Icon icon={Shape.PhoneCall} />
</Button>
<Button appearance={Appearance.Error} text="End" on:click={endCall}>
<Icon icon={Shape.PhoneXMark} />
</Button>
</Controls>
{/if}
</div>
</div>
</div>
Expand Down
7 changes: 6 additions & 1 deletion src/lib/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,12 @@
"minimalCallingAlertsDescription": "Instead of displaying a full screen alert when a call is incoming, with this mode enabled the call dialog will show within the sidebar.",
"startCallMessage": "πŸ“ž A call started at {value}.",
"endCallMessage": "πŸ“΄ The call ended at {formattedEndTime}. Duration: {duration}.",
"callMissed": "πŸ“ž Missed call"
"callMissed": "πŸ“ž Missed call",
"hasCancelled": "Has cancelled the call.",
stavares843 marked this conversation as resolved.
Show resolved Hide resolved
"disconnecting": "Disconnecting...",
"noAnswer": "No answer, leaving the call...",
"connecting": "Connecting...",
"noResponse": "No response"
},
"notifications": {
"name": "Notifications",
Expand Down
Loading
Loading