our video off => show our avatar

This commit is contained in:
Audric Ackermann 2021-10-28 12:03:11 +11:00
parent dfa04c68f4
commit d50d7eb803
No known key found for this signature in database
GPG Key ID: 999F434D76324AD4
7 changed files with 155 additions and 90 deletions

View File

@ -148,8 +148,8 @@
"linkPreviewsTitle": "Send Link Previews",
"linkPreviewDescription": "Previews are supported for most urls",
"linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.",
"mediaPermissionsTitle": "Microphone and Camera",
"mediaPermissionsDescription": "Allow access to camera and microphone",
"mediaPermissionsTitle": "Microphone",
"mediaPermissionsDescription": "Allow access to microphone",
"spellCheckTitle": "Spell Check",
"spellCheckDescription": "Enable spell check of text entered in message composition box",
"spellCheckDirty": "You must restart Session to apply your new settings",
@ -439,8 +439,8 @@
"accept": "Accept",
"decline": "Decline",
"endCall": "End call",
"micAndCameraPermissionNeededTitle": "Camera and Microphone access required",
"micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy",
"cameraPermissionNeededTitle": "Voice/Video Call permissions required",
"cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.",
"unableToCall": "cancel your ongoing call first",
"unableToCallTitle": "Cannot start new call",
"callMissed": "Missed call from $name$",
@ -449,6 +449,7 @@
"noCameraFound": "No camera found",
"noAudioInputFound": "No audio input found",
"callMediaPermissionsTitle": "Voice and video calls",
"callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.",
"callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users",
"callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user."
}

View File

