Skip to content

Commit 716282b

Browse files
authored
add story book for hard mute feature (#5456)
* add story book for hard mute feature * address PR comment * Update MediaAccess documentation with UI component example * Fix capitalization in media access state references * address PR comment * Change files * fix lint issue * update
1 parent b427e2d commit 716282b

9 files changed

+305
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"type": "patch",
3+
"area": "improvement",
4+
"workstream": "Hard mute",
5+
"comment": "Add story book for Media access (hard mute) feature",
6+
"packageName": "@azure/communication-react",
7+
"email": "[email protected]",
8+
"dependentChangeType": "patch"
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"type": "patch",
3+
"area": "improvement",
4+
"workstream": "Hard mute",
5+
"comment": "Add story book for Media access (hard mute) feature",
6+
"packageName": "@azure/communication-react",
7+
"email": "[email protected]",
8+
"dependentChangeType": "patch"
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { Meta, Source } from '@storybook/addon-docs';
2+
3+
import CustomMediaAccessCompositeText from '!!raw-loader!./snippets/MediaAccessComposite.snippet.tsx';
4+
import MediaAccessMicCameraCapabilitiesText from '!!raw-loader!./snippets/MediaAccessMicCameraCapabilities.snippet.tsx';
5+
import MediaAccessRemoteParticipantsText from '!!raw-loader!./snippets/MediaAccessRemoteParticipants.snippet.tsx';
6+
7+
<Meta title="Concepts/MediaAccess" />
8+
9+
# Media access
10+
11+
The media access feature in Teams meetings allows the Organizer, Co-organizer, and Presenter to control whether attendees can enable their mic or camera.
12+
This can be managed through the Teams meeting options “Allow mic/camera for attendees” or on a per-participant basis with the options “Disable mic/camera” and “Enable mic/camera.”
13+
14+
Teams meeting attendees can check their own media access state using the capabilities `unMuteMic` and `turnVideoOn`, or view the media states for remote participants.
15+
16+
ACS users must have the Organizer, Co-organizer, or Presenter role to use the media access feature.
17+
18+
The supported scenarios for the media access feature are:
19+
20+
- Teams Interop Meetings
21+
- Teams Interop Meetings as a Teams user
22+
- Teams ad-hoc call
23+
24+
Participants can disable/enable audio/video using the contextual menu button on their video gallery tile like shown below:
25+
26+
<img
27+
style={{ width: 'auto', height: 'auto' }}
28+
src="images/media-access/media-access-disable-mic-camera-video-tile.png"
29+
/>
30+
31+
Participants can also disable/enable audio/video using the contextual menu button on their participant item in the people pane like
32+
shown below:
33+
34+
<img
35+
style={{ width: 'auto', height: 'auto' }}
36+
src="images/media-access/media-access-disable-mic-camera-people-pane.png"
37+
/>
38+
39+
A local participant with audio or video disabled will see a mic or camera disabled icon on the control bar, notifications that mic and camera have been disabled and will not be able to unmute or turn the video on, as shown below:
40+
41+
<img
42+
style={{ width: 'auto', height: 'auto' }}
43+
src="images/media-access/media-access-local-participant-mic-camera-disabled.png"
44+
/>
45+
46+
The concept of the media access feature is the same in Microsoft Teams which you can read
47+
more about here -
48+
[Manage attendee audio and video permissions in Microsoft Teams meetings](https://support.microsoft.com/en-us/office/manage-attendee-audio-and-video-permissions-in-microsoft-teams-meetings-f9db15e1-f46f-46da-95c6-34f9f39e671a).
49+
50+
## Listening to local participant `unmuteMic` and `turnVideoOn` capabilities changes
51+
52+
You can listen to `capabilitiesChanged` events on the CallAdapter or CallWithChatAdapter by defining your own
53+
`capabilitiesChangedListener` callback. The following code snippet shows an example of listening to `capabilitiesChanged`
54+
events on the CallAdapter to log the added and removed participants to the browser console. But you can choose to
55+
do more if you wish.
56+
57+
<Source code={MediaAccessMicCameraCapabilitiesText} />
58+
59+
Note: Assigning a `capabilitiesChangedListener` callback to listen for 'capabilitiesChanged' events will not override the
60+
behavior of CallComposite and CallWithChatComposite which places participants in the main view of
61+
VideoGallery.
62+
63+
## UI component to use remote participant(s) media access state
64+
65+
If you want to build your own UI components using the media access states we suggest using the `VideoGallerySelector` or creating your own custom selector to pipe in the media access states for remote participants to your UI component.
66+
Here is an example of how we put to together the VideoGallery component with these media access states.
67+
68+
<Source code={MediaAccessRemoteParticipantsText} />
69+
70+
## Programatic media access for participants
71+
72+
The CallAdapter and CallWithChatAdapter can also be used to programatically change media access one or more participants using
73+
the functions `forbidAudio`, `permitAudio`, `forbidVideo`, `permitVideo`, `forbidOthersAudio`, `permitOthersAudio`, `forbidOthersVideo` and `permitOthersVideo`.
74+
The example below shows a code snippet where a button is added to invoke the `forbidAudio` and `permitAudio` function to change media access state
75+
for remote participants from an added dropdown that is populated by remote participants in the call.
76+
77+
<Source code={CustomMediaAccessCompositeText} />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { AzureCommunicationTokenCredential, CommunicationUserIdentifier } from '@azure/communication-common';
2+
import {
3+
CallComposite,
4+
CompositeLocale,
5+
toFlatCommunicationIdentifier,
6+
useAzureCommunicationCallAdapter
7+
} from '@azure/communication-react';
8+
import { Dropdown, IDropdownOption, PartialTheme, PrimaryButton, Theme } from '@fluentui/react';
9+
import React, { useMemo, useState } from 'react';
10+
11+
export type ContainerProps = {
12+
userId: CommunicationUserIdentifier;
13+
token: string;
14+
meetingLink: string;
15+
formFactor?: 'desktop' | 'mobile';
16+
fluentTheme?: PartialTheme | Theme;
17+
locale?: CompositeLocale;
18+
};
19+
20+
export const ContosoCallContainer = (props: ContainerProps): JSX.Element => {
21+
// Keep state of the selected participants to toggle audio
22+
const [selectedParticipants, setSelectedParticipants] = useState<string[]>([]);
23+
24+
const credential = useMemo(() => {
25+
try {
26+
return new AzureCommunicationTokenCredential(props.token);
27+
} catch {
28+
console.error('Failed to construct token credential');
29+
return undefined;
30+
}
31+
}, [props.token]);
32+
33+
const callAdapterArgs = useMemo(
34+
() => ({
35+
userId: props.userId,
36+
credential,
37+
locator: {
38+
meetingLink: props.meetingLink
39+
}
40+
}),
41+
[props.userId, credential, props.meetingLink]
42+
);
43+
44+
const adapter = useAzureCommunicationCallAdapter(callAdapterArgs);
45+
46+
const participantsOptions = useMemo(
47+
() =>
48+
Object.values(adapter?.getState().call?.remoteParticipants ?? {}).map((participant) => ({
49+
key: toFlatCommunicationIdentifier(participant.identifier),
50+
text: participant.displayName ?? 'Unnamed participant'
51+
})),
52+
[adapter]
53+
);
54+
55+
const onChange = (event: React.FormEvent<HTMLDivElement>, item?: IDropdownOption): void => {
56+
if (item) {
57+
setSelectedParticipants(
58+
item.selected
59+
? [...selectedParticipants, item.key as string]
60+
: selectedParticipants.filter((key) => key !== item.key)
61+
);
62+
}
63+
};
64+
65+
if (adapter) {
66+
return (
67+
<div style={{ height: '90vh', width: '90vw' }}>
68+
<CallComposite
69+
adapter={adapter}
70+
formFactor={props.formFactor}
71+
fluentTheme={props.fluentTheme}
72+
locale={props?.locale}
73+
/>
74+
<Dropdown
75+
placeholder="Select participants to toggle audio"
76+
label="Select participants"
77+
selectedKeys={selectedParticipants}
78+
onChange={onChange}
79+
multiSelect
80+
options={participantsOptions}
81+
/>
82+
<PrimaryButton
83+
onClick={() => {
84+
if (selectedParticipants && selectedParticipants.length > 0) {
85+
adapter.forbidAudio(selectedParticipants);
86+
}
87+
}}
88+
disabled={!selectedParticipants || selectedParticipants.length === 0}
89+
>
90+
Disable mic for attendee(s)
91+
</PrimaryButton>
92+
<PrimaryButton
93+
onClick={() => {
94+
if (selectedParticipants && selectedParticipants.length > 0) {
95+
adapter.permitAudio(selectedParticipants);
96+
}
97+
}}
98+
disabled={!selectedParticipants || selectedParticipants.length === 0}
99+
>
100+
Enable mic for attendee(s)
101+
</PrimaryButton>
102+
</div>
103+
);
104+
}
105+
return <>Initializing...</>;
106+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { CapabilitiesChangeInfo } from '@azure/communication-calling';
2+
import { AzureCommunicationTokenCredential, CommunicationUserIdentifier } from '@azure/communication-common';
3+
import {
4+
CallAdapter,
5+
CallComposite,
6+
CallCompositeOptions,
7+
CompositeLocale,
8+
useAzureCommunicationCallAdapter
9+
} from '@azure/communication-react';
10+
import { PartialTheme, Theme } from '@fluentui/react';
11+
import React, { useCallback, useMemo } from 'react';
12+
13+
export type ContainerProps = {
14+
userId: CommunicationUserIdentifier;
15+
token: string;
16+
formFactor?: 'desktop' | 'mobile';
17+
fluentTheme?: PartialTheme | Theme;
18+
locale?: CompositeLocale;
19+
options?: CallCompositeOptions;
20+
meetingLink?: string;
21+
};
22+
23+
export const ContosoCallContainer = (props: ContainerProps): JSX.Element => {
24+
const credential = useMemo(() => {
25+
try {
26+
return new AzureCommunicationTokenCredential(props.token);
27+
} catch {
28+
console.error('Failed to construct token credential');
29+
return undefined;
30+
}
31+
}, [props.token]);
32+
33+
const callAdapterArgs = useMemo(
34+
() => ({
35+
userId: props.userId,
36+
credential,
37+
locator: props.meetingLink
38+
? {
39+
meetingLink: props.meetingLink
40+
}
41+
: undefined
42+
}),
43+
[props.userId, credential, props.meetingLink]
44+
);
45+
46+
/**
47+
* Logging local participants' Media access state with capabilitiesChanged event
48+
* unmuteMic: true if the user can unmute the microphone, false otherwise
49+
* turnVideoOn: true if the user can turn on the video, false otherwise
50+
*/
51+
const afterCallAdapterCreate = useCallback(async (adapter: CallAdapter): Promise<CallAdapter> => {
52+
adapter.on('capabilitiesChanged', (capabilitiesChangeInfo: CapabilitiesChangeInfo) => {
53+
if (capabilitiesChangeInfo.newValue.unmuteMic !== undefined) {
54+
console.log('unmuteMic capabilities changed info: ', capabilitiesChangeInfo);
55+
}
56+
if (capabilitiesChangeInfo.newValue.turnVideoOn !== undefined) {
57+
console.log('turnVideoOn capabilities changed info: ', capabilitiesChangeInfo);
58+
}
59+
});
60+
return adapter;
61+
}, []);
62+
63+
const adapter = useAzureCommunicationCallAdapter(callAdapterArgs, afterCallAdapterCreate);
64+
65+
if (!props.meetingLink) {
66+
return <>Teams meeting link is not provided.</>;
67+
}
68+
69+
if (adapter) {
70+
return (
71+
<div style={{ height: '90vh', width: '90vw' }}>
72+
<CallComposite
73+
adapter={adapter}
74+
formFactor={props.formFactor}
75+
fluentTheme={props.fluentTheme}
76+
locale={props?.locale}
77+
options={props?.options}
78+
/>
79+
</div>
80+
);
81+
}
82+
if (credential === undefined) {
83+
return <>Failed to construct credential. Provided token is malformed.</>;
84+
}
85+
return <>Initializing...</>;
86+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { VideoGallery, usePropsFor } from '@azure/communication-react';
2+
import React from 'react';
3+
4+
export const CallScreen = (): JSX.Element => {
5+
// Use usePropsFor to get properties for VideoGallery
6+
const videoGalleryProps = usePropsFor(VideoGallery);
7+
8+
// Logging remote participants' media access state without modifying the array
9+
videoGalleryProps.remoteParticipants.forEach((participant) => {
10+
console.log(
11+
`Participant [${participant.userId}:${participant.displayName}]'s media access:`,
12+
participant.mediaAccess
13+
);
14+
});
15+
16+
// Display VideoGallery
17+
return <VideoGallery {...videoGalleryProps} />;
18+
};

0 commit comments

Comments
 (0)