From 98ba5ad2478115d5a3d28b74785e93c1ef0cc297 Mon Sep 17 00:00:00 2001 From: fuyan Date: Sun, 13 Oct 2024 12:51:19 -0700 Subject: [PATCH 01/13] initial commit --- .../src/baseSelectors.ts | 11 ++++++ .../src/handlers/createCommonHandlers.ts | 30 ++++++++++++++- .../src/handlers/createHandlers.ts | 3 ++ .../src/participantListSelector.ts | 24 +++++++++--- .../src/utils/participantListSelectorUtils.ts | 14 +++++-- .../src/CallClientState.ts | 32 +++++++++++++++- .../src/CallContext.ts | 25 +++++++++++- .../src/CallSubscriber.ts | 9 ++++- .../calling-stateful-client/src/Converter.ts | 3 +- .../src/MediaAccessSubscriber.ts | 38 +++++++++++++++++++ .../src/index-public.ts | 1 + .../review/beta/communication-react.api.md | 36 ++++++++++++++++++ packages/communication-react/src/index.ts | 1 + .../src/components/ParticipantList.tsx | 5 ++- packages/react-components/src/index.ts | 2 + .../src/types/ParticipantListParticipant.ts | 12 ++++++ .../CallComposite/MockCallAdapter.ts | 6 +++ .../adapter/AzureCommunicationCallAdapter.ts | 10 ++++- .../CallComposite/adapter/CallAdapter.ts | 3 ++ .../components/SidePane/usePeoplePane.tsx | 32 ++++++++++++++++ .../CallComposite/hooks/useHandlers.ts | 18 ++++++++- .../AzureCommunicationCallWithChatAdapter.ts | 8 ++++ .../adapter/CallWithChatAdapter.ts | 3 ++ .../adapter/CallWithChatBackedCallAdapter.ts | 10 +++++ 24 files changed, 319 insertions(+), 17 deletions(-) create mode 100644 packages/calling-stateful-client/src/MediaAccessSubscriber.ts diff --git a/packages/calling-component-bindings/src/baseSelectors.ts b/packages/calling-component-bindings/src/baseSelectors.ts index acaecace824..ad3f105132c 100644 --- a/packages/calling-component-bindings/src/baseSelectors.ts +++ b/packages/calling-component-bindings/src/baseSelectors.ts @@ -28,6 +28,7 @@ import { _SupportedCaptionLanguage, _SupportedSpokenLanguage } from '@internal/r import { ConferencePhoneInfo } from '@internal/calling-stateful-client'; /* @conditional-compile-remove(breakout-rooms) */ import { CallNotifications } from '@internal/calling-stateful-client'; +import { MediaAccessCallFeatureState } from '@internal/calling-stateful-client/dist/dist-esm/CallClientState'; /** * Common props used to reference calling declarative client state. @@ -113,6 +114,16 @@ export const getSpotlightCallFeature = ( return state.calls[props.callId]?.spotlight; }; +/** + * @private + */ +export const getMediaAccessCallFeature = ( + state: CallClientState, + props: CallingBaseSelectorProps +): MediaAccessCallFeatureState | undefined => { + return state.calls[props.callId]?.mediaAccess; +}; + /** * @private */ diff --git a/packages/calling-component-bindings/src/handlers/createCommonHandlers.ts b/packages/calling-component-bindings/src/handlers/createCommonHandlers.ts index 02427e7ec25..7087b2182e8 100644 --- a/packages/calling-component-bindings/src/handlers/createCommonHandlers.ts +++ b/packages/calling-component-bindings/src/handlers/createCommonHandlers.ts @@ -106,6 +106,10 @@ export interface CommonCallingHandlers { onMuteParticipant: (userId: string) => Promise; /* @conditional-compile-remove(soft-mute) */ onMuteAllRemoteParticipants: () => Promise; + + onForbidParticipantAudio?: (userIds: string[]) => Promise; + onPermitParticipantAudio?: (userIds: string[]) => Promise; + // onForbidAllRemoteParticipantsAudio: () => Promise; } /** @@ -714,6 +718,28 @@ export const createDefaultCommonCallingHandlers = memoizeOne( await call?.feature(Features.Spotlight).stopSpotlight(participants); } : undefined; + // const canForbidOthersMedia = call?.feature(Features.Capabilities).capabilities.forbidOthersMedia.isPresent; + // const onForbidParticipantAudio = canForbidOthersMedia + // ? async (userIds: string[]): Promise => { + // const participants = userIds?.map((userId) => _toCommunicationIdentifier(userId)); + // await call?.feature(Features.MediaAccess).forbidAudio(participants); + // } + // : undefined; + // const onPermitParticipantAudio = canForbidOthersMedia + // ? async (userIds: string[]): Promise => { + // const participants = userIds?.map((userId) => _toCommunicationIdentifier(userId)); + // await call?.feature(Features.MediaAccess).permitAudio(participants); + // } + // : undefined; + + const onForbidParticipantAudio = async (userIds: string[]): Promise => { + const participants = userIds?.map((userId) => _toCommunicationIdentifier(userId)); + await call?.feature(Features.MediaAccess).forbidAudio(participants); + }; + const onPermitParticipantAudio = async (userIds: string[]): Promise => { + const participants = userIds?.map((userId) => _toCommunicationIdentifier(userId)); + await call?.feature(Features.MediaAccess).permitAudio(participants); + }; return { onHangUp, @@ -768,7 +794,9 @@ export const createDefaultCommonCallingHandlers = memoizeOne( /* @conditional-compile-remove(soft-mute) */ onMuteAllRemoteParticipants, onAcceptCall: notImplemented, - onRejectCall: notImplemented + onRejectCall: notImplemented, + onForbidParticipantAudio, + onPermitParticipantAudio }; } ); diff --git a/packages/calling-component-bindings/src/handlers/createHandlers.ts b/packages/calling-component-bindings/src/handlers/createHandlers.ts index 7f8424caa07..fc0b7260c0b 100644 --- a/packages/calling-component-bindings/src/handlers/createHandlers.ts +++ b/packages/calling-component-bindings/src/handlers/createHandlers.ts @@ -144,4 +144,7 @@ export interface _ComponentCallingHandlers { onStartRemoteSpotlight: (userIds: string[]) => Promise; /** VideoGallery callback prop to stop remote spotlight */ onStopRemoteSpotlight: (userIds: string[]) => Promise; + + // onForbidParticipantAudio: (userIds: string[]) => Promise; + // onPermitParticipantAudio: (userIds: string[]) => Promise; } diff --git a/packages/calling-component-bindings/src/participantListSelector.ts b/packages/calling-component-bindings/src/participantListSelector.ts index 938ff72b1b4..87f9e206a1f 100644 --- a/packages/calling-component-bindings/src/participantListSelector.ts +++ b/packages/calling-component-bindings/src/participantListSelector.ts @@ -8,7 +8,8 @@ import { getDisplayName, getIsScreenSharingOn, getIsMuted, - CallingBaseSelectorProps + CallingBaseSelectorProps, + getCapabilities } from './baseSelectors'; import { getRole } from './baseSelectors'; import { isHideAttendeeNamesEnabled } from './baseSelectors'; @@ -73,6 +74,11 @@ const convertRemoteParticipantsToParticipantListParticipants = ( spotlightedParticipants, toFlatCommunicationIdentifier(participant.identifier) ); + const mediaAccess = { + isAudioPermitted: participant.mediaAccess?.isAudioPermitted, + isVideoPermitted: participant.mediaAccess?.isVideoPermitted + }; + console.log('hi there convertRemoteParticipantsToParticipantListParticipants', participant.mediaAccess); return memoizeFn( toFlatCommunicationIdentifier(participant.identifier), displayName, @@ -83,7 +89,8 @@ const convertRemoteParticipantsToParticipantListParticipants = ( participant.raisedHand, localUserCanRemoveOthers, remoteParticipantReaction, - spotlight + spotlight, + mediaAccess ); }) .sort((a, b) => { @@ -134,7 +141,8 @@ export const participantListSelector: ParticipantListSelector = createSelector( getParticipantCount, isHideAttendeeNamesEnabled, getLocalParticipantReactionState, - getSpotlightCallFeature + getSpotlightCallFeature, + getCapabilities ], ( userId, @@ -147,13 +155,15 @@ export const participantListSelector: ParticipantListSelector = createSelector( partitipantCount, isHideAttendeeNamesEnabled, localParticipantReactionState, - spotlightCallFeature + spotlightCallFeature, + capabilities ): { participants: CallParticipantListParticipant[]; myUserId: string; totalParticipantCount?: number; } => { const localUserCanRemoveOthers = localUserCanRemoveOthersTrampoline(role); + console.log('hi there participantListSelector', remoteParticipants); const participants = remoteParticipants ? convertRemoteParticipantsToParticipantListParticipants( _updateUserDisplayNames(Object.values(remoteParticipants)), @@ -173,7 +183,11 @@ export const participantListSelector: ParticipantListSelector = createSelector( // Local participant can never remove themselves. isRemovable: false, reaction: memoizedConvertToVideoTileReaction(localParticipantReactionState), - spotlight: memoizedSpotlight(spotlightCallFeature?.spotlightedParticipants, userId) + spotlight: memoizedSpotlight(spotlightCallFeature?.spotlightedParticipants, userId), + mediaAccess: { + isAudioPermitted: !!capabilities?.unmuteMic.isPresent, + isVideoPermitted: !!capabilities?.turnVideoOn.isPresent + } }); /* @conditional-compile-remove(total-participant-count) */ const totalParticipantCount = partitipantCount; diff --git a/packages/calling-component-bindings/src/utils/participantListSelectorUtils.ts b/packages/calling-component-bindings/src/utils/participantListSelectorUtils.ts index 92fa6455952..ac6642e497d 100644 --- a/packages/calling-component-bindings/src/utils/participantListSelectorUtils.ts +++ b/packages/calling-component-bindings/src/utils/participantListSelectorUtils.ts @@ -11,6 +11,8 @@ import { Spotlight } from '@internal/react-components'; import { RaisedHandState } from '@internal/calling-stateful-client'; import { ReactionState } from '@internal/calling-stateful-client'; import { Reaction } from '@internal/react-components'; +import { MediaAccess } from '@internal/react-components'; + import memoizeOne from 'memoize-one'; const convertRemoteParticipantToParticipantListParticipant = ( @@ -23,7 +25,8 @@ const convertRemoteParticipantToParticipantListParticipant = ( raisedHand: RaisedHandState | undefined, localUserCanRemoveOthers: boolean, reaction: undefined | Reaction, - spotlight: undefined | Spotlight + spotlight: undefined | Spotlight, + mediaAccess: MediaAccess | undefined ): CallParticipantListParticipant => { const identifier = fromFlatCommunicationIdentifier(userId); return { @@ -41,7 +44,8 @@ const convertRemoteParticipantToParticipantListParticipant = ( getIdentifierKind(identifier).kind === 'phoneNumber') && localUserCanRemoveOthers, reaction, - spotlight + spotlight, + mediaAccess }; }; @@ -59,7 +63,8 @@ export const memoizedConvertAllremoteParticipants = memoizeFnAll( raisedHand: RaisedHandState | undefined, localUserCanRemoveOthers: boolean, reaction: undefined | Reaction, - spotlight: undefined | Spotlight + spotlight: undefined | Spotlight, + mediaAccess: MediaAccess | undefined ): CallParticipantListParticipant => { return convertRemoteParticipantToParticipantListParticipant( userId, @@ -71,7 +76,8 @@ export const memoizedConvertAllremoteParticipants = memoizeFnAll( raisedHand, localUserCanRemoveOthers, reaction, - spotlight + spotlight, + mediaAccess ); } ); diff --git a/packages/calling-stateful-client/src/CallClientState.ts b/packages/calling-stateful-client/src/CallClientState.ts index 202a82aa0eb..2db833408d2 100644 --- a/packages/calling-stateful-client/src/CallClientState.ts +++ b/packages/calling-stateful-client/src/CallClientState.ts @@ -27,7 +27,8 @@ import type { NetworkDiagnosticType, DiagnosticValueType, DiagnosticQuality, - DiagnosticFlag + DiagnosticFlag, + MediaAccess } from '@azure/communication-calling'; import { TeamsCallInfo } from '@azure/communication-calling'; import { CallInfo } from '@azure/communication-calling'; @@ -197,6 +198,25 @@ export interface SpotlightState { spotlightedOrderPosition?: number; } +/** + * State only version of {@link @azure/communication-calling#MediaAccessCallFeature} + * + * @alpha + */ +export interface MediaAccessCallFeatureState { + mediaAccesses: MediaAccess[]; +} + +/** + * Media access state + * + * @alpha + */ +export interface MediaAccessState { + isAudioPermitted?: boolean; + isVideoPermitted?: boolean; +} + /* @conditional-compile-remove(breakout-rooms) */ /** * Breakout rooms state @@ -527,6 +547,11 @@ export interface RemoteParticipantState { * The diagnostic status of RemoteParticipant{@link @azure/communication-calling#RemoteDiagnostics}. */ diagnostics?: Record; + + /** + * Proxy of {@link @azure/communication-calling#Call.MediaAccess.mediaAccesses}. + */ + mediaAccess?: MediaAccessState; } /** @@ -712,6 +737,11 @@ export interface CallState { * Proxy of {@link @azure/communication-calling#BreakoutRoomsFeature}. */ breakoutRooms?: BreakoutRoomsState; + + /** + * Proxy of {@link @azure/communication-calling#MediaAccessCallFeature}. + */ + mediaAccess?: MediaAccessCallFeatureState; } /** diff --git a/packages/calling-stateful-client/src/CallContext.ts b/packages/calling-stateful-client/src/CallContext.ts index 025a61e9572..7c4f7277367 100644 --- a/packages/calling-stateful-client/src/CallContext.ts +++ b/packages/calling-stateful-client/src/CallContext.ts @@ -8,7 +8,8 @@ import { DominantSpeakersInfo, ParticipantRole, ScalingMode, - VideoDeviceInfo + VideoDeviceInfo, + MediaAccess } from '@azure/communication-calling'; import { RaisedHand } from '@azure/communication-calling'; /* @conditional-compile-remove(breakout-rooms) */ @@ -190,6 +191,7 @@ export class CallContext { existingCall.info = call.info; existingCall.meetingConference = call.meetingConference; + existingCall.mediaAccess = call.mediaAccess; } else { draft.calls[latestCallId] = call; } @@ -1162,6 +1164,27 @@ export class CallContext { }); } + public setMediaAccesses(callId: string, mediaAccesses: MediaAccess[]): void { + this.modifyState((draft: CallClientState) => { + const call = draft.calls[this._callIdHistory.latestCallId(callId)]; + if (!call) { + return; + } + call.mediaAccess = { + mediaAccesses + }; + mediaAccesses.forEach((participantMediaAccess) => { + const participant = call.remoteParticipants[toFlatCommunicationIdentifier(participantMediaAccess.participant)]; + if (participant) { + participant.mediaAccess = { + isAudioPermitted: participantMediaAccess.isAudioPermitted, + isVideoPermitted: participantMediaAccess.isVideoPermitted + }; + } + }); + }); + } + setIsCaptionActive(callId: string, isCaptionsActive: boolean): void { this.modifyState((draft: CallClientState) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; diff --git a/packages/calling-stateful-client/src/CallSubscriber.ts b/packages/calling-stateful-client/src/CallSubscriber.ts index e67119613ba..a905cda8157 100644 --- a/packages/calling-stateful-client/src/CallSubscriber.ts +++ b/packages/calling-stateful-client/src/CallSubscriber.ts @@ -32,6 +32,7 @@ import { LocalRecordingSubscriber } from './LocalRecordingSubscriber'; import { BreakoutRoomsSubscriber } from './BreakoutRoomsSubscriber'; /* @conditional-compile-remove(together-mode) */ import { TogetherModeSubscriber } from './TogetherModeSubscriber'; +import { MediaAccessSubscriber } from './MediaAccessSubscriber'; /** * Keeps track of the listeners assigned to a particular call because when we get an event from SDK, it doesn't tell us @@ -64,6 +65,7 @@ export class CallSubscriber { private _breakoutRoomsSubscriber: BreakoutRoomsSubscriber; /* @conditional-compile-remove(together-mode) */ private _togetherModeSubscriber: TogetherModeSubscriber; + private _mediaAccessSubscriber: MediaAccessSubscriber; constructor(call: CallCommon, context: CallContext, internalContext: InternalCallContext) { this._call = call; @@ -103,7 +105,11 @@ export class CallSubscriber { context: this._context, localOptimalVideoCountFeature: this._call.feature(Features.OptimalVideoCount) }); - + this._mediaAccessSubscriber = new MediaAccessSubscriber( + this._callIdRef, + this._context, + this._call.feature(Features.MediaAccess) + ); this._localVideoStreamVideoEffectsSubscribers = new Map(); this._capabilitiesSubscriber = new CapabilitiesSubscriber( @@ -240,6 +246,7 @@ export class CallSubscriber { this._breakoutRoomsSubscriber.unsubscribe(); /* @conditional-compile-remove(together-mode) */ this._togetherModeSubscriber.unsubscribe(); + this._mediaAccessSubscriber.unsubscribe(); }; // This is a helper function to safely call subscriber functions. This is needed in order to prevent events diff --git a/packages/calling-stateful-client/src/Converter.ts b/packages/calling-stateful-client/src/Converter.ts index 68090b65307..9ff994973c5 100644 --- a/packages/calling-stateful-client/src/Converter.ts +++ b/packages/calling-stateful-client/src/Converter.ts @@ -99,7 +99,8 @@ export function convertSdkParticipantToDeclarativeParticipant( isSpeaking: participant.isSpeaking, raisedHand: undefined, role: participant.role, - spotlight: undefined + spotlight: undefined, + mediaAccess: undefined }; } diff --git a/packages/calling-stateful-client/src/MediaAccessSubscriber.ts b/packages/calling-stateful-client/src/MediaAccessSubscriber.ts new file mode 100644 index 00000000000..aa525423bbd --- /dev/null +++ b/packages/calling-stateful-client/src/MediaAccessSubscriber.ts @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { MediaAccessCallFeature, MediaAccessChangedEvent } from '@azure/communication-calling'; +import { CallContext } from './CallContext'; +import { CallIdRef } from './CallIdRef'; + +/** + * @private + */ +export class MediaAccessSubscriber { + private _callIdRef: CallIdRef; + private _context: CallContext; + private _mediaAccessCallFeature: MediaAccessCallFeature; + + constructor(callIdRef: CallIdRef, context: CallContext, mediaAccessCallFeature: MediaAccessCallFeature) { + this._callIdRef = callIdRef; + this._context = context; + this._mediaAccessCallFeature = mediaAccessCallFeature; + + const mediaAccesses = this._mediaAccessCallFeature.getOthersMediaAccess(); + this._context.setMediaAccesses(this._callIdRef.callId, mediaAccesses); + this.subscribe(); + } + + private subscribe = (): void => { + this._mediaAccessCallFeature.on('mediaAccessChanged', this.mediaAccessChanged); + }; + + public unsubscribe = (): void => { + this._mediaAccessCallFeature.off('mediaAccessChanged', this.mediaAccessChanged); + }; + + private mediaAccessChanged = (data: MediaAccessChangedEvent): void => { + console.log('hi there audioVideoAccess changed', data); + this._context.setMediaAccesses(this._callIdRef.callId, data.mediaAccesses); + }; +} diff --git a/packages/calling-stateful-client/src/index-public.ts b/packages/calling-stateful-client/src/index-public.ts index 1c0f7dacae9..a24e73cd17d 100644 --- a/packages/calling-stateful-client/src/index-public.ts +++ b/packages/calling-stateful-client/src/index-public.ts @@ -55,3 +55,4 @@ export type { LocalRecordingCallFeatureState } from './CallClientState'; export type { ConferencePhoneInfo } from './CallClientState'; /* @conditional-compile-remove(breakout-rooms) */ export type { BreakoutRoomsState } from './CallClientState'; +export type { MediaAccessState, MediaAccessCallFeatureState as MediaAccessCallFeature } from './CallClientState'; diff --git a/packages/communication-react/review/beta/communication-react.api.md b/packages/communication-react/review/beta/communication-react.api.md index c32ab12760d..7afbc7f1a7b 100644 --- a/packages/communication-react/review/beta/communication-react.api.md +++ b/packages/communication-react/review/beta/communication-react.api.md @@ -75,6 +75,7 @@ import { LatestMediaDiagnostics } from '@azure/communication-calling'; import { LatestNetworkDiagnostics } from '@azure/communication-calling'; import { LocalRecordingInfo } from '@azure/communication-calling'; import { LocalVideoStream } from '@azure/communication-calling'; +import type { MediaAccess as MediaAccess_2 } from '@azure/communication-calling'; import type { MediaDiagnosticChangedEventArgs } from '@azure/communication-calling'; import type { MediaDiagnosticType } from '@azure/communication-calling'; import { MediaStreamType } from '@azure/communication-calling'; @@ -447,6 +448,8 @@ export interface CallAdapterCallOperations { disposeScreenShareStreamView(remoteUserId: string): Promise; // @deprecated disposeStreamView(remoteUserId?: string, options?: VideoStreamOptions): Promise; + // (undocumented) + forbidParticipantAudio(userIds: string[]): Promise; holdCall(): Promise; leaveCall(forEveryone?: boolean): Promise; lowerHand(): Promise; @@ -454,6 +457,8 @@ export interface CallAdapterCallOperations { muteAllRemoteParticipants(): Promise; muteParticipant(userId: string): Promise; onReactionClick(reaction: Reaction_2): Promise; + // (undocumented) + permitParticipantAudio(userIds: string[]): Promise; raiseHand(): Promise; removeParticipant(userId: string): Promise; removeParticipant(participant: CommunicationIdentifier): Promise; @@ -1101,6 +1106,7 @@ export type CallParticipantListParticipant = ParticipantListParticipant & { raisedHand?: RaisedHand; reaction?: Reaction; spotlight?: Spotlight; + mediaAccess?: MediaAccess; }; // @beta @@ -1140,6 +1146,7 @@ export interface CallState { localParticipantReaction?: ReactionState; localRecording: LocalRecordingCallFeatureState; localVideoStreams: LocalVideoStreamState[]; + mediaAccess?: MediaAccessCallFeature; meetingConference?: { conferencePhones: ConferencePhoneInfo[]; }; @@ -1194,6 +1201,8 @@ export interface CallWithChatAdapterManagement { // (undocumented) downloadResourceToCache(resourceDetails: ResourceDetails): Promise; fetchInitialData(): Promise; + // (undocumented) + forbidParticipantAudio: (userIds: string[]) => Promise; holdCall(): Promise; // @deprecated joinCall(microphoneOn?: boolean): Call | undefined; @@ -1205,6 +1214,8 @@ export interface CallWithChatAdapterManagement { muteAllRemoteParticipants(): Promise; muteParticipant(userId: string): Promise; onReactionClick(reaction: Reaction_2): Promise; + // (undocumented) + permitParticipantAudio: (userIds: string[]) => Promise; queryCameras(): Promise; queryMicrophones(): Promise; querySpeakers(): Promise; @@ -2153,6 +2164,8 @@ export interface CommonCallingHandlers { // (undocumented) onDisposeRemoteVideoStreamView: (userId: string) => Promise; // (undocumented) + onForbidParticipantAudio?: (userIds: string[]) => Promise; + // (undocumented) onHangUp: (forEveryone?: boolean) => Promise; // (undocumented) onLowerHand: () => Promise; @@ -2161,6 +2174,8 @@ export interface CommonCallingHandlers { // (undocumented) onMuteParticipant: (userId: string) => Promise; // (undocumented) + onPermitParticipantAudio?: (userIds: string[]) => Promise; + // (undocumented) onRaiseHand: () => Promise; // (undocumented) onReactionClick: (reaction: Reaction_2) => Promise; @@ -3492,6 +3507,26 @@ export type LocalVideoTileSize = '9:16' | '16:9' | 'hidden' | 'followDeviceOrien // @public export type LongPressTrigger = 'mouseAndTouch' | 'touch'; +// @public +export type MediaAccess = { + isAudioPermitted: boolean; + isVideoPermitted: boolean; +}; + +// @alpha +export interface MediaAccessCallFeature { + // (undocumented) + mediaAccesses: MediaAccess_2[]; +} + +// @alpha +export interface MediaAccessState { + // (undocumented) + isAudioPermitted?: boolean; + // (undocumented) + isVideoPermitted?: boolean; +} + // @public export type MediaDiagnosticChangedEvent = MediaDiagnosticChangedEventArgs & { type: 'media'; @@ -4262,6 +4297,7 @@ export interface RemoteParticipantState { identifier: CommunicationIdentifierKind; isMuted: boolean; isSpeaking: boolean; + mediaAccess?: MediaAccessState; raisedHand?: RaisedHandState; reactionState?: ReactionState; role?: ParticipantRole; diff --git a/packages/communication-react/src/index.ts b/packages/communication-react/src/index.ts index ef315af2902..3b9f36cdd58 100644 --- a/packages/communication-react/src/index.ts +++ b/packages/communication-react/src/index.ts @@ -438,3 +438,4 @@ export type { ActiveNotification } from '../../react-components/src'; export type { MeetingConferencePhoneInfoModalStrings } from '../../react-components/src'; +export type { MediaAccess } from '../../react-components/src'; diff --git a/packages/react-components/src/components/ParticipantList.tsx b/packages/react-components/src/components/ParticipantList.tsx index 064d879a33d..718a1831745 100644 --- a/packages/react-components/src/components/ParticipantList.tsx +++ b/packages/react-components/src/components/ParticipantList.tsx @@ -182,7 +182,10 @@ const onRenderParticipantDefault = ( ariaLabel={strings.sharingIconLabel} /> )} - {callingParticipant.isMuted && ( + {!callingParticipant.mediaAccess?.isAudioPermitted && ( + + )} + {callingParticipant.mediaAccess?.isAudioPermitted && callingParticipant.isMuted && ( )} {callingParticipant.spotlight && } diff --git a/packages/react-components/src/index.ts b/packages/react-components/src/index.ts index c29a9d332a6..d9604faf5a8 100644 --- a/packages/react-components/src/index.ts +++ b/packages/react-components/src/index.ts @@ -68,6 +68,8 @@ export type { Spotlight } from './types'; export type { Reaction, ReactionResources, ReactionSprite } from './types'; +export type { MediaAccess } from './types'; + export type { SpokenLanguageStrings, CaptionLanguageStrings, diff --git a/packages/react-components/src/types/ParticipantListParticipant.ts b/packages/react-components/src/types/ParticipantListParticipant.ts index 07f2f0cbfb2..fc69c2b08ad 100644 --- a/packages/react-components/src/types/ParticipantListParticipant.ts +++ b/packages/react-components/src/types/ParticipantListParticipant.ts @@ -27,6 +27,8 @@ export type CallParticipantListParticipant = ParticipantListParticipant & { reaction?: Reaction; /** Whether calling participant is spotlighted **/ spotlight?: Spotlight; + /** Whether calling participant has media access **/ + mediaAccess?: MediaAccess; }; /** @@ -50,6 +52,16 @@ export type RaisedHand = { raisedHandOrderPosition: number; }; +/** + * Media access state with order + * + * @public + */ +export type MediaAccess = { + isAudioPermitted: boolean; + isVideoPermitted: boolean; +}; + /** * Reaction state with reaction type to render * diff --git a/packages/react-composites/src/composites/CallComposite/MockCallAdapter.ts b/packages/react-composites/src/composites/CallComposite/MockCallAdapter.ts index f73e34b022a..01e9f6a0f23 100644 --- a/packages/react-composites/src/composites/CallComposite/MockCallAdapter.ts +++ b/packages/react-composites/src/composites/CallComposite/MockCallAdapter.ts @@ -228,6 +228,12 @@ export class _MockCallAdapter implements CallAdapter { returnFromBreakoutRoom(): Promise { throw Error('returnFromBreakoutRoom not implemented'); } + forbidParticipantAudio(userIds: string[]): Promise { + throw Error('forbidParticipantAudio not implemented'); + } + permitParticipantAudio(userIds: string[]): Promise { + throw Error('permitParticipantAudio not implemented'); + } } /** diff --git a/packages/react-composites/src/composites/CallComposite/adapter/AzureCommunicationCallAdapter.ts b/packages/react-composites/src/composites/CallComposite/adapter/AzureCommunicationCallAdapter.ts index c2c55bfe7e6..00cca77d91e 100644 --- a/packages/react-composites/src/composites/CallComposite/adapter/AzureCommunicationCallAdapter.ts +++ b/packages/react-composites/src/composites/CallComposite/adapter/AzureCommunicationCallAdapter.ts @@ -230,7 +230,7 @@ class CallContext { // This context privately tracks how captions was started to determine if captions is running only in the background. // If so we should not show the UI. this.state = captionsUIVisibilityModifier(this.state); - + console.log('hi there state', this.state.call?.remoteParticipants); this.emitter.emit('stateChanged', this.state); } @@ -1142,6 +1142,14 @@ export class AzureCommunicationCallAdapter { + this.handlers.onForbidParticipantAudio?.(userIds); + } + + public async permitParticipantAudio(userIds: string[]): Promise { + this.handlers.onPermitParticipantAudio?.(userIds); + } + /* @conditional-compile-remove(breakout-rooms) */ public async returnFromBreakoutRoom(): Promise { if (!this.originCall) { diff --git a/packages/react-composites/src/composites/CallComposite/adapter/CallAdapter.ts b/packages/react-composites/src/composites/CallComposite/adapter/CallAdapter.ts index a4e57b73ff1..63c43064479 100644 --- a/packages/react-composites/src/composites/CallComposite/adapter/CallAdapter.ts +++ b/packages/react-composites/src/composites/CallComposite/adapter/CallAdapter.ts @@ -783,6 +783,9 @@ export interface CallAdapterCallOperations { * Return to origin call of breakout room */ returnFromBreakoutRoom(): Promise; + + forbidParticipantAudio(userIds: string[]): Promise; + permitParticipantAudio(userIds: string[]): Promise; } /** diff --git a/packages/react-composites/src/composites/CallComposite/components/SidePane/usePeoplePane.tsx b/packages/react-composites/src/composites/CallComposite/components/SidePane/usePeoplePane.tsx index 6a91be7ec6a..ac8ebd177d3 100644 --- a/packages/react-composites/src/composites/CallComposite/components/SidePane/usePeoplePane.tsx +++ b/packages/react-composites/src/composites/CallComposite/components/SidePane/usePeoplePane.tsx @@ -285,6 +285,38 @@ export const usePeoplePane = (props: { ariaLabel: localeStrings.pinParticipantMenuItemAriaLabel }); } + + // if (!remoteParticipants?.[participantId]?.mediaAccess && onUnBlockParticipantMicrophone) { + // _defaultMenuItems.push({ + // key: 'unblock-microphone', + // text: 'Unblock microphone', + // iconProps: { + // iconName: 'UnblockMicrophone', + // styles: { root: { lineHeight: '1rem', textAlign: 'center' } } + // }, + // onClick: () => { + // onUnBlockParticipantMicrophone(participantId); + // }, + // 'data-ui-id': 'participant-item-unblock-microphone-button', + // ariaLabel: 'Unblock microphone' + // }); + // } + + // if (remoteParticipants?.[participantId]?.isAudioPermitted && onBlockParticipantMicrophone) { + // _defaultMenuItems.push({ + // key: 'block-microphone', + // text: 'Block microphone', + // iconProps: { + // iconName: 'BlockMicrophone', + // styles: { root: { lineHeight: '1rem', textAlign: 'center' } } + // }, + // onClick: () => { + // onBlockParticipantMicrophone(participantId); + // }, + // 'data-ui-id': 'participant-item-block-microphone-button', + // ariaLabel: 'Block microphone' + // }); + // } } if (defaultMenuItems) { _defaultMenuItems.push(...defaultMenuItems); diff --git a/packages/react-composites/src/composites/CallComposite/hooks/useHandlers.ts b/packages/react-composites/src/composites/CallComposite/hooks/useHandlers.ts index 878a6af325a..548809e1217 100644 --- a/packages/react-composites/src/composites/CallComposite/hooks/useHandlers.ts +++ b/packages/react-composites/src/composites/CallComposite/hooks/useHandlers.ts @@ -231,7 +231,23 @@ const createCompositeHandlers = memoizeOne( /* @conditional-compile-remove(soft-mute) */ onMuteAllRemoteParticipants: async (): Promise => { await adapter.muteAllRemoteParticipants(); - } + }, + // onForbidParticipantAudio: async (userIds: string[]): Promise => { + // await adapter.forbidParticipantAudio(userIds); + // }, + // onPermitParticipantAudio: async (userIds: string[]): Promise => { + // await adapter.permitParticipantAudio(userIds); + // } + onForbidParticipantAudio: capabilities?.forbidOthersMedia.isPresent + ? async (userIds: string[]): Promise => { + await adapter.forbidParticipantAudio(userIds); + } + : undefined, + onPermitParticipantAudio: capabilities?.forbidOthersMedia.isPresent + ? async (userIds: string[]): Promise => { + await adapter.permitParticipantAudio(userIds); + } + : undefined }; } ); diff --git a/packages/react-composites/src/composites/CallWithChatComposite/adapter/AzureCommunicationCallWithChatAdapter.ts b/packages/react-composites/src/composites/CallWithChatComposite/adapter/AzureCommunicationCallWithChatAdapter.ts index 72f18a939c5..01c5a09f448 100644 --- a/packages/react-composites/src/composites/CallWithChatComposite/adapter/AzureCommunicationCallWithChatAdapter.ts +++ b/packages/react-composites/src/composites/CallWithChatComposite/adapter/AzureCommunicationCallWithChatAdapter.ts @@ -712,6 +712,14 @@ export class AzureCommunicationCallWithChatAdapter implements CallWithChatAdapte } } + public async forbidParticipantAudio(userIds: string[]): Promise { + return this.callAdapter.forbidParticipantAudio(userIds); + } + + public async permitParticipantAudio(userIds: string[]): Promise { + return this.callAdapter.permitParticipantAudio(userIds); + } + on(event: 'callParticipantsJoined', listener: ParticipantsJoinedListener): void; on(event: 'callParticipantsLeft', listener: ParticipantsLeftListener): void; on(event: 'callEnded', listener: CallEndedListener): void; diff --git a/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatAdapter.ts b/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatAdapter.ts index aef2f15d376..fd02efeaecb 100644 --- a/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatAdapter.ts +++ b/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatAdapter.ts @@ -516,6 +516,9 @@ export interface CallWithChatAdapterManagement { * Return to origin call of breakout room */ returnFromBreakoutRoom(): Promise; + + forbidParticipantAudio: (userIds: string[]) => Promise; + permitParticipantAudio: (userIds: string[]) => Promise; } /** diff --git a/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatBackedCallAdapter.ts b/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatBackedCallAdapter.ts index 9ec26cd83e4..bdff2cf56c0 100644 --- a/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatBackedCallAdapter.ts +++ b/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatBackedCallAdapter.ts @@ -245,6 +245,14 @@ export class CallWithChatBackedCallAdapter implements CallAdapter { public async returnFromBreakoutRoom(): Promise { return this.callWithChatAdapter.returnFromBreakoutRoom(); } + + public async forbidParticipantAudio(userIds: string[]): Promise { + return this.callWithChatAdapter.forbidParticipantAudio(userIds); + } + + public async permitParticipantAudio(userIds: string[]): Promise { + return this.callWithChatAdapter.permitParticipantAudio(userIds); + } } function callAdapterStateFromCallWithChatAdapterState( @@ -278,5 +286,7 @@ function callAdapterStateFromCallWithChatAdapterState( hideDeepNoiseSuppressionButton: callWithChatAdapterState.hideDeepNoiseSuppressionButton, selectedVideoBackgroundEffect: callWithChatAdapterState.selectedVideoBackgroundEffect, reactions: callWithChatAdapterState.reactions + // forbidParticipantAudio: callWithChatAdapterState.forbidParticipantAudio, + // permitParticipantAudio: callWithChatAdapterState.permitParticipantAudio, }; } From ac9043cc4a6efc603e1c4239ec13ade7a876cbae Mon Sep 17 00:00:00 2001 From: fuyan Date: Sun, 13 Oct 2024 22:48:47 -0700 Subject: [PATCH 02/13] UI changes --- .../src/handlers/createCommonHandlers.ts | 28 +- .../src/utils/videoGalleryUtils.ts | 17 +- .../src/CallClientState.ts | 4 +- .../src/CapabilitiesSubscriber.ts | 1 + .../review/beta/communication-react.api.md | 53 +++- .../src/components/ParticipantList.tsx | 3 + .../src/components/RemoteVideoTile.tsx | 10 +- .../src/components/VideoGallery.tsx | 28 +- .../useVideoTileContextualMenuProps.ts | 39 ++- .../localization/locales/en-GB/strings.json | 8 +- .../localization/locales/en-US/strings.json | 8 +- .../src/types/VideoGalleryParticipant.ts | 4 +- .../CallComposite/MockCallAdapter.ts | 6 + .../src/composites/CallComposite/Strings.tsx | 12 + .../adapter/AzureCommunicationCallAdapter.ts | 12 + .../CallComposite/adapter/CallAdapter.ts | 2 + .../components/CallArrangement.tsx | 30 ++- .../components/SidePane/usePeoplePane.tsx | 240 +++++++++++++----- .../CallComposite/hooks/useHandlers.ts | 10 + .../AzureCommunicationCallWithChatAdapter.ts | 8 + .../adapter/CallWithChatAdapter.ts | 2 + .../adapter/CallWithChatBackedCallAdapter.ts | 8 + .../composites/common/Drawer/MoreDrawer.tsx | 3 + .../localization/locales/en-US/strings.json | 14 +- 24 files changed, 461 insertions(+), 89 deletions(-) diff --git a/packages/calling-component-bindings/src/handlers/createCommonHandlers.ts b/packages/calling-component-bindings/src/handlers/createCommonHandlers.ts index 7087b2182e8..f56c83dbb24 100644 --- a/packages/calling-component-bindings/src/handlers/createCommonHandlers.ts +++ b/packages/calling-component-bindings/src/handlers/createCommonHandlers.ts @@ -109,7 +109,8 @@ export interface CommonCallingHandlers { onForbidParticipantAudio?: (userIds: string[]) => Promise; onPermitParticipantAudio?: (userIds: string[]) => Promise; - // onForbidAllRemoteParticipantsAudio: () => Promise; + onForbidAllAttendeesAudio?: () => Promise; + onPermitAllAttendeesAudio?: () => Promise; } /** @@ -696,6 +697,7 @@ export const createDefaultCommonCallingHandlers = memoizeOne( const onMuteAllRemoteParticipants = async (): Promise => { call?.muteAllRemoteParticipants(); }; + const canStartSpotlight = call?.feature(Features.Capabilities).capabilities.spotlightParticipant.isPresent; const canRemoveSpotlight = call?.feature(Features.Capabilities).capabilities.removeParticipantsSpotlight.isPresent; const onStartLocalSpotlight = canStartSpotlight @@ -719,6 +721,7 @@ export const createDefaultCommonCallingHandlers = memoizeOne( } : undefined; // const canForbidOthersMedia = call?.feature(Features.Capabilities).capabilities.forbidOthersMedia.isPresent; + // console.log('hi there canForbidOthersMedia', canForbidOthersMedia); // const onForbidParticipantAudio = canForbidOthersMedia // ? async (userIds: string[]): Promise => { // const participants = userIds?.map((userId) => _toCommunicationIdentifier(userId)); @@ -740,6 +743,25 @@ export const createDefaultCommonCallingHandlers = memoizeOne( const participants = userIds?.map((userId) => _toCommunicationIdentifier(userId)); await call?.feature(Features.MediaAccess).permitAudio(participants); }; + const onForbidAllAttendeesAudio = async (): Promise => { + await call?.feature(Features.MediaAccess).forbidOthersAudio(); + }; + + const onPermitAllAttendeesAudio = async (): Promise => { + await call?.feature(Features.MediaAccess).permitOthersAudio(); + }; + + // const onForbidAllAttendeesAudio = canForbidOthersMedia + // ? async (): Promise => { + // await call?.feature(Features.MediaAccess).forbidOthersAudio(); + // } + // : undefined; + + // const onPermitAllAttendeesAudio = canForbidOthersMedia + // ? async (): Promise => { + // await call?.feature(Features.MediaAccess).permitOthersAudio(); + // } + // : undefined; return { onHangUp, @@ -796,7 +818,9 @@ export const createDefaultCommonCallingHandlers = memoizeOne( onAcceptCall: notImplemented, onRejectCall: notImplemented, onForbidParticipantAudio, - onPermitParticipantAudio + onPermitParticipantAudio, + onForbidAllAttendeesAudio, + onPermitAllAttendeesAudio }; } ); diff --git a/packages/calling-component-bindings/src/utils/videoGalleryUtils.ts b/packages/calling-component-bindings/src/utils/videoGalleryUtils.ts index e015efbe913..06bd02cdf2d 100644 --- a/packages/calling-component-bindings/src/utils/videoGalleryUtils.ts +++ b/packages/calling-component-bindings/src/utils/videoGalleryUtils.ts @@ -6,7 +6,7 @@ import { SpotlightedParticipant } from '@azure/communication-calling'; import { ParticipantRole } from '@azure/communication-calling'; import { memoizeFnAll, toFlatCommunicationIdentifier } from '@internal/acs-ui-common'; import { RemoteParticipantState, RemoteVideoStreamState } from '@internal/calling-stateful-client'; -import { VideoGalleryRemoteParticipant, VideoGalleryStream } from '@internal/react-components'; +import { MediaAccess, VideoGalleryRemoteParticipant, VideoGalleryStream } from '@internal/react-components'; import memoizeOne from 'memoize-one'; import { _convertParticipantState, ParticipantConnectionState } from './callUtils'; import { maskDisplayNameWithRole } from './callUtils'; @@ -72,7 +72,8 @@ export const _videoGalleryRemoteParticipantsMemo: _VideoGalleryRemoteParticipant participant.raisedHand, participant.contentSharingStream, remoteParticipantReaction, - spotlight + spotlight, + participant.mediaAccess ); }) ); @@ -90,7 +91,8 @@ const memoizedAllConvertRemoteParticipant = memoizeFnAll( raisedHand?: RaisedHandState, contentSharingStream?: HTMLElement, reaction?: Reaction, - spotlight?: Spotlight + spotlight?: Spotlight, + mediaAccess?: MediaAccess ): VideoGalleryRemoteParticipant => { return convertRemoteParticipantToVideoGalleryRemoteParticipant( userId, @@ -102,7 +104,8 @@ const memoizedAllConvertRemoteParticipant = memoizeFnAll( raisedHand, contentSharingStream, reaction, - spotlight + spotlight, + mediaAccess ); } ); @@ -118,7 +121,8 @@ export const convertRemoteParticipantToVideoGalleryRemoteParticipant = ( raisedHand?: RaisedHandState, contentSharingStream?: HTMLElement, reaction?: Reaction, - spotlight?: Spotlight + spotlight?: Spotlight, + mediaAccess?: MediaAccess ): VideoGalleryRemoteParticipant => { const rawVideoStreamsArray = Object.values(videoStreams); let videoStream: VideoGalleryStream | undefined = undefined; @@ -161,7 +165,8 @@ export const convertRemoteParticipantToVideoGalleryRemoteParticipant = ( state, raisedHand, reaction, - spotlight + spotlight, + mediaAccess }; }; diff --git a/packages/calling-stateful-client/src/CallClientState.ts b/packages/calling-stateful-client/src/CallClientState.ts index 2db833408d2..2b2bb2422b2 100644 --- a/packages/calling-stateful-client/src/CallClientState.ts +++ b/packages/calling-stateful-client/src/CallClientState.ts @@ -213,8 +213,8 @@ export interface MediaAccessCallFeatureState { * @alpha */ export interface MediaAccessState { - isAudioPermitted?: boolean; - isVideoPermitted?: boolean; + isAudioPermitted: boolean; + isVideoPermitted: boolean; } /* @conditional-compile-remove(breakout-rooms) */ diff --git a/packages/calling-stateful-client/src/CapabilitiesSubscriber.ts b/packages/calling-stateful-client/src/CapabilitiesSubscriber.ts index 7cfff07de12..338615197ae 100644 --- a/packages/calling-stateful-client/src/CapabilitiesSubscriber.ts +++ b/packages/calling-stateful-client/src/CapabilitiesSubscriber.ts @@ -33,6 +33,7 @@ export class CapabilitiesSubscriber { private capabilitiesChanged = (data: CapabilitiesChangeInfo): void => { this._context.setCapabilities(this._callIdRef.callId, this._capabilitiesFeature.capabilities, data); + console.log('hi there capabilities changed', data); if (data.oldValue.viewAttendeeNames !== data.newValue.viewAttendeeNames) { this._context.setHideAttendeeNames(this._callIdRef.callId, data); } diff --git a/packages/communication-react/review/beta/communication-react.api.md b/packages/communication-react/review/beta/communication-react.api.md index 7afbc7f1a7b..b2403e3f1ca 100644 --- a/packages/communication-react/review/beta/communication-react.api.md +++ b/packages/communication-react/review/beta/communication-react.api.md @@ -449,6 +449,8 @@ export interface CallAdapterCallOperations { // @deprecated disposeStreamView(remoteUserId?: string, options?: VideoStreamOptions): Promise; // (undocumented) + forbidAllAttendeesAudio(): Promise; + // (undocumented) forbidParticipantAudio(userIds: string[]): Promise; holdCall(): Promise; leaveCall(forEveryone?: boolean): Promise; @@ -458,6 +460,8 @@ export interface CallAdapterCallOperations { muteParticipant(userId: string): Promise; onReactionClick(reaction: Reaction_2): Promise; // (undocumented) + permitAllAttendeesAudio(): Promise; + // (undocumented) permitParticipantAudio(userIds: string[]): Promise; raiseHand(): Promise; removeParticipant(userId: string): Promise; @@ -857,6 +861,18 @@ export interface CallCompositeStrings { failedToJoinCallDueToNoNetworkTitle: string; failedToJoinTeamsMeetingReasonAccessDeniedMoreDetails?: string; failedToJoinTeamsMeetingReasonAccessDeniedTitle: string; + // (undocumented) + forbidAllAttendeesAudioCancelButtonLabel: string; + // (undocumented) + forbidAllAttendeesAudioConfirmButtonLabel: string; + // (undocumented) + forbidAllAttendeesAudioDialogContent: string; + // (undocumented) + forbidAllAttendeesAudioDialogTitle: string; + // (undocumented) + forbidAllAttendeesAudioMenuLabel: string; + // (undocumented) + forbidParticipantAudioMenuLabel: string; hangUpCancelButtonLabel?: string; holdScreenLabel?: string; invalidMeetingIdentifier: string; @@ -923,6 +939,18 @@ export interface CallCompositeStrings { peoplePaneTitle: string; permissionToReachTargetParticipantNotAllowedMoreDetails?: string; permissionToReachTargetParticipantNotAllowedTitle?: string; + // (undocumented) + permitAllAttendeesAudioCancelButtonLabel: string; + // (undocumented) + permitAllAttendeesAudioConfirmButtonLabel: string; + // (undocumented) + permitAllAttendeesAudioDialogContent: string; + // (undocumented) + permitAllAttendeesAudioDialogTitle: string; + // (undocumented) + permitAllAttendeesAudioMenuLabel: string; + // (undocumented) + permitParticipantAudioMenuLabel: string; phoneCallMoreButtonLabel: string; pinParticipantLimitReachedMenuLabel: string; pinParticipantMenuItemAriaLabel: string; @@ -1202,6 +1230,8 @@ export interface CallWithChatAdapterManagement { downloadResourceToCache(resourceDetails: ResourceDetails): Promise; fetchInitialData(): Promise; // (undocumented) + forbidAllAttendeesAudio: () => Promise; + // (undocumented) forbidParticipantAudio: (userIds: string[]) => Promise; holdCall(): Promise; // @deprecated @@ -1215,6 +1245,8 @@ export interface CallWithChatAdapterManagement { muteParticipant(userId: string): Promise; onReactionClick(reaction: Reaction_2): Promise; // (undocumented) + permitAllAttendeesAudio: () => Promise; + // (undocumented) permitParticipantAudio: (userIds: string[]) => Promise; queryCameras(): Promise; queryMicrophones(): Promise; @@ -2164,6 +2196,8 @@ export interface CommonCallingHandlers { // (undocumented) onDisposeRemoteVideoStreamView: (userId: string) => Promise; // (undocumented) + onForbidAllAttendeesAudio?: () => Promise; + // (undocumented) onForbidParticipantAudio?: (userIds: string[]) => Promise; // (undocumented) onHangUp: (forEveryone?: boolean) => Promise; @@ -2174,6 +2208,8 @@ export interface CommonCallingHandlers { // (undocumented) onMuteParticipant: (userId: string) => Promise; // (undocumented) + onPermitAllAttendeesAudio?: () => Promise; + // (undocumented) onPermitParticipantAudio?: (userIds: string[]) => Promise; // (undocumented) onRaiseHand: () => Promise; @@ -3522,9 +3558,9 @@ export interface MediaAccessCallFeature { // @alpha export interface MediaAccessState { // (undocumented) - isAudioPermitted?: boolean; + isAudioPermitted: boolean; // (undocumented) - isVideoPermitted?: boolean; + isVideoPermitted: boolean; } // @public @@ -4066,6 +4102,8 @@ export type ParticipantListProps = { strings?: ParticipantListStrings; participantAriaLabelledBy?: string; pinnedParticipants?: string[]; + onForbidParticipantAudio?: (userIds: string[]) => Promise; + onPermitParticipantAudio?: (userIds: string[]) => Promise; }; // @public @@ -5105,6 +5143,7 @@ export type VideoGalleryParticipant = { videoStream?: VideoGalleryStream; isScreenSharingOn?: boolean; spotlight?: Spotlight; + mediaAccess?: MediaAccess; }; // @public @@ -5125,7 +5164,11 @@ export interface VideoGalleryProps { // @deprecated (undocumented) onDisposeRemoteStreamView?: (userId: string) => Promise; onDisposeRemoteVideoStreamView?: (userId: string) => Promise; + // (undocumented) + onForbidParticipantAudio?: (userIds: string[]) => Promise; onMuteParticipant?: (userId: string) => Promise; + // (undocumented) + onPermitParticipantAudio?: (userIds: string[]) => Promise; onPinParticipant?: (userId: string) => void; onRenderAvatar?: OnRenderAvatarCallback; onRenderLocalVideoTile?: (localParticipant: VideoGalleryLocalParticipant) => JSX.Element; @@ -5152,6 +5195,8 @@ export interface VideoGalleryProps { // @public export interface VideoGalleryRemoteParticipant extends VideoGalleryParticipant { isSpeaking?: boolean; + // (undocumented) + mediaAccess?: MediaAccess; raisedHand?: RaisedHand; reaction?: Reaction; screenShareStream?: VideoGalleryStream; @@ -5190,6 +5235,8 @@ export interface VideoGalleryStrings { displayNamePlaceholder: string; fillRemoteParticipantFrame: string; fitRemoteParticipantToFrame: string; + // (undocumented) + forbidParticipantAudio: string; localScreenShareLoadingMessage: string; localVideoCameraSwitcherLabel: string; localVideoLabel: string; @@ -5197,6 +5244,8 @@ export interface VideoGalleryStrings { localVideoMovementLabel: string; localVideoSelectedDescription: string; muteParticipantMenuItemLabel: string; + // (undocumented) + permitParticipantAudio: string; pinnedParticipantAnnouncementAriaLabel: string; pinParticipantForMe: string; pinParticipantMenuItemAriaLabel: string; diff --git a/packages/react-components/src/components/ParticipantList.tsx b/packages/react-components/src/components/ParticipantList.tsx index 718a1831745..a344e9da2a4 100644 --- a/packages/react-components/src/components/ParticipantList.tsx +++ b/packages/react-components/src/components/ParticipantList.tsx @@ -114,6 +114,9 @@ export type ParticipantListProps = { participantAriaLabelledBy?: string; /** List of pinned participants */ pinnedParticipants?: string[]; + + onForbidParticipantAudio?: (userIds: string[]) => Promise; + onPermitParticipantAudio?: (userIds: string[]) => Promise; }; const onRenderParticipantDefault = ( diff --git a/packages/react-components/src/components/RemoteVideoTile.tsx b/packages/react-components/src/components/RemoteVideoTile.tsx index 46bebd579e1..c631ca2b26d 100644 --- a/packages/react-components/src/components/RemoteVideoTile.tsx +++ b/packages/react-components/src/components/RemoteVideoTile.tsx @@ -71,6 +71,8 @@ export const _RemoteVideoTile = React.memo( toggleAnnouncerString?: (announcerString: string) => void; reactionResources?: ReactionResources; onLongTouch?: (() => void) | undefined; + onForbidParticipantAudio?: (userIds: string[]) => Promise; + onPermitParticipantAudio?: (userIds: string[]) => Promise; }) => { const { isAvailable, @@ -100,7 +102,9 @@ export const _RemoteVideoTile = React.memo( toggleAnnouncerString, strings, reactionResources, - streamId + streamId, + onForbidParticipantAudio, + onPermitParticipantAudio } = props; const remoteVideoStreamProps: RemoteVideoStreamLifecycleMaintainerProps = useMemo( @@ -147,7 +151,9 @@ export const _RemoteVideoTile = React.memo( onStartSpotlight, onStopSpotlight, maxParticipantsToSpotlight, - /* @conditional-compile-remove(soft-mute) */ onMuteParticipant + /* @conditional-compile-remove(soft-mute) */ onMuteParticipant, + onForbidParticipantAudio, + onPermitParticipantAudio }); const videoTileContextualMenuProps = useMemo(() => { diff --git a/packages/react-components/src/components/VideoGallery.tsx b/packages/react-components/src/components/VideoGallery.tsx index 47bba74cd51..b81377017e1 100644 --- a/packages/react-components/src/components/VideoGallery.tsx +++ b/packages/react-components/src/components/VideoGallery.tsx @@ -133,6 +133,8 @@ export interface VideoGalleryStrings { muteParticipantMenuItemLabel: string; /** Text shown when waiting for others to join the call */ waitingScreenText: string; + forbidParticipantAudio: string; + permitParticipantAudio: string; } /** @@ -315,6 +317,8 @@ export interface VideoGalleryProps { * This callback is to mute a remote participant */ onMuteParticipant?: (userId: string) => Promise; + onForbidParticipantAudio?: (userIds: string[]) => Promise; + onPermitParticipantAudio?: (userIds: string[]) => Promise; } /** @@ -400,7 +404,9 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { reactionResources, videoTilesOptions, /* @conditional-compile-remove(soft-mute) */ - onMuteParticipant + onMuteParticipant, + onForbidParticipantAudio, + onPermitParticipantAudio } = props; const ids = useIdentifiers(); @@ -650,32 +656,36 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { reactionResources={reactionResources} /* @conditional-compile-remove(soft-mute) */ onMuteParticipant={onMuteParticipant} + onForbidParticipantAudio={onForbidParticipantAudio} + onPermitParticipantAudio={onPermitParticipantAudio} /> ); }, [ + selectedScalingModeState, + pinnedParticipants, + videoTilesOptions?.alwaysShowLabelBackground, onCreateRemoteStreamView, onDisposeRemoteVideoStreamView, - remoteVideoViewOptions, - localParticipant, onRenderAvatar, showMuteIndicator, strings, - drawerMenuHostId, + localParticipant.userId, remoteVideoTileMenu, - selectedScalingModeState, - pinnedParticipants, + drawerMenuHostId, onPinParticipant, onUnpinParticipant, - toggleAnnouncerString, onUpdateScalingMode, + toggleAnnouncerString, spotlightedParticipants, onStartRemoteSpotlight, onStopRemoteSpotlight, maxParticipantsToSpotlight, - /* @conditional-compile-remove(soft-mute) */ onMuteParticipant, reactionResources, - videoTilesOptions + onMuteParticipant, + onForbidParticipantAudio, + onPermitParticipantAudio, + remoteVideoViewOptions ] ); diff --git a/packages/react-components/src/components/VideoGallery/useVideoTileContextualMenuProps.ts b/packages/react-components/src/components/VideoGallery/useVideoTileContextualMenuProps.ts index c97269ef742..0e8c05475a1 100644 --- a/packages/react-components/src/components/VideoGallery/useVideoTileContextualMenuProps.ts +++ b/packages/react-components/src/components/VideoGallery/useVideoTileContextualMenuProps.ts @@ -29,6 +29,10 @@ export const useVideoTileContextualMenuProps = (props: { spotlightLimitReachedMenuTitle?: string; /* @conditional-compile-remove(soft-mute) */ muteParticipantMenuItemLabel?: string; + forbidParticipantAudio?: string; + permitParticipantAudio?: string; + forbidParticipantAudioTileMenuLabel?: string; + permitParticipantAudioTileMenuLabel?: string; }; view?: { updateScalingMode: (scalingMode: ViewScalingMode) => Promise }; isPinned?: boolean; @@ -45,6 +49,8 @@ export const useVideoTileContextualMenuProps = (props: { myUserId?: string; /* @conditional-compile-remove(soft-mute) */ onMuteParticipant?: (userId: string) => void; + onForbidParticipantAudio?: (userIds: string[]) => void; + onPermitParticipantAudio?: (userIds: string[]) => void; }): IContextualMenuProps | undefined => { const { participant, @@ -62,7 +68,9 @@ export const useVideoTileContextualMenuProps = (props: { onStopSpotlight, maxParticipantsToSpotlight, myUserId, - /* @conditional-compile-remove(soft-mute) */ onMuteParticipant + /* @conditional-compile-remove(soft-mute) */ onMuteParticipant, + onForbidParticipantAudio, + onPermitParticipantAudio } = props; const scalingMode = useMemo(() => { return props.participant.videoStream?.scalingMode; @@ -173,6 +181,35 @@ export const useVideoTileContextualMenuProps = (props: { }); } } + + if (!participant.mediaAccess?.isAudioPermitted && onPermitParticipantAudio) { + items.push({ + key: 'permitParticipantAudio', + text: strings?.permitParticipantAudioTileMenuLabel, + iconProps: { + iconName: 'Microphone', + styles: { root: { lineHeight: 0 } } + }, + onClick: () => onPermitParticipantAudio([participant.userId]), + 'data-ui-id': 'video-tile-unblock-microphone', + ariaLabel: 'Unblock microphone' + }); + } + + if (participant.mediaAccess?.isAudioPermitted && onForbidParticipantAudio) { + items.push({ + key: 'forbidParticipantAudio', + text: strings?.forbidParticipantAudioTileMenuLabel, + iconProps: { + iconName: 'ControlButtonMicProhibited', + styles: { root: { lineHeight: 0 } } + }, + onClick: () => onForbidParticipantAudio([participant.userId]), + 'data-ui-id': 'video-tile-block-microphone', + ariaLabel: 'Block microphone' + }); + } + if (scalingMode) { if (scalingMode === 'Crop' && strings?.fitRemoteParticipantToFrame) { items.push({ diff --git a/packages/react-components/src/localization/locales/en-GB/strings.json b/packages/react-components/src/localization/locales/en-GB/strings.json index cf977d08c71..bd6c27aa948 100644 --- a/packages/react-components/src/localization/locales/en-GB/strings.json +++ b/packages/react-components/src/localization/locales/en-GB/strings.json @@ -510,7 +510,11 @@ "stopSpotlightOnSelfVideoTileMenuLabel": "Exit spotlight", "attendeeRole": "Attendee", "muteParticipantMenuItemLabel": "Mute", - "waitingScreenText": "Waiting for others to join" + "waitingScreenText": "Waiting for others to join", + "forbidParticipantAudio": "Disable mic", + "permitParticipantAudio": "Allow mic", + "forbidParticipantVideo": "Disable camera", + "permitParticipantVideo": "Allow camera" }, "dialpad": { "placeholderText": "Enter phone number", @@ -667,4 +671,4 @@ "incomingCallNotificationAccceptWithVideoButtonLabel": "Accept with Video", "incomingCallNotificationDismissButtonAriaLabel": "Dismiss" } -} \ No newline at end of file +} diff --git a/packages/react-components/src/localization/locales/en-US/strings.json b/packages/react-components/src/localization/locales/en-US/strings.json index 01f2ef5cae8..11eae6c7363 100644 --- a/packages/react-components/src/localization/locales/en-US/strings.json +++ b/packages/react-components/src/localization/locales/en-US/strings.json @@ -510,7 +510,13 @@ "stopSpotlightOnSelfVideoTileMenuLabel": "Exit spotlight", "attendeeRole": "Attendee", "muteParticipantMenuItemLabel": "Mute", - "waitingScreenText": "Waiting for others to join" + "waitingScreenText": "Waiting for others to join", + "forbidParticipantAudio": "Disable mic", + "permitParticipantAudio": "Allow mic", + "forbidParticipantAudioTileMenuLabel": "Disable mic", + "permitParticipantAudioTileMenuLabel": "Allow mic", + "forbidParticipantVideo": "Disable camera", + "permitParticipantVideo": "Allow camera" }, "dialpad": { "placeholderText": "Enter phone number", diff --git a/packages/react-components/src/types/VideoGalleryParticipant.ts b/packages/react-components/src/types/VideoGalleryParticipant.ts index a83b5035ebb..2aef4ca8662 100644 --- a/packages/react-components/src/types/VideoGalleryParticipant.ts +++ b/packages/react-components/src/types/VideoGalleryParticipant.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { ParticipantState } from './ParticipantListParticipant'; +import { MediaAccess, ParticipantState } from './ParticipantListParticipant'; import { RaisedHand } from './ParticipantListParticipant'; import { Reaction } from './ParticipantListParticipant'; @@ -43,6 +43,7 @@ export type VideoGalleryParticipant = { isScreenSharingOn?: boolean; /** Whether participant is spotlighted **/ spotlight?: Spotlight; + mediaAccess?: MediaAccess; }; /** @@ -126,4 +127,5 @@ export interface VideoGalleryRemoteParticipant extends VideoGalleryParticipant { * @public * */ reaction?: Reaction; + mediaAccess?: MediaAccess; } diff --git a/packages/react-composites/src/composites/CallComposite/MockCallAdapter.ts b/packages/react-composites/src/composites/CallComposite/MockCallAdapter.ts index 01e9f6a0f23..68e1fc7268a 100644 --- a/packages/react-composites/src/composites/CallComposite/MockCallAdapter.ts +++ b/packages/react-composites/src/composites/CallComposite/MockCallAdapter.ts @@ -234,6 +234,12 @@ export class _MockCallAdapter implements CallAdapter { permitParticipantAudio(userIds: string[]): Promise { throw Error('permitParticipantAudio not implemented'); } + forbidAllAttendeesAudio(): Promise { + throw Error('forbidAllAttendeesAudio not implemented'); + } + permitAllAttendeesAudio(): Promise { + throw Error('permitAllAttendeesAudio not implemented'); + } } /** diff --git a/packages/react-composites/src/composites/CallComposite/Strings.tsx b/packages/react-composites/src/composites/CallComposite/Strings.tsx index 10a6912979f..0d44de7f8d1 100644 --- a/packages/react-composites/src/composites/CallComposite/Strings.tsx +++ b/packages/react-composites/src/composites/CallComposite/Strings.tsx @@ -894,4 +894,16 @@ export interface CallCompositeStrings { * notification. */ returnFromBreakoutRoomBannerButtonLabel: string; + forbidParticipantAudioMenuLabel: string; + permitParticipantAudioMenuLabel: string; + forbidAllAttendeesAudioDialogTitle: string; + forbidAllAttendeesAudioDialogContent: string; + forbidAllAttendeesAudioConfirmButtonLabel: string; + forbidAllAttendeesAudioCancelButtonLabel: string; + permitAllAttendeesAudioDialogTitle: string; + permitAllAttendeesAudioDialogContent: string; + permitAllAttendeesAudioConfirmButtonLabel: string; + permitAllAttendeesAudioCancelButtonLabel: string; + forbidAllAttendeesAudioMenuLabel: string; + permitAllAttendeesAudioMenuLabel: string; } diff --git a/packages/react-composites/src/composites/CallComposite/adapter/AzureCommunicationCallAdapter.ts b/packages/react-composites/src/composites/CallComposite/adapter/AzureCommunicationCallAdapter.ts index 00cca77d91e..010cc9d938f 100644 --- a/packages/react-composites/src/composites/CallComposite/adapter/AzureCommunicationCallAdapter.ts +++ b/packages/react-composites/src/composites/CallComposite/adapter/AzureCommunicationCallAdapter.ts @@ -626,6 +626,10 @@ export class AzureCommunicationCallAdapter { + this.handlers.onForbidAllAttendeesAudio?.(); + } + + public async permitAllAttendeesAudio(): Promise { + this.handlers.onPermitAllAttendeesAudio?.(); + } + /* @conditional-compile-remove(breakout-rooms) */ public async returnFromBreakoutRoom(): Promise { if (!this.originCall) { diff --git a/packages/react-composites/src/composites/CallComposite/adapter/CallAdapter.ts b/packages/react-composites/src/composites/CallComposite/adapter/CallAdapter.ts index 63c43064479..1b33445a5fb 100644 --- a/packages/react-composites/src/composites/CallComposite/adapter/CallAdapter.ts +++ b/packages/react-composites/src/composites/CallComposite/adapter/CallAdapter.ts @@ -786,6 +786,8 @@ export interface CallAdapterCallOperations { forbidParticipantAudio(userIds: string[]): Promise; permitParticipantAudio(userIds: string[]): Promise; + forbidAllAttendeesAudio(): Promise; + permitAllAttendeesAudio(): Promise; } /** diff --git a/packages/react-composites/src/composites/CallComposite/components/CallArrangement.tsx b/packages/react-composites/src/composites/CallComposite/components/CallArrangement.tsx index 9168b646d83..bc54aa1aa1f 100644 --- a/packages/react-composites/src/composites/CallComposite/components/CallArrangement.tsx +++ b/packages/react-composites/src/composites/CallComposite/components/CallArrangement.tsx @@ -233,7 +233,9 @@ export const CallArrangement = (props: CallArrangementProps): JSX.Element => { onMuteParticipant, spotlightedParticipants, maxParticipantsToSpotlight, - localParticipant + localParticipant, + onForbidParticipantAudio, + onPermitParticipantAudio } = videoGalleryProps; const [showTeamsMeetingConferenceModal, setShowTeamsMeetingConferenceModal] = useState(false); @@ -332,6 +334,29 @@ export const CallArrangement = (props: CallArrangementProps): JSX.Element => { /* @conditional-compile-remove(soft-mute) */ muteAllHandlers.onMuteAllRemoteParticipants ]); + const onToggleParticipantMicPeoplePaneProps = useMemo(() => { + return { + onForbidParticipantAudio: ['Unknown', 'Organizer', 'Presenter', 'Co-organizer'].includes(role ?? '') + ? onForbidParticipantAudio + : undefined, + onPermitParticipantAudio: ['Unknown', 'Organizer', 'Presenter', 'Co-organizer'].includes(role ?? '') + ? onPermitParticipantAudio + : undefined, + onForbidAllAttendeesAudio: ['Unknown', 'Organizer', 'Presenter', 'Co-organizer'].includes(role ?? '') + ? muteAllHandlers.onForbidAllAttendeesAudio + : undefined, + onPermitAllAttendeesAudio: ['Unknown', 'Organizer', 'Presenter', 'Co-organizer'].includes(role ?? '') + ? muteAllHandlers.onPermitAllAttendeesAudio + : undefined + }; + }, [ + role, + onForbidParticipantAudio, + onPermitParticipantAudio, + muteAllHandlers.onForbidAllAttendeesAudio, + muteAllHandlers.onPermitAllAttendeesAudio + ]); + const spotlightPeoplePaneProps = useMemo(() => { return { spotlightedParticipantUserIds: spotlightedParticipants, @@ -358,7 +383,8 @@ export const CallArrangement = (props: CallArrangementProps): JSX.Element => { ...peoplePaneProps, ...spotlightPeoplePaneProps, ...onMuteParticipantPeoplePaneProps, - ...pinPeoplePaneProps + ...pinPeoplePaneProps, + ...onToggleParticipantMicPeoplePaneProps }); const togglePeoplePane = useCallback(() => { if (isPeoplePaneOpen) { diff --git a/packages/react-composites/src/composites/CallComposite/components/SidePane/usePeoplePane.tsx b/packages/react-composites/src/composites/CallComposite/components/SidePane/usePeoplePane.tsx index ac8ebd177d3..24a12d16a15 100644 --- a/packages/react-composites/src/composites/CallComposite/components/SidePane/usePeoplePane.tsx +++ b/packages/react-composites/src/composites/CallComposite/components/SidePane/usePeoplePane.tsx @@ -43,6 +43,10 @@ export const usePeoplePane = (props: { onPinParticipant?: (userId: string) => void; onUnpinParticipant?: (userId: string) => void; disablePinMenuItem?: boolean; + onForbidParticipantAudio?: (userIds: string[]) => Promise; + onPermitParticipantAudio?: (userIds: string[]) => Promise; + onForbidAllAttendeesAudio?: () => Promise; + onPermitAllAttendeesAudio?: () => Promise; }): { openPeoplePane: () => void; closePeoplePane: () => void; @@ -71,7 +75,11 @@ export const usePeoplePane = (props: { onUnpinParticipant, disablePinMenuItem, /* @conditional-compile-remove(soft-mute) */ - onMuteAllRemoteParticipants + onMuteAllRemoteParticipants, + onForbidParticipantAudio, + onPermitParticipantAudio, + onForbidAllAttendeesAudio, + onPermitAllAttendeesAudio } = props; const closePane = useCallback(() => { @@ -100,12 +108,25 @@ export const usePeoplePane = (props: { ] ); + const [showForbidAllAttendeesAudioPrompt, setShowForbidAllAttendeesAudioPrompt] = React.useState(false); + const [showPermitAllAttendeesAudioPrompt, setShowPermitAllAttendeesAudioPrompt] = React.useState(false); + /* @conditional-compile-remove(soft-mute) */ const onMuteAllPromptConfirm = useCallback(() => { onMuteAllRemoteParticipants && onMuteAllRemoteParticipants(); setShowMuteAllPrompt(false); }, [onMuteAllRemoteParticipants, setShowMuteAllPrompt]); + const onForbidAllAttendeesPromptConfirm = useCallback(() => { + onForbidAllAttendeesAudio && onForbidAllAttendeesAudio(); + setShowForbidAllAttendeesAudioPrompt(false); + }, [onForbidAllAttendeesAudio, setShowForbidAllAttendeesAudioPrompt]); + console.log(onPermitAllAttendeesAudio); + const onPermitAllAttendeesPromptConfirm = useCallback(() => { + onPermitAllAttendeesAudio && onPermitAllAttendeesAudio(); + setShowPermitAllAttendeesAudioPrompt(false); + }, [onPermitAllAttendeesAudio, setShowPermitAllAttendeesAudioPrompt]); + const sidePaneHeaderMenuProps: IContextualMenuProps = useMemo(() => { const menuItems: IContextualMenuItem[] = []; /* @conditional-compile-remove(soft-mute) */ @@ -134,6 +155,61 @@ export const usePeoplePane = (props: { disabled: isAllMuted }); } + + if (onForbidAllAttendeesAudio && remoteParticipants) { + let hasAttendee = false; + if (remoteParticipants) { + for (const participant of Object.values(remoteParticipants)) { + if (participant.role && participant.role === 'Attendee' && participant.mediaAccess?.isAudioPermitted) { + hasAttendee = true; + break; + } + } + } + hasAttendee && + menuItems.push({ + ['data-ui-id']: 'people-pane-forbid-all-attendees-audio', + key: 'forbidAllAttendeesAudio', + text: localeStrings.forbidAllAttendeesAudioMenuLabel, + iconProps: { + iconName: 'ControlButtonMicProhibited', // ControlButtonMicProhibited + styles: { root: { lineHeight: 0 } } + }, + onClick: () => { + setShowForbidAllAttendeesAudioPrompt(true); + }, + ariaLabel: localeStrings.forbidAllAttendeesAudioMenuLabel, + disabled: !hasAttendee + }); + } + + if (onPermitAllAttendeesAudio && remoteParticipants) { + let hasAttendee = false; + if (remoteParticipants) { + for (const participant of Object.values(remoteParticipants)) { + if (participant.role && participant.role === 'Attendee' && !participant.mediaAccess?.isAudioPermitted) { + hasAttendee = true; + break; + } + } + } + hasAttendee && + menuItems.push({ + ['data-ui-id']: 'people-pane-permit-all-attendees-audio', + key: 'permitAllAttendeesAudio', + text: localeStrings.permitAllAttendeesAudioMenuLabel, + iconProps: { + iconName: 'ContextualMenuMicMutedIcon', + styles: { root: { lineHeight: 0 } } + }, + onClick: () => { + setShowPermitAllAttendeesAudioPrompt(true); + }, + ariaLabel: localeStrings.permitAllAttendeesAudioMenuLabel, + disabled: !hasAttendee + }); + } + if (onStopAllSpotlight && spotlightedParticipantUserIds && spotlightedParticipantUserIds.length > 0) { menuItems.push({ key: 'stopAllSpotlightKey', @@ -149,13 +225,16 @@ export const usePeoplePane = (props: { items: menuItems }; }, [ + onMuteAllRemoteParticipants, + remoteParticipants, + onForbidAllAttendeesAudio, + onPermitAllAttendeesAudio, onStopAllSpotlight, spotlightedParticipantUserIds, - localeStrings.stopAllSpotlightMenuLabel, - /* @conditional-compile-remove(soft-mute) */ localeStrings.muteAllMenuLabel, - /* @conditional-compile-remove(soft-mute) */ onMuteAllRemoteParticipants, - /* @conditional-compile-remove(soft-mute) */ setShowMuteAllPrompt, - /* @conditional-compile-remove(soft-mute) */ remoteParticipants + localeStrings.muteAllMenuLabel, + localeStrings.forbidAllAttendeesAudioMenuLabel, + localeStrings.permitAllAttendeesAudioMenuLabel, + localeStrings.stopAllSpotlightMenuLabel ]); const onRenderHeader = useCallback( @@ -285,38 +364,46 @@ export const usePeoplePane = (props: { ariaLabel: localeStrings.pinParticipantMenuItemAriaLabel }); } + const remoteParticipant = remoteParticipants?.[participantId]; + if ( + !remoteParticipant?.mediaAccess?.isAudioPermitted && + remoteParticipant?.role === 'Attendee' && + onPermitParticipantAudio + ) { + _defaultMenuItems.push({ + key: 'permit-audio', + text: localeStrings.permitParticipantAudioMenuLabel, + iconProps: { + iconName: 'ContextualMenuMicMutedIcon', + styles: { root: { lineHeight: '1rem', textAlign: 'center' } } + }, + onClick: () => { + onPermitParticipantAudio([participantId]); + }, + 'data-ui-id': 'participant-item-permit-microphone-button', + ariaLabel: localeStrings.permitParticipantAudioMenuLabel + }); + } - // if (!remoteParticipants?.[participantId]?.mediaAccess && onUnBlockParticipantMicrophone) { - // _defaultMenuItems.push({ - // key: 'unblock-microphone', - // text: 'Unblock microphone', - // iconProps: { - // iconName: 'UnblockMicrophone', - // styles: { root: { lineHeight: '1rem', textAlign: 'center' } } - // }, - // onClick: () => { - // onUnBlockParticipantMicrophone(participantId); - // }, - // 'data-ui-id': 'participant-item-unblock-microphone-button', - // ariaLabel: 'Unblock microphone' - // }); - // } - - // if (remoteParticipants?.[participantId]?.isAudioPermitted && onBlockParticipantMicrophone) { - // _defaultMenuItems.push({ - // key: 'block-microphone', - // text: 'Block microphone', - // iconProps: { - // iconName: 'BlockMicrophone', - // styles: { root: { lineHeight: '1rem', textAlign: 'center' } } - // }, - // onClick: () => { - // onBlockParticipantMicrophone(participantId); - // }, - // 'data-ui-id': 'participant-item-block-microphone-button', - // ariaLabel: 'Block microphone' - // }); - // } + if ( + remoteParticipant?.mediaAccess?.isAudioPermitted && + remoteParticipant?.role === 'Attendee' && + onForbidParticipantAudio + ) { + _defaultMenuItems.push({ + key: 'forbid-audio', + text: localeStrings.forbidParticipantAudioMenuLabel, + iconProps: { + iconName: 'ControlButtonMicProhibited', + styles: { root: { lineHeight: '1rem', textAlign: 'center' } } + }, + onClick: () => { + onForbidParticipantAudio([participantId]); + }, + 'data-ui-id': 'participant-item-forbid-microphone-button', + ariaLabel: localeStrings.forbidParticipantAudioMenuLabel + }); + } } if (defaultMenuItems) { _defaultMenuItems.push(...defaultMenuItems); @@ -326,31 +413,33 @@ export const usePeoplePane = (props: { : _defaultMenuItems; }, [ + pinnedParticipants, spotlightedParticipantUserIds, - onStartLocalSpotlight, - onStopLocalSpotlight, - onStartRemoteSpotlight, - onStopRemoteSpotlight, - onFetchParticipantMenuItems, - /* @conditional-compile-remove(soft-mute) */ onMuteParticipant, - /* @conditional-compile-remove(soft-mute) */ remoteParticipants, - localeStrings.stopSpotlightMenuLabel, + onFetchParticipantMenuItems, localeStrings.stopSpotlightOnSelfMenuLabel, + localeStrings.stopSpotlightMenuLabel, localeStrings.addSpotlightMenuLabel, localeStrings.startSpotlightMenuLabel, localeStrings.spotlightLimitReachedMenuTitle, - maxParticipantsToSpotlight, - pinnedParticipants, - onPinParticipant, - onUnpinParticipant, - disablePinMenuItem, + localeStrings?.unpinParticipantMenuLabel, localeStrings.pinParticipantMenuLabel, - localeStrings.pinParticipantLimitReachedMenuLabel, - localeStrings.unpinParticipantMenuLabel, localeStrings.unpinParticipantMenuItemAriaLabel, - localeStrings.pinParticipantMenuItemAriaLabel + localeStrings.pinParticipantLimitReachedMenuLabel, + localeStrings.pinParticipantMenuItemAriaLabel, + localeStrings.permitParticipantAudioMenuLabel, + localeStrings.forbidParticipantAudioMenuLabel, + onStopLocalSpotlight, + onStopRemoteSpotlight, + maxParticipantsToSpotlight, + onStartLocalSpotlight, + onStartRemoteSpotlight, + onUnpinParticipant, + onPinParticipant, + onPermitParticipantAudio, + onForbidParticipantAudio, + disablePinMenuItem ] ); @@ -370,6 +459,30 @@ export const usePeoplePane = (props: { onCancel={() => setShowMuteAllPrompt(false)} /> } + { + onForbidAllAttendeesPromptConfirm()} + isOpen={showForbidAllAttendeesAudioPrompt} + onCancel={() => setShowForbidAllAttendeesAudioPrompt(false)} + /> + } + { + onPermitAllAttendeesPromptConfirm()} + isOpen={showPermitAllAttendeesAudioPrompt} + onCancel={() => setShowForbidAllAttendeesAudioPrompt(false)} + /> + } ); }, [ + muteAllPromptLabels, + showMuteAllPrompt, + localeStrings.forbidAllAttendeesAudioDialogTitle, + localeStrings.forbidAllAttendeesAudioDialogContent, + localeStrings.forbidAllAttendeesAudioConfirmButtonLabel, + localeStrings.forbidAllAttendeesAudioCancelButtonLabel, + localeStrings.permitAllAttendeesAudioDialogTitle, + localeStrings.permitAllAttendeesAudioDialogContent, + localeStrings.permitAllAttendeesAudioConfirmButtonLabel, + localeStrings.permitAllAttendeesAudioCancelButtonLabel, + showForbidAllAttendeesAudioPrompt, + showPermitAllAttendeesAudioPrompt, inviteLink, - mobileView, onFetchAvatarPersonaData, onFetchParticipantMenuItemsForCallComposite, setDrawerMenuItems, + mobileView, setParticipantActioned, sidePaneHeaderMenuProps, pinnedParticipants, role, alternateCallerId, - /* @conditional-compile-remove(soft-mute) */ showMuteAllPrompt, - /* @conditional-compile-remove(soft-mute) */ setShowMuteAllPrompt, - /* @conditional-compile-remove(soft-mute) */ muteAllPromptLabels, - /* @conditional-compile-remove(soft-mute) */ onMuteAllPromptConfirm + onMuteAllPromptConfirm, + onForbidAllAttendeesPromptConfirm, + onPermitAllAttendeesPromptConfirm ]); const sidePaneRenderer: SidePaneRenderer = useMemo( diff --git a/packages/react-composites/src/composites/CallComposite/hooks/useHandlers.ts b/packages/react-composites/src/composites/CallComposite/hooks/useHandlers.ts index 548809e1217..45040f6e0dc 100644 --- a/packages/react-composites/src/composites/CallComposite/hooks/useHandlers.ts +++ b/packages/react-composites/src/composites/CallComposite/hooks/useHandlers.ts @@ -247,6 +247,16 @@ const createCompositeHandlers = memoizeOne( ? async (userIds: string[]): Promise => { await adapter.permitParticipantAudio(userIds); } + : undefined, + onForbidAllAttendeesAudio: capabilities?.forbidOthersMedia.isPresent + ? async (): Promise => { + await adapter.forbidAllAttendeesAudio(); + } + : undefined, + onPermitAllAttendeesAudio: capabilities?.forbidOthersMedia.isPresent + ? async (): Promise => { + await adapter.permitAllAttendeesAudio(); + } : undefined }; } diff --git a/packages/react-composites/src/composites/CallWithChatComposite/adapter/AzureCommunicationCallWithChatAdapter.ts b/packages/react-composites/src/composites/CallWithChatComposite/adapter/AzureCommunicationCallWithChatAdapter.ts index 01c5a09f448..6cafb38a566 100644 --- a/packages/react-composites/src/composites/CallWithChatComposite/adapter/AzureCommunicationCallWithChatAdapter.ts +++ b/packages/react-composites/src/composites/CallWithChatComposite/adapter/AzureCommunicationCallWithChatAdapter.ts @@ -720,6 +720,14 @@ export class AzureCommunicationCallWithChatAdapter implements CallWithChatAdapte return this.callAdapter.permitParticipantAudio(userIds); } + public async forbidAllAttendeesAudio(): Promise { + return this.callAdapter.forbidAllAttendeesAudio(); + } + + public async permitAllAttendeesAudio(): Promise { + return this.callAdapter.permitAllAttendeesAudio(); + } + on(event: 'callParticipantsJoined', listener: ParticipantsJoinedListener): void; on(event: 'callParticipantsLeft', listener: ParticipantsLeftListener): void; on(event: 'callEnded', listener: CallEndedListener): void; diff --git a/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatAdapter.ts b/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatAdapter.ts index fd02efeaecb..baf08706032 100644 --- a/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatAdapter.ts +++ b/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatAdapter.ts @@ -519,6 +519,8 @@ export interface CallWithChatAdapterManagement { forbidParticipantAudio: (userIds: string[]) => Promise; permitParticipantAudio: (userIds: string[]) => Promise; + forbidAllAttendeesAudio: () => Promise; + permitAllAttendeesAudio: () => Promise; } /** diff --git a/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatBackedCallAdapter.ts b/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatBackedCallAdapter.ts index bdff2cf56c0..8cc8872c384 100644 --- a/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatBackedCallAdapter.ts +++ b/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatBackedCallAdapter.ts @@ -253,6 +253,14 @@ export class CallWithChatBackedCallAdapter implements CallAdapter { public async permitParticipantAudio(userIds: string[]): Promise { return this.callWithChatAdapter.permitParticipantAudio(userIds); } + + public async forbidAllAttendeesAudio(): Promise { + return this.callWithChatAdapter.forbidAllAttendeesAudio(); + } + + public async permitAllAttendeesAudio(): Promise { + return this.callWithChatAdapter.permitAllAttendeesAudio(); + } } function callAdapterStateFromCallWithChatAdapterState( diff --git a/packages/react-composites/src/composites/common/Drawer/MoreDrawer.tsx b/packages/react-composites/src/composites/common/Drawer/MoreDrawer.tsx index 101ca9893f9..9f0d103e0aa 100644 --- a/packages/react-composites/src/composites/common/Drawer/MoreDrawer.tsx +++ b/packages/react-composites/src/composites/common/Drawer/MoreDrawer.tsx @@ -154,6 +154,9 @@ export interface MoreDrawerProps extends MoreDrawerDevicesMenuProps { onClickMeetingPhoneInfo?: () => void; /* @conditional-compile-remove(soft-mute) */ onMuteAllRemoteParticipants?: () => void; + + onForbidAllAttendeesAudio?: () => void; + onPermitAllAttendeesAudio?: () => void; } const inferCallWithChatControlOptions = ( diff --git a/packages/react-composites/src/composites/localization/locales/en-US/strings.json b/packages/react-composites/src/composites/localization/locales/en-US/strings.json index d33d7af6ca1..5796545eb95 100644 --- a/packages/react-composites/src/composites/localization/locales/en-US/strings.json +++ b/packages/react-composites/src/composites/localization/locales/en-US/strings.json @@ -356,7 +356,19 @@ "joinBreakoutRoomBannerTitle": "Breakout room '{roomName}' is open", "joinBreakoutRoomBannerButtonLabel": "Join", "returnFromBreakoutRoomBannerTitle": "Return to main meeting", - "returnFromBreakoutRoomBannerButtonLabel": "Rejoin" + "returnFromBreakoutRoomBannerButtonLabel": "Rejoin", + "forbidParticipantAudioMenuLabel": "Disable mic", + "permitParticipantAudioMenuLabel": "Allow mic", + "forbidAllAttendeesAudioDialogTitle": "Disable mic for attendees?", + "forbidAllAttendeesAudioDialogContent": "Attendees won’t be able to unmute themselves. The organizer and presenters can let people unmute as needed.", + "forbidAllAttendeesAudioConfirmButtonLabel": "Disable mics", + "forbidAllAttendeesAudioCancelButtonLabel": "Cancel", + "permitAllAttendeesAudioDialogTitle": "Allow mic for attendees?", + "permitAllAttendeesAudioDialogContent": "Everyone in the meeting will be able to unmute themselves.", + "permitAllAttendeesAudioConfirmButtonLabel": "Allow mics", + "permitAllAttendeesAudioCancelButtonLabel": "Cancel", + "forbidAllAttendeesAudioMenuLabel": "Disable mic for attendees", + "permitAllAttendeesAudioMenuLabel": "Allow mic for attendees" }, "chat": { "chatListHeader": "In this chat", From a1093879f0d44f023b710f29fa370298cf599685 Mon Sep 17 00:00:00 2001 From: fuyan Date: Wed, 16 Oct 2024 10:02:47 -0700 Subject: [PATCH 03/13] update --- common/config/babel/features.js | 4 +- .../src/baseSelectors.ts | 20 +++---- .../src/handlers/createCommonHandlers.ts | 19 ++++++- .../src/participantListSelector.ts | 5 ++ .../src/utils/participantListSelectorUtils.ts | 5 ++ .../src/utils/videoGalleryUtils.ts | 11 +++- .../src/CallClientState.ts | 16 +++--- .../src/CallContext.ts | 11 ++-- .../src/CallSubscriber.ts | 4 ++ .../calling-stateful-client/src/Converter.ts | 1 + .../src/MediaAccessSubscriber.ts | 1 + .../src/index-public.ts | 1 + .../review/beta/communication-react.api.md | 1 - packages/communication-react/src/index.ts | 1 + .../src/components/LocalVideoTile.tsx | 7 ++- .../src/components/ParticipantList.tsx | 32 +++++++++-- .../src/components/RemoteVideoTile.tsx | 5 +- .../src/components/VideoGallery.tsx | 1 + .../useVideoTileContextualMenuProps.ts | 4 +- .../src/components/VideoTile.tsx | 13 ++++- packages/react-components/src/index.ts | 2 +- .../src/types/ParticipantListParticipant.ts | 3 +- .../src/types/VideoGalleryParticipant.ts | 8 ++- .../CallComposite/MockCallAdapter.ts | 4 ++ .../src/composites/CallComposite/Strings.tsx | 12 ++++ .../adapter/AzureCommunicationCallAdapter.ts | 8 +-- .../CallComposite/adapter/CallAdapter.ts | 5 +- .../components/CallArrangement.tsx | 12 ++-- .../components/SidePane/usePeoplePane.tsx | 56 +++++++++++++------ .../CallComposite/hooks/useHandlers.ts | 4 ++ .../AzureCommunicationCallWithChatAdapter.ts | 8 +-- .../adapter/CallWithChatAdapter.ts | 5 +- .../adapter/CallWithChatBackedCallAdapter.ts | 8 +-- .../composites/common/Drawer/MoreDrawer.tsx | 3 +- 34 files changed, 216 insertions(+), 84 deletions(-) diff --git a/common/config/babel/features.js b/common/config/babel/features.js index b67316391ca..7b88b29d4e6 100644 --- a/common/config/babel/features.js +++ b/common/config/babel/features.js @@ -24,7 +24,9 @@ module.exports = { // Feature for showing dtmp dialer by default "dtmf-dialer-on-by-default", // Feature for together mode - "together-mode" + "together-mode", + // Feature for hard mute audio/video + "meida-access" ], beta: [ "call-readiness", diff --git a/packages/calling-component-bindings/src/baseSelectors.ts b/packages/calling-component-bindings/src/baseSelectors.ts index ad3f105132c..f07c4f01d52 100644 --- a/packages/calling-component-bindings/src/baseSelectors.ts +++ b/packages/calling-component-bindings/src/baseSelectors.ts @@ -28,7 +28,7 @@ import { _SupportedCaptionLanguage, _SupportedSpokenLanguage } from '@internal/r import { ConferencePhoneInfo } from '@internal/calling-stateful-client'; /* @conditional-compile-remove(breakout-rooms) */ import { CallNotifications } from '@internal/calling-stateful-client'; -import { MediaAccessCallFeatureState } from '@internal/calling-stateful-client/dist/dist-esm/CallClientState'; +// import { MediaAccessCallFeatureState } from '@internal/calling-stateful-client/dist/dist-esm/CallClientState'; /** * Common props used to reference calling declarative client state. @@ -114,15 +114,15 @@ export const getSpotlightCallFeature = ( return state.calls[props.callId]?.spotlight; }; -/** - * @private - */ -export const getMediaAccessCallFeature = ( - state: CallClientState, - props: CallingBaseSelectorProps -): MediaAccessCallFeatureState | undefined => { - return state.calls[props.callId]?.mediaAccess; -}; +// /** +// * @private +// */ +// export const getMediaAccessCallFeature = ( +// state: CallClientState, +// props: CallingBaseSelectorProps +// ): MediaAccessCallFeatureState | undefined => { +// return state.calls[props.callId]?.mediaAccess; +// }; /** * @private diff --git a/packages/calling-component-bindings/src/handlers/createCommonHandlers.ts b/packages/calling-component-bindings/src/handlers/createCommonHandlers.ts index f56c83dbb24..03a16f2aba5 100644 --- a/packages/calling-component-bindings/src/handlers/createCommonHandlers.ts +++ b/packages/calling-component-bindings/src/handlers/createCommonHandlers.ts @@ -106,10 +106,13 @@ export interface CommonCallingHandlers { onMuteParticipant: (userId: string) => Promise; /* @conditional-compile-remove(soft-mute) */ onMuteAllRemoteParticipants: () => Promise; - + /* @conditional-compile-remove(media-access) */ onForbidParticipantAudio?: (userIds: string[]) => Promise; + /* @conditional-compile-remove(media-access) */ onPermitParticipantAudio?: (userIds: string[]) => Promise; + /* @conditional-compile-remove(media-access) */ onForbidAllAttendeesAudio?: () => Promise; + /* @conditional-compile-remove(media-access) */ onPermitAllAttendeesAudio?: () => Promise; } @@ -721,6 +724,11 @@ export const createDefaultCommonCallingHandlers = memoizeOne( } : undefined; // const canForbidOthersMedia = call?.feature(Features.Capabilities).capabilities.forbidOthersMedia.isPresent; + // const callState = call && callClient.getState().calls[call.id]; + // const canForbidOthersMedia = callState + // ? callState.capabilitiesFeature?.capabilities.forbidOthersMedia.isPresent + // : false; + // console.log('hi there canForbidOthersMedia', canForbidOthersMedia); // const onForbidParticipantAudio = canForbidOthersMedia // ? async (userIds: string[]): Promise => { @@ -735,18 +743,21 @@ export const createDefaultCommonCallingHandlers = memoizeOne( // } // : undefined; + /* @conditional-compile-remove(media-access) */ const onForbidParticipantAudio = async (userIds: string[]): Promise => { const participants = userIds?.map((userId) => _toCommunicationIdentifier(userId)); await call?.feature(Features.MediaAccess).forbidAudio(participants); }; + /* @conditional-compile-remove(media-access) */ const onPermitParticipantAudio = async (userIds: string[]): Promise => { const participants = userIds?.map((userId) => _toCommunicationIdentifier(userId)); await call?.feature(Features.MediaAccess).permitAudio(participants); }; + /* @conditional-compile-remove(media-access) */ const onForbidAllAttendeesAudio = async (): Promise => { await call?.feature(Features.MediaAccess).forbidOthersAudio(); }; - + /* @conditional-compile-remove(media-access) */ const onPermitAllAttendeesAudio = async (): Promise => { await call?.feature(Features.MediaAccess).permitOthersAudio(); }; @@ -817,9 +828,13 @@ export const createDefaultCommonCallingHandlers = memoizeOne( onMuteAllRemoteParticipants, onAcceptCall: notImplemented, onRejectCall: notImplemented, + /* @conditional-compile-remove(media-access) */ onForbidParticipantAudio, + /* @conditional-compile-remove(media-access) */ onPermitParticipantAudio, + /* @conditional-compile-remove(media-access) */ onForbidAllAttendeesAudio, + /* @conditional-compile-remove(media-access) */ onPermitAllAttendeesAudio }; } diff --git a/packages/calling-component-bindings/src/participantListSelector.ts b/packages/calling-component-bindings/src/participantListSelector.ts index 87f9e206a1f..c1a03920d2d 100644 --- a/packages/calling-component-bindings/src/participantListSelector.ts +++ b/packages/calling-component-bindings/src/participantListSelector.ts @@ -74,6 +74,7 @@ const convertRemoteParticipantsToParticipantListParticipants = ( spotlightedParticipants, toFlatCommunicationIdentifier(participant.identifier) ); + /* @conditional-compile-remove(media-access) */ const mediaAccess = { isAudioPermitted: participant.mediaAccess?.isAudioPermitted, isVideoPermitted: participant.mediaAccess?.isVideoPermitted @@ -90,6 +91,7 @@ const convertRemoteParticipantsToParticipantListParticipants = ( localUserCanRemoveOthers, remoteParticipantReaction, spotlight, + /* @conditional-compile-remove(media-access) */ mediaAccess ); }) @@ -142,6 +144,7 @@ export const participantListSelector: ParticipantListSelector = createSelector( isHideAttendeeNamesEnabled, getLocalParticipantReactionState, getSpotlightCallFeature, + /* @conditional-compile-remove(media-access) */ getCapabilities ], ( @@ -156,6 +159,7 @@ export const participantListSelector: ParticipantListSelector = createSelector( isHideAttendeeNamesEnabled, localParticipantReactionState, spotlightCallFeature, + /* @conditional-compile-remove(media-access) */ capabilities ): { participants: CallParticipantListParticipant[]; @@ -184,6 +188,7 @@ export const participantListSelector: ParticipantListSelector = createSelector( isRemovable: false, reaction: memoizedConvertToVideoTileReaction(localParticipantReactionState), spotlight: memoizedSpotlight(spotlightCallFeature?.spotlightedParticipants, userId), + /* @conditional-compile-remove(media-access) */ mediaAccess: { isAudioPermitted: !!capabilities?.unmuteMic.isPresent, isVideoPermitted: !!capabilities?.turnVideoOn.isPresent diff --git a/packages/calling-component-bindings/src/utils/participantListSelectorUtils.ts b/packages/calling-component-bindings/src/utils/participantListSelectorUtils.ts index ac6642e497d..d6eb781fc2b 100644 --- a/packages/calling-component-bindings/src/utils/participantListSelectorUtils.ts +++ b/packages/calling-component-bindings/src/utils/participantListSelectorUtils.ts @@ -11,6 +11,7 @@ import { Spotlight } from '@internal/react-components'; import { RaisedHandState } from '@internal/calling-stateful-client'; import { ReactionState } from '@internal/calling-stateful-client'; import { Reaction } from '@internal/react-components'; +/* @conditional-compile-remove(media-access) */ import { MediaAccess } from '@internal/react-components'; import memoizeOne from 'memoize-one'; @@ -26,6 +27,7 @@ const convertRemoteParticipantToParticipantListParticipant = ( localUserCanRemoveOthers: boolean, reaction: undefined | Reaction, spotlight: undefined | Spotlight, + /* @conditional-compile-remove(media-access) */ mediaAccess: MediaAccess | undefined ): CallParticipantListParticipant => { const identifier = fromFlatCommunicationIdentifier(userId); @@ -45,6 +47,7 @@ const convertRemoteParticipantToParticipantListParticipant = ( localUserCanRemoveOthers, reaction, spotlight, + /* @conditional-compile-remove(media-access) */ mediaAccess }; }; @@ -64,6 +67,7 @@ export const memoizedConvertAllremoteParticipants = memoizeFnAll( localUserCanRemoveOthers: boolean, reaction: undefined | Reaction, spotlight: undefined | Spotlight, + /* @conditional-compile-remove(media-access) */ mediaAccess: MediaAccess | undefined ): CallParticipantListParticipant => { return convertRemoteParticipantToParticipantListParticipant( @@ -77,6 +81,7 @@ export const memoizedConvertAllremoteParticipants = memoizeFnAll( localUserCanRemoveOthers, reaction, spotlight, + /* @conditional-compile-remove(media-access) */ mediaAccess ); } diff --git a/packages/calling-component-bindings/src/utils/videoGalleryUtils.ts b/packages/calling-component-bindings/src/utils/videoGalleryUtils.ts index 06bd02cdf2d..64966ddccf7 100644 --- a/packages/calling-component-bindings/src/utils/videoGalleryUtils.ts +++ b/packages/calling-component-bindings/src/utils/videoGalleryUtils.ts @@ -6,7 +6,11 @@ import { SpotlightedParticipant } from '@azure/communication-calling'; import { ParticipantRole } from '@azure/communication-calling'; import { memoizeFnAll, toFlatCommunicationIdentifier } from '@internal/acs-ui-common'; import { RemoteParticipantState, RemoteVideoStreamState } from '@internal/calling-stateful-client'; -import { MediaAccess, VideoGalleryRemoteParticipant, VideoGalleryStream } from '@internal/react-components'; +import { + VideoGalleryRemoteParticipant, + VideoGalleryStream, + /* @conditional-compile-remove(media-access) */ MediaAccess +} from '@internal/react-components'; import memoizeOne from 'memoize-one'; import { _convertParticipantState, ParticipantConnectionState } from './callUtils'; import { maskDisplayNameWithRole } from './callUtils'; @@ -73,6 +77,7 @@ export const _videoGalleryRemoteParticipantsMemo: _VideoGalleryRemoteParticipant participant.contentSharingStream, remoteParticipantReaction, spotlight, + /* @conditional-compile-remove(media-access) */ participant.mediaAccess ); }) @@ -92,6 +97,7 @@ const memoizedAllConvertRemoteParticipant = memoizeFnAll( contentSharingStream?: HTMLElement, reaction?: Reaction, spotlight?: Spotlight, + /* @conditional-compile-remove(media-access) */ mediaAccess?: MediaAccess ): VideoGalleryRemoteParticipant => { return convertRemoteParticipantToVideoGalleryRemoteParticipant( @@ -105,6 +111,7 @@ const memoizedAllConvertRemoteParticipant = memoizeFnAll( contentSharingStream, reaction, spotlight, + /* @conditional-compile-remove(media-access) */ mediaAccess ); } @@ -122,6 +129,7 @@ export const convertRemoteParticipantToVideoGalleryRemoteParticipant = ( contentSharingStream?: HTMLElement, reaction?: Reaction, spotlight?: Spotlight, + /* @conditional-compile-remove(media-access) */ mediaAccess?: MediaAccess ): VideoGalleryRemoteParticipant => { const rawVideoStreamsArray = Object.values(videoStreams); @@ -166,6 +174,7 @@ export const convertRemoteParticipantToVideoGalleryRemoteParticipant = ( raisedHand, reaction, spotlight, + /* @conditional-compile-remove(media-access) */ mediaAccess }; }; diff --git a/packages/calling-stateful-client/src/CallClientState.ts b/packages/calling-stateful-client/src/CallClientState.ts index 2b2bb2422b2..a8bf657d69e 100644 --- a/packages/calling-stateful-client/src/CallClientState.ts +++ b/packages/calling-stateful-client/src/CallClientState.ts @@ -28,7 +28,7 @@ import type { DiagnosticValueType, DiagnosticQuality, DiagnosticFlag, - MediaAccess + /* @conditional-compile-remove(media-access) */ MediaAccess } from '@azure/communication-calling'; import { TeamsCallInfo } from '@azure/communication-calling'; import { CallInfo } from '@azure/communication-calling'; @@ -197,7 +197,7 @@ export interface SpotlightState { */ spotlightedOrderPosition?: number; } - +/* @conditional-compile-remove(media-access) */ /** * State only version of {@link @azure/communication-calling#MediaAccessCallFeature} * @@ -206,7 +206,7 @@ export interface SpotlightState { export interface MediaAccessCallFeatureState { mediaAccesses: MediaAccess[]; } - +/* @conditional-compile-remove(media-access) */ /** * Media access state * @@ -547,7 +547,7 @@ export interface RemoteParticipantState { * The diagnostic status of RemoteParticipant{@link @azure/communication-calling#RemoteDiagnostics}. */ diagnostics?: Record; - + /* @conditional-compile-remove(meida-access) */ /** * Proxy of {@link @azure/communication-calling#Call.MediaAccess.mediaAccesses}. */ @@ -738,10 +738,10 @@ export interface CallState { */ breakoutRooms?: BreakoutRoomsState; - /** - * Proxy of {@link @azure/communication-calling#MediaAccessCallFeature}. - */ - mediaAccess?: MediaAccessCallFeatureState; + // /** + // * Proxy of {@link @azure/communication-calling#MediaAccessCallFeature}. + // */ + // mediaAccess?: MediaAccessCallFeatureState; } /** diff --git a/packages/calling-stateful-client/src/CallContext.ts b/packages/calling-stateful-client/src/CallContext.ts index 7c4f7277367..750b0515667 100644 --- a/packages/calling-stateful-client/src/CallContext.ts +++ b/packages/calling-stateful-client/src/CallContext.ts @@ -9,7 +9,7 @@ import { ParticipantRole, ScalingMode, VideoDeviceInfo, - MediaAccess + /* @conditional-compile-remove(media-access) */ MediaAccess } from '@azure/communication-calling'; import { RaisedHand } from '@azure/communication-calling'; /* @conditional-compile-remove(breakout-rooms) */ @@ -191,7 +191,7 @@ export class CallContext { existingCall.info = call.info; existingCall.meetingConference = call.meetingConference; - existingCall.mediaAccess = call.mediaAccess; + // existingCall.mediaAccess = call.mediaAccess; } else { draft.calls[latestCallId] = call; } @@ -1164,15 +1164,16 @@ export class CallContext { }); } + /* @conditional-compile-remove(media-access) */ public setMediaAccesses(callId: string, mediaAccesses: MediaAccess[]): void { this.modifyState((draft: CallClientState) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (!call) { return; } - call.mediaAccess = { - mediaAccesses - }; + // call.mediaAccess = { + // mediaAccesses + // }; mediaAccesses.forEach((participantMediaAccess) => { const participant = call.remoteParticipants[toFlatCommunicationIdentifier(participantMediaAccess.participant)]; if (participant) { diff --git a/packages/calling-stateful-client/src/CallSubscriber.ts b/packages/calling-stateful-client/src/CallSubscriber.ts index a905cda8157..c4ad5608849 100644 --- a/packages/calling-stateful-client/src/CallSubscriber.ts +++ b/packages/calling-stateful-client/src/CallSubscriber.ts @@ -32,6 +32,7 @@ import { LocalRecordingSubscriber } from './LocalRecordingSubscriber'; import { BreakoutRoomsSubscriber } from './BreakoutRoomsSubscriber'; /* @conditional-compile-remove(together-mode) */ import { TogetherModeSubscriber } from './TogetherModeSubscriber'; +/* @conditional-compile-remove(media-access) */ import { MediaAccessSubscriber } from './MediaAccessSubscriber'; /** @@ -65,6 +66,7 @@ export class CallSubscriber { private _breakoutRoomsSubscriber: BreakoutRoomsSubscriber; /* @conditional-compile-remove(together-mode) */ private _togetherModeSubscriber: TogetherModeSubscriber; + /* @conditional-compile-remove(media-access) */ private _mediaAccessSubscriber: MediaAccessSubscriber; constructor(call: CallCommon, context: CallContext, internalContext: InternalCallContext) { @@ -105,6 +107,7 @@ export class CallSubscriber { context: this._context, localOptimalVideoCountFeature: this._call.feature(Features.OptimalVideoCount) }); + /* @conditional-compile-remove(media-access) */ this._mediaAccessSubscriber = new MediaAccessSubscriber( this._callIdRef, this._context, @@ -246,6 +249,7 @@ export class CallSubscriber { this._breakoutRoomsSubscriber.unsubscribe(); /* @conditional-compile-remove(together-mode) */ this._togetherModeSubscriber.unsubscribe(); + /* @conditional-compile-remove(media-access) */ this._mediaAccessSubscriber.unsubscribe(); }; diff --git a/packages/calling-stateful-client/src/Converter.ts b/packages/calling-stateful-client/src/Converter.ts index 9ff994973c5..0a63122cc81 100644 --- a/packages/calling-stateful-client/src/Converter.ts +++ b/packages/calling-stateful-client/src/Converter.ts @@ -100,6 +100,7 @@ export function convertSdkParticipantToDeclarativeParticipant( raisedHand: undefined, role: participant.role, spotlight: undefined, + /* @conditional-compile-remove(media-access) */ mediaAccess: undefined }; } diff --git a/packages/calling-stateful-client/src/MediaAccessSubscriber.ts b/packages/calling-stateful-client/src/MediaAccessSubscriber.ts index aa525423bbd..5302832b662 100644 --- a/packages/calling-stateful-client/src/MediaAccessSubscriber.ts +++ b/packages/calling-stateful-client/src/MediaAccessSubscriber.ts @@ -5,6 +5,7 @@ import { MediaAccessCallFeature, MediaAccessChangedEvent } from '@azure/communic import { CallContext } from './CallContext'; import { CallIdRef } from './CallIdRef'; +/* @conditional-compile-remove(media-access) */ /** * @private */ diff --git a/packages/calling-stateful-client/src/index-public.ts b/packages/calling-stateful-client/src/index-public.ts index a24e73cd17d..6b3f4728bf5 100644 --- a/packages/calling-stateful-client/src/index-public.ts +++ b/packages/calling-stateful-client/src/index-public.ts @@ -55,4 +55,5 @@ export type { LocalRecordingCallFeatureState } from './CallClientState'; export type { ConferencePhoneInfo } from './CallClientState'; /* @conditional-compile-remove(breakout-rooms) */ export type { BreakoutRoomsState } from './CallClientState'; +/* @conditional-compile-remove(media-access) */ export type { MediaAccessState, MediaAccessCallFeatureState as MediaAccessCallFeature } from './CallClientState'; diff --git a/packages/communication-react/review/beta/communication-react.api.md b/packages/communication-react/review/beta/communication-react.api.md index b2403e3f1ca..636de7de8e1 100644 --- a/packages/communication-react/review/beta/communication-react.api.md +++ b/packages/communication-react/review/beta/communication-react.api.md @@ -1174,7 +1174,6 @@ export interface CallState { localParticipantReaction?: ReactionState; localRecording: LocalRecordingCallFeatureState; localVideoStreams: LocalVideoStreamState[]; - mediaAccess?: MediaAccessCallFeature; meetingConference?: { conferencePhones: ConferencePhoneInfo[]; }; diff --git a/packages/communication-react/src/index.ts b/packages/communication-react/src/index.ts index 3b9f36cdd58..8926f5e96cd 100644 --- a/packages/communication-react/src/index.ts +++ b/packages/communication-react/src/index.ts @@ -438,4 +438,5 @@ export type { ActiveNotification } from '../../react-components/src'; export type { MeetingConferencePhoneInfoModalStrings } from '../../react-components/src'; +/* @conditional-compile-remove(media-access) */ export type { MediaAccess } from '../../react-components/src'; diff --git a/packages/react-components/src/components/LocalVideoTile.tsx b/packages/react-components/src/components/LocalVideoTile.tsx index 851818c5a2f..d37858df204 100644 --- a/packages/react-components/src/components/LocalVideoTile.tsx +++ b/packages/react-components/src/components/LocalVideoTile.tsx @@ -6,7 +6,7 @@ import { concatStyleSets, IContextualMenuProps, Layer } from '@fluentui/react'; import { _formatString } from '@internal/acs-ui-common'; import React, { useMemo } from 'react'; import { KeyboardEvent, useCallback } from 'react'; -import { OnRenderAvatarCallback, VideoStreamOptions, CreateVideoStreamViewResult } from '../types'; +import { OnRenderAvatarCallback, VideoStreamOptions, CreateVideoStreamViewResult, MediaAccess } from '../types'; import { Reaction } from '../types'; import { LocalVideoCameraCycleButton, LocalVideoCameraCycleButtonProps } from './LocalVideoCameraButton'; import { StreamMedia } from './StreamMedia'; @@ -68,6 +68,7 @@ export const _LocalVideoTile = React.memo( strings?: VideoGalleryStrings; reactionResources?: ReactionResources; participantsCount?: number; + mediaAccess?: MediaAccess; }) => { const { isAvailable, @@ -97,7 +98,8 @@ export const _LocalVideoTile = React.memo( maxParticipantsToSpotlight, menuKind, strings, - reactionResources + reactionResources, + mediaAccess } = props; const theme = useTheme(); @@ -262,6 +264,7 @@ export const _LocalVideoTile = React.memo( ) } overlay={videoTileOverlay} + mediaAccess={mediaAccess} > {drawerMenuItemProps.length > 0 && ( diff --git a/packages/react-components/src/components/ParticipantList.tsx b/packages/react-components/src/components/ParticipantList.tsx index a344e9da2a4..aae84700e73 100644 --- a/packages/react-components/src/components/ParticipantList.tsx +++ b/packages/react-components/src/components/ParticipantList.tsx @@ -185,12 +185,8 @@ const onRenderParticipantDefault = ( ariaLabel={strings.sharingIconLabel} /> )} - {!callingParticipant.mediaAccess?.isAudioPermitted && ( - - )} - {callingParticipant.mediaAccess?.isAudioPermitted && callingParticipant.isMuted && ( - - )} + {getControlButtonMicProhibitedTrampoline(callingParticipant)} + {getParticipantItemMicOffTrampoline(callingParticipant)} {callingParticipant.spotlight && } {isPinned && } @@ -198,6 +194,30 @@ const onRenderParticipantDefault = ( ) : () => null; + const getControlButtonMicProhibitedTrampoline = (callingParticipant: CallParticipantListParticipant) => { + /* @conditional-compile-remove(media-access) */ + return ( + !callingParticipant.mediaAccess?.isAudioPermitted && ( + + ) + ); + + return <>; + }; + + const getParticipantItemMicOffTrampoline = (callingParticipant: CallParticipantListParticipant) => { + /* @conditional-compile-remove(media-access) */ + if (callingParticipant.mediaAccess?.isAudioPermitted && callingParticipant.isMuted) { + return ; + } + + if (callingParticipant.isMuted) { + return ; + } + + return <>; + }; + const onRenderAvatarWithRaiseHand = callingParticipant?.raisedHand && onRenderAvatar ? ( diff --git a/packages/react-components/src/components/RemoteVideoTile.tsx b/packages/react-components/src/components/RemoteVideoTile.tsx index c631ca2b26d..c4b893fa1ae 100644 --- a/packages/react-components/src/components/RemoteVideoTile.tsx +++ b/packages/react-components/src/components/RemoteVideoTile.tsx @@ -152,8 +152,8 @@ export const _RemoteVideoTile = React.memo( onStopSpotlight, maxParticipantsToSpotlight, /* @conditional-compile-remove(soft-mute) */ onMuteParticipant, - onForbidParticipantAudio, - onPermitParticipantAudio + /* @conditional-compile-remove(media-access) */ onForbidParticipantAudio, + /* @conditional-compile-remove(media-access) */ onPermitParticipantAudio }); const videoTileContextualMenuProps = useMemo(() => { @@ -259,6 +259,7 @@ export const _RemoteVideoTile = React.memo( } isSpotlighted={isSpotlighted} overlay={reactionOverlay} + mediaAccess={remoteParticipant.mediaAccess} /> {drawerMenuItemProps.length > 0 && ( diff --git a/packages/react-components/src/components/VideoGallery.tsx b/packages/react-components/src/components/VideoGallery.tsx index b81377017e1..d07390225d5 100644 --- a/packages/react-components/src/components/VideoGallery.tsx +++ b/packages/react-components/src/components/VideoGallery.tsx @@ -525,6 +525,7 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { strings={strings} reactionResources={reactionResources} participantsCount={remoteParticipants.length + 1} + mediaAccess={localParticipant.mediaAccess} /> ); diff --git a/packages/react-components/src/components/VideoGallery/useVideoTileContextualMenuProps.ts b/packages/react-components/src/components/VideoGallery/useVideoTileContextualMenuProps.ts index 0e8c05475a1..cc63b376bd0 100644 --- a/packages/react-components/src/components/VideoGallery/useVideoTileContextualMenuProps.ts +++ b/packages/react-components/src/components/VideoGallery/useVideoTileContextualMenuProps.ts @@ -181,7 +181,7 @@ export const useVideoTileContextualMenuProps = (props: { }); } } - + /* @conditional-compile-remove(media-access) */ if (!participant.mediaAccess?.isAudioPermitted && onPermitParticipantAudio) { items.push({ key: 'permitParticipantAudio', @@ -195,7 +195,7 @@ export const useVideoTileContextualMenuProps = (props: { ariaLabel: 'Unblock microphone' }); } - + /* @conditional-compile-remove(media-access) */ if (participant.mediaAccess?.isAudioPermitted && onForbidParticipantAudio) { items.push({ key: 'forbidParticipantAudio', diff --git a/packages/react-components/src/components/VideoTile.tsx b/packages/react-components/src/components/VideoTile.tsx index 1a00cd552c5..0a7e8a4bc1a 100644 --- a/packages/react-components/src/components/VideoTile.tsx +++ b/packages/react-components/src/components/VideoTile.tsx @@ -16,7 +16,7 @@ import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 're import { useIdentifiers } from '../identifiers'; import { ComponentLocale, useLocale } from '../localization'; import { useTheme } from '../theming'; -import { BaseCustomStyles, CustomAvatarOptions, OnRenderAvatarCallback } from '../types'; +import { BaseCustomStyles, CustomAvatarOptions, MediaAccess, OnRenderAvatarCallback } from '../types'; import { CallingTheme } from '../theming'; import { RaisedHand } from '../types'; import { RaisedHandIcon } from './assets/RaisedHandIcon'; @@ -175,6 +175,7 @@ export interface VideoTileProps { * Reactions resources' url and metadata. */ reactionResources?: ReactionResources; + mediaAccess?: MediaAccess; } // Coin max size is set to PersonaSize.size100 @@ -266,7 +267,8 @@ export const VideoTile = (props: VideoTileProps): JSX.Element => { raisedHand, personaMinSize = DEFAULT_PERSONA_MIN_SIZE_PX, personaMaxSize = DEFAULT_PERSONA_MAX_SIZE_PX, - contextualMenu + contextualMenu, + mediaAccess } = props; const [isHovered, setIsHovered] = useState(false); @@ -451,11 +453,16 @@ export const VideoTile = (props: VideoTileProps): JSX.Element => { {bracketedParticipantString(participantStateString, !!canShowLabel)} )} - {showMuteIndicator && isMuted && ( + {mediaAccess?.isAudioPermitted && showMuteIndicator && isMuted && ( )} + {!mediaAccess?.isAudioPermitted && showMuteIndicator && ( + + + + )} {isSpotlighted && ( diff --git a/packages/react-components/src/index.ts b/packages/react-components/src/index.ts index d9604faf5a8..d46d8a84794 100644 --- a/packages/react-components/src/index.ts +++ b/packages/react-components/src/index.ts @@ -67,7 +67,7 @@ export type { RaisedHand } from './types'; export type { Spotlight } from './types'; export type { Reaction, ReactionResources, ReactionSprite } from './types'; - +/* @conditional-compile-remove(media-access) */ export type { MediaAccess } from './types'; export type { diff --git a/packages/react-components/src/types/ParticipantListParticipant.ts b/packages/react-components/src/types/ParticipantListParticipant.ts index fc69c2b08ad..53ecf259c25 100644 --- a/packages/react-components/src/types/ParticipantListParticipant.ts +++ b/packages/react-components/src/types/ParticipantListParticipant.ts @@ -27,6 +27,7 @@ export type CallParticipantListParticipant = ParticipantListParticipant & { reaction?: Reaction; /** Whether calling participant is spotlighted **/ spotlight?: Spotlight; + /* @conditional-compile-remove(media-access) */ /** Whether calling participant has media access **/ mediaAccess?: MediaAccess; }; @@ -51,7 +52,7 @@ export type Spotlight = { export type RaisedHand = { raisedHandOrderPosition: number; }; - +/* @conditional-compile-remove(media-access) */ /** * Media access state with order * diff --git a/packages/react-components/src/types/VideoGalleryParticipant.ts b/packages/react-components/src/types/VideoGalleryParticipant.ts index 2aef4ca8662..f64871a5d76 100644 --- a/packages/react-components/src/types/VideoGalleryParticipant.ts +++ b/packages/react-components/src/types/VideoGalleryParticipant.ts @@ -1,7 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { MediaAccess, ParticipantState } from './ParticipantListParticipant'; +import { + ParticipantState, + /* @conditional-compile-remove(media-access) */ MediaAccess +} from './ParticipantListParticipant'; import { RaisedHand } from './ParticipantListParticipant'; import { Reaction } from './ParticipantListParticipant'; @@ -43,6 +46,8 @@ export type VideoGalleryParticipant = { isScreenSharingOn?: boolean; /** Whether participant is spotlighted **/ spotlight?: Spotlight; + /* @conditional-compile-remove(media-access) */ + /** audio video access states **/ mediaAccess?: MediaAccess; }; @@ -127,5 +132,6 @@ export interface VideoGalleryRemoteParticipant extends VideoGalleryParticipant { * @public * */ reaction?: Reaction; + /* @conditional-compile-remove(media-access) */ mediaAccess?: MediaAccess; } diff --git a/packages/react-composites/src/composites/CallComposite/MockCallAdapter.ts b/packages/react-composites/src/composites/CallComposite/MockCallAdapter.ts index 68e1fc7268a..1cff1480964 100644 --- a/packages/react-composites/src/composites/CallComposite/MockCallAdapter.ts +++ b/packages/react-composites/src/composites/CallComposite/MockCallAdapter.ts @@ -228,15 +228,19 @@ export class _MockCallAdapter implements CallAdapter { returnFromBreakoutRoom(): Promise { throw Error('returnFromBreakoutRoom not implemented'); } + /* @conditional-compile-remove(media-access) */ forbidParticipantAudio(userIds: string[]): Promise { throw Error('forbidParticipantAudio not implemented'); } + /* @conditional-compile-remove(media-access) */ permitParticipantAudio(userIds: string[]): Promise { throw Error('permitParticipantAudio not implemented'); } + /* @conditional-compile-remove(media-access) */ forbidAllAttendeesAudio(): Promise { throw Error('forbidAllAttendeesAudio not implemented'); } + /* @conditional-compile-remove(media-access) */ permitAllAttendeesAudio(): Promise { throw Error('permitAllAttendeesAudio not implemented'); } diff --git a/packages/react-composites/src/composites/CallComposite/Strings.tsx b/packages/react-composites/src/composites/CallComposite/Strings.tsx index 0d44de7f8d1..a3544f52489 100644 --- a/packages/react-composites/src/composites/CallComposite/Strings.tsx +++ b/packages/react-composites/src/composites/CallComposite/Strings.tsx @@ -894,16 +894,28 @@ export interface CallCompositeStrings { * notification. */ returnFromBreakoutRoomBannerButtonLabel: string; + /* @conditional-compile-remove(media-access) */ forbidParticipantAudioMenuLabel: string; + /* @conditional-compile-remove(media-access) */ permitParticipantAudioMenuLabel: string; + /* @conditional-compile-remove(media-access) */ forbidAllAttendeesAudioDialogTitle: string; + /* @conditional-compile-remove(media-access) */ forbidAllAttendeesAudioDialogContent: string; + /* @conditional-compile-remove(media-access) */ forbidAllAttendeesAudioConfirmButtonLabel: string; + /* @conditional-compile-remove(media-access) */ forbidAllAttendeesAudioCancelButtonLabel: string; + /* @conditional-compile-remove(media-access) */ permitAllAttendeesAudioDialogTitle: string; + /* @conditional-compile-remove(media-access) */ permitAllAttendeesAudioDialogContent: string; + /* @conditional-compile-remove(media-access) */ permitAllAttendeesAudioConfirmButtonLabel: string; + /* @conditional-compile-remove(media-access) */ permitAllAttendeesAudioCancelButtonLabel: string; + /* @conditional-compile-remove(media-access) */ forbidAllAttendeesAudioMenuLabel: string; + /* @conditional-compile-remove(media-access) */ permitAllAttendeesAudioMenuLabel: string; } diff --git a/packages/react-composites/src/composites/CallComposite/adapter/AzureCommunicationCallAdapter.ts b/packages/react-composites/src/composites/CallComposite/adapter/AzureCommunicationCallAdapter.ts index 010cc9d938f..bcb3868e3c0 100644 --- a/packages/react-composites/src/composites/CallComposite/adapter/AzureCommunicationCallAdapter.ts +++ b/packages/react-composites/src/composites/CallComposite/adapter/AzureCommunicationCallAdapter.ts @@ -1145,19 +1145,19 @@ export class AzureCommunicationCallAdapter { this.handlers.onStopAllSpotlight(); } - + /* @conditional-compile-remove(media-access) */ public async forbidParticipantAudio(userIds: string[]): Promise { this.handlers.onForbidParticipantAudio?.(userIds); } - + /* @conditional-compile-remove(media-access) */ public async permitParticipantAudio(userIds: string[]): Promise { this.handlers.onPermitParticipantAudio?.(userIds); } - + /* @conditional-compile-remove(media-access) */ public async forbidAllAttendeesAudio(): Promise { this.handlers.onForbidAllAttendeesAudio?.(); } - + /* @conditional-compile-remove(media-access) */ public async permitAllAttendeesAudio(): Promise { this.handlers.onPermitAllAttendeesAudio?.(); } diff --git a/packages/react-composites/src/composites/CallComposite/adapter/CallAdapter.ts b/packages/react-composites/src/composites/CallComposite/adapter/CallAdapter.ts index 1b33445a5fb..e24216f7630 100644 --- a/packages/react-composites/src/composites/CallComposite/adapter/CallAdapter.ts +++ b/packages/react-composites/src/composites/CallComposite/adapter/CallAdapter.ts @@ -783,10 +783,13 @@ export interface CallAdapterCallOperations { * Return to origin call of breakout room */ returnFromBreakoutRoom(): Promise; - + /* @conditional-compile-remove(media-access) */ forbidParticipantAudio(userIds: string[]): Promise; + /* @conditional-compile-remove(media-access) */ permitParticipantAudio(userIds: string[]): Promise; + /* @conditional-compile-remove(media-access) */ forbidAllAttendeesAudio(): Promise; + /* @conditional-compile-remove(media-access) */ permitAllAttendeesAudio(): Promise; } diff --git a/packages/react-composites/src/composites/CallComposite/components/CallArrangement.tsx b/packages/react-composites/src/composites/CallComposite/components/CallArrangement.tsx index bc54aa1aa1f..5155a21559c 100644 --- a/packages/react-composites/src/composites/CallComposite/components/CallArrangement.tsx +++ b/packages/react-composites/src/composites/CallComposite/components/CallArrangement.tsx @@ -333,7 +333,7 @@ export const CallArrangement = (props: CallArrangementProps): JSX.Element => { /* @conditional-compile-remove(soft-mute) */ capabilities?.muteOthers, /* @conditional-compile-remove(soft-mute) */ muteAllHandlers.onMuteAllRemoteParticipants ]); - + /* @conditional-compile-remove(media-access) */ const onToggleParticipantMicPeoplePaneProps = useMemo(() => { return { onForbidParticipantAudio: ['Unknown', 'Organizer', 'Presenter', 'Co-organizer'].includes(role ?? '') @@ -351,10 +351,10 @@ export const CallArrangement = (props: CallArrangementProps): JSX.Element => { }; }, [ role, - onForbidParticipantAudio, - onPermitParticipantAudio, - muteAllHandlers.onForbidAllAttendeesAudio, - muteAllHandlers.onPermitAllAttendeesAudio + /* @conditional-compile-remove(media-access) */ onForbidParticipantAudio, + /* @conditional-compile-remove(media-access) */ onPermitParticipantAudio, + /* @conditional-compile-remove(media-access) */ muteAllHandlers.onForbidAllAttendeesAudio, + /* @conditional-compile-remove(media-access) */ muteAllHandlers.onPermitAllAttendeesAudio ]); const spotlightPeoplePaneProps = useMemo(() => { @@ -384,7 +384,7 @@ export const CallArrangement = (props: CallArrangementProps): JSX.Element => { ...spotlightPeoplePaneProps, ...onMuteParticipantPeoplePaneProps, ...pinPeoplePaneProps, - ...onToggleParticipantMicPeoplePaneProps + /* @conditional-compile-remove(media-access) */ ...onToggleParticipantMicPeoplePaneProps }); const togglePeoplePane = useCallback(() => { if (isPeoplePaneOpen) { diff --git a/packages/react-composites/src/composites/CallComposite/components/SidePane/usePeoplePane.tsx b/packages/react-composites/src/composites/CallComposite/components/SidePane/usePeoplePane.tsx index 24a12d16a15..ee7a339da13 100644 --- a/packages/react-composites/src/composites/CallComposite/components/SidePane/usePeoplePane.tsx +++ b/packages/react-composites/src/composites/CallComposite/components/SidePane/usePeoplePane.tsx @@ -43,9 +43,13 @@ export const usePeoplePane = (props: { onPinParticipant?: (userId: string) => void; onUnpinParticipant?: (userId: string) => void; disablePinMenuItem?: boolean; + /* @conditional-compile-remove(media-access) */ onForbidParticipantAudio?: (userIds: string[]) => Promise; + /* @conditional-compile-remove(media-access) */ onPermitParticipantAudio?: (userIds: string[]) => Promise; + /* @conditional-compile-remove(media-access) */ onForbidAllAttendeesAudio?: () => Promise; + /* @conditional-compile-remove(media-access) */ onPermitAllAttendeesAudio?: () => Promise; }): { openPeoplePane: () => void; @@ -76,9 +80,13 @@ export const usePeoplePane = (props: { disablePinMenuItem, /* @conditional-compile-remove(soft-mute) */ onMuteAllRemoteParticipants, + /* @conditional-compile-remove(media-access) */ onForbidParticipantAudio, + /* @conditional-compile-remove(media-access) */ onPermitParticipantAudio, + /* @conditional-compile-remove(media-access) */ onForbidAllAttendeesAudio, + /* @conditional-compile-remove(media-access) */ onPermitAllAttendeesAudio } = props; @@ -155,7 +163,7 @@ export const usePeoplePane = (props: { disabled: isAllMuted }); } - + /* @conditional-compile-remove(media-access) */ if (onForbidAllAttendeesAudio && remoteParticipants) { let hasAttendee = false; if (remoteParticipants) { @@ -182,7 +190,7 @@ export const usePeoplePane = (props: { disabled: !hasAttendee }); } - + /* @conditional-compile-remove(media-access) */ if (onPermitAllAttendeesAudio && remoteParticipants) { let hasAttendee = false; if (remoteParticipants) { @@ -227,14 +235,14 @@ export const usePeoplePane = (props: { }, [ onMuteAllRemoteParticipants, remoteParticipants, - onForbidAllAttendeesAudio, - onPermitAllAttendeesAudio, onStopAllSpotlight, spotlightedParticipantUserIds, localeStrings.muteAllMenuLabel, - localeStrings.forbidAllAttendeesAudioMenuLabel, - localeStrings.permitAllAttendeesAudioMenuLabel, - localeStrings.stopAllSpotlightMenuLabel + /* @conditional-compile-remove(media-access) */ onForbidAllAttendeesAudio, + /* @conditional-compile-remove(media-access) */ onPermitAllAttendeesAudio, + /* @conditional-compile-remove(media-access) */ localeStrings.forbidAllAttendeesAudioMenuLabel, + /* @conditional-compile-remove(media-access) */ localeStrings.permitAllAttendeesAudioMenuLabel, + /* @conditional-compile-remove(media-access) */ localeStrings.stopAllSpotlightMenuLabel ]); const onRenderHeader = useCallback( @@ -364,7 +372,9 @@ export const usePeoplePane = (props: { ariaLabel: localeStrings.pinParticipantMenuItemAriaLabel }); } + /* @conditional-compile-remove(media-access) */ const remoteParticipant = remoteParticipants?.[participantId]; + /* @conditional-compile-remove(media-access) */ if ( !remoteParticipant?.mediaAccess?.isAudioPermitted && remoteParticipant?.role === 'Attendee' && @@ -384,7 +394,7 @@ export const usePeoplePane = (props: { ariaLabel: localeStrings.permitParticipantAudioMenuLabel }); } - + /* @conditional-compile-remove(media-access) */ if ( remoteParticipant?.mediaAccess?.isAudioPermitted && remoteParticipant?.role === 'Attendee' && @@ -460,6 +470,7 @@ export const usePeoplePane = (props: { /> } { + /* @conditional-compile-remove(media-access) */ } { + /* @conditional-compile-remove(media-access) */ => { // await adapter.permitParticipantAudio(userIds); // } + /* @conditional-compile-remove(media-access) */ onForbidParticipantAudio: capabilities?.forbidOthersMedia.isPresent ? async (userIds: string[]): Promise => { await adapter.forbidParticipantAudio(userIds); } : undefined, + /* @conditional-compile-remove(media-access) */ onPermitParticipantAudio: capabilities?.forbidOthersMedia.isPresent ? async (userIds: string[]): Promise => { await adapter.permitParticipantAudio(userIds); } : undefined, + /* @conditional-compile-remove(media-access) */ onForbidAllAttendeesAudio: capabilities?.forbidOthersMedia.isPresent ? async (): Promise => { await adapter.forbidAllAttendeesAudio(); } : undefined, + /* @conditional-compile-remove(media-access) */ onPermitAllAttendeesAudio: capabilities?.forbidOthersMedia.isPresent ? async (): Promise => { await adapter.permitAllAttendeesAudio(); diff --git a/packages/react-composites/src/composites/CallWithChatComposite/adapter/AzureCommunicationCallWithChatAdapter.ts b/packages/react-composites/src/composites/CallWithChatComposite/adapter/AzureCommunicationCallWithChatAdapter.ts index 6cafb38a566..0a0bd3622c9 100644 --- a/packages/react-composites/src/composites/CallWithChatComposite/adapter/AzureCommunicationCallWithChatAdapter.ts +++ b/packages/react-composites/src/composites/CallWithChatComposite/adapter/AzureCommunicationCallWithChatAdapter.ts @@ -711,19 +711,19 @@ export class AzureCommunicationCallWithChatAdapter implements CallWithChatAdapte await this.callAdapter.returnFromBreakoutRoom(); } } - + /* @conditional-compile-remove(media-access) */ public async forbidParticipantAudio(userIds: string[]): Promise { return this.callAdapter.forbidParticipantAudio(userIds); } - + /* @conditional-compile-remove(media-access) */ public async permitParticipantAudio(userIds: string[]): Promise { return this.callAdapter.permitParticipantAudio(userIds); } - + /* @conditional-compile-remove(media-access) */ public async forbidAllAttendeesAudio(): Promise { return this.callAdapter.forbidAllAttendeesAudio(); } - + /* @conditional-compile-remove(media-access) */ public async permitAllAttendeesAudio(): Promise { return this.callAdapter.permitAllAttendeesAudio(); } diff --git a/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatAdapter.ts b/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatAdapter.ts index baf08706032..abfc550e8cc 100644 --- a/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatAdapter.ts +++ b/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatAdapter.ts @@ -516,10 +516,13 @@ export interface CallWithChatAdapterManagement { * Return to origin call of breakout room */ returnFromBreakoutRoom(): Promise; - + /* @conditional-compile-remove(media-access) */ forbidParticipantAudio: (userIds: string[]) => Promise; + /* @conditional-compile-remove(media-access) */ permitParticipantAudio: (userIds: string[]) => Promise; + /* @conditional-compile-remove(media-access) */ forbidAllAttendeesAudio: () => Promise; + /* @conditional-compile-remove(media-access) */ permitAllAttendeesAudio: () => Promise; } diff --git a/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatBackedCallAdapter.ts b/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatBackedCallAdapter.ts index 8cc8872c384..ac46c6260d8 100644 --- a/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatBackedCallAdapter.ts +++ b/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatBackedCallAdapter.ts @@ -245,19 +245,19 @@ export class CallWithChatBackedCallAdapter implements CallAdapter { public async returnFromBreakoutRoom(): Promise { return this.callWithChatAdapter.returnFromBreakoutRoom(); } - + /* @conditional-compile-remove(media-access) */ public async forbidParticipantAudio(userIds: string[]): Promise { return this.callWithChatAdapter.forbidParticipantAudio(userIds); } - + /* @conditional-compile-remove(media-access) */ public async permitParticipantAudio(userIds: string[]): Promise { return this.callWithChatAdapter.permitParticipantAudio(userIds); } - + /* @conditional-compile-remove(media-access) */ public async forbidAllAttendeesAudio(): Promise { return this.callWithChatAdapter.forbidAllAttendeesAudio(); } - + /* @conditional-compile-remove(media-access) */ public async permitAllAttendeesAudio(): Promise { return this.callWithChatAdapter.permitAllAttendeesAudio(); } diff --git a/packages/react-composites/src/composites/common/Drawer/MoreDrawer.tsx b/packages/react-composites/src/composites/common/Drawer/MoreDrawer.tsx index 9f0d103e0aa..1a0baee0c32 100644 --- a/packages/react-composites/src/composites/common/Drawer/MoreDrawer.tsx +++ b/packages/react-composites/src/composites/common/Drawer/MoreDrawer.tsx @@ -154,8 +154,9 @@ export interface MoreDrawerProps extends MoreDrawerDevicesMenuProps { onClickMeetingPhoneInfo?: () => void; /* @conditional-compile-remove(soft-mute) */ onMuteAllRemoteParticipants?: () => void; - + /* @conditional-compile-remove(media-access) */ onForbidAllAttendeesAudio?: () => void; + /* @conditional-compile-remove(media-access) */ onPermitAllAttendeesAudio?: () => void; } From bc9c17a50e4c94cbe36cb6942217b19dfc8c980c Mon Sep 17 00:00:00 2001 From: fuyan Date: Wed, 16 Oct 2024 14:37:47 -0700 Subject: [PATCH 04/13] update --- .../src/utils/videoGalleryUtils.ts | 5 ++++- .../review/beta/communication-react.api.md | 2 ++ packages/react-components/src/components/ParticipantList.tsx | 4 ---- .../src/components/VideoGallery/LocalScreenShare.tsx | 1 + 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/calling-component-bindings/src/utils/videoGalleryUtils.ts b/packages/calling-component-bindings/src/utils/videoGalleryUtils.ts index 64966ddccf7..3578484c125 100644 --- a/packages/calling-component-bindings/src/utils/videoGalleryUtils.ts +++ b/packages/calling-component-bindings/src/utils/videoGalleryUtils.ts @@ -232,7 +232,10 @@ export const memoizeLocalParticipant = memoizeOne( raisedHand: raisedHand, reaction, spotlight: localSpotlight, - capabilities + mediaAccess: { + isAudioPermitted: capabilities?.unmuteMic.isPresent, + isVideoPermitted: capabilities?.turnVideoOn.isPresent + } }) ); diff --git a/packages/communication-react/review/beta/communication-react.api.md b/packages/communication-react/review/beta/communication-react.api.md index 41e244541c6..41eef1fe58c 100644 --- a/packages/communication-react/review/beta/communication-react.api.md +++ b/packages/communication-react/review/beta/communication-react.api.md @@ -5360,6 +5360,8 @@ export interface VideoTileProps { isPinned?: boolean; isSpeaking?: boolean; isSpotlighted?: boolean; + // (undocumented) + mediaAccess?: MediaAccess; noVideoAvailableAriaLabel?: string; onLongTouch?: () => void; onRenderPlaceholder?: OnRenderAvatarCallback; diff --git a/packages/react-components/src/components/ParticipantList.tsx b/packages/react-components/src/components/ParticipantList.tsx index aae84700e73..7edd053879f 100644 --- a/packages/react-components/src/components/ParticipantList.tsx +++ b/packages/react-components/src/components/ParticipantList.tsx @@ -211,10 +211,6 @@ const onRenderParticipantDefault = ( return ; } - if (callingParticipant.isMuted) { - return ; - } - return <>; }; diff --git a/packages/react-components/src/components/VideoGallery/LocalScreenShare.tsx b/packages/react-components/src/components/VideoGallery/LocalScreenShare.tsx index 32e8d4b41d7..d87d7853330 100644 --- a/packages/react-components/src/components/VideoGallery/LocalScreenShare.tsx +++ b/packages/react-components/src/components/VideoGallery/LocalScreenShare.tsx @@ -61,6 +61,7 @@ export const LocalScreenShare = React.memo( ) : undefined } onRenderPlaceholder={() => } + mediaAccess={localParticipant.mediaAccess} /> ); } From 2c7310d70769686b45965996ce876151b3bf29e0 Mon Sep 17 00:00:00 2001 From: fuyan Date: Wed, 16 Oct 2024 22:17:19 -0700 Subject: [PATCH 05/13] update --- .../src/handlers/createCommonHandlers.ts | 38 +++- .../review/beta/communication-react.api.md | 58 ++++++ .../src/components/ParticipantList.tsx | 28 ++- .../src/components/RemoteVideoTile.tsx | 10 +- .../src/components/VideoGallery.tsx | 12 +- .../useVideoTileContextualMenuProps.ts | 89 ++++++-- .../src/components/VideoTile.tsx | 5 + .../localization/locales/en-US/strings.json | 4 +- .../CallComposite/MockCallAdapter.ts | 16 ++ .../src/composites/CallComposite/Strings.tsx | 25 +++ .../adapter/AzureCommunicationCallAdapter.ts | 17 ++ .../CallComposite/adapter/CallAdapter.ts | 8 + .../components/CallArrangement.tsx | 28 ++- .../components/SidePane/usePeoplePane.tsx | 196 ++++++++++++++++-- .../CallComposite/hooks/useHandlers.ts | 24 +++ .../AzureCommunicationCallWithChatAdapter.ts | 17 ++ .../adapter/CallWithChatAdapter.ts | 8 + .../adapter/CallWithChatBackedCallAdapter.ts | 16 ++ .../composites/common/Drawer/MoreDrawer.tsx | 4 + .../localization/locales/en-US/strings.json | 14 +- 20 files changed, 564 insertions(+), 53 deletions(-) diff --git a/packages/calling-component-bindings/src/handlers/createCommonHandlers.ts b/packages/calling-component-bindings/src/handlers/createCommonHandlers.ts index 03a16f2aba5..00e61997c70 100644 --- a/packages/calling-component-bindings/src/handlers/createCommonHandlers.ts +++ b/packages/calling-component-bindings/src/handlers/createCommonHandlers.ts @@ -114,6 +114,15 @@ export interface CommonCallingHandlers { onForbidAllAttendeesAudio?: () => Promise; /* @conditional-compile-remove(media-access) */ onPermitAllAttendeesAudio?: () => Promise; + + /* @conditional-compile-remove(media-access) */ + onForbidParticipantVideo?: (userIds: string[]) => Promise; + /* @conditional-compile-remove(media-access) */ + onPermitParticipantVideo?: (userIds: string[]) => Promise; + /* @conditional-compile-remove(media-access) */ + onForbidAllAttendeesVideo?: () => Promise; + /* @conditional-compile-remove(media-access) */ + onPermitAllAttendeesVideo?: () => Promise; } /** @@ -762,6 +771,25 @@ export const createDefaultCommonCallingHandlers = memoizeOne( await call?.feature(Features.MediaAccess).permitOthersAudio(); }; + /* @conditional-compile-remove(media-access) */ + const onForbidParticipantVideo = async (userIds: string[]): Promise => { + const participants = userIds?.map((userId) => _toCommunicationIdentifier(userId)); + await call?.feature(Features.MediaAccess).forbidVideo(participants); + }; + /* @conditional-compile-remove(media-access) */ + const onPermitParticipantVideo = async (userIds: string[]): Promise => { + const participants = userIds?.map((userId) => _toCommunicationIdentifier(userId)); + await call?.feature(Features.MediaAccess).permitVideo(participants); + }; + /* @conditional-compile-remove(media-access) */ + const onForbidAllAttendeesVideo = async (): Promise => { + await call?.feature(Features.MediaAccess).forbidOthersVideo(); + }; + /* @conditional-compile-remove(media-access) */ + const onPermitAllAttendeesVideo = async (): Promise => { + await call?.feature(Features.MediaAccess).permitOthersVideo(); + }; + // const onForbidAllAttendeesAudio = canForbidOthersMedia // ? async (): Promise => { // await call?.feature(Features.MediaAccess).forbidOthersAudio(); @@ -835,7 +863,15 @@ export const createDefaultCommonCallingHandlers = memoizeOne( /* @conditional-compile-remove(media-access) */ onForbidAllAttendeesAudio, /* @conditional-compile-remove(media-access) */ - onPermitAllAttendeesAudio + onPermitAllAttendeesAudio, + /* @conditional-compile-remove(media-access) */ + onForbidParticipantVideo, + /* @conditional-compile-remove(media-access) */ + onPermitParticipantVideo, + /* @conditional-compile-remove(media-access) */ + onForbidAllAttendeesVideo, + /* @conditional-compile-remove(media-access) */ + onPermitAllAttendeesVideo }; } ); diff --git a/packages/communication-react/review/beta/communication-react.api.md b/packages/communication-react/review/beta/communication-react.api.md index 41eef1fe58c..6233c253f35 100644 --- a/packages/communication-react/review/beta/communication-react.api.md +++ b/packages/communication-react/review/beta/communication-react.api.md @@ -451,7 +451,11 @@ export interface CallAdapterCallOperations { // (undocumented) forbidAllAttendeesAudio(): Promise; // (undocumented) + forbidAllAttendeesVideo(): Promise; + // (undocumented) forbidParticipantAudio(userIds: string[]): Promise; + // (undocumented) + forbidParticipantVideo(userIds: string[]): Promise; holdCall(): Promise; leaveCall(forEveryone?: boolean): Promise; lowerHand(): Promise; @@ -462,7 +466,11 @@ export interface CallAdapterCallOperations { // (undocumented) permitAllAttendeesAudio(): Promise; // (undocumented) + permitAllAttendeesVideo(): Promise; + // (undocumented) permitParticipantAudio(userIds: string[]): Promise; + // (undocumented) + permitParticipantVideo(userIds: string[]): Promise; raiseHand(): Promise; removeParticipant(userId: string): Promise; removeParticipant(participant: CommunicationIdentifier): Promise; @@ -882,7 +890,19 @@ export interface CallCompositeStrings { // (undocumented) forbidAllAttendeesAudioMenuLabel: string; // (undocumented) + forbidAllAttendeesVideoCancelButtonLabel: string; + // (undocumented) + forbidAllAttendeesVideoConfirmButtonLabel: string; + // (undocumented) + forbidAllAttendeesVideoDialogContent: string; + // (undocumented) + forbidAllAttendeesVideoDialogTitle: string; + // (undocumented) + forbidAllAttendeesVideoMenuLabel: string; + // (undocumented) forbidParticipantAudioMenuLabel: string; + // (undocumented) + forbidParticipantVideoMenuLabel: string; hangUpCancelButtonLabel?: string; holdScreenLabel?: string; invalidMeetingIdentifier: string; @@ -960,7 +980,19 @@ export interface CallCompositeStrings { // (undocumented) permitAllAttendeesAudioMenuLabel: string; // (undocumented) + permitAllAttendeesVideoCancelButtonLabel: string; + // (undocumented) + permitAllAttendeesVideoConfirmButtonLabel: string; + // (undocumented) + permitAllAttendeesVideoDialogContent: string; + // (undocumented) + permitAllAttendeesVideoDialogTitle: string; + // (undocumented) + permitAllAttendeesVideoMenuLabel: string; + // (undocumented) permitParticipantAudioMenuLabel: string; + // (undocumented) + permitParticipantVideoMenuLabel: string; phoneCallMoreButtonLabel: string; pinParticipantLimitReachedMenuLabel: string; pinParticipantMenuItemAriaLabel: string; @@ -1241,7 +1273,11 @@ export interface CallWithChatAdapterManagement { // (undocumented) forbidAllAttendeesAudio: () => Promise; // (undocumented) + forbidAllAttendeesVideo: () => Promise; + // (undocumented) forbidParticipantAudio: (userIds: string[]) => Promise; + // (undocumented) + forbidParticipantVideo: (userIds: string[]) => Promise; holdCall(): Promise; // @deprecated joinCall(microphoneOn?: boolean): Call | undefined; @@ -1256,7 +1292,11 @@ export interface CallWithChatAdapterManagement { // (undocumented) permitAllAttendeesAudio: () => Promise; // (undocumented) + permitAllAttendeesVideo: () => Promise; + // (undocumented) permitParticipantAudio: (userIds: string[]) => Promise; + // (undocumented) + permitParticipantVideo: (userIds: string[]) => Promise; queryCameras(): Promise; queryMicrophones(): Promise; querySpeakers(): Promise; @@ -2228,8 +2268,12 @@ export interface CommonCallingHandlers { // (undocumented) onForbidAllAttendeesAudio?: () => Promise; // (undocumented) + onForbidAllAttendeesVideo?: () => Promise; + // (undocumented) onForbidParticipantAudio?: (userIds: string[]) => Promise; // (undocumented) + onForbidParticipantVideo?: (userIds: string[]) => Promise; + // (undocumented) onHangUp: (forEveryone?: boolean) => Promise; // (undocumented) onLowerHand: () => Promise; @@ -2240,8 +2284,12 @@ export interface CommonCallingHandlers { // (undocumented) onPermitAllAttendeesAudio?: () => Promise; // (undocumented) + onPermitAllAttendeesVideo?: () => Promise; + // (undocumented) onPermitParticipantAudio?: (userIds: string[]) => Promise; // (undocumented) + onPermitParticipantVideo?: (userIds: string[]) => Promise; + // (undocumented) onRaiseHand: () => Promise; // (undocumented) onReactionClick: (reaction: Reaction_2) => Promise; @@ -4156,6 +4204,8 @@ export type ParticipantListProps = { pinnedParticipants?: string[]; onForbidParticipantAudio?: (userIds: string[]) => Promise; onPermitParticipantAudio?: (userIds: string[]) => Promise; + onForbidParticipantVideo?: (userIds: string[]) => Promise; + onPermitParticipantVideo?: (userIds: string[]) => Promise; }; // @public @@ -5218,9 +5268,13 @@ export interface VideoGalleryProps { onDisposeRemoteVideoStreamView?: (userId: string) => Promise; // (undocumented) onForbidParticipantAudio?: (userIds: string[]) => Promise; + // (undocumented) + onForbidParticipantVideo?: (userIds: string[]) => Promise; onMuteParticipant?: (userId: string) => Promise; // (undocumented) onPermitParticipantAudio?: (userIds: string[]) => Promise; + // (undocumented) + onPermitParticipantVideo?: (userIds: string[]) => Promise; onPinParticipant?: (userId: string) => void; onRenderAvatar?: OnRenderAvatarCallback; onRenderLocalVideoTile?: (localParticipant: VideoGalleryLocalParticipant) => JSX.Element; @@ -5289,6 +5343,8 @@ export interface VideoGalleryStrings { fitRemoteParticipantToFrame: string; // (undocumented) forbidParticipantAudio: string; + // (undocumented) + forbidParticipantVideo: string; localScreenShareLoadingMessage: string; localVideoCameraSwitcherLabel: string; localVideoLabel: string; @@ -5298,6 +5354,8 @@ export interface VideoGalleryStrings { muteParticipantMenuItemLabel: string; // (undocumented) permitParticipantAudio: string; + // (undocumented) + permitParticipantVideo: string; pinnedParticipantAnnouncementAriaLabel: string; pinParticipantForMe: string; pinParticipantMenuItemAriaLabel: string; diff --git a/packages/react-components/src/components/ParticipantList.tsx b/packages/react-components/src/components/ParticipantList.tsx index 7edd053879f..771b5ff7fcf 100644 --- a/packages/react-components/src/components/ParticipantList.tsx +++ b/packages/react-components/src/components/ParticipantList.tsx @@ -117,6 +117,8 @@ export type ParticipantListProps = { onForbidParticipantAudio?: (userIds: string[]) => Promise; onPermitParticipantAudio?: (userIds: string[]) => Promise; + onForbidParticipantVideo?: (userIds: string[]) => Promise; + onPermitParticipantVideo?: (userIds: string[]) => Promise; }; const onRenderParticipantDefault = ( @@ -185,6 +187,7 @@ const onRenderParticipantDefault = ( ariaLabel={strings.sharingIconLabel} /> )} + {getParticipantItemCameraProhibitedTrampoline(callingParticipant)} {getControlButtonMicProhibitedTrampoline(callingParticipant)} {getParticipantItemMicOffTrampoline(callingParticipant)} {callingParticipant.spotlight && } @@ -194,18 +197,16 @@ const onRenderParticipantDefault = ( ) : () => null; - const getControlButtonMicProhibitedTrampoline = (callingParticipant: CallParticipantListParticipant) => { + const getControlButtonMicProhibitedTrampoline = (callingParticipant: CallParticipantListParticipant): JSX.Element => { /* @conditional-compile-remove(media-access) */ - return ( - !callingParticipant.mediaAccess?.isAudioPermitted && ( - - ) - ); + if (!callingParticipant.mediaAccess?.isAudioPermitted) { + return ; + } return <>; }; - const getParticipantItemMicOffTrampoline = (callingParticipant: CallParticipantListParticipant) => { + const getParticipantItemMicOffTrampoline = (callingParticipant: CallParticipantListParticipant): JSX.Element => { /* @conditional-compile-remove(media-access) */ if (callingParticipant.mediaAccess?.isAudioPermitted && callingParticipant.isMuted) { return ; @@ -214,6 +215,19 @@ const onRenderParticipantDefault = ( return <>; }; + const getParticipantItemCameraProhibitedTrampoline = ( + callingParticipant: CallParticipantListParticipant + ): JSX.Element => { + /* @conditional-compile-remove(media-access) */ + if (!callingParticipant.mediaAccess?.isVideoPermitted) { + return ( + + ); + } + + return <>; + }; + const onRenderAvatarWithRaiseHand = callingParticipant?.raisedHand && onRenderAvatar ? ( diff --git a/packages/react-components/src/components/RemoteVideoTile.tsx b/packages/react-components/src/components/RemoteVideoTile.tsx index c4b893fa1ae..b7cc1469acd 100644 --- a/packages/react-components/src/components/RemoteVideoTile.tsx +++ b/packages/react-components/src/components/RemoteVideoTile.tsx @@ -73,6 +73,8 @@ export const _RemoteVideoTile = React.memo( onLongTouch?: (() => void) | undefined; onForbidParticipantAudio?: (userIds: string[]) => Promise; onPermitParticipantAudio?: (userIds: string[]) => Promise; + onForbidParticipantVideo?: (userIds: string[]) => Promise; + onPermitParticipantVideo?: (userIds: string[]) => Promise; }) => { const { isAvailable, @@ -104,7 +106,9 @@ export const _RemoteVideoTile = React.memo( reactionResources, streamId, onForbidParticipantAudio, - onPermitParticipantAudio + onPermitParticipantAudio, + onForbidParticipantVideo, + onPermitParticipantVideo } = props; const remoteVideoStreamProps: RemoteVideoStreamLifecycleMaintainerProps = useMemo( @@ -153,7 +157,9 @@ export const _RemoteVideoTile = React.memo( maxParticipantsToSpotlight, /* @conditional-compile-remove(soft-mute) */ onMuteParticipant, /* @conditional-compile-remove(media-access) */ onForbidParticipantAudio, - /* @conditional-compile-remove(media-access) */ onPermitParticipantAudio + /* @conditional-compile-remove(media-access) */ onPermitParticipantAudio, + /* @conditional-compile-remove(media-access) */ onForbidParticipantVideo, + /* @conditional-compile-remove(media-access) */ onPermitParticipantVideo }); const videoTileContextualMenuProps = useMemo(() => { diff --git a/packages/react-components/src/components/VideoGallery.tsx b/packages/react-components/src/components/VideoGallery.tsx index b0ff051f4fb..b339e6cebfb 100644 --- a/packages/react-components/src/components/VideoGallery.tsx +++ b/packages/react-components/src/components/VideoGallery.tsx @@ -135,6 +135,8 @@ export interface VideoGalleryStrings { waitingScreenText: string; forbidParticipantAudio: string; permitParticipantAudio: string; + forbidParticipantVideo: string; + permitParticipantVideo: string; } /** @@ -319,6 +321,8 @@ export interface VideoGalleryProps { onMuteParticipant?: (userId: string) => Promise; onForbidParticipantAudio?: (userIds: string[]) => Promise; onPermitParticipantAudio?: (userIds: string[]) => Promise; + onForbidParticipantVideo?: (userIds: string[]) => Promise; + onPermitParticipantVideo?: (userIds: string[]) => Promise; } /** @@ -406,7 +410,9 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { /* @conditional-compile-remove(soft-mute) */ onMuteParticipant, onForbidParticipantAudio, - onPermitParticipantAudio + onPermitParticipantAudio, + onForbidParticipantVideo, + onPermitParticipantVideo } = props; const ids = useIdentifiers(); @@ -660,6 +666,8 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { onMuteParticipant={onMuteParticipant} onForbidParticipantAudio={onForbidParticipantAudio} onPermitParticipantAudio={onPermitParticipantAudio} + onForbidParticipantVideo={onForbidParticipantVideo} + onPermitParticipantVideo={onPermitParticipantVideo} /> ); }, @@ -687,6 +695,8 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { onMuteParticipant, onForbidParticipantAudio, onPermitParticipantAudio, + onForbidParticipantVideo, + onPermitParticipantVideo, remoteVideoViewOptions ] ); diff --git a/packages/react-components/src/components/VideoGallery/useVideoTileContextualMenuProps.ts b/packages/react-components/src/components/VideoGallery/useVideoTileContextualMenuProps.ts index cc63b376bd0..a6e69de1b5a 100644 --- a/packages/react-components/src/components/VideoGallery/useVideoTileContextualMenuProps.ts +++ b/packages/react-components/src/components/VideoGallery/useVideoTileContextualMenuProps.ts @@ -31,8 +31,12 @@ export const useVideoTileContextualMenuProps = (props: { muteParticipantMenuItemLabel?: string; forbidParticipantAudio?: string; permitParticipantAudio?: string; + forbidParticipantVideo?: string; + permitParticipantVideo?: string; forbidParticipantAudioTileMenuLabel?: string; permitParticipantAudioTileMenuLabel?: string; + forbidParticipantVideoTileMenuLabel?: string; + permitParticipantVideoTileMenuLabel?: string; }; view?: { updateScalingMode: (scalingMode: ViewScalingMode) => Promise }; isPinned?: boolean; @@ -51,6 +55,8 @@ export const useVideoTileContextualMenuProps = (props: { onMuteParticipant?: (userId: string) => void; onForbidParticipantAudio?: (userIds: string[]) => void; onPermitParticipantAudio?: (userIds: string[]) => void; + onForbidParticipantVideo?: (userIds: string[]) => void; + onPermitParticipantVideo?: (userIds: string[]) => void; }): IContextualMenuProps | undefined => { const { participant, @@ -70,7 +76,9 @@ export const useVideoTileContextualMenuProps = (props: { myUserId, /* @conditional-compile-remove(soft-mute) */ onMuteParticipant, onForbidParticipantAudio, - onPermitParticipantAudio + onPermitParticipantAudio, + onForbidParticipantVideo, + onPermitParticipantVideo } = props; const scalingMode = useMemo(() => { return props.participant.videoStream?.scalingMode; @@ -191,7 +199,7 @@ export const useVideoTileContextualMenuProps = (props: { styles: { root: { lineHeight: 0 } } }, onClick: () => onPermitParticipantAudio([participant.userId]), - 'data-ui-id': 'video-tile-unblock-microphone', + 'data-ui-id': 'audio-tile-unblock-microphone', ariaLabel: 'Unblock microphone' }); } @@ -205,6 +213,35 @@ export const useVideoTileContextualMenuProps = (props: { styles: { root: { lineHeight: 0 } } }, onClick: () => onForbidParticipantAudio([participant.userId]), + 'data-ui-id': 'audio-tile-block-microphone', + ariaLabel: 'Block microphone' + }); + } + + /* @conditional-compile-remove(media-access) */ + if (!participant.mediaAccess?.isVideoPermitted && onPermitParticipantVideo) { + items.push({ + key: 'permitParticipantVideo', + text: strings?.permitParticipantVideoTileMenuLabel, + iconProps: { + iconName: 'Microphone', + styles: { root: { lineHeight: 0 } } + }, + onClick: () => onPermitParticipantVideo([participant.userId]), + 'data-ui-id': 'video-tile-unblock-microphone', + ariaLabel: 'Unblock microphone' + }); + } + /* @conditional-compile-remove(media-access) */ + if (participant.mediaAccess?.isVideoPermitted && onForbidParticipantVideo) { + items.push({ + key: 'forbidParticipantVideo', + text: strings?.forbidParticipantVideoTileMenuLabel, + iconProps: { + iconName: 'ControlButtonMicProhibited', + styles: { root: { lineHeight: 0 } } + }, + onClick: () => onForbidParticipantVideo([participant.userId]), 'data-ui-id': 'video-tile-block-microphone', ariaLabel: 'Block microphone' }); @@ -249,25 +286,47 @@ export const useVideoTileContextualMenuProps = (props: { return { items, styles: {}, calloutProps: { preventDismissOnEvent } }; }, [ - scalingMode, - strings, - view, + onMuteParticipant, + strings?.muteParticipantMenuItemLabel, + strings?.unpinParticipantForMe, + strings?.pinParticipantForMe, + strings?.unpinParticipantMenuItemAriaLabel, + strings?.pinnedParticipantAnnouncementAriaLabel, + strings?.pinParticipantForMeLimitReached, + strings?.stopSpotlightOnSelfVideoTileMenuLabel, + strings?.stopSpotlightVideoTileMenuLabel, + strings?.addSpotlightVideoTileMenuLabel, + strings?.startSpotlightVideoTileMenuLabel, + strings?.spotlightLimitReachedMenuTitle, + strings?.permitParticipantAudioTileMenuLabel, + strings?.forbidParticipantAudioTileMenuLabel, + strings?.permitParticipantVideoTileMenuLabel, + strings?.forbidParticipantVideoTileMenuLabel, + strings?.fitRemoteParticipantToFrame, + strings?.fillRemoteParticipantFrame, isPinned, - onPinParticipant, - onUnpinParticipant, - onUpdateScalingMode, + isSpotlighted, + participant.mediaAccess?.isAudioPermitted, + participant.mediaAccess?.isVideoPermitted, + participant.isMuted, participant.userId, participant.displayName, - disablePinMenuItem, + onPermitParticipantAudio, + onForbidParticipantAudio, + onPermitParticipantVideo, + onForbidParticipantVideo, + scalingMode, + onUnpinParticipant, + onPinParticipant, toggleAnnouncerString, - spotlightedParticipantUserIds, - isSpotlighted, - onStartSpotlight, + disablePinMenuItem, + myUserId, onStopSpotlight, + spotlightedParticipantUserIds, maxParticipantsToSpotlight, - myUserId, - /* @conditional-compile-remove(soft-mute) */ onMuteParticipant, - /* @conditional-compile-remove(soft-mute) */ participant.isMuted + onStartSpotlight, + onUpdateScalingMode, + view ]); return contextualMenuProps; diff --git a/packages/react-components/src/components/VideoTile.tsx b/packages/react-components/src/components/VideoTile.tsx index 0a7e8a4bc1a..5ec4c615743 100644 --- a/packages/react-components/src/components/VideoTile.tsx +++ b/packages/react-components/src/components/VideoTile.tsx @@ -453,6 +453,11 @@ export const VideoTile = (props: VideoTileProps): JSX.Element => { {bracketedParticipantString(participantStateString, !!canShowLabel)} )} + {!mediaAccess?.isVideoPermitted && ( + + + + )} {mediaAccess?.isAudioPermitted && showMuteIndicator && isMuted && ( diff --git a/packages/react-components/src/localization/locales/en-US/strings.json b/packages/react-components/src/localization/locales/en-US/strings.json index 11eae6c7363..19ec4cacb95 100644 --- a/packages/react-components/src/localization/locales/en-US/strings.json +++ b/packages/react-components/src/localization/locales/en-US/strings.json @@ -516,7 +516,9 @@ "forbidParticipantAudioTileMenuLabel": "Disable mic", "permitParticipantAudioTileMenuLabel": "Allow mic", "forbidParticipantVideo": "Disable camera", - "permitParticipantVideo": "Allow camera" + "permitParticipantVideo": "Allow camera", + "forbidParticipantVideoTileMenuLabel": "Disable camera", + "permitParticipantVideoTileMenuLabel": "Allow camera" }, "dialpad": { "placeholderText": "Enter phone number", diff --git a/packages/react-composites/src/composites/CallComposite/MockCallAdapter.ts b/packages/react-composites/src/composites/CallComposite/MockCallAdapter.ts index 856f9ca7466..fea1a694b0e 100644 --- a/packages/react-composites/src/composites/CallComposite/MockCallAdapter.ts +++ b/packages/react-composites/src/composites/CallComposite/MockCallAdapter.ts @@ -245,6 +245,22 @@ export class _MockCallAdapter implements CallAdapter { permitAllAttendeesAudio(): Promise { throw Error('permitAllAttendeesAudio not implemented'); } + /* @conditional-compile-remove(media-access) */ + forbidParticipantVideo(userIds: string[]): Promise { + throw Error('forbidParticipantAudio not implemented'); + } + /* @conditional-compile-remove(media-access) */ + permitParticipantVideo(userIds: string[]): Promise { + throw Error('permitParticipantAudio not implemented'); + } + /* @conditional-compile-remove(media-access) */ + forbidAllAttendeesVideo(): Promise { + throw Error('forbidAllAttendeesAudio not implemented'); + } + /* @conditional-compile-remove(media-access) */ + permitAllAttendeesVideo(): Promise { + throw Error('permitAllAttendeesAudio not implemented'); + } } /** diff --git a/packages/react-composites/src/composites/CallComposite/Strings.tsx b/packages/react-composites/src/composites/CallComposite/Strings.tsx index a3544f52489..6d47d43be9e 100644 --- a/packages/react-composites/src/composites/CallComposite/Strings.tsx +++ b/packages/react-composites/src/composites/CallComposite/Strings.tsx @@ -918,4 +918,29 @@ export interface CallCompositeStrings { forbidAllAttendeesAudioMenuLabel: string; /* @conditional-compile-remove(media-access) */ permitAllAttendeesAudioMenuLabel: string; + + /* @conditional-compile-remove(media-access) */ + forbidParticipantVideoMenuLabel: string; + /* @conditional-compile-remove(media-access) */ + permitParticipantVideoMenuLabel: string; + /* @conditional-compile-remove(media-access) */ + forbidAllAttendeesVideoDialogTitle: string; + /* @conditional-compile-remove(media-access) */ + forbidAllAttendeesVideoDialogContent: string; + /* @conditional-compile-remove(media-access) */ + forbidAllAttendeesVideoConfirmButtonLabel: string; + /* @conditional-compile-remove(media-access) */ + forbidAllAttendeesVideoCancelButtonLabel: string; + /* @conditional-compile-remove(media-access) */ + permitAllAttendeesVideoDialogTitle: string; + /* @conditional-compile-remove(media-access) */ + permitAllAttendeesVideoDialogContent: string; + /* @conditional-compile-remove(media-access) */ + permitAllAttendeesVideoConfirmButtonLabel: string; + /* @conditional-compile-remove(media-access) */ + permitAllAttendeesVideoCancelButtonLabel: string; + /* @conditional-compile-remove(media-access) */ + forbidAllAttendeesVideoMenuLabel: string; + /* @conditional-compile-remove(media-access) */ + permitAllAttendeesVideoMenuLabel: string; } diff --git a/packages/react-composites/src/composites/CallComposite/adapter/AzureCommunicationCallAdapter.ts b/packages/react-composites/src/composites/CallComposite/adapter/AzureCommunicationCallAdapter.ts index 0281b914151..6b81107708b 100644 --- a/packages/react-composites/src/composites/CallComposite/adapter/AzureCommunicationCallAdapter.ts +++ b/packages/react-composites/src/composites/CallComposite/adapter/AzureCommunicationCallAdapter.ts @@ -1183,6 +1183,23 @@ export class AzureCommunicationCallAdapter { + this.handlers.onForbidParticipantVideo?.(userIds); + } + /* @conditional-compile-remove(media-access) */ + public async permitParticipantVideo(userIds: string[]): Promise { + this.handlers.onPermitParticipantVideo?.(userIds); + } + /* @conditional-compile-remove(media-access) */ + public async forbidAllAttendeesVideo(): Promise { + this.handlers.onForbidAllAttendeesVideo?.(); + } + /* @conditional-compile-remove(media-access) */ + public async permitAllAttendeesVideo(): Promise { + this.handlers.onPermitAllAttendeesVideo?.(); + } + /* @conditional-compile-remove(breakout-rooms) */ public async returnFromBreakoutRoom(): Promise { if (!this.originCall) { diff --git a/packages/react-composites/src/composites/CallComposite/adapter/CallAdapter.ts b/packages/react-composites/src/composites/CallComposite/adapter/CallAdapter.ts index 1c8c2044e20..13c481667dd 100644 --- a/packages/react-composites/src/composites/CallComposite/adapter/CallAdapter.ts +++ b/packages/react-composites/src/composites/CallComposite/adapter/CallAdapter.ts @@ -791,6 +791,14 @@ export interface CallAdapterCallOperations { forbidAllAttendeesAudio(): Promise; /* @conditional-compile-remove(media-access) */ permitAllAttendeesAudio(): Promise; + /* @conditional-compile-remove(media-access) */ + forbidParticipantVideo(userIds: string[]): Promise; + /* @conditional-compile-remove(media-access) */ + permitParticipantVideo(userIds: string[]): Promise; + /* @conditional-compile-remove(media-access) */ + forbidAllAttendeesVideo(): Promise; + /* @conditional-compile-remove(media-access) */ + permitAllAttendeesVideo(): Promise; } /** diff --git a/packages/react-composites/src/composites/CallComposite/components/CallArrangement.tsx b/packages/react-composites/src/composites/CallComposite/components/CallArrangement.tsx index 5155a21559c..20237ff6e33 100644 --- a/packages/react-composites/src/composites/CallComposite/components/CallArrangement.tsx +++ b/packages/react-composites/src/composites/CallComposite/components/CallArrangement.tsx @@ -235,7 +235,9 @@ export const CallArrangement = (props: CallArrangementProps): JSX.Element => { maxParticipantsToSpotlight, localParticipant, onForbidParticipantAudio, - onPermitParticipantAudio + onPermitParticipantAudio, + onForbidParticipantVideo, + onPermitParticipantVideo } = videoGalleryProps; const [showTeamsMeetingConferenceModal, setShowTeamsMeetingConferenceModal] = useState(false); @@ -347,14 +349,30 @@ export const CallArrangement = (props: CallArrangementProps): JSX.Element => { : undefined, onPermitAllAttendeesAudio: ['Unknown', 'Organizer', 'Presenter', 'Co-organizer'].includes(role ?? '') ? muteAllHandlers.onPermitAllAttendeesAudio + : undefined, + onForbidParticipantVideo: ['Unknown', 'Organizer', 'Presenter', 'Co-organizer'].includes(role ?? '') + ? onForbidParticipantVideo + : undefined, + onPermitParticipantVideo: ['Unknown', 'Organizer', 'Presenter', 'Co-organizer'].includes(role ?? '') + ? onPermitParticipantVideo + : undefined, + onForbidAllAttendeesVideo: ['Unknown', 'Organizer', 'Presenter', 'Co-organizer'].includes(role ?? '') + ? muteAllHandlers.onForbidAllAttendeesVideo + : undefined, + onPermitAllAttendeesVideo: ['Unknown', 'Organizer', 'Presenter', 'Co-organizer'].includes(role ?? '') + ? muteAllHandlers.onPermitAllAttendeesVideo : undefined }; }, [ role, - /* @conditional-compile-remove(media-access) */ onForbidParticipantAudio, - /* @conditional-compile-remove(media-access) */ onPermitParticipantAudio, - /* @conditional-compile-remove(media-access) */ muteAllHandlers.onForbidAllAttendeesAudio, - /* @conditional-compile-remove(media-access) */ muteAllHandlers.onPermitAllAttendeesAudio + onForbidParticipantAudio, + onPermitParticipantAudio, + muteAllHandlers.onForbidAllAttendeesAudio, + muteAllHandlers.onPermitAllAttendeesAudio, + muteAllHandlers.onForbidAllAttendeesVideo, + muteAllHandlers.onPermitAllAttendeesVideo, + onForbidParticipantVideo, + onPermitParticipantVideo ]); const spotlightPeoplePaneProps = useMemo(() => { diff --git a/packages/react-composites/src/composites/CallComposite/components/SidePane/usePeoplePane.tsx b/packages/react-composites/src/composites/CallComposite/components/SidePane/usePeoplePane.tsx index 233e0943a5c..d3c0f10a23b 100644 --- a/packages/react-composites/src/composites/CallComposite/components/SidePane/usePeoplePane.tsx +++ b/packages/react-composites/src/composites/CallComposite/components/SidePane/usePeoplePane.tsx @@ -51,6 +51,14 @@ export const usePeoplePane = (props: { onForbidAllAttendeesAudio?: () => Promise; /* @conditional-compile-remove(media-access) */ onPermitAllAttendeesAudio?: () => Promise; + /* @conditional-compile-remove(media-access) */ + onForbidParticipantVideo?: (userIds: string[]) => Promise; + /* @conditional-compile-remove(media-access) */ + onPermitParticipantVideo?: (userIds: string[]) => Promise; + /* @conditional-compile-remove(media-access) */ + onForbidAllAttendeesVideo?: () => Promise; + /* @conditional-compile-remove(media-access) */ + onPermitAllAttendeesVideo?: () => Promise; }): { openPeoplePane: () => void; closePeoplePane: () => void; @@ -87,7 +95,15 @@ export const usePeoplePane = (props: { /* @conditional-compile-remove(media-access) */ onForbidAllAttendeesAudio, /* @conditional-compile-remove(media-access) */ - onPermitAllAttendeesAudio + onPermitAllAttendeesAudio, + /* @conditional-compile-remove(media-access) */ + onForbidParticipantVideo, + /* @conditional-compile-remove(media-access) */ + onPermitParticipantVideo, + /* @conditional-compile-remove(media-access) */ + onForbidAllAttendeesVideo, + /* @conditional-compile-remove(media-access) */ + onPermitAllAttendeesVideo } = props; const closePane = useCallback(() => { @@ -118,6 +134,8 @@ export const usePeoplePane = (props: { const [showForbidAllAttendeesAudioPrompt, setShowForbidAllAttendeesAudioPrompt] = React.useState(false); const [showPermitAllAttendeesAudioPrompt, setShowPermitAllAttendeesAudioPrompt] = React.useState(false); + const [showForbidAllAttendeesVideoPrompt, setShowForbidAllAttendeesVideoPrompt] = React.useState(false); + const [showPermitAllAttendeesVideoPrompt, setShowPermitAllAttendeesVideoPrompt] = React.useState(false); /* @conditional-compile-remove(soft-mute) */ const onMuteAllPromptConfirm = useCallback(() => { @@ -129,12 +147,22 @@ export const usePeoplePane = (props: { onForbidAllAttendeesAudio && onForbidAllAttendeesAudio(); setShowForbidAllAttendeesAudioPrompt(false); }, [onForbidAllAttendeesAudio, setShowForbidAllAttendeesAudioPrompt]); - console.log(onPermitAllAttendeesAudio); + const onPermitAllAttendeesPromptConfirm = useCallback(() => { onPermitAllAttendeesAudio && onPermitAllAttendeesAudio(); setShowPermitAllAttendeesAudioPrompt(false); }, [onPermitAllAttendeesAudio, setShowPermitAllAttendeesAudioPrompt]); + const onForbidAllAttendeesVideoPromptConfirm = useCallback(() => { + onForbidAllAttendeesVideo && onForbidAllAttendeesVideo(); + setShowForbidAllAttendeesVideoPrompt(false); + }, [onForbidAllAttendeesVideo, setShowForbidAllAttendeesVideoPrompt]); + + const onPermitAllAttendeesVideoPromptConfirm = useCallback(() => { + onPermitAllAttendeesVideo && onPermitAllAttendeesVideo(); + setShowPermitAllAttendeesVideoPrompt(false); + }, [onPermitAllAttendeesVideo, setShowPermitAllAttendeesVideoPrompt]); + const sidePaneHeaderMenuProps: IContextualMenuProps = useMemo(() => { const menuItems: IContextualMenuItem[] = []; /* @conditional-compile-remove(soft-mute) */ @@ -218,6 +246,61 @@ export const usePeoplePane = (props: { }); } + /* @conditional-compile-remove(media-access) */ + if (onForbidAllAttendeesVideo && remoteParticipants) { + let hasAttendee = false; + if (remoteParticipants) { + for (const participant of Object.values(remoteParticipants)) { + if (participant.role && participant.role === 'Attendee' && participant.mediaAccess?.isVideoPermitted) { + hasAttendee = true; + break; + } + } + } + hasAttendee && + menuItems.push({ + ['data-ui-id']: 'people-pane-forbid-all-attendees-video', + key: 'forbidAllAttendeesVideo', + text: localeStrings.forbidAllAttendeesVideoMenuLabel, + iconProps: { + iconName: 'ControlButtonCameraProhibited', // ControlButtonMicProhibited + styles: { root: { lineHeight: 0 } } + }, + onClick: () => { + setShowForbidAllAttendeesVideoPrompt(true); + }, + ariaLabel: localeStrings.forbidAllAttendeesVideoMenuLabel, + disabled: !hasAttendee + }); + } + /* @conditional-compile-remove(media-access) */ + if (onPermitAllAttendeesVideo && remoteParticipants) { + let hasAttendee = false; + if (remoteParticipants) { + for (const participant of Object.values(remoteParticipants)) { + if (participant.role && participant.role === 'Attendee' && !participant.mediaAccess?.isVideoPermitted) { + hasAttendee = true; + break; + } + } + } + hasAttendee && + menuItems.push({ + ['data-ui-id']: 'people-pane-permit-all-attendees-video', + key: 'permitAllAttendeesVideo', + text: localeStrings.permitAllAttendeesVideoMenuLabel, + iconProps: { + iconName: 'ContextualMenuCameraOff', + styles: { root: { lineHeight: 0 } } + }, + onClick: () => { + setShowPermitAllAttendeesVideoPrompt(true); + }, + ariaLabel: localeStrings.permitAllAttendeesVideoMenuLabel, + disabled: !hasAttendee + }); + } + if (onStopAllSpotlight && spotlightedParticipantUserIds && spotlightedParticipantUserIds.length > 0) { menuItems.push({ key: 'stopAllSpotlightKey', @@ -414,7 +497,48 @@ export const usePeoplePane = (props: { ariaLabel: localeStrings.forbidParticipantAudioMenuLabel }); } + /* @conditional-compile-remove(media-access) */ + if ( + !remoteParticipant?.mediaAccess?.isVideoPermitted && + remoteParticipant?.role === 'Attendee' && + onPermitParticipantVideo + ) { + _defaultMenuItems.push({ + key: 'permit-video', + text: localeStrings.permitParticipantVideoMenuLabel, + iconProps: { + iconName: 'ControlButtonCameraOff', + styles: { root: { lineHeight: '1rem', textAlign: 'center' } } + }, + onClick: () => { + onPermitParticipantVideo([participantId]); + }, + 'data-ui-id': 'participant-item-permit-camera-button', + ariaLabel: localeStrings.permitParticipantVideoMenuLabel + }); + } + /* @conditional-compile-remove(media-access) */ + if ( + remoteParticipant?.mediaAccess?.isVideoPermitted && + remoteParticipant?.role === 'Attendee' && + onForbidParticipantVideo + ) { + _defaultMenuItems.push({ + key: 'forbid-video', + text: localeStrings.forbidParticipantVideoMenuLabel, + iconProps: { + iconName: 'ControlButtonCameraProhibited', + styles: { root: { lineHeight: '1rem', textAlign: 'center' } } + }, + onClick: () => { + onForbidParticipantVideo([participantId]); + }, + 'data-ui-id': 'participant-item-forbid-camera-button', + ariaLabel: localeStrings.forbidParticipantVideoMenuLabel + }); + } } + if (defaultMenuItems) { _defaultMenuItems.push(...defaultMenuItems); } @@ -440,6 +564,8 @@ export const usePeoplePane = (props: { localeStrings.pinParticipantMenuItemAriaLabel, localeStrings.permitParticipantAudioMenuLabel, localeStrings.forbidParticipantAudioMenuLabel, + localeStrings.permitParticipantVideoMenuLabel, + localeStrings.forbidParticipantVideoMenuLabel, onStopLocalSpotlight, onStopRemoteSpotlight, maxParticipantsToSpotlight, @@ -449,6 +575,8 @@ export const usePeoplePane = (props: { onPinParticipant, onPermitParticipantAudio, onForbidParticipantAudio, + onPermitParticipantVideo, + onForbidParticipantVideo, disablePinMenuItem ] ); @@ -495,6 +623,32 @@ export const usePeoplePane = (props: { onCancel={() => setShowForbidAllAttendeesAudioPrompt(false)} /> } + { + /* @conditional-compile-remove(media-access) */ + onForbidAllAttendeesVideoPromptConfirm()} + isOpen={showForbidAllAttendeesVideoPrompt} + onCancel={() => setShowForbidAllAttendeesVideoPrompt(false)} + /> + } + { + /* @conditional-compile-remove(media-access) */ + onPermitAllAttendeesVideoPromptConfirm()} + isOpen={showPermitAllAttendeesVideoPrompt} + onCancel={() => setShowForbidAllAttendeesVideoPrompt(false)} + /> + } => { await adapter.permitAllAttendeesAudio(); } + : undefined, + /* @conditional-compile-remove(media-access) */ + onForbidParticipantVideo: capabilities?.forbidOthersMedia.isPresent + ? async (userIds: string[]): Promise => { + await adapter.forbidParticipantVideo(userIds); + } + : undefined, + /* @conditional-compile-remove(media-access) */ + onPermitParticipantVideo: capabilities?.forbidOthersMedia.isPresent + ? async (userIds: string[]): Promise => { + await adapter.permitParticipantVideo(userIds); + } + : undefined, + /* @conditional-compile-remove(media-access) */ + onForbidAllAttendeesVideo: capabilities?.forbidOthersMedia.isPresent + ? async (): Promise => { + await adapter.forbidAllAttendeesVideo(); + } + : undefined, + /* @conditional-compile-remove(media-access) */ + onPermitAllAttendeesVideo: capabilities?.forbidOthersMedia.isPresent + ? async (): Promise => { + await adapter.permitAllAttendeesVideo(); + } : undefined }; } diff --git a/packages/react-composites/src/composites/CallWithChatComposite/adapter/AzureCommunicationCallWithChatAdapter.ts b/packages/react-composites/src/composites/CallWithChatComposite/adapter/AzureCommunicationCallWithChatAdapter.ts index a4219e34442..91d21d33a23 100644 --- a/packages/react-composites/src/composites/CallWithChatComposite/adapter/AzureCommunicationCallWithChatAdapter.ts +++ b/packages/react-composites/src/composites/CallWithChatComposite/adapter/AzureCommunicationCallWithChatAdapter.ts @@ -727,6 +727,23 @@ export class AzureCommunicationCallWithChatAdapter implements CallWithChatAdapte return this.callAdapter.permitAllAttendeesAudio(); } + /* @conditional-compile-remove(media-access) */ + public async forbidParticipantVideo(userIds: string[]): Promise { + return this.callAdapter.forbidParticipantVideo(userIds); + } + /* @conditional-compile-remove(media-access) */ + public async permitParticipantVideo(userIds: string[]): Promise { + return this.callAdapter.permitParticipantVideo(userIds); + } + /* @conditional-compile-remove(media-access) */ + public async forbidAllAttendeesVideo(): Promise { + return this.callAdapter.forbidAllAttendeesVideo(); + } + /* @conditional-compile-remove(media-access) */ + public async permitAllAttendeesVideo(): Promise { + return this.callAdapter.permitAllAttendeesVideo(); + } + on(event: 'callParticipantsJoined', listener: ParticipantsJoinedListener): void; on(event: 'callParticipantsLeft', listener: ParticipantsLeftListener): void; on(event: 'callEnded', listener: CallEndedListener): void; diff --git a/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatAdapter.ts b/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatAdapter.ts index cdad79833d5..a2d85ba1113 100644 --- a/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatAdapter.ts +++ b/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatAdapter.ts @@ -525,6 +525,14 @@ export interface CallWithChatAdapterManagement { forbidAllAttendeesAudio: () => Promise; /* @conditional-compile-remove(media-access) */ permitAllAttendeesAudio: () => Promise; + /* @conditional-compile-remove(media-access) */ + forbidParticipantVideo: (userIds: string[]) => Promise; + /* @conditional-compile-remove(media-access) */ + permitParticipantVideo: (userIds: string[]) => Promise; + /* @conditional-compile-remove(media-access) */ + forbidAllAttendeesVideo: () => Promise; + /* @conditional-compile-remove(media-access) */ + permitAllAttendeesVideo: () => Promise; } /** diff --git a/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatBackedCallAdapter.ts b/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatBackedCallAdapter.ts index 28940084a9c..34367b948aa 100644 --- a/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatBackedCallAdapter.ts +++ b/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatBackedCallAdapter.ts @@ -262,6 +262,22 @@ export class CallWithChatBackedCallAdapter implements CallAdapter { public async permitAllAttendeesAudio(): Promise { return this.callWithChatAdapter.permitAllAttendeesAudio(); } + /* @conditional-compile-remove(media-access) */ + public async forbidParticipantVideo(userIds: string[]): Promise { + return this.callWithChatAdapter.forbidParticipantAudio(userIds); + } + /* @conditional-compile-remove(media-access) */ + public async permitParticipantVideo(userIds: string[]): Promise { + return this.callWithChatAdapter.permitParticipantAudio(userIds); + } + /* @conditional-compile-remove(media-access) */ + public async forbidAllAttendeesVideo(): Promise { + return this.callWithChatAdapter.forbidAllAttendeesAudio(); + } + /* @conditional-compile-remove(media-access) */ + public async permitAllAttendeesVideo(): Promise { + return this.callWithChatAdapter.permitAllAttendeesAudio(); + } } function callAdapterStateFromCallWithChatAdapterState( diff --git a/packages/react-composites/src/composites/common/Drawer/MoreDrawer.tsx b/packages/react-composites/src/composites/common/Drawer/MoreDrawer.tsx index 1a0baee0c32..99667f56891 100644 --- a/packages/react-composites/src/composites/common/Drawer/MoreDrawer.tsx +++ b/packages/react-composites/src/composites/common/Drawer/MoreDrawer.tsx @@ -158,6 +158,10 @@ export interface MoreDrawerProps extends MoreDrawerDevicesMenuProps { onForbidAllAttendeesAudio?: () => void; /* @conditional-compile-remove(media-access) */ onPermitAllAttendeesAudio?: () => void; + /* @conditional-compile-remove(media-access) */ + onForbidAllAttendeesVideo?: () => void; + /* @conditional-compile-remove(media-access) */ + onPermitAllAttendeesVideo?: () => void; } const inferCallWithChatControlOptions = ( diff --git a/packages/react-composites/src/composites/localization/locales/en-US/strings.json b/packages/react-composites/src/composites/localization/locales/en-US/strings.json index cd1fc6dcd0f..cd39bdee3aa 100644 --- a/packages/react-composites/src/composites/localization/locales/en-US/strings.json +++ b/packages/react-composites/src/composites/localization/locales/en-US/strings.json @@ -368,7 +368,19 @@ "permitAllAttendeesAudioConfirmButtonLabel": "Allow mics", "permitAllAttendeesAudioCancelButtonLabel": "Cancel", "forbidAllAttendeesAudioMenuLabel": "Disable mic for attendees", - "permitAllAttendeesAudioMenuLabel": "Allow mic for attendees" + "permitAllAttendeesAudioMenuLabel": "Allow mic for attendees", + "forbidParticipantVideoMenuLabel": "Disable camera", + "permitParticipantVideoMenuLabel": "Allow camera", + "forbidAllAttendeesVideoDialogTitle": "Disable camera for attendees?", + "forbidAllAttendeesVideoDialogContent": "Attendees won’t be able to turn on video themselves. The organizer and presenters can let people turn on camera as needed.", + "forbidAllAttendeesVideoConfirmButtonLabel": "Disable cameras", + "forbidAllAttendeesVideoCancelButtonLabel": "Cancel", + "permitAllAttendeesVideoDialogTitle": "Allow camera for attendees?", + "permitAllAttendeesVideoDialogContent": "Everyone in the meeting will be able to turn on video themselves.", + "permitAllAttendeesVideoConfirmButtonLabel": "Allow cameras", + "permitAllAttendeesVideoCancelButtonLabel": "Cancel", + "forbidAllAttendeesVideoMenuLabel": "Disable camera for attendees", + "permitAllAttendeesVideoMenuLabel": "Allow camera for attendees" }, "chat": { "chatListHeader": "In this chat", From 0c686968bf2463b1faf6640d1930ffd2eb679f42 Mon Sep 17 00:00:00 2001 From: fuyan Date: Thu, 17 Oct 2024 13:47:29 -0700 Subject: [PATCH 06/13] update --- .../review/beta/communication-react.api.md | 8 ++++++++ .../src/components/VideoTile.tsx | 4 ++-- .../react-components/src/theming/icons.tsx | 1 + .../components/SidePane/usePeoplePane.tsx | 18 +++++++++++------- .../src/composites/common/icons.tsx | 10 +++++++++- 5 files changed, 31 insertions(+), 10 deletions(-) diff --git a/packages/communication-react/review/beta/communication-react.api.md b/packages/communication-react/review/beta/communication-react.api.md index f253563d804..994b4b98ae4 100644 --- a/packages/communication-react/review/beta/communication-react.api.md +++ b/packages/communication-react/review/beta/communication-react.api.md @@ -670,7 +670,9 @@ export type CallCompositeIcons = { ControlButtonScreenShareStart?: JSX.Element; ControlButtonScreenShareStop?: JSX.Element; ControlButtonCameraProhibited?: JSX.Element; + ControlButtonCameraProhibitedSmall?: JSX.Element; ControlButtonMicProhibited?: JSX.Element; + ControlButtonMicProhibitedSmall?: JSX.Element; ControlButtonRaiseHand?: JSX.Element; ControlButtonLowerHand?: JSX.Element; ControlButtonExitSpotlight?: JSX.Element; @@ -1512,7 +1514,9 @@ export type CallWithChatCompositeIcons = { ControlButtonScreenShareStart?: JSX.Element; ControlButtonScreenShareStop?: JSX.Element; ControlButtonCameraProhibited?: JSX.Element; + ControlButtonCameraProhibitedSmall?: JSX.Element; ControlButtonMicProhibited?: JSX.Element; + ControlButtonMicProhibitedSmall?: JSX.Element; ErrorBarCallCameraAccessDenied?: JSX.Element; ErrorBarCallCameraAlreadyInUse?: JSX.Element; ErrorBarCallLocalVideoFreeze?: JSX.Element; @@ -2845,6 +2849,7 @@ export const DEFAULT_COMPONENT_ICONS: { SendBoxSend: React_2.JSX.Element; SendBoxSendHovered: React_2.JSX.Element; VideoTileMicOff: React_2.JSX.Element; + VideoTileCameraOff: React_2.JSX.Element; DialpadBackspace: React_2.JSX.Element; SitePermissionsSparkle: React_2.JSX.Element; SitePermissionCamera: React_2.JSX.Element; @@ -2928,7 +2933,9 @@ export const DEFAULT_COMPOSITE_ICONS: { ControlButtonScreenShareStart: JSX.Element; ControlButtonScreenShareStop: JSX.Element; ControlButtonCameraProhibited?: JSX.Element | undefined; + ControlButtonCameraProhibitedSmall?: JSX.Element | undefined; ControlButtonMicProhibited?: JSX.Element | undefined; + ControlButtonMicProhibitedSmall?: JSX.Element | undefined; ControlButtonRaiseHand: JSX.Element; ControlButtonLowerHand: JSX.Element; ControlButtonExitSpotlight?: JSX.Element | undefined; @@ -3025,6 +3032,7 @@ export const DEFAULT_COMPOSITE_ICONS: { HoldCallContextualMenuItem: React_2.JSX.Element; HoldCallButton: React_2.JSX.Element; ResumeCall: React_2.JSX.Element; + VideoTileCameraOff: React_2.JSX.Element; DialpadBackspace: React_2.JSX.Element; SitePermissionsSparkle: React_2.JSX.Element; SitePermissionCamera: React_2.JSX.Element; diff --git a/packages/react-components/src/components/VideoTile.tsx b/packages/react-components/src/components/VideoTile.tsx index 5ec4c615743..5665dc92172 100644 --- a/packages/react-components/src/components/VideoTile.tsx +++ b/packages/react-components/src/components/VideoTile.tsx @@ -455,7 +455,7 @@ export const VideoTile = (props: VideoTileProps): JSX.Element => { )} {!mediaAccess?.isVideoPermitted && ( - + )} {mediaAccess?.isAudioPermitted && showMuteIndicator && isMuted && ( @@ -465,7 +465,7 @@ export const VideoTile = (props: VideoTileProps): JSX.Element => { )} {!mediaAccess?.isAudioPermitted && showMuteIndicator && ( - + )} {isSpotlighted && ( diff --git a/packages/react-components/src/theming/icons.tsx b/packages/react-components/src/theming/icons.tsx index 467d9e8bcd5..53eccd32a81 100644 --- a/packages/react-components/src/theming/icons.tsx +++ b/packages/react-components/src/theming/icons.tsx @@ -339,6 +339,7 @@ export const DEFAULT_COMPONENT_ICONS = { SendBoxSend: , SendBoxSendHovered: , VideoTileMicOff: , + VideoTileCameraOff: , DialpadBackspace: , /* @conditional-compile-remove(call-readiness) */ SitePermissionsSparkle: , diff --git a/packages/react-composites/src/composites/CallComposite/components/SidePane/usePeoplePane.tsx b/packages/react-composites/src/composites/CallComposite/components/SidePane/usePeoplePane.tsx index d3c0f10a23b..e8727ce0bd9 100644 --- a/packages/react-composites/src/composites/CallComposite/components/SidePane/usePeoplePane.tsx +++ b/packages/react-composites/src/composites/CallComposite/components/SidePane/usePeoplePane.tsx @@ -263,7 +263,7 @@ export const usePeoplePane = (props: { key: 'forbidAllAttendeesVideo', text: localeStrings.forbidAllAttendeesVideoMenuLabel, iconProps: { - iconName: 'ControlButtonCameraProhibited', // ControlButtonMicProhibited + iconName: 'ControlButtonCameraProhibitedSmall', styles: { root: { lineHeight: 0 } } }, onClick: () => { @@ -318,14 +318,18 @@ export const usePeoplePane = (props: { }, [ onMuteAllRemoteParticipants, remoteParticipants, + onForbidAllAttendeesAudio, + onPermitAllAttendeesAudio, + onForbidAllAttendeesVideo, + onPermitAllAttendeesVideo, onStopAllSpotlight, spotlightedParticipantUserIds, localeStrings.muteAllMenuLabel, - /* @conditional-compile-remove(media-access) */ onForbidAllAttendeesAudio, - /* @conditional-compile-remove(media-access) */ onPermitAllAttendeesAudio, - /* @conditional-compile-remove(media-access) */ localeStrings.forbidAllAttendeesAudioMenuLabel, - /* @conditional-compile-remove(media-access) */ localeStrings.permitAllAttendeesAudioMenuLabel, - /* @conditional-compile-remove(media-access) */ localeStrings.stopAllSpotlightMenuLabel + localeStrings.forbidAllAttendeesAudioMenuLabel, + localeStrings.permitAllAttendeesAudioMenuLabel, + localeStrings.forbidAllAttendeesVideoMenuLabel, + localeStrings.permitAllAttendeesVideoMenuLabel, + localeStrings.stopAllSpotlightMenuLabel ]); const onRenderHeader = useCallback( @@ -527,7 +531,7 @@ export const usePeoplePane = (props: { key: 'forbid-video', text: localeStrings.forbidParticipantVideoMenuLabel, iconProps: { - iconName: 'ControlButtonCameraProhibited', + iconName: 'ControlButtonCameraProhibitedSmall', styles: { root: { lineHeight: '1rem', textAlign: 'center' } } }, onClick: () => { diff --git a/packages/react-composites/src/composites/common/icons.tsx b/packages/react-composites/src/composites/common/icons.tsx index cbdd05e0263..f4e89c10d0f 100644 --- a/packages/react-composites/src/composites/common/icons.tsx +++ b/packages/react-composites/src/composites/common/icons.tsx @@ -18,7 +18,9 @@ import { Video20Filled, VideoOff20Filled, WifiWarning20Filled, - Circle20Regular + Circle20Regular, + VideoProhibited16Filled, + MicProhibited16Filled } from '@fluentui/react-icons'; import { PersonCall20Regular, Clock20Filled } from '@fluentui/react-icons'; import { MoreHorizontal20Filled, VideoPersonStarOff20Filled } from '@fluentui/react-icons'; @@ -64,8 +66,10 @@ export const COMPOSITE_ONLY_ICONS: CompositeIcons = { ControlBarChatButtonInactive: , ControlButtonCameraProhibited: , + ControlButtonCameraProhibitedSmall: , ControlButtonMicProhibited: , + ControlButtonMicProhibitedSmall: , ControlButtonExitSpotlight: , ControlBarPeopleButton: , MoreDrawerMicrophones: , @@ -175,8 +179,10 @@ export type CallCompositeIcons = { ControlButtonScreenShareStop?: JSX.Element; ControlButtonCameraProhibited?: JSX.Element; + ControlButtonCameraProhibitedSmall?: JSX.Element; ControlButtonMicProhibited?: JSX.Element; + ControlButtonMicProhibitedSmall?: JSX.Element; ControlButtonRaiseHand?: JSX.Element; ControlButtonLowerHand?: JSX.Element; ControlButtonExitSpotlight?: JSX.Element; @@ -289,8 +295,10 @@ export type CallWithChatCompositeIcons = { ControlButtonScreenShareStop?: JSX.Element; ControlButtonCameraProhibited?: JSX.Element; + ControlButtonCameraProhibitedSmall?: JSX.Element; ControlButtonMicProhibited?: JSX.Element; + ControlButtonMicProhibitedSmall?: JSX.Element; ErrorBarCallCameraAccessDenied?: JSX.Element; ErrorBarCallCameraAlreadyInUse?: JSX.Element; ErrorBarCallLocalVideoFreeze?: JSX.Element; From cef3388e49d627221ffe0c60b1f47b74b1bfcfbd Mon Sep 17 00:00:00 2001 From: fuyan Date: Fri, 18 Oct 2024 16:44:30 -0700 Subject: [PATCH 07/13] update --- .../src/MediaAccessSubscriber.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/calling-stateful-client/src/MediaAccessSubscriber.ts b/packages/calling-stateful-client/src/MediaAccessSubscriber.ts index 5302832b662..a832bafd1c4 100644 --- a/packages/calling-stateful-client/src/MediaAccessSubscriber.ts +++ b/packages/calling-stateful-client/src/MediaAccessSubscriber.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { MediaAccessCallFeature, MediaAccessChangedEvent } from '@azure/communication-calling'; +import { MediaAccessCallFeature } from '@azure/communication-calling'; import { CallContext } from './CallContext'; import { CallIdRef } from './CallIdRef'; @@ -32,8 +32,13 @@ export class MediaAccessSubscriber { this._mediaAccessCallFeature.off('mediaAccessChanged', this.mediaAccessChanged); }; - private mediaAccessChanged = (data: MediaAccessChangedEvent): void => { - console.log('hi there audioVideoAccess changed', data); - this._context.setMediaAccesses(this._callIdRef.callId, data.mediaAccesses); + private mediaAccessChanged = (): void => { + const mediaAccesses = this._mediaAccessCallFeature.getOthersMediaAccess(); + this._context.setMediaAccesses(this._callIdRef.callId, mediaAccesses); }; + + // private mediaAccessChanged = (data: MediaAccessChangedEvent): void => { + // console.log('hi there audioVideoAccess changed', data); + // this._context.setMediaAccesses(this._callIdRef.callId, data.mediaAccesses); + // }; } From 5ac24509d600d6624cda8e816c386401a611bbb0 Mon Sep 17 00:00:00 2001 From: fuyan Date: Mon, 21 Oct 2024 16:26:19 -0700 Subject: [PATCH 08/13] update --- .../src/handlers/createCommonHandlers.ts | 8 ++++---- .../src/MediaAccessSubscriber.ts | 4 ++-- .../CallComposite/hooks/useHandlers.ts | 16 ++++++++-------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/calling-component-bindings/src/handlers/createCommonHandlers.ts b/packages/calling-component-bindings/src/handlers/createCommonHandlers.ts index 15cdd08c8e5..96462263a5d 100644 --- a/packages/calling-component-bindings/src/handlers/createCommonHandlers.ts +++ b/packages/calling-component-bindings/src/handlers/createCommonHandlers.ts @@ -759,11 +759,11 @@ export const createDefaultCommonCallingHandlers = memoizeOne( }; /* @conditional-compile-remove(media-access) */ const onForbidAllAttendeesAudio = async (): Promise => { - await call?.feature(Features.MediaAccess).forbidOthersAudio(); + await call?.feature(Features.MediaAccess).forbidRemoteParticipantsAudio(); }; /* @conditional-compile-remove(media-access) */ const onPermitAllAttendeesAudio = async (): Promise => { - await call?.feature(Features.MediaAccess).permitOthersAudio(); + await call?.feature(Features.MediaAccess).permitRemoteParticipantsAudio(); }; /* @conditional-compile-remove(media-access) */ @@ -778,11 +778,11 @@ export const createDefaultCommonCallingHandlers = memoizeOne( }; /* @conditional-compile-remove(media-access) */ const onForbidAllAttendeesVideo = async (): Promise => { - await call?.feature(Features.MediaAccess).forbidOthersVideo(); + await call?.feature(Features.MediaAccess).forbidRemoteParticipantsVideo(); }; /* @conditional-compile-remove(media-access) */ const onPermitAllAttendeesVideo = async (): Promise => { - await call?.feature(Features.MediaAccess).permitOthersVideo(); + await call?.feature(Features.MediaAccess).permitRemoteParticipantsVideo(); }; // const onForbidAllAttendeesAudio = canForbidOthersMedia diff --git a/packages/calling-stateful-client/src/MediaAccessSubscriber.ts b/packages/calling-stateful-client/src/MediaAccessSubscriber.ts index a832bafd1c4..2f3360b284a 100644 --- a/packages/calling-stateful-client/src/MediaAccessSubscriber.ts +++ b/packages/calling-stateful-client/src/MediaAccessSubscriber.ts @@ -19,7 +19,7 @@ export class MediaAccessSubscriber { this._context = context; this._mediaAccessCallFeature = mediaAccessCallFeature; - const mediaAccesses = this._mediaAccessCallFeature.getOthersMediaAccess(); + const mediaAccesses = this._mediaAccessCallFeature.getRemoteParticipantsMediaAccess(); this._context.setMediaAccesses(this._callIdRef.callId, mediaAccesses); this.subscribe(); } @@ -33,7 +33,7 @@ export class MediaAccessSubscriber { }; private mediaAccessChanged = (): void => { - const mediaAccesses = this._mediaAccessCallFeature.getOthersMediaAccess(); + const mediaAccesses = this._mediaAccessCallFeature.getRemoteParticipantsMediaAccess(); this._context.setMediaAccesses(this._callIdRef.callId, mediaAccesses); }; diff --git a/packages/react-composites/src/composites/CallComposite/hooks/useHandlers.ts b/packages/react-composites/src/composites/CallComposite/hooks/useHandlers.ts index 9117b7be539..7d50d07e232 100644 --- a/packages/react-composites/src/composites/CallComposite/hooks/useHandlers.ts +++ b/packages/react-composites/src/composites/CallComposite/hooks/useHandlers.ts @@ -239,49 +239,49 @@ const createCompositeHandlers = memoizeOne( // await adapter.permitParticipantAudio(userIds); // } /* @conditional-compile-remove(media-access) */ - onForbidParticipantAudio: capabilities?.forbidOthersMedia.isPresent + onForbidParticipantAudio: capabilities?.forbidRemoteParticipantsAudio.isPresent ? async (userIds: string[]): Promise => { await adapter.forbidParticipantAudio(userIds); } : undefined, /* @conditional-compile-remove(media-access) */ - onPermitParticipantAudio: capabilities?.forbidOthersMedia.isPresent + onPermitParticipantAudio: capabilities?.forbidRemoteParticipantsAudio.isPresent ? async (userIds: string[]): Promise => { await adapter.permitParticipantAudio(userIds); } : undefined, /* @conditional-compile-remove(media-access) */ - onForbidAllAttendeesAudio: capabilities?.forbidOthersMedia.isPresent + onForbidAllAttendeesAudio: capabilities?.forbidRemoteParticipantsAudio.isPresent ? async (): Promise => { await adapter.forbidAllAttendeesAudio(); } : undefined, /* @conditional-compile-remove(media-access) */ - onPermitAllAttendeesAudio: capabilities?.forbidOthersMedia.isPresent + onPermitAllAttendeesAudio: capabilities?.forbidRemoteParticipantsAudio.isPresent ? async (): Promise => { await adapter.permitAllAttendeesAudio(); } : undefined, /* @conditional-compile-remove(media-access) */ - onForbidParticipantVideo: capabilities?.forbidOthersMedia.isPresent + onForbidParticipantVideo: capabilities?.forbidRemoteParticipantsVideo.isPresent ? async (userIds: string[]): Promise => { await adapter.forbidParticipantVideo(userIds); } : undefined, /* @conditional-compile-remove(media-access) */ - onPermitParticipantVideo: capabilities?.forbidOthersMedia.isPresent + onPermitParticipantVideo: capabilities?.forbidRemoteParticipantsVideo.isPresent ? async (userIds: string[]): Promise => { await adapter.permitParticipantVideo(userIds); } : undefined, /* @conditional-compile-remove(media-access) */ - onForbidAllAttendeesVideo: capabilities?.forbidOthersMedia.isPresent + onForbidAllAttendeesVideo: capabilities?.forbidRemoteParticipantsVideo.isPresent ? async (): Promise => { await adapter.forbidAllAttendeesVideo(); } : undefined, /* @conditional-compile-remove(media-access) */ - onPermitAllAttendeesVideo: capabilities?.forbidOthersMedia.isPresent + onPermitAllAttendeesVideo: capabilities?.forbidRemoteParticipantsVideo.isPresent ? async (): Promise => { await adapter.permitAllAttendeesVideo(); } From 541876587cc410301b7966d0ba08f35b2fdab8c8 Mon Sep 17 00:00:00 2001 From: fuyan Date: Mon, 21 Oct 2024 21:51:53 -0700 Subject: [PATCH 09/13] update --- .../src/participantListSelector.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/calling-component-bindings/src/participantListSelector.ts b/packages/calling-component-bindings/src/participantListSelector.ts index c1a03920d2d..c80480081b9 100644 --- a/packages/calling-component-bindings/src/participantListSelector.ts +++ b/packages/calling-component-bindings/src/participantListSelector.ts @@ -177,6 +177,8 @@ export const participantListSelector: ParticipantListSelector = createSelector( spotlightCallFeature?.spotlightedParticipants ) : []; + + console.log('hi there capabilties', capabilities); participants.push({ userId: userId, displayName: displayName, @@ -190,8 +192,8 @@ export const participantListSelector: ParticipantListSelector = createSelector( spotlight: memoizedSpotlight(spotlightCallFeature?.spotlightedParticipants, userId), /* @conditional-compile-remove(media-access) */ mediaAccess: { - isAudioPermitted: !!capabilities?.unmuteMic.isPresent, - isVideoPermitted: !!capabilities?.turnVideoOn.isPresent + isAudioPermitted: capabilities ? capabilities.unmuteMic.isPresent : true, + isVideoPermitted: capabilities ? capabilities.turnVideoOn.isPresent : true } }); /* @conditional-compile-remove(total-participant-count) */ From 89b923ca34f3851f32f03f65f7be9a354c321b88 Mon Sep 17 00:00:00 2001 From: fuyan Date: Mon, 21 Oct 2024 22:21:29 -0700 Subject: [PATCH 10/13] update --- packages/react-components/src/components/RemoteVideoTile.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/react-components/src/components/RemoteVideoTile.tsx b/packages/react-components/src/components/RemoteVideoTile.tsx index 210b95d4d14..f7687e12425 100644 --- a/packages/react-components/src/components/RemoteVideoTile.tsx +++ b/packages/react-components/src/components/RemoteVideoTile.tsx @@ -155,7 +155,6 @@ export const _RemoteVideoTile = React.memo( onStartSpotlight, onStopSpotlight, maxParticipantsToSpotlight, - /* @conditional-compile-remove(soft-mute) */ onMuteParticipant, /* @conditional-compile-remove(media-access) */ onForbidParticipantAudio, /* @conditional-compile-remove(media-access) */ onPermitParticipantAudio, /* @conditional-compile-remove(media-access) */ onForbidParticipantVideo, From b91ddebf43bd3be1a58a2cfe9ce1e82ecf859a1f Mon Sep 17 00:00:00 2001 From: fuyan Date: Wed, 30 Oct 2024 21:48:17 -0700 Subject: [PATCH 11/13] Update --- .../review/beta/communication-react.api.md | 1 + .../components/SidePane/usePeoplePane.tsx | 164 +++++++++--------- 2 files changed, 83 insertions(+), 82 deletions(-) diff --git a/packages/communication-react/review/beta/communication-react.api.md b/packages/communication-react/review/beta/communication-react.api.md index 235e02c0f20..1e17d947746 100644 --- a/packages/communication-react/review/beta/communication-react.api.md +++ b/packages/communication-react/review/beta/communication-react.api.md @@ -4482,6 +4482,7 @@ export type RemoteDiagnosticType = NetworkDiagnosticType | MediaDiagnosticType | export interface RemoteParticipantState { callEndReason?: CallEndReason; contentSharingStream?: HTMLElement; + // (undocumented) diagnostics?: Partial>; displayName?: string; identifier: CommunicationIdentifierKind; diff --git a/packages/react-composites/src/composites/CallComposite/components/SidePane/usePeoplePane.tsx b/packages/react-composites/src/composites/CallComposite/components/SidePane/usePeoplePane.tsx index e4f4ccc5ac8..a774b0c40ec 100644 --- a/packages/react-composites/src/composites/CallComposite/components/SidePane/usePeoplePane.tsx +++ b/packages/react-composites/src/composites/CallComposite/components/SidePane/usePeoplePane.tsx @@ -359,6 +359,88 @@ export const usePeoplePane = (props: { disabled: isMuted }); } + /* @conditional-compile-remove(media-access) */ + const remoteParticipant = remoteParticipants?.[participantId]; + /* @conditional-compile-remove(media-access) */ + if ( + !remoteParticipant?.mediaAccess?.isAudioPermitted && + remoteParticipant?.role === 'Attendee' && + onPermitParticipantAudio + ) { + _defaultMenuItems.push({ + key: 'permit-audio', + text: localeStrings.permitParticipantAudioMenuLabel, + iconProps: { + iconName: 'ContextualMenuMicMutedIcon', + styles: { root: { lineHeight: '1rem', textAlign: 'center' } } + }, + onClick: () => { + onPermitParticipantAudio([participantId]); + }, + 'data-ui-id': 'participant-item-permit-microphone-button', + ariaLabel: localeStrings.permitParticipantAudioMenuLabel + }); + } + /* @conditional-compile-remove(media-access) */ + if ( + remoteParticipant?.mediaAccess?.isAudioPermitted && + remoteParticipant?.role === 'Attendee' && + onForbidParticipantAudio + ) { + _defaultMenuItems.push({ + key: 'forbid-audio', + text: localeStrings.forbidParticipantAudioMenuLabel, + iconProps: { + iconName: 'ControlButtonMicProhibited', + styles: { root: { lineHeight: '1rem', textAlign: 'center' } } + }, + onClick: () => { + onForbidParticipantAudio([participantId]); + }, + 'data-ui-id': 'participant-item-forbid-microphone-button', + ariaLabel: localeStrings.forbidParticipantAudioMenuLabel + }); + } + /* @conditional-compile-remove(media-access) */ + if ( + !remoteParticipant?.mediaAccess?.isVideoPermitted && + remoteParticipant?.role === 'Attendee' && + onPermitParticipantVideo + ) { + _defaultMenuItems.push({ + key: 'permit-video', + text: localeStrings.permitParticipantVideoMenuLabel, + iconProps: { + iconName: 'ControlButtonCameraOff', + styles: { root: { lineHeight: '1rem', textAlign: 'center' } } + }, + onClick: () => { + onPermitParticipantVideo([participantId]); + }, + 'data-ui-id': 'participant-item-permit-camera-button', + ariaLabel: localeStrings.permitParticipantVideoMenuLabel + }); + } + /* @conditional-compile-remove(media-access) */ + if ( + remoteParticipant?.mediaAccess?.isVideoPermitted && + remoteParticipant?.role === 'Attendee' && + onForbidParticipantVideo + ) { + _defaultMenuItems.push({ + key: 'forbid-video', + text: localeStrings.forbidParticipantVideoMenuLabel, + iconProps: { + iconName: 'ControlButtonCameraProhibitedSmall', + styles: { root: { lineHeight: '1rem', textAlign: 'center' } } + }, + onClick: () => { + onForbidParticipantVideo([participantId]); + }, + 'data-ui-id': 'participant-item-forbid-camera-button', + ariaLabel: localeStrings.forbidParticipantVideoMenuLabel + }); + } if (isSpotlighted) { const stopSpotlightMenuText = isMe ? localeStrings.stopSpotlightOnSelfMenuLabel @@ -448,88 +530,6 @@ export const usePeoplePane = (props: { ariaLabel: localeStrings.pinParticipantMenuItemAriaLabel }); } - /* @conditional-compile-remove(media-access) */ - const remoteParticipant = remoteParticipants?.[participantId]; - /* @conditional-compile-remove(media-access) */ - if ( - !remoteParticipant?.mediaAccess?.isAudioPermitted && - remoteParticipant?.role === 'Attendee' && - onPermitParticipantAudio - ) { - _defaultMenuItems.push({ - key: 'permit-audio', - text: localeStrings.permitParticipantAudioMenuLabel, - iconProps: { - iconName: 'ContextualMenuMicMutedIcon', - styles: { root: { lineHeight: '1rem', textAlign: 'center' } } - }, - onClick: () => { - onPermitParticipantAudio([participantId]); - }, - 'data-ui-id': 'participant-item-permit-microphone-button', - ariaLabel: localeStrings.permitParticipantAudioMenuLabel - }); - } - /* @conditional-compile-remove(media-access) */ - if ( - remoteParticipant?.mediaAccess?.isAudioPermitted && - remoteParticipant?.role === 'Attendee' && - onForbidParticipantAudio - ) { - _defaultMenuItems.push({ - key: 'forbid-audio', - text: localeStrings.forbidParticipantAudioMenuLabel, - iconProps: { - iconName: 'ControlButtonMicProhibited', - styles: { root: { lineHeight: '1rem', textAlign: 'center' } } - }, - onClick: () => { - onForbidParticipantAudio([participantId]); - }, - 'data-ui-id': 'participant-item-forbid-microphone-button', - ariaLabel: localeStrings.forbidParticipantAudioMenuLabel - }); - } - /* @conditional-compile-remove(media-access) */ - if ( - !remoteParticipant?.mediaAccess?.isVideoPermitted && - remoteParticipant?.role === 'Attendee' && - onPermitParticipantVideo - ) { - _defaultMenuItems.push({ - key: 'permit-video', - text: localeStrings.permitParticipantVideoMenuLabel, - iconProps: { - iconName: 'ControlButtonCameraOff', - styles: { root: { lineHeight: '1rem', textAlign: 'center' } } - }, - onClick: () => { - onPermitParticipantVideo([participantId]); - }, - 'data-ui-id': 'participant-item-permit-camera-button', - ariaLabel: localeStrings.permitParticipantVideoMenuLabel - }); - } - /* @conditional-compile-remove(media-access) */ - if ( - remoteParticipant?.mediaAccess?.isVideoPermitted && - remoteParticipant?.role === 'Attendee' && - onForbidParticipantVideo - ) { - _defaultMenuItems.push({ - key: 'forbid-video', - text: localeStrings.forbidParticipantVideoMenuLabel, - iconProps: { - iconName: 'ControlButtonCameraProhibitedSmall', - styles: { root: { lineHeight: '1rem', textAlign: 'center' } } - }, - onClick: () => { - onForbidParticipantVideo([participantId]); - }, - 'data-ui-id': 'participant-item-forbid-camera-button', - ariaLabel: localeStrings.forbidParticipantVideoMenuLabel - }); - } } if (defaultMenuItems) { From e33b0fd47bc49d59a1f1d440800e56dddfb0ee37 Mon Sep 17 00:00:00 2001 From: fuyan Date: Fri, 1 Nov 2024 13:58:00 -0700 Subject: [PATCH 12/13] update --- .../src/utils/videoGalleryUtils.ts | 20 ++- .../src/videoGallerySelector.ts | 4 +- .../src/CallContext.ts | 4 +- .../src/MediaAccessSubscriber.ts | 13 +- .../review/beta/communication-react.api.md | 3 +- .../useVideoTileContextualMenuProps.ts | 115 +++++++++--------- .../src/types/VideoGalleryParticipant.ts | 7 +- 7 files changed, 86 insertions(+), 80 deletions(-) diff --git a/packages/calling-component-bindings/src/utils/videoGalleryUtils.ts b/packages/calling-component-bindings/src/utils/videoGalleryUtils.ts index 3578484c125..07a77628096 100644 --- a/packages/calling-component-bindings/src/utils/videoGalleryUtils.ts +++ b/packages/calling-component-bindings/src/utils/videoGalleryUtils.ts @@ -78,7 +78,9 @@ export const _videoGalleryRemoteParticipantsMemo: _VideoGalleryRemoteParticipant remoteParticipantReaction, spotlight, /* @conditional-compile-remove(media-access) */ - participant.mediaAccess + participant.mediaAccess, + /* @conditional-compile-remove(media-access) */ + participant.role ); }) ); @@ -98,7 +100,9 @@ const memoizedAllConvertRemoteParticipant = memoizeFnAll( reaction?: Reaction, spotlight?: Spotlight, /* @conditional-compile-remove(media-access) */ - mediaAccess?: MediaAccess + mediaAccess?: MediaAccess, + /* @conditional-compile-remove(media-access) */ + role?: string ): VideoGalleryRemoteParticipant => { return convertRemoteParticipantToVideoGalleryRemoteParticipant( userId, @@ -112,7 +116,9 @@ const memoizedAllConvertRemoteParticipant = memoizeFnAll( reaction, spotlight, /* @conditional-compile-remove(media-access) */ - mediaAccess + mediaAccess, + /* @conditional-compile-remove(media-access) */ + role ); } ); @@ -130,7 +136,9 @@ export const convertRemoteParticipantToVideoGalleryRemoteParticipant = ( reaction?: Reaction, spotlight?: Spotlight, /* @conditional-compile-remove(media-access) */ - mediaAccess?: MediaAccess + mediaAccess?: MediaAccess, + /* @conditional-compile-remove(media-access) */ + role?: string ): VideoGalleryRemoteParticipant => { const rawVideoStreamsArray = Object.values(videoStreams); let videoStream: VideoGalleryStream | undefined = undefined; @@ -175,7 +183,9 @@ export const convertRemoteParticipantToVideoGalleryRemoteParticipant = ( reaction, spotlight, /* @conditional-compile-remove(media-access) */ - mediaAccess + mediaAccess, + /* @conditional-compile-remove(media-access) */ + role }; }; diff --git a/packages/calling-component-bindings/src/videoGallerySelector.ts b/packages/calling-component-bindings/src/videoGallerySelector.ts index 1e2406a94f1..302566bf252 100644 --- a/packages/calling-component-bindings/src/videoGallerySelector.ts +++ b/packages/calling-component-bindings/src/videoGallerySelector.ts @@ -115,7 +115,9 @@ export const videoGallerySelector: VideoGallerySelector = createSelector( screenShareRemoteParticipant.raisedHand, screenShareRemoteParticipant.contentSharingStream, undefined, - screenShareRemoteParticipant.spotlight + screenShareRemoteParticipant.spotlight, + screenShareRemoteParticipant.mediaAccess, + role ) : undefined, localParticipant: memoizeLocalParticipant( diff --git a/packages/calling-stateful-client/src/CallContext.ts b/packages/calling-stateful-client/src/CallContext.ts index 5cdf1e23168..d6361823546 100644 --- a/packages/calling-stateful-client/src/CallContext.ts +++ b/packages/calling-stateful-client/src/CallContext.ts @@ -1169,9 +1169,7 @@ export class CallContext { if (!call) { return; } - // call.mediaAccess = { - // mediaAccesses - // }; + mediaAccesses.forEach((participantMediaAccess) => { const participant = call.remoteParticipants[toFlatCommunicationIdentifier(participantMediaAccess.participant)]; if (participant) { diff --git a/packages/calling-stateful-client/src/MediaAccessSubscriber.ts b/packages/calling-stateful-client/src/MediaAccessSubscriber.ts index 2f3360b284a..b2b5b09805c 100644 --- a/packages/calling-stateful-client/src/MediaAccessSubscriber.ts +++ b/packages/calling-stateful-client/src/MediaAccessSubscriber.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { MediaAccessCallFeature } from '@azure/communication-calling'; +import { MediaAccessCallFeature, MediaAccessChangedEvent } from '@azure/communication-calling'; import { CallContext } from './CallContext'; import { CallIdRef } from './CallIdRef'; @@ -32,13 +32,8 @@ export class MediaAccessSubscriber { this._mediaAccessCallFeature.off('mediaAccessChanged', this.mediaAccessChanged); }; - private mediaAccessChanged = (): void => { - const mediaAccesses = this._mediaAccessCallFeature.getRemoteParticipantsMediaAccess(); - this._context.setMediaAccesses(this._callIdRef.callId, mediaAccesses); + private mediaAccessChanged = (data: MediaAccessChangedEvent): void => { + console.log('hi there audioVideoAccess changed', data); + this._context.setMediaAccesses(this._callIdRef.callId, data.mediaAccesses); }; - - // private mediaAccessChanged = (data: MediaAccessChangedEvent): void => { - // console.log('hi there audioVideoAccess changed', data); - // this._context.setMediaAccesses(this._callIdRef.callId, data.mediaAccesses); - // }; } diff --git a/packages/communication-react/review/beta/communication-react.api.md b/packages/communication-react/review/beta/communication-react.api.md index 1e17d947746..bef4fcbd885 100644 --- a/packages/communication-react/review/beta/communication-react.api.md +++ b/packages/communication-react/review/beta/communication-react.api.md @@ -5339,6 +5339,7 @@ export type VideoGalleryParticipant = { isScreenSharingOn?: boolean; spotlight?: Spotlight; mediaAccess?: MediaAccess; + role?: string; }; // @public @@ -5394,8 +5395,6 @@ export interface VideoGalleryProps { // @public export interface VideoGalleryRemoteParticipant extends VideoGalleryParticipant { isSpeaking?: boolean; - // (undocumented) - mediaAccess?: MediaAccess; raisedHand?: RaisedHand; reaction?: Reaction; screenShareStream?: VideoGalleryStream; diff --git a/packages/react-components/src/components/VideoGallery/useVideoTileContextualMenuProps.ts b/packages/react-components/src/components/VideoGallery/useVideoTileContextualMenuProps.ts index 5dfb88389f9..db936c5fb11 100644 --- a/packages/react-components/src/components/VideoGallery/useVideoTileContextualMenuProps.ts +++ b/packages/react-components/src/components/VideoGallery/useVideoTileContextualMenuProps.ts @@ -98,6 +98,64 @@ export const useVideoTileContextualMenuProps = (props: { disabled: participant.isMuted }); } + const isAttendee = participant.role === 'Attendee'; + /* @conditional-compile-remove(media-access) */ + if (isAttendee && !participant.mediaAccess?.isAudioPermitted && onPermitParticipantAudio) { + items.push({ + key: 'permitParticipantAudio', + text: strings?.permitParticipantAudioTileMenuLabel, + iconProps: { + iconName: 'Microphone', + styles: { root: { lineHeight: 0 } } + }, + onClick: () => onPermitParticipantAudio([participant.userId]), + 'data-ui-id': 'audio-tile-unblock-microphone', + ariaLabel: 'Unblock microphone' + }); + } + /* @conditional-compile-remove(media-access) */ + if (isAttendee && participant.mediaAccess?.isAudioPermitted && onForbidParticipantAudio) { + items.push({ + key: 'forbidParticipantAudio', + text: strings?.forbidParticipantAudioTileMenuLabel, + iconProps: { + iconName: 'ControlButtonMicProhibited', + styles: { root: { lineHeight: 0 } } + }, + onClick: () => onForbidParticipantAudio([participant.userId]), + 'data-ui-id': 'audio-tile-block-microphone', + ariaLabel: 'Block microphone' + }); + } + + /* @conditional-compile-remove(media-access) */ + if (isAttendee && !participant.mediaAccess?.isVideoPermitted && onPermitParticipantVideo) { + items.push({ + key: 'permitParticipantVideo', + text: strings?.permitParticipantVideoTileMenuLabel, + iconProps: { + iconName: 'Camera', + styles: { root: { lineHeight: 0 } } + }, + onClick: () => onPermitParticipantVideo([participant.userId]), + 'data-ui-id': 'video-tile-permit-camera', + ariaLabel: 'Permit camera' + }); + } + /* @conditional-compile-remove(media-access) */ + if (isAttendee && participant.mediaAccess?.isVideoPermitted && onForbidParticipantVideo) { + items.push({ + key: 'forbidParticipantVideo', + text: strings?.forbidParticipantVideoTileMenuLabel, + iconProps: { + iconName: 'ControlButtonCameraProhibited', + styles: { root: { lineHeight: 0 } } + }, + onClick: () => onForbidParticipantVideo([participant.userId]), + 'data-ui-id': 'video-tile-block-microphone', + ariaLabel: 'Block microphone' + }); + } if (isPinned !== undefined) { if (isPinned && onUnpinParticipant && strings?.unpinParticipantForMe) { let unpinActionString: string | undefined = undefined; @@ -186,63 +244,6 @@ export const useVideoTileContextualMenuProps = (props: { }); } } - /* @conditional-compile-remove(media-access) */ - if (!participant.mediaAccess?.isAudioPermitted && onPermitParticipantAudio) { - items.push({ - key: 'permitParticipantAudio', - text: strings?.permitParticipantAudioTileMenuLabel, - iconProps: { - iconName: 'Microphone', - styles: { root: { lineHeight: 0 } } - }, - onClick: () => onPermitParticipantAudio([participant.userId]), - 'data-ui-id': 'audio-tile-unblock-microphone', - ariaLabel: 'Unblock microphone' - }); - } - /* @conditional-compile-remove(media-access) */ - if (participant.mediaAccess?.isAudioPermitted && onForbidParticipantAudio) { - items.push({ - key: 'forbidParticipantAudio', - text: strings?.forbidParticipantAudioTileMenuLabel, - iconProps: { - iconName: 'ControlButtonMicProhibited', - styles: { root: { lineHeight: 0 } } - }, - onClick: () => onForbidParticipantAudio([participant.userId]), - 'data-ui-id': 'audio-tile-block-microphone', - ariaLabel: 'Block microphone' - }); - } - - /* @conditional-compile-remove(media-access) */ - if (!participant.mediaAccess?.isVideoPermitted && onPermitParticipantVideo) { - items.push({ - key: 'permitParticipantVideo', - text: strings?.permitParticipantVideoTileMenuLabel, - iconProps: { - iconName: 'Microphone', - styles: { root: { lineHeight: 0 } } - }, - onClick: () => onPermitParticipantVideo([participant.userId]), - 'data-ui-id': 'video-tile-unblock-microphone', - ariaLabel: 'Unblock microphone' - }); - } - /* @conditional-compile-remove(media-access) */ - if (participant.mediaAccess?.isVideoPermitted && onForbidParticipantVideo) { - items.push({ - key: 'forbidParticipantVideo', - text: strings?.forbidParticipantVideoTileMenuLabel, - iconProps: { - iconName: 'ControlButtonMicProhibited', - styles: { root: { lineHeight: 0 } } - }, - onClick: () => onForbidParticipantVideo([participant.userId]), - 'data-ui-id': 'video-tile-block-microphone', - ariaLabel: 'Block microphone' - }); - } if (scalingMode) { if (scalingMode === 'Crop' && strings?.fitRemoteParticipantToFrame) { diff --git a/packages/react-components/src/types/VideoGalleryParticipant.ts b/packages/react-components/src/types/VideoGalleryParticipant.ts index f64871a5d76..31f914c44c9 100644 --- a/packages/react-components/src/types/VideoGalleryParticipant.ts +++ b/packages/react-components/src/types/VideoGalleryParticipant.ts @@ -47,8 +47,11 @@ export type VideoGalleryParticipant = { /** Whether participant is spotlighted **/ spotlight?: Spotlight; /* @conditional-compile-remove(media-access) */ - /** audio video access states **/ + /** Audio video access states **/ mediaAccess?: MediaAccess; + /* @conditional-compile-remove(media-access) */ + /** Participant user role **/ + role?: string; }; /** @@ -132,6 +135,4 @@ export interface VideoGalleryRemoteParticipant extends VideoGalleryParticipant { * @public * */ reaction?: Reaction; - /* @conditional-compile-remove(media-access) */ - mediaAccess?: MediaAccess; } From 107c4c132b1f6d6911d851e9d277fc3498a7bf81 Mon Sep 17 00:00:00 2001 From: fuyan Date: Fri, 1 Nov 2024 16:14:01 -0700 Subject: [PATCH 13/13] update --- .../useVideoTileContextualMenuProps.ts | 13 ++++++------ .../src/components/VideoTile.tsx | 20 ++++++------------- .../components/SidePane/usePeoplePane.tsx | 2 +- 3 files changed, 14 insertions(+), 21 deletions(-) diff --git a/packages/react-components/src/components/VideoGallery/useVideoTileContextualMenuProps.ts b/packages/react-components/src/components/VideoGallery/useVideoTileContextualMenuProps.ts index db936c5fb11..9a8e2ed3ced 100644 --- a/packages/react-components/src/components/VideoGallery/useVideoTileContextualMenuProps.ts +++ b/packages/react-components/src/components/VideoGallery/useVideoTileContextualMenuProps.ts @@ -286,6 +286,10 @@ export const useVideoTileContextualMenuProps = (props: { }, [ onMuteParticipant, strings?.muteParticipantMenuItemLabel, + strings?.permitParticipantAudioTileMenuLabel, + strings?.forbidParticipantAudioTileMenuLabel, + strings?.permitParticipantVideoTileMenuLabel, + strings?.forbidParticipantVideoTileMenuLabel, strings?.unpinParticipantForMe, strings?.pinParticipantForMe, strings?.unpinParticipantMenuItemAriaLabel, @@ -296,14 +300,9 @@ export const useVideoTileContextualMenuProps = (props: { strings?.addSpotlightVideoTileMenuLabel, strings?.startSpotlightVideoTileMenuLabel, strings?.spotlightLimitReachedMenuTitle, - strings?.permitParticipantAudioTileMenuLabel, - strings?.forbidParticipantAudioTileMenuLabel, - strings?.permitParticipantVideoTileMenuLabel, - strings?.forbidParticipantVideoTileMenuLabel, strings?.fitRemoteParticipantToFrame, strings?.fillRemoteParticipantFrame, - isPinned, - isSpotlighted, + participant.role, participant.mediaAccess?.isAudioPermitted, participant.mediaAccess?.isVideoPermitted, participant.isMuted, @@ -313,6 +312,8 @@ export const useVideoTileContextualMenuProps = (props: { onForbidParticipantAudio, onPermitParticipantVideo, onForbidParticipantVideo, + isPinned, + isSpotlighted, scalingMode, onUnpinParticipant, onPinParticipant, diff --git a/packages/react-components/src/components/VideoTile.tsx b/packages/react-components/src/components/VideoTile.tsx index 8a90783899d..1283fd251c8 100644 --- a/packages/react-components/src/components/VideoTile.tsx +++ b/packages/react-components/src/components/VideoTile.tsx @@ -498,21 +498,13 @@ export const VideoTile = (props: VideoTileProps): JSX.Element => { {bracketedParticipantString(participantStateString, !!canShowLabel)} )} - {!mediaAccess?.isVideoPermitted && ( - - - - )} - {mediaAccess?.isAudioPermitted && showMuteIndicator && isMuted && ( - - - - )} - {!mediaAccess?.isAudioPermitted && showMuteIndicator && ( - + + {!mediaAccess?.isVideoPermitted && } + {mediaAccess?.isAudioPermitted && showMuteIndicator && isMuted && } + {!mediaAccess?.isAudioPermitted && showMuteIndicator && ( - - )} + )} + {isSpotlighted && ( diff --git a/packages/react-composites/src/composites/CallComposite/components/SidePane/usePeoplePane.tsx b/packages/react-composites/src/composites/CallComposite/components/SidePane/usePeoplePane.tsx index a774b0c40ec..4036de0c7df 100644 --- a/packages/react-composites/src/composites/CallComposite/components/SidePane/usePeoplePane.tsx +++ b/packages/react-composites/src/composites/CallComposite/components/SidePane/usePeoplePane.tsx @@ -279,7 +279,7 @@ export const usePeoplePane = (props: { key: 'permitAllAttendeesVideo', text: localeStrings.permitAllAttendeesVideoMenuLabel, iconProps: { - iconName: 'ContextualMenuCameraOff', + iconName: 'ControlButtonCameraOff', styles: { root: { lineHeight: 0 } } }, onClick: () => {