From 4b9078e64af65cf36b483f30121c1df7ca6a4a7f Mon Sep 17 00:00:00 2001 From: aforge Date: Wed, 6 Apr 2022 23:19:09 -0700 Subject: [PATCH] Calls logic minor cleanup. --- src/config.js | 6 ++ src/views/messages-view.jsx | 24 +++-- src/views/tinode-web.jsx | 206 +++++++++++++----------------------- src/widgets/rtc-panel.jsx | 63 +++++++---- 4 files changed, 136 insertions(+), 163 deletions(-) diff --git a/src/config.js b/src/config.js index 6bb25364..0ce9d625 100644 --- a/src/config.js +++ b/src/config.js @@ -106,3 +106,9 @@ export const LINK_PRIVACY_POLICY = 'https://tinode.co/privacy.html'; // Link to Terms of Service. export const LINK_TERMS_OF_SERVICE = 'https://tinode.co/terms.html'; + +// Video call states. +export const CALL_STATE_NONE = 0; +export const CALL_STATE_OUTGOING_INITATED = 1; +export const CALL_STATE_INCOMING_RECEIVED = 2; +export const CALL_STATE_IN_PROGRESS = 3; diff --git a/src/views/messages-view.jsx b/src/views/messages-view.jsx index 68ac9875..1e6371aa 100644 --- a/src/views/messages-view.jsx +++ b/src/views/messages-view.jsx @@ -19,7 +19,8 @@ import SendMessage from '../widgets/send-message.jsx'; import { DEFAULT_P2P_ACCESS_MODE, IMAGE_PREVIEW_DIM, KEYPRESS_DELAY, MESSAGES_PAGE, MAX_EXTERN_ATTACHMENT_SIZE, MAX_IMAGE_DIM, MAX_INBAND_ATTACHMENT_SIZE, - READ_DELAY, QUOTED_REPLY_LENGTH } from '../config.js'; + READ_DELAY, QUOTED_REPLY_LENGTH, + CALL_STATE_OUTGOING_INITATED, CALL_STATE_IN_PROGRESS } from '../config.js'; import { blobToBase64, fileToBase64, imageScaled, makeImageUrl } from '../lib/blob-helpers.js'; import HashNavigation from '../lib/navigation.js'; @@ -78,6 +79,13 @@ function isPeerRestricted(acs) { return false; } +function shouldPresentCallPanel(callState) { + // Show call panel if either: + // - call is outgoing (and the client is waiting for the other side to pick up) or, + // - call is already in progress. + return callState == CALL_STATE_OUTGOING_INITATED || callState == CALL_STATE_IN_PROGRESS; +} + class MessagesView extends React.Component { constructor(props) { super(props); @@ -196,11 +204,6 @@ 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 = { @@ -216,7 +219,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, + rtcPanel: null, typingIndicator: false, scrollPosition: 0, fetchingMessages: false, @@ -293,7 +296,7 @@ class MessagesView extends React.Component { channel: topic.isChannelType() }); - if (nextProps.callTopic == topic.name && (nextProps.callState == 1 || nextProps.callState == 3)) { + if (nextProps.callTopic == topic.name && shouldPresentCallPanel(nextProps.callState)) { Object.assign(nextState, { rtcPanel: nextProps.callTopic, }); @@ -311,8 +314,9 @@ class MessagesView extends React.Component { }); } } else { - // same topic - if (!prevState.rtcPanel && nextProps.callTopic == prevState.topic && (nextProps.callState == 1 || nextProps.callState == 3)) { + // We are still in same topic. Show the call panel if necessary. + if (nextProps.callTopic == prevState.topic && !prevState.rtcPanel && + shouldPresentCallPanel(nextProps.callState)) { Object.assign(nextState, { rtcPanel: nextProps.callTopic, }); diff --git a/src/views/tinode-web.jsx b/src/views/tinode-web.jsx index 6490fc11..00944ab0 100644 --- a/src/views/tinode-web.jsx +++ b/src/views/tinode-web.jsx @@ -17,7 +17,9 @@ import MessagesView from './messages-view.jsx'; import SidepanelView from './sidepanel-view.jsx'; import { API_KEY, APP_NAME, DEFAULT_P2P_ACCESS_MODE, FORWARDED_PREVIEW_LENGTH, LOGGING_ENABLED, - MEDIA_BREAKPOINT, RECEIVED_DELAY } from '../config.js'; + MEDIA_BREAKPOINT, RECEIVED_DELAY, + CALL_STATE_NONE, CALL_STATE_OUTGOING_INITATED, + CALL_STATE_INCOMING_RECEIVED, CALL_STATE_IN_PROGRESS } from '../config.js'; import { PACKAGE_VERSION } from '../version.js'; import { base64ReEncode, makeImageUrl } from '../lib/blob-helpers.js'; import { detectServerAddress, isLocalHost, isSecureConnection } from '../lib/host-name.js'; @@ -163,9 +165,6 @@ class TinodeWeb extends React.Component { 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); } @@ -219,9 +218,12 @@ class TinodeWeb extends React.Component { infoPanel: undefined, mobilePanel: 'sidepanel', + // Video calls. callTopic: undefined, - callState: 0, - //isCallOriginator: false, + callState: CALL_STATE_NONE, + // If true, call state should be transitioned to CALL_STATE_IN_PROGRESS upon + // switching to the call topic. + callShouldStart: false, contextMenuVisible: false, contextMenuBounds: null, @@ -486,7 +488,13 @@ class TinodeWeb extends React.Component { } handleError(err, level, action, actionText) { - this.setState({errorText: err, errorLevel: level, errorAction: action, errorActionText: actionText}); + this.setState({ + errorText: err, + errorLevel: level, + errorAction: action, + errorActionText: actionText, + callShouldStart: false, + }); } // User clicked Login button in the side panel. @@ -852,26 +860,7 @@ 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 */ @@ -1293,11 +1282,20 @@ class TinodeWeb extends React.Component { // New topic was created, here is the new topic name. handleNewTopicCreated(oldName, newName) { + let nextState = {}; + if (this.state.callShouldStart) { + nextState = {callState: CALL_STATE_IN_PROGRESS, callShouldStart: false}; + } if (this.state.topicSelected == oldName && oldName != newName) { // If the current URl contains the old topic name, replace it with new. // Update the name of the selected topic first so the navigator doen't clear // the state. - this.setState({topicSelected: newName}, () => { + const newTopic = {topicSelected: newName}; + this.setState(...nextState, ...newTopic, () => { + HashNavigation.navigateTo(HashNavigation.setUrlTopic('', newName)); + }); + } else { + this.setState(nextState, () => { HashNavigation.navigateTo(HashNavigation.setUrlTopic('', newName)); }); } @@ -1679,91 +1677,35 @@ class TinodeWeb extends React.Component { handleStartVideoCall() { this.setState({ callTopic: this.state.topicSelected, - callState: 1, + callState: CALL_STATE_OUTGOING_INITATED }); } - 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; - } - - let until = new Date(this.state.callLeaseExpires); - until.setSeconds(until.getSeconds() + 10); - topic.call('extend-lease', this.state.callSeq, undefined, until) - .then((ctrl) => { - this.handleTeleLeaseReply(ctrl, 2000); - }); - } - handleTeleInvite(callTopic, callSeq, callState) { - if (callState == 1) { - let head = {mime: 'application/tinode-video-call'}; - this.handleSendMessage('started', undefined, undefined, head) - .then((ctrl) => { - if (ctrl.code < 200 || ctrl.code >= 300 || typeof ctrl.params == 'undefined' || typeof ctrl.params['seq'] == 'undefined') { - this.handleCallClose(); - return; - } - this.setState({callSeq: ctrl.params['seq']}); + switch (callState) { + case CALL_STATE_OUTGOING_INITATED: + let head = {mime: 'application/tinode-video-call'}; + this.handleSendMessage('started', undefined, undefined, head) + .then((ctrl) => { + if (ctrl.code < 200 || ctrl.code >= 300 || + typeof ctrl.params == 'undefined' || typeof ctrl.params['seq'] == 'undefined') { + this.handleCallClose(); + return; + } + this.setState({callSeq: ctrl.params['seq']}); + }); + break; + case CALL_STATE_IN_PROGRESS: + const topic = this.tinode.getTopic(callTopic); + if (!topic) { + return; + } + // We've accepted the call. Let the other side know. + topic.call('accept', callSeq).catch((err) => { + this.handleCallClose(); + this.handleError(err.message, 'err'); }); - } else if (callState == 3) { - const topic = this.tinode.getTopic(callTopic); - if (!topic) { - return; - } - topic.call('accept', callSeq).then((ctrl) => { - this.handleTeleLeaseReply(ctrl, 2000); - }); + break; } } @@ -1818,21 +1760,29 @@ class TinodeWeb extends React.Component { } this.setState({ callTopic: undefined, - callState: 0, - callLeaseExpires: undefined, - callTimeout: undefined + callState: CALL_STATE_NONE }); } handleTeleAcceptCall(topicName) { - this.handleTopicSelected(this.state.callTopic); - this.setState({ - callState: 3 - }); + const topic = this.tinode.getTopic(topicName); + if (!topic) { + return; + } + if (topic.isSubscribed()) { + this.handleTopicSelected(this.state.callTopic); + this.setState({ + callState: CALL_STATE_IN_PROGRESS + }); + } else { + // We need to switch and subscribe to callTopic first. + this.setState({ + callShouldStart: true, + }, () => this.handleTopicSelected(this.state.callTopic)); + } } handleInfoMessage(info) { - // if (info.what != 'call') { return; } @@ -1840,12 +1790,11 @@ class TinodeWeb extends React.Component { case 'invite': console.log('invite: ', info); if (info.from != this.state.myUserId) { - // incoming call. + // Incoming call. this.setState({ callTopic: info.src, - callState: 2, + callState: CALL_STATE_INCOMING_RECEIVED, callSeq: info.seq - //sdp: info.sdp }); } break; @@ -1854,28 +1803,21 @@ class TinodeWeb extends React.Component { // If another my session has accepted the call. if (info.topic == 'me' && info.from == this.state.myUserId) { this.setState({ - callTopic: null,//tele.src, - callState: 0, - callSeq: null//tele.seq, - //sdp: tele.sdp + callTopic: null, + callState: CALL_STATE_NONE, + callSeq: null }); + return; } if (info.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); - } + this.setState({callState: CALL_STATE_IN_PROGRESS}); } break; case 'hang-up': - //if (tele.topic == 'me') { - // remote hangup. + // Remote hangup. this.handleCallClose(); break; - //} } } @@ -2081,7 +2023,7 @@ class TinodeWeb extends React.Component { null } - {this.state.callTopic && this.state.callState == 2 ? + {this.state.callTopic && this.state.callState == CALL_STATE_INCOMING_RECEIVED ? pc.addTrack(track, stream)); - console.log('RtcPanel: info msg', info); + stream.getTracks().forEach(track => { + console.log('local stream adding track ', track.id, track.kind, track.readyState); + pc.addTrack(track, stream); + }); } onInfo(info) { @@ -87,7 +90,6 @@ export default class RtcPanel extends React.PureComponent { return; } switch (info.event) { - //case 'invite' case 'accept': // Call accepted. this.handleVideoCallAccepted(info); @@ -107,6 +109,7 @@ export default class RtcPanel extends React.PureComponent { } } + /* componentDidUpdate(props) { const topic = this.props.topic; if (this.onInfo != topic.onInfo) { @@ -114,31 +117,43 @@ export default class RtcPanel extends React.PureComponent { topic.onInfo = this.onInfo; } } + */ componentDidMount() { - if ((this.props.callState == 1 || this.props.callState == 3) && this.localRef.current) { - this.launchStream(); + const topic = this.props.topic; + this.previousOnInfo = topic.onInfo; + topic.onInfo = this.onInfo; + if ((this.props.callState == CALL_STATE_OUTGOING_INITATED || + this.props.callState == CALL_STATE_IN_PROGRESS) && this.localRef.current) { + this.start(); } } componentWillUnmount() { this.props.topic.onInfo = this.previousOnInfo; + console.log('will unmount. stop'); this.stop(); } - launchStream() { + start() { 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). + if (this.props.callState == CALL_STATE_IN_PROGRESS) { + // We apparently just accepted the call. this.props.onInvite(this.props.topic.name, this.props.seq, this.props.callState); - }) - .catch(this.handleGetUserMediaError); + return; + } + // This is an outgoing call waiting for the other side to pick up. + // Start local video. + navigator.mediaDevices.getUserMedia(this.localStreamConstraints) + .then(stream => { + this.setState({localStream: stream}); + this.localRef.current.srcObject = stream; + // Send call invitation. + this.props.onInvite(this.props.topic.name, this.props.seq, this.props.callState); + }) + .catch(this.handleGetUserMediaError); } } @@ -178,7 +193,7 @@ export default class RtcPanel extends React.PureComponent { .then(() => { this.props.onSendOffer(this.props.topic.name, this.props.seq, this.state.pc.localDescription.toJSON()); }) - .catch(reportError); + .catch(this.reportError); } handleICECandidateEvent(event) { @@ -191,7 +206,7 @@ export default class RtcPanel extends React.PureComponent { var candidate = new RTCIceCandidate(info.payload); this.state.pc.addIceCandidate(candidate) - .catch(reportError); + .catch(this.reportError); } handleICEConnectionStateChangeEvent(event) { @@ -243,7 +258,6 @@ export default class RtcPanel extends React.PureComponent { this.handleCloseClick(); } - handleVideoOfferMsg(info) { var localStream = null; @@ -252,6 +266,7 @@ export default class RtcPanel extends React.PureComponent { var desc = new RTCSessionDescription(info.payload); pc.setRemoteDescription(desc).then(() => { + console.log('!!! handleVideoOffer/setRemoteDescription -> getUserMesia'); return navigator.mediaDevices.getUserMedia(this.localStreamConstraints); }) .then(stream => { @@ -259,7 +274,10 @@ export default class RtcPanel extends React.PureComponent { this.localRef.current.srcObject = stream; this.setState({localStream: stream}); - localStream.getTracks().forEach(track => pc.addTrack(track, localStream)); + localStream.getTracks().forEach(track => { + console.log('local stream A adding track ', track.id, track.kind, track.readyState); + pc.addTrack(track, localStream); + }); }) .then(() => { return pc.createAnswer(); @@ -284,11 +302,14 @@ export default class RtcPanel extends React.PureComponent { let tracks = stream.getTracks(); if (tracks != null) { - tracks.forEach(function(track) { + tracks.forEach((track) => { track.stop(); + track.enabled = false; + console.log('stopped track ', track.id, track.kind, track.readyState); }); } el.srcObject = null; + el.src = ''; } stop() {