Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add story book for hard mute feature #5456

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
74 changes: 74 additions & 0 deletions packages/storybook8/stories/Concepts/MediaAccess/Doc.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Meta, Source } from '@storybook/addon-docs';

import MediaAccessMicCameraCapabilitiesText from '!!raw-loader!./snippets/MediaAccessMicCameraCapabilities.snippet.tsx';
import MediaAccessRemoteParticipantsText from '!!raw-loader!./snippets/MediaAccessRemoteParticipants.snippet.tsx';
import CustomMediaAccessCompositeText from '!!raw-loader!./snippets/MediaAccessComposite.snippet.tsx';

<Meta title="Concepts/MediaAccess" />

# Media access

The media access feature in Teams meetings allows the Organizer, Co-organizer, and Presenter to control whether attendees can enable their mic or camera.
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.”

Teams meeting attendees can check their own media access state using the capabilities unMuteMic and turnVideoOn, or view the media states for remote participants.

ACS users must have the Organizer, Co-organizer, or Presenter role to use the media access feature.

The supported scenarios for the media access feature are:

- Teams Interop Meetings
- Teams Interop Meetings as a Teams user
- Teams ad-hoc call

Participants can disable/enable audio/video using the contextual menu button on their video gallery tile like shown below:

<img
style={{ width: 'auto', height: 'auto' }}
src="images/media-access/media-access-disable-mic-camera-video-tile.png"
/>

Participants can also disable/enable audio/video using the contextual menu button on their participant item in the people pane like
shown below:

<img
style={{ width: 'auto', height: 'auto' }}
src="images/media-access/media-access-disable-mic-camera-people-pane.png"
/>

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:

<img
style={{ width: 'auto', height: 'auto' }}
src="images/media-access/media-access-local-participant-mic-camera-disabled.png"
/>

The concept of the media access feature is the same in Microsoft Teams which you can read
more about here -
[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).

## Listening to local participant unmuteMic and turnVideoOn capabilities changes

You can listen to 'capabilitiesChanged' events on the CallAdapter or CallWithChatAdapter by defining your own
`capabilitiesChangedListener` callback. The following code snippet shows an example of listening to 'capabilitiesChanged'
events on the CallAdapter to log the added and removed participants to the browser console. But you can choose to
do more if you wish.

<Source code={MediaAccessMicCameraCapabilitiesText} />

Note: Assigning a `capabilitiesChangedListener` callback to listen for 'capabilitiesChanged' events will not override the
behavior of CallComposite and CallWithChatComposite which places spotlighted participants in the main view of
VideoGallery.

## Get remote participant(s) media access states

<Source code={MediaAccessRemoteParticipantsText} />

## Programatic media access for participants

The CallAdapter and CallWithChatAdapter can also be used to programatically change media access one or more participants using
the functions `forbidAudio`, `permitAudio`, `forbidVideo`, `permitVideo`, `forbidOthersAudio`, `permitOthersAudio`, `forbidOthersVideo` and `permitOthersVideo`.
The example below shows a code snippet where a button is added to invoke the `forbidAudio` and `permitAudio` function to change media access state
for remote participants from an added dropdown that is populated by remote participants in the call.

<Source code={CustomMediaAccessCompositeText} />
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { AzureCommunicationTokenCredential, CommunicationUserIdentifier } from '@azure/communication-common';
import {
CallComposite,
CompositeLocale,
toFlatCommunicationIdentifier,
useAzureCommunicationCallAdapter
} from '@azure/communication-react';
import { Dropdown, IDropdownOption, PartialTheme, PrimaryButton, Theme } from '@fluentui/react';
import React, { useMemo, useState } from 'react';

export type ContainerProps = {
userId: CommunicationUserIdentifier;
token: string;
meetingLink: string;
formFactor?: 'desktop' | 'mobile';
fluentTheme?: PartialTheme | Theme;
locale?: CompositeLocale;
};

