removed incomplete video conferences

This commit is contained in:
or-else
2025-02-14 21:40:44 +03:00
parent f91d8a979d
commit 6c1c8cf336
57 changed files with 138 additions and 1536 deletions
-2
View File
@@ -7,5 +7,3 @@ export const CALL_STATE_IN_PROGRESS = 3;
// Video call message headers representing call state.
export const CALL_HEAD_STARTED = 'started';
// Gap between video conference carousel items.
export const VC_CAROUSEL_ITEM_GAP = 16;
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -1653,7 +1653,7 @@
"defaultMessage": "Conference call",
"description": "Conference call label",
"missing": false,
"obsolete": false
"obsolete": true
},
"password_reset_success": {
"translation": "Passwort erfolgreich zurückgesetzt",
@@ -1667,7 +1667,7 @@
"defaultMessage": "+ Join",
"description": "Call to action to join a conference call",
"missing": false,
"obsolete": false
"obsolete": true
},
"scan_qr_code": {
"translation": "QR-Code scannen",
+2 -2
View File
@@ -1653,7 +1653,7 @@
"defaultMessage": "Conference call",
"description": "Conference call label",
"missing": false,
"obsolete": false
"obsolete": true
},
"password_reset_success": {
"translation": "Password reset successfully",
@@ -1667,7 +1667,7 @@
"defaultMessage": "+ Join",
"description": "Call to action to join a conference call",
"missing": false,
"obsolete": false
"obsolete": true
},
"scan_qr_code": {
"translation": "Scan QR Code",
+2 -2
View File
@@ -1653,7 +1653,7 @@
"defaultMessage": "Conference call",
"description": "Conference call label",
"missing": false,
"obsolete": false
"obsolete": true
},
"password_reset_success": {
"translation": "Restablecimiento de contraseña con éxito",
@@ -1667,7 +1667,7 @@
"defaultMessage": "+ Join",
"description": "Call to action to join a conference call",
"missing": false,
"obsolete": false
"obsolete": true
},
"scan_qr_code": {
"translation": "Escanear código QR",
-8
View File
@@ -143,10 +143,6 @@
"defaultMessage": "missed",
"description": "Label for missed incoming call"
},
"calls_conference": {
"defaultMessage": "Conference call",
"description": "Conference call label"
},
"calls_incoming": {
"defaultMessage": "Incoming call",
"description": "Incoming call label"
@@ -374,10 +370,6 @@
"defaultMessage": "Invalid security token",
"description": "Error message when resetting password."
},
"join_conference": {
"defaultMessage": "+ Join",
"description": "Call to action to join a conference call"
},
"label_client": {
"defaultMessage": "Client:",
"description": "Label for a client version"
+2 -2
View File
@@ -1653,7 +1653,7 @@
"defaultMessage": "Conference call",
"description": "Conference call label",
"missing": false,
"obsolete": false
"obsolete": true
},
"password_reset_success": {
"translation": "Mot de passe réinitialisé avec succès",
@@ -1667,7 +1667,7 @@
"defaultMessage": "+ Join",
"description": "Call to action to join a conference call",
"missing": false,
"obsolete": false
"obsolete": true
},
"scan_qr_code": {
"translation": "Scanner le code QR",
+2 -2
View File
@@ -1653,7 +1653,7 @@
"defaultMessage": "Conference call",
"description": "Conference call label",
"missing": false,
"obsolete": false
"obsolete": true
},
"password_reset_success": {
"translation": "비밀번호 재설정 성공",
@@ -1667,7 +1667,7 @@
"defaultMessage": "+ Join",
"description": "Call to action to join a conference call",
"missing": false,
"obsolete": false
"obsolete": true
},
"scan_qr_code": {
"translation": "QR 코드 스캔",
+2 -2
View File
@@ -1653,7 +1653,7 @@
"defaultMessage": "Conference call",
"description": "Conference call label",
"missing": false,
"obsolete": false
"obsolete": true
},
"password_reset_success": {
"translation": "Resetarea parolei cu succes",
@@ -1667,7 +1667,7 @@
"defaultMessage": "+ Join",
"description": "Call to action to join a conference call",
"missing": false,
"obsolete": false
"obsolete": true
},
"scan_qr_code": {
"translation": "Scanați codul QR",
+2 -2
View File
@@ -1653,7 +1653,7 @@
"defaultMessage": "Conference call",
"description": "Conference call label",
"missing": false,
"obsolete": false
"obsolete": true
},
"password_reset_success": {
"translation": "Пароль успешно изменен",
@@ -1667,7 +1667,7 @@
"defaultMessage": "+ Join",
"description": "Call to action to join a conference call",
"missing": false,
"obsolete": false
"obsolete": true
},
"scan_qr_code": {
"translation": "Отсканируйте QR код",
+2 -2
View File
@@ -1653,14 +1653,14 @@
"defaultMessage": "Conference call",
"description": "Conference call label",
"missing": true,
"obsolete": false
"obsolete": true
},
"join_conference": {
"translation": "",
"defaultMessage": "+ Join",
"description": "Call to action to join a conference call",
"missing": true,
"obsolete": false
"obsolete": true
},
"pin_message": {
"translation": "",
+2 -2
View File
@@ -1653,14 +1653,14 @@
"defaultMessage": "Conference call",
"description": "Conference call label",
"missing": true,
"obsolete": false
"obsolete": true
},
"join_conference": {
"translation": "",
"defaultMessage": "+ Join",
"description": "Call to action to join a conference call",
"missing": true,
"obsolete": false
"obsolete": true
},
"pin_message": {
"translation": "",
+2 -2
View File
@@ -1653,7 +1653,7 @@
"defaultMessage": "Conference call",
"description": "Conference call label",
"missing": false,
"obsolete": false
"obsolete": true
},
"password_reset_success": {
"translation": "密碼重置成功",
@@ -1667,7 +1667,7 @@
"defaultMessage": "+ Join",
"description": "Call to action to join a conference call",
"missing": false,
"obsolete": false
"obsolete": true
},
"scan_qr_code": {
"translation": "掃描二維碼",
+2 -2
View File
@@ -1653,7 +1653,7 @@
"defaultMessage": "Conference call",
"description": "Conference call label",
"missing": false,
"obsolete": false
"obsolete": true
},
"password_reset_success": {
"translation": "密码重置成功",
@@ -1667,7 +1667,7 @@
"defaultMessage": "+ Join",
"description": "Call to action to join a conference call",
"missing": false,
"obsolete": false
"obsolete": true
},
"scan_qr_code": {
"translation": "扫描二维码",
+1 -3
View File
@@ -143,8 +143,6 @@ export function fullFormatter(style, data, values, key, stack) {
attr.callState = data.state;
attr.incoming = data.incoming;
attr.duration = data.duration;
attr.vc = data.vc;
attr.onCallJoin = this.onCallJoin;
}
break;
case 'VD':
@@ -210,7 +208,7 @@ function handleImageData(el, data, attr) {
attr.src = null;
}
} else {
// Use custom element instead of <img>.
// Use custom element instead of <img> or <video>.
el = UploadingImage;
}
+16 -38
View File
@@ -19,7 +19,6 @@ import LogoView from './logo-view.jsx';
import MetaMessage from '../widgets/meta-message.jsx';
import PinnedMessages from '../widgets/pinned-messages.jsx';
import SendMessage from '../widgets/send-message.jsx';
import VCPanel from '../widgets/vc-panel.jsx';
import VideoPreview from '../widgets/video-preview.jsx';
import { DEFAULT_P2P_ACCESS_MODE, EDIT_PREVIEW_LENGTH, IMAGE_PREVIEW_DIM, IMMEDIATE_P2P_SUBSCRIPTION,
@@ -1454,44 +1453,23 @@ class MessagesView extends React.Component {
);
} else if (this.state.rtcPanel) {
// P2P call.
if (Tinode.isP2PTopicName(this.state.rtcPanel)) {
component2 = (
<CallPanel
topic={this.state.topic}
seq={this.props.callSeq}
callState={this.props.callState}
callAudioOnly={this.props.callAudioOnly}
tinode={this.props.tinode}
title={this.state.title}
avatar={this.state.avatar || true}
component2 = (
<CallPanel
topic={this.state.topic}
seq={this.props.callSeq}
callState={this.props.callState}
callAudioOnly={this.props.callAudioOnly}
tinode={this.props.tinode}
title={this.state.title}
avatar={this.state.avatar || true}
onError={this.props.onError}
onHangup={this.handleCallHangup}
onInvite={this.props.onCallInvite}
onSendOffer={this.props.onCallSendOffer}
onIceCandidate={this.props.onCallIceCandidate}
onSendAnswer={this.props.onCallSendAnswer} />
);
} else {
// Video conference or stream.
component2 = (
<VCPanel
topic={this.state.topic}
seq={this.props.callSeq}
callState={this.props.callState}
callAudioOnly={this.props.callAudioOnly}
tinode={this.props.tinode}
title={this.state.title}
avatar={this.state.avatar || true}
myUid={this.props.myUserId}
viewportWidth={this.props.viewportWidth}
onError={this.props.onError}
onHangup={this.handleCallHangup}
onCreateCall={this.props.onCallInvite}
onJoin={this.props.onVCJoin} />
);
}
onError={this.props.onError}
onHangup={this.handleCallHangup}
onInvite={this.props.onCallInvite}
onSendOffer={this.props.onCallSendOffer}
onIceCandidate={this.props.onCallIceCandidate}
onSendAnswer={this.props.onCallSendAnswer} />
);
} else {
const topic = this.props.tinode.getTopic(this.state.topic);
const isChannel = topic.isChannelType();
+3 -22
View File
@@ -181,7 +181,6 @@ class TinodeWeb extends React.Component {
this.handleCallSendOffer = this.handleCallSendOffer.bind(this);
this.handleCallIceCandidate = this.handleCallIceCandidate.bind(this);
this.handleCallSendAnswer = this.handleCallSendAnswer.bind(this);
this.handleVCCallJoin = this.handleVCCallJoin.bind(this);
this.handleCallAccept = this.handleCallAccept.bind(this);
@@ -1626,7 +1625,6 @@ class TinodeWeb extends React.Component {
}
webrtc = writer && !!this.tinode.getServerParam('iceServers');
const mediaServerAvailable = !!this.tinode.getServerParam('vcEndpoint');
return [
subscribed ? {
@@ -1637,9 +1635,8 @@ class TinodeWeb extends React.Component {
title: this.props.intl.formatMessage(messages.menu_item_audio_call),
handler: this.handleStartAudioCall
} : null,
subscribed && webrtc ? {
subscribed && Tinode.isP2PTopicName(topicName) && webrtc ? {
title: this.props.intl.formatMessage(messages.menu_item_video_call),
disabled: Tinode.isGroupTopicName(topicName) && !mediaServerAvailable,
handler: this.handleStartVideoCall
} : null,
subscribed ? 'messages_clear' : null,
@@ -1797,12 +1794,7 @@ class TinodeWeb extends React.Component {
switch (callState) {
case CALL_STATE_OUTGOING_INITATED:
const head = { webrtc: CALL_HEAD_STARTED, aonly: !!audioOnly };
const msg = Drafty.videoCall(audioOnly);
if (Tinode.isGroupTopicName(callTopic)) {
// Video conference.
head.vc = true;
}
return this.handleSendMessage(msg, undefined, undefined, head)
return this.handleSendMessage(Drafty.videoCall(audioOnly), undefined, undefined, head)
.then(ctrl => {
if (ctrl.code < 200 || ctrl.code >= 300 || !ctrl.params || !ctrl.params.seq) {
this.handleCallClose();
@@ -1811,7 +1803,6 @@ class TinodeWeb extends React.Component {
this.setState({callSeq: ctrl.params['seq']});
return ctrl
});
break;
case CALL_STATE_IN_PROGRESS:
const topic = this.tinode.getTopic(callTopic);
if (!topic) {
@@ -1867,15 +1858,6 @@ class TinodeWeb extends React.Component {
topic.videoCall('answer', callSeq, sdp);
}
handleVCCallJoin(callTopic, callSeq) {
const topic = this.tinode.getTopic(callTopic);
if (!topic) {
return;
}
topic.videoCall('vc-join', callSeq);
}
handleCallClose() {
if (this.callTimeoutTimer) {
clearTimeout(this.callTimeoutTimer);
@@ -1896,7 +1878,7 @@ class TinodeWeb extends React.Component {
this.handleTopicSelected(setCallTopic ? topicName : this.state.callTopic);
const upd = {
callState: CALL_STATE_IN_PROGRESS
}
};
if (setCallTopic) {
upd.callTopic = topicName;
upd.callSeq = callSeq;
@@ -2140,7 +2122,6 @@ class TinodeWeb extends React.Component {
onCallSendOffer={this.handleCallSendOffer}
onCallIceCandidate={this.handleCallIceCandidate}
onCallSendAnswer={this.handleCallSendAnswer}
onVCJoin={this.handleVCCallJoin}
errorText={this.state.errorText}
errorLevel={this.state.errorLevel}
+2 -17
View File
@@ -7,21 +7,12 @@ import { secondsToTime } from '../lib/strformat.js';
export default class CallMessage extends React.PureComponent {
render() {
const bigIcon = this.props.vc ?
<div className="composed-material">
<i className="material-icons gray second-small">group</i>
<i className="material-icons gray big outline">call</i>
</div>
:
<div><i className="material-icons big gray">call</i></div>;
const isCallDropped = ['busy', 'declined', 'disconnected', 'missed'].includes(this.props.callState);
const successClass = 'material-icons medium ' + (isCallDropped ? 'red' : 'green');
const callIcon = this.props.incoming ?
(isCallDropped ? 'call_missed' : 'call_received') :
(isCallDropped ? 'call_missed_outgoing' : 'call_made');
const title = this.props.vc ?
<FormattedMessage id="calls_conference" defaultMessage="Conference call" description="Conference call label" /> :
this.props.incoming ?
const title = this.props.incoming ?
<FormattedMessage id="calls_incoming" defaultMessage="Incoming call" description="Incoming call label" /> :
<FormattedMessage id="calls_outgoing" defaultMessage="Outgoing call" description="Outgoing call label" />;
let duration;
@@ -45,17 +36,11 @@ export default class CallMessage extends React.PureComponent {
} else {
duration = <span>{secondsToTime(this.props.duration / 1000)}</span>;
}
const allowJoin = this.props.vc && this.props.callState == 'started';
return <div className="call-message">
<div><i className="material-icons big gray">{bigIcon}</i></div>
<div><i className="material-icons big gray">call</i></div>
<div className="flex-column narrow">
<div>{title}</div>
<div className="duration"><i className={successClass}>{callIcon}</i> {duration}</div>
{allowJoin ?
<a onClick={this.props.onCallJoin}>
<FormattedMessage id="join_conference" defaultMessage="+ Join" description="Call to action to join a conference call" />
</a>
: null}
</div>
</div>;
}
+1 -7
View File
@@ -29,7 +29,6 @@ class BaseChatMessage extends React.PureComponent {
this.handleContextClick = this.handleContextClick.bind(this);
this.handleCancelUpload = this.handleCancelUpload.bind(this);
this.handleQuoteClick = this.handleQuoteClick.bind(this);
this.handleCallJoin = this.handleCallJoin.bind(this);
this.formatterContext = {
formatMessage: props.intl.formatMessage.bind(props.intl),
@@ -39,8 +38,7 @@ class BaseChatMessage extends React.PureComponent {
onImagePreview: this.handleExpandImage,
onVideoPreview: this.handlePlayVideo,
onFormButtonClick: this.handleFormButtonClick,
onQuoteClick: this.handleQuoteClick,
onCallJoin: this.handleCallJoin
onQuoteClick: this.handleQuoteClick
};
}
@@ -151,10 +149,6 @@ class BaseChatMessage extends React.PureComponent {
}
}
handleCallJoin() {
this.props.onAcceptCall(this.props.topic, true, this.props.seq);
}
render() {
const sideClass = this.props.sequence + ' ' + (this.props.response ? 'left' : 'right');
const bubbleClass = (this.props.sequence == 'single' || this.props.sequence == 'last') ? 'bubble tip' : 'bubble';
-97
View File
@@ -1,97 +0,0 @@
// Video conference carousel item.
import React from 'react';
import LetterTile from './letter-tile.jsx';
/**
* Props:
* - cameraPub
* - micPub
* - isSpeaking
* - tinode
* - photo
* - userId
* - name
*/
export default class VCCarouselItem extends React.PureComponent {
constructor(props) {
super(props);
this.handleVideoRefChange = this.handleVideoRefChange.bind(this);
this.handleAudioRefChange = this.handleAudioRefChange.bind(this);
this.videoRef = null;
this.audioRef = null;
}
componentDidUpdate(prevProps) {
if (this.videoRef && !this.videoRef.srcObject && this.props.cameraPub && this.props.cameraPub.videoTrack) {
this.props.cameraPub.videoTrack.attach(this.videoRef);
}
if (this.audioRef && !this.audioRef.srcObject && this.props.micPub && this.props.micPub.audioTrack) {
this.props.micPub.audioTrack.attach(this.audioRef);
}
}
handleVideoRefChange(node) {
if (!node) {
return;
}
this.videoRef = node;
// Attach video track to video element.
const cameraPub = this.props.cameraPub;
const cameraEnabled = cameraPub && cameraPub.isSubscribed && !cameraPub.isMuted;
if (cameraEnabled && cameraPub.videoTrack) {
cameraPub.videoTrack.attach(node);
}
}
handleAudioRefChange(node) {
if (!node) {
return;
}
this.audioRef = node;
// Attach audio track to audio element.
const micPub = this.props.micPub;
const micEnabled = micPub && micPub.isSubscribed && !micPub.isMuted;
if (micEnabled && micPub.audioTrack) {
micPub.audioTrack.attach(node);
}
}
render() {
let classes = 'call-party carousel-item';
if (this.props.isSpeaking) {
classes += ' call-party-speaking';
}
const micPub = this.props.micPub;
const cameraPub = this.props.cameraPub;
return (
<div className={classes}>
<video ref={this.handleVideoRefChange}></video>
<audio ref={this.handleAudioRefChange}></audio>
{micPub && micPub.isMuted ?
<i className="material-icons muted">mic_off</i>
: null
}
{cameraPub && cameraPub.isMuted ?
<div className="avatar-box">
<LetterTile
tinode={this.props.tinode}
avatar={this.props.photo || true}
topic={this.props.userId}
title={this.props.name} />
</div>
: null}
<div className="caller-name inactive">
{this.props.name}
</div>
</div>
);
}
}
-104
View File
@@ -1,104 +0,0 @@
import React from 'react';
import { Track } from 'livekit-client';
import VCCarouselItem from './vc-carousel-item.jsx';
import { VC_CAROUSEL_ITEM_GAP } from '../constants.js';
export default class VCCarousel extends React.Component {
constructor(props) {
super(props);
this.state = {
prevVisible: false,
nextVisible: false
};
this.handleScrollReference = this.handleScrollReference.bind(this);
this.handleScrollEvent = this.handleScrollEvent.bind(this);
this.handleScrollClick = this.handleScrollClick.bind(this);
this.carouselRef = null;
this.contentRef = React.createRef();
}
componentDidUpdate(prevProps) {
if (this.carouselRef && this.props.viewportWidth != prevProps.viewportWidth) {
this.handleScrollEvent({target: this.carouselRef});
}
}
componentWillUnmount() {
if (this.carouselRef) {
this.carouselRef.removeEventListener('scroll', this.handleScrollEvent);
}
}
// Don't use React.createRef as the ref.current is not available in componentDidMount in this component.
handleScrollReference(node) {
if (node) {
this.carouselRef = node;
this.carouselRef.addEventListener('scroll', this.handleScrollEvent);
this.handleScrollEvent({target: this.carouselRef});
}
}
// Get older messages and show/hide [go to latest message] button.
handleScrollEvent(event) {
this.setState({
// Show [prev] / [next] as appropriate.
prevVisible: event.target.scrollLeft > VC_CAROUSEL_ITEM_GAP,
nextVisible: event.target.scrollWidth - event.target.scrollLeft - event.target.offsetWidth > VC_CAROUSEL_ITEM_GAP
});
}
handleScrollClick(next) {
// this.carouselRef.offsetWidth - is the width of the visible window.
const val = this.carouselRef.offsetWidth + VC_CAROUSEL_ITEM_GAP;
this.carouselRef.scrollBy(next ? val : -val, 0);
}
render() {
let peers = [];
if (this.props.participants) {
Object.entries(this.props.participants).forEach(([identity, party], i) => {
const participant = party.participant;
const cameraPub = participant.getTrack(Track.Source.Camera);
const micPub = participant.getTrack(Track.Source.Microphone);
peers.push(<VCCarouselItem
tinode={this.props.tinode}
isSpeaking={participant.isSpeaking}
cameraPub={cameraPub ? cameraPub : null}
micPub={micPub ? micPub : null}
name={party.name}
userId={identity}
photo={party.photo}
key={i}
/>);
});
}
return (
<div id="carousel-wrapper">
<div id="carousel" ref={this.handleScrollReference}>
<div id="carousel-content" ref={this.contentRef}>
{peers}
</div>
</div>
{this.state.prevVisible ?
<button id="carousel-prev" className="action-button" onClick={_ => this.handleScrollClick(false)}>
<i className="material-icons">navigate_before</i>
</button>
:
null}
{this.state.nextVisible ?
<button id="carousel-next" className="action-button" onClick={_ => this.handleScrollClick(true)}>
<i className="material-icons">navigate_next</i>
</button>
:
null}
</div>
);
}
}
-412
View File
@@ -1,412 +0,0 @@
// Video conference or broadcast: local and/or peer viewports and controls.
import React from 'react';
import { FormattedMessage, injectIntl } from 'react-intl';
import {
LocalVideoTrack,
MediaDeviceFailure,
ParticipantEvent,
RemoteParticipant,
Room,
RoomEvent,
Track,
VideoPresets
} from 'livekit-client';
import LetterTile from './letter-tile.jsx';
import VCCarousel from './vc-carousel.jsx';
import { makeImageUrl } from '../lib/blob-helpers.js';
import { MAX_PEER_TITLE_LENGTH } from '../config.js';
import { CALL_STATE_OUTGOING_INITATED } from '../constants.js';
import { clipStr } from '../lib/utils.js'
class VCPanel extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
room: undefined,
participants: {},
activeSpeaker: undefined,
previousOnInfo: undefined,
};
this.isOutgoingCall = props.callState == CALL_STATE_OUTGOING_INITATED;
const topic = this.props.tinode.getTopic(this.props.topic);
this.isBroadcast = topic && topic.chan;
this.localRef = React.createRef();
this.remoteRef = React.createRef();
this.onInfo = this.onInfo.bind(this);
this.start = this.start.bind(this);
this.stop = this.stop.bind(this);
this.reportError = this.reportError.bind(this);
this.handleCloseClick = this.handleCloseClick.bind(this);
this.handleToggleCameraClick = this.handleToggleCameraClick.bind(this);
this.handleToggleMicClick = this.handleToggleMicClick.bind(this);
this.handleRemoteHangup = this.handleRemoteHangup.bind(this);
this.handleTrackSubscribed = this.handleTrackSubscribed.bind(this);
this.handleTrackUnsubscribed = this.handleTrackUnsubscribed.bind(this);
this.handleActiveSpeakerChange = this.handleActiveSpeakerChange.bind(this);
this.handleDisconnect = this.handleDisconnect.bind(this);
this.handleLocalTrackUnpublished = this.handleLocalTrackUnpublished.bind(this);
this.handleLocalTrackPublished = this.handleLocalTrackPublished.bind(this);
this.handleParticipantConnected = this.handleParticipantConnected.bind(this);
this.handleParticipantDisconnected = this.handleParticipantDisconnected.bind(this);
this.handleData = this.handleData.bind(this);
this.handleDevicesChanged = this.handleDevicesChanged.bind(this);
this.startRoom = this.startRoom.bind(this);
}
componentDidMount() {
const topic = this.props.tinode.getTopic(this.props.topic);
this.previousOnInfo = topic.onInfo;
topic.onInfo = this.onInfo;
this.start();
}
componentWillUnmount() {
const topic = this.props.tinode.getTopic(this.props.topic);
if (topic) {
topic.onInfo = this.previousOnInfo;
}
this.stop();
}
onInfo(info) {
if (info.what != 'call') {
return;
}
switch (info.event) {
case 'vc-token':
const ts = info.payload;
this.setState({token: ts.token});
this.startRoom(ts.token);
break;
case 'hang-up':
this.handleRemoteHangup(info);
break;
default:
console.warn("Unknown call event", info.event);
break;
}
}
handleParticipantConnected(participant) {
if (!participant) return;
if (participant instanceof RemoteParticipant) {
participant
.on(ParticipantEvent.TrackMuted, pub => {
console.log('track was muted', pub.trackSid, participant.identity);
this.forceUpdate(); // Redraw carousel.
})
.on(ParticipantEvent.TrackUnmuted, pub => {
console.log('track was unmuted', pub.trackSid, participant.identity);
this.forceUpdate(); // Redraw carousel.
})
.on(ParticipantEvent.IsSpeakingChanged, _ => {
this.forceUpdate(); // Redraw carousel.
})
.on(ParticipantEvent.ConnectionQualityChanged, _ => {
this.forceUpdate(); // Redraw carousel.
});
const me = this.props.tinode.getMeTopic();
const c = me.getContact(participant.identity);
const participants = Object.assign({}, this.state.participants);
participants[participant.identity] = {
name: c && c.public && c.public.fn ? c.public.fn : participant.identity,
photo: makeImageUrl(c && c.public ? c.public.photo : null),
participant: participant
};
this.setState({participants: participants});
}
}
handleParticipantDisconnected(participant) {
const participants = Object.assign({}, this.state.participants);
delete participants[participant.identity];
this.setState({participants: participants});
}
handleData(msg, participant) {
console.log('Data message ', msg, participant);
}
handleTrackSubscribed(track, publication, participant) {
if (participant instanceof RemoteParticipant) {
// Check if we're presenting a remote video stream.
if (this.remoteRef.current && !(this.remoteRef.current.src != '' || this.remoteRef.srcObject)) {
track.attach(this.remoteRef.current);
}
}
// Redraw the carousel and remote view.
this.forceUpdate();
}
handleTrackUnsubscribed(track, publication, participant) {
console.log('handleTrackUnsubscribed', track);
// remove tracks from all attached elements
track.detach();
this.forceUpdate();
}
handleLocalTrackPublished(pub) {
console.log('Local track published', pub);
const track = pub.track;
if ((track instanceof LocalVideoTrack) && this.localRef.current) {
const elem = this.localRef.current;
track.attach(elem);
// flip.
elem.style.transform = 'scale(-1, 1)';
}
}
handleLocalTrackUnpublished(pub) {
const track = pub.track;
console.log('handleLocalTrackUnpublished', track);
track.detach();
// Redraw carousel.
this.forceUpdate();
}
handleActiveSpeakerChange(speakers) {
console.log('handleActiveSpeakerChange', speakers);
speakers.forEach(s => {
if (s.isSpeaking) {
console.log('Speaking participant ', s);
// Display speaker in remote peer view.
const elem = this.remoteRef.current;
const me = this.props.tinode.getMeTopic();
const c = me.getContact(s.identity);
const name = c && c.public && c.public.fn ? c.public.fn : undefined;
this.setState({activeSpeaker: name});
const cameraPub = s.getTrack(Track.Source.Camera);
const cameraEnabled = cameraPub && cameraPub.isSubscribed && !cameraPub.isMuted;
if (cameraEnabled && cameraPub.videoTrack) {
cameraPub.videoTrack.attach(elem);
}
}
});
}
handleDevicesChanged() {
console.log('devices changed');
// TODO: implement.
}
handleDisconnect() {
console.log('disconnected from room');
this.handleCloseClick();
}
startRoom(token) {
const roomOpts = {
adaptiveStream: true,
dynacast: true,
publishDefaults: {
simulcast: true,
videoSimulcastLayers: [VideoPresets.h90, VideoPresets.h216],
videoCodec: 'vp8'
},
videoCaptureDefaults: {
resolution: VideoPresets.h720.resolution
}
};
const connectOpts = {
autoSubscribe: true
};
// creates a new room with options
const room = new Room(roomOpts);
this.setState({room: room});
const startTime = Date.now();
const topic = this.props.tinode.getTopic(this.props.topic);
const shouldPublish = this.isOutgoingCall || !this.isBroadcast;
room
.on(RoomEvent.ParticipantConnected, this.handleParticipantConnected)
.on(RoomEvent.ParticipantDisconnected, this.handleParticipantDisconnected)
.on(RoomEvent.DataReceived, this.handleData)
.on(RoomEvent.TrackSubscribed, this.handleTrackSubscribed)
.on(RoomEvent.TrackUnsubscribed, this.handleTrackUnsubscribed)
.on(RoomEvent.ActiveSpeakersChanged, this.handleActiveSpeakerChange)
.on(RoomEvent.Disconnected, this.handleDisconnect)
.on(RoomEvent.LocalTrackUnpublished, this.handleLocalTrackUnpublished)
.on(RoomEvent.LocalTrackPublished, this.handleLocalTrackPublished)
.on(RoomEvent.RoomMetadataChanged, metadata => {
console.log('VC room new metadata ', metadata);
})
.on(RoomEvent.MediaDevicesChanged, this.handleDevicesChanged)
.on(RoomEvent.AudioPlaybackStatusChanged, e => {
// TODO: handle it.
console.log('Audio playback status change: ', e);
})
.on(RoomEvent.MediaDevicesError, e => {
const failure = MediaDeviceFailure.getFailure(e);
this.reportError(failure);
})
.on(RoomEvent.ConnectionQualityChanged, (quality, participant) => {
const id = participant ? participant.identity : 'nil';
console.log('Connection quality changed', id, ' -> ', quality);
})
.on(RoomEvent.SignalConnected, _ => {
const signalConnectionTime = Date.now() - startTime;
console.log(`signal connection established in ${signalConnectionTime}ms`);
// speed up publishing by starting to publish before it's fully connected
// publishing is accepted as soon as signal connection has established
if (shouldPublish) {
room.localParticipant.enableCameraAndMicrophone();
console.log(`tracks published in ${Date.now() - startTime}ms`);
}
});
const vcEndpoint = this.props.tinode.getServerParam('vcEndpoint', null);
room.connect(vcEndpoint, token, connectOpts).then(_ => {
console.log('Connected to VC room', room.name, '. Access token', token);
room.participants.forEach((participant) => {
this.handleParticipantConnected(participant);
});
this.handleParticipantConnected(room.localParticipant);
});
}
start() {
if (this.state.localStream) {
this.props.onError(this.props.intl.formatMessage(messages.already_in_call), 'info');
return;
}
if (this.isOutgoingCall) {
this.props.onCreateCall(this.props.topic, this.props.seq, this.props.callState, this.props.callAudioOnly)
.then(ctrl => {
const token = ctrl.params.token;
const seq = ctrl.params.seq;
this.setState({callSeq: seq, token: token});
this.startRoom(token);
});
} else {
this.props.onJoin(this.props.topic, this.props.seq);
}
}
stop() {
if (this.state.room) {
this.state.room.disconnect();
}
}
reportError(err) {
this.props.onError(err.message, 'err');
}
// Call disconnected by remote.
handleRemoteHangup() {
this.handleCloseClick();
}
handleCloseClick() {
this.stop();
this.props.onHangup(this.props.topic, this.props.seq);
}
handleToggleCameraClick() {
if (!this.state.room) return;
const enabled = this.state.room.localParticipant.isCameraEnabled;
this.state.room.localParticipant.setCameraEnabled(!enabled)
.then(_ => this.forceUpdate());
this.forceUpdate();
}
handleToggleMicClick() {
if (!this.state.room) return;
const enabled = this.state.room.localParticipant.isMicrophoneEnabled;
this.state.room.localParticipant.setMicrophoneEnabled(!enabled)
.then(_ => this.forceUpdate());
// Make sure we redraw the mute/unmute icons (e.g. mic -> mic_off).
this.forceUpdate();
}
render() {
const isPublishing = this.isOutgoingCall || !this.isBroadcast;
const disabled = !isPublishing || !this.state.room || !this.state.room.localParticipant;
const audioIcon = !disabled && this.state.room.localParticipant.isMicrophoneEnabled ? 'mic' : 'mic_off';
const videoIcon = !disabled && this.state.room.localParticipant.isCameraEnabled ? 'videocam' : 'videocam_off';
const peerTitle = this.state.activeSpeaker ? this.state.activeSpeaker : clipStr(this.props.title, MAX_PEER_TITLE_LENGTH);
const remoteActive = this.remoteRef.current && (this.remoteRef.current.src || this.remoteRef.current.srcObject)
return (
<div id="video-container">
{!this.isBroadcast && Object.keys(this.state.participants).length > 0 ?
<VCCarousel
viewportWidth={this.props.viewportWidth}
tinode={this.props.tinode}
participants={this.state.participants} /> :
null
}
<div id="video-container-panel">
{isPublishing ?
<div className="call-party self" disabled={this.state.audioOnly}>
<video ref={this.localRef} autoPlay muted playsInline />
<div className="caller-name inactive">
<FormattedMessage id="calls_you_label"
defaultMessage="You" description="Shown over the local video screen" />
</div>
</div> :
null }
<div className="call-party peer" disabled={!remoteActive}>
<video ref={this.remoteRef} autoPlay playsInline />
{remoteActive ?
<div className="caller-name inactive">{peerTitle}</div>
:
<div className="caller-card">
<div className="avatar-box">
<LetterTile
tinode={this.props.tinode}
avatar={this.props.avatar}
topic={this.props.topic}
title={this.props.title} />
</div>
<div className="caller-name">{peerTitle}</div>
</div>
}
</div>
</div>
<div className="controls">
<button className="danger" onClick={this.handleCloseClick}>
<i className="material-icons">call_end</i>
</button>
<button className="secondary" onClick={this.handleToggleCameraClick} disabled={disabled}>
<i className="material-icons">{videoIcon}</i>
</button>
<button className="secondary" onClick={this.handleToggleMicClick} disabled={disabled}>
<i className="material-icons">{audioIcon}</i>
</button>
</div>
</div>
);
}
};
export default injectIntl(VCPanel);