зеркало из
1
0
Форкнуть 0

Add example of Teams interoperation (#172)

Add an example for how an application might show a banner when a Teams call is being recorded or transcribed.
This commit is contained in:
Prathmesh Prabhu 2021-04-27 16:28:50 -07:00 коммит произвёл GitHub
Родитель f32d1a3d46
Коммит 82250083c2
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
6 изменённых файлов: 387 добавлений и 0 удалений

Просмотреть файл

@ -0,0 +1,27 @@
import { Description, Heading, Source, Title } from '@storybook/addon-docs/blocks';
import React from 'react';
const CallComponentText = require('!!raw-loader!./snippets/CallComponent.snippet.tsx').default;
const TeamsInteropText = require('!!raw-loader!./snippets/TeamsInterop.snippet.tsx').default;
export const getDocs: () => JSX.Element = () => {
return (
<>
<Title>Teams Interop</Title>
<Description>
Azure Communication Services applications can inter-operate with Microsoft Teams. There are some additional
considerations when connecting to a Teams meeting.
</Description>
<Heading>Compliance notifications for recording and transcription</Heading>
<Description>
This example shows how you might notify your users when a Teams meeting is being recorded or transcribed. Here,
a MessageBar is optionally added to the video frame:
</Description>
<Source code={CallComponentText} />
<Description>
The state machine tracking when and what banner to display can be encapsulated in a vanilla Typescript package:
</Description>
<Source code={TeamsInteropText} />
</>
);
};

Просмотреть файл

@ -0,0 +1,46 @@
import { button } from '@storybook/addon-knobs';
import { Meta } from '@storybook/react/types-6-0';
import React, { useState } from 'react';
import { EXAMPLES_FOLDER_PREFIX } from '../../constants';
import { CallComponent } from './snippets/CallComponent.snippet';
import { getDocs } from './Docs';
export const NoticeBanner: () => JSX.Element = () => {
const [teamsInterop, setTeamsInterop] = useState({
recordingEnabled: false,
transcriptionEnabled: false
});
button('Toggle Recording', () => {
setTeamsInterop({
recordingEnabled: !teamsInterop.recordingEnabled,
transcriptionEnabled: teamsInterop.transcriptionEnabled
});
// Without an explicit return, the Canvas iframe is re-rendered, and all Components are recreated.
// This causes the state in this component to be lost.
return false;
});
button('Toggle Transcription', () => {
setTeamsInterop({
recordingEnabled: teamsInterop.recordingEnabled,
transcriptionEnabled: !teamsInterop.transcriptionEnabled
});
// Without an explicit return, the Canvas iframe is re-rendered, and all Components are recreated.
// This causes the state in this component to be lost.
return false;
});
// TODO: Fix dark theming.
// Once https://github.com/Azure/communication-ui-sdk/pull/169 lands, same fix should be applied here.
return <CallComponent {...teamsInterop} />;
};
export default {
title: `${EXAMPLES_FOLDER_PREFIX}/TeamsInterop`,
component: NoticeBanner,
parameters: {
docs: {
page: () => getDocs()
}
}
} as Meta;

Просмотреть файл

@ -0,0 +1,37 @@
import { MessageBar } from '@fluentui/react';
import React, { useRef } from 'react';
import { bannerMessage, TeamsInterop } from './TeamsInterop.snippet';
export const Banner = (props: TeamsInterop): JSX.Element => {
const history = useRef({
teamsInteropCurrent: {
recordingEnabled: false,
transcriptionEnabled: false
},
teamsInteropPrevious: {
recordingEnabled: false,
transcriptionEnabled: false
}
});
// Only update history if props differ from the latest snapshot in the history.
// This avoids jank caused by duplicate renders without any intervening prop update.
if (
props.recordingEnabled !== history.current.teamsInteropCurrent.recordingEnabled ||
props.transcriptionEnabled !== history.current.teamsInteropCurrent.transcriptionEnabled
) {
history.current = {
teamsInteropCurrent: {
recordingEnabled: props.recordingEnabled,
transcriptionEnabled: props.transcriptionEnabled
},
teamsInteropPrevious: {
recordingEnabled: history.current.teamsInteropCurrent.recordingEnabled,
transcriptionEnabled: history.current.teamsInteropCurrent.transcriptionEnabled
}
};
}
const msg = bannerMessage(history.current);
return msg !== null ? <MessageBar>{msg}</MessageBar> : <></>;
};

Просмотреть файл

@ -0,0 +1,36 @@
import { StreamMedia, VideoTile } from '@azure/communication-ui';
import React from 'react';
import { renderVideoStream } from '../../../utils';
import { Banner } from './Banner.snippet';
import { CallControlBar } from './CallControlBar.snippet';
import { TeamsInterop } from './TeamsInterop.snippet';
export interface CallProps {
teamsInteropCurrent: TeamsInterop;
teamsInteropPrevious: TeamsInterop;
}
export const CallComponent = (props: TeamsInterop): JSX.Element => {
const videoTileStyles = {
root: { height: '100%', width: '100%' },
overlayContainer: {}
};
return (
<VideoTile
styles={videoTileStyles}
invertVideo={true}
isVideoReady={true}
videoProvider={
// Replace with your own video provider.
<StreamMedia videoStreamElement={renderVideoStream()} />
}
placeholderProvider={<></>}
>
{/* Optional Banner */}
<Banner {...props} />
<CallControlBar />
</VideoTile>
);
};

Просмотреть файл

@ -0,0 +1,24 @@
import {
ControlBar,
labeledAudioButtonProps,
labeledHangupButtonProps,
labeledOptionsButtonProps,
labeledVideoButtonProps
} from '@azure/communication-ui';
import { DefaultButton } from '@fluentui/react';
import React from 'react';
// TODO: Add unique keys to the list here.
export const CallControlBar = (): JSX.Element => {
return (
<ControlBar
layout="dockedBottom"
styles={{ root: { background: 'white', minHeight: '4.25rem', alignItems: 'center' } }}
>
<DefaultButton {...labeledVideoButtonProps} checked={true} />
<DefaultButton {...labeledAudioButtonProps} checked={true} />
<DefaultButton {...labeledOptionsButtonProps} />
<DefaultButton {...labeledHangupButtonProps} style={{ borderRadius: '0.25rem', marginLeft: '0.25rem' }} />
</ControlBar>
);
};

Просмотреть файл

@ -0,0 +1,217 @@
import { Link } from '@fluentui/react';
import React from 'react';
// State tracking for Teams interop.
//
// In a real application, this would be fetched from ACS APIs.
export interface TeamsInterop {
recordingEnabled: boolean;
transcriptionEnabled: boolean;
}
// Historical state of Teams interop, for computing state transition events.
export interface TeamsInteropHistory {
teamsInteropCurrent: TeamsInterop;
teamsInteropPrevious: TeamsInterop;
}
// Return the banner message to display for teams interop.
//
// Returns null if no banner needs to be displayed.
export function bannerMessage(props: TeamsInteropHistory): JSX.Element | null {
/*
This function implements a single transition in a state machine.
Nodes of the state machine correspond to the possible values of the TeamsInterp tuple:
(recordingEnabled, transcriptionEnabled).
Thus there are 4 distinct states.
There are 12 transitions between the 4 states.
The diagram below omits the transitions 2->3, 3->2, 1->4 and 4->1 for clarity.
1. 2.
1->2
! recording ! recording
! transcribing transcribing
1->3 2->1 2->4
3->1 4->2
3->4
recording recording
! transcribing transcribing
3. 4->3 4.
The implementation consists of nested if blocks. The outer conditional selects the source node
and the inner conditional selects the transition out of the node.
*/
const prev = props.teamsInteropPrevious;
const cur = props.teamsInteropCurrent;
// Source: 4
if (prev.recordingEnabled && prev.transcriptionEnabled) {
// Transition: 4 -> 3
if (cur.recordingEnabled && !cur.transcriptionEnabled) {
return (
<>
<b>Transcription has stopped.</b> You are now only recording this meeting.
<PrivacyPolicy />
</>
);
}
// Transition: 4 -> 2
if (!cur.recordingEnabled && cur.transcriptionEnabled) {
return (
<>
<b>Recording has stopped.</b> You are now only transcribing this meeting.
<PrivacyPolicy />
</>
);
}
// Transition: 4 -> 1
if (!cur.recordingEnabled && !cur.transcriptionEnabled) {
return (
<>
<b>Recording and transcription are being saved. </b> Recording and transcription have stopped.
<LearnMore />
</>
);
}
return null;
}
// Source: 3
if (prev.recordingEnabled && !prev.transcriptionEnabled) {
// Transition: 3 -> 4
if (cur.recordingEnabled && cur.transcriptionEnabled) {
return (
<>
<b>Recording and transcription have started.</b> By joining, you are giving consent for this meeting to be
transcribed.
<PrivacyPolicy />
</>
);
}
// Transition: 3 -> 2
if (!cur.recordingEnabled && cur.transcriptionEnabled) {
// This could be considered either:
// - Recording was stopped
// - Transcription was started.
// We prefer notifying users of a start event because it links to the privacy policy.
return (
<>
<b>Transcription has started.</b> By joining, you are giving consent for this meeting to be transcribed.
<PrivacyPolicy />
</>
);
}
// Transition: 3 -> 1
if (!cur.recordingEnabled && !cur.transcriptionEnabled) {
return (
<>
<b>Recording is being saved.</b> Recording has stopped.
<LearnMore />
</>
);
}
return null;
}
// Source: 2
if (!prev.recordingEnabled && prev.transcriptionEnabled) {
// Transition: 3 -> 4
if (cur.recordingEnabled && cur.transcriptionEnabled) {
return (
<>
<b>Recording and transcription have started.</b> By joining, you are giving consent for this meeting to be
transcribed.
<PrivacyPolicy />
</>
);
}
// Transition: 3 -> 2
if (!cur.recordingEnabled && cur.transcriptionEnabled) {
// This could be considered either:
// - Transcription was stopped
// - Recording was started.
// We prefer notifying users of a start event because it links to the privacy policy.
return (
<>
<b>Recording has started.</b> By joining, you are giving consent for this meeting to be transcribed.
<PrivacyPolicy />
</>
);
}
// Transition: 3 -> 1
if (!cur.recordingEnabled && !cur.transcriptionEnabled) {
return (
<>
<b>Transcription is being saved.</b> Transcription has stopped.
<LearnMore />
</>
);
}
return null;
}
// Source: 1
if (!prev.recordingEnabled && !prev.transcriptionEnabled) {
// Transition: 1 -> 4
if (cur.recordingEnabled && cur.transcriptionEnabled) {
return (
<>
<b>Recording and transcription have started.</b> By joining, you are giving consent for this meeting to be
transcribed.
<PrivacyPolicy />
</>
);
}
// Transition: 1 -> 3
if (cur.recordingEnabled && !cur.transcriptionEnabled) {
return (
<>
<b>Recording has started.</b> By joining, you are giving consent for this meeting to be transcribed.
<PrivacyPolicy />
</>
);
}
// Transition: 1 -> 2
if (!cur.recordingEnabled && cur.transcriptionEnabled) {
return (
<>
<b>Transcription has started.</b> By joining, you are giving consent for this meeting to be transcribed.
<PrivacyPolicy />
</>
);
}
return null;
}
return null;
}
function PrivacyPolicy(): JSX.Element {
return (
<Link
href="https://privacy.microsoft.com/en-US/privacystatement#mainnoticetoendusersmodule"
target="_blank"
underline
>
Privacy policy
</Link>
);
}
function LearnMore(): JSX.Element {
return (
<Link
href="https://support.microsoft.com/en-us/office/record-a-meeting-in-teams-34dfbe7f-b07d-4a27-b4c6-de62f1348c24"
target="_blank"
underline
>
Learn more
</Link>
);
}