diff --git a/css/base.css b/css/base.css
index 3129077b..257a032d 100644
--- a/css/base.css
+++ b/css/base.css
@@ -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
+}
diff --git a/src/views/call-view.jsx b/src/views/call-view.jsx
new file mode 100644
index 00000000..957b50b0
--- /dev/null
+++ b/src/views/call-view.jsx
@@ -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 (
+
+
+
+
+
+
+
+
+
{this.state.fullName}
+
+
+
+
+
+ {this.props.callState >= 1 && this.props.callState < 3 ?
+
+ :
+ null
+ }
+ {this.props.callState >= 2 && this.props.callState < 3 ?
+
+ :
+ null
+ }
+
+
+
+
+ );
+ }
+};
+
+export default injectIntl(CallView);
diff --git a/src/views/messages-view.jsx b/src/views/messages-view.jsx
index 946184df..238682f5 100644
--- a/src/views/messages-view.jsx
+++ b/src/views/messages-view.jsx
@@ -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 = (
+
+ );
} else {
const topic = this.props.tinode.getTopic(this.state.topic);
const isChannel = topic.isChannelType();
@@ -1124,7 +1198,7 @@ class MessagesView extends React.Component {
>
);
}
-
+ //
component = {component2}
}
return component;
diff --git a/src/views/tinode-web.jsx b/src/views/tinode-web.jsx
index c65d70cc..23bb20b3 100644
--- a/src/views/tinode-web.jsx
+++ b/src/views/tinode-web.jsx
@@ -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 (
@@ -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 ?
+ :
+ null
+ }
);
}
diff --git a/src/widgets/rtc-panel.jsx b/src/widgets/rtc-panel.jsx
new file mode 100644
index 00000000..48f2f29e
--- /dev/null
+++ b/src/widgets/rtc-panel.jsx
@@ -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 (
+
+
+
+
+
+
+
+
+
+ );
+ }
+};