diff --git a/app/components/CallOverlay.js b/app/components/CallOverlay.js
index 1511094..ca402e4 100644
--- a/app/components/CallOverlay.js
+++ b/app/components/CallOverlay.js
@@ -1,218 +1,221 @@
import React from 'react';
import { View, Text } from 'react-native';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import moment from 'moment';
import momentFormat from 'moment-duration-format';
import autoBind from 'auto-bind';
import { Appbar } from 'react-native-paper';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import { Colors } from 'react-native-paper';
import styles from '../assets/styles/blink/_AudioCallBox.scss';
function toTitleCase(str) {
return str.replace(
/\w\S*/g,
function(txt) {
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
}
);
}
class CallOverlay extends React.Component {
constructor(props) {
super(props);
autoBind(this);
this.state = {
call: this.props.call,
callState: this.props.call ? this.props.call.state : null,
direction: this.props.call ? this.props.call.direction: null,
remoteUri: this.props.remoteUri,
remoteDisplayName: this.props.remoteDisplayName,
reconnectingCall: this.props.reconnectingCall
}
this.duration = null;
this.finalDuration = null;
this.timer = null;
this._isMounted = true;
}
componentDidMount() {
if (this.state.call) {
if (this.state.call.state === 'established') {
this.startTimer();
}
this.state.call.on('stateChanged', this.callStateChanged);
this.setState({callState: this.state.call.state});
}
}
+ componentWillUnmount() {
+ this._isMounted = false;
+
+ if (this.state.call) {
+ this.state.call.removeListener('stateChanged', this.callStateChanged);
+ }
+
+ clearTimeout(this.timer);
+ }
+
+
//getDerivedStateFromProps(nextProps, state) {
UNSAFE_componentWillReceiveProps(nextProps) {
if (!this._isMounted) {
return;
}
if (nextProps.reconnectingCall != this.state.reconnectingCall) {
this.setState({reconnectingCall: nextProps.reconnectingCall});
}
if (nextProps.call !== null && nextProps.call !== this.state.call) {
nextProps.call.on('stateChanged', this.callStateChanged);
if (this.state.call !== null) {
this.state.call.removeListener('stateChanged', this.callStateChanged);
}
this.setState({call: nextProps.call});
}
- this.setState({remoteDisplayName: nextProps.remoteDisplayName, remoteUri: nextProps.remoteUri});
- }
-
- componentWillUnmount() {
- this._isMounted = false;
-
- if (this.state.call) {
- this.state.call.removeListener('stateChanged', this.callStateChanged);
- }
-
- clearTimeout(this.timer);
+ this.setState({remoteDisplayName: nextProps.remoteDisplayName,
+ remoteUri: nextProps.remoteUri});
}
callStateChanged(oldState, newState, data) {
if (newState === 'established' && this._isMounted && !this.props.terminated) {
this.startTimer();
}
if (newState === 'terminated') {
if (this.state.call) {
this.state.call.removeListener('stateChanged', this.callStateChanged);
}
clearTimeout(this.timer);
this.finalDuration = this.duration;
this.duration = null;
this.timer = null;
}
if (!this._isMounted) {
return;
}
this.setState({callState: newState});
}
startTimer() {
if (this.timer !== null) {
// already armed
return;
}
// TODO: consider using window.requestAnimationFrame
const startTime = new Date();
this.timer = setInterval(() => {
const duration = moment.duration(new Date() - startTime);
if (this.duration > 3600) {
this.duration = duration.format('hh:mm:ss', {trim: false});
} else {
this.duration = duration.format('mm:ss', {trim: false});
}
if (this.props.show) {
this.forceUpdate();
}
}, 1000);
}
render() {
let header = null;
if (this.props.terminated) {
clearTimeout(this.timer);
this.duration = null;
this.timer = null;
}
let displayName = this.state.remoteUri;
if (this.state.remoteDisplayName && this.state.remoteDisplayName !== this.state.remoteUri) {
displayName = this.state.remoteDisplayName;
}
if (this.props.show) {
let callDetail;
if (this.duration) {
callDetail = {this.duration};
callDetail = 'Duration: ' + this.duration;
} else {
if (this.state.reconnectingCall) {
callDetail = 'Reconnecting the call...';
} else if (this.props.terminated) {
callDetail = 'Call ended';
} else if (this.state.callState === 'terminated') {
callDetail = this.finalDuration ? 'Call ended after ' + this.finalDuration : 'Call ended';
} else {
if (this.state.callState) {
if (this.state.callState === 'incoming') {
callDetail = 'Waiting for incoming call...';
+ } else if (this.state.callState === 'accepted') {
+ callDetail = 'Accepted. Waiting for media...';
} else {
callDetail = toTitleCase(this.state.callState);
}
} else if (this.state.direction) {
callDetail = 'Connecting', this.state.direction, 'call...';
} else {
callDetail = 'Connecting...';
}
}
}
if (this.props.info) {
callDetail = callDetail + ' - ' + this.props.info;
}
if (this.state.remoteUri && this.state.remoteUri.search('videoconference') > -1) {
displayName = this.state.remoteUri.split('@')[0];
header = (
);
} else {
header = (
);
}
}
return header
}
}
CallOverlay.propTypes = {
show: PropTypes.bool.isRequired,
remoteUri: PropTypes.string,
remoteDisplayName: PropTypes.string,
call: PropTypes.object,
connection: PropTypes.object,
reconnectingCall: PropTypes.bool,
terminated : PropTypes.bool,
info: PropTypes.string
};
-
export default CallOverlay;
diff --git a/app/components/Conference.js b/app/components/Conference.js
index c803729..ae9a997 100644
--- a/app/components/Conference.js
+++ b/app/components/Conference.js
@@ -1,404 +1,405 @@
import React from 'react';
import { View } from 'react-native';
import PropTypes from 'prop-types';
import assert from 'assert';
import debug from 'react-native-debug';
import autoBind from 'auto-bind';
import ConferenceBox from './ConferenceBox';
import LocalMedia from './LocalMedia';
import config from '../config';
import utils from '../utils';
const DEBUG = debug('blinkrtc:Conference');
debug.enable('*');
class Conference extends React.Component {
constructor(props) {
super(props);
autoBind(this);
this.defaultWaitInterval = 90; // until we can connect or reconnect
this.waitCounter = 0;
this.waitInterval = this.defaultWaitInterval;
this.userHangup = false;
this.ended = false;
this.started = false;
this.participants = [];
this.state = {
currentCall: null,
callState: null,
targetUri: this.props.targetUri,
callUUID: this.props.callUUID,
localMedia: this.props.localMedia,
connection: this.props.connection,
account: this.props.account,
registrationState: this.props.registrationState,
startedByPush: this.props.startedByPush,
reconnectingCall: this.props.reconnectingCall,
myInvitedParties: this.props.myInvitedParties,
isFavorite: this.props.favoriteUris.indexOf(this.props.targetUri) > -1
}
if (this.props.connection) {
this.props.connection.on('stateChanged', this.connectionStateChanged);
}
if (this.props.participantsToInvite) {
this.props.participantsToInvite.forEach((p) => {
if (this.participants.indexOf(p) === -1) {
this.participants.push(p);
}
});
}
}
componentWillUnmount() {
this.ended = true;
if (this.state.currentCall) {
this.state.currentCall.removeListener('stateChanged', this.callStateChanged);
}
if (this.state.connection) {
this.state.connection.removeListener('stateChanged', this.connectionStateChanged);
}
}
callStateChanged(oldState, newState, data) {
- utils.timestampedLog('Conference: callStateChanged', oldState, '->', newState);
+ //utils.timestampedLog('Conference: callStateChanged', oldState, '->', newState);
if (newState === 'established') {
this.setState({reconnectingCall: false});
}
this.setState({callState: newState});
}
connectionStateChanged(oldState, newState) {
switch (newState) {
case 'disconnected':
if (oldState === 'ready') {
utils.timestampedLog('Conference: connection failed, reconnecting the call...');
this.waitInterval = this.defaultWaitInterval;
}
break;
default:
break;
}
}
//getDerivedStateFromProps(nextProps, state) {
UNSAFE_componentWillReceiveProps(nextProps) {
//console.log('Conference got props');
if (nextProps.account !== null && nextProps.account !== this.props.account) {
this.setState({account: nextProps.account});
}
this.setState({registrationState: nextProps.registrationState});
if (nextProps.connection !== null && nextProps.connection !== this.state.connection) {
this.setState({connection: nextProps.connection});
nextProps.connection.on('stateChanged', this.connectionStateChanged);
}
if (nextProps.reconnectingCall !== this.state.reconnectingCall) {
this.setState({reconnectingCall: nextProps.reconnectingCall});
}
if (nextProps.localMedia !== null && nextProps.localMedia !== this.state.localMedia) {
this.setState({localMedia: nextProps.localMedia});
}
if (nextProps.callUUID !== null && this.state.callUUID !== nextProps.callUUID) {
this.setState({callUUID: nextProps.callUUID,
reconnectingCall: true,
currentCall: null});
this.startCallWhenReady();
}
this.setState({myInvitedParties: nextProps.myInvitedParties,
isFavorite: nextProps.favoriteUris.indexOf(this.props.targetUri) > -1
});
}
mediaPlaying() {
this.startCallWhenReady();
}
canConnect() {
if (!this.state.localMedia) {
console.log('Conference: no local media');
return false;
}
if (!this.state.connection) {
console.log('Conference: no connection yet');
return false;
}
if (this.state.connection.state !== 'ready') {
console.log('Conference: connection is not ready');
return false;
}
if (!this.state.account) {
console.log('Conference: no account yet');
return false;
}
if (this.state.registrationState !== 'registered') {
console.log('Conference: account not ready yet');
return false;
}
if (this.state.currentCall) {
console.log('Conference: call already in progress');
return false;
}
return true;
}
async startCallWhenReady() {
utils.timestampedLog('Conference: start conference', this.state.callUUID, 'when ready to', this.props.targetUri);
this.waitCounter = 0;
//utils.timestampedLog('Conference: waiting for connecting to the conference', this.waitInterval, 'seconds');
let diff = 0;
while (this.waitCounter < this.waitInterval) {
if (this.userHangup) {
this.props.hangupCall(this.state.callUUID, 'user_cancelled_conference');
return;
}
if (this.state.currentCall) {
return;
}
if (this.waitCounter >= this.waitInterval - 1) {
utils.timestampedLog('Conference: cancelling conference', this.state.callUUID);
this.props.hangupCall(this.state.callUUID, 'timeout');
}
if (!this.canConnect()) {
- //console.log('Waiting', this.waitCounter);
+ console.log('Retrying for', (this.waitInterval - this.waitCounter), 'seconds');
await this._sleep(1000);
} else {
this.waitCounter = 0;
this.start();
return;
}
this.waitCounter++;
}
}
_sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
start() {
const options = {
id: this.state.callUUID,
pcConfig: {iceServers: config.iceServers},
localStream: this.state.localMedia,
audio: this.props.proposedMedia.audio,
video: this.props.proposedMedia.video,
offerOptions: {
offerToReceiveAudio: false,
offerToReceiveVideo: false
},
initialParticipants: this.props.participantsToInvite
};
utils.timestampedLog('Conference: Sylkrtc.js will start conference call', this.state.callUUID, 'to', this.props.targetUri.toLowerCase());
confCall = this.state.account.joinConference(this.props.targetUri.toLowerCase(), options);
if (confCall) {
confCall.on('stateChanged', this.callStateChanged);
this.setState({currentCall: confCall});
}
}
saveParticipant(callUUID, room, uri) {
console.log('Save saveParticipant', uri);
if (this.participants.indexOf(uri) === -1) {
this.participants.push(uri);
}
this.props.saveParticipant(callUUID, room, uri);
}
showSaveDialog() {
if (!this.userHangup) {
return false;
}
if (this.state.reconnectingCall) {
console.log('No save dialog because call is reconnecting')
return false;
}
if (this.participants.length === 0) {
console.log('No show dialog because there are no participants')
return false;
}
if (this.state.isFavorite) {
let room = this.state.targetUri.split('@')[0];
let must_display = false;
if (this.props.myInvitedParties.hasOwnProperty(room)) {
let old_participants = this.state.myInvitedParties[room];
this.participants.forEach((p) => {
if (old_participants.indexOf(p) === -1) {
console.log(p, 'is not in', old_participants);
must_display = true;
}
});
}
if (must_display) {
console.log('Show save dialog because we have new participants');
return true;
} else {
console.log('No save dialog because is already favorite with same participants')
return false;
}
} else {
console.log('Show save dialog because is not in favorites');
return true;
}
return true;
}
saveConference() {
if (!this.state.isFavorite) {
this.props.setFavoriteUri(this.props.targetUri);
}
let room = this.state.targetUri.split('@')[0];
if (this.props.myInvitedParties.hasOwnProperty(room)) {
let participants = this.state.myInvitedParties[room];
this.participants.forEach((p) => {
if (participants.indexOf(p) === -1) {
participants.push(p);
}
});
this.props.saveInvitedParties(this.props.targetUri, participants);
} else {
this.props.saveInvitedParties(this.props.targetUri, this.participants);
}
this.props.hangupCall(this.state.callUUID, 'user_hangup_conference_confirmed');
}
hangup(reason='user_hangup_conference') {
this.userHangup = true;
if (!this.showSaveDialog()) {
reason = 'user_hangup_conference_confirmed';
}
this.props.hangupCall(this.state.callUUID, reason);
if (this.waitCounter > 0) {
this.waitCounter = this.waitInterval;
}
}
render() {
let box = null;
if (this.state.localMedia !== null) {
if (this.state.currentCall != null && (this.state.callState === 'established')) {
box = (
);
} else {
box = (
);
}
} else {
console.log('Error: cannot start conference without local media');
}
return box;
}
}
Conference.propTypes = {
notificationCenter : PropTypes.func,
account : PropTypes.object,
connection : PropTypes.object,
registrationState : PropTypes.string,
hangupCall : PropTypes.func,
saveParticipant : PropTypes.func,
saveInvitedParties : PropTypes.func,
previousParticipants : PropTypes.array,
currentCall : PropTypes.object,
localMedia : PropTypes.object,
targetUri : PropTypes.string,
participantsToInvite : PropTypes.array,
generatedVideoTrack : PropTypes.bool,
toggleMute : PropTypes.func,
toggleSpeakerPhone : PropTypes.func,
callUUID : PropTypes.string,
proposedMedia : PropTypes.object,
isLandscape : PropTypes.bool,
isTablet : PropTypes.bool,
muted : PropTypes.bool,
defaultDomain : PropTypes.string,
startedByPush : PropTypes.bool,
inFocus : PropTypes.bool,
setFavoriteUri : PropTypes.func,
saveInvitedParties : PropTypes.func,
reconnectingCall : PropTypes.bool,
contacts : PropTypes.array,
favoriteUris : PropTypes.array
};
export default Conference;
diff --git a/app/components/ConferenceBox.js b/app/components/ConferenceBox.js
index a55ef7d..7e4fdc9 100644
--- a/app/components/ConferenceBox.js
+++ b/app/components/ConferenceBox.js
@@ -1,1472 +1,1514 @@
'use strict';
import React, {Component, Fragment} from 'react';
import { View, Platform, TouchableWithoutFeedback, Dimensions, SafeAreaView, ScrollView, FlatList } from 'react-native';
import PropTypes from 'prop-types';
import * as sylkrtc from 'react-native-sylkrtc';
import classNames from 'classnames';
import debug from 'react-native-debug';
import superagent from 'superagent';
import autoBind from 'auto-bind';
import { RTCView } from 'react-native-webrtc';
import { IconButton, Appbar, Portal, Modal, Surface, Paragraph } from 'react-native-paper';
import config from '../config';
import utils from '../utils';
//import AudioPlayer from './AudioPlayer';
import ConferenceDrawer from './ConferenceDrawer';
import ConferenceDrawerLog from './ConferenceDrawerLog';
// import ConferenceDrawerFiles from './ConferenceDrawerFiles';
import ConferenceDrawerParticipant from './ConferenceDrawerParticipant';
import ConferenceDrawerParticipantList from './ConferenceDrawerParticipantList';
import ConferenceDrawerSpeakerSelection from './ConferenceDrawerSpeakerSelection';
import ConferenceDrawerSpeakerSelectionWrapper from './ConferenceDrawerSpeakerSelectionWrapper';
import ConferenceHeader from './ConferenceHeader';
import ConferenceCarousel from './ConferenceCarousel';
import ConferenceParticipant from './ConferenceParticipant';
import ConferenceMatrixParticipant from './ConferenceMatrixParticipant';
import ConferenceParticipantSelf from './ConferenceParticipantSelf';
import InviteParticipantsModal from './InviteParticipantsModal';
import ConferenceAudioParticipantList from './ConferenceAudioParticipantList';
import ConferenceAudioParticipant from './ConferenceAudioParticipant';
import styles from '../assets/styles/blink/_ConferenceBox.scss';
const DEBUG = debug('blinkrtc:ConferenceBox');
debug.enable('*');
function toTitleCase(str) {
return str.replace(
/\w\S*/g,
function(txt) {
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
}
);
}
class ConferenceBox extends Component {
constructor(props) {
super(props);
autoBind(this);
this.audioBytesReceived = new Map();
this.audioBandwidth = new Map();
this.bandwidthDownload = 0;
this.bandwidthUpload = 0;
this.videoBytesReceived = new Map();
this.videoBandwidth = new Map();
this.audioPacketLoss = new Map();
this.videoPacketLoss = new Map();
this.packetLoss = new Map();
this.latency = new Map();
this.mediaLost = new Map();
this.sampleInterval = 5;
this.state = {
callOverlayVisible: true,
+ call: this.props.call,
ended: false,
audioMuted: this.props.muted,
videoMuted: !this.props.inFocus,
videoMutedbyUser: false,
participants: props.call.participants.slice(),
showInviteModal: false,
showDrawer: false,
showFiles: false,
shareOverlayVisible: false,
showSpeakerSelection: false,
activeSpeakers: props.call.activeParticipants.slice(),
selfDisplayedLarge: false,
eventLog: [],
sharedFiles: props.call.sharedFiles.slice(),
largeVideoStream: null,
previousParticipants: this.props.previousParticipants,
inFocus: this.props.inFocus,
reconnectingCall: this.props.reconnectingCall,
terminated: this.props.terminated
};
const friendlyName = this.props.remoteUri.split('@')[0];
//if (window.location.origin.startsWith('file://')) {
this.callUrl = `${config.publicUrl}/conference/${friendlyName}`;
//} else {
// this.callUrl = `${window.location.origin}/conference/${friendlyName}`;
//}
const emailMessage = `You can join me in the conference using a Web browser at ${this.callUrl} ` +
'or by using the freely available Sylk WebRTC client app at http://sylkserver.com';
const subject = 'Join me, maybe?';
this.emailLink = `mailto:?subject=${encodeURI(subject)}&body=${encodeURI(emailMessage)}`;
this.overlayTimer = null;
this.logEvent = {};
this.haveVideo = false;
this.uploads = [];
this.selectSpeaker = 1;
this.foundContacts = new Map();
if (this.props.call) {
this.lookupContact(this.props.call.localIdentity._uri, this.props.call.localIdentity._displayName);
}
[
'error',
'warning',
'info',
'debug'
].forEach((level) => {
this.logEvent[level] = (
(action, messages, originator) => {
const log = this.state.eventLog.slice();
log.unshift({originator, originator, level: level, action: action, messages: messages});
this.setState({eventLog: log});
}
);
});
this.invitedParticipants = new Map();
props.initialParticipants.forEach((uri) => {
this.invitedParticipants.set(uri, {timestamp: Date.now(), status: 'Invited'})
this.lookupContact(uri);
});
this.participantsTimer = setInterval(() => {
this.updateParticipantsStatus();
}, this.sampleInterval * 1000);
}
getInfo() {
let info;
if (this.bandwidthDownload > 0 && this.bandwidthUpload > 0) {
info = '⇣' + this.bandwidthDownload + ' ⇡' + this.bandwidthUpload;
} else if (this.bandwidthDownload > 0) {
info = '⇣' + this.bandwidthDownload ;
} else if (this.bandwidthUpload > 0) {
info = '⇡' + this.bandwidthUpload;
}
if (info) {
return info + ' Mbit/s';
}
return info;
}
updateParticipantsStatus() {
let participants_uris = [];
this.state.participants.forEach((p) => {
participants_uris.push(p.identity._uri);
});
this.getConnectionStats();
const invitedParties = Array.from(this.invitedParticipants.keys());
//console.log('Invited participants', invitedParties);
//console.log('Current participants', participants_uris);
let p;
let interval;
invitedParties.forEach((_uri) => {
if (participants_uris.indexOf(_uri) > 0) {
this.invitedParticipants.delete(_uri);
}
p = this.invitedParticipants.get(_uri);
if (!p) {
return;
}
interval = Math.floor((Date.now() - p.timestamp) / 1000);
//console.log(_uri, 'was invited', interval, 'seconds ago');
if (interval >= 60) {
this.invitedParticipants.delete(_uri);
this.forceUpdate();
}
if (p.status.indexOf('Invited') > -1 && interval > 5) {
p.status = '.';
}
if (p.status.indexOf('.') > -1) {
if (interval > 45) {
p.status = 'No answer';
} else {
p.status = p.status + '.';
}
}
});
this.forceUpdate();
}
componentDidMount() {
for (let p of this.state.participants) {
p.on('stateChanged', this.onParticipantStateChanged);
p.attach();
}
this.props.call.on('participantJoined', this.onParticipantJoined);
this.props.call.on('participantLeft', this.onParticipantLeft);
this.props.call.on('roomConfigured', this.onConfigureRoom);
this.props.call.on('fileSharing', this.onFileSharing);
if (this.state.participants.length > 1) {
this.armOverlayTimer();
}
// attach to ourselves first if there are no other participants
if (this.state.participants.length === 0) {
setTimeout(() => {
const item = {
stream: this.props.call.getLocalStreams()[0],
identity: this.props.call.localIdentity
};
this.selectVideo(item);
});
} else {
this.state.participants.forEach((p) => {
if (p.identity._uri.search('guest.') === -1 && p.identity._uri !== this.props.call.localIdentity._uri) {
// used for history item
this.props.saveParticipant(this.props.call.id, this.props.remoteUri.split('@')[0], p.identity._uri);
this.lookupContact(p.identity._uri, p.identity._displayName);
}
});
// this.changeResolution();
}
if (this.props.call.getLocalStreams()[0].getVideoTracks().length !== 0) {
this.haveVideo = true;
}
if (this.state.videoMuted) {
this._muteVideo();
}
}
componentWillUnmount() {
clearTimeout(this.overlayTimer);
clearTimeout(this.participantsTimer);
this.uploads.forEach((upload) => {
this.props.notificationCenter().removeNotification(upload[1]);
upload[0].abort();
})
}
//getDerivedStateFromProps(nextProps, state) {
UNSAFE_componentWillReceiveProps(nextProps) {
if (nextProps.hasOwnProperty('muted')) {
this.setState({audioMuted: nextProps.muted});
}
+ if (nextProps.call !== null && nextProps.call !== this.state.call) {
+ this.setState({call: nextProps.call});
+ }
+
if (nextProps.inFocus !== this.state.inFocus) {
if (nextProps.inFocus) {
if (!this.state.videoMutedbyUser) {
this._resumeVideo();
}
} else {
this._muteVideo();
}
this.setState({inFocus: nextProps.inFocus});
}
if (nextProps.reconnectingCall !== this.state.reconnectingCall) {
this.setState({reconnectingCall: nextProps.reconnectingCall});
}
this.setState({terminated: nextProps.terminated});
}
findObjectByKey(array, key, value) {
for (var i = 0; i < array.length; i++) {
if (array[i][key] === value) {
return array[i];
}
}
return null;
}
lookupContact(uri, displayName) {
let photo;
let username = uri.split('@')[0];
if (this.props.contacts) {
let username = uri.split('@')[0];
let isPhoneNumber = username.match(/^(\+|0)(\d+)$/);
if (isPhoneNumber) {
var contact_obj = this.findObjectByKey(this.props.contacts, 'remoteParty', username);
} else {
var contact_obj = this.findObjectByKey(this.props.contacts, 'remoteParty', uri);
}
if (contact_obj) {
displayName = contact_obj.displayName;
photo = contact_obj.photo;
if (isPhoneNumber) {
uri = username;
}
} else {
if (isPhoneNumber) {
uri = username;
displayName = toTitleCase(username);
}
}
}
const c = {photo: photo, displayName: displayName || toTitleCase(username)};
this.foundContacts.set(uri, c)
}
getConnectionStats() {
let audioPackets = 0;
let videoPackets = 0;
let delay = 0;
let audioPacketsLost = 0;
let videoPacketsLost = 0;
let audioPacketLoss = 0;
let videoPacketLoss = 0;
let totalPackets = 0;
let totalPacketsLost = 0;
let totalPacketLoss = 0;
let totalAudioBandwidth = 0;
let totalVideoBandwidth = 0;
let totalSpeed = 0;
let bandwidthUpload = 0;
let mediaType;
if (this.state.participants.length === 0) {
this.bandwidthDownload = 0;
this.videoBandwidth.set('total', 0);
this.audioBandwidth.set('total', 0);
}
let participants = this.state.participants.concat(this.props.call);
participants.forEach((p) => {
if (!p._pc) {
return;
}
let identity;
if (p.identity) {
identity = p.identity.uri;
} else {
identity = 'myself';
}
p._pc.getStats(null).then(stats => {
audioPackets = 0;
videoPackets = 0;
audioPacketsLost = 0;
videoPacketsLost = 0;
audioPacketLoss = 0;
videoPacketLoss = 0;
stats.forEach(report => {
if (report.type === "ssrc") {
report.values.forEach(object => { if (object.mediaType) {
mediaType = object.mediaType;
}
});
report.values.forEach(object => {
if (object.bytesReceived && identity !== 'myself') {
const bytesReceived = Math.floor(object.bytesReceived);
if (mediaType === 'audio') {
if (this.audioBytesReceived.has(p.id)) {
const lastBytes = this.audioBytesReceived.get(p.id);
const diff = bytesReceived - lastBytes;
const speed = Math.floor(diff / this.sampleInterval * 8 / 1000);
totalAudioBandwidth = totalAudioBandwidth + speed;
totalSpeed = totalSpeed + speed;
//console.log(identity, 'audio bandwidth', speed, 'kbit/s from', identity);
this.audioBandwidth.set(p.id, speed);
}
this.audioBytesReceived.set(p.id, bytesReceived);
} else if (mediaType === 'video') {
if (this.videoBytesReceived.has(p.id)) {
const lastBytes = this.videoBytesReceived.get(p.id);
const diff = bytesReceived - lastBytes;
const speed = Math.floor(diff / this.sampleInterval * 8 / 1000);
totalVideoBandwidth = totalVideoBandwidth + speed;
totalSpeed = totalSpeed + speed;
//console.log(identity, 'video bandwidth', speed, 'kbit/s from', identity);
this.videoBandwidth.set(p.id, speed);
}
this.videoBytesReceived.set(p.id, bytesReceived);
}
} else if (object.bytesSent && identity === 'myself') {
const bytesSent = Math.floor(object.bytesSent);
if (mediaType === 'audio') {
if (this.audioBytesReceived.has(p.id)) {
const lastBytes = this.audioBytesReceived.get(p.id);
const diff = bytesSent - lastBytes;
const speed = Math.floor(diff / this.sampleInterval * 8 / 1000);
bandwidthUpload = bandwidthUpload + speed;
//console.log(identity, 'audio bandwidth', speed, 'kbit/s from', identity);
this.audioBandwidth.set(p.id, speed);
}
this.audioBytesReceived.set(p.id, bytesSent);
} else if (mediaType === 'video') {
if (this.videoBytesReceived.has(p.id)) {
const lastBytes = this.videoBytesReceived.get(p.id);
const diff = bytesSent - lastBytes;
const speed = Math.floor(diff / this.sampleInterval * 8 / 1000);
bandwidthUpload = bandwidthUpload + speed;
//console.log(identity, 'video bandwidth', speed, 'kbit/s from', identity);
this.videoBandwidth.set(p.id, speed);
}
this.videoBytesReceived.set(p.id, bytesSent);
}
} else if (object.totalAudioEnergy) {
//console.log('Total audio energy', object.totalAudioEnergy, 'from', identity);
} else if (object.audioOutputLevel) {
//console.log('Output level', object.audioOutputLevel, 'from', identity);
this.mediaLost.set(p.id, Math.floor(object.audioOutputLevel) < 5 ? true : false);
} else if (object.audioInputLevel) {
//console.log('Input level', object.audioInputLevel, 'from', identity);
this.mediaLost.set(p.id, Math.floor(object.audioInputLevel) < 5 ? true : false);
} else if (object.packetsLost) {
totalPackets = totalPackets + Math.floor(object.packetsLost);
totalPacketsLost = totalPacketsLost + Math.floor(object.packetsLost);
if (mediaType === 'audio') {
audioPackets = audioPackets + Math.floor(object.packetsLost);
audioPacketsLost = audioPacketsLost + Math.floor(object.packetsLost);
} else if (mediaType === 'video') {
videoPackets = videoPackets + Math.floor(object.packetsLost);
videoPacketsLost = videoPacketsLost + Math.floor(object.packetsLost);
}
if (object.packetsLost > 0) {
//console.log(identity, mediaType, 'packetsLost', object.packetsLost);
}
} else if (object.packetsReceived && identity !== 'myself') {
totalPackets = totalPackets + Math.floor(object.packetsReceived);
if (mediaType === 'audio') {
audioPackets = audioPackets + Math.floor(object.packetsReceived);
} else if (mediaType === 'video') {
videoPackets = videoPackets + Math.floor(object.packetsReceived);
}
//console.log(identity, mediaType, 'packetsReceived', object.packetsReceived);
} else if (object.packetsSent && identity === 'myself') {
totalPackets = totalPackets + Math.floor(object.packetsSent);
if (mediaType === 'audio') {
audioPackets = audioPackets + Math.floor(object.packetsSent);
} else if (mediaType === 'video') {
videoPackets = videoPackets + Math.floor(object.packetsSent);
}
//console.log(identity, mediaType, 'packetsSent', object.packetsSent);
} else if (object.googCurrentDelayMs && identity !== 'myself') {
delay = object.googCurrentDelayMs;
//console.log('mediaType', mediaType, 'identity', identity, 'delay', delay);
this.latency.set(p.id, Math.ceil(delay));
//console.log(object);
}
if (identity === 'myself') {
//console.log(object);
}
});
if (videoPackets > 0) {
videoPacketLoss = Math.floor(videoPacketsLost / videoPackets * 100);
+ } else {
+ videoPacketLoss = 100;
}
if (audioPackets > 0) {
audioPacketLoss = Math.floor(audioPacketsLost / audioPackets * 100);
+ } else {
+ audioPacketLoss = 100;
}
if (totalPackets > 0) {
totalPacketLoss = Math.floor(totalPacketsLost / totalPackets * 100);
+ } else {
+ totalPacketLoss = 100;
}
this.audioPacketLoss.set(p.id, audioPacketLoss);
this.videoPacketLoss.set(p.id, videoPacketLoss);
this.packetLoss.set(p.id, totalPacketLoss);
}});
- console.log(identity, 'audio loss', audioPacketLoss, '%, video loss', videoPacketLoss, '%, total loss', totalPacketLoss, '%');
+ //console.log(identity, p.id, 'audio loss', audioPacketLoss, '%, video loss', videoPacketLoss, '%, total loss', totalPacketLoss, '%');
const bandwidthDownload = totalVideoBandwidth + totalAudioBandwidth;
this.bandwidthDownload = Math.ceil(bandwidthDownload / 1000 * 100) / 100;
this.bandwidthUpload = Math.ceil(bandwidthUpload / 1000 * 100) / 100;
this.videoBandwidth.set('total', totalVideoBandwidth);
this.audioBandwidth.set('total', totalAudioBandwidth);
//console.log('audio bandwidth', totalAudioBandwidth);
//console.log('video bandwidth', totalVideoBandwidth);
//console.log('total bandwidth', this.bandwidthDownload);
//console.log('this.latency', this.latency);
});
});
};
onParticipantJoined(p) {
//console.log(p.identity.uri, 'joined the conference');
if (p.identity._uri.search('guest.') === -1 && p.identity._uri !== this.props.call.localIdentity._uri) {
// used for history item
this.props.saveParticipant(this.props.call.id, this.props.remoteUri.split('@')[0], p.identity._uri);
}
this.lookupContact(p.identity._uri, p.identity._displayName);
if (this.invitedParticipants.has(p.identity._uri)) {
this.invitedParticipants.delete(p.identity._uri);
}
// this.refs.audioPlayerParticipantJoined.play();
p.on('stateChanged', this.onParticipantStateChanged);
p.attach();
this.setState({
participants: this.state.participants.concat([p])
});
// this.changeResolution();
if (this.state.participants.length > 1) {
this.armOverlayTimer();
} else {
this.setState({callOverlayVisible: true});
}
}
onParticipantLeft(p) {
//console.log(p.identity.uri, 'left the conference');
const participants = this.state.participants.slice();
this.audioBandwidth.delete(p.id);
this.videoBandwidth.delete(p.id);
this.latency.delete(p.id);
this.audioBytesReceived.delete(p.id);
this.videoBytesReceived.delete(p.id);
this.audioPacketLoss.delete(p.id);
this.videoPacketLoss.delete(p.id);
this.packetLoss.delete(p.id);
this.mediaLost.delete(p.id);
const idx = participants.indexOf(p);
if (idx !== -1) {
participants.splice(idx, 1);
this.setState({
participants: participants
});
}
p.detach(true);
// this.changeResolution();
if (this.state.participants.length > 1) {
this.armOverlayTimer();
} else {
this.setState({callOverlayVisible: true});
}
}
onParticipantStateChanged(oldState, newState) {
if (newState === 'established' || newState === null) {
this.maybeSwitchLargeVideo();
}
}
onConfigureRoom(config) {
const newState = {};
newState.activeSpeakers = config.activeParticipants;
this.setState(newState);
if (config.activeParticipants.length === 0) {
this.logEvent.info('set speakers to', ['Nobody'], config.originator);
} else {
const speakers = config.activeParticipants.map((p) => {return p.identity.displayName || p.identity.uri});
this.logEvent.info('set speakers to', speakers, config.originator);
}
this.maybeSwitchLargeVideo();
}
onFileSharing(files) {
let stateFiles = this.state.sharedFiles;
stateFiles = stateFiles.concat(files);
this.setState({sharedFiles: stateFiles});
files.forEach((file)=>{
if (file.session !== this.props.call.id) {
this.props.notificationCenter().postFileShared(file, this.showFiles);
}
})
}
onVideoSelected(item) {
const participants = this.state.participants.slice();
const idx = participants.indexOf(item);
participants.splice(idx, 1);
participants.unshift(item);
if (item.videoPaused) {
item.resumeVideo();
}
this.setState({
participants: participants
});
}
changeResolution() {
let stream = this.props.call.getLocalStreams()[0];
if (this.state.participants.length < 2) {
this.props.call.scaleLocalTrack(stream, 1.5);
} else if (this.state.participants.length < 5) {
this.props.call.scaleLocalTrack(stream, 2);
} else {
this.props.call.scaleLocalTrack(stream, 1);
}
}
selectVideo(item) {
DEBUG('Switching video to: %o', item);
if (item.stream) {
this.setState({selfDisplayedLarge: true, largeVideoStream: item.stream});
}
}
maybeSwitchLargeVideo() {
// Switch the large video to another source, maybe.
if (this.state.participants.length === 0 && !this.state.selfDisplayedLarge) {
// none of the participants are eligible, show ourselves
const item = {
stream: this.props.call.getLocalStreams()[0],
identity: this.props.call.localIdentity
};
this.selectVideo(item);
} else if (this.state.selfDisplayedLarge) {
this.setState({selfDisplayedLarge: false});
}
}
handleClipboardButton() {
utils.copyToClipboard(this.callUrl);
this.props.notificationCenter().postSystemNotification('Join me, maybe?', {body: 'Link copied to the clipboard'});
this.setState({shareOverlayVisible: false});
}
handleEmailButton(event) {
// if (navigator.userAgent.indexOf('Chrome') > 0) {
// let emailWindow = window.open(this.emailLink, '_blank');
// setTimeout(() => {
// emailWindow.close();
// }, 500);
// } else {
// window.open(this.emailLink, '_self');
// }
this.setState({shareOverlayVisible: false});
}
handleShareOverlayEntered() {
this.setState({shareOverlayVisible: true});
}
handleShareOverlayExited() {
this.setState({shareOverlayVisible: false});
}
handleActiveSpeakerSelected(participant, secondVideo=false) { // eslint-disable-line space-infix-ops
let newActiveSpeakers = this.state.activeSpeakers.slice();
if (secondVideo) {
if (participant.id !== 'none') {
if (newActiveSpeakers.length >= 1) {
newActiveSpeakers[1] = participant;
} else {
newActiveSpeakers[0] = participant;
}
} else {
newActiveSpeakers.splice(1,1);
}
} else {
if (participant.id !== 'none') {
newActiveSpeakers[0] = participant;
} else {
newActiveSpeakers.shift();
}
}
this.props.call.configureRoom(newActiveSpeakers.map((element) => element.publisherId), (error) => {
if (error) {
// This causes a state update, hence the drawer lists update
this.logEvent.error('set speakers failed', [], this.localIdentity);
}
});
}
handleDrop(files) {
DEBUG('Dropped file %o', files);
this.uploadFiles(files);
};
handleFiles(e) {
DEBUG('Selected files %o', e.target.files);
this.uploadFiles(e.target.files);
event.target.value = '';
}
toggleSpeakerSelection() {
this.setState({showSpeakerSelection: !this.state.showSpeakerSelection});
}
startSpeakerSelection(number) {
this.selectSpeaker = number;
this.toggleSpeakerSelection();
}
uploadFiles(files) {
for (var key in files) {
// is the item a File?
if (files.hasOwnProperty(key) && files[key] instanceof File) {
let uploadRequest;
let complete = false;
const filename = files[key].name
let progressNotification = this.props.notificationCenter().postFileUploadProgress(
filename,
(notification) => {
if (!complete) {
uploadRequest.abort();
this.uploads.splice(this.uploads.indexOf(uploadRequest), 1);
}
}
);
uploadRequest = superagent
.post(`${config.fileSharingUrl}/${this.props.remoteUri}/${this.props.call.id}/${filename}`)
.send(files[key])
.on('progress', (e) => {
this.props.notificationCenter().editFileUploadNotification(e.percent, progressNotification);
})
.end((err, response) => {
complete = true;
this.props.notificationCenter().removeFileUploadNotification(progressNotification);
if (err) {
this.props.notificationCenter().postFileUploadFailed(filename);
}
this.uploads.splice(this.uploads.indexOf(uploadRequest), 1);
});
this.uploads.push([uploadRequest, progressNotification]);
}
}
}
downloadFile(filename) {
// const a = document.createElement('a');
// a.href = `${config.fileSharingUrl}/${this.props.remoteUri}/${this.props.call.id}/${filename}`;
// a.target = '_blank';
// a.download = filename;
// const clickEvent = document.createEvent('MouseEvent');
// clickEvent.initMouseEvent('click', true, true, window, 0,
// clickEvent.screenX, clickEvent.screenY, clickEvent.clientX, clickEvent.clientY,
// clickEvent.ctrlKey, clickEvent.altKey, clickEvent.shiftKey, clickEvent.metaKey,
// 0, null);
// a.dispatchEvent(clickEvent);
}
preventOverlay(event) {
// Stop the overlay when we are the thumbnail bar
event.stopPropagation();
}
muteAudio(event) {
event.preventDefault();
this.props.toggleMute(this.props.call.id, !this.state.audioMuted);
}
toggleCamera(event) {
event.preventDefault();
const localStream = this.props.call.getLocalStreams()[0];
if (localStream.getVideoTracks().length > 0) {
const track = localStream.getVideoTracks()[0];
track._switchCamera();
}
}
muteVideo(event) {
event.preventDefault();
if (this.state.videoMuted) {
this._resumeVideo();
this.setState({videoMutedbyUser: false});
} else {
this.setState({videoMutedbyUser: true});
this._muteVideo();
}
}
_muteVideo() {
const localStream = this.props.call.getLocalStreams()[0];
if (localStream && localStream.getVideoTracks().length > 0) {
const track = localStream.getVideoTracks()[0];
if (!this.state.videoMuted) {
console.log('Mute camera');
track.enabled = false;
this.setState({videoMuted: true});
}
}
}
_resumeVideo() {
const localStream = this.props.call.getLocalStreams()[0];
if (localStream && localStream.getVideoTracks().length > 0) {
const track = localStream.getVideoTracks()[0];
if (this.state.videoMuted) {
console.log('Resume camera');
track.enabled = true;
this.setState({videoMuted: false});
}
}
}
hangup(event) {
event.preventDefault();
for (let participant of this.state.participants) {
participant.detach();
}
this.props.hangup('user_hangup_conference');
}
armOverlayTimer() {
if (this.props.audioOnly) {
return;
}
clearTimeout(this.overlayTimer);
this.overlayTimer = setTimeout(() => {
this.setState({callOverlayVisible: false});
}, 4000);
}
showOverlay() {
if (this.props.audioOnly) {
return;
}
// if (!this.state.shareOverlayVisible && !this.state.showDrawer && !this.state.showFiles) {
// if (!this.state.callOverlayVisible) {
this.setState({callOverlayVisible: !this.state.callOverlayVisible});
// }
// this.armOverlayTimer();
// }
}
toggleInviteModal() {
this.setState({showInviteModal: !this.state.showInviteModal});
}
toggleDrawer() {
this.setState({callOverlayVisible: true, showDrawer: !this.state.showDrawer, showFiles: false, showSpeakerSelection: false});
clearTimeout(this.overlayTimer);
}
toggleFiles() {
this.setState({callOverlayVisible: true, showFiles: !this.state.showFiles, showDrawer: false});
clearTimeout(this.overlayTimer);
}
showFiles() {
this.setState({callOverlayVisible: true, showFiles: true, showDrawer: false});
clearTimeout(this.overlayTimer);
}
inviteParticipants(uris) {
this.props.call.inviteParticipants(uris);
uris.forEach((uri) => {
uri = uri.replace(/ /g, '');
if (this.props.call.localIdentity._uri === uri) {
return;
}
this.invitedParticipants.set(uri, {timestamp: Date.now(), status: 'Invited'})
this.props.saveParticipant(this.props.call.id, this.props.remoteUri.split('@')[0], uri);
this.lookupContact(uri);
});
this.forceUpdate()
}
render() {
+ //console.log('Conference box this.state.reconnectingCall', this.state.reconnectingCall);
+ let participantsCount = this.state.participants.length + 1;
+
if (this.props.call === null) {
return ();
}
let watermark;
const largeVideoClasses = classNames({
'animated' : true,
'fadeIn' : true,
'large' : true,
'mirror' : !this.props.call.sharingScreen && !this.props.generatedVideoTrack,
'fit' : this.props.call.sharingScreen
});
let matrixClasses = classNames({
'matrix' : true
});
const containerClasses = classNames({
'video-container': true,
'conference': true,
'drawer-visible': this.state.showDrawer || this.state.showFiles
});
const remoteUri = this.props.remoteUri.split('@')[0];
// const shareOverlay = (
//
//
//
//
// Invite other online users of this service, share this link with others or email, so they can easily join this conference.
//
//
//
//
//
//
//
//
//
//
//
// );
const buttons = {};
// const commonButtonTopClasses = classNames({
// 'btn' : true,
// 'btn-link' : true
// });
// const fullScreenButtonIcons = classNames({
// 'fa' : true,
// 'fa-2x' : true,
// 'fa-expand' : !this.isFullScreen(),
// 'fa-compress' : this.isFullScreen()
// });
const topButtons = [];
// if (!this.state.showFiles) {
// if (this.state.sharedFiles.length !== 0) {
// topButtons.push(
//
//
//
// );
// }
// }
if (!this.state.showDrawer) {
topButtons.push();
}
buttons.top = {right: topButtons};
const muteButtonIcons = this.state.audioMuted ? 'microphone-off' : 'microphone';
const muteVideoButtonIcons = this.state.videoMuted ? 'video-off' : 'video';
const buttonClass = (Platform.OS === 'ios') ? styles.iosButton : styles.androidButton;
const bottomButtons = [];
if (!this.state.reconnectingCall) {
bottomButtons.push(
);
}
if (this.haveVideo) {
bottomButtons.push(
);
}
bottomButtons.push(
);
if (this.haveVideo) {
bottomButtons.push(
);
}
if (!this.state.reconnectingCall) {
bottomButtons.push(
)
// bottomButtons.push(
//
//
//
// );
}
bottomButtons.push(
);
buttons.bottom = bottomButtons;
const audioParticipants = [];
let _contact;
let _identity;
let participants_uris = [];
if (this.props.audioOnly) {
_contact = this.foundContacts.get(this.props.call.localIdentity._uri);
_identity = {uri: this.props.call.localIdentity._uri,
displayName: _contact.displayName,
photo: _contact.photo
};
participants_uris.push(this.props.call.localIdentity._uri);
audioParticipants.push(
);
this.state.participants.forEach((p) => {
_contact = this.foundContacts.get(p.identity._uri);
_identity = {uri: p.identity._uri.indexOf('@guest') > -1 ? 'From the web': p.identity._uri,
displayName: (_contact && _contact.displayName != p.identity._displayName) ? _contact.displayName : p.identity._displayName,
photo: _contact ? _contact.photo: null
};
participants_uris.push(p.identity._uri);
let status;
if (this.mediaLost.has(p.id) && this.mediaLost.get(p.id)) {
status = 'Muted';
} else if (this.packetLoss.has(p.id) && this.packetLoss.get(p.id) > 3) {
- status = this.packetLoss.get(p.id) + '% loss';
- } else if (this.latency.has(p.id) && this.latency.get(p.id) > 200) {
+ if (this.packetLoss.get(p.id) === 100) {
+ status = 'No media';
+ participantsCount = participantsCount - 1;
+ return;
+ } else {
+ status = this.packetLoss.get(p.id) + '% loss';
+ }
+ } else if (this.latency.has(p.id) && this.latency.get(p.id) > 150) {
status = this.latency.get(p.id) + ' ms delay';
- //} else if (this.audioBandwidth.has(p.id)) {
- // status = this.audioBandwidth.get(p.id) + ' kbit/s';
}
audioParticipants.push(
);
});
const invitedParties = Array.from(this.invitedParticipants.keys());
let p;
invitedParties.forEach((_uri) => {
if (participants_uris.indexOf(_uri) > 0) {
return;
}
p = this.invitedParticipants.get(_uri);
_contact = this.foundContacts.get(_uri);
_identity = {uri: _uri,
displayName: (_contact && _contact.displayName ) ? _contact.displayName : _uri,
photo: _contact ? _contact.photo: null
};
audioParticipants.push(
);
});
const alreadyInvitedParticipants = this.invitedParticipants ? Array.from(this.invitedParticipants.keys()) : [];
return (
{audioParticipants}
{return p.identity.uri})}
close={this.toggleInviteModal}
room={this.props.remoteUri.split('@')[0]}
defaultDomain = {this.props.defaultDomain}
accountId = {this.props.call.localIdentity._uri}
notificationCenter = {this.props.notificationCenter}
/>
{drawerParticipants}
);
}
const participants = [];
const drawerParticipants = [];
if (this.state.participants.length > 0) {
if (this.state.activeSpeakers.findIndex((element) => {return element.id === this.props.call.id}) === -1) {
participants.push(
);
}
}
drawerParticipants.push(
);
let videos = [];
let status = '';
if (this.state.participants.length === 0) {
videos.push(
);
} else {
const activeSpeakers = this.state.activeSpeakers;
const activeSpeakersCount = activeSpeakers.length;
if (activeSpeakersCount > 0) {
activeSpeakers.forEach((p) => {
status = '';
if (this.mediaLost.has(p.id) && this.mediaLost.get(p.id)) {
status = 'Muted';
} else if (this.packetLoss.has(p.id) && this.packetLoss.get(p.id) > 3) {
- status = this.packetLoss.get(p.id) + '% loss';
+ if (this.packetLoss.get(p.id) === 100) {
+ status = 'No media';
+ return;
+ } else {
+ status = this.packetLoss.get(p.id) + '% loss';
+ }
+
} else if (this.latency.has(p.id) && this.latency.get(p.id) > 100) {
status = this.latency.get(p.id) + ' ms delay';
}
if (this.mediaLost.has(p.id) && this.mediaLost.get(p.id)) {
status = 'Muted';
- } else if (this.packetLoss.has(p.id) && this.packetLoss.get(p.id) > 2) {
- status = this.packetLoss.get(p.id) + '% loss';
- //} else if (this.audioBandwidth.has(p.id)) {
- // status = this.audioBandwidth.get(p.id) + ' kbit/s';
+ } else if (this.packetLoss.has(p.id) && this.packetLoss.get(p.id) > 3) {
+ if (this.packetLoss.get(p.id) === 100) {
+ status = 'No media';
+ return;
+ } else {
+ status = this.packetLoss.get(p.id) + '% loss';
+ }
}
videos.push(
);
});
this.state.participants.forEach((p) => {
status = '';
if (this.mediaLost.has(p.id) && this.mediaLost.get(p.id)) {
status = 'Muted';
} else if (this.packetLoss.has(p.id) && this.packetLoss.get(p.id) > 3) {
- status = this.packetLoss.get(p.id) + '% loss';
+ if (this.packetLoss.get(p.id) === 100) {
+ status = 'No media';
+ participantsCount = participantsCount - 1;
+ return;
+ } else {
+ status = this.packetLoss.get(p.id) + '% loss';
+ }
} else if (this.latency.has(p.id) && this.latency.get(p.id) > 100) {
status = this.latency.get(p.id) + ' ms delay';
}
if (this.state.activeSpeakers.indexOf(p) === -1) {
participants.push(
{}}
pauseVideo={true}
display={false}
status={status}
/>
);
}
drawerParticipants.push(
);
});
} else {
this.state.participants.forEach((p, idx) => {
status = '';
- console.log(this.packetLoss);
if (this.mediaLost.has(p.id) && this.mediaLost.get(p.id)) {
status = 'Muted';
} else if (this.packetLoss.has(p.id) && this.packetLoss.get(p.id) > 3) {
- status = this.packetLoss.get(p.id) + '% loss';
+ if (this.packetLoss.get(p.id) === 100) {
+ status = 'No media';
+ participantsCount = participantsCount - 1;
+ return;
+ } else {
+ status = this.packetLoss.get(p.id) + '% loss';
+ }
} else if (this.latency.has(p.id) && this.latency.get(p.id) > 100) {
status = this.latency.get(p.id) + ' ms';
}
videos.push(
= 4) || (idx >= 2 && this.props.isTablet === false)}
isLandscape={this.props.isLandscape}
isTablet={this.props.isTablet}
useTwoRows={this.state.participants.length > 2}
status={status}
/>
);
if (idx >= 4 || idx >= 2 && this.props.isTablet === false) {
participants.push(
);
}
drawerParticipants.push(
);
});
}
}
// let filesDrawerContent = (
//
// );
const currentParticipants = this.state.participants.map((p) => {return p.identity.uri})
const alreadyInvitedParticipants = this.invitedParticipants ? Array.from(this.invitedParticipants.keys()) : [];
return (
{videos}
{participants}
{drawerParticipants}
);
}
}
ConferenceBox.propTypes = {
notificationCenter : PropTypes.func.isRequired,
call : PropTypes.object,
connection : PropTypes.object,
hangup : PropTypes.func,
saveParticipant : PropTypes.func,
previousParticipants: PropTypes.array,
remoteUri : PropTypes.string,
generatedVideoTrack : PropTypes.bool,
toggleMute : PropTypes.func,
toggleSpeakerPhone : PropTypes.func,
speakerPhoneEnabled : PropTypes.bool,
isLandscape : PropTypes.bool,
isTablet : PropTypes.bool,
muted : PropTypes.bool,
defaultDomain : PropTypes.string,
inFocus : PropTypes.bool,
reconnectingCall : PropTypes.bool,
audioOnly : PropTypes.bool,
contacts : PropTypes.array,
initialParticipants : PropTypes.array,
terminated : PropTypes.bool
};
export default ConferenceBox;
diff --git a/app/components/ConferenceHeader.js b/app/components/ConferenceHeader.js
index d5b90d1..46fae78 100644
--- a/app/components/ConferenceHeader.js
+++ b/app/components/ConferenceHeader.js
@@ -1,110 +1,194 @@
-import React, { useState, useEffect, useRef, Fragment } from 'react';
+import React, { useState, useEffect, useRef, Fragment, Component } from 'react';
import { View } from 'react-native';
+import autoBind from 'auto-bind';
import PropTypes from 'prop-types';
import moment from 'moment';
+
import momentFormat from 'moment-duration-format';
import { Text, Appbar } from 'react-native-paper';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import styles from '../assets/styles/blink/_ConferenceHeader.scss';
-const useInterval = (callback, delay) => {
- const savedCallback = useRef();
- // Remember the latest callback.
- useEffect(() => {
- savedCallback.current = callback;
- }, [callback]);
+class ConferenceHeader extends React.Component {
+ constructor(props) {
+ super(props);
+ autoBind(this);
+
+ this.state = {
+ call: this.props.call,
+ callState: this.props.call ? this.props.call.state : null,
+ participants: this.props.participants,
+ reconnectingCall: this.props.reconnectingCall,
+ info: this.props.info
+ }
+
+ this.duration = null;
+ this.timer = null;
+ this._isMounted = false;
+ }
+
+ componentDidMount() {
+ this._isMounted = true;
- // Set up the interval.
- useEffect(() => {
- function tick() {
- savedCallback.current();
+ if (!this.state.call) {
+ return;
}
- if (delay !== null) {
- let id = setInterval(tick, delay);
- return () => clearInterval(id);
+
+ if (this.state.call.state === 'established') {
+ this.startTimer();
}
- }, [delay]);
-}
+ this.state.call.on('stateChanged', this.callStateChanged);
+ this.setState({callState: this.state.call.state});
+ }
+ startTimer() {
+ if (this.timer !== null) {
+ // already armed
+ return;
+ }
-const ConferenceHeader = (props) => {
- let [seconds, setSeconds] = useState(0);
+ // TODO: consider using window.requestAnimationFrame
- useInterval(() => {
- setSeconds(seconds + 1);
- }, 1000);
+ const startTime = new Date();
+ this.timer = setInterval(() => {
+ const duration = moment.duration(new Date() - startTime);
- let duration = moment.duration(seconds, 'seconds');
+ if (this.duration > 3600) {
+ this.duration = duration.format('hh:mm:ss', {trim: false});
+ } else {
+ this.duration = duration.format('mm:ss', {trim: false});
+ }
- if (duration > 3600) {
- duration = duration.format('hh:mm:ss', {trim: false});
- } else {
- duration = duration.format('mm:ss', {trim: false});
+ if (this.props.show) {
+ this.forceUpdate();
+ }
+ }, 1000);
}
- let videoHeader;
- let callButtons;
-
- if (props.show) {
- const participantCount = props.participants.length + 1;
- // const callDetail = (
- //
- // {duration} - {participantCount} participant{participantCount > 1 ? 's' : ''}
- //
- // );
-
- const room = props.remoteUri.split('@')[0];
- let callDetail;
-
- if (props.reconnectingCall) {
- callDetail = 'Reconnecting call...';
- } else if (props.terminated) {
- callDetail = 'Call ended';
- } else {
- callDetail = `${duration} - ${participantCount} participant${participantCount > 1 ? 's' : ''}`;
+ componentWillUnmount() {
+ this._isMounted = false;
+
+ if (this.state.call) {
+ this.state.call.removeListener('stateChanged', this.callStateChanged);
}
- if (props.info) {
- callDetail = callDetail + ' - ' + props.info;
+ clearTimeout(this.timer);
+ }
+
+ //getDerivedStateFromProps(nextProps, state) {
+ UNSAFE_componentWillReceiveProps(nextProps) {
+ if (!this._isMounted) {
+ return;
}
- videoHeader = (
-
-
- {props.audioOnly ? null : props.buttons.top.right}
-
- );
+ if (nextProps.reconnectingCall != this.state.reconnectingCall) {
+ this.setState({reconnectingCall: nextProps.reconnectingCall});
+ }
+
+ if (nextProps.call !== null && nextProps.call !== this.state.call) {
+ nextProps.call.on('stateChanged', this.callStateChanged);
+
+ if (this.state.call !== null) {
+ this.state.call.removeListener('stateChanged', this.callStateChanged);
+ }
+
+ this.setState({call: nextProps.call});
+ }
- callButtons = (
-
- {props.buttons.bottom}
+ this.setState({info: nextProps.info,
+ participants: nextProps.participants});
+ }
+
+ callStateChanged(oldState, newState, data) {
+ console.log('callStateChanged', newState);
+ if (newState === 'established' && this._isMounted && !this.props.terminated) {
+ this.startTimer();
+ }
+
+ if (newState === 'terminated') {
+ if (this.state.call) {
+ this.state.call.removeListener('stateChanged', this.callStateChanged);
+ }
+
+ clearTimeout(this.timer);
+ this.duration = null;
+ this.timer = null;
+ }
+
+ if (!this._isMounted) {
+ return;
+ }
+
+ this.setState({callState: newState});
+ }
+
+ render() {
+
+ let videoHeader;
+ let callButtons;
+
+ if (this.props.terminated) {
+ clearTimeout(this.timer);
+ this.duration = null;
+ this.timer = null;
+ }
+
+ if (this.props.show) {
+ const room = this.props.remoteUri.split('@')[0];
+ let callDetail;
+
+ if (this.state.reconnectingCall) {
+ callDetail = 'Reconnecting call...';
+ } else if (this.state.terminated) {
+ callDetail = 'Conference ended';
+ } else if (this.duration) {
+ callDetail = (this.props.isTablet ? 'Duration: ' : '') + this.duration + ' - ' + this.state.participants + ' participant' + (this.state.participants > 1 ? 's' : '');
+ }
+
+ if (this.state.info) {
+ callDetail = callDetail + ' - ' + this.state.info;
+ }
+
+ videoHeader = (
+
+
+ {this.props.audioOnly ? null : this.props.buttons.top.right}
+
+ );
+
+ callButtons = (
+
+ {this.props.buttons.bottom}
+
+ );
+ }
+
+ return (
+
+ {videoHeader}
+ {callButtons}
);
}
-
- return (
-
- {videoHeader}
- {callButtons}
-
- );
}
ConferenceHeader.propTypes = {
show: PropTypes.bool.isRequired,
remoteUri: PropTypes.string.isRequired,
- participants: PropTypes.array.isRequired,
+ call: PropTypes.object,
+ isTablet: PropTypes.bool,
+ participants: PropTypes.integer,
buttons: PropTypes.object.isRequired,
reconnectingCall: PropTypes.bool,
audioOnly: PropTypes.bool,
terminated: PropTypes.bool,
info: PropTypes.string
};
export default ConferenceHeader;
diff --git a/app/components/LocalMedia.js b/app/components/LocalMedia.js
index cf2ba76..cc4fbed 100644
--- a/app/components/LocalMedia.js
+++ b/app/components/LocalMedia.js
@@ -1,139 +1,144 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import autoBind from 'auto-bind';
import { View, Dimensions } from 'react-native';
import { RTCView } from 'react-native-webrtc';
import { IconButton, Button, Text} from 'react-native-paper';
import CallOverlay from './CallOverlay';
import styles from '../assets/styles/blink/_LocalMedia.scss';
class LocalMedia extends Component {
constructor(props) {
super(props);
autoBind(this);
this.localVideo = React.createRef();
this.state = {
localMedia: this.props.localMedia,
historyEntry: this.props.historyEntry,
- participants: this.props.participants
+ participants: this.props.participants,
+ reconnectingCall: this.props.reconnectingCall
};
}
componentDidMount() {
this.props.mediaPlaying();
}
//getDerivedStateFromProps(nextProps, state)
UNSAFE_componentWillReceiveProps(nextProps) {
if (nextProps.localMedia && nextProps.localMedia !== this.state.localMedia) {
this.props.mediaPlaying();
}
- this.setState({historyEntry: nextProps.historyEntry, participants: nextProps.participants});
+ this.setState({historyEntry: nextProps.historyEntry,
+ participants: nextProps.participants,
+ reconnectingCall: nextProps.reconnectingCall});
}
saveConference(event) {
event.preventDefault();
this.props.saveConference();
}
showSaveDialog() {
if (!this.props.showSaveDialog) {
return false;
}
return this.props.showSaveDialog();
}
hangupCall(event) {
event.preventDefault();
this.props.hangupCall('user_hangup_conference_confirmed');
}
render() {
let {height, width} = Dimensions.get('window');
let videoStyle = {
height,
width
};
const streamUrl = this.props.localMedia ? this.props.localMedia.toURL() : null;
const buttonSize = this.props.isTablet ? 40 : 34;
const buttonContainerClass = this.props.isTablet ? styles.tabletButtonContainer : styles.buttonContainer;
return (
{this.showSaveDialog() ?
Save conference maybe?
Would you like to save participants {this.state.participants.toString().replace(/,/g, ', ')} for having another conference later?
You can find later it in your Favorites.
:
}
);
}
}
LocalMedia.propTypes = {
call : PropTypes.object,
remoteUri : PropTypes.string,
remoteDisplayName : PropTypes.string,
localMedia : PropTypes.object.isRequired,
mediaPlaying : PropTypes.func.isRequired,
hangupCall : PropTypes.func,
showSaveDialog : PropTypes.func,
saveConference : PropTypes.func,
reconnectingCall : PropTypes.bool,
connection : PropTypes.object,
participants : PropTypes.array,
terminated : PropTypes.bool
};
export default LocalMedia;