2017-04-03 16:53:04 +00:00
|
|
|
/* global $, APP, interfaceConfig, JitsiMeetJS */
|
2017-05-31 15:42:50 +00:00
|
|
|
|
|
|
|
/* eslint-disable no-unused-vars */
|
|
|
|
import React from 'react';
|
2017-06-19 16:01:44 +00:00
|
|
|
import ReactDOM from 'react-dom';
|
2017-05-31 15:42:50 +00:00
|
|
|
|
|
|
|
import {
|
|
|
|
MuteButton,
|
|
|
|
KickButton,
|
|
|
|
REMOTE_CONTROL_MENU_STATES,
|
|
|
|
RemoteControlButton,
|
|
|
|
RemoteVideoMenu,
|
|
|
|
VolumeSlider
|
|
|
|
} from '../../../react/features/remote-video-menu';
|
|
|
|
/* eslint-enable no-unused-vars */
|
|
|
|
|
2016-11-11 15:00:54 +00:00
|
|
|
const logger = require("jitsi-meet-logger").getLogger(__filename);
|
2015-12-14 12:26:50 +00:00
|
|
|
|
|
|
|
|
|
|
|
import SmallVideo from "./SmallVideo";
|
|
|
|
import UIUtils from "../util/UIUtil";
|
|
|
|
import UIEvents from '../../../service/UI/UIEvents';
|
2016-08-04 20:23:38 +00:00
|
|
|
import JitsiPopover from "../util/JitsiPopover";
|
2015-12-14 12:26:50 +00:00
|
|
|
|
2016-10-26 19:14:38 +00:00
|
|
|
const MUTED_DIALOG_BUTTON_VALUES = {
|
|
|
|
cancel: 0,
|
|
|
|
muted: 1
|
|
|
|
};
|
2017-04-03 16:53:04 +00:00
|
|
|
const ParticipantConnectionStatus
|
|
|
|
= JitsiMeetJS.constants.participantConnectionStatus;
|
2016-10-26 19:14:38 +00:00
|
|
|
|
2016-09-16 20:17:00 +00:00
|
|
|
/**
|
|
|
|
* Creates new instance of the <tt>RemoteVideo</tt>.
|
|
|
|
* @param user {JitsiParticipant} the user for whom remote video instance will
|
|
|
|
* be created.
|
|
|
|
* @param {VideoLayout} VideoLayout the video layout instance.
|
|
|
|
* @param {EventEmitter} emitter the event emitter which will be used by
|
|
|
|
* the new instance to emit events.
|
|
|
|
* @constructor
|
|
|
|
*/
|
|
|
|
function RemoteVideo(user, VideoLayout, emitter) {
|
|
|
|
this.user = user;
|
|
|
|
this.id = user.getId();
|
2015-12-14 12:26:50 +00:00
|
|
|
this.emitter = emitter;
|
2016-09-16 20:17:00 +00:00
|
|
|
this.videoSpanId = `participant_${this.id}`;
|
2016-03-15 20:42:53 +00:00
|
|
|
SmallVideo.call(this, VideoLayout);
|
2017-02-12 06:02:16 +00:00
|
|
|
this._audioStreamElement = null;
|
2016-08-23 23:37:41 +00:00
|
|
|
this.hasRemoteVideoMenu = false;
|
2017-01-06 01:18:07 +00:00
|
|
|
this._supportsRemoteControl = false;
|
2015-06-23 08:00:46 +00:00
|
|
|
this.addRemoteVideoContainer();
|
2017-07-05 18:17:30 +00:00
|
|
|
this.updateIndicators();
|
2015-06-23 08:00:46 +00:00
|
|
|
this.setDisplayName();
|
2016-10-26 20:45:51 +00:00
|
|
|
this.bindHoverHandler();
|
2015-06-23 08:00:46 +00:00
|
|
|
this.flipX = false;
|
2015-07-15 10:14:34 +00:00
|
|
|
this.isLocal = false;
|
2016-10-28 16:16:40 +00:00
|
|
|
this.popupMenuIsHovered = false;
|
2016-09-22 20:57:43 +00:00
|
|
|
/**
|
|
|
|
* The flag is set to <tt>true</tt> after the 'onplay' event has been
|
|
|
|
* triggered on the current video element. It goes back to <tt>false</tt>
|
|
|
|
* when the stream is removed. It is used to determine whether the video
|
|
|
|
* playback has ever started.
|
|
|
|
* @type {boolean}
|
|
|
|
*/
|
|
|
|
this.wasVideoPlayed = false;
|
2016-09-22 21:21:01 +00:00
|
|
|
/**
|
|
|
|
* The flag is set to <tt>true</tt> if remote participant's video gets muted
|
|
|
|
* during his media connection disruption. This is to prevent black video
|
|
|
|
* being render on the thumbnail, because even though once the video has
|
|
|
|
* been played the image usually remains on the video element it seems that
|
|
|
|
* after longer period of the video element being hidden this image can be
|
|
|
|
* lost.
|
|
|
|
* @type {boolean}
|
|
|
|
*/
|
|
|
|
this.mutedWhileDisconnected = false;
|
2017-05-31 15:42:50 +00:00
|
|
|
|
|
|
|
// Bind event handlers so they are only bound once for every instance.
|
|
|
|
// TODO The event handlers should be turned into actions so changes can be
|
|
|
|
// handled through reducers and middleware.
|
|
|
|
this._kickHandler = this._kickHandler.bind(this);
|
|
|
|
this._muteHandler = this._muteHandler.bind(this);
|
|
|
|
this._requestRemoteControlPermissions
|
|
|
|
= this._requestRemoteControlPermissions.bind(this);
|
|
|
|
this._setAudioVolume = this._setAudioVolume.bind(this);
|
|
|
|
this._stopRemoteControl = this._stopRemoteControl.bind(this);
|
2015-06-23 08:00:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
RemoteVideo.prototype = Object.create(SmallVideo.prototype);
|
|
|
|
RemoteVideo.prototype.constructor = RemoteVideo;
|
|
|
|
|
|
|
|
RemoteVideo.prototype.addRemoteVideoContainer = function() {
|
|
|
|
this.container = RemoteVideo.createContainer(this.videoSpanId);
|
2016-08-08 22:03:00 +00:00
|
|
|
|
|
|
|
this.initBrowserSpecificProperties();
|
|
|
|
|
2017-01-06 01:18:07 +00:00
|
|
|
if (APP.conference.isModerator || this._supportsRemoteControl) {
|
2015-06-23 08:00:46 +00:00
|
|
|
this.addRemoteVideoMenu();
|
2015-12-14 12:26:50 +00:00
|
|
|
}
|
2016-09-15 02:20:54 +00:00
|
|
|
|
2016-10-03 16:12:04 +00:00
|
|
|
this.VideoLayout.resizeThumbnails(false, true);
|
2016-09-28 21:31:40 +00:00
|
|
|
|
|
|
|
this.addAudioLevelIndicator();
|
2015-06-23 08:00:46 +00:00
|
|
|
|
|
|
|
return this.container;
|
|
|
|
};
|
|
|
|
|
2016-08-04 20:23:38 +00:00
|
|
|
/**
|
|
|
|
* Initializes the remote participant popup menu, by specifying previously
|
|
|
|
* constructed popupMenuElement, containing all the menu items.
|
|
|
|
*
|
|
|
|
* @param popupMenuElement a pre-constructed element, containing the menu items
|
|
|
|
* to display in the popup
|
|
|
|
*/
|
|
|
|
RemoteVideo.prototype._initPopupMenu = function (popupMenuElement) {
|
2016-10-20 13:18:20 +00:00
|
|
|
let options = {
|
|
|
|
content: popupMenuElement.outerHTML,
|
|
|
|
skin: "black",
|
2016-10-25 22:57:29 +00:00
|
|
|
hasArrow: false,
|
2017-05-17 22:40:41 +00:00
|
|
|
position: interfaceConfig.VERTICAL_FILMSTRIP ? 'left' : 'top'
|
2016-10-20 13:18:20 +00:00
|
|
|
};
|
|
|
|
let element = $("#" + this.videoSpanId + " .remotevideomenu");
|
|
|
|
this.popover = new JitsiPopover(element, options);
|
2016-10-28 16:16:40 +00:00
|
|
|
this.popover.addOnHoverPopover(isHovered => {
|
|
|
|
this.popupMenuIsHovered = isHovered;
|
|
|
|
this.updateView();
|
|
|
|
});
|
2016-08-04 20:23:38 +00:00
|
|
|
|
|
|
|
// override popover show method to make sure we will update the content
|
|
|
|
// before showing the popover
|
2016-10-20 13:18:20 +00:00
|
|
|
let origShowFunc = this.popover.show;
|
2016-08-04 20:23:38 +00:00
|
|
|
this.popover.show = function () {
|
|
|
|
// update content by forcing it, to finish even if popover
|
|
|
|
// is not visible
|
2016-09-21 18:10:11 +00:00
|
|
|
this.updateRemoteVideoMenu(this.isAudioMuted, true);
|
2016-08-04 20:23:38 +00:00
|
|
|
// call the original show, passing its actual this
|
|
|
|
origShowFunc.call(this.popover);
|
|
|
|
}.bind(this);
|
|
|
|
};
|
|
|
|
|
2016-10-28 16:16:40 +00:00
|
|
|
/**
|
|
|
|
* Checks whether current video is considered hovered. Currently it is hovered
|
|
|
|
* if the mouse is over the video, or if the connection indicator or the popup
|
|
|
|
* menu is shown(hovered).
|
|
|
|
* @private
|
|
|
|
* NOTE: extends SmallVideo's method
|
|
|
|
*/
|
|
|
|
RemoteVideo.prototype._isHovered = function () {
|
|
|
|
let isHovered = SmallVideo.prototype._isHovered.call(this)
|
|
|
|
|| this.popupMenuIsHovered;
|
|
|
|
return isHovered;
|
|
|
|
};
|
|
|
|
|
2016-08-04 20:23:38 +00:00
|
|
|
/**
|
|
|
|
* Generates the popup menu content.
|
|
|
|
*
|
|
|
|
* @returns {Element|*} the constructed element, containing popup menu items
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
RemoteVideo.prototype._generatePopupContent = function () {
|
2017-05-31 15:42:50 +00:00
|
|
|
const { controller } = APP.remoteControl;
|
|
|
|
let remoteControlState = null;
|
|
|
|
let onRemoteControlToggle;
|
|
|
|
|
|
|
|
if (this._supportsRemoteControl) {
|
|
|
|
if (controller.getRequestedParticipant() === this.id) {
|
|
|
|
onRemoteControlToggle = () => {};
|
|
|
|
remoteControlState = REMOTE_CONTROL_MENU_STATES.REQUESTING;
|
|
|
|
} else if (!controller.isStarted()) {
|
|
|
|
onRemoteControlToggle = this._requestRemoteControlPermissions;
|
|
|
|
remoteControlState = REMOTE_CONTROL_MENU_STATES.NOT_STARTED;
|
2017-01-06 01:18:07 +00:00
|
|
|
} else {
|
2017-05-31 15:42:50 +00:00
|
|
|
onRemoteControlToggle = this._stopRemoteControl;
|
|
|
|
remoteControlState = REMOTE_CONTROL_MENU_STATES.STARTED;
|
2017-01-06 01:18:07 +00:00
|
|
|
}
|
|
|
|
}
|
2016-08-04 20:23:38 +00:00
|
|
|
|
2017-05-31 15:42:50 +00:00
|
|
|
let initialVolumeValue, onVolumeChange;
|
2016-08-04 20:23:38 +00:00
|
|
|
|
2017-05-31 15:42:50 +00:00
|
|
|
// Feature check for volume setting as temasys objects cannot adjust volume.
|
2017-02-12 06:02:16 +00:00
|
|
|
if (this._canSetAudioVolume()) {
|
2017-05-31 15:42:50 +00:00
|
|
|
initialVolumeValue = this._getAudioElement().volume;
|
|
|
|
onVolumeChange = this._setAudioVolume;
|
2017-02-12 06:02:16 +00:00
|
|
|
}
|
|
|
|
|
2017-05-31 15:42:50 +00:00
|
|
|
const { isModerator } = APP.conference;
|
|
|
|
const participantID = this.id;
|
|
|
|
|
|
|
|
/* jshint ignore:start */
|
|
|
|
return (
|
|
|
|
<RemoteVideoMenu id = { participantID }>
|
|
|
|
{ isModerator
|
|
|
|
? <MuteButton
|
|
|
|
isAudioMuted = { this.isAudioMuted }
|
|
|
|
onClick = { this._muteHandler }
|
|
|
|
participantID = { participantID } />
|
|
|
|
: null }
|
|
|
|
{ isModerator
|
|
|
|
? <KickButton
|
|
|
|
onClick = { this._kickHandler }
|
|
|
|
participantID = { participantID } />
|
|
|
|
: null }
|
|
|
|
{ remoteControlState
|
|
|
|
? <RemoteControlButton
|
|
|
|
onClick = { onRemoteControlToggle }
|
|
|
|
participantID = { participantID }
|
|
|
|
remoteControlState = { remoteControlState } />
|
|
|
|
: null }
|
|
|
|
{ onVolumeChange
|
|
|
|
? <VolumeSlider
|
|
|
|
initialValue = { initialVolumeValue }
|
|
|
|
onChange = { onVolumeChange } />
|
|
|
|
: null }
|
|
|
|
</RemoteVideoMenu>
|
|
|
|
);
|
|
|
|
/* jshint ignore:end */
|
2016-10-27 13:09:27 +00:00
|
|
|
};
|
2016-08-04 20:23:38 +00:00
|
|
|
|
2017-01-06 01:18:07 +00:00
|
|
|
/**
|
|
|
|
* Sets the remote control supported value and initializes or updates the menu
|
|
|
|
* depending on the remote control is supported or not.
|
|
|
|
* @param {boolean} isSupported
|
|
|
|
*/
|
|
|
|
RemoteVideo.prototype.setRemoteControlSupport = function(isSupported = false) {
|
|
|
|
if(this._supportsRemoteControl === isSupported) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this._supportsRemoteControl = isSupported;
|
|
|
|
if(!isSupported) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(!this.hasRemoteVideoMenu) {
|
|
|
|
//create menu
|
|
|
|
this.addRemoteVideoMenu();
|
|
|
|
} else {
|
|
|
|
//update the content
|
|
|
|
this.updateRemoteVideoMenu(this.isAudioMuted, true);
|
|
|
|
}
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Requests permissions for remote control session.
|
|
|
|
*/
|
|
|
|
RemoteVideo.prototype._requestRemoteControlPermissions = function () {
|
2017-01-20 20:26:25 +00:00
|
|
|
APP.remoteControl.controller.requestPermissions(
|
|
|
|
this.id, this.VideoLayout.getLargeVideoWrapper()).then(result => {
|
2017-01-06 01:18:07 +00:00
|
|
|
if(result === null) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.updateRemoteVideoMenu(this.isAudioMuted, true);
|
2017-07-10 03:01:48 +00:00
|
|
|
APP.UI.messageHandler.notify(
|
2017-01-06 01:18:07 +00:00
|
|
|
"dialog.remoteControlTitle",
|
|
|
|
(result === false) ? "dialog.remoteControlDeniedMessage"
|
|
|
|
: "dialog.remoteControlAllowedMessage",
|
|
|
|
{user: this.user.getDisplayName()
|
|
|
|
|| interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME}
|
|
|
|
);
|
2017-01-20 20:32:30 +00:00
|
|
|
if(result === true) {//the remote control permissions has been granted
|
|
|
|
// pin the controlled participant
|
|
|
|
let pinnedId = this.VideoLayout.getPinnedId();
|
|
|
|
if(pinnedId !== this.id) {
|
|
|
|
this.VideoLayout.handleVideoThumbClicked(this.id);
|
|
|
|
}
|
2017-01-10 18:51:25 +00:00
|
|
|
}
|
2017-01-06 01:18:07 +00:00
|
|
|
}, error => {
|
|
|
|
logger.error(error);
|
|
|
|
this.updateRemoteVideoMenu(this.isAudioMuted, true);
|
2017-07-10 03:01:48 +00:00
|
|
|
APP.UI.messageHandler.notify(
|
2017-01-06 01:18:07 +00:00
|
|
|
"dialog.remoteControlTitle",
|
|
|
|
"dialog.remoteControlErrorMessage",
|
|
|
|
{user: this.user.getDisplayName()
|
|
|
|
|| interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME}
|
|
|
|
);
|
|
|
|
});
|
|
|
|
this.updateRemoteVideoMenu(this.isAudioMuted, true);
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Stops remote control session.
|
|
|
|
*/
|
|
|
|
RemoteVideo.prototype._stopRemoteControl = function () {
|
|
|
|
// send message about stopping
|
|
|
|
APP.remoteControl.controller.stop();
|
|
|
|
this.updateRemoteVideoMenu(this.isAudioMuted, true);
|
|
|
|
};
|
|
|
|
|
2016-10-27 13:09:27 +00:00
|
|
|
RemoteVideo.prototype._muteHandler = function () {
|
|
|
|
if (this.isAudioMuted)
|
|
|
|
return;
|
|
|
|
|
|
|
|
RemoteVideo.showMuteParticipantDialog().then(reason => {
|
|
|
|
if(reason === MUTED_DIALOG_BUTTON_VALUES.muted) {
|
|
|
|
this.emitter.emit(UIEvents.REMOTE_AUDIO_MUTED, this.id);
|
|
|
|
}
|
|
|
|
}).catch(e => {
|
|
|
|
//currently shouldn't be called
|
2016-11-11 15:00:54 +00:00
|
|
|
logger.error(e);
|
2016-10-27 13:09:27 +00:00
|
|
|
});
|
2016-08-04 20:23:38 +00:00
|
|
|
|
2016-10-27 13:09:27 +00:00
|
|
|
this.popover.forceHide();
|
|
|
|
};
|
2016-08-04 20:23:38 +00:00
|
|
|
|
2016-10-27 13:09:27 +00:00
|
|
|
RemoteVideo.prototype._kickHandler = function () {
|
|
|
|
this.emitter.emit(UIEvents.USER_KICKED, this.id);
|
|
|
|
this.popover.forceHide();
|
|
|
|
};
|
2016-08-04 20:23:38 +00:00
|
|
|
|
2017-02-12 06:02:16 +00:00
|
|
|
/**
|
|
|
|
* Get the remote participant's audio element.
|
|
|
|
*
|
|
|
|
* @returns {Element} audio element
|
|
|
|
*/
|
|
|
|
RemoteVideo.prototype._getAudioElement = function () {
|
|
|
|
return this._audioStreamElement;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if the remote participant's audio can have its volume adjusted.
|
|
|
|
*
|
|
|
|
* @returns {boolean} true if the volume can be adjusted.
|
|
|
|
*/
|
|
|
|
RemoteVideo.prototype._canSetAudioVolume = function () {
|
|
|
|
const audioElement = this._getAudioElement();
|
|
|
|
return audioElement && audioElement.volume !== undefined;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Change the remote participant's volume level.
|
|
|
|
*
|
|
|
|
* @param {int} newVal - The value to set the slider to.
|
|
|
|
*/
|
2017-05-31 15:42:50 +00:00
|
|
|
RemoteVideo.prototype._setAudioVolume = function (newVal) {
|
2017-02-12 06:02:16 +00:00
|
|
|
if (this._canSetAudioVolume()) {
|
2017-05-31 15:42:50 +00:00
|
|
|
this._getAudioElement().volume = newVal;
|
2017-02-12 06:02:16 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2016-08-04 20:23:38 +00:00
|
|
|
/**
|
|
|
|
* Updates the remote video menu.
|
|
|
|
*
|
|
|
|
* @param isMuted the new muted state to update to
|
|
|
|
* @param force to work even if popover is not visible
|
|
|
|
*/
|
|
|
|
RemoteVideo.prototype.updateRemoteVideoMenu = function (isMuted, force) {
|
2016-09-21 18:10:11 +00:00
|
|
|
this.isAudioMuted = isMuted;
|
2016-08-04 20:23:38 +00:00
|
|
|
|
2017-05-31 16:31:26 +00:00
|
|
|
if (!this.popover) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2016-08-04 20:23:38 +00:00
|
|
|
// generate content, translate it and add it to document only if
|
|
|
|
// popover is visible or we force to do so.
|
|
|
|
if(this.popover.popoverShown || force) {
|
|
|
|
this.popover.updateContent(this._generatePopupContent());
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2016-09-22 21:21:01 +00:00
|
|
|
/**
|
|
|
|
* @inheritDoc
|
2017-05-25 13:48:06 +00:00
|
|
|
* @override
|
2016-09-22 21:21:01 +00:00
|
|
|
*/
|
2017-05-25 13:48:06 +00:00
|
|
|
RemoteVideo.prototype.setVideoMutedView = function(isMuted) {
|
|
|
|
SmallVideo.prototype.setVideoMutedView.call(this, isMuted);
|
2016-09-22 21:21:01 +00:00
|
|
|
// Update 'mutedWhileDisconnected' flag
|
2017-05-25 13:48:06 +00:00
|
|
|
this._figureOutMutedWhileDisconnected();
|
2016-09-28 16:29:47 +00:00
|
|
|
};
|
2016-09-22 21:21:01 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Figures out the value of {@link #mutedWhileDisconnected} flag by taking into
|
|
|
|
* account remote participant's network connectivity and video muted status.
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
*/
|
2017-05-25 13:48:06 +00:00
|
|
|
RemoteVideo.prototype._figureOutMutedWhileDisconnected = function() {
|
|
|
|
const isActive = this.isConnectionActive();
|
|
|
|
if (!isActive && this.isVideoMuted) {
|
|
|
|
this.mutedWhileDisconnected = true;
|
|
|
|
} else if (isActive && !this.isVideoMuted) {
|
|
|
|
this.mutedWhileDisconnected = false;
|
|
|
|
}
|
2016-09-28 16:29:47 +00:00
|
|
|
};
|
2016-09-22 21:21:01 +00:00
|
|
|
|
2015-06-23 08:00:46 +00:00
|
|
|
/**
|
2015-12-14 12:26:50 +00:00
|
|
|
* Adds the remote video menu element for the given <tt>id</tt> in the
|
2015-06-23 08:00:46 +00:00
|
|
|
* given <tt>parentElement</tt>.
|
|
|
|
*
|
2015-12-14 12:26:50 +00:00
|
|
|
* @param id the id indicating the video for which we're adding a menu.
|
2015-06-23 08:00:46 +00:00
|
|
|
* @param parentElement the parent element where this menu will be added
|
|
|
|
*/
|
2017-01-12 20:51:53 +00:00
|
|
|
RemoteVideo.prototype.addRemoteVideoMenu = function () {
|
|
|
|
if (interfaceConfig.filmStripOnly) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
var spanElement = document.createElement('span');
|
|
|
|
spanElement.className = 'remotevideomenu';
|
2016-09-18 21:35:35 +00:00
|
|
|
|
2017-01-12 20:51:53 +00:00
|
|
|
this.container.appendChild(spanElement);
|
2015-06-23 08:00:46 +00:00
|
|
|
|
2017-01-12 20:51:53 +00:00
|
|
|
var menuElement = document.createElement('i');
|
2017-07-12 22:12:35 +00:00
|
|
|
menuElement.className = 'icon-thumb-menu';
|
2017-01-12 20:51:53 +00:00
|
|
|
menuElement.title = 'Remote user controls';
|
|
|
|
spanElement.appendChild(menuElement);
|
2015-06-23 08:00:46 +00:00
|
|
|
|
2017-01-12 20:51:53 +00:00
|
|
|
this._initPopupMenu(this._generatePopupContent());
|
|
|
|
this.hasRemoteVideoMenu = true;
|
|
|
|
};
|
2015-06-23 08:00:46 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Removes the remote stream element corresponding to the given stream and
|
|
|
|
* parent container.
|
|
|
|
*
|
2016-01-29 00:33:27 +00:00
|
|
|
* @param stream the MediaStream
|
2015-06-23 08:00:46 +00:00
|
|
|
* @param isVideo <tt>true</tt> if given <tt>stream</tt> is a video one.
|
|
|
|
*/
|
2016-02-23 22:47:55 +00:00
|
|
|
RemoteVideo.prototype.removeRemoteStreamElement = function (stream) {
|
2015-06-23 08:00:46 +00:00
|
|
|
if (!this.container)
|
|
|
|
return false;
|
|
|
|
|
2016-02-23 22:47:55 +00:00
|
|
|
var isVideo = stream.isVideoTrack();
|
|
|
|
|
|
|
|
var elementID = SmallVideo.getStreamElementID(stream);
|
2016-07-15 18:12:14 +00:00
|
|
|
var select = $('#' + elementID);
|
2015-06-23 08:00:46 +00:00
|
|
|
select.remove();
|
|
|
|
|
2016-09-22 20:57:43 +00:00
|
|
|
if (isVideo) {
|
|
|
|
this.wasVideoPlayed = false;
|
|
|
|
}
|
|
|
|
|
2016-11-11 15:00:54 +00:00
|
|
|
logger.info((isVideo ? "Video" : "Audio") +
|
2015-12-14 12:26:50 +00:00
|
|
|
" removed " + this.id, select);
|
2015-07-30 08:23:21 +00:00
|
|
|
|
2016-03-23 22:45:27 +00:00
|
|
|
// when removing only the video element and we are on stage
|
|
|
|
// update the stage
|
2017-06-15 15:01:32 +00:00
|
|
|
if (isVideo && this.isCurrentlyOnLargeVideo()) {
|
2017-07-10 10:06:48 +00:00
|
|
|
this.VideoLayout.updateLargeVideo(this.id);
|
2017-06-15 15:01:32 +00:00
|
|
|
} else {
|
2016-09-22 21:21:01 +00:00
|
|
|
// Missing video stream will affect display mode
|
|
|
|
this.updateView();
|
2017-06-15 15:01:32 +00:00
|
|
|
}
|
2015-06-23 08:00:46 +00:00
|
|
|
};
|
|
|
|
|
2016-09-16 20:51:19 +00:00
|
|
|
/**
|
|
|
|
* Checks whether the remote user associated with this <tt>RemoteVideo</tt>
|
|
|
|
* has connectivity issues.
|
|
|
|
*
|
|
|
|
* @return {boolean} <tt>true</tt> if the user's connection is fine or
|
|
|
|
* <tt>false</tt> otherwise.
|
|
|
|
*/
|
|
|
|
RemoteVideo.prototype.isConnectionActive = function() {
|
2017-04-03 16:53:04 +00:00
|
|
|
return this.user.getConnectionStatus()
|
|
|
|
=== ParticipantConnectionStatus.ACTIVE;
|
2016-09-16 20:51:19 +00:00
|
|
|
};
|
|
|
|
|
2016-09-19 20:59:56 +00:00
|
|
|
/**
|
|
|
|
* The remote video is considered "playable" once the stream has started
|
|
|
|
* according to the {@link #hasVideoStarted} result.
|
2017-05-30 20:05:07 +00:00
|
|
|
* It will be allowed to display video also in
|
|
|
|
* {@link ParticipantConnectionStatus.INTERRUPTED} if the video was ever played
|
|
|
|
* and was not muted while not in ACTIVE state. This basically means that there
|
|
|
|
* is stalled video image cached that could be displayed. It's used to show
|
|
|
|
* "grey video image" in user's thumbnail when there are connectivity issues.
|
2016-09-19 20:59:56 +00:00
|
|
|
*
|
|
|
|
* @inheritdoc
|
|
|
|
* @override
|
|
|
|
*/
|
|
|
|
RemoteVideo.prototype.isVideoPlayable = function () {
|
2017-05-30 20:05:07 +00:00
|
|
|
const connectionState
|
|
|
|
= APP.conference.getParticipantConnectionStatus(this.id);
|
|
|
|
|
2016-09-19 20:59:56 +00:00
|
|
|
return SmallVideo.prototype.isVideoPlayable.call(this)
|
2017-05-30 20:05:07 +00:00
|
|
|
&& this.hasVideoStarted()
|
|
|
|
&& (connectionState === ParticipantConnectionStatus.ACTIVE
|
|
|
|
|| (connectionState === ParticipantConnectionStatus.INTERRUPTED
|
|
|
|
&& !this.mutedWhileDisconnected));
|
2016-09-19 20:59:56 +00:00
|
|
|
};
|
|
|
|
|
2016-09-16 20:51:19 +00:00
|
|
|
/**
|
|
|
|
* @inheritDoc
|
|
|
|
*/
|
|
|
|
RemoteVideo.prototype.updateView = function () {
|
2017-04-05 15:14:26 +00:00
|
|
|
$(this.container).toggleClass('audio-only', APP.conference.isAudioOnly());
|
2016-09-22 21:21:01 +00:00
|
|
|
|
2017-03-21 17:14:13 +00:00
|
|
|
this.updateConnectionStatusIndicator();
|
2016-09-22 21:21:01 +00:00
|
|
|
|
|
|
|
// This must be called after 'updateConnectionStatusIndicator' because it
|
|
|
|
// affects the display mode by modifying 'mutedWhileDisconnected' flag
|
|
|
|
SmallVideo.prototype.updateView.call(this);
|
2016-09-16 20:51:19 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Updates the UI to reflect user's connectivity status.
|
|
|
|
*/
|
2017-03-21 17:14:13 +00:00
|
|
|
RemoteVideo.prototype.updateConnectionStatusIndicator = function () {
|
2017-05-25 13:48:06 +00:00
|
|
|
const connectionStatus = this.user.getConnectionStatus();
|
2017-03-21 17:14:13 +00:00
|
|
|
|
2017-05-25 13:48:06 +00:00
|
|
|
logger.debug(`${this.id} thumbnail connection status: ${connectionStatus}`);
|
2016-09-16 20:51:19 +00:00
|
|
|
|
2017-05-25 13:48:06 +00:00
|
|
|
// FIXME rename 'mutedWhileDisconnected' to 'mutedWhileNotRendering'
|
2016-09-22 21:21:01 +00:00
|
|
|
// Update 'mutedWhileDisconnected' flag
|
2017-05-25 13:48:06 +00:00
|
|
|
this._figureOutMutedWhileDisconnected();
|
2017-06-19 20:20:13 +00:00
|
|
|
this.updateConnectionStatus(connectionStatus);
|
2016-09-19 19:18:52 +00:00
|
|
|
|
2017-05-25 13:48:06 +00:00
|
|
|
const isInterrupted
|
|
|
|
= connectionStatus === ParticipantConnectionStatus.INTERRUPTED;
|
2016-09-19 19:18:52 +00:00
|
|
|
// Toggle thumbnail video problem filter
|
|
|
|
this.selectVideoElement().toggleClass(
|
2017-05-25 13:48:06 +00:00
|
|
|
"videoThumbnailProblemFilter", isInterrupted);
|
2016-09-19 19:18:52 +00:00
|
|
|
this.$avatar().toggleClass(
|
2017-05-25 13:48:06 +00:00
|
|
|
"videoThumbnailProblemFilter", isInterrupted);
|
2015-06-23 08:00:46 +00:00
|
|
|
};
|
|
|
|
|
2015-07-29 16:41:22 +00:00
|
|
|
/**
|
|
|
|
* Removes RemoteVideo from the page.
|
|
|
|
*/
|
|
|
|
RemoteVideo.prototype.remove = function () {
|
2016-11-11 15:00:54 +00:00
|
|
|
logger.log("Remove thumbnail", this.id);
|
2017-06-19 19:06:23 +00:00
|
|
|
|
|
|
|
this.removeAudioLevelIndicator();
|
|
|
|
|
2017-06-19 16:01:44 +00:00
|
|
|
const toolbarContainer
|
|
|
|
= this.container.querySelector('.videocontainer__toolbar');
|
|
|
|
|
|
|
|
if (toolbarContainer) {
|
|
|
|
ReactDOM.unmountComponentAtNode(toolbarContainer);
|
|
|
|
}
|
|
|
|
|
2015-07-29 16:41:22 +00:00
|
|
|
this.removeConnectionIndicator();
|
2017-06-29 18:21:03 +00:00
|
|
|
|
2017-06-29 03:35:43 +00:00
|
|
|
this.removeDisplayName();
|
2017-06-29 18:21:03 +00:00
|
|
|
|
|
|
|
this.removeAvatar();
|
|
|
|
|
2017-07-10 22:29:44 +00:00
|
|
|
this._unmountIndicators();
|
|
|
|
|
2015-10-01 22:46:42 +00:00
|
|
|
// Make sure that the large video is updated if are removing its
|
|
|
|
// corresponding small video.
|
2016-03-23 22:45:27 +00:00
|
|
|
this.VideoLayout.updateAfterThumbRemoved(this.id);
|
2015-07-29 16:41:22 +00:00
|
|
|
// Remove whole container
|
2015-12-14 12:26:50 +00:00
|
|
|
if (this.container.parentNode) {
|
2015-07-29 16:41:22 +00:00
|
|
|
this.container.parentNode.removeChild(this.container);
|
2015-12-14 12:26:50 +00:00
|
|
|
}
|
2015-07-29 16:41:22 +00:00
|
|
|
};
|
|
|
|
|
2016-02-01 21:00:51 +00:00
|
|
|
RemoteVideo.prototype.waitForPlayback = function (streamElement, stream) {
|
2015-07-15 12:01:36 +00:00
|
|
|
|
2015-11-06 21:59:38 +00:00
|
|
|
var webRtcStream = stream.getOriginalStream();
|
2015-12-14 12:26:50 +00:00
|
|
|
var isVideo = stream.isVideoTrack();
|
2015-11-06 21:59:38 +00:00
|
|
|
if (!isVideo || webRtcStream.id === 'mixedmslabel') {
|
2015-07-15 12:01:36 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var self = this;
|
|
|
|
|
2016-11-08 02:47:43 +00:00
|
|
|
// Triggers when video playback starts
|
2015-07-15 12:01:36 +00:00
|
|
|
var onPlayingHandler = function () {
|
2016-09-22 20:57:43 +00:00
|
|
|
self.wasVideoPlayed = true;
|
2016-11-08 02:47:43 +00:00
|
|
|
self.VideoLayout.remoteVideoActive(streamElement, self.id);
|
2016-02-01 21:00:51 +00:00
|
|
|
streamElement.onplaying = null;
|
2016-09-19 19:18:52 +00:00
|
|
|
// Refresh to show the video
|
|
|
|
self.updateView();
|
2015-07-15 12:01:36 +00:00
|
|
|
};
|
2016-02-01 21:00:51 +00:00
|
|
|
streamElement.onplaying = onPlayingHandler;
|
2015-07-15 12:01:36 +00:00
|
|
|
};
|
|
|
|
|
2016-02-01 22:08:15 +00:00
|
|
|
/**
|
2016-09-22 20:57:43 +00:00
|
|
|
* Checks whether the video stream has started for this RemoteVideo instance.
|
2016-02-01 22:08:15 +00:00
|
|
|
*
|
2016-09-22 20:57:43 +00:00
|
|
|
* @returns {boolean} true if this RemoteVideo has a video stream for which
|
|
|
|
* the playback has been started.
|
2016-02-01 22:08:15 +00:00
|
|
|
*/
|
|
|
|
RemoteVideo.prototype.hasVideoStarted = function () {
|
2016-09-22 20:57:43 +00:00
|
|
|
return this.wasVideoPlayed;
|
2016-02-01 22:08:15 +00:00
|
|
|
};
|
|
|
|
|
2015-10-01 20:10:55 +00:00
|
|
|
RemoteVideo.prototype.addRemoteStreamElement = function (stream) {
|
2015-12-14 12:26:50 +00:00
|
|
|
if (!this.container) {
|
2015-06-23 08:00:46 +00:00
|
|
|
return;
|
2015-12-14 12:26:50 +00:00
|
|
|
}
|
2015-06-23 08:00:46 +00:00
|
|
|
|
2015-12-14 12:26:50 +00:00
|
|
|
let isVideo = stream.isVideoTrack();
|
2016-01-29 00:33:27 +00:00
|
|
|
isVideo ? this.videoStream = stream : this.audioStream = stream;
|
2015-06-23 08:00:46 +00:00
|
|
|
|
2016-07-21 03:13:26 +00:00
|
|
|
if (isVideo)
|
|
|
|
this.setVideoType(stream.videoType);
|
|
|
|
|
2015-06-23 08:00:46 +00:00
|
|
|
// Add click handler.
|
2015-12-14 12:26:50 +00:00
|
|
|
let onClickHandler = (event) => {
|
2016-01-28 14:36:55 +00:00
|
|
|
let source = event.target || event.srcElement;
|
2015-07-20 17:32:04 +00:00
|
|
|
|
2016-01-28 14:36:55 +00:00
|
|
|
// ignore click if it was done in popup menu
|
|
|
|
if ($(source).parents('.popupmenu').length === 0) {
|
2016-03-24 01:43:29 +00:00
|
|
|
this.VideoLayout.handleVideoThumbClicked(this.id);
|
2016-01-28 14:36:55 +00:00
|
|
|
}
|
2015-07-20 17:32:04 +00:00
|
|
|
|
2015-07-10 09:57:20 +00:00
|
|
|
// On IE we need to populate this handler on video <object>
|
|
|
|
// and it does not give event instance as an argument,
|
|
|
|
// so we check here for methods.
|
|
|
|
if (event.stopPropagation && event.preventDefault) {
|
|
|
|
event.stopPropagation();
|
|
|
|
event.preventDefault();
|
|
|
|
}
|
2015-06-23 08:00:46 +00:00
|
|
|
return false;
|
|
|
|
};
|
2015-07-10 09:57:20 +00:00
|
|
|
this.container.onclick = onClickHandler;
|
2016-01-22 22:37:33 +00:00
|
|
|
|
|
|
|
if(!stream.getOriginalStream())
|
|
|
|
return;
|
|
|
|
|
|
|
|
let streamElement = SmallVideo.createStreamElement(stream);
|
|
|
|
|
|
|
|
// Put new stream element always in front
|
|
|
|
UIUtils.prependChild(this.container, streamElement);
|
|
|
|
|
2016-02-01 21:00:51 +00:00
|
|
|
// If we hide element when Temasys plugin is used then
|
|
|
|
// we'll never receive 'onplay' event and other logic won't work as expected
|
2016-02-02 21:50:02 +00:00
|
|
|
// NOTE: hiding will not have effect when Temasys plugin is in use, as
|
|
|
|
// calling attach will show it back
|
|
|
|
$(streamElement).hide();
|
2016-01-22 22:37:33 +00:00
|
|
|
|
2016-02-10 15:26:16 +00:00
|
|
|
// If the container is currently visible
|
|
|
|
// we attach the stream to the element.
|
2016-01-22 22:37:33 +00:00
|
|
|
if (!isVideo || (this.container.offsetParent !== null && isVideo)) {
|
2016-02-01 21:00:51 +00:00
|
|
|
this.waitForPlayback(streamElement, stream);
|
2016-01-22 22:37:33 +00:00
|
|
|
|
2016-02-01 21:00:51 +00:00
|
|
|
streamElement = stream.attach(streamElement);
|
2016-01-22 22:37:33 +00:00
|
|
|
}
|
|
|
|
|
2016-02-01 21:00:51 +00:00
|
|
|
$(streamElement).click(onClickHandler);
|
2017-02-12 06:02:16 +00:00
|
|
|
|
|
|
|
if (!isVideo) {
|
|
|
|
this._audioStreamElement = streamElement;
|
|
|
|
}
|
2016-10-31 16:12:28 +00:00
|
|
|
};
|
2015-06-23 08:00:46 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets the display name for the given video span id.
|
2016-10-20 19:28:10 +00:00
|
|
|
*
|
|
|
|
* @param displayName the display name to set
|
2015-06-23 08:00:46 +00:00
|
|
|
*/
|
2016-10-20 19:28:10 +00:00
|
|
|
RemoteVideo.prototype.setDisplayName = function(displayName) {
|
2015-06-23 08:00:46 +00:00
|
|
|
if (!this.container) {
|
2016-11-11 15:00:54 +00:00
|
|
|
logger.warn( "Unable to set displayName - " + this.videoSpanId +
|
2015-07-28 21:52:32 +00:00
|
|
|
" does not exist");
|
2015-06-23 08:00:46 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-06-29 03:35:43 +00:00
|
|
|
this.updateDisplayName({
|
|
|
|
displayName: displayName || interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME,
|
|
|
|
elementID: `${this.videoSpanId}_name`,
|
|
|
|
participantID: this.id
|
|
|
|
});
|
2015-07-28 21:52:32 +00:00
|
|
|
};
|
2015-06-23 08:00:46 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Removes remote video menu element from video element identified by
|
|
|
|
* given <tt>videoElementId</tt>.
|
|
|
|
*
|
|
|
|
* @param videoElementId the id of local or remote video element.
|
|
|
|
*/
|
|
|
|
RemoteVideo.prototype.removeRemoteVideoMenu = function() {
|
2016-09-18 21:35:35 +00:00
|
|
|
var menuSpan = $('#' + this.videoSpanId + '> .remotevideomenu');
|
2015-06-23 08:00:46 +00:00
|
|
|
if (menuSpan.length) {
|
2016-08-04 20:23:38 +00:00
|
|
|
this.popover.forceHide();
|
2015-06-23 08:00:46 +00:00
|
|
|
menuSpan.remove();
|
2016-08-23 23:37:41 +00:00
|
|
|
this.hasRemoteVideoMenu = false;
|
2015-06-23 08:00:46 +00:00
|
|
|
}
|
2015-07-20 17:32:04 +00:00
|
|
|
};
|
|
|
|
|
2015-06-23 08:00:46 +00:00
|
|
|
RemoteVideo.createContainer = function (spanId) {
|
2016-09-15 02:20:54 +00:00
|
|
|
let container = document.createElement('span');
|
2015-06-23 08:00:46 +00:00
|
|
|
container.id = spanId;
|
2016-11-04 20:24:05 +00:00
|
|
|
container.className = 'videocontainer';
|
2016-09-15 02:20:54 +00:00
|
|
|
|
2016-11-01 13:17:44 +00:00
|
|
|
let wrapper = document.createElement('div');
|
2016-11-03 10:13:03 +00:00
|
|
|
wrapper.className = 'videocontainer__background';
|
2016-11-01 13:17:44 +00:00
|
|
|
container.appendChild(wrapper);
|
|
|
|
|
2016-10-26 03:05:32 +00:00
|
|
|
let indicatorBar = document.createElement('div');
|
|
|
|
indicatorBar.className = "videocontainer__toptoolbar";
|
|
|
|
container.appendChild(indicatorBar);
|
|
|
|
|
2016-09-15 02:20:54 +00:00
|
|
|
let toolbar = document.createElement('div');
|
|
|
|
toolbar.className = "videocontainer__toolbar";
|
|
|
|
container.appendChild(toolbar);
|
|
|
|
|
2016-10-26 20:47:28 +00:00
|
|
|
let overlay = document.createElement('div');
|
2016-10-27 19:32:22 +00:00
|
|
|
overlay.className = "videocontainer__hoverOverlay";
|
2016-10-26 20:47:28 +00:00
|
|
|
container.appendChild(overlay);
|
|
|
|
|
2017-06-29 03:35:43 +00:00
|
|
|
const displayNameContainer = document.createElement('div');
|
|
|
|
displayNameContainer.className = 'displayNameContainer';
|
|
|
|
container.appendChild(displayNameContainer);
|
|
|
|
|
2017-06-29 18:21:03 +00:00
|
|
|
const avatarContainer = document.createElement('div');
|
|
|
|
avatarContainer.className = 'avatar-container';
|
|
|
|
container.appendChild(avatarContainer);
|
|
|
|
|
2017-05-17 21:05:48 +00:00
|
|
|
var remotes = document.getElementById('filmstripRemoteVideosContainer');
|
2015-06-23 08:00:46 +00:00
|
|
|
return remotes.appendChild(container);
|
|
|
|
};
|
|
|
|
|
2016-10-26 19:14:38 +00:00
|
|
|
/**
|
|
|
|
* Shows 2 button dialog for confirmation from the user for muting remote
|
|
|
|
* participant.
|
|
|
|
*/
|
|
|
|
RemoteVideo.showMuteParticipantDialog = function () {
|
|
|
|
return new Promise(resolve => {
|
|
|
|
APP.UI.messageHandler.openTwoButtonDialog({
|
|
|
|
titleKey : "dialog.muteParticipantTitle",
|
2016-10-27 23:31:00 +00:00
|
|
|
msgString: "<div data-i18n='dialog.muteParticipantBody'></div>",
|
|
|
|
leftButtonKey: "dialog.muteParticipantButton",
|
|
|
|
dontShowAgain: {
|
|
|
|
id: "dontShowMuteParticipantDialog",
|
|
|
|
textKey: "dialog.doNotShowMessageAgain",
|
|
|
|
checked: true,
|
|
|
|
buttonValues: [true]
|
2016-10-26 19:14:38 +00:00
|
|
|
},
|
2016-10-27 23:31:00 +00:00
|
|
|
submitFunction: () => resolve(MUTED_DIALOG_BUTTON_VALUES.muted),
|
2016-10-26 19:14:38 +00:00
|
|
|
closeFunction: () => resolve(MUTED_DIALOG_BUTTON_VALUES.cancel)
|
|
|
|
});
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2015-12-14 12:26:50 +00:00
|
|
|
export default RemoteVideo;
|