feat(local-video): convert to react (#1705)

* feat(local-video): convert to react

- Create a VideoTrack component for displaying a video element.
  This mirrors native also having a VideoTrack component.
- The VideoTrack component does not let React update it to prevent
  the video element from re-rendering, which could cause flickers
  and would not work with temasys's overriding of the video element.
- VideoTrack extends AbstractVideoTrack to mirror native
  implementation and to get the dispatch of the onplaying event.
- Remove the onclick handler on the video element. Honestly, I
  didn't get it to work, and did not try, but it is also unnecessary
  because another handler already exists on the video wrapper.

* ref(device-selection): VideoInputPreview uses VideoTrack to show video

* squash into conversion: change css selectors

* squash into conversion: mix in abstract props

* squash into conversion: change shouldComponentUpdate check

* squash: update comment about why triggerOnPlayingUpdate is used
This commit is contained in:
virtuacoplenny 2017-07-14 12:22:27 -07:00 committed by yanas
parent 37328b3995
commit 244de8096f
6 changed files with 274 additions and 234 deletions

View File

@ -157,8 +157,8 @@
-o-transform: scale(-1, 1); -o-transform: scale(-1, 1);
} }
#localVideoWrapper>video, #localVideoWrapper video,
#localVideoWrapper>object { #localVideoWrapper object {
border-radius: $borderRadius !important; border-radius: $borderRadius !important;
cursor: hand; cursor: hand;
object-fit: cover; object-fit: cover;
@ -183,8 +183,8 @@
#sharedVideo, #sharedVideo,
#etherpad, #etherpad,
#localVideoWrapper>video, #localVideoWrapper video,
#localVideoWrapper>object, #localVideoWrapper object,
#localVideoWrapper, #localVideoWrapper,
#largeVideoWrapper, #largeVideoWrapper,
#largeVideoWrapper>video, #largeVideoWrapper>video,

View File

