Merge branch 'vc' of https://github.com/tinode/webapp into vc
This commit is contained in:
@@ -7,7 +7,13 @@ import { secondsToTime } from '../lib/strformat.js';
|
||||
|
||||
export default class CallMessage extends React.PureComponent {
|
||||
render() {
|
||||
const bigIcon = this.props.vc ? 'video_call' : 'call';
|
||||
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 ?
|
||||
@@ -46,7 +52,9 @@ export default class CallMessage extends React.PureComponent {
|
||||
<div>{title}</div>
|
||||
<div className="duration"><i className={successClass}>{callIcon}</i> {duration}</div>
|
||||
{allowJoin ?
|
||||
<a onClick={this.props.onCallJoin}>Join</a>
|
||||
<a onClick={this.props.onCallJoin}>
|
||||
<FormattedMessage id="join_conference" defaultMessage="+ Join" description="Call to action to join a conference call" />
|
||||
</a>
|
||||
: null}
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
@@ -6,7 +6,8 @@ import { secondsToTime } from '../lib/strformat.js';
|
||||
|
||||
// Props:
|
||||
// incoming: true | false.
|
||||
// success: true | false.
|
||||
// callState: 'busy' | 'declined' | 'disconnected' | 'missed'.
|
||||
// duration: <number> (call duration in milliseconds).
|
||||
export default class CallStatus extends React.PureComponent {
|
||||
render() {
|
||||
const isCallDropped = ['busy', 'declined', 'disconnected', 'missed'].includes(this.props.callState);
|
||||
|
||||
@@ -29,7 +29,7 @@ export default class InlineVideo extends React.PureComponent {
|
||||
{React.createElement('img', this.props)}
|
||||
<div className="play-control">
|
||||
{this.props.onClick ?
|
||||
<i className="material-icons white bigger">play_arrow</i>
|
||||
<i className="material-icons white x-big">play_arrow</i>
|
||||
:
|
||||
<img src="img/broken_video.png" style={{filter: 'invert(100%)'}} width="36" height="36" />}
|
||||
</div>
|
||||
|
||||
@@ -1,34 +1,44 @@
|
||||
// Video conference carousel item.
|
||||
import React from 'react';
|
||||
import Track from 'livekit-client';
|
||||
|
||||
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.state = {
|
||||
videoRef: undefined,
|
||||
audioRef: undefined
|
||||
};
|
||||
|
||||
this.handleVideoRefChange = this.handleVideoRefChange.bind(this);
|
||||
this.handleAudioRefChange = this.handleAudioRefChange.bind(this);
|
||||
|
||||
this.videoRef = null;
|
||||
this.audioRef = null;
|
||||
}
|
||||
|
||||
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.videoRef && !this.videoRef.srcObject && this.props.cameraPub && this.props.cameraPub.videoTrack) {
|
||||
this.props.cameraPub.videoTrack.attach(this.videoRef);
|
||||
}
|
||||
if (this.state.audioRef && !this.state.audioRef.srcObject && this.props.micPub && this.props.micPub.audioTrack) {
|
||||
this.props.micPub.audioTrack.attach(this.state.audioRef);
|
||||
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.setState({videoRef: node});
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.videoRef = node;
|
||||
|
||||
// Attach video track to video element.
|
||||
const cameraPub = this.props.cameraPub;
|
||||
@@ -39,14 +49,17 @@ export default class VCCarouselItem extends React.PureComponent {
|
||||
}
|
||||
|
||||
handleAudioRefChange(node) {
|
||||
if (!node) { return; }
|
||||
this.setState({audioRef: 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 && this.state.audioTrack) {
|
||||
this.state.audioTrack.attach(node);
|
||||
if (micEnabled && micPub.audioTrack) {
|
||||
micPub.audioTrack.attach(node);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,28 +73,24 @@ export default class VCCarouselItem extends React.PureComponent {
|
||||
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 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}
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
+58
-54
@@ -2,7 +2,6 @@ import React from 'react';
|
||||
|
||||
import { Track } from 'livekit-client';
|
||||
|
||||
import LetterTile from './letter-tile.jsx';
|
||||
import VCCarouselItem from './vc-carousel-item.jsx';
|
||||
|
||||
import { VC_CAROUSEL_ITEM_GAP } from '../constants.js';
|
||||
@@ -10,39 +9,56 @@ import { VC_CAROUSEL_ITEM_GAP } from '../constants.js';
|
||||
export default 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.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();
|
||||
this.prevRef = React.createRef();
|
||||
this.nextRef = React.createRef();
|
||||
}
|
||||
|
||||
handlePrevClick() {
|
||||
const gap = VC_CAROUSEL_ITEM_GAP;
|
||||
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';
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.carouselRef && this.props.viewportWidth != prevProps.viewportWidth) {
|
||||
this.handleScrollEvent({target: this.carouselRef});
|
||||
}
|
||||
}
|
||||
|
||||
handleNextClick() {
|
||||
const gap = VC_CAROUSEL_ITEM_GAP;
|
||||
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';
|
||||
componentWillUnmount() {
|
||||
if (this.carouselRef) {
|
||||
this.carouselRef.removeEventListener('scroll', this.handleScrollEvent);
|
||||
}
|
||||
if (this.contentRef.current.scrollWidth - width - gap <= this.carouselRef.current.scrollLeft + width) {
|
||||
this.nextRef.current.style.display = 'none';
|
||||
}
|
||||
|
||||
// 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) {
|
||||
@@ -57,44 +73,32 @@ export default class VCCarousel extends React.Component {
|
||||
cameraPub={cameraPub ? cameraPub : null}
|
||||
micPub={micPub ? micPub : null}
|
||||
name={party.name}
|
||||
identity={identity}
|
||||
userId={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 id="carousel-wrapper">
|
||||
<div id="carousel" ref={this.handleScrollReference}>
|
||||
<div id="carousel-content" ref={this.contentRef}>
|
||||
{peers}
|
||||
</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>
|
||||
</>
|
||||
{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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+58
-69
@@ -1,17 +1,12 @@
|
||||
// Video conference or broadcast: local and/or peer viewports and controls.
|
||||
import React from 'react';
|
||||
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
||||
import { FormattedMessage, injectIntl } from 'react-intl';
|
||||
|
||||
import {
|
||||
connect,
|
||||
LocalParticipant,
|
||||
LocalVideoTrack,
|
||||
MediaDeviceFailure,
|
||||
Participant,
|
||||
ParticipantEvent,
|
||||
RemoteParticipant,
|
||||
RemoteTrack,
|
||||
RemoteTrackPublication,
|
||||
Room,
|
||||
RoomEvent,
|
||||
Track,
|
||||
@@ -70,8 +65,6 @@ class VCPanel extends React.PureComponent {
|
||||
this.handleData = this.handleData.bind(this);
|
||||
this.handleDevicesChanged = this.handleDevicesChanged.bind(this);
|
||||
this.startRoom = this.startRoom.bind(this);
|
||||
|
||||
this.participants = {};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -83,7 +76,9 @@ class VCPanel extends React.PureComponent {
|
||||
|
||||
componentWillUnmount() {
|
||||
const topic = this.props.tinode.getTopic(this.props.topic);
|
||||
topic.onInfo = this.previousOnInfo;
|
||||
if (topic) {
|
||||
topic.onInfo = this.previousOnInfo;
|
||||
}
|
||||
this.stop();
|
||||
}
|
||||
|
||||
@@ -128,23 +123,21 @@ class VCPanel extends React.PureComponent {
|
||||
|
||||
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,
|
||||
|
||||
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.participants[participant.identity] = p
|
||||
console.log('participant connected', this.participants);
|
||||
this.setState({participants: this.participants}, _ => { this.forceUpdate(); });
|
||||
this.setState({participants: participants});
|
||||
}
|
||||
}
|
||||
|
||||
handleParticipantDisconnected(participant) {
|
||||
console.log('Deleting participant ', participant.identity);
|
||||
delete this.participants[participant.identity];
|
||||
this.setState({participants: this.participants});
|
||||
const participants = Object.assign({}, this.state.participants);
|
||||
delete participants[participant.identity];
|
||||
this.setState({participants: participants});
|
||||
}
|
||||
|
||||
handleData(msg, participant) {
|
||||
@@ -364,58 +357,54 @@ class VCPanel extends React.PureComponent {
|
||||
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 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> :
|
||||
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 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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user