Video conferencing skeleton.
This commit is contained in:
@@ -309,6 +309,11 @@ ul.menu > li.separator:hover {
|
|||||||
background-color: #ccc;
|
background-color: #ccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ul.menu > li.disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
/* The down arrow head - trigger for context menu */
|
/* The down arrow head - trigger for context menu */
|
||||||
.menuTrigger {
|
.menuTrigger {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -3242,3 +3247,87 @@ ul.phone-country-selector span.country {
|
|||||||
width: unset;
|
width: unset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Video conferencing. */
|
||||||
|
.call-party-speaking {
|
||||||
|
border: 2px solid red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.call-party.carousel-item .caller-name.inactive {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0.25rem;
|
||||||
|
right: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.call-party.carousel-item .muted {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.25rem;
|
||||||
|
right: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-box.carousel-item {
|
||||||
|
/* Center the avatar in parent view. */
|
||||||
|
width: 45px;
|
||||||
|
height: 45px;
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
#carousel-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 800px;
|
||||||
|
position: relative;
|
||||||
|
align-self: center;
|
||||||
|
padding-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#carousel {
|
||||||
|
overflow: auto;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#carousel-prev, #carousel-next {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-content: center;
|
||||||
|
background: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
outline: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
min-width: unset;
|
||||||
|
height: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
#carousel-prev {
|
||||||
|
top: 50%;
|
||||||
|
left: 0;
|
||||||
|
transform: translate(50%, -50%);
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#carousel-next {
|
||||||
|
top: 50%;
|
||||||
|
right: 0;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
#carousel-content {
|
||||||
|
display: grid;
|
||||||
|
justify-content: center;
|
||||||
|
grid-gap: 16px;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
margin: auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-item {
|
||||||
|
width: 180px;
|
||||||
|
height: 100px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|||||||
@@ -136,6 +136,8 @@ export function fullFormatter(style, data, values, key, stack) {
|
|||||||
attr.callState = data.state;
|
attr.callState = data.state;
|
||||||
attr.incoming = data.incoming;
|
attr.incoming = data.incoming;
|
||||||
attr.duration = data.duration;
|
attr.duration = data.duration;
|
||||||
|
attr.vc = data.vc;
|
||||||
|
attr.onCallJoin = this.onCallJoin;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'VD':
|
case 'VD':
|
||||||
|
|||||||
+40
-16
@@ -19,6 +19,7 @@ import LogoView from './logo-view.jsx';
|
|||||||
import MetaMessage from '../widgets/meta-message.jsx';
|
import MetaMessage from '../widgets/meta-message.jsx';
|
||||||
import PinnedMessages from '../widgets/pinned-messages.jsx';
|
import PinnedMessages from '../widgets/pinned-messages.jsx';
|
||||||
import SendMessage from '../widgets/send-message.jsx';
|
import SendMessage from '../widgets/send-message.jsx';
|
||||||
|
import VCPanel from '../widgets/vc-panel.jsx';
|
||||||
import VideoPreview from '../widgets/video-preview.jsx';
|
import VideoPreview from '../widgets/video-preview.jsx';
|
||||||
|
|
||||||
import { DEFAULT_P2P_ACCESS_MODE, EDIT_PREVIEW_LENGTH, IMAGE_PREVIEW_DIM, IMMEDIATE_P2P_SUBSCRIPTION,
|
import { DEFAULT_P2P_ACCESS_MODE, EDIT_PREVIEW_LENGTH, IMAGE_PREVIEW_DIM, IMMEDIATE_P2P_SUBSCRIPTION,
|
||||||
@@ -1440,23 +1441,44 @@ class MessagesView extends React.Component {
|
|||||||
onSendMessage={this.sendFileAttachment} />
|
onSendMessage={this.sendFileAttachment} />
|
||||||
);
|
);
|
||||||
} else if (this.state.rtcPanel) {
|
} else if (this.state.rtcPanel) {
|
||||||
component2 = (
|
// P2P call.
|
||||||
<CallPanel
|
if (this.state.rtcPanel.startsWith("usr")) {
|
||||||
topic={this.state.topic}
|
component2 = (
|
||||||
seq={this.props.callSeq}
|
<CallPanel
|
||||||
callState={this.props.callState}
|
topic={this.state.topic}
|
||||||
callAudioOnly={this.props.callAudioOnly}
|
seq={this.props.callSeq}
|
||||||
tinode={this.props.tinode}
|
callState={this.props.callState}
|
||||||
title={this.state.title}
|
callAudioOnly={this.props.callAudioOnly}
|
||||||
avatar={this.state.avatar || true}
|
tinode={this.props.tinode}
|
||||||
|
title={this.state.title}
|
||||||
|
avatar={this.state.avatar || true}
|
||||||
|
|
||||||
onError={this.props.onError}
|
onError={this.props.onError}
|
||||||
onHangup={this.handleCallHangup}
|
onHangup={this.handleCallHangup}
|
||||||
onInvite={this.props.onCallInvite}
|
onInvite={this.props.onCallInvite}
|
||||||
onSendOffer={this.props.onCallSendOffer}
|
onSendOffer={this.props.onCallSendOffer}
|
||||||
onIceCandidate={this.props.onCallIceCandidate}
|
onIceCandidate={this.props.onCallIceCandidate}
|
||||||
onSendAnswer={this.props.onCallSendAnswer} />
|
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}
|
||||||
|
|
||||||
|
onError={this.props.onError}
|
||||||
|
onHangup={this.handleCallHangup}
|
||||||
|
onCreateCall={this.props.onCallInvite}
|
||||||
|
onJoin={this.props.onVCJoin} />
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const topic = this.props.tinode.getTopic(this.state.topic);
|
const topic = this.props.tinode.getTopic(this.state.topic);
|
||||||
const isChannel = topic.isChannelType();
|
const isChannel = topic.isChannelType();
|
||||||
@@ -1537,6 +1559,7 @@ class MessagesView extends React.Component {
|
|||||||
messageNodes.push(
|
messageNodes.push(
|
||||||
<ChatMessage
|
<ChatMessage
|
||||||
tinode={this.props.tinode}
|
tinode={this.props.tinode}
|
||||||
|
topic={this.state.topic}
|
||||||
content={msg.content}
|
content={msg.content}
|
||||||
mimeType={msg.head && msg.head.mime}
|
mimeType={msg.head && msg.head.mime}
|
||||||
replyToSeq={replyToSeq}
|
replyToSeq={replyToSeq}
|
||||||
@@ -1563,6 +1586,7 @@ class MessagesView extends React.Component {
|
|||||||
pickReply={this.handlePickReply}
|
pickReply={this.handlePickReply}
|
||||||
editMessage={this.handleEditMessage}
|
editMessage={this.handleEditMessage}
|
||||||
onQuoteClick={this.handleQuoteClick}
|
onQuoteClick={this.handleQuoteClick}
|
||||||
|
onAcceptCall={this.props.onAcceptCall}
|
||||||
onError={this.props.onError}
|
onError={this.props.onError}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
key={msg.seq} />
|
key={msg.seq} />
|
||||||
|
|||||||
+36
-10
@@ -176,6 +176,7 @@ class TinodeWeb extends React.Component {
|
|||||||
this.handleCallSendOffer = this.handleCallSendOffer.bind(this);
|
this.handleCallSendOffer = this.handleCallSendOffer.bind(this);
|
||||||
this.handleCallIceCandidate = this.handleCallIceCandidate.bind(this);
|
this.handleCallIceCandidate = this.handleCallIceCandidate.bind(this);
|
||||||
this.handleCallSendAnswer = this.handleCallSendAnswer.bind(this);
|
this.handleCallSendAnswer = this.handleCallSendAnswer.bind(this);
|
||||||
|
this.handleVCCallJoin = this.handleVCCallJoin.bind(this);
|
||||||
|
|
||||||
this.handleCallAccept = this.handleCallAccept.bind(this);
|
this.handleCallAccept = this.handleCallAccept.bind(this);
|
||||||
|
|
||||||
@@ -1591,7 +1592,7 @@ class TinodeWeb extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let muted = false, blocked = false, self_blocked = false, subscribed = false, deleter = false,
|
let muted = false, blocked = false, self_blocked = false, subscribed = false, deleter = false,
|
||||||
archived = false, webrtc = false;
|
archived = false, webrtc = false, writer = false;
|
||||||
if (topic) {
|
if (topic) {
|
||||||
subscribed = topic.isSubscribed();
|
subscribed = topic.isSubscribed();
|
||||||
archived = topic.isArchived();
|
archived = topic.isArchived();
|
||||||
@@ -1602,10 +1603,12 @@ class TinodeWeb extends React.Component {
|
|||||||
blocked = !acs.isJoiner();
|
blocked = !acs.isJoiner();
|
||||||
self_blocked = !acs.isJoiner('want');
|
self_blocked = !acs.isJoiner('want');
|
||||||
deleter = acs.isDeleter();
|
deleter = acs.isDeleter();
|
||||||
|
writer = acs.isWriter();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
webrtc = !!this.tinode.getServerParam('iceServers');
|
webrtc = writer && !!this.tinode.getServerParam('iceServers');
|
||||||
|
const mediaServerAvailable = !!this.tinode.getServerParam('vcEndpoint');
|
||||||
|
|
||||||
return [
|
return [
|
||||||
subscribed ? {
|
subscribed ? {
|
||||||
@@ -1616,8 +1619,9 @@ class TinodeWeb extends React.Component {
|
|||||||
title: this.props.intl.formatMessage(messages.menu_item_audio_call),
|
title: this.props.intl.formatMessage(messages.menu_item_audio_call),
|
||||||
handler: this.handleStartAudioCall
|
handler: this.handleStartAudioCall
|
||||||
} : null,
|
} : null,
|
||||||
subscribed && Tinode.isP2PTopicName(topicName) && webrtc ? {
|
subscribed && webrtc ? {
|
||||||
title: this.props.intl.formatMessage(messages.menu_item_video_call),
|
title: this.props.intl.formatMessage(messages.menu_item_video_call),
|
||||||
|
disabled: Tinode.isGroupTopicName(topicName) && !mediaServerAvailable,
|
||||||
handler: this.handleStartVideoCall
|
handler: this.handleStartVideoCall
|
||||||
} : null,
|
} : null,
|
||||||
subscribed ? 'messages_clear' : null,
|
subscribed ? 'messages_clear' : null,
|
||||||
@@ -1772,13 +1776,19 @@ class TinodeWeb extends React.Component {
|
|||||||
switch (callState) {
|
switch (callState) {
|
||||||
case CALL_STATE_OUTGOING_INITATED:
|
case CALL_STATE_OUTGOING_INITATED:
|
||||||
const head = { webrtc: CALL_HEAD_STARTED, aonly: !!audioOnly };
|
const head = { webrtc: CALL_HEAD_STARTED, aonly: !!audioOnly };
|
||||||
this.handleSendMessage(Drafty.videoCall(audioOnly), undefined, undefined, head)
|
let msg = Drafty.videoCall(audioOnly);
|
||||||
|
if (Tinode.isGroupTopicName(callTopic)) {
|
||||||
|
// Video conference.
|
||||||
|
head.vc = true;
|
||||||
|
}
|
||||||
|
return this.handleSendMessage(msg, undefined, undefined, head)
|
||||||
.then(ctrl => {
|
.then(ctrl => {
|
||||||
if (ctrl.code < 200 || ctrl.code >= 300 || !ctrl.params || !ctrl.params.seq) {
|
if (ctrl.code < 200 || ctrl.code >= 300 || !ctrl.params || !ctrl.params.seq) {
|
||||||
this.handleCallClose();
|
this.handleCallClose();
|
||||||
return;
|
return ctrl;
|
||||||
}
|
}
|
||||||
this.setState({callSeq: ctrl.params['seq']});
|
this.setState({callSeq: ctrl.params['seq']});
|
||||||
|
return ctrl
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case CALL_STATE_IN_PROGRESS:
|
case CALL_STATE_IN_PROGRESS:
|
||||||
@@ -1836,6 +1846,15 @@ class TinodeWeb extends React.Component {
|
|||||||
topic.videoCall('answer', callSeq, sdp);
|
topic.videoCall('answer', callSeq, sdp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleVCCallJoin(callTopic, callSeq) {
|
||||||
|
const topic = this.tinode.getTopic(callTopic);
|
||||||
|
if (!topic) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
topic.videoCall('vc-join', callSeq);
|
||||||
|
}
|
||||||
|
|
||||||
handleCallClose() {
|
handleCallClose() {
|
||||||
if (this.callTimeoutTimer) {
|
if (this.callTimeoutTimer) {
|
||||||
clearTimeout(this.callTimeoutTimer);
|
clearTimeout(this.callTimeoutTimer);
|
||||||
@@ -1847,16 +1866,21 @@ class TinodeWeb extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCallAccept(topicName) {
|
handleCallAccept(topicName, setCallTopic, callSeq) {
|
||||||
const topic = this.tinode.getTopic(topicName);
|
const topic = this.tinode.getTopic(topicName);
|
||||||
if (!topic) {
|
if (!topic) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (topic.isSubscribed()) {
|
if (topic.isSubscribed()) {
|
||||||
this.handleTopicSelected(this.state.callTopic);
|
this.handleTopicSelected(setCallTopic ? topicName : this.state.callTopic);
|
||||||
this.setState({
|
let upd = {
|
||||||
callState: CALL_STATE_IN_PROGRESS
|
callState: CALL_STATE_IN_PROGRESS
|
||||||
});
|
}
|
||||||
|
if (setCallTopic) {
|
||||||
|
upd.callTopic = topicName;
|
||||||
|
upd.callSeq = callSeq;
|
||||||
|
}
|
||||||
|
this.setState(upd);
|
||||||
} else {
|
} else {
|
||||||
// We need to switch and subscribe to callTopic first.
|
// We need to switch and subscribe to callTopic first.
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -1894,7 +1918,7 @@ class TinodeWeb extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleDataMessage(data) {
|
handleDataMessage(data) {
|
||||||
if (data.head && data.head.webrtc && data.head.webrtc == CALL_HEAD_STARTED) {
|
if (data.head && data.head.webrtc && data.head.webrtc == CALL_HEAD_STARTED && Tinode.isP2PTopicName(data.topic)) {
|
||||||
// If it's a video call invite message.
|
// If it's a video call invite message.
|
||||||
// See if we need to display incoming call UI.
|
// See if we need to display incoming call UI.
|
||||||
const topic = this.tinode.getTopic(data.topic);
|
const topic = this.tinode.getTopic(data.topic);
|
||||||
@@ -2089,11 +2113,13 @@ class TinodeWeb extends React.Component {
|
|||||||
callState={this.state.callState}
|
callState={this.state.callState}
|
||||||
callAudioOnly={this.state.callAudioOnly}
|
callAudioOnly={this.state.callAudioOnly}
|
||||||
onCallHangup={this.handleCallHangup}
|
onCallHangup={this.handleCallHangup}
|
||||||
|
onAcceptCall={this.handleCallAccept}
|
||||||
|
|
||||||
onCallInvite={this.handleCallInvite}
|
onCallInvite={this.handleCallInvite}
|
||||||
onCallSendOffer={this.handleCallSendOffer}
|
onCallSendOffer={this.handleCallSendOffer}
|
||||||
onCallIceCandidate={this.handleCallIceCandidate}
|
onCallIceCandidate={this.handleCallIceCandidate}
|
||||||
onCallSendAnswer={this.handleCallSendAnswer}
|
onCallSendAnswer={this.handleCallSendAnswer}
|
||||||
|
onVCJoin={this.handleVCCallJoin}
|
||||||
|
|
||||||
errorText={this.state.errorText}
|
errorText={this.state.errorText}
|
||||||
errorLevel={this.state.errorLevel}
|
errorLevel={this.state.errorLevel}
|
||||||
|
|||||||
@@ -7,11 +7,17 @@ import { secondsToTime } from '../lib/strformat.js';
|
|||||||
|
|
||||||
export default class CallMessage extends React.PureComponent {
|
export default class CallMessage extends React.PureComponent {
|
||||||
render() {
|
render() {
|
||||||
|
const bigIcon = this.props.vc ? 'video_call' : 'call';
|
||||||
const isCallDropped = ['busy', 'declined', 'disconnected', 'missed'].includes(this.props.callState);
|
const isCallDropped = ['busy', 'declined', 'disconnected', 'missed'].includes(this.props.callState);
|
||||||
const successClass = 'material-icons medium ' + (isCallDropped ? 'red' : 'green');
|
const successClass = 'material-icons medium ' + (isCallDropped ? 'red' : 'green');
|
||||||
const callIcon = this.props.incoming ?
|
const callIcon = this.props.incoming ?
|
||||||
(isCallDropped ? 'call_missed' : 'call_received') :
|
(isCallDropped ? 'call_missed' : 'call_received') :
|
||||||
(isCallDropped ? 'call_missed_outgoing' : 'call_made');
|
(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 ?
|
||||||
|
<FormattedMessage id="calls_incoming" defaultMessage="Incoming call" description="Incoming call label" /> :
|
||||||
|
<FormattedMessage id="calls_outgoing" defaultMessage="Outgoing call" description="Outgoing call label" />;
|
||||||
let duration;
|
let duration;
|
||||||
if (isCallDropped) {
|
if (isCallDropped) {
|
||||||
switch (this.props.callState) {
|
switch (this.props.callState) {
|
||||||
@@ -33,14 +39,15 @@ export default class CallMessage extends React.PureComponent {
|
|||||||
} else {
|
} else {
|
||||||
duration = <span>{secondsToTime(this.props.duration / 1000)}</span>;
|
duration = <span>{secondsToTime(this.props.duration / 1000)}</span>;
|
||||||
}
|
}
|
||||||
|
const allowJoin = this.props.vc && this.props.callState == 'started';
|
||||||
return <div className="call-message">
|
return <div className="call-message">
|
||||||
<div><i className="material-icons big gray">call</i></div>
|
<div><i className="material-icons big gray">{bigIcon}</i></div>
|
||||||
<div className="flex-column narrow">
|
<div className="flex-column narrow">
|
||||||
<div>{this.props.incoming ?
|
<div>{title}</div>
|
||||||
<FormattedMessage id="calls_incoming" defaultMessage="Incoming call" description="Incoming call label" /> :
|
|
||||||
<FormattedMessage id="calls_outgoing" defaultMessage="Outgoing call" description="Outgoing call label" />
|
|
||||||
}</div>
|
|
||||||
<div className="duration"><i className={successClass}>{callIcon}</i> {duration}</div>
|
<div className="duration"><i className={successClass}>{callIcon}</i> {duration}</div>
|
||||||
|
{allowJoin ?
|
||||||
|
<a onClick={this.props.onCallJoin}>Join</a>
|
||||||
|
: null}
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ class BaseChatMessage extends React.PureComponent {
|
|||||||
this.handleContextClick = this.handleContextClick.bind(this);
|
this.handleContextClick = this.handleContextClick.bind(this);
|
||||||
this.handleCancelUpload = this.handleCancelUpload.bind(this);
|
this.handleCancelUpload = this.handleCancelUpload.bind(this);
|
||||||
this.handleQuoteClick = this.handleQuoteClick.bind(this);
|
this.handleQuoteClick = this.handleQuoteClick.bind(this);
|
||||||
|
this.handleCallJoin = this.handleCallJoin.bind(this);
|
||||||
|
|
||||||
this.formatterContext = {
|
this.formatterContext = {
|
||||||
formatMessage: props.intl.formatMessage.bind(props.intl),
|
formatMessage: props.intl.formatMessage.bind(props.intl),
|
||||||
@@ -38,7 +39,8 @@ class BaseChatMessage extends React.PureComponent {
|
|||||||
onImagePreview: this.handleExpandImage,
|
onImagePreview: this.handleExpandImage,
|
||||||
onVideoPreview: this.handlePlayVideo,
|
onVideoPreview: this.handlePlayVideo,
|
||||||
onFormButtonClick: this.handleFormButtonClick,
|
onFormButtonClick: this.handleFormButtonClick,
|
||||||
onQuoteClick: this.handleQuoteClick
|
onQuoteClick: this.handleQuoteClick,
|
||||||
|
onCallJoin: this.handleCallJoin
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,6 +151,10 @@ class BaseChatMessage extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleCallJoin() {
|
||||||
|
this.props.onAcceptCall(this.props.topic, true, this.props.seq);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const sideClass = this.props.sequence + ' ' + (this.props.response ? 'left' : 'right');
|
const sideClass = this.props.sequence + ' ' + (this.props.response ? 'left' : 'right');
|
||||||
const bubbleClass = (this.props.sequence == 'single' || this.props.sequence == 'last') ? 'bubble tip' : 'bubble';
|
const bubbleClass = (this.props.sequence == 'single' || this.props.sequence == 'last') ? 'bubble tip' : 'bubble';
|
||||||
|
|||||||
@@ -493,11 +493,12 @@ class ContextMenu extends React.Component {
|
|||||||
item = this.MenuItems[item];
|
item = this.MenuItems[item];
|
||||||
}
|
}
|
||||||
if (item && item.title) {
|
if (item && item.title) {
|
||||||
|
const className = item.disabled ? 'disabled' : undefined;
|
||||||
menu.push(
|
menu.push(
|
||||||
item.title == '-' ?
|
item.title == '-' ?
|
||||||
<li className="separator" key={count} />
|
<li className="separator" key={count} />
|
||||||
:
|
:
|
||||||
<li onClick={this.handleClick} data-id={count} key={count}>{item.title}</li>
|
<li className={className} onClick={this.handleClick} data-id={count} key={count}>{item.title}</li>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
count++;
|
count++;
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
// Video conference carousel item.
|
||||||
|
import React from 'react';
|
||||||
|
import { injectIntl } from 'react-intl';
|
||||||
|
import {
|
||||||
|
Track,
|
||||||
|
} from 'livekit-client';
|
||||||
|
|
||||||
|
import LetterTile from './letter-tile.jsx';
|
||||||
|
|
||||||
|
class VCCarouselItem extends React.PureComponent {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
videoRef: undefined,
|
||||||
|
audioRef: undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
this.handleVideoRefChange = this.handleVideoRefChange.bind(this);
|
||||||
|
this.handleAudioRefChange = this.handleAudioRefChange.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (this.state.videoRef && !this.state.videoRef.srcObject && this.props.cameraPub && this.props.cameraPub.videoTrack) {
|
||||||
|
this.props.cameraPub.videoTrack.attach(this.state.videoRef);
|
||||||
|
}
|
||||||
|
if (this.state.audioRef && !this.state.audioRef.srcObject && this.props.micPub && this.props.micPub.audioTrack) {
|
||||||
|
this.props.micPub.audioTrack.attach(this.state.audioRef);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleVideoRefChange(node) {
|
||||||
|
if (!node) { return; }
|
||||||
|
this.setState({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.setState({audioRef: node});
|
||||||
|
|
||||||
|
// Attach audio track to audio element.
|
||||||
|
const micPub = this.props.micPub;
|
||||||
|
const micEnabled = micPub && micPub.isSubscribed && !micPub.isMuted;
|
||||||
|
if (micEnabled && this.state.audioTrack) {
|
||||||
|
this.state.audioTrack.attach(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const name = this.props.name;
|
||||||
|
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} key={this.props.key}>
|
||||||
|
<>
|
||||||
|
<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 carousel-item">
|
||||||
|
<LetterTile
|
||||||
|
tinode={this.props.tinode}
|
||||||
|
avatar={this.props.photo}
|
||||||
|
topic={this.props.identity}
|
||||||
|
title={this.props.name} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
: null}
|
||||||
|
<div className="caller-name inactive">
|
||||||
|
{this.props.name}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default injectIntl(VCCarouselItem);
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { injectIntl } from 'react-intl';
|
||||||
|
import {
|
||||||
|
Track,
|
||||||
|
} from 'livekit-client';
|
||||||
|
|
||||||
|
import LetterTile from './letter-tile.jsx';
|
||||||
|
import VCCarouselItem from './vc-carousel-item.jsx';
|
||||||
|
|
||||||
|
class VCCarousel extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.handlePrevClick = this.handlePrevClick.bind(this);
|
||||||
|
this.handleNextClick = this.handleNextClick.bind(this);
|
||||||
|
|
||||||
|
this.carouselRef = React.createRef();
|
||||||
|
this.contentRef = React.createRef();
|
||||||
|
this.prevRef = React.createRef();
|
||||||
|
this.nextRef = React.createRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePrevClick() {
|
||||||
|
const gap = 16;
|
||||||
|
const width = this.carouselRef.current.offsetWidth;
|
||||||
|
this.carouselRef.current.scrollBy(-(width + gap), 0);
|
||||||
|
if (this.carouselRef.current.scrollLeft - width - gap <= 0) {
|
||||||
|
this.prevRef.current.style.display = "none";
|
||||||
|
}
|
||||||
|
if (!this.contentRef.current.scrollWidth - width - gap <= this.carouselRef.current.scrollLeft + width) {
|
||||||
|
this.nextRef.current.style.display = "flex";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleNextClick() {
|
||||||
|
const gap = 16;
|
||||||
|
const width = this.carouselRef.current.offsetWidth;
|
||||||
|
this.carouselRef.current.scrollBy(width + gap, 0);
|
||||||
|
if (this.carouselRef.current.scrollWidth !== 0) {
|
||||||
|
this.prevRef.current.style.display = "flex";
|
||||||
|
}
|
||||||
|
if (this.contentRef.current.scrollWidth - width - gap <= this.carouselRef.current.scrollLeft + width) {
|
||||||
|
this.nextRef.current.style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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}
|
||||||
|
identity={identity}
|
||||||
|
photo={party.photo}
|
||||||
|
key={i}
|
||||||
|
/>);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div id="carousel-wrapper">
|
||||||
|
<div id="carousel" ref={this.carouselRef}>
|
||||||
|
<div id="carousel-content" ref={this.contentRef}>
|
||||||
|
{peers}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button id="carousel-prev" ref={this.prevRef} onClick={this.handlePrevClick}>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path fill="none" d="M0 0h24v24H0V0z" />
|
||||||
|
<path d="M15.61 7.41L14.2 6l-6 6 6 6 1.41-1.41L11.03 12l4.58-4.59z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button id="carousel-next" ref={this.nextRef} onClick={this.handleNextClick}>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path fill="none" d="M0 0h24v24H0V0z" />
|
||||||
|
<path d="M10.02 6L8.61 7.41 13.19 12l-4.58 4.59L10.02 18l6-6-6-6z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default injectIntl(VCCarousel);
|
||||||
@@ -0,0 +1,424 @@
|
|||||||
|
// Video conference or broadcast: local and/or peer viewports and controls.
|
||||||
|
import React from 'react';
|
||||||
|
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import {
|
||||||
|
connect,
|
||||||
|
LocalParticipant,
|
||||||
|
MediaDeviceFailure,
|
||||||
|
Participant,
|
||||||
|
ParticipantEvent,
|
||||||
|
RemoteParticipant,
|
||||||
|
RemoteTrack,
|
||||||
|
RemoteTrackPublication,
|
||||||
|
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.handleTrackSubscribed2 = this.handleTrackSubscribed2.bind(this);
|
||||||
|
this.handleTrackUnsubscribed2 = this.handleTrackUnsubscribed2.bind(this);
|
||||||
|
this.handleActiveSpeakerChange2 = this.handleActiveSpeakerChange2.bind(this);
|
||||||
|
this.handleDisconnect2 = this.handleDisconnect2.bind(this);
|
||||||
|
this.handleLocalTrackUnpublished2 = this.handleLocalTrackUnpublished2.bind(this);
|
||||||
|
this.handleLocalTrackPublished2 = this.handleLocalTrackPublished2.bind(this);
|
||||||
|
this.handleParticipantConnected2 = this.handleParticipantConnected2.bind(this);
|
||||||
|
this.handleParticipantDisconnected2 = this.handleParticipantDisconnected2.bind(this);
|
||||||
|
this.handleData2 = this.handleData2.bind(this);
|
||||||
|
this.handleDevicesChanged = this.handleDevicesChanged.bind(this);
|
||||||
|
this.startRoom = this.startRoom.bind(this);
|
||||||
|
|
||||||
|
this.participants = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
handleParticipantConnected2(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 name = c && c.public && c.public.fn ? c.public.fn : participant.identity;
|
||||||
|
const photo = makeImageUrl(c && c.public ? c.public.photo : null);
|
||||||
|
const p = {
|
||||||
|
name: name,
|
||||||
|
photo: photo,
|
||||||
|
participant: participant
|
||||||
|
};
|
||||||
|
this.participants[participant.identity] = p
|
||||||
|
console.log('participant connected', this.participants);
|
||||||
|
this.setState({participants: this.participants}, _ => { this.forceUpdate(); });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleParticipantDisconnected2(participant) {
|
||||||
|
console.log('Deleting participant ', participant.identity);
|
||||||
|
delete this.participants[participant.identity];
|
||||||
|
this.setState({participants: this.participants});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleData2(msg, participant) {
|
||||||
|
console.log('Data message ', msg, participant);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTrackSubscribed2(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();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTrackUnsubscribed2(track, publication, participant) {
|
||||||
|
console.log('handleTrackUnsubscribed2', track);
|
||||||
|
// remove tracks from all attached elements
|
||||||
|
track.detach();
|
||||||
|
this.forceUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLocalTrackPublished2(pub) {
|
||||||
|
console.log('Local track published', pub);
|
||||||
|
const track = pub.track;
|
||||||
|
if (this.localRef.current) {
|
||||||
|
const elem = this.localRef.current;
|
||||||
|
track.attach(elem);
|
||||||
|
// flip.
|
||||||
|
elem.style.transform = 'scale(-1, 1)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLocalTrackUnpublished2(pub) {
|
||||||
|
const track = pub.track;
|
||||||
|
console.log('handleLocalTrackUnpublished2', track);
|
||||||
|
track.detach();
|
||||||
|
// Redraw carousel.
|
||||||
|
this.forceUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleActiveSpeakerChange2(speakers) {
|
||||||
|
console.log('handleActiveSpeakerChange2', 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.
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDisconnect2() {
|
||||||
|
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.handleParticipantConnected2)
|
||||||
|
.on(RoomEvent.ParticipantDisconnected, this.handleParticipantDisconnected2)
|
||||||
|
.on(RoomEvent.DataReceived, this.handleData2)
|
||||||
|
.on(RoomEvent.TrackSubscribed, this.handleTrackSubscribed2)
|
||||||
|
.on(RoomEvent.TrackUnsubscribed, this.handleTrackUnsubscribed2)
|
||||||
|
.on(RoomEvent.ActiveSpeakersChanged, this.handleActiveSpeakerChange2)
|
||||||
|
.on(RoomEvent.Disconnected, this.handleDisconnect2)
|
||||||
|
.on(RoomEvent.LocalTrackUnpublished, this.handleLocalTrackUnpublished2)
|
||||||
|
.on(RoomEvent.LocalTrackPublished, this.handleLocalTrackPublished2)
|
||||||
|
|
||||||
|
.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.handleParticipantConnected2(participant);
|
||||||
|
});
|
||||||
|
this.handleParticipantConnected2(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
|
||||||
|
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);
|
||||||
Reference in New Issue
Block a user