@ -13,7 +13,7 @@ const SessionToastContainerPrivate = () => {
rtl={false}
pauseOnFocusLoss={false}
draggable={false}
pauseOnHover={false}
pauseOnHover={true}
transition={Slide}
limit={5}
/>

View File

@ -15,6 +15,7 @@ import {
import { openConversationWithMessages } from '../../../state/ducks/conversations';
import { Avatar, AvatarSize } from '../../Avatar';
import { getConversationController } from '../../../session/conversations';
import { CallManagerOptionsType } from '../../../session/utils/CallManager';
export const DraggableCallWindow = styled.div`
position: absolute;
@ -28,11 +29,11 @@ export const DraggableCallWindow = styled.div`
border: var(--session-border);
`;
export const StyledVideoElement = styled.video<{ isRemoteVideoMuted: boolean }>`
export const StyledVideoElement = styled.video<{ isVideoMuted: boolean }>`
padding: 0 1rem;
height: 100%;
width: 100%;
opacity: ${props => (props.isRemoteVideoMuted ? 0 : 1)};
opacity: ${props => (props.isVideoMuted ? 0 : 1)};
`;
const StyledDraggableVideoElement = styled(StyledVideoElement)`
@ -97,16 +98,10 @@ export const DraggableCallContainer = () => {
useEffect(() => {
if (ongoingCallPubkey !== selectedConversationKey) {
CallManager.setVideoEventsListener(
(
_localStream: MediaStream | null,
remoteStream: MediaStream | null,
_camerasList: any,
_audioList: any,
remoteVideoIsMuted: boolean
) => {
({ isRemoteVideoStreamMuted, remoteStream }: CallManagerOptionsType) => {
if (mountedState() && videoRefRemote?.current) {
videoRefRemote.current.srcObject = remoteStream;
setIsRemoteVideoMuted(remoteVideoIsMuted);
setIsRemoteVideoMuted(isRemoteVideoStreamMuted);
}
}
);
@ -157,7 +152,7 @@ export const DraggableCallContainer = () => {
<StyledDraggableVideoElement
ref={videoRefRemote}
autoPlay={true}
isRemoteVideoMuted={isRemoteVideoMuted}
isVideoMuted={isRemoteVideoMuted}
/>
{isRemoteVideoMuted && (
<CenteredAvatarInDraggable>

View File

@ -5,7 +5,7 @@ import { useSelector } from 'react-redux';
import useMountedState from 'react-use/lib/useMountedState';
import styled from 'styled-components';
import _ from 'underscore';
import { CallManager, ToastUtils } from '../../../session/utils';
import { CallManager, ToastUtils, UserUtils } from '../../../session/utils';
import {
getHasOngoingCall,
getHasOngoingCallWith,
@ -13,7 +13,7 @@ import {
} from '../../../state/selectors/conversations';
import { SessionIconButton } from '../icon';
import { animation, contextMenu, Item, Menu } from 'react-contexify';
import { InputItem } from '../../../session/utils/CallManager';
import { CallManagerOptionsType, InputItem } from '../../../session/utils/CallManager';
import { DropDownAndToggleButton } from '../icon/DropDownAndToggleButton';
import { StyledVideoElement } from './CallContainer';
import { Avatar, AvatarSize } from '../../Avatar';
@ -124,8 +124,9 @@ const AudioInputMenu = ({
};
const CenteredAvatarInConversation = styled.div`
position: absolute;
top: 0;
top: -50%;
transform: translateY(-50%);
position: relative;
bottom: 0;
left: 0;
right: 50%;
@ -151,39 +152,50 @@ export const InConversationCallContainer = () => {
const videoRefLocal = useRef<any>();
const mountedState = useMountedState();
const [isVideoMuted, setVideoMuted] = useState(true);
const [isLocalVideoMuted, setLocalVideoMuted] = useState(true);
const [isRemoteVideoMuted, setIsRemoteVideoMuted] = useState(true);
const [isAudioMuted, setAudioMuted] = useState(false);
const videoTriggerId = 'video-menu-trigger-id';
const audioTriggerId = 'audio-menu-trigger-id';
const avatarPath = ongoingCallPubkey
const remoteAvatarPath = ongoingCallPubkey
? getConversationController()
.get(ongoingCallPubkey)
.getAvatarPath()
: undefined;
const ourPubkey = UserUtils.getOurPubKeyStrFromCache();
const ourUsername = getConversationController()
.get(ourPubkey)
.getProfileName();
const ourAvatarPath = getConversationController()
.get(ourPubkey)
.getAvatarPath();
useEffect(() => {
if (ongoingCallPubkey === selectedConversationKey) {
CallManager.setVideoEventsListener(
(
localStream: MediaStream | null,
remoteStream: MediaStream | null,
camerasList: Array<InputItem>,
audioInputList: Array<InputItem>,
isRemoteVideoStreamMuted: boolean
) => {
if (mountedState() && videoRefRemote?.current && videoRefLocal?.current) {
videoRefLocal.current.srcObject = localStream;
setIsRemoteVideoMuted(isRemoteVideoStreamMuted);
videoRefRemote.current.srcObject = remoteStream;
CallManager.setVideoEventsListener((options: CallManagerOptionsType) => {
const {
audioInputsList,
camerasList,
isLocalVideoStreamMuted,
isRemoteVideoStreamMuted,
localStream,
remoteStream,
} = options;
if (mountedState() && videoRefRemote?.current && videoRefLocal?.current) {
videoRefLocal.current.srcObject = localStream;
setIsRemoteVideoMuted(isRemoteVideoStreamMuted);
setLocalVideoMuted(isLocalVideoStreamMuted);
videoRefRemote.current.srcObject = remoteStream;
setCurrentConnectedCameras(camerasList);
setCurrentConnectedAudioInputs(audioInputList);
}
setCurrentConnectedCameras(camerasList);
setCurrentConnectedAudioInputs(audioInputsList);
}
);
});
}
return () => {
@ -204,14 +216,14 @@ export const InConversationCallContainer = () => {
return;
}
if (isVideoMuted) {
if (isLocalVideoMuted) {
// select the first one
await CallManager.selectCameraByDeviceId(currentConnectedCameras[0].deviceId);
} else {
await CallManager.selectCameraByDeviceId(CallManager.INPUT_DISABLED_DEVICE_ID);
}
setVideoMuted(!isVideoMuted);
setLocalVideoMuted(!isLocalVideoMuted);
};
const handleMicrophoneToggle = async () => {
@ -263,13 +275,13 @@ export const InConversationCallContainer = () => {
<StyledVideoElement
ref={videoRefRemote}
autoPlay={true}
isRemoteVideoMuted={isRemoteVideoMuted}
isVideoMuted={isRemoteVideoMuted}
/>
{isRemoteVideoMuted && (
<CenteredAvatarInConversation>
<Avatar
size={AvatarSize.XL}
avatarPath={avatarPath}
avatarPath={remoteAvatarPath}
name={ongoingCallUsername}
pubkey={ongoingCallPubkey}
/>
@ -281,8 +293,18 @@ export const InConversationCallContainer = () => {
ref={videoRefLocal}
autoPlay={true}
muted={true}
isRemoteVideoMuted={false}
isVideoMuted={isLocalVideoMuted}
/>
{isLocalVideoMuted && (
<CenteredAvatarInConversation>
<Avatar
size={AvatarSize.XL}
avatarPath={ourAvatarPath}
name={ourUsername}
pubkey={ourPubkey}
/>
</CenteredAvatarInConversation>
)}
</VideoContainer>
<InConvoCallWindowControls>
@ -298,7 +320,7 @@ export const InConversationCallContainer = () => {
/>
<DropDownAndToggleButton
iconType="camera"
isMuted={isVideoMuted}
isMuted={isLocalVideoMuted}
onMainButtonClick={handleCameraToggle}
onArrowClick={showVideoInputMenu}
/>
@ -312,7 +334,7 @@ export const InConversationCallContainer = () => {
<VideoInputMenu
triggerId={videoTriggerId}
onUnmute={() => {
setVideoMuted(false);
setLocalVideoMuted(false);
}}
camerasList={currentConnectedCameras}
/>

View File

@ -373,7 +373,7 @@ export function getStartCallMenuItem(conversationId: string): JSX.Element | null
}
if (!getCallMediaPermissionsSettings()) {
ToastUtils.pushMicAndCameraPermissionNeeded();
ToastUtils.pushVideoCallPermissionNeeded();
return;
}

View File

@ -22,26 +22,28 @@ export type InputItem = { deviceId: string; label: string };
// const VIDEO_WIDTH = 640;
// const VIDEO_RATIO = 16 / 9;
type CallManagerListener =
| ((
localStream: MediaStream | null,
remoteStream: MediaStream | null,
camerasList: Array<InputItem>,
audioInputsList: Array<InputItem>,
isRemoteVideoStreamMuted: boolean
) => void)
| null;
export type CallManagerOptionsType = {
localStream: MediaStream | null;
remoteStream: MediaStream | null;
camerasList: Array<InputItem>;
audioInputsList: Array<InputItem>;
isLocalVideoStreamMuted: boolean;
isRemoteVideoStreamMuted: boolean;
};
export type CallManagerListener = ((options: CallManagerOptionsType) => void) | null;
let videoEventsListener: CallManagerListener;
function callVideoListener() {
if (videoEventsListener) {
videoEventsListener(
mediaDevices,
videoEventsListener({
localStream: mediaDevices,
remoteStream,
camerasList,
audioInputsList,
remoteVideoStreamIsMuted
);
isRemoteVideoStreamMuted: remoteVideoStreamIsMuted,
isLocalVideoStreamMuted: selectedCameraId === INPUT_DISABLED_DEVICE_ID,
});
}
}
@ -79,7 +81,7 @@ const configuration: RTCConfiguration = {
iceTransportPolicy: 'relay',
};
let selectedCameraId: string | undefined;
let selectedCameraId: string = INPUT_DISABLED_DEVICE_ID;
let selectedAudioInputId: string | undefined;
let camerasList: Array<InputItem> = [];
let audioInputsList: Array<InputItem> = [];
@ -115,8 +117,7 @@ async function updateInputLists() {
}
function sendVideoStatusViaDataChannel() {
const videoEnabledLocally =
selectedCameraId !== undefined && selectedCameraId !== INPUT_DISABLED_DEVICE_ID;
const videoEnabledLocally = selectedCameraId !== INPUT_DISABLED_DEVICE_ID;
const stringToSend = JSON.stringify({
video: videoEnabledLocally,
});
@ -127,7 +128,7 @@ function sendVideoStatusViaDataChannel() {
export async function selectCameraByDeviceId(cameraDeviceId: string) {
if (cameraDeviceId === INPUT_DISABLED_DEVICE_ID) {
selectedCameraId = cameraDeviceId;
selectedCameraId = INPUT_DISABLED_DEVICE_ID;
const sender = peerConnection?.getSenders().find(s => {
return s.track?.kind === 'video';
@ -136,6 +137,7 @@ export async function selectCameraByDeviceId(cameraDeviceId: string) {
sender.track.enabled = false;
}
sendVideoStatusViaDataChannel();
callVideoListener();
return;
}
if (camerasList.some(m => m.deviceId === cameraDeviceId)) {
@ -164,12 +166,15 @@ export async function selectCameraByDeviceId(cameraDeviceId: string) {
mediaDevices?.removeTrack(t);
});
mediaDevices?.addTrack(videoTrack);
sendVideoStatusViaDataChannel();
callVideoListener();
} else {
throw new Error('Failed to get sender for selectCameraByDeviceId ');
}
} catch (e) {
window.log.warn('selectCameraByDeviceId failed with', e.message);
callVideoListener();
}
}
}
@ -301,7 +306,7 @@ async function openMediaDevicesAndAddTracks() {
}
});
} catch (err) {
ToastUtils.pushMicAndCameraPermissionNeeded();
ToastUtils.pushVideoCallPermissionNeeded();
closeVideoCall();
}
callVideoListener();
@ -310,7 +315,7 @@ async function openMediaDevicesAndAddTracks() {
// tslint:disable-next-line: function-name
export async function USER_callRecipient(recipient: string) {
if (!getCallMediaPermissionsSettings()) {
ToastUtils.pushMicAndCameraPermissionNeeded();
ToastUtils.pushVideoCallPermissionNeeded();
return;
}
await updateInputLists();
@ -420,8 +425,16 @@ function closeVideoCall() {
mediaDevices = null;
remoteStream = null;
selectedCameraId = INPUT_DISABLED_DEVICE_ID;
if (videoEventsListener) {
videoEventsListener(null, null, [], [], true);
videoEventsListener({
audioInputsList: [],
camerasList: [],
isLocalVideoStreamMuted: true,
isRemoteVideoStreamMuted: true,
localStream: null,
remoteStream: null,
});
}
}
@ -592,7 +605,14 @@ export function handleCallTypeEndCall(sender: string) {
if (callingConvos.length === 1 && callingConvos[0].id === sender) {
closeVideoCall();
if (videoEventsListener) {
videoEventsListener(null, null, [], [], true);
videoEventsListener({
audioInputsList: [],
camerasList: [],
isLocalVideoStreamMuted: true,
isRemoteVideoStreamMuted: true,
localStream: null,
remoteStream: null,
});
}
window.inboxStore?.dispatch(endCall({ pubkey: sender }));
}
@ -633,21 +653,20 @@ export async function handleCallTypeOffer(
const convos = getConversationController().getConversations();
const callingConvos = convos.filter(convo => convo.callState !== undefined);
if (!getCallMediaPermissionsSettings()) {
await handleMissedCall(sender, incomingOfferTimestamp, true);
return;
}
if (callingConvos.length > 0) {
// we just got a new offer from someone we are NOT already in a call with
if (callingConvos.length !== 1 || callingConvos[0].id !== sender) {
await handleMissedCall(sender, incomingOfferTimestamp);
await handleMissedCall(sender, incomingOfferTimestamp, false);
return;
}
}
if (!getCallMediaPermissionsSettings()) {
await handleMissedCall(sender, incomingOfferTimestamp);
// TODO audric show where to turn it on
throw new Error('TODO AUDRIC');
return;
}
const readyForOffer =
!makingOffer && (peerConnection?.signalingState === 'stable' || isSettingRemoteAnswerPending);
const polite = lastOutgoingOfferTimestamp < incomingOfferTimestamp;
@ -672,6 +691,7 @@ export async function handleCallTypeOffer(
await buildAnswerAndSendIt(sender);
}
}
window.inboxStore?.dispatch(incomingCall({ pubkey: sender }));
// don't need to do the sending here as we dispatch an answer in a
} catch (err) {
@ -682,16 +702,28 @@ export async function handleCallTypeOffer(
callCache.set(sender, new Array());
}
callCache.get(sender)?.push(callMessage);
window.inboxStore?.dispatch(incomingCall({ pubkey: sender }));
}
async function handleMissedCall(sender: string, incomingOfferTimestamp: number) {
async function handleMissedCall(
sender: string,
incomingOfferTimestamp: number,
isBecauseOfCallPermission: boolean
) {
const incomingCallConversation = await getConversationById(sender);
ToastUtils.pushedMissedCall(
incomingCallConversation?.getNickname() ||
incomingCallConversation?.getProfileName() ||
'Unknown'
);
if (!isBecauseOfCallPermission) {
ToastUtils.pushedMissedCall(
incomingCallConversation?.getNickname() ||
incomingCallConversation?.getProfileName() ||
'Unknown'
);
} else {
ToastUtils.pushedMissedCallCauseOfPermission(
incomingCallConversation?.getNickname() ||
incomingCallConversation?.getProfileName() ||
'Unknown'
);
}
await incomingCallConversation?.addSingleMessage({
conversationId: incomingCallConversation.id,

View File

@ -148,15 +148,30 @@ export function pushedMissedCall(conversationName: string) {
);
}
export function pushMicAndCameraPermissionNeeded() {
const openPrivacySettings = () => {
window.inboxStore?.dispatch(showLeftPaneSection(SectionType.Settings));
window.inboxStore?.dispatch(showSettingsSection(SessionSettingCategory.Privacy));
};
export function pushedMissedCallCauseOfPermission(conversationName: string) {
const id = 'missedCallPermission';
toast.info(
<SessionToast
title={window.i18n('callMissedTitle')}
description={window.i18n('callMissedCausePermission', conversationName)}
type={SessionToastType.Info}
onToastClick={openPrivacySettings}
/>,
{ toastId: id, updateId: id, autoClose: 10000 }
);
}
export function pushVideoCallPermissionNeeded() {
pushToastInfo(
'micAndCameraPermissionNeeded',
window.i18n('micAndCameraPermissionNeededTitle'),
window.i18n('micAndCameraPermissionNeeded'),
() => {
window.inboxStore?.dispatch(showLeftPaneSection(SectionType.Settings));
window.inboxStore?.dispatch(showSettingsSection(SessionSettingCategory.Privacy));
}
'videoCallPermissionNeeded',
window.i18n('cameraPermissionNeededTitle'),
window.i18n('cameraPermissionNeeded'),
openPrivacySettings
);
}