Skip to content

Commit a0eefbf

Browse files
Merge pull request #251 from Azure-Samples/elee/rtt
Add RealTimeText to sample
2 parents 84c6524 + f564794 commit a0eefbf

File tree

4 files changed

+273
-37
lines changed

4 files changed

+273
-37
lines changed

Project/src/App.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1148,6 +1148,13 @@ div.volumeVisualizer::before {
11481148
transform: translateZ(0);
11491149
}
11501150

1151+
.scrollable-rtt-container {
1152+
overflow: auto;
1153+
max-height: 300px;
1154+
display: flex;
1155+
flex-direction: column-reverse;
1156+
}
1157+
11511158
.custom-video-effects-buttons:not(.outgoing) {
11521159
display: flex;
11531160
position: absolute;

Project/src/MakeCall/CallCaption.js

Lines changed: 48 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@ import { Dropdown } from 'office-ui-fabric-react/lib/Dropdown';
55
// CallCaption react function component
66
const CallCaption = ({ call }) => {
77
const [captionsFeature, setCaptionsFeature] = useState(call.feature(Features.Captions));
8+
const [capabilitiesFeature, setCapabilitiesFeature] = useState(call.feature(Features.Capabilities));
89
const [captions, setCaptions] = useState(captionsFeature.captions);
910
const [currentSpokenLanguage, setCurrentSpokenLanguage] = useState(captions.activeSpokenLanguage);
10-
const [currentCaptionLanguage, setCurrentCaptionLanguage] = useState(captions.activeCaptionLanguage);
11+
const [currentCaptionLanguage, setCurrentCaptionLanguage] = useState(null);
12+
let captionLanguageCurrent = null;
13+
1114

1215
useEffect(() => {
1316
try {
@@ -23,7 +26,7 @@ const CallCaption = ({ call }) => {
2326
captions.off('CaptionsActiveChanged', captionsActiveHandler);
2427
captions.off('CaptionsReceived', captionsReceivedHandler);
2528
captions.off('SpokenLanguageChanged', activeSpokenLanguageHandler);
26-
if (captions.captionsType === 'TeamsCaptions') {
29+
if (captions.kind === 'TeamsCaptions' && capabilitiesFeature.capabilities.setCaptionLanguage?.isPresent) {
2730
captions.off('CaptionLanguageChanged', activeCaptionLanguageHandler);
2831
}
2932
};
@@ -37,7 +40,12 @@ const CallCaption = ({ call }) => {
3740
captions.on('CaptionsActiveChanged', captionsActiveHandler);
3841
captions.on('CaptionsReceived', captionsReceivedHandler);
3942
captions.on('SpokenLanguageChanged', activeSpokenLanguageHandler);
40-
if (captions.captionsType === 'TeamsCaptions') {
43+
capabilitiesFeature.on('CapabilitiesChanged', (value) => {
44+
if (value.newValue.setCaptionLanguage) {
45+
setCapabilitiesFeature(call.feature(Features.Capabilities));
46+
}
47+
});
48+
if (captions.kind === 'TeamsCaptions' && capabilitiesFeature.capabilities.setCaptionLanguage?.isPresent) {
4149
captions.on('CaptionLanguageChanged', activeCaptionLanguageHandler);
4250
}
4351
} catch (e) {
@@ -55,46 +63,49 @@ const CallCaption = ({ call }) => {
5563

5664
const captionsActiveHandler = () => {
5765
console.log('CaptionsActiveChanged: ', captions.isCaptionsFeatureActive);
66+
setCurrentSpokenLanguage(captions.activeSpokenLanguage);
67+
setCurrentCaptionLanguage(captions.activeCaptionLanguage);
5868
}
5969
const activeSpokenLanguageHandler = () => {
6070
setCurrentSpokenLanguage(captions.activeSpokenLanguage);
6171
}
6272
const activeCaptionLanguageHandler = () => {
6373
setCurrentCaptionLanguage(captions.activeCaptionLanguage);
74+
captionLanguageCurrent = captions.activeCaptionLanguage;
6475
}
6576

6677
const captionsReceivedHandler = (captionData) => {
67-
let mri = '';
68-
if (captionData.speaker.identifier.kind === 'communicationUser') {
69-
mri = captionData.speaker.identifier.communicationUserId;
70-
} else if (captionData.speaker.identifier.kind === 'microsoftTeamsUser') {
71-
mri = captionData.speaker.identifier.microsoftTeamsUserId;
72-
} else if (captionData.speaker.identifier.kind === 'phoneNumber') {
73-
mri = captionData.speaker.identifier.phoneNumber;
74-
}
75-
76-
let captionAreasContainer = document.getElementById('captionsArea');
77-
const newClassName = `prefix${mri.replace(/:/g, '').replace(/-/g, '').replace(/\+/g, '')}`;
78-
const captionText = `${captionData.timestamp.toUTCString()}
79-
${captionData.speaker.displayName}: ${captionData.captionText ?? captionData.spokenText}`;
80-
81-
let foundCaptionContainer = captionAreasContainer.querySelector(`.${newClassName}[isNotFinal='true']`);
82-
if (!foundCaptionContainer) {
83-
let captionContainer = document.createElement('div');
84-
captionContainer.setAttribute('isNotFinal', 'true');
85-
captionContainer.style['borderBottom'] = '1px solid';
86-
captionContainer.style['whiteSpace'] = 'pre-line';
87-
captionContainer.textContent = captionText;
88-
captionContainer.classList.add(newClassName);
89-
captionContainer.classList.add('caption-item')
90-
91-
captionAreasContainer.appendChild(captionContainer);
92-
93-
} else {
94-
foundCaptionContainer.textContent = captionText;
95-
96-
if (captionData.resultType === 'Final') {
97-
foundCaptionContainer.setAttribute('isNotFinal', 'false');
78+
if (!captionLanguageCurrent || captionLanguageCurrent === captionData.captionLanguage) {
79+
let mri = '';
80+
switch (captionData.speaker.identifier.kind) {
81+
case 'communicationUser': { mri = captionData.speaker.identifier.communicationUserId; break; }
82+
case 'microsoftTeamsUser': { mri = captionData.speaker.identifier.microsoftTeamsUserId; break; }
83+
case 'phoneNumber': { mri = captionData.speaker.identifier.phoneNumber; break; }
84+
}
85+
let captionAreasContainer = document.getElementById('captionsArea');
86+
const newClassName = `prefix${mri.replace(/:/g, '').replace(/-/g, '').replace(/\+/g, '')}`;
87+
const captionText = `${captionData.timestamp.toUTCString()}
88+
${captionData.speaker.displayName ?? mri}: ${captionData.captionText ?? captionData.spokenText}`;
89+
90+
let foundCaptionContainer = captionAreasContainer.querySelector(`.${newClassName}[isNotFinal='true']`);
91+
92+
if (!foundCaptionContainer) {
93+
let captionContainer = document.createElement('div');
94+
captionContainer.setAttribute('isNotFinal', 'true');
95+
captionContainer.style['borderBottom'] = '1px solid';
96+
captionContainer.style['whiteSpace'] = 'pre-line';
97+
captionContainer.textContent = captionText;
98+
captionContainer.classList.add(newClassName);
99+
captionContainer.classList.add('caption-item')
100+
101+
captionAreasContainer.appendChild(captionContainer);
102+
103+
} else {
104+
foundCaptionContainer.textContent = captionText;
105+
106+
if (captionData.resultType === 'Final') {
107+
foundCaptionContainer.setAttribute('isNotFinal', 'false');
108+
}
98109
}
99110
}
100111
};
@@ -113,7 +124,7 @@ const CallCaption = ({ call }) => {
113124
onChange={spokenLanguageSelectionChanged}
114125
label={'Spoken Language'}
115126
options={keyedSupportedSpokenLanguages}
116-
styles={{ label: {color: '#edebe9'}, dropdown: { width: 100 } }}
127+
styles={{ label: {color: '#edebe9'}, dropdown: { width: 100 }, root: {paddingBottom: '1rem'} }}
117128
/>
118129
}
119130

@@ -131,14 +142,14 @@ const CallCaption = ({ call }) => {
131142
onChange={captionLanguageSelectionChanged}
132143
label={'Caption Language'}
133144
options={keyedSupportedCaptionLanguages}
134-
styles={{ label: {color: '#edebe9'}, dropdown: { width: 100, overflow: 'scroll' } }}
145+
styles={{ label: {color: '#edebe9'}, dropdown: { width: 100, overflow: 'scroll' }, root: {paddingBottom: '1rem'} }}
135146
/>
136147
}
137148

138149
return (
139150
<>
140151
{captions && <SpokenLanguageDropdown/>}
141-
{captions && captions.captionsType === 'TeamsCaptions' && <CaptionLanguageDropdown/>}
152+
{captions && captions.kind === 'TeamsCaptions' && capabilitiesFeature.capabilities.setCaptionLanguage?.isPresent && <CaptionLanguageDropdown/>}
142153
<div className="scrollable-captions-container">
143154
<div id="captionsArea" className="captions-area">
144155
</div>

Project/src/MakeCall/CallCard.js

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import CallCaption from "./CallCaption";
2020
import Lobby from "./Lobby";
2121
import { ParticipantMenuOptions } from './ParticipantMenuOptions';
2222
import MediaConstraint from './MediaConstraint';
23+
import RealTimeTextCard from "./RealTimeTextCard";
2324

2425
export default class CallCard extends React.Component {
2526
constructor(props) {
@@ -40,6 +41,9 @@ export default class CallCard extends React.Component {
4041
this.raiseHandFeature = this.call.feature(Features.RaiseHand);
4142
this.capabilitiesFeature = this.call.feature(Features.Capabilities);
4243
this.capabilities = this.capabilitiesFeature.capabilities;
44+
if (Features.RealTimeText) {
45+
this.realTimeTextFeature = this.call.feature(Features.RealTimeText);
46+
}
4347
this.dominantSpeakersFeature = this.call.feature(Features.DominantSpeakers);
4448
this.recordingFeature = this.call.feature(Features.Recording);
4549
this.transcriptionFeature = this.call.feature(Features.Transcription);
@@ -86,6 +90,9 @@ export default class CallCard extends React.Component {
8690
callMessage: undefined,
8791
dominantSpeakerMode: false,
8892
captionOn: false,
93+
realTimeTextOn: false,
94+
firstRealTimeTextReceivedorSent: false,
95+
showCanNotHideorCloseRealTimeTextBanner: false,
8996
dominantRemoteParticipant: undefined,
9097
logMediaStats: false,
9198
sentResolution: '',
@@ -111,6 +118,10 @@ export default class CallCard extends React.Component {
111118
this.isSetCallConstraints = this.call.setConstraints !== undefined;
112119
}
113120

121+
setFirstRealTimeTextReceivedorSent = (state) => {
122+
this.setState({ firstRealTimeTextReceivedorSent: state });
123+
}
124+
114125
componentWillUnmount() {
115126
this.call.off('stateChanged', () => { });
116127
this.deviceManager.off('videoDevicesUpdated', () => { });
@@ -468,6 +479,7 @@ export default class CallCard extends React.Component {
468479
this.recordingFeature.on('isRecordingActiveChanged', this.isRecordingActiveChangedHandler);
469480
this.transcriptionFeature.on('isTranscriptionActiveChanged', this.isTranscriptionActiveChangedHandler);
470481
this.lobby?.on('lobbyParticipantsUpdated', this.lobbyParticipantsUpdatedHandler);
482+
this.realTimeTextFeature?.on('realTimeTextReceived', this.realTimeTextReceivedHandler);
471483
}
472484
}
473485

@@ -649,6 +661,62 @@ export default class CallCard extends React.Component {
649661
this.capabilities = this.capabilitiesFeature.capabilities;
650662
}
651663

664+
realTimeTextReceivedHandler = (rttData) => {
665+
this.setState({ realTimeTextOn: true });
666+
if (!this.state.firstRealTimeTextReceivedorSent) {
667+
this.setState({ firstRealTimeTextReceivedorSent: true });
668+
}
669+
if (rttData) {
670+
671+
let mri = '';
672+
let displayName = '';
673+
switch (rttData.sender.identifier.kind) {
674+
case 'communicationUser': { mri = rttData.sender.identifier.communicationUserId; displayName = rttData.sender.displayName; break; }
675+
case 'microsoftTeamsUser': { mri = rttData.sender.identifier.microsoftTeamsUserId; displayName = rttData.sender.displayName; break; }
676+
case 'phoneNumber': { mri = rttData.sender.identifier.phoneNumber; displayName = rttData.sender.displayName; break; }
677+
}
678+
679+
let rttAreaContainer = document.getElementById('rttArea');
680+
681+
const newClassName = `prefix${mri.replace(/:/g, '').replace(/-/g, '').replace(/\+/g, '')}`;
682+
const rttText = `${(rttData.receivedTimestamp).toUTCString()} ${displayName ?? mri} isTyping: `;
683+
684+
let foundRTTContainer = rttAreaContainer.querySelector(`.${newClassName}[isNotFinal='true']`);
685+
686+
if (!foundRTTContainer) {
687+
if (rttData.text.trim() === '') {
688+
return
689+
}
690+
let rttContainer = document.createElement('div');
691+
rttContainer.setAttribute('isNotFinal', 'true');
692+
rttContainer.style['borderBottom'] = '1px solid';
693+
rttContainer.style['whiteSpace'] = 'pre-line';
694+
rttContainer.textContent = rttText + rttData.text;
695+
rttContainer.classList.add(newClassName);
696+
697+
rttAreaContainer.appendChild(rttContainer);
698+
699+
setTimeout(() => {
700+
rttAreaContainer.removeChild(rttContainer);
701+
}, 40000);
702+
} else {
703+
if (rttData.text.trim() === '') {
704+
rttAreaContainer.removeChild(foundRTTContainer);
705+
}
706+
if (rttData.resultType === 'Final') {
707+
foundRTTContainer.setAttribute('isNotFinal', 'false');
708+
foundRTTContainer.textContent = foundRTTContainer.textContent.replace(' isTyping', '');
709+
if (rttData.isLocal) {
710+
let rttTextField = document.getElementById('rttTextField');
711+
rttTextField.value = null;
712+
}
713+
} else {
714+
foundRTTContainer.textContent = rttText + rttData.text;
715+
}
716+
}
717+
}
718+
}
719+
652720
dominantSpeakersChanged = () => {
653721
const dominantSpeakersMris = this.dominantSpeakersFeature.dominantSpeakers.speakersList;
654722
const remoteParticipants = dominantSpeakersMris.map(dominantSpeakerMri => {
@@ -1455,6 +1523,27 @@ export default class CallCard extends React.Component {
14551523
<Icon iconName="TextBox" />
14561524
}
14571525
</span>
1526+
{ Features.RealTimeText &&
1527+
<span className="in-call-button"
1528+
title={`${this.state.realTimeTextOn ? 'Hide RealTimeText Card' : 'Show RealTimeText Card'}`}
1529+
variant="secondary"
1530+
hidden={this.state.callState !== 'Connected'}
1531+
onClick={() => {
1532+
if (!this.state.firstRealTimeTextReceivedorSent) {
1533+
this.setState((prevState) => ({ realTimeTextOn: !prevState.realTimeTextOn }))
1534+
} else {
1535+
this.setState((prevState) => ({ showCanNotHideorCloseRealTimeTextBanner: true}))
1536+
}}}>
1537+
{
1538+
this.state.realTimeTextOn &&
1539+
<Icon iconName="Comment" />
1540+
}
1541+
{
1542+
!this.state.realTimeTextOn &&
1543+
<Icon iconName="Comment" />
1544+
}
1545+
</span>
1546+
}
14581547
<span className="in-call-button"
14591548
title={`${this.state.showDataChannel ? 'Turn data channel off' : 'Turn data channel on'}`}
14601549
variant="secondary"
@@ -1741,6 +1830,38 @@ export default class CallCard extends React.Component {
17411830
</div>
17421831
</div>
17431832
}
1833+
{
1834+
Features.RealTimeText && this.state.realTimeTextOn &&
1835+
<div className="mt-5">
1836+
<div className="ms-Grid-row">
1837+
<h3>RealTimeText</h3>
1838+
</div>
1839+
<div className="md-grid-row">
1840+
{
1841+
this.state.realTimeTextOn &&
1842+
this.state.firstRealTimeTextReceivedorSent &&
1843+
this.state.showCanNotHideorCloseRealTimeTextBanner &&
1844+
<MessageBar
1845+
messageBarType={MessageBarType.warn}
1846+
isMultiline={true}
1847+
onDismiss={() => { this.setState({ showCanNotHideorCloseRealTimeTextBanner: undefined }) }}
1848+
dismissButtonAriaLabel="Close">
1849+
<b>Note: RealTimeText can not be closed or hidden after you have sent or received a message.</b>
1850+
</MessageBar>
1851+
}
1852+
{
1853+
this.state.realTimeTextOn &&
1854+
<RealTimeTextCard
1855+
call={this.call}
1856+
state={{
1857+
firstRealTimeTextReceivedorSent: this.state.firstRealTimeTextReceivedorSent,
1858+
setFirstRealTimeTextReceivedorSent: this.setFirstRealTimeTextReceivedorSent
1859+
}}
1860+
/>
1861+
}
1862+
</div>
1863+
</div>
1864+
}
17441865
{
17451866
this.state.showDataChannel &&
17461867
<div className="mt-5">

0 commit comments

Comments
 (0)