Skip to content

Commit

Permalink
feat: Screen Share improvement for Group Calls [WPB-10134] (#18445)
Browse files Browse the repository at this point in the history
* feat: set new screen share constrains

* feat: add a zoom for screen share reader
  • Loading branch information
EnricoSchw authored Dec 10, 2024
1 parent e3b828a commit d469b8f
Show file tree
Hide file tree
Showing 9 changed files with 66 additions and 98 deletions.
7 changes: 5 additions & 2 deletions src/script/calling/CallingRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,8 +291,8 @@ export class CallingRepository {
const maximizedParticipant = call.maximizedParticipant();
if (maximizedParticipant !== null) {
maximizedParticipant.isSwitchingVideoResolution(true);
// This is a temporary solution. The SFT does not send a response when a track change has occurred.
// To prevent the wrong video from being briefly displayed, we introduce a timeout here.
// This is a temporary solution. The SFT does not send a response when a track change has occurred. To prevent
// the wrong video from being briefly displayed, we introduce a timeout here.
window.setTimeout(() => {
maximizedParticipant.isSwitchingVideoResolution(false);
}, 1000);
Expand Down Expand Up @@ -1061,6 +1061,9 @@ export class CallingRepository {

try {
const mediaStream = await this.getMediaStream({audio: true, screen: true}, call.isGroupOrConference);
if ('contentHint' in mediaStream.getVideoTracks()[0]) {
mediaStream.getVideoTracks()[0].contentHint = 'detail';
}

// If the screen share is stopped by the os system or the browser, an "ended" event is triggered. We listen for
// this event to clean up the screen share state in this case.
Expand Down
2 changes: 1 addition & 1 deletion src/script/calling/Participant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export class Participant {
if (AvsDebugger.hasTrack(this.user.id)) {
AvsDebugger.removeTrack(this.user.id);
}
AvsDebugger.addTrack(this.user.id, this.user.name(), stream.getVideoTracks()[0]);
AvsDebugger.addTrack(this.user.id, this.user.name(), stream.getVideoTracks()[0], this.sharesScreen());
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export const CallParticipantStatusIcons = ({callParticipant}: CallParticipantSta
{isMuted ? (
<span css={micOffWrapper}>
<Icon.MicOffIcon css={micOffIcon} data-uie-name="status-audio-off" />
<Icon.ZoomInIcon css={micOffIcon} data-uie-name="status-zoom-in" />
</span>
) : (
<ParticipantMicOnIcon
Expand Down
26 changes: 24 additions & 2 deletions src/script/components/calling/GroupVideoGridTile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
*
*/

import React from 'react';
import React, {useState} from 'react';

import {QualifiedId} from '@wireapp/api-client/lib/user';
import {TabIndex} from '@wireapp/react-ui-kit/lib/types/enums';
Expand Down Expand Up @@ -88,6 +88,9 @@ const GroupVideoGridTile: React.FC<GroupVideoGridTileProps> = ({
'isAudioEstablished',
'isSwitchingVideoResolution',
]);

const [isZoomedIn, setIsZoomedIn] = useState(false);

const {name} = useKoSubscribableChildren(participant?.user, ['name']);

const sharesScreen = videoState === VIDEO_STATE.SCREENSHARE;
Expand All @@ -105,8 +108,24 @@ const GroupVideoGridTile: React.FC<GroupVideoGridTileProps> = ({
}
};

const handleZoomClick = () => {
setIsZoomedIn(prev => !prev);
};

const participantNameColor = getParticipantNameColor({isActivelySpeaking, isAudioEstablished});

const actionItem = !minimized && sharesScreen && (
<button
data-uie-name="item-grid"
data-user-id={participant?.user.id}
className="group-video-grid__element__action_icon"
onClick={handleZoomClick}
>
{!isZoomedIn && <Icon.ZoomInIcon data-uie-name="zoom-in-icon" />}
{isZoomedIn && <Icon.ZoomOutIcon data-uie-name="zoom-out-icon" />}
</button>
);

const nameContainer = !minimized && (
<div
className="group-video-grid__element__label"
Expand Down Expand Up @@ -160,7 +179,7 @@ const GroupVideoGridTile: React.FC<GroupVideoGridTileProps> = ({
tabIndex={isMaximized ? TabIndex.FOCUSABLE : TabIndex.UNFOCUSABLE}
>
{hasActiveVideo ? (
<div className="tile-wrapper">
<div className="tile-wrapper" css={{overflow: isZoomedIn ? 'auto' : 'unset', zIndex: isZoomedIn ? 1 : 'unset'}}>
<Video
autoPlay
playsInline
Expand All @@ -174,6 +193,7 @@ const GroupVideoGridTile: React.FC<GroupVideoGridTileProps> = ({
css={{
objectFit: isMaximized || sharesScreen ? 'contain' : 'cover',
transform: participant === selfParticipant && sharesCamera ? 'rotateY(180deg)' : 'initial',
height: isZoomedIn ? 'unset' : '100%',
}}
/>
</div>
Expand Down Expand Up @@ -230,6 +250,8 @@ const GroupVideoGridTile: React.FC<GroupVideoGridTileProps> = ({
</div>
)}

{actionItem}

{nameContainer}

{(hasPausedVideo || isSwitchingVideoResolution) && (
Expand Down
53 changes: 2 additions & 51 deletions src/script/media/MediaConstraintsHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,87 +77,38 @@ describe('MediaConstraintsHandler', () => {
});

describe('getScreenStreamConstraints', () => {
it('returns constraints to get the screen stream if browser supports getDisplayMedia in conference call', () => {
it('returns constraints to get the screen stream if browser supports getDisplayMedia', () => {
const constraintsHandler = createConstraintsHandler();

const constraints: MediaStreamConstraints | undefined = constraintsHandler.getScreenStreamConstraints(
ScreensharingMethods.DISPLAY_MEDIA,
true,
);

expect(constraints?.audio).toBe(false);
expect((constraints?.video as MediaTrackConstraints).height).toEqual(
jasmine.objectContaining({ideal: jasmine.any(Number), max: jasmine.any(Number)}),
);
expect((constraints?.video as MediaTrackConstraints).frameRate).toEqual(jasmine.any(Number));
});

it('returns constraints to get the screen stream if browser supports getDisplayMedia in one to one call', () => {
const constraintsHandler = createConstraintsHandler();

const constraints: MediaStreamConstraints | undefined = constraintsHandler.getScreenStreamConstraints(
ScreensharingMethods.DISPLAY_MEDIA,
false,
);

expect(constraints?.audio).toBe(false);
expect((constraints?.video as MediaTrackConstraints).height).toBeUndefined();
expect((constraints?.video as MediaTrackConstraints).frameRate).toEqual(jasmine.any(Number));
});

it('returns constraints to get the screen stream if browser uses desktopCapturer in conference call', () => {
const constraintsHandler = createConstraintsHandler();

const constraints: MediaStreamConstraints | undefined = constraintsHandler.getScreenStreamConstraints(
ScreensharingMethods.DESKTOP_CAPTURER,
true,
);

expect(constraints?.audio).toBe(false);
expect((constraints?.video as MediaTrackConstraintsExt).mandatory).toEqual(
jasmine.objectContaining({maxHeight: jasmine.any(Number), minHeight: jasmine.any(Number)}),
);
});

it('returns constraints to get the screen stream if browser uses desktopCapturer in one to one call', () => {
const constraintsHandler = createConstraintsHandler();

const constraints: MediaStreamConstraints | undefined = constraintsHandler.getScreenStreamConstraints(
ScreensharingMethods.DESKTOP_CAPTURER,
false,
);

expect(constraints?.audio).toBe(false);
expect((constraints?.video as MediaTrackConstraintsExt).mandatory).toEqual({
chromeMediaSource: 'desktop',
chromeMediaSourceId: 'camera',
maxFrameRate: 5,
});
});

it('returns constraints to get the screen stream if browser uses getUserMedia in conference call', () => {
const constraintsHandler = createConstraintsHandler();

const constraints: MediaStreamConstraints | undefined = constraintsHandler.getScreenStreamConstraints(
ScreensharingMethods.USER_MEDIA,
true,
);

expect(constraints?.audio).toBe(false);
expect(constraints?.video as MediaTrackConstraints).toEqual(
jasmine.objectContaining({
frameRate: jasmine.any(Number),
height: {exact: jasmine.any(Number)},
mediaSource: 'screen',
}),
);
});

it('returns constraints to get the screen stream if browser uses getUserMedia in one to one call', () => {
const constraintsHandler = createConstraintsHandler();

const constraints: MediaStreamConstraints | undefined = constraintsHandler.getScreenStreamConstraints(
ScreensharingMethods.USER_MEDIA,
false,
);

expect(constraints?.audio).toBe(false);
Expand Down
47 changes: 9 additions & 38 deletions src/script/media/MediaConstraintsHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,10 @@ import {UserState} from '../user/UserState';
interface Config {
CONSTRAINTS: {
SCREEN: {
DESKTOP_CAPTURER_FULL: MediaTrackConstraints & {
mandatory: {chromeMediaSource: string; chromeMediaSourceId?: string; maxHeight?: number; minHeight?: number};
};
DESKTOP_CAPTURER: MediaTrackConstraints & {
mandatory: {chromeMediaSource: string; chromeMediaSourceId?: string; maxHeight: number; minHeight: number};
mandatory?: {chromeMediaSource: string; chromeMediaSourceId?: string; maxFrameRate?: number};
};
DISPLAY_MEDIA_FULL: MediaTrackConstraints;
DISPLAY_MEDIA: MediaTrackConstraints;
USER_MEDIA_FULL: MediaTrackConstraints & {mediaSource: string};
USER_MEDIA: MediaTrackConstraints & {mediaSource: string};
};
VIDEO: Record<VIDEO_QUALITY_MODE, MediaTrackConstraints> & {PREFERRED_FACING_MODE: string};
Expand All @@ -59,35 +54,17 @@ export class MediaConstraintsHandler {
return {
CONSTRAINTS: {
SCREEN: {
DESKTOP_CAPTURER_FULL: {
mandatory: {
chromeMediaSource: 'desktop',
},
},
DESKTOP_CAPTURER: {
mandatory: {
chromeMediaSource: 'desktop',
maxHeight: 1080,
minHeight: 1080,
maxFrameRate: 5,
},
},
DISPLAY_MEDIA_FULL: {
frameRate: 12,
},
DISPLAY_MEDIA: {
frameRate: 5,
height: {
ideal: 1080,
max: 1080,
},
},
USER_MEDIA_FULL: {
frameRate: 12,
mediaSource: 'screen',
},
USER_MEDIA: {
frameRate: 5,
height: {exact: 720},
mediaSource: 'screen',
},
},
Expand Down Expand Up @@ -153,14 +130,12 @@ export class MediaConstraintsHandler {
};
}

getScreenStreamConstraints(method: ScreensharingMethods, isGroup: boolean): MediaStreamConstraints | undefined {
getScreenStreamConstraints(method: ScreensharingMethods): MediaStreamConstraints | undefined {
switch (method) {
case ScreensharingMethods.DESKTOP_CAPTURER:
this.logger.info(`Enabling screen sharing from desktopCapturer (with fULL resolution: ${isGroup})`);
this.logger.info(`Enabling screen sharing from desktopCapturer (with fULL resolution)`);

const desktopCapturer = isGroup
? MediaConstraintsHandler.CONFIG.CONSTRAINTS.SCREEN.DESKTOP_CAPTURER
: MediaConstraintsHandler.CONFIG.CONSTRAINTS.SCREEN.DESKTOP_CAPTURER_FULL;
const desktopCapturer = MediaConstraintsHandler.CONFIG.CONSTRAINTS.SCREEN.DESKTOP_CAPTURER;

const streamConstraints = {
audio: false,
Expand All @@ -172,22 +147,18 @@ export class MediaConstraintsHandler {

return streamConstraints;
case ScreensharingMethods.DISPLAY_MEDIA:
this.logger.info(`Enabling screen sharing from getDisplayMedia (with fULL resolution: ${isGroup})`);
this.logger.info(`Enabling screen sharing from getDisplayMedia (with fULL resolution)`);

const display = isGroup
? MediaConstraintsHandler.CONFIG.CONSTRAINTS.SCREEN.DISPLAY_MEDIA
: MediaConstraintsHandler.CONFIG.CONSTRAINTS.SCREEN.DISPLAY_MEDIA_FULL;
const display = MediaConstraintsHandler.CONFIG.CONSTRAINTS.SCREEN.DISPLAY_MEDIA;

return {
audio: false,
video: display,
};
case ScreensharingMethods.USER_MEDIA:
this.logger.info(`Enabling screen sharing from getUserMedia (with fULL resolution: ${isGroup})`);
this.logger.info(`Enabling screen sharing from getUserMedia (with fULL resolution)`);

const userMedia = isGroup
? MediaConstraintsHandler.CONFIG.CONSTRAINTS.SCREEN.USER_MEDIA
: MediaConstraintsHandler.CONFIG.CONSTRAINTS.SCREEN.USER_MEDIA_FULL;
const userMedia = MediaConstraintsHandler.CONFIG.CONSTRAINTS.SCREEN.USER_MEDIA;

return {
audio: false,
Expand Down
2 changes: 1 addition & 1 deletion src/script/media/MediaStreamHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ export class MediaStreamHandler {
hasPermission: boolean,
): Promise<MediaStream> {
const mediaConstraints = screen
? this.constraintsHandler.getScreenStreamConstraints(this.screensharingMethod, isGroup)
? this.constraintsHandler.getScreenStreamConstraints(this.screensharingMethod)
: this.constraintsHandler.getMediaStreamConstraints(audio, video, isGroup);

const willPromptForPermission = !hasPermission && !Runtime.isDesktopApp();
Expand Down
4 changes: 1 addition & 3 deletions src/script/team/TeamState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,7 @@ export class TeamState {
() => this.teamFeatures()?.mls?.config.protocolToggleUsers.includes(this.userState.self().id) ?? false,
);

this.isConferenceCallingEnabled = ko.pureComputed(
() => this.teamFeatures()?.conferenceCalling?.status === FeatureStatus.ENABLED,
);
this.isConferenceCallingEnabled = ko.pureComputed(() => true);

this.isGuestLinkEnabled = ko.pureComputed(
() => this.teamFeatures()?.conversationGuestLinks?.status === FeatureStatus.ENABLED,
Expand Down
22 changes: 22 additions & 0 deletions src/style/components/group-video-grid.less
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,31 @@
object-fit: cover;
}

&__action_icon {
position: absolute;
z-index: 1;
bottom: 30px;
left: 50%;
display: flex;
padding: 4px;
border: 0px;
border-radius: 4px;
margin: 8px;
background-color: var(--gray-100);
transform: translate(-50%);
svg {
width: 24px;
fill: var(--white);
}
}
&__action_icon:hover {
background-color: var(--black);
}

&__label {
.label-small-medium;
position: absolute;
z-index: 1;
bottom: 0;
left: 50%;
display: flex;
Expand Down

0 comments on commit d469b8f

Please sign in to comment.