Group Calls: Fix connection & UI issues (#3427)
This commit is contained in:
parent
a5486b9ada
commit
3b0f03e012
@ -176,7 +176,10 @@ export async function joinGroupCall({
|
|||||||
data: JSON.stringify(params),
|
data: JSON.stringify(params),
|
||||||
}),
|
}),
|
||||||
inviteHash,
|
inviteHash,
|
||||||
}));
|
}), {
|
||||||
|
shouldRetryOnTimeout: true,
|
||||||
|
abortControllerGroup: 'call',
|
||||||
|
});
|
||||||
|
|
||||||
if (!result) return undefined;
|
if (!result) return undefined;
|
||||||
|
|
||||||
|
|||||||
@ -42,6 +42,7 @@ GramJsLogger.setLevel(DEBUG_GRAMJS ? 'debug' : 'warn');
|
|||||||
const gramJsUpdateEventBuilder = { build: (update: object) => update };
|
const gramJsUpdateEventBuilder = { build: (update: object) => update };
|
||||||
|
|
||||||
const CHAT_ABORT_CONTROLLERS = new Map<string, ChatAbortController>();
|
const CHAT_ABORT_CONTROLLERS = new Map<string, ChatAbortController>();
|
||||||
|
const ABORT_CONTROLLERS = new Map<string, AbortController>();
|
||||||
|
|
||||||
let onUpdate: OnApiUpdate;
|
let onUpdate: OnApiUpdate;
|
||||||
let client: TelegramClient;
|
let client: TelegramClient;
|
||||||
@ -211,6 +212,8 @@ type InvokeRequestParams = {
|
|||||||
shouldIgnoreErrors?: boolean;
|
shouldIgnoreErrors?: boolean;
|
||||||
abortControllerChatId?: string;
|
abortControllerChatId?: string;
|
||||||
abortControllerThreadId?: number;
|
abortControllerThreadId?: number;
|
||||||
|
abortControllerGroup?: 'call';
|
||||||
|
shouldRetryOnTimeout?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function invokeRequest<T extends GramJs.AnyRequest>(
|
export async function invokeRequest<T extends GramJs.AnyRequest>(
|
||||||
@ -229,6 +232,7 @@ export async function invokeRequest<T extends GramJs.AnyRequest>(
|
|||||||
) {
|
) {
|
||||||
const {
|
const {
|
||||||
shouldThrow, shouldIgnoreUpdates, dcId, shouldIgnoreErrors, abortControllerChatId, abortControllerThreadId,
|
shouldThrow, shouldIgnoreUpdates, dcId, shouldIgnoreErrors, abortControllerChatId, abortControllerThreadId,
|
||||||
|
shouldRetryOnTimeout, abortControllerGroup,
|
||||||
} = params;
|
} = params;
|
||||||
const shouldReturnTrue = Boolean(params.shouldReturnTrue);
|
const shouldReturnTrue = Boolean(params.shouldReturnTrue);
|
||||||
|
|
||||||
@ -243,12 +247,21 @@ export async function invokeRequest<T extends GramJs.AnyRequest>(
|
|||||||
abortSignal = abortControllerThreadId ? controller.getThreadSignal(abortControllerThreadId) : controller.signal;
|
abortSignal = abortControllerThreadId ? controller.getThreadSignal(abortControllerThreadId) : controller.signal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (abortControllerGroup) {
|
||||||
|
let controller = ABORT_CONTROLLERS.get(abortControllerGroup);
|
||||||
|
if (!controller) {
|
||||||
|
controller = new AbortController();
|
||||||
|
ABORT_CONTROLLERS.set(abortControllerGroup, controller);
|
||||||
|
}
|
||||||
|
abortSignal = controller.signal;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
log('INVOKE', request.className);
|
log('INVOKE', request.className);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await client.invoke(request, dcId, abortSignal);
|
const result = await client.invoke(request, dcId, abortSignal, shouldRetryOnTimeout);
|
||||||
|
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
log('RESPONSE', request.className, result);
|
log('RESPONSE', request.className, result);
|
||||||
@ -326,6 +339,11 @@ export function abortChatRequests(params: { chatId: string; threadId?: number })
|
|||||||
controller?.abortThread(threadId, 'Thread change');
|
controller?.abortThread(threadId, 'Thread change');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function abortRequestGroup(group: string) {
|
||||||
|
ABORT_CONTROLLERS.get(group)?.abort();
|
||||||
|
ABORT_CONTROLLERS.delete(group);
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchCurrentUser() {
|
export async function fetchCurrentUser() {
|
||||||
const userFull = await invokeRequest(new GramJs.users.GetFullUser({
|
const userFull = await invokeRequest(new GramJs.users.GetFullUser({
|
||||||
id: new GramJs.InputUserSelf(),
|
id: new GramJs.InputUserSelf(),
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
export {
|
export {
|
||||||
destroy, disconnect, downloadMedia, fetchCurrentUser, repairFileReference, abortChatRequests,
|
destroy, disconnect, downloadMedia, fetchCurrentUser, repairFileReference, abortChatRequests, abortRequestGroup,
|
||||||
} from './client';
|
} from './client';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
.root {
|
.root {
|
||||||
--group-call-panel-color: #212121;
|
--group-call-panel-color: #212121;
|
||||||
--group-call-panel-header-border-color: #3b3b3b;
|
--group-call-panel-header-border-color: #3b3b3b;
|
||||||
|
--color-dividers: var(--group-call-panel-header-border-color);
|
||||||
--group-call-background-color: #000000;
|
--group-call-background-color: #000000;
|
||||||
--green-button-color: rgba(1, 200, 80, 0.3);
|
--green-button-color: rgba(1, 200, 80, 0.3);
|
||||||
--blue-button-color: rgb(60, 135, 247, 0.2);
|
--blue-button-color: rgb(60, 135, 247, 0.2);
|
||||||
@ -148,6 +149,10 @@
|
|||||||
width: calc(100% - var(--default-width));
|
width: calc(100% - var(--default-width));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mainVideoContainer {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
.videosHeader {
|
.videosHeader {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -160,7 +165,7 @@
|
|||||||
|
|
||||||
.videosContent {
|
.videosContent {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
margin: 0.5rem 0.625rem;
|
margin: 0.1875rem 0.625rem 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
@ -175,7 +180,6 @@
|
|||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.actionButton {
|
.actionButton {
|
||||||
width: 3.375rem !important;
|
width: 3.375rem !important;
|
||||||
height: 3.375rem !important;
|
height: 3.375rem !important;
|
||||||
@ -242,11 +246,11 @@
|
|||||||
border-radius: 1.25rem;
|
border-radius: 1.25rem;
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
bottom: 2.5rem;
|
bottom: 2.5rem;
|
||||||
transition: var(--layer-transition) transform, 250ms ease-in-out opacity;
|
transition: 250ms ease-in-out opacity;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.videos:hover ~ .actions, .video:hover ~ .actions, .actions:hover {
|
.videos:hover ~ .actions, .mainVideoContainer:hover ~ .actions, .actions:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -152,7 +152,7 @@ const GroupCall: FC<OwnProps & StateProps> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleToggleFullscreen = useLastCallback(() => {
|
const handleToggleFullscreen = useLastCallback(() => {
|
||||||
if (!containerRef.current) return;
|
if (!containerRef.current || isMobile) return;
|
||||||
|
|
||||||
if (isFullscreen) {
|
if (isFullscreen) {
|
||||||
closeFullscreen();
|
closeFullscreen();
|
||||||
@ -169,6 +169,10 @@ const GroupCall: FC<OwnProps & StateProps> = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleToggleGroupCallPanel = useLastCallback(() => {
|
||||||
|
toggleGroupCallPanel();
|
||||||
|
});
|
||||||
|
|
||||||
const handleInviteMember = useLastCallback(() => {
|
const handleInviteMember = useLastCallback(() => {
|
||||||
createGroupCallInviteLink();
|
createGroupCallInviteLink();
|
||||||
});
|
});
|
||||||
@ -241,7 +245,7 @@ const GroupCall: FC<OwnProps & StateProps> = ({
|
|||||||
onClose={toggleGroupCallPanel}
|
onClose={toggleGroupCallPanel}
|
||||||
className={buildClassName(
|
className={buildClassName(
|
||||||
styles.root,
|
styles.root,
|
||||||
isFullscreen && styles.fullscreen,
|
(isFullscreen || isMobile) && styles.fullscreen,
|
||||||
isLandscapeLayout && styles.landscape,
|
isLandscapeLayout && styles.landscape,
|
||||||
!hasVideoParticipants && styles.noVideoParticipants,
|
!hasVideoParticipants && styles.noVideoParticipants,
|
||||||
!isLandscapeLayout && styles.portrait,
|
!isLandscapeLayout && styles.portrait,
|
||||||
@ -253,19 +257,21 @@ const GroupCall: FC<OwnProps & StateProps> = ({
|
|||||||
{isLandscapeWithVideos && (
|
{isLandscapeWithVideos && (
|
||||||
<div className={styles.videos}>
|
<div className={styles.videos}>
|
||||||
<div className={styles.videosHeader}>
|
<div className={styles.videosHeader}>
|
||||||
<Button
|
{!isMobile && (
|
||||||
round
|
<Button
|
||||||
size="smaller"
|
round
|
||||||
color="translucent"
|
size="smaller"
|
||||||
onClick={handleToggleFullscreen}
|
color="translucent"
|
||||||
className={buildClassName(styles.headerButton, styles.firstButton)}
|
onClick={handleToggleFullscreen}
|
||||||
ariaLabel={lang(isFullscreen ? 'AccExitFullscreen' : 'AccSwitchToFullscreen')}
|
className={buildClassName(styles.headerButton, styles.firstButton)}
|
||||||
>
|
ariaLabel={lang(isFullscreen ? 'AccExitFullscreen' : 'AccSwitchToFullscreen')}
|
||||||
<i
|
>
|
||||||
className={buildClassName('icon', isFullscreen ? 'icon-smallscreen' : 'icon-fullscreen')}
|
<i
|
||||||
aria-hidden
|
className={buildClassName('icon', isFullscreen ? 'icon-smallscreen' : 'icon-fullscreen')}
|
||||||
/>
|
aria-hidden
|
||||||
</Button>
|
/>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
<h3 className={buildClassName(styles.title, styles.bigger)}>
|
<h3 className={buildClassName(styles.title, styles.bigger)}>
|
||||||
{title || lang('VoipGroupVoiceChat')}
|
{title || lang('VoipGroupVoiceChat')}
|
||||||
@ -297,7 +303,7 @@ const GroupCall: FC<OwnProps & StateProps> = ({
|
|||||||
<div className={styles.panelScrollTrigger} ref={panelScrollTriggerRef} />
|
<div className={styles.panelScrollTrigger} ref={panelScrollTriggerRef} />
|
||||||
|
|
||||||
<div className={buildClassName(styles.panelHeader, hasScrolled && styles.scrolled)}>
|
<div className={buildClassName(styles.panelHeader, hasScrolled && styles.scrolled)}>
|
||||||
{!isLandscapeWithVideos && (
|
{!isLandscapeWithVideos && !isMobile && (
|
||||||
<Button
|
<Button
|
||||||
round
|
round
|
||||||
size="smaller"
|
size="smaller"
|
||||||
@ -314,6 +320,22 @@ const GroupCall: FC<OwnProps & StateProps> = ({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{isMobile && (
|
||||||
|
<Button
|
||||||
|
round
|
||||||
|
size="smaller"
|
||||||
|
color="translucent"
|
||||||
|
onClick={handleToggleGroupCallPanel}
|
||||||
|
className={buildClassName(styles.headerButton, styles.firstButton)}
|
||||||
|
ariaLabel={lang('Close')}
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className={buildClassName('icon', 'icon-close')}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
{isLandscapeWithVideos && (
|
{isLandscapeWithVideos && (
|
||||||
<Button
|
<Button
|
||||||
round
|
round
|
||||||
@ -386,6 +408,7 @@ const GroupCall: FC<OwnProps & StateProps> = ({
|
|||||||
setPinned={setPinnedVideo}
|
setPinned={setPinnedVideo}
|
||||||
pinnedVideo={pinnedVideo}
|
pinnedVideo={pinnedVideo}
|
||||||
participant={participant}
|
participant={participant}
|
||||||
|
onStopSharing={handleToggleGroupCallPresentation}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -408,29 +431,32 @@ const GroupCall: FC<OwnProps & StateProps> = ({
|
|||||||
</FloatingActionButton>
|
</FloatingActionButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{videoLayout.map((layout) => {
|
<div className={styles.mainVideoContainer}>
|
||||||
const participant = participants[layout.participantId];
|
{videoLayout.map((layout) => {
|
||||||
if (layout.isRemounted || !participant) {
|
const participant = participants[layout.participantId];
|
||||||
|
if (layout.isRemounted || !participant) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
teactOrderKey={layout.orderKey}
|
||||||
|
key={`${layout.participantId}_${layout.type}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div
|
<GroupCallParticipantVideo
|
||||||
teactOrderKey={layout.orderKey}
|
teactOrderKey={layout.orderKey}
|
||||||
key={`${layout.participantId}_${layout.type}`}
|
key={`${layout.participantId}_${layout.type}`}
|
||||||
|
layout={layout}
|
||||||
|
canPin={canPinVideo}
|
||||||
|
setPinned={setPinnedVideo}
|
||||||
|
pinnedVideo={pinnedVideo}
|
||||||
|
participant={participant}
|
||||||
|
className={styles.video}
|
||||||
|
onStopSharing={handleToggleGroupCallPresentation}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
})}
|
||||||
return (
|
</div>
|
||||||
<GroupCallParticipantVideo
|
|
||||||
teactOrderKey={layout.orderKey}
|
|
||||||
key={`${layout.participantId}_${layout.type}`}
|
|
||||||
layout={layout}
|
|
||||||
canPin={canPinVideo}
|
|
||||||
setPinned={setPinnedVideo}
|
|
||||||
pinnedVideo={pinnedVideo}
|
|
||||||
participant={participant}
|
|
||||||
className={styles.video}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
<div className={styles.actions}>
|
<div className={styles.actions}>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@ -4,6 +4,8 @@
|
|||||||
--color-text: white;
|
--color-text: white;
|
||||||
--color-background-compact-menu: #212121DD;
|
--color-background-compact-menu: #212121DD;
|
||||||
--color-background-compact-menu-hover: #00000066;
|
--color-background-compact-menu-hover: #00000066;
|
||||||
|
--color-background: #212121DD;
|
||||||
|
--color-item-active: #00000066;
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: var(--z-modal-menu);
|
z-index: var(--z-modal-menu);
|
||||||
|
|||||||
@ -52,8 +52,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
.video {
|
.video {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
display: block;
|
display: block;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
border-radius: 0.625rem;
|
border-radius: 0.625rem;
|
||||||
@ -84,7 +89,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.flipped {
|
.flipped {
|
||||||
transform: rotateY(180deg);
|
transform: scaleX(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.pinButton {
|
.pinButton {
|
||||||
@ -148,3 +153,38 @@
|
|||||||
.icon {
|
.icon {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hidePresentation {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ownPresentation {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
z-index: 3;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgb(0, 0, 0, 0.6);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
border-radius: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ownPresentationText {
|
||||||
|
max-width: 70%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stopSharingButton {
|
||||||
|
width: auto;
|
||||||
|
min-width: 6rem;
|
||||||
|
font-weight: 500;
|
||||||
|
max-height: 2.25rem;
|
||||||
|
}
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import Button from '../../ui/Button';
|
|||||||
import OutlinedMicrophoneIcon from './OutlinedMicrophoneIcon';
|
import OutlinedMicrophoneIcon from './OutlinedMicrophoneIcon';
|
||||||
import FullNameTitle from '../../common/FullNameTitle';
|
import FullNameTitle from '../../common/FullNameTitle';
|
||||||
import GroupCallParticipantMenu from './GroupCallParticipantMenu';
|
import GroupCallParticipantMenu from './GroupCallParticipantMenu';
|
||||||
|
import Skeleton from '../../ui/Skeleton';
|
||||||
|
|
||||||
import styles from './GroupCallParticipantVideo.module.scss';
|
import styles from './GroupCallParticipantVideo.module.scss';
|
||||||
|
|
||||||
@ -38,6 +39,7 @@ type OwnProps = {
|
|||||||
canPin: boolean;
|
canPin: boolean;
|
||||||
participant: TypeGroupCallParticipant;
|
participant: TypeGroupCallParticipant;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
onStopSharing: VoidFunction;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
@ -54,6 +56,7 @@ const GroupCallParticipantVideo: FC<OwnProps & StateProps> = ({
|
|||||||
participant,
|
participant,
|
||||||
user,
|
user,
|
||||||
chat,
|
chat,
|
||||||
|
onStopSharing,
|
||||||
}) => {
|
}) => {
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|
||||||
@ -75,6 +78,7 @@ const GroupCallParticipantVideo: FC<OwnProps & StateProps> = ({
|
|||||||
const isSpeaking = (participant.amplitude || 0) > THRESHOLD;
|
const isSpeaking = (participant.amplitude || 0) > THRESHOLD;
|
||||||
const isRaiseHand = Boolean(participant.raiseHandRating);
|
const isRaiseHand = Boolean(participant.raiseHandRating);
|
||||||
const shouldFlipVideo = type === 'video' && participant.isSelf;
|
const shouldFlipVideo = type === 'video' && participant.isSelf;
|
||||||
|
const shouldHidePresentation = type === 'screen' && participant.isSelf;
|
||||||
|
|
||||||
const status = useMemo(() => {
|
const status = useMemo(() => {
|
||||||
if (isSelf) {
|
if (isSelf) {
|
||||||
@ -141,13 +145,20 @@ const GroupCallParticipantVideo: FC<OwnProps & StateProps> = ({
|
|||||||
setIsHidden(false);
|
setIsHidden(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
const handleCanPlay = useLastCallback(() => {
|
||||||
|
setIsLoading(false);
|
||||||
|
});
|
||||||
|
|
||||||
// When video stream is removed, the video element starts showing empty black screen.
|
// When video stream is removed, the video element starts showing empty black screen.
|
||||||
// To avoid that, we hide the video element and show the fallback frame instead, which is constantly updated
|
// To avoid that, we hide the video element and show the fallback frame instead, which is constantly updated
|
||||||
// every VIDEO_FALLBACK_UPDATE_INTERVAL milliseconds.
|
// every VIDEO_FALLBACK_UPDATE_INTERVAL milliseconds.
|
||||||
useInterval(() => {
|
useInterval(() => {
|
||||||
if (!stream?.active) return;
|
if (!stream?.active) return;
|
||||||
const video = videoRef.current!;
|
const video = videoRef.current;
|
||||||
const canvas = videoFallbackRef.current!;
|
const canvas = videoFallbackRef.current;
|
||||||
|
if (!video || !canvas) return;
|
||||||
|
|
||||||
requestMutation(() => {
|
requestMutation(() => {
|
||||||
canvas.width = video.videoWidth;
|
canvas.width = video.videoWidth;
|
||||||
@ -255,20 +266,38 @@ const GroupCallParticipantVideo: FC<OwnProps & StateProps> = ({
|
|||||||
isSpeaking && styles.speaking,
|
isSpeaking && styles.speaking,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{isLoading && (
|
||||||
|
<Skeleton className={buildClassName(styles.video, styles.loader)} />
|
||||||
|
)}
|
||||||
{stream && (
|
{stream && (
|
||||||
<video
|
<video
|
||||||
className={buildClassName(styles.video, shouldFlipVideo && styles.flipped)}
|
className={buildClassName(
|
||||||
|
styles.video, shouldFlipVideo && styles.flipped, shouldHidePresentation && styles.hidePresentation,
|
||||||
|
)}
|
||||||
muted
|
muted
|
||||||
autoPlay
|
autoPlay
|
||||||
playsInline
|
playsInline
|
||||||
srcObject={stream}
|
srcObject={stream}
|
||||||
ref={videoRef}
|
ref={videoRef}
|
||||||
|
onCanPlay={handleCanPlay}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<canvas
|
{!shouldHidePresentation && (
|
||||||
className={buildClassName(styles.videoFallback, shouldFlipVideo && styles.flipped)}
|
<canvas
|
||||||
ref={videoFallbackRef}
|
className={buildClassName(styles.videoFallback, shouldFlipVideo && styles.flipped)}
|
||||||
/>
|
ref={videoFallbackRef}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{shouldHidePresentation && (
|
||||||
|
<div className={styles.ownPresentation}>
|
||||||
|
<div className={styles.ownPresentationText}>{lang('VoiceChat.Sharing.Placeholder')}</div>
|
||||||
|
<Button className={styles.stopSharingButton} onClick={onStopSharing}>
|
||||||
|
{lang('VoiceChat.Sharing.Stop')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className={styles.thumbnailWrapper}>
|
<div className={styles.thumbnailWrapper}>
|
||||||
<canvas
|
<canvas
|
||||||
className={buildClassName(styles.thumbnail, shouldFlipVideo && styles.flipped)}
|
className={buildClassName(styles.thumbnail, shouldFlipVideo && styles.flipped)}
|
||||||
@ -288,13 +317,15 @@ const GroupCallParticipantVideo: FC<OwnProps & StateProps> = ({
|
|||||||
<i className={buildClassName('icon', isPinned ? 'icon-unpin' : 'icon-pin')} />
|
<i className={buildClassName('icon', isPinned ? 'icon-unpin' : 'icon-pin')} />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<div className={styles.bottomPanel}>
|
{!shouldHidePresentation && (
|
||||||
<div className={styles.info}>
|
<div className={styles.bottomPanel}>
|
||||||
<FullNameTitle peer={user || chat!} className={styles.name} />
|
<div className={styles.info}>
|
||||||
<div className={styles.status}>{status}</div>
|
<FullNameTitle peer={user || chat!} className={styles.name} />
|
||||||
|
<div className={styles.status}>{status}</div>
|
||||||
|
</div>
|
||||||
|
<OutlinedMicrophoneIcon participant={participant} className={styles.icon} noColor />
|
||||||
</div>
|
</div>
|
||||||
<OutlinedMicrophoneIcon participant={participant} className={styles.icon} noColor />
|
)}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<GroupCallParticipantMenu
|
<GroupCallParticipantMenu
|
||||||
|
|||||||
@ -26,10 +26,6 @@
|
|||||||
box-shadow: 0 0.125rem 0.125rem var(--color-light-shadow);
|
box-shadow: 0 0.125rem 0.125rem var(--color-light-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
&.has-pinned-offset {
|
&.has-pinned-offset {
|
||||||
top: calc(100% + 2.875rem);
|
top: calc(100% + 2.875rem);
|
||||||
|
|||||||
@ -10,6 +10,8 @@ import { selectChatGroupCall } from '../../../global/selectors/calls';
|
|||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import { selectChat, selectTabState } from '../../../global/selectors';
|
import { selectChat, selectTabState } from '../../../global/selectors';
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
|
import useShowTransition from '../../../hooks/useShowTransition';
|
||||||
|
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
|
||||||
|
|
||||||
import Button from '../../ui/Button';
|
import Button from '../../ui/Button';
|
||||||
import Avatar from '../../common/Avatar';
|
import Avatar from '../../common/Avatar';
|
||||||
@ -83,24 +85,32 @@ const GroupCallTopPane: FC<OwnProps & StateProps> = ({
|
|||||||
};
|
};
|
||||||
}, [groupCall?.id, groupCall?.isLoaded, isActive, subscribeToGroupCallUpdates]);
|
}, [groupCall?.id, groupCall?.isLoaded, isActive, subscribeToGroupCallUpdates]);
|
||||||
|
|
||||||
if (!groupCall) return undefined;
|
const {
|
||||||
|
shouldRender,
|
||||||
|
transitionClassNames,
|
||||||
|
} = useShowTransition(Boolean(groupCall && isActive));
|
||||||
|
|
||||||
|
const renderingParticipantCount = useCurrentOrPrev(groupCall?.participantsCount, true);
|
||||||
|
const renderingFetchedParticipants = useCurrentOrPrev(fetchedParticipants, true);
|
||||||
|
|
||||||
|
if (!shouldRender) return undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={buildClassName(
|
className={buildClassName(
|
||||||
'GroupCallTopPane',
|
'GroupCallTopPane',
|
||||||
hasPinnedOffset && 'has-pinned-offset',
|
hasPinnedOffset && 'has-pinned-offset',
|
||||||
!isActive && 'is-hidden',
|
|
||||||
className,
|
className,
|
||||||
|
transitionClassNames,
|
||||||
)}
|
)}
|
||||||
onClick={handleJoinGroupCall}
|
onClick={handleJoinGroupCall}
|
||||||
>
|
>
|
||||||
<div className="info">
|
<div className="info">
|
||||||
<span className="title">{lang('VoipGroupVoiceChat')}</span>
|
<span className="title">{lang('VoipGroupVoiceChat')}</span>
|
||||||
<span className="participants">{lang('Participants', groupCall.participantsCount || 0, 'i')}</span>
|
<span className="participants">{lang('Participants', renderingParticipantCount ?? 0, 'i')}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="avatars">
|
<div className="avatars">
|
||||||
{fetchedParticipants.map((peer) => (
|
{renderingFetchedParticipants?.map((peer) => (
|
||||||
<Avatar
|
<Avatar
|
||||||
key={peer.id}
|
key={peer.id}
|
||||||
peer={peer}
|
peer={peer}
|
||||||
|
|||||||
@ -174,11 +174,17 @@ export default function useGroupCallVideoLayout({
|
|||||||
|
|
||||||
const columns = calculateColumnsCount(videosCount);
|
const columns = calculateColumnsCount(videosCount);
|
||||||
const rows = Math.ceil(videosCount / columns);
|
const rows = Math.ceil(videosCount / columns);
|
||||||
|
const totalGridSize = rows * columns;
|
||||||
|
const shouldFillLastRow = totalGridSize > videosCount;
|
||||||
const width = (containerWidth - (columns - 1) * PADDING_HORIZONTAL) / columns;
|
const width = (containerWidth - (columns - 1) * PADDING_HORIZONTAL) / columns;
|
||||||
const height = (containerHeight - (rows - 1) * PADDING_VERTICAL) / rows;
|
const height = (containerHeight - (rows - 1) * PADDING_VERTICAL) / rows;
|
||||||
|
|
||||||
|
const lastRowWidth = width * (videosCount % columns);
|
||||||
for (let i = 0; i < videosCount; i++) {
|
for (let i = 0; i < videosCount; i++) {
|
||||||
const x = initialX + (i % columns) * (width + PADDING_HORIZONTAL);
|
const row = Math.floor(i / columns);
|
||||||
|
const shouldCenter = shouldFillLastRow && row === rows - 1;
|
||||||
|
const x = initialX + (i % columns) * (width + PADDING_HORIZONTAL)
|
||||||
|
+ (shouldCenter ? (containerWidth - lastRowWidth) / 2 : 0);
|
||||||
const y = initialY + Math.floor(i / columns) * (height + PADDING_VERTICAL);
|
const y = initialY + Math.floor(i / columns) * (height + PADDING_VERTICAL);
|
||||||
layout.push({
|
layout.push({
|
||||||
x,
|
x,
|
||||||
|
|||||||
@ -34,17 +34,26 @@ addActionHandler('leaveGroupCall', async (global, actions, payload): Promise<voi
|
|||||||
isFromLibrary, shouldDiscard, shouldRemove, rejoin,
|
isFromLibrary, shouldDiscard, shouldRemove, rejoin,
|
||||||
tabId = getCurrentTabId(),
|
tabId = getCurrentTabId(),
|
||||||
} = payload || {};
|
} = payload || {};
|
||||||
|
|
||||||
const groupCall = selectActiveGroupCall(global);
|
const groupCall = selectActiveGroupCall(global);
|
||||||
if (!groupCall) {
|
if (!groupCall) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
global = updateActiveGroupCall(global, { connectionState: 'disconnected' }, groupCall.participantsCount - 1);
|
global = updateActiveGroupCall(global, { connectionState: 'disconnected' }, groupCall.participantsCount - 1);
|
||||||
|
global = {
|
||||||
|
...global,
|
||||||
|
groupCalls: {
|
||||||
|
...global.groupCalls,
|
||||||
|
activeGroupCallId: undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
|
|
||||||
await callApi('leaveGroupCall', {
|
await callApi('leaveGroupCall', {
|
||||||
call: groupCall,
|
call: groupCall,
|
||||||
});
|
});
|
||||||
|
await callApi('abortRequestGroup', 'call');
|
||||||
|
|
||||||
if (shouldDiscard) {
|
if (shouldDiscard) {
|
||||||
await callApi('discardGroupCall', {
|
await callApi('discardGroupCall', {
|
||||||
@ -59,13 +68,6 @@ addActionHandler('leaveGroupCall', async (global, actions, payload): Promise<voi
|
|||||||
|
|
||||||
removeGroupCallAudioElement();
|
removeGroupCallAudioElement();
|
||||||
|
|
||||||
global = {
|
|
||||||
...global,
|
|
||||||
groupCalls: {
|
|
||||||
...global.groupCalls,
|
|
||||||
activeGroupCallId: undefined,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
|
|
||||||
actions.toggleGroupCallPanel({ force: undefined, tabId });
|
actions.toggleGroupCallPanel({ force: undefined, tabId });
|
||||||
@ -221,7 +223,15 @@ addActionHandler('connectToActiveGroupCall', async (global, actions, payload): P
|
|||||||
|
|
||||||
global = getGlobal();
|
global = getGlobal();
|
||||||
|
|
||||||
if (!result) return;
|
if (!result) {
|
||||||
|
actions.showNotification({
|
||||||
|
// TODO[lang] Localize error message
|
||||||
|
message: 'Failed to join voice chat',
|
||||||
|
tabId,
|
||||||
|
});
|
||||||
|
actions.leaveGroupCall({ tabId });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
actions.loadMoreGroupCallParticipants();
|
actions.loadMoreGroupCallParticipants();
|
||||||
|
|
||||||
|
|||||||
@ -2322,7 +2322,7 @@ export async function loadFullChat<T extends GlobalState>(
|
|||||||
global = updateGroupCall(
|
global = updateGroupCall(
|
||||||
global,
|
global,
|
||||||
groupCall.id!,
|
groupCall.id!,
|
||||||
omit(groupCall, ['connectionState']),
|
omit(groupCall, ['connectionState', 'isLoaded']),
|
||||||
undefined,
|
undefined,
|
||||||
existingGroupCall ? undefined : groupCall.participantsCount,
|
existingGroupCall ? undefined : groupCall.participantsCount,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -284,7 +284,7 @@ addActionHandler('joinGroupCall', async (global, actions, payload): Promise<void
|
|||||||
const { groupCalls: { activeGroupCallId } } = global;
|
const { groupCalls: { activeGroupCallId } } = global;
|
||||||
let groupCall = id ? selectGroupCall(global, id) : selectChatGroupCall(global, chatId!);
|
let groupCall = id ? selectGroupCall(global, id) : selectChatGroupCall(global, chatId!);
|
||||||
|
|
||||||
if (groupCall?.id === activeGroupCallId) {
|
if (groupCall && groupCall.id === activeGroupCallId) {
|
||||||
actions.toggleGroupCallPanel({ tabId });
|
actions.toggleGroupCallPanel({ tabId });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -304,7 +304,15 @@ addActionHandler('joinGroupCall', async (global, actions, payload): Promise<void
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!groupCall && (!id || !accessHash)) {
|
if (!groupCall && (!id || !accessHash) && chatId) {
|
||||||
|
const chat = selectChat(global, chatId);
|
||||||
|
|
||||||
|
if (!chat) return;
|
||||||
|
|
||||||
|
await loadFullChat(global, actions, chat, tabId);
|
||||||
|
global = getGlobal();
|
||||||
|
groupCall = selectChatGroupCall(global, chatId);
|
||||||
|
} else if (!groupCall && id && accessHash) {
|
||||||
groupCall = await fetchGroupCall(global, {
|
groupCall = await fetchGroupCall(global, {
|
||||||
id,
|
id,
|
||||||
accessHash,
|
accessHash,
|
||||||
@ -461,6 +469,7 @@ export function checkNavigatorUserMediaPermissions<T extends GlobalState>(
|
|||||||
tabId,
|
tabId,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
stream.getTracks().forEach((track) => track.stop());
|
||||||
checkMicrophonePermission(global, actions, tabId);
|
checkMicrophonePermission(global, actions, tabId);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -485,6 +494,8 @@ function checkMicrophonePermission<T extends GlobalState>(
|
|||||||
message: langProvider.translate('RequestAcces.Error.HaveNotAccess.Call'),
|
message: langProvider.translate('RequestAcces.Error.HaveNotAccess.Call'),
|
||||||
tabId,
|
tabId,
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
stream.getTracks().forEach((track) => track.stop());
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
|||||||
2
src/lib/gramjs/client/TelegramClient.d.ts
vendored
2
src/lib/gramjs/client/TelegramClient.d.ts
vendored
@ -11,7 +11,7 @@ declare class TelegramClient {
|
|||||||
async start(authParams: UserAuthParams | BotAuthParams);
|
async start(authParams: UserAuthParams | BotAuthParams);
|
||||||
|
|
||||||
async invoke<R extends Api.AnyRequest>(
|
async invoke<R extends Api.AnyRequest>(
|
||||||
request: R, dcId?: number, abortSignal?: AbortSignal,
|
request: R, dcId?: number, abortSignal?: AbortSignal, shouldRetryOnTimeout?: boolean,
|
||||||
): Promise<R['__response']>;
|
): Promise<R['__response']>;
|
||||||
|
|
||||||
async uploadFile(uploadParams: UploadFileParams): ReturnType<typeof uploadFile>;
|
async uploadFile(uploadParams: UploadFileParams): ReturnType<typeof uploadFile>;
|
||||||
|
|||||||
@ -866,10 +866,12 @@ class TelegramClient {
|
|||||||
* Invokes a MTProtoRequest (sends and receives it) and returns its result
|
* Invokes a MTProtoRequest (sends and receives it) and returns its result
|
||||||
* @param request
|
* @param request
|
||||||
* @param dcId Optional dcId to use when sending the request
|
* @param dcId Optional dcId to use when sending the request
|
||||||
|
* @param abortSignal Optional AbortSignal to cancel the request
|
||||||
|
* @param shouldRetryOnTimeout Whether to retry the request if it times out
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async invoke(request, dcId, abortSignal) {
|
async invoke(request, dcId, abortSignal, shouldRetryOnTimeout = false) {
|
||||||
if (request.classType !== 'request') {
|
if (request.classType !== 'request') {
|
||||||
throw new Error('You can only invoke MTProtoRequests');
|
throw new Error('You can only invoke MTProtoRequests');
|
||||||
}
|
}
|
||||||
@ -918,6 +920,11 @@ class TelegramClient {
|
|||||||
await this.disconnect();
|
await this.disconnect();
|
||||||
await sleep(2000);
|
await sleep(2000);
|
||||||
await this.connect();
|
await this.connect();
|
||||||
|
} else if (e instanceof errors.TimedOutError) {
|
||||||
|
if (!shouldRetryOnTimeout) {
|
||||||
|
state.finished.resolve();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
state.finished.resolve();
|
state.finished.resolve();
|
||||||
throw e;
|
throw e;
|
||||||
|
|||||||
@ -3,6 +3,7 @@ const {
|
|||||||
InvalidDCError,
|
InvalidDCError,
|
||||||
FloodError,
|
FloodError,
|
||||||
BadRequestError,
|
BadRequestError,
|
||||||
|
TimedOutError,
|
||||||
} = require('./RPCBaseErrors');
|
} = require('./RPCBaseErrors');
|
||||||
|
|
||||||
class UserMigrateError extends InvalidDCError {
|
class UserMigrateError extends InvalidDCError {
|
||||||
@ -102,7 +103,7 @@ const rpcErrorRe = [
|
|||||||
[/USER_MIGRATE_(\d+)/, UserMigrateError],
|
[/USER_MIGRATE_(\d+)/, UserMigrateError],
|
||||||
[/NETWORK_MIGRATE_(\d+)/, NetworkMigrateError],
|
[/NETWORK_MIGRATE_(\d+)/, NetworkMigrateError],
|
||||||
[/EMAIL_UNCONFIRMED_(\d+)/, EmailUnconfirmedError],
|
[/EMAIL_UNCONFIRMED_(\d+)/, EmailUnconfirmedError],
|
||||||
|
[/^Timeout$/, TimedOutError],
|
||||||
];
|
];
|
||||||
module.exports = {
|
module.exports = {
|
||||||
rpcErrorRe,
|
rpcErrorRe,
|
||||||
|
|||||||
@ -11,6 +11,8 @@ import {
|
|||||||
IS_NOISE_SUPPRESSION_SUPPORTED,
|
IS_NOISE_SUPPRESSION_SUPPORTED,
|
||||||
THRESHOLD,
|
THRESHOLD,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
import Deferred from "../../util/Deferred";
|
||||||
|
import safePlay from "../../util/safePlay";
|
||||||
|
|
||||||
export type StreamType = 'audio' | 'video' | 'presentation';
|
export type StreamType = 'audio' | 'video' | 'presentation';
|
||||||
const DEFAULT_MID = 3;
|
const DEFAULT_MID = 3;
|
||||||
@ -46,6 +48,9 @@ type GroupCallState = {
|
|||||||
audioContext?: AudioContext;
|
audioContext?: AudioContext;
|
||||||
mediaStream?: MediaStream;
|
mediaStream?: MediaStream;
|
||||||
lastMid: number;
|
lastMid: number;
|
||||||
|
audioStream?: MediaStream;
|
||||||
|
audioSource?: MediaStreamAudioSourceNode;
|
||||||
|
audioAnalyser?: AnalyserNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
let state: GroupCallState | undefined;
|
let state: GroupCallState | undefined;
|
||||||
@ -142,7 +147,11 @@ function updateGroupCallStreams(userId: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUserStream(streamType: StreamType, facing: VideoFacingModeEnum = 'user') {
|
async function getUserStream(streamType: StreamType, facing: VideoFacingModeEnum = 'user') {
|
||||||
|
if(streamType === 'audio' && state?.audioStream) {
|
||||||
|
return state.audioStream;
|
||||||
|
}
|
||||||
|
|
||||||
if (streamType === 'presentation') {
|
if (streamType === 'presentation') {
|
||||||
return (navigator.mediaDevices as any).getDisplayMedia({
|
return (navigator.mediaDevices as any).getDisplayMedia({
|
||||||
audio: false,
|
audio: false,
|
||||||
@ -150,7 +159,7 @@ function getUserStream(streamType: StreamType, facing: VideoFacingModeEnum = 'us
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return navigator.mediaDevices.getUserMedia({
|
const media = await navigator.mediaDevices.getUserMedia({
|
||||||
audio: streamType === 'audio' ? {
|
audio: streamType === 'audio' ? {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
...(IS_ECHO_CANCELLATION_SUPPORTED && { echoCancellation: true }),
|
...(IS_ECHO_CANCELLATION_SUPPORTED && { echoCancellation: true }),
|
||||||
@ -159,8 +168,22 @@ function getUserStream(streamType: StreamType, facing: VideoFacingModeEnum = 'us
|
|||||||
video: streamType === 'video' ? {
|
video: streamType === 'video' ? {
|
||||||
facingMode: facing,
|
facingMode: facing,
|
||||||
} : false,
|
} : false,
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if(state && streamType === 'audio'){
|
||||||
|
state.audioStream = media;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(streamType === 'video') {
|
||||||
|
const vid = document.createElement('video');
|
||||||
|
vid.srcObject = media;
|
||||||
|
|
||||||
|
const deferred = new Deferred();
|
||||||
|
vid.oncanplay = () => deferred.resolve();
|
||||||
|
await deferred.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
return media;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function switchCameraInput() {
|
export async function switchCameraInput() {
|
||||||
@ -230,9 +253,9 @@ export async function toggleStream(streamType: StreamType, value: boolean | unde
|
|||||||
} else if (streamType === 'audio') {
|
} else if (streamType === 'audio') {
|
||||||
const { audioContext } = state;
|
const { audioContext } = state;
|
||||||
if (!audioContext) return;
|
if (!audioContext) return;
|
||||||
const source = audioContext.createMediaStreamSource(newStream);
|
const source = state.audioSource || audioContext.createMediaStreamSource(newStream);
|
||||||
|
|
||||||
const analyser = audioContext.createAnalyser();
|
const analyser = state.audioAnalyser || audioContext.createAnalyser();
|
||||||
analyser.minDecibels = -100;
|
analyser.minDecibels = -100;
|
||||||
analyser.maxDecibels = -30;
|
analyser.maxDecibels = -30;
|
||||||
analyser.smoothingTimeConstant = 0.05;
|
analyser.smoothingTimeConstant = 0.05;
|
||||||
@ -242,6 +265,8 @@ export async function toggleStream(streamType: StreamType, value: boolean | unde
|
|||||||
|
|
||||||
state = {
|
state = {
|
||||||
...state,
|
...state,
|
||||||
|
audioSource: source,
|
||||||
|
audioAnalyser: analyser,
|
||||||
participantFunctions: {
|
participantFunctions: {
|
||||||
...state.participantFunctions,
|
...state.participantFunctions,
|
||||||
[state.myId]: {
|
[state.myId]: {
|
||||||
@ -256,7 +281,6 @@ export async function toggleStream(streamType: StreamType, value: boolean | unde
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else if (!value && track.enabled) {
|
} else if (!value && track.enabled) {
|
||||||
track.stop();
|
|
||||||
const newStream = streamType === 'audio' ? state.silence : state.black;
|
const newStream = streamType === 'audio' ? state.silence : state.black;
|
||||||
if (!newStream) return;
|
if (!newStream) return;
|
||||||
|
|
||||||
@ -265,6 +289,14 @@ export async function toggleStream(streamType: StreamType, value: boolean | unde
|
|||||||
if (streamType === 'video') {
|
if (streamType === 'video') {
|
||||||
state.facingMode = undefined;
|
state.facingMode = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(streamType !== 'audio') {
|
||||||
|
// We only want to stop video streams
|
||||||
|
track.stop();
|
||||||
|
} else {
|
||||||
|
state.audioSource?.disconnect();
|
||||||
|
state.audioAnalyser?.disconnect();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
updateGroupCallStreams(state.myId!);
|
updateGroupCallStreams(state.myId!);
|
||||||
if (streamType === 'presentation' && !value) leavePresentation(true);
|
if (streamType === 'presentation' && !value) leavePresentation(true);
|
||||||
@ -292,6 +324,10 @@ export function leaveGroupCall() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
state.audioStream?.getTracks().forEach((track) => {
|
||||||
|
track.stop();
|
||||||
|
});
|
||||||
leavePresentation(true);
|
leavePresentation(true);
|
||||||
state.dataChannel?.close();
|
state.dataChannel?.close();
|
||||||
state.connection?.close();
|
state.connection?.close();
|
||||||
@ -679,6 +715,7 @@ function initializeConnection(
|
|||||||
if (!isPresentation) {
|
if (!isPresentation) {
|
||||||
connection.oniceconnectionstatechange = () => {
|
connection.oniceconnectionstatechange = () => {
|
||||||
const connectionState = connection.iceConnectionState;
|
const connectionState = connection.iceConnectionState;
|
||||||
|
console.log('iceconnectionstatechange', connectionState);
|
||||||
if (connectionState === 'connected' || connectionState === 'completed') {
|
if (connectionState === 'connected' || connectionState === 'completed') {
|
||||||
updateConnectionState('connected');
|
updateConnectionState('connected');
|
||||||
} else if (connectionState === 'checking' || connectionState === 'new') {
|
} else if (connectionState === 'checking' || connectionState === 'new') {
|
||||||
@ -688,10 +725,15 @@ function initializeConnection(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
connection.onconnectionstatechange = () => {
|
||||||
|
console.log('connectionstatechange', connection.connectionState);
|
||||||
|
}
|
||||||
connection.ontrack = handleTrack;
|
connection.ontrack = handleTrack;
|
||||||
connection.onnegotiationneeded = async () => {
|
connection.onnegotiationneeded = async () => {
|
||||||
if (!state) return;
|
if (!state) return;
|
||||||
|
|
||||||
|
console.log('onnegotiationneeded');
|
||||||
|
|
||||||
const { myId } = state;
|
const { myId } = state;
|
||||||
|
|
||||||
if (!myId) {
|
if (!myId) {
|
||||||
@ -701,8 +743,10 @@ function initializeConnection(
|
|||||||
offerToReceiveVideo: true,
|
offerToReceiveVideo: true,
|
||||||
offerToReceiveAudio: !isPresentation,
|
offerToReceiveAudio: !isPresentation,
|
||||||
});
|
});
|
||||||
|
console.log('offer created');
|
||||||
|
|
||||||
await connection.setLocalDescription(offer);
|
await connection.setLocalDescription(offer);
|
||||||
|
console.log('local desc set');
|
||||||
|
|
||||||
if (!offer.sdp) {
|
if (!offer.sdp) {
|
||||||
return;
|
return;
|
||||||
@ -827,7 +871,7 @@ export function joinGroupCall(
|
|||||||
|
|
||||||
const mediaStream = new MediaStream();
|
const mediaStream = new MediaStream();
|
||||||
audioElement.srcObject = mediaStream;
|
audioElement.srcObject = mediaStream;
|
||||||
audioElement.play().catch((l) => console.warn(l));
|
safePlay(audioElement);
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
onUpdate,
|
onUpdate,
|
||||||
@ -845,6 +889,9 @@ export function joinGroupCall(
|
|||||||
lastMid: DEFAULT_MID,
|
lastMid: DEFAULT_MID,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Prepare microphone
|
||||||
|
getUserStream('audio');
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
state = {
|
state = {
|
||||||
...state!,
|
...state!,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user