Calls logic minor cleanup.

This commit is contained in:
aforge
2022-04-06 23:19:09 -07:00
parent b310a94180
commit 4b9078e64a
4 changed files with 136 additions and 163 deletions
+6
View File
@@ -106,3 +106,9 @@ export const LINK_PRIVACY_POLICY = 'https://tinode.co/privacy.html';
// Link to Terms of Service. // Link to Terms of Service.
export const LINK_TERMS_OF_SERVICE = 'https://tinode.co/terms.html'; 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;
+14 -10
View File
@@ -19,7 +19,8 @@ import SendMessage from '../widgets/send-message.jsx';
import { DEFAULT_P2P_ACCESS_MODE, IMAGE_PREVIEW_DIM, KEYPRESS_DELAY, import { DEFAULT_P2P_ACCESS_MODE, IMAGE_PREVIEW_DIM, KEYPRESS_DELAY,
MESSAGES_PAGE, MAX_EXTERN_ATTACHMENT_SIZE, MAX_IMAGE_DIM, MAX_INBAND_ATTACHMENT_SIZE, 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, import { blobToBase64, fileToBase64,
imageScaled, makeImageUrl } from '../lib/blob-helpers.js'; imageScaled, makeImageUrl } from '../lib/blob-helpers.js';
import HashNavigation from '../lib/navigation.js'; import HashNavigation from '../lib/navigation.js';
@@ -78,6 +79,13 @@ function isPeerRestricted(acs) {
return false; 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 { class MessagesView extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
@@ -196,11 +204,6 @@ class MessagesView extends React.Component {
static getDerivedStateFromProps(nextProps, prevState) { static getDerivedStateFromProps(nextProps, prevState) {
let nextState = {}; let nextState = {};
/*
if (nextProps.callState == 3 || nextProps.callState == 1) {
nextState.rtcPanel = { topic: nextProps.topic };
}
*/
if (!nextProps.topic) { if (!nextProps.topic) {
// Default state: no topic. // Default state: no topic.
nextState = { nextState = {
@@ -216,7 +219,7 @@ class MessagesView extends React.Component {
docPreview: null, docPreview: null,
imagePreview: null, imagePreview: null,
imagePostview: 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, typingIndicator: false,
scrollPosition: 0, scrollPosition: 0,
fetchingMessages: false, fetchingMessages: false,
@@ -293,7 +296,7 @@ class MessagesView extends React.Component {
channel: topic.isChannelType() 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, { Object.assign(nextState, {
rtcPanel: nextProps.callTopic, rtcPanel: nextProps.callTopic,
}); });
@@ -311,8 +314,9 @@ class MessagesView extends React.Component {
}); });
} }
} else { } else {
// same topic // We are still in same topic. Show the call panel if necessary.
if (!prevState.rtcPanel && nextProps.callTopic == prevState.topic && (nextProps.callState == 1 || nextProps.callState == 3)) { if (nextProps.callTopic == prevState.topic && !prevState.rtcPanel &&
shouldPresentCallPanel(nextProps.callState)) {
Object.assign(nextState, { Object.assign(nextState, {
rtcPanel: nextProps.callTopic, rtcPanel: nextProps.callTopic,
}); });
+59 -117
View File
@@ -17,7 +17,9 @@ import MessagesView from './messages-view.jsx';
import SidepanelView from './sidepanel-view.jsx'; import SidepanelView from './sidepanel-view.jsx';
import { API_KEY, APP_NAME, DEFAULT_P2P_ACCESS_MODE, FORWARDED_PREVIEW_LENGTH, LOGGING_ENABLED, 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 { PACKAGE_VERSION } from '../version.js';
import { base64ReEncode, makeImageUrl } from '../lib/blob-helpers.js'; import { base64ReEncode, makeImageUrl } from '../lib/blob-helpers.js';
import { detectServerAddress, isLocalHost, isSecureConnection } from '../lib/host-name.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.handleTeleSendAnswer = this.handleTeleSendAnswer.bind(this);
this.handleTeleAcceptCall = this.handleTeleAcceptCall.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); this.sendMessageToTopic = this.sendMessageToTopic.bind(this);
} }
@@ -219,9 +218,12 @@ class TinodeWeb extends React.Component {
infoPanel: undefined, infoPanel: undefined,
mobilePanel: 'sidepanel', mobilePanel: 'sidepanel',
// Video calls.
callTopic: undefined, callTopic: undefined,
callState: 0, callState: CALL_STATE_NONE,
//isCallOriginator: false, // If true, call state should be transitioned to CALL_STATE_IN_PROGRESS upon
// switching to the call topic.
callShouldStart: false,
contextMenuVisible: false, contextMenuVisible: false,
contextMenuBounds: null, contextMenuBounds: null,
@@ -486,7 +488,13 @@ class TinodeWeb extends React.Component {
} }
handleError(err, level, action, actionText) { 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. // User clicked Login button in the side panel.
@@ -852,26 +860,7 @@ class TinodeWeb extends React.Component {
this.receivedTimer = undefined; this.receivedTimer = undefined;
topic.noteRecv(data.seq); topic.noteRecv(data.seq);
}, RECEIVED_DELAY); }, 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 */ /* 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. // New topic was created, here is the new topic name.
handleNewTopicCreated(oldName, newName) { 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 (this.state.topicSelected == oldName && oldName != newName) {
// If the current URl contains the old topic name, replace it with new. // 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 // Update the name of the selected topic first so the navigator doen't clear
// the state. // 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)); HashNavigation.navigateTo(HashNavigation.setUrlTopic('', newName));
}); });
} }
@@ -1679,91 +1677,35 @@ class TinodeWeb extends React.Component {
handleStartVideoCall() { handleStartVideoCall() {
this.setState({ this.setState({
callTopic: this.state.topicSelected, 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) { handleTeleInvite(callTopic, callSeq, callState) {
if (callState == 1) { switch (callState) {
case CALL_STATE_OUTGOING_INITATED:
let head = {mime: 'application/tinode-video-call'}; let head = {mime: 'application/tinode-video-call'};
this.handleSendMessage('started', undefined, undefined, head) this.handleSendMessage('started', undefined, undefined, head)
.then((ctrl) => { .then((ctrl) => {
if (ctrl.code < 200 || ctrl.code >= 300 || typeof ctrl.params == 'undefined' || typeof ctrl.params['seq'] == 'undefined') { if (ctrl.code < 200 || ctrl.code >= 300 ||
typeof ctrl.params == 'undefined' || typeof ctrl.params['seq'] == 'undefined') {
this.handleCallClose(); this.handleCallClose();
return; return;
} }
this.setState({callSeq: ctrl.params['seq']}); this.setState({callSeq: ctrl.params['seq']});
}); });
} else if (callState == 3) { break;
case CALL_STATE_IN_PROGRESS:
const topic = this.tinode.getTopic(callTopic); const topic = this.tinode.getTopic(callTopic);
if (!topic) { if (!topic) {
return; return;
} }
topic.call('accept', callSeq).then((ctrl) => { // We've accepted the call. Let the other side know.
this.handleTeleLeaseReply(ctrl, 2000); topic.call('accept', callSeq).catch((err) => {
this.handleCallClose();
this.handleError(err.message, 'err');
}); });
break;
} }
} }
@@ -1818,21 +1760,29 @@ class TinodeWeb extends React.Component {
} }
this.setState({ this.setState({
callTopic: undefined, callTopic: undefined,
callState: 0, callState: CALL_STATE_NONE
callLeaseExpires: undefined,
callTimeout: undefined
}); });
} }
handleTeleAcceptCall(topicName) { handleTeleAcceptCall(topicName) {
const topic = this.tinode.getTopic(topicName);
if (!topic) {
return;
}
if (topic.isSubscribed()) {
this.handleTopicSelected(this.state.callTopic); this.handleTopicSelected(this.state.callTopic);
this.setState({ this.setState({
callState: 3 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) { handleInfoMessage(info) {
//
if (info.what != 'call') { if (info.what != 'call') {
return; return;
} }
@@ -1840,12 +1790,11 @@ class TinodeWeb extends React.Component {
case 'invite': case 'invite':
console.log('invite: ', info); console.log('invite: ', info);
if (info.from != this.state.myUserId) { if (info.from != this.state.myUserId) {
// incoming call. // Incoming call.
this.setState({ this.setState({
callTopic: info.src, callTopic: info.src,
callState: 2, callState: CALL_STATE_INCOMING_RECEIVED,
callSeq: info.seq callSeq: info.seq
//sdp: info.sdp
}); });
} }
break; break;
@@ -1854,28 +1803,21 @@ class TinodeWeb extends React.Component {
// If another my session has accepted the call. // If another my session has accepted the call.
if (info.topic == 'me' && info.from == this.state.myUserId) { if (info.topic == 'me' && info.from == this.state.myUserId) {
this.setState({ this.setState({
callTopic: null,//tele.src, callTopic: null,
callState: 0, callState: CALL_STATE_NONE,
callSeq: null//tele.seq, callSeq: null
//sdp: tele.sdp
}); });
return;
} }
if (info.topic == this.state.callTopic) { if (info.topic == this.state.callTopic) {
// Update state. // Update state.
this.setState({callState: 3}); this.setState({callState: CALL_STATE_IN_PROGRESS});
if (this.state.callTimeout) {
clearTimeout(this.state.callTimeout);
this.setState({callTimeout: undefined});
this.setLeaseTimer(this.state.callSeq, this.state.callLeaseExpires, Date.now(), 2000);
}
} }
break; break;
case 'hang-up': case 'hang-up':
//if (tele.topic == 'me') { // Remote hangup.
// remote hangup.
this.handleCallClose(); this.handleCallClose();
break; break;
//}
} }
} }
@@ -2081,7 +2023,7 @@ class TinodeWeb extends React.Component {
null null
} }
{this.state.callTopic && this.state.callState == 2 ? {this.state.callTopic && this.state.callState == CALL_STATE_INCOMING_RECEIVED ?
<CallView <CallView
tinode={this.tinode} tinode={this.tinode}
onClose={this.handleCallClose} onClose={this.handleCallClose}
+36 -15
View File
@@ -2,6 +2,8 @@ import React from 'react';
import LetterTile from '../widgets/letter-tile.jsx'; import LetterTile from '../widgets/letter-tile.jsx';
import { CALL_STATE_OUTGOING_INITATED, CALL_STATE_IN_PROGRESS } from '../config.js';
export default class RtcPanel extends React.PureComponent { export default class RtcPanel extends React.PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
@@ -12,7 +14,6 @@ export default class RtcPanel extends React.PureComponent {
isStarted: false, isStarted: false,
localStream: undefined, localStream: undefined,
pc: undefined, pc: undefined,
remoteStream: undefined,
previousOnInfo: undefined previousOnInfo: undefined
}; };
@@ -43,7 +44,7 @@ export default class RtcPanel extends React.PureComponent {
this.remoteRef = React.createRef(); this.remoteRef = React.createRef();
this.onInfo = this.onInfo.bind(this); this.onInfo = this.onInfo.bind(this);
this.launchStream = this.launchStream.bind(this); this.start = this.start.bind(this);
this.createPeerConnection = this.createPeerConnection.bind(this); this.createPeerConnection = this.createPeerConnection.bind(this);
@@ -77,8 +78,10 @@ export default class RtcPanel extends React.PureComponent {
handleVideoCallAccepted(info) { handleVideoCallAccepted(info) {
const pc = this.createPeerConnection(); const pc = this.createPeerConnection();
const stream = this.state.localStream; const stream = this.state.localStream;
stream.getTracks().forEach(track => pc.addTrack(track, stream)); stream.getTracks().forEach(track => {
console.log('RtcPanel: info msg', info); console.log('local stream adding track ', track.id, track.kind, track.readyState);
pc.addTrack(track, stream);
});
} }
onInfo(info) { onInfo(info) {
@@ -87,7 +90,6 @@ export default class RtcPanel extends React.PureComponent {
return; return;
} }
switch (info.event) { switch (info.event) {
//case 'invite'
case 'accept': case 'accept':
// Call accepted. // Call accepted.
this.handleVideoCallAccepted(info); this.handleVideoCallAccepted(info);
@@ -107,6 +109,7 @@ export default class RtcPanel extends React.PureComponent {
} }
} }
/*
componentDidUpdate(props) { componentDidUpdate(props) {
const topic = this.props.topic; const topic = this.props.topic;
if (this.onInfo != topic.onInfo) { if (this.onInfo != topic.onInfo) {
@@ -114,28 +117,40 @@ export default class RtcPanel extends React.PureComponent {
topic.onInfo = this.onInfo; topic.onInfo = this.onInfo;
} }
} }
*/
componentDidMount() { componentDidMount() {
if ((this.props.callState == 1 || this.props.callState == 3) && this.localRef.current) { const topic = this.props.topic;
this.launchStream(); 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() { componentWillUnmount() {
this.props.topic.onInfo = this.previousOnInfo; this.props.topic.onInfo = this.previousOnInfo;
console.log('will unmount. stop');
this.stop(); this.stop();
} }
launchStream() { start() {
if (this.state.localStream) { if (this.state.localStream) {
console.log("You can't start a call because you already have one open!"); console.log("You can't start a call because you already have one open!");
} else { } else {
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);
return;
}
// This is an outgoing call waiting for the other side to pick up.
// Start local video.
navigator.mediaDevices.getUserMedia(this.localStreamConstraints) navigator.mediaDevices.getUserMedia(this.localStreamConstraints)
.then(stream => { .then(stream => {
console.log('Adding local stream.', this.localRef.current);
this.setState({localStream: stream}); this.setState({localStream: stream});
this.localRef.current.srcObject = stream; this.localRef.current.srcObject = stream;
// Send call invitation (or accept call). // Send call invitation.
this.props.onInvite(this.props.topic.name, this.props.seq, this.props.callState); this.props.onInvite(this.props.topic.name, this.props.seq, this.props.callState);
}) })
.catch(this.handleGetUserMediaError); .catch(this.handleGetUserMediaError);
@@ -178,7 +193,7 @@ export default class RtcPanel extends React.PureComponent {
.then(() => { .then(() => {
this.props.onSendOffer(this.props.topic.name, this.props.seq, this.state.pc.localDescription.toJSON()); this.props.onSendOffer(this.props.topic.name, this.props.seq, this.state.pc.localDescription.toJSON());
}) })
.catch(reportError); .catch(this.reportError);
} }
handleICECandidateEvent(event) { handleICECandidateEvent(event) {
@@ -191,7 +206,7 @@ export default class RtcPanel extends React.PureComponent {
var candidate = new RTCIceCandidate(info.payload); var candidate = new RTCIceCandidate(info.payload);
this.state.pc.addIceCandidate(candidate) this.state.pc.addIceCandidate(candidate)
.catch(reportError); .catch(this.reportError);
} }
handleICEConnectionStateChangeEvent(event) { handleICEConnectionStateChangeEvent(event) {
@@ -243,7 +258,6 @@ export default class RtcPanel extends React.PureComponent {
this.handleCloseClick(); this.handleCloseClick();
} }
handleVideoOfferMsg(info) { handleVideoOfferMsg(info) {
var localStream = null; var localStream = null;
@@ -252,6 +266,7 @@ export default class RtcPanel extends React.PureComponent {
var desc = new RTCSessionDescription(info.payload); var desc = new RTCSessionDescription(info.payload);
pc.setRemoteDescription(desc).then(() => { pc.setRemoteDescription(desc).then(() => {
console.log('!!! handleVideoOffer/setRemoteDescription -> getUserMesia');
return navigator.mediaDevices.getUserMedia(this.localStreamConstraints); return navigator.mediaDevices.getUserMedia(this.localStreamConstraints);
}) })
.then(stream => { .then(stream => {
@@ -259,7 +274,10 @@ export default class RtcPanel extends React.PureComponent {
this.localRef.current.srcObject = stream; this.localRef.current.srcObject = stream;
this.setState({localStream: 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(() => { .then(() => {
return pc.createAnswer(); return pc.createAnswer();
@@ -284,11 +302,14 @@ export default class RtcPanel extends React.PureComponent {
let tracks = stream.getTracks(); let tracks = stream.getTracks();
if (tracks != null) { if (tracks != null) {
tracks.forEach(function(track) { tracks.forEach((track) => {
track.stop(); track.stop();
track.enabled = false;
console.log('stopped track ', track.id, track.kind, track.readyState);
}); });
} }
el.srcObject = null; el.srcObject = null;
el.src = '';
} }
stop() { stop() {