Video calls

This commit is contained in:
aforge
2022-02-13 19:31:06 -08:00
parent 3c11cbbdf1
commit 5370b39fb6
5 changed files with 930 additions and 3 deletions
+30
View File
@@ -2590,3 +2590,33 @@ ul.tabbar li.active > a {
opacity: 0.3;
}
}
.h-100 {
height: 100%!important;
}
#video_container {
display: flex;
width: 85%;
margin: auto;
height: 80%
}
video {
width:100%;
}
.align-items-center {
-ms-flex-align: center!important;
align-items: center!important;
}
.local_div {
width:50%;
float:left
}
.remote_div {
width:50%;
float:left
}
+179
View File
@@ -0,0 +1,179 @@
// InfoView: panel with topic/user info.
import React from 'react';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import Tinode from 'tinode-sdk';
import AvatarUpload from '../widgets/avatar-upload.jsx';
import BadgeList from '../widgets/badge-list.jsx';
import MenuCancel from '../widgets/menu-cancel.jsx';
import { MAX_TITLE_LENGTH, MAX_TOPIC_DESCRIPTION_LENGTH, NO_ACCESS_MODE } from '../config.js';
import { makeImageUrl } from '../lib/blob-helpers.js';
import { arrayEqual, theCard } from '../lib/utils.js';
function _clip(str, length) {
return str && str.substring(0, length);
}
class CallView extends React.Component {
constructor(props) {
super(props);
this.state = {
topic: null,
fullName: undefined,
avatar: null,
trustedBadges: [],
previousMetaDesc: undefined,
offerSent: false,
};
this.resetDesc = this.resetDesc.bind(this);
this.onMetaDesc = this.onMetaDesc.bind(this);
this.handleRejectCall = this.handleRejectCall.bind(this);
this.handleAcceptCall = this.handleAcceptCall.bind(this);
}
componentDidMount() {
const topic = this.props.tinode.getTopic(this.props.topic);
if (!topic) {
return;
}
this.resetDesc(topic, this.props);
if (this.props.callState == 2) {
this.props.onRinging(this.props.topic, this.props.seq);
}
}
// No need to separately handle component mount.
componentDidUpdate(props) {
const topic = this.props.tinode.getTopic(props.topic);
if (!topic) {
return;
}
if (this.onMetaDesc != topic.onMetaDesc) {
this.previousMetaDesc = topic.onMetaDesc;
topic.onMetaDesc = this.onMetaDesc;
}
if (this.state.topic != props.topic) {
this.setState({topic: props.topic});
this.resetDesc(topic, props);
}
}
componentWillUnmount() {
const topic = this.props.tinode.getTopic(this.props.topic);
if (!topic) {
return;
}
this.setState({topic: null});
topic.onMetaDesc = this.previousMetaDesc;
}
resetDesc(topic, props) {
const defacs = topic.getDefaultAccess() || {};
const acs = topic.getAccessMode();
const badges = [];
if (topic.trusted) {
for (const [key, val] of Object.entries(topic.trusted)) {
if (val) {
badges.push(key);
}
}
}
this.setState({
fullName: _clip(topic.public ? topic.public.fn : undefined, MAX_TITLE_LENGTH),
avatar: makeImageUrl(topic.public ? topic.public.photo : null),
trustedBadges: badges,
});
}
onMetaDesc(desc) {
const topic = this.props.tinode.getTopic(this.props.topic);
if (!topic) {
return;
}
this.resetDesc(topic, this.props);
if (this.previousMetaDesc && this.previousMetaDesc != this.onMetaDesc) {
this.previousMetaDesc(desc);
}
}
handleAcceptCall() {
this.props.onAcceptCall(this.props.topic);
}
handleRejectCall() {
this.props.onReject(this.props.topic, this.props.seq);
this.props.onClose();
}
render() {
const {formatMessage} = this.props.intl;
let panelTitle = 'Unknown';
switch (this.props.callState) {
case 1:
panelTitle = 'Calling';
break;
case 2:
panelTitle = 'Incoming Call From';
break;
case 3:
panelTitle = 'Call Established';
break;
}
return (
<div id="info-view">
<div className="caption-panel" id="info-caption-panel">
<div className="panel-title" id="info-title">{panelTitle}</div>
<div>
<MenuCancel onCancel={this.handleRejectCall} />
</div>
</div>
<div id="info-view-content" className="scrollable-panel">
<div className="panel-form-column">
<center>
<AvatarUpload
tinode={this.props.tinode}
avatar={this.state.avatar}
readOnly={true}
uid={this.props.topic}
title={this.state.fullName} />
</center>
<div className="group" style={{padding: "10px", textAlign: "center"}}>
<div className="large ellipsized">{this.state.fullName}</div>
</div>
<div className="group" style={{textAlign: "center"}}>
<BadgeList trustedBadges={this.state.trustedBadges} />
</div>
<div id="actions" className="group" style={{padding: "20px", textAlign: "center"}}>
{this.props.callState >= 1 && this.props.callState < 3 ?
<button onClick={this.handleRejectCall} style={{borderRadius: "10px", backgroundColor: "red"}}>
<i className="material-icons">call_end</i>
</button>
:
null
}
{this.props.callState >= 2 && this.props.callState < 3 ?
<button onClick={this.handleAcceptCall} style={{borderRadius: "10px", backgroundColor: "green"}}>
<i className="material-icons">call</i>
</button>
:
null
}
</div>
</div>
</div>
</div>
);
}
};
export default injectIntl(CallView);
+76 -2
View File
@@ -14,6 +14,7 @@ import Invitation from '../widgets/invitation.jsx';
import LetterTile from '../widgets/letter-tile.jsx';
import LoadSpinner from '../widgets/load-spinner.jsx';
import LogoView from './logo-view.jsx';
import RtcPanel from '../widgets/rtc-panel.jsx';
import SendMessage from '../widgets/send-message.jsx';
import { DEFAULT_P2P_ACCESS_MODE, IMAGE_PREVIEW_DIM, KEYPRESS_DELAY,
@@ -113,6 +114,8 @@ class MessagesView extends React.Component {
this.handlePickReply = this.handlePickReply.bind(this);
this.handleCancelReply = this.handleCancelReply.bind(this);
this.handleQuoteClick = this.handleQuoteClick.bind(this);
this.handleRtcClick = this.handleRtcClick.bind(this);
this.handleRtcClose = this.handleRtcClose.bind(this);
this.chatMessageRefs = {};
this.getOrCreateMessageRef = this.getOrCreateMessageRef.bind(this);
@@ -173,7 +176,17 @@ class MessagesView extends React.Component {
topic.onPres = this.handleSubsUpdated;
}
}
/*
if (this.props.callTopic == this.props.topic && !this.state.rtcPanel) {
this.handleRtcClick();
}
*/
/*
if (this.state.rtcPanel == null && topic && topic.name == this.props.callTopic && (this.props.callState == 1 || this.props.callState == 3)) {
console.log('--> setting rtc panel, call state = ', this.props.callState);
this.setState({rtcPanel: { topic: this.props.callTopic }});
}
*/
if (!this.props.applicationVisible) {
// If application is not visible, flush all unsent 'read' notifications.
this.clearNotificationQueue();
@@ -189,6 +202,11 @@ class MessagesView extends React.Component {
static getDerivedStateFromProps(nextProps, prevState) {
let nextState = {};
/*
if (nextProps.callState == 3 || nextProps.callState == 1) {
nextState.rtcPanel = { topic: nextProps.topic };
}
*/
if (!nextProps.topic) {
// Default state: no topic.
nextState = {
@@ -204,6 +222,7 @@ class MessagesView extends React.Component {
docPreview: null,
imagePreview: null,
imagePostview: null,
rtcPanel: null,//typeof nextProps.topic != 'undefined' && nextPropstopic.name == this.props.callTopic && (nextProps.callState == 1 || nextProps.callState == 3) ? { topic: nextProps.topic } : null,
typingIndicator: false,
scrollPosition: 0,
fetchingMessages: false,
@@ -229,6 +248,7 @@ class MessagesView extends React.Component {
docPreview: null,
imagePreview: null,
imagePostview: null,
rtcPanel: null,
typingIndicator: false,
scrollPosition: 0,
fetchingMessages: false,
@@ -278,6 +298,12 @@ class MessagesView extends React.Component {
latestClearId: topic.maxClearId(),
channel: topic.isChannelType()
});
if (nextProps.callTopic == topic.name && (nextProps.callState == 1 || nextProps.callState == 3)) {
Object.assign(nextState, {
rtcPanel: nextProps.callTopic,
});
}
} else {
// Invalid topic.
Object.assign(nextState, {
@@ -290,6 +316,13 @@ class MessagesView extends React.Component {
channel: false
});
}
} else {
// same topic
if (!prevState.rtcPanel && nextProps.callTopic == prevState.topic && (nextProps.callState == 1 || nextProps.callState == 3)) {
Object.assign(nextState, {
rtcPanel: nextProps.callTopic,
});
}
}
if (nextProps.acs) {
@@ -750,6 +783,28 @@ class MessagesView extends React.Component {
}
}
handleRtcClick() {
this.setState({
rtcPanel: {
/*
file: file,
name: file.name,
size: file.size,
type: file.type
*/
topic: this.state.topic
}
});
}
handleRtcClose() {
this.props.onVideoCallClosed();
this.setState({
rtcPanel: null
});
}
// sendImageAttachment sends the image bits inband as Drafty message.
sendImageAttachment(caption, blob) {
const mime = this.state.imagePreview.mime;
@@ -934,6 +989,25 @@ class MessagesView extends React.Component {
onClose={this.handleClosePreview}
onSendMessage={this.sendFileAttachment} />
);
} else if (this.state.rtcPanel) {
const topic = this.props.tinode.getTopic(this.state.topic);
component2 = (
<RtcPanel
topic={topic}
seq={this.props.callSeq}
callState={this.props.callState}
onError={this.props.onError}
onClose={this.handleRtcClose}
onHangup={this.props.onTeleHangup}
onInvite={this.props.onTeleInvite}
onSendOffer={this.props.onTeleSendOffer}
onIceCandidate={this.props.onTeleIceCandidate}
onSendAnswer={this.props.onTeleSendAnswer}
//content={this.state.imagePostview}
//onClose={this.handleClosePreview}
/>
);
} else {
const topic = this.props.tinode.getTopic(this.state.topic);
const isChannel = topic.isChannelType();
@@ -1124,7 +1198,7 @@ class MessagesView extends React.Component {
</>
);
}
//<button onClick={this.handleRtcClick} >abc</button>
component = <div id="topic-view">{component2}</div>
}
return component;
+282 -1
View File
@@ -12,6 +12,7 @@ import ContextMenu from '../widgets/context-menu.jsx';
import ForwardDialog from '../widgets/forward-dialog.jsx';
import InfoView from './info-view.jsx';
import CallView from './call-view.jsx';
import MessagesView from './messages-view.jsx';
import SidepanelView from './sidepanel-view.jsx';
@@ -63,6 +64,11 @@ const messages = defineMessages({
id: 'menu_item_info',
defaultMessage: 'Info',
description: 'Show extended topic information'
},
menu_item_video_call: {
id: 'menu_item_video_call',
defaultMessage: 'Video Call',
description: 'Start video call'
}
});
@@ -145,6 +151,22 @@ class TinodeWeb extends React.Component {
this.handleShowForwardDialog = this.handleShowForwardDialog.bind(this);
this.handleHideForwardDialog = this.handleHideForwardDialog.bind(this);
this.handleStartVideoCall = this.handleStartVideoCall.bind(this);
this.handleTeleMessage = this.handleTeleMessage.bind(this);
this.handleCallClose = this.handleCallClose.bind(this);
this.handleTeleInvite = this.handleTeleInvite.bind(this);
this.handleTeleRinging = this.handleTeleRinging.bind(this);
this.handleTeleHangup = this.handleTeleHangup.bind(this);
this.handleTeleSendOffer = this.handleTeleSendOffer.bind(this);
this.handleTeleIceCandidate = this.handleTeleIceCandidate.bind(this);
this.handleTeleSendAnswer = this.handleTeleSendAnswer.bind(this);
this.handleTeleAcceptCall = this.handleTeleAcceptCall.bind(this);
this.handleTeleUpdateLease = this.handleTeleUpdateLease.bind(this);
this.handleTeleLeaseReply = this.handleTeleLeaseReply.bind(this);
this.setLeaseTimer = this.setLeaseTimer.bind(this);
this.sendMessageToTopic = this.sendMessageToTopic.bind(this);
}
@@ -197,6 +219,10 @@ class TinodeWeb extends React.Component {
infoPanel: undefined,
mobilePanel: 'sidepanel',
callTopic: undefined,
callState: 0,
//isCallOriginator: false,
contextMenuVisible: false,
contextMenuBounds: null,
contextMenuClickAt: null,
@@ -252,6 +278,7 @@ class TinodeWeb extends React.Component {
this.tinode.onConnect = this.handleConnected;
this.tinode.onDisconnect = this.handleDisconnect;
this.tinode.onAutoreconnectIteration = this.handleAutoreconnectIteration;
this.tinode.onTeleMessage = this.handleTeleMessage;
}).then(() => {
// Initialize desktop alerts.
if (this.state.desktopAlertsEnabled) {
@@ -825,7 +852,26 @@ class TinodeWeb extends React.Component {
this.receivedTimer = undefined;
topic.noteRecv(data.seq);
}, RECEIVED_DELAY);
/*
if (typeof data.head != 'undefined' && data.head['mime'] == 'application/tinode-video-call' && topic.msgRecvCount(data.seq) == 0) {
this.setState({
callTopic: data.topic,
callState: 2
});
}
*/
}
/*
// Incoming call.
if (data.from != this.state.myUserId && data.seq > topic.maxMsgSeq()) {
this.setState({
callTopic: data.topic,
callState: 2
});
}
*/
}
/* Fnd topic: find contacts by tokens */
@@ -1355,6 +1401,7 @@ class TinodeWeb extends React.Component {
this.tinode.onConnect = this.handleConnected;
this.tinode.onDisconnect = this.handleDisconnect;
this.tinode.onAutoreconnectIteration = this.handleAutoreconnectIteration;
this.tinode.onTeleMessage = this.handleTeleMessage;
HashNavigation.navigateTo('');
}
@@ -1498,6 +1545,10 @@ class TinodeWeb extends React.Component {
title: this.props.intl.formatMessage(messages.menu_item_info),
handler: this.handleShowInfoView
} : null,
subscribed && Tinode.isP2PTopicName(topicName) ? {
title: this.props.intl.formatMessage(messages.menu_item_video_call),
handler: this.handleStartVideoCall
} : null,
subscribed ? 'messages_clear' : null,
subscribed && deleter ? 'messages_clear_hard' : null,
muted ? (blocked ? null : 'topic_unmute') : 'topic_mute',
@@ -1628,6 +1679,209 @@ class TinodeWeb extends React.Component {
}
}
handleStartVideoCall() {
this.setState({
callTopic: this.state.topicSelected,
callState: 1,
});
}
setLeaseTimer(seq, leaseExp, now, shift) {
if (leaseExp < now) {
// Lease has expired.
this.handleCallClose();
return;
}
let period = leaseExp - now - shift;
if (period < 0) {
period = 0;
}
console.log('X: setting timeout to ', period, ', seq = ', seq, ', leaseExp = ', leaseExp, ', now = ', now, ', shift = ', shift);
//setTimeout(() => {console.log("before callTimeout")}, period - 500);
let callTimeout = setTimeout(this.handleTeleUpdateLease, period);
console.log('setting call timeout -> ', callTimeout);
let callState = {
callLeaseExpires: leaseExp,
callTimeout: callTimeout
};
if (seq) {
callState.callSeq = seq;
}
this.setState(callState);
}
handleTeleLeaseReply(ctrl, shift) {
if (ctrl.code < 200 || ctrl.code >= 300 || !('params' in ctrl)) {
this.handleCallClose();
return;
}
const ds = ctrl.params['lease-expires'];
if (!ds) {
this.handleCallClose();
return;
}
const leaseExp = new Date(ds);
const now = Date.now();
this.setLeaseTimer(ctrl.params['seq'], leaseExp.getTime(), now, shift);
}
handleTeleUpdateLease() {
console.log('update lease event');
const topic = this.tinode.getTopic(this.state.callTopic);
if (!topic) {
return;
}
const now = Date.now();
if (now >= this.state.callLeaseExpires) {
// Lease has expired.
this.handleCallClose();
return;
}
/*
if (!extend) {
this.handleCallClose();
return;
}
*/
let until = new Date(this.state.callLeaseExpires); //this.state.callLeaseExpires.getSeconds() + 10;
until.setSeconds(until.getSeconds() + 10);
topic.call('extend-lease', this.state.callSeq, undefined, until)
.then((ctrl) => {
this.handleTeleLeaseReply(ctrl, 2000);
});
}
handleTeleInvite(callTopic, callSeq, callState) {
const topic = this.tinode.getTopic(this.state.topicSelected);
if (!topic) {
return;
}
if (callState == 1) {
topic.call('invite').then((ctrl) => {
if (ctrl.params && ctrl.params['sip-code'] == 100) {
this.handleTeleLeaseReply(ctrl, 0);
}
});
} else if (callState == 3) {
topic.call('accept', callSeq).then((ctrl) => {
this.handleTeleLeaseReply(ctrl, 2000);
});
}
}
handleTeleRinging(callTopic, callSeq) {
const topic = this.tinode.getTopic(callTopic);
if (!topic) {
return;
}
topic.call('ringing', callSeq);
}
handleTeleHangup(callTopic, callSeq) {
const topic = this.tinode.getTopic(callTopic);
if (!topic) {
return;
}
topic.call('hang-up', callSeq);
}
handleTeleSendOffer(callTopic, callSeq, sdp) {
const topic = this.tinode.getTopic(callTopic);
if (!topic) {
return;
}
topic.call('offer', callSeq, sdp);
}
handleTeleIceCandidate(callTopic, callSeq, candidate) {
const topic = this.tinode.getTopic(callTopic);
if (!topic) {
return;
}
topic.call('ice-candidate', callSeq, candidate);
}
handleTeleSendAnswer(callTopic, callSeq, sdp) {
const topic = this.tinode.getTopic(callTopic);
if (!topic) {
return;
}
topic.call('answer', callSeq, sdp);
}
handleCallClose() {
if (this.state.callTimeout) {
console.log('closing call timeout ', this.state.callTimeout);
clearTimeout(this.state.callTimeout);
}
this.setState({
callTopic: undefined,
callState: 0,
callLeaseExpires: undefined,
callTimeout: undefined
});
}
handleTeleAcceptCall(topicName) {
this.handleTopicSelected(this.state.callTopic);
this.setState({
callState: 3
});
}
handleTeleMessage(tele) {
//
switch (tele.what) {
case 'invite':
console.log('invite: ', tele);
if (tele.from != this.state.myUserId) {
// incoming call.
this.setState({
callTopic: tele.src,
callState: 2,
callSeq: tele.seq,
sdp: tele.sdp
});
}
break;
case 'accept':
console.log('TinodeWeb: received accept msg ', tele);
// If another my session has accepted the call.
if (tele.topic == 'me' && tele.from == this.state.myUserId) {
this.setState({
callTopic: null,//tele.src,
callState: 0,
callSeq: null,//tele.seq,
//sdp: tele.sdp
});
}
if (tele.topic == this.state.callTopic) {
// Update state.
this.setState({callState: 3});
if (this.state.callTimeout) {
clearTimeout(this.state.callTimeout);
this.setState({callTimeout: undefined});
this.setLeaseTimer(this.state.callSeq, this.state.callLeaseExpires, Date.now(), 2000);
}
}
break;
case 'hang-up':
//if (tele.topic == 'me') {
// remote hangup.
this.handleCallClose();
break;
//}
}
}
render() {
return (
<div id="app-container" ref={this.selfRef}>
@@ -1765,6 +2019,17 @@ class TinodeWeb extends React.Component {
forwardMessage={this.state.forwardMessage}
onCancelForwardMessage={this.handleHideForwardDialog}
callTopic={this.state.callTopic}
callSeq={this.state.callSeq}
callState={this.state.callState}
onTeleHangup={this.handleTeleHangup}
onTeleInvite={this.handleTeleInvite}
onTeleSendOffer={this.handleTeleSendOffer}
onTeleIceCandidate={this.handleTeleIceCandidate}
onTeleSendAnswer={this.handleTeleSendAnswer}
//onTemeMedia
errorText={this.state.errorText}
errorLevel={this.state.errorLevel}
errorAction={this.state.errorAction}
@@ -1779,7 +2044,8 @@ class TinodeWeb extends React.Component {
showContextMenu={this.handleShowContextMenu}
onChangePermissions={this.handleChangePermissions}
onNewChat={this.handleNewChatInvitation}
sendMessage={this.handleSendMessage} />
sendMessage={this.handleSendMessage}
onVideoCallClosed={this.handleCallClose} />
{this.state.infoPanel ?
<InfoView
@@ -1817,6 +2083,21 @@ class TinodeWeb extends React.Component {
:
null
}
{this.state.callTopic && this.state.callState == 2 ?
<CallView
tinode={this.tinode}
onClose={this.handleCallClose}
topic={this.state.callTopic}
seq={this.state.callSeq}
callState={this.state.callState}
onRinging={this.handleTeleRinging}
onAcceptCall={this.handleTeleAcceptCall}
onReject={this.handleTeleHangup}
/>
:
null
}
</div>
);
}
+363
View File
@@ -0,0 +1,363 @@
import React from 'react';
export default class RtcPanel extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
isChannelReady: false,
isInitiator: false,
isStarted: false,
localStream: undefined,
pc: undefined,
remoteStream: undefined,
previousOnTele: undefined
};
this.localStreamConstraints = {
audio: true,
video: true
};
this.pcConfig = {
iceServers: [
{ urls: [ "stun:bn-turn1.xirsys.com" ] },
{
username: "0kYXFmQL9xojOrUy4VFemlTnNPVFZpp7jfPjpB3AjxahuRe4QWrCs6Ll1vDc7TTjAAAAAGAG2whXZWJUdXRzUGx1cw==",
credential: "285ff060-5a58-11eb-b269-0242ac140004",
urls: [
"turn:bn-turn1.xirsys.com:80?transport=udp",
"turn:bn-turn1.xirsys.com:3478?transport=udp",
"turn:bn-turn1.xirsys.com:80?transport=tcp",
"turn:bn-turn1.xirsys.com:3478?transport=tcp",
"turns:bn-turn1.xirsys.com:443?transport=tcp",
"turns:bn-turn1.xirsys.com:5349?transport=tcp"
]
}
]};
this.localRef = React.createRef();
this.remoteRef = React.createRef();
this.onTele = this.onTele.bind(this);
this.launchStream = this.launchStream.bind(this);
this.createPeerConnection = this.createPeerConnection.bind(this);
this.handleNegotiationNeededEvent = this.handleNegotiationNeededEvent.bind(this);
this.handleICECandidateEvent = this.handleICECandidateEvent.bind(this);
this.handleNewICECandidateMsg = this.handleNewICECandidateMsg.bind(this);
this.handleICEConnectionStateChangeEvent = this.handleICEConnectionStateChangeEvent.bind(this);
this.handleSignalingStateChangeEvent = this.handleSignalingStateChangeEvent.bind(this);
this.handleICEGatheringStateChangeEvent = this.handleICEGatheringStateChangeEvent.bind(this);
this.handleTrackEvent = this.handleTrackEvent.bind(this);
this.handleVideoOfferMsg = this.handleVideoOfferMsg.bind(this);
this.handleVideoAnswerMsg = this.handleVideoAnswerMsg.bind(this);
this.handleNewICECandidateMsg = this.handleNewICECandidateMsg.bind(this);
this.reportError = this.reportError.bind(this);
this.handleGetUserMediaError = this.handleGetUserMediaError.bind(this);
this.stop = this.stop.bind(this);
this.stopTracks = this.stopTracks.bind(this);
this.handleCloseClick = this.handleCloseClick.bind(this);
this.handleToggleCameraClick = this.handleToggleCameraClick.bind(this);
this.handleToggleMicClick = this.handleToggleMicClick.bind(this);
this.toggleMedia = this.toggleMedia.bind(this);
this.handleRemoteHangup = this.handleRemoteHangup.bind(this);
this.handleVideoCallAccepted = this.handleVideoCallAccepted.bind(this);
}
handleVideoCallAccepted(tele) {
const pc = this.createPeerConnection();
const stream = this.state.localStream;
stream.getTracks().forEach(track => pc.addTrack(track, stream));
console.log('RtcPanel: tele msg', tele);
}
onTele(tele) {
console.log('tele --> ', tele);
switch (tele.what) {
//case 'invite'
case 'accept':
// Call accepted.
this.handleVideoCallAccepted(tele);
break;
case 'offer':
this.handleVideoOfferMsg(tele);
break;
case 'answer':
this.handleVideoAnswerMsg(tele);
break;
case 'ice-candidate':
this.handleNewICECandidateMsg(tele);
break;
case 'hang-up':
this.handleRemoteHangup(tele);
break;
}
}
componentDidUpdate(props) {
const topic = this.props.topic;
if (this.onTele != topic.onTele) {
this.previousOnTele = topic.onTele;
topic.onTele = this.onTele;
}
}
componentDidMount() {
if ((this.props.callState == 1 || this.props.callState == 3) && this.localRef.current) {
this.launchStream();
}
}
componentWillUnmount() {
this.props.topic.onTele = this.previousOnTele;
}
launchStream() {
if (this.state.localStream) {
console.log("You can't start a call because you already have one open!");
} else {
navigator.mediaDevices.getUserMedia(this.localStreamConstraints)
.then(stream => {
console.log('Adding local stream.', this.localRef.current);
this.setState({localStream: stream});
this.localRef.current.srcObject = stream;
// Send call invitation (or accept call).
this.props.onInvite(this.props.topic.name, this.props.seq, this.props.callState);
})
.catch(this.handleGetUserMediaError);
}
}
createPeerConnection() {
var pc = new RTCPeerConnection(this.pcConfig);
pc.onicecandidate = this.handleICECandidateEvent;
pc.oniceconnectionstatechange = this.handleICEConnectionStateChangeEvent;
pc.onicegatheringstatechange = this.handleICEGatheringStateChangeEvent;
pc.onsignalingstatechange = this.handleSignalingStateChangeEvent;
pc.onnegotiationneeded = this.handleNegotiationNeededEvent;
pc.ontrack = this.handleTrackEvent;
this.setState({pc: pc});
console.log('Created RTCPeerConnnection');
return pc;
}
handleVideoAnswerMsg(tele) {
console.log("*** Call recipient has accepted our call");
// Configure the remote description, which is the SDP payload
// in our "video-answer" message.
var desc = new RTCSessionDescription(tele.payload);
this.state.pc.setRemoteDescription(desc).catch(this.reportError);
}
reportError(errMessage) {
console.log(`Error ${errMessage.name}: ${errMessage.message}`);
}
handleNegotiationNeededEvent() {
this.state.pc.createOffer().then(offer => {
return this.state.pc.setLocalDescription(offer);
})
.then(() => {
this.props.onSendOffer(this.props.topic.name, this.props.seq, this.state.pc.localDescription.toJSON());
})
.catch(reportError);
}
handleICECandidateEvent(event) {
if (event.candidate) {
this.props.onIceCandidate(this.props.topic.name, this.props.seq, event.candidate.toJSON());
}
}
handleNewICECandidateMsg(tele) {
var candidate = new RTCIceCandidate(tele.payload);
this.state.pc.addIceCandidate(candidate)
.catch(reportError);
}
handleICEConnectionStateChangeEvent(event) {
switch (this.state.pc.iceConnectionState) {
case "closed":
case "failed":
this.handleCloseClick();
break;
}
}
handleSignalingStateChangeEvent(event) {
switch (this.state.pc.signalingState) {
case "closed":
this.handleCloseClick();
break;
}
}
handleICEGatheringStateChangeEvent(event) {
// Our sample just logs information to console here,
// but you can do whatever you need.
}
handleTrackEvent(event) {
this.remoteRef.current.srcObject = event.streams[0];
}
handleGetUserMediaError(e) {
console.log('-->', e);
switch(e.name) {
case "NotFoundError":
console.log("Unable to open your call because no camera and/or microphone" +
"were found.");
break;
case "SecurityError":
case "PermissionDeniedError":
// Do nothing; this is the same as the user canceling the call.
break;
default:
console.log("Error opening your camera and/or microphone: " + e.message);
break;
}
// Make sure we shut down our end of the RTCPeerConnection so we're
// ready to try again.
this.handleCloseClick();
}
handleVideoOfferMsg(tele) {
var localStream = null;
const pc = this.createPeerConnection();
var desc = new RTCSessionDescription(tele.payload);
pc.setRemoteDescription(desc).then(() => {
return navigator.mediaDevices.getUserMedia(this.localStreamConstraints);
})
.then(stream => {
localStream = stream;
this.localRef.current.srcObject = stream;
this.setState({localStream: stream});
localStream.getTracks().forEach(track => pc.addTrack(track, localStream));
})
.then(() => {
return pc.createAnswer();
})
.then(answer => {
return pc.setLocalDescription(answer);
})
.then(() => {
this.props.onSendAnswer(this.props.topic.name, this.props.seq, pc.localDescription.toJSON());
})
.catch(this.handleGetUserMediaError);
}
handleRemoteHangup() {
this.handleCloseClick();
}
stopTracks(el) {
//let lr = this.localRef.current;
if (el == null) { return; }
let stream = el.srcObject;
if (stream == null) { return; }
let tracks = stream.getTracks();
if (tracks != null) {
tracks.forEach(function(track) {
track.stop();
});
}
el.srcObject = null;
}
stop() {
this.stopTracks(this.localRef.current);
this.stopTracks(this.remoteRef.current);
if (this.state.pc != null) {
this.state.pc.ontrack = null;
this.state.pc.onremovetrack = null;
this.state.pc.onremovestream = null;
this.state.pc.onicecandidate = null;
this.state.pc.oniceconnectionstatechange = null;
this.state.pc.onsignalingstatechange = null;
this.state.pc.onicegatheringstatechange = null;
this.state.pc.onnegotiationneeded = null;
this.state.pc.close();
}
this.setState({isStarted: false, isChannelReady: false, isInitiator: false, pc: null});
}
handleCloseClick() {
this.stop();
this.props.onHangup(this.props.topic.name, this.props.seq);
this.props.onClose();
}
toggleMedia(kind) {
const video = this.localRef.current;
// A video's MediaStream object is available through its srcObject attribute
const mediaStream = video.srcObject;
// Through the MediaStream, you can get the MediaStreamTracks with getTracks():
const tracks = mediaStream.getTracks();
// Or stop all like so:
tracks.forEach(track => {
if (track.kind == kind) {
track.stop();
}
});
}
handleToggleCameraClick() {
this.toggleMedia('video');
}
handleToggleMicClick() {
this.toggleMedia('audio');
}
render() {
if (!this.props.topic) {
return null;
}
return (
<div className="h-100" id="video_display">
<div id ="video_container" className="align-items-center" style={{marginTop: "10%"}}>
<div className="local_div" id="div1">
<video id="localVideo" className="" ref={this.localRef} autoPlay muted playsInline></video>
</div>
<div className="remote_div" id="div2">
<video id="remoteVideo" className="" ref={this.remoteRef} autoPlay playsInline></video>
</div>
</div>
<div id="close" style={{padding: "20px", textAlign: "center"}}>
<button onClick={this.handleCloseClick} style={{borderRadius: "10px", backgroundColor: "red"}}>
<i className="material-icons">call_end</i>
</button>
<button onClick={this.handleToggleCameraClick} style={{borderRadius: "10px", backgroundColor: "gray"}}><i className="material-icons">video_camera_front</i></button>
<button onClick={this.handleToggleMicClick} style={{borderRadius: "10px", backgroundColor: "gray"}}><i className="material-icons">mic</i></button>
</div>
<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
</div>
);
}
};