@ -1,11 +1,18 @@
/* global $, config, interfaceConfig, APP, JitsiMeetJS */ /* global $, config, interfaceConfig, APP, JitsiMeetJS */
/* eslint-disable no-unused-vars */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { VideoTrack } from '../../../react/features/base/media';
/* eslint-enable no-unused-vars */
const logger = require("jitsi-meet-logger").getLogger(__filename); const logger = require("jitsi-meet-logger").getLogger(__filename);
import UIUtil from "../util/UIUtil";
import UIEvents from "../../../service/UI/UIEvents"; import UIEvents from "../../../service/UI/UIEvents";
import SmallVideo from "./SmallVideo"; import SmallVideo from "./SmallVideo";
const RTCUIUtils = JitsiMeetJS.util.RTCUIHelper;
const TrackEvents = JitsiMeetJS.events.track; const TrackEvents = JitsiMeetJS.events.track;
function LocalVideo(VideoLayout, emitter) { function LocalVideo(VideoLayout, emitter) {
@ -85,31 +92,34 @@ LocalVideo.prototype.changeVideo = function (stream) {
localVideoContainerSelector.off('click'); localVideoContainerSelector.off('click');
localVideoContainerSelector.on('click', localVideoClick); localVideoContainerSelector.on('click', localVideoClick);
let localVideo = document.createElement('video'); this.localVideoId = 'localVideo_' + stream.getId();
localVideo.id = this.localVideoId = 'localVideo_' + stream.getId();
RTCUIUtils.setAutoPlay(localVideo, true);
RTCUIUtils.setVolume(localVideo, 0);
var localVideoContainer = document.getElementById('localVideoWrapper'); var localVideoContainer = document.getElementById('localVideoWrapper');
// Put the new video always in front
UIUtil.prependChild(localVideoContainer, localVideo);
// Add click handler to both video and video wrapper elements in case /* jshint ignore:start */
// there's no video. ReactDOM.render(
<Provider store = { APP.store }>
// onclick has to be used with Temasys plugin <VideoTrack
localVideo.onclick = localVideoClick; id = { this.localVideoId }
videoTrack = {{ jitsiTrack: stream }} />
</Provider>,
localVideoContainer
);
/* jshint ignore:end */
let isVideo = stream.videoType != "desktop"; let isVideo = stream.videoType != "desktop";
this._enableDisableContextMenu(isVideo); this._enableDisableContextMenu(isVideo);
this.setFlipX(isVideo? APP.settings.getLocalFlipX() : false); this.setFlipX(isVideo? APP.settings.getLocalFlipX() : false);
// Attach WebRTC stream
localVideo = stream.attach(localVideo);
let endedHandler = () => { let endedHandler = () => {
localVideoContainer.removeChild(localVideo); // Only remove if there is no video and not a transition state.
// Previous non-react logic created a new video element with each track
// removal whereas react reuses the video component so it could be the
// stream ended but a new one is being used.
if (this.videoStream.isEnded()) {
ReactDOM.unmountComponentAtNode(localVideoContainer);
}
// when removing only the video element and we are on stage // when removing only the video element and we are on stage
// update the stage // update the stage
if (this.isCurrentlyOnLargeVideo()) { if (this.isCurrentlyOnLargeVideo()) {

View File

@ -12,6 +12,18 @@ import { Video } from './_';
* @abstract * @abstract
*/ */
export default class AbstractVideoTrack extends Component { export default class AbstractVideoTrack extends Component {
/**
* Default values for AbstractVideoTrack component's properties.
*
* @static
*/
static defaultProps = {
/**
* Dispatch an action when the video starts playing.
*/
triggerOnPlayingUpdate: true
};
/** /**
* AbstractVideoTrack component's property types. * AbstractVideoTrack component's property types.
* *
@ -19,7 +31,18 @@ export default class AbstractVideoTrack extends Component {
*/ */
static propTypes = { static propTypes = {
dispatch: React.PropTypes.func, dispatch: React.PropTypes.func,
/**
* Whether or not the store should be updated about the playing status
* of the video. Defaults to true. One use case for setting this prop
* to false is using multiple locals streams from the same video source,
* such as when previewing video. In those cases, the store may have no
* need to be updated about the existence or state of the stream.
*/
triggerOnPlayingUpdate: React.PropTypes.bool,
videoTrack: React.PropTypes.object, videoTrack: React.PropTypes.object,
waitForVideoStarted: React.PropTypes.bool, waitForVideoStarted: React.PropTypes.bool,
/** /**
@ -117,7 +140,9 @@ export default class AbstractVideoTrack extends Component {
_onVideoPlaying() { _onVideoPlaying() {
const videoTrack = this.props.videoTrack; const videoTrack = this.props.videoTrack;
if (videoTrack && !videoTrack.videoStarted) { if (this.props.triggerOnPlayingUpdate
&& videoTrack
&& !videoTrack.videoStarted) {
this.props.dispatch(trackVideoStarted(videoTrack.jitsiTrack)); this.props.dispatch(trackVideoStarted(videoTrack.jitsiTrack));
} }
} }

View File

@ -0,0 +1,202 @@
import React from 'react';
import { connect } from 'react-redux';
import AbstractVideoTrack from '../AbstractVideoTrack';
/**
* Component that renders a video element for a passed in video track.
*
* @extends AbstractVideoTrack
*/
class VideoTrack extends AbstractVideoTrack {
/**
* Default values for {@code VideoTrack} component's properties.
*
* @static
*/
static defaultProps = {
...AbstractVideoTrack.defaultProps,
className: '',
id: ''
};
/**
* {@code VideoTrack} component's property types.
*
* @static
*/
static propTypes = {
...AbstractVideoTrack.propTypes,
/**
* CSS classes to add to the video element.
*/
className: React.PropTypes.string,
/**
* The value of the id attribute of the video. Used by the torture tests
* to locate video elements.
*/
id: React.PropTypes.string
};
/**
* Initializes a new VideoTrack instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props) {
super(props);
/**
* The internal reference to the DOM/HTML element intended for
* displaying a video. This element may be an HTML video element or a
* temasys video object.
*
* @private
* @type {HTMLVideoElement|Object}
*/
this._videoElement = null;
// Bind event handlers so they are only bound once for every instance.
this._setVideoElement = this._setVideoElement.bind(this);
}
/**
* Invokes the library for rendering the video on initial display. Sets the
* volume level to zero to ensure no sound plays.
*
* @inheritdoc
* @returns {void}
*/
componentDidMount() {
// Add these attributes directly onto the video element so temasys can
// use them when converting the video to an object.
this._videoElement.volume = 0;
this._videoElement.onplaying = this._onVideoPlaying;
this._attachTrack(this.props.videoTrack);
}
/**
* Remove any existing associations between the current video track and the
* component's video element.
*
* @inheritdoc
* @returns {void}
*/
componentWillUnmount() {
this._detachTrack(this.props.videoTrack);
}
/**
* Updates the video display only if a new track is added. This component's
* updating is blackboxed from React to prevent re-rendering of video
* element, as the lib uses track.attach(videoElement) instead. Also,
* re-rendering cannot be used with temasys, which replaces video elements
* with an object.
*
* @inheritdoc
* @returns {boolean} - False is always returned to blackbox this component.
* from React.
*/
shouldComponentUpdate(nextProps) {
const currentJitsiTrack = this.props.videoTrack
&& this.props.videoTrack.jitsiTrack;
const nextJitsiTrack = nextProps.videoTrack
&& nextProps.videoTrack.jitsiTrack;
if (currentJitsiTrack !== nextJitsiTrack) {
this._detachTrack(this.props.videoTrack);
this._attachTrack(nextProps.videoTrack);
}
return false;
}
/**
* Renders the video element.
*
* @override
* @returns {ReactElement}
*/
render() {
// The wrapping div is necessary because temasys will replace the video
// with an object but react will keep expecting the video element. The
// div gives a constant element for react to keep track of.
return (
<div>
<video
autoPlay = { true }
className = { this.props.className }
id = { this.props.id }
ref = { this._setVideoElement } />
</div>
);
}
/**
* Calls into the passed in track to associate the track with the
* component's video element and render video.
*
* @param {Object} videoTrack - The redux representation of the
* {@code JitsiLocalTrack}.
* @private
* @returns {void}
*/
_attachTrack(videoTrack) {
if (!videoTrack || !videoTrack.jitsiTrack) {
return;
}
const updatedVideoElement
= videoTrack.jitsiTrack.attach(this._videoElement);
// Sets the instance variable for the video element again as the element
// maybe have been replaced with a new object by temasys.
this._setVideoElement(updatedVideoElement);
}
/**
* Removes the association to the component's video element from the passed
* in redux representation of jitsi video track to stop the track from
* rendering. With temasys, the video element must still be visible for
* detaching to complete.
*
* @param {Object} videoTrack - The redux representation of the
* {@code JitsiLocalTrack}.
* @private
* @returns {void}
*/
_detachTrack(videoTrack) {
// Detach the video element from the track only if it has already
// been attached. This accounts for a special case with temasys
// where if detach is being called before attach, the video
// element is converted to Object without updating this
// component's reference to the video element.
if (this._videoElement
&& videoTrack
&& videoTrack.jitsiTrack
&& videoTrack.jitsiTrack.containers.includes(this._videoElement)) {
videoTrack.jitsiTrack.detach(this._videoElement);
}
}
/**
* Sets an instance variable for the component's video element so it can be
* referenced later for attaching and detaching a JitsiLocalTrack.
*
* @param {Object} element - DOM element for the component's video display.
* @private
* @returns {void}
*/
_setVideoElement(element) {
this._videoElement = element;
}
}
export default connect()(VideoTrack);

View File

@ -1 +1,2 @@
export { default as Audio } from './Audio'; export { default as Audio } from './Audio';
export { default as VideoTrack } from './VideoTrack';

View File

@ -1,6 +1,6 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { translate } from '../../base/i18n'; import { VideoTrack } from '../../base/media';
const VIDEO_ERROR_CLASS = 'video-preview-has-error'; const VIDEO_ERROR_CLASS = 'video-preview-has-error';
@ -23,86 +23,12 @@ class VideoInputPreview extends Component {
*/ */
error: React.PropTypes.string, error: React.PropTypes.string,
/**
* Invoked to obtain translated strings.
*/
t: React.PropTypes.func,
/** /**
* The JitsiLocalTrack to display. * The JitsiLocalTrack to display.
*/ */
track: React.PropTypes.object track: React.PropTypes.object
}; };
/**
* Initializes a new VideoInputPreview instance.
*
* @param {Object} props - The read-only React Component props with which
* the new instance is to be initialized.
*/
constructor(props) {
super(props);
/**
* The internal reference to the DOM/HTML element intended for showing
* error messages.
*
* @private
* @type {HTMLDivElement}
*/
this._errorElement = null;
/**
* The internal reference to topmost DOM/HTML element backing the React
* {@code Component}. Accessed directly for toggling a classname to
* indicate an error is present so styling can be changed to display it.
*
* @private
* @type {HTMLDivElement}
*/
this._rootElement = null;
/**
* The internal reference to the DOM/HTML element intended for
* displaying a video. This element may be an HTML video element or a
* temasys video object.
*
* @private
* @type {HTMLVideoElement|Object}
*/
this._videoElement = null;
// Bind event handlers so they are only bound once for every instance.
this._setErrorElement = this._setErrorElement.bind(this);
this._setRootElement = this._setRootElement.bind(this);
this._setVideoElement = this._setVideoElement.bind(this);
}
/**
* Invokes the library for rendering the video on initial display.
*
* @inheritdoc
* @returns {void}
*/
componentDidMount() {
if (this.props.error) {
this._updateErrorView(this.props.error);
} else {
this._attachTrack(this.props.track);
}
}
/**
* Remove any existing associations between the current previewed track and
* the component's video element.
*
* @inheritdoc
* @returns {void}
*/
componentWillUnmount() {
this._detachTrack(this.props.track);
}
/** /**
* Implements React's {@link Component#render()}. * Implements React's {@link Component#render()}.
* *
@ -110,146 +36,22 @@ class VideoInputPreview extends Component {
* @returns {ReactElement} * @returns {ReactElement}
*/ */
render() { render() {
const { error } = this.props;
const errorClass = error ? VIDEO_ERROR_CLASS : '';
const className = `video-input-preview ${errorClass}`;
return ( return (
<div <div className = { className }>
className = 'video-input-preview' <VideoTrack
ref = { this._setRootElement }>
<video
autoPlay = { true }
className = 'video-input-preview-display flipVideoX' className = 'video-input-preview-display flipVideoX'
ref = { this._setVideoElement } /> triggerOnPlayingUpdate = { false }
<div videoTrack = {{ jitsiTrack: this.props.track }} />
className = 'video-input-preview-error' <div className = 'video-input-preview-error'>
ref = { this._setErrorElement } /> { error || '' }
</div>
</div> </div>
); );
} }
/**
* Only update when the deviceId has changed. This component is somewhat
* black-boxed from React's rendering so lib-jitsi-meet can instead handle
* updating of the video preview, which takes browser differences into
* consideration. For example, temasys's video object must be visible to
* update the displayed track, but React's re-rendering could potentially
* remove the video object from the page.
*
* @inheritdoc
* @returns {void}
*/
shouldComponentUpdate(nextProps) {
const hasNewTrack = nextProps.track !== this.props.track;
if (hasNewTrack || nextProps.error) {
this._detachTrack(this.props.track);
this._updateErrorView(nextProps.error);
}
// Never attempt to show the new track if there is an error present.
if (hasNewTrack && !nextProps.error) {
this._attachTrack(nextProps.track);
}
return false;
}
/**
* Calls into the passed in track to associate the track with the
* component's video element and render video. Also sets the instance
* variable for the video element as the element the track attached to,
* which could be an Object if on a temasys supported browser.
*
* @param {JitsiLocalTrack} track - The library's track model which will be
* displayed.
* @private
* @returns {void}
*/
_attachTrack(track) {
if (!track) {
return;
}
const updatedVideoElement = track.attach(this._videoElement);
this._setVideoElement(updatedVideoElement);
}
/**
* Removes the association to the component's video element from the passed
* in JitsiLocalTrack to stop the track from rendering. With temasys, the
* video element must still be visible for detaching to complete.
*
* @param {JitsiLocalTrack} track - The library's track model which needs
* to stop previewing in the video element.
* @private
* @returns {void}
*/
_detachTrack(track) {
// Detach the video element from the track only if it has already
// been attached. This accounts for a special case with temasys
// where if detach is being called before attach, the video
// element is converted to Object without updating this
// component's reference to the video element.
if (this._videoElement
&& track
&& track.containers.includes(this._videoElement)) {
track.detach(this._videoElement);
}
}
/**
* Sets an instance variable for the component's element intended for
* displaying error messages. The element will be accessed directly to
* display an error message.
*
* @param {Object} element - DOM element intended for displaying errors.
* @private
* @returns {void}
*/
_setErrorElement(element) {
this._errorElement = element;
}
/**
* Sets the component's root element.
*
* @param {Object} element - The highest DOM element in the component.
* @private
* @returns {void}
*/
_setRootElement(element) {
this._rootElement = element;
}
/**
* Sets an instance variable for the component's video element so it can be
* referenced later for attaching and detaching a JitsiLocalTrack.
*
* @param {Object} element - DOM element for the component's video display.
* @private
* @returns {void}
*/
_setVideoElement(element) {
this._videoElement = element;
}
/**
* Adds or removes a class to the component's parent node to indicate an
* error has occurred. Also sets the error text.
*
* @param {string} error - The error message to display. If falsy, error
* message display will be hidden.
* @private
* @returns {void}
*/
_updateErrorView(error) {
if (error) {
this._rootElement.classList.add(VIDEO_ERROR_CLASS);
} else {
this._rootElement.classList.remove(VIDEO_ERROR_CLASS);
}
this._errorElement.innerText = error || '';
}
} }
export default translate(VideoInputPreview); export default VideoInputPreview;