export const ContosoCallContainer = (props: ContainerProps): JSX.Element => {
// Keep state of the selected participants to toggle audio
const [selectedParticipants, setSelectedParticipants] = useState<string[]>([]);

const credential = useMemo(() => {
try {
return new AzureCommunicationTokenCredential(props.token);
} catch {
console.error('Failed to construct token credential');
return undefined;
}
}, [props.token]);

const callAdapterArgs = useMemo(
() => ({
userId: props.userId,
credential,
locator: {
meetingLink: props.meetingLink
}
}),
[props.userId, credential, props.meetingLink]
);

const adapter = useAzureCommunicationCallAdapter(callAdapterArgs);

const participantsOptions = useMemo(
() =>
Object.values(adapter?.getState().call?.remoteParticipants ?? {}).map((participant) => ({
key: toFlatCommunicationIdentifier(participant.identifier),
text: participant.displayName ?? 'Unnamed participant'
})),
[adapter]
);

const onChange = (event: React.FormEvent<HTMLDivElement>, item?: IDropdownOption): void => {
if (item) {
setSelectedParticipants(
item.selected
? [...selectedParticipants, item.key as string]
: selectedParticipants.filter((key) => key !== item.key)
);
}
};

if (adapter) {
return (
<div style={{ height: '90vh', width: '90vw' }}>
<CallComposite
adapter={adapter}
formFactor={props.formFactor}
fluentTheme={props.fluentTheme}
locale={props?.locale}
/>
<Dropdown
placeholder="Select participants to toggle audio"
label="Select participants"
selectedKeys={selectedParticipants}
onChange={onChange}
multiSelect
options={participantsOptions}
/>
<PrimaryButton
onClick={() => {
if (selectedParticipants && selectedParticipants.length > 0) {
adapter.forbidAudio(selectedParticipants);
}
}}
disabled={!selectedParticipants || selectedParticipants.length === 0}
>
Disable mic for attendee(s)
</PrimaryButton>
<PrimaryButton
onClick={() => {
if (selectedParticipants && selectedParticipants.length > 0) {
adapter.permitAudio(selectedParticipants);
}
}}
disabled={!selectedParticipants || selectedParticipants.length === 0}
>
Enable mic for attendee(s)
</PrimaryButton>
</div>
);
}
return <>Initializing...</>;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { CapabilitiesChangeInfo } from '@azure/communication-calling';
import { AzureCommunicationTokenCredential, CommunicationUserIdentifier } from '@azure/communication-common';
import {
CallAdapter,
CallComposite,
CallCompositeOptions,
CompositeLocale,
useAzureCommunicationCallAdapter
} from '@azure/communication-react';
import { PartialTheme, Theme } from '@fluentui/react';
import React, { useCallback, useMemo } from 'react';

export type ContainerProps = {
userId: CommunicationUserIdentifier;
token: string;
formFactor?: 'desktop' | 'mobile';
fluentTheme?: PartialTheme | Theme;
locale?: CompositeLocale;
options?: CallCompositeOptions;
meetingLink?: string;
};

export const ContosoCallContainer = (props: ContainerProps): JSX.Element => {
const credential = useMemo(() => {
try {
return new AzureCommunicationTokenCredential(props.token);
} catch {
console.error('Failed to construct token credential');
return undefined;
}
}, [props.token]);

const callAdapterArgs = useMemo(
() => ({
userId: props.userId,
credential,
locator: props.meetingLink
? {
meetingLink: props.meetingLink
}
: undefined
}),
[props.userId, credential, props.meetingLink]
);

/**
* Logging local participants' Media access state with capabilitiesChanged event
* unmuteMic: true if the user can unmute the microphone, false otherwise
* turnVideoOn: true if the user can turn on the video, false otherwise
*/
const afterCallAdapterCreate = useCallback(async (adapter: CallAdapter): Promise<CallAdapter> => {
adapter.on('capabilitiesChanged', (capabilitiesChangeInfo: CapabilitiesChangeInfo) => {
if (capabilitiesChangeInfo.newValue.unmuteMic !== undefined) {
console.log('unmuteMic capabilities changed info: ', capabilitiesChangeInfo);
}
if (capabilitiesChangeInfo.newValue.turnVideoOn !== undefined) {
console.log('turnVideoOn capabilities changed info: ', capabilitiesChangeInfo);
}
});
return adapter;
}, []);

const adapter = useAzureCommunicationCallAdapter(callAdapterArgs, afterCallAdapterCreate);

if (!props.meetingLink) {
return <>Teams meeting link is not provided.</>;
}

if (adapter) {
return (
<div style={{ height: '90vh', width: '90vw' }}>
<CallComposite
adapter={adapter}
formFactor={props.formFactor}
fluentTheme={props.fluentTheme}
locale={props?.locale}
options={props?.options}
/>
</div>
);
}
if (credential === undefined) {
return <>Failed to construct credential. Provided token is malformed.</>;
}
return <>Initializing...</>;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { VideoGallery, usePropsFor } from '@azure/communication-react';
import React from 'react';

export const CallScreen = (): JSX.Element => {
// Use usePropsFor to get properties for VideoGallery
const videoGalleryProps = usePropsFor(VideoGallery);

// Logging remote participants' Media access state without modifying the array
videoGalleryProps.remoteParticipants.forEach((participant) => {
console.log('Participant media access:', participant.mediaAccess);
});

// Display VideoGallery
return <VideoGallery {...videoGalleryProps} />;
};
Loading