jiti-meet/modules/UI/videolayout/VideoLayout.js

1229 lines
35 KiB
JavaScript
Raw Normal View History

/* global APP, $, interfaceConfig */
const logger = require('jitsi-meet-logger').getLogger(__filename);
import {
getNearestReceiverVideoQualityLevel,
setMaxReceiverVideoQuality
} from '../../../react/features/base/conference';
import {
JitsiParticipantConnectionStatus
} from '../../../react/features/base/lib-jitsi-meet';
import { VIDEO_TYPE } from '../../../react/features/base/media';
import {
getLocalParticipant as getLocalParticipantFromStore,
getPinnedParticipant,
pinParticipant
} from '../../../react/features/base/participants';
import {
shouldDisplayTileView
} from '../../../react/features/video-layout';
import { SHARED_VIDEO_CONTAINER_TYPE } from '../shared_video/SharedVideo';
import SharedVideoThumb from '../shared_video/SharedVideoThumb';
import Filmstrip from './Filmstrip';
import UIEvents from '../../../service/UI/UIEvents';
import UIUtil from '../util/UIUtil';
2015-06-23 08:00:46 +00:00
import RemoteVideo from './RemoteVideo';
import LargeVideoManager from './LargeVideoManager';
import { VIDEO_CONTAINER_TYPE } from './VideoContainer';
import LocalVideo from './LocalVideo';
2015-12-14 12:26:50 +00:00
const remoteVideos = {};
let localVideoThumbnail = null;
2015-06-23 08:00:46 +00:00
let eventEmitter = null;
let largeVideo;
/**
* flipX state of the localVideo
*/
let localFlipX = null;
/**
* Handler for local flip X changed event.
* @param {Object} val
*/
function onLocalFlipXChanged(val) {
localFlipX = val;
if (largeVideo) {
largeVideo.onLocalFlipXChange(val);
}
}
/**
* Returns the redux representation of all known users.
*
* @private
* @returns {Array}
*/
function getAllParticipants() {
return APP.store.getState()['features/base/participants'];
}
/**
* Returns an array of all thumbnails in the filmstrip.
*
* @private
* @returns {Array}
*/
function getAllThumbnails() {
return [
localVideoThumbnail,
...Object.values(remoteVideos)
];
}
/**
* Private helper to get the redux representation of the local participant.
*
* @private
* @returns {Object}
*/
function getLocalParticipant() {
return getLocalParticipantFromStore(APP.store.getState());
}
/**
* Returns the user ID of the remote participant that is current the dominant
* speaker.
*
* @private
* @returns {string|null}
*/
function getCurrentRemoteDominantSpeakerID() {
const dominantSpeaker = getAllParticipants()
.find(participant => participant.dominantSpeaker);
if (dominantSpeaker) {
return dominantSpeaker.local ? null : dominantSpeaker.id;
}
return null;
}
2015-12-14 14:51:02 +00:00
/**
* Returns the corresponding resource id to the given peer container
* DOM element.
*
* @return the corresponding resource id to the given peer container
* DOM element
*/
function getPeerContainerResourceId(containerElement) {
2015-12-14 14:51:02 +00:00
if (localVideoThumbnail.container === containerElement) {
return localVideoThumbnail.id;
}
const i = containerElement.id.indexOf('participant_');
2015-12-14 14:51:02 +00:00
if (i >= 0) {
return containerElement.id.substring(i + 12);
}
}
const VideoLayout = {
init(emitter) {
eventEmitter = emitter;
localVideoThumbnail = new LocalVideo(
VideoLayout,
emitter,
this._updateLargeVideoIfDisplayed.bind(this));
// sets default video type of local video
// FIXME container type is totally different thing from the video type
localVideoThumbnail.setVideoType(VIDEO_CONTAINER_TYPE);
// if we do not resize the thumbs here, if there is no video device
// the local video thumb maybe one pixel
this.resizeThumbnails(true);
this.registerListeners();
},
/**
* Registering listeners for UI events in Video layout component.
*
* @returns {void}
*/
registerListeners() {
eventEmitter.addListener(UIEvents.LOCAL_FLIPX_CHANGED,
onLocalFlipXChanged);
},
/**
* Cleans up state of this singleton {@code VideoLayout}.
*
* @returns {void}
*/
reset() {
this._resetLargeVideo();
this._resetFilmstrip();
2015-12-14 12:26:50 +00:00
},
initLargeVideo() {
this._resetLargeVideo();
largeVideo = new LargeVideoManager(eventEmitter);
if (localFlipX) {
largeVideo.onLocalFlipXChange(localFlipX);
}
largeVideo.updateContainerSize();
2015-12-25 16:55:45 +00:00
},
2016-09-28 21:31:40 +00:00
/**
* Sets the audio level of the video elements associated to the given id.
*
* @param id the video identifier in the form it comes from the library
* @param lvl the new audio level to update to
*/
2015-12-25 16:55:45 +00:00
setAudioLevel(id, lvl) {
const smallVideo = this.getSmallVideo(id);
if (smallVideo) {
2016-09-28 21:31:40 +00:00
smallVideo.updateAudioLevelIndicator(lvl);
}
2016-09-28 21:31:40 +00:00
if (largeVideo && id === largeVideo.id) {
2016-09-28 21:31:40 +00:00
largeVideo.updateLargeVideoAudioLevel(lvl);
}
2015-12-25 16:55:45 +00:00
},
changeLocalVideo(stream) {
const localId = getLocalParticipant().id;
2015-12-30 12:14:56 +00:00
this.onVideoTypeChanged(localId, stream.videoType);
localVideoThumbnail.changeVideo(stream);
this._updateLargeVideoIfDisplayed(localId);
2015-12-14 12:26:50 +00:00
},
/**
* Get's the localID of the conference and set it to the local video
* (small one). This needs to be called as early as possible, when muc is
* actually joined. Otherwise events can come with information like email
* and setting them assume the id is already set.
*/
mucJoined() {
2015-12-25 16:55:45 +00:00
if (largeVideo && !largeVideo.id) {
this.updateLargeVideo(getLocalParticipant().id, true);
2015-12-14 12:26:50 +00:00
}
// FIXME: replace this call with a generic update call once SmallVideo
// only contains a ReactElement. Then remove this call once the
// Filmstrip is fully in React.
localVideoThumbnail.updateIndicators();
2015-12-14 12:26:50 +00:00
},
/**
* Adds or removes icons for not available camera and microphone.
* @param resourceJid the jid of user
* @param devices available devices
*/
setDeviceAvailabilityIcons(id, devices) {
2016-01-25 22:39:05 +00:00
if (APP.conference.isLocalId(id)) {
localVideoThumbnail.setDeviceAvailabilityIcons(devices);
return;
2016-01-25 22:39:05 +00:00
}
const video = remoteVideos[id];
2016-01-25 22:39:05 +00:00
if (!video) {
return;
}
2016-01-25 22:39:05 +00:00
video.setDeviceAvailabilityIcons(devices);
2015-12-14 12:26:50 +00:00
},
2016-05-01 18:35:18 +00:00
/**
* Shows/hides local video.
* @param {boolean} true to make the local video visible, false - otherwise
*/
setLocalVideoVisible(visible) {
localVideoThumbnail.setVisible(visible);
},
/**
2014-06-19 13:40:54 +00:00
* Checks if removed video is currently displayed and tries to display
* another one instead.
* Uses focusedID if any or dominantSpeakerID if any,
* otherwise elects new video, in this order.
*/
_updateAfterThumbRemoved(id) {
// Always trigger an update if large video is empty.
if (!largeVideo
|| (this.getLargeVideoID() && !this.isCurrentlyOnLarge(id))) {
2015-12-14 14:51:02 +00:00
return;
}
const pinnedId = this.getPinnedId();
2015-12-14 12:26:50 +00:00
let newId;
if (pinnedId) {
newId = pinnedId;
} else if (getCurrentRemoteDominantSpeakerID()) {
newId = getCurrentRemoteDominantSpeakerID();
} else { // Otherwise select last visible video
2015-12-14 14:51:02 +00:00
newId = this.electLastVisibleVideo();
}
2015-12-14 14:51:02 +00:00
2015-12-25 16:55:45 +00:00
this.updateLargeVideo(newId);
2015-12-14 12:26:50 +00:00
},
electLastVisibleVideo() {
// pick the last visible video in the row
// if nobody else is left, this picks the local video
const remoteThumbs = Filmstrip.getThumbs(true).remoteThumbs;
2016-09-15 02:20:54 +00:00
let thumbs = remoteThumbs.filter('[id!="mixedstream"]');
2015-12-30 10:55:51 +00:00
const lastVisible = thumbs.filter(':visible:last');
2015-12-30 10:55:51 +00:00
if (lastVisible.length) {
const id = getPeerContainerResourceId(lastVisible[0]);
2015-12-14 14:51:02 +00:00
if (remoteVideos[id]) {
logger.info(`electLastVisibleVideo: ${id}`);
2015-12-14 14:51:02 +00:00
return id;
}
2015-12-14 14:51:02 +00:00
// The RemoteVideo was removed (but the DOM elements may still
// exist).
}
logger.info('Last visible video no longer exists');
thumbs = Filmstrip.getThumbs().remoteThumbs;
2015-12-30 10:55:51 +00:00
if (thumbs.length) {
const id = getPeerContainerResourceId(thumbs[0]);
2015-12-14 14:51:02 +00:00
if (remoteVideos[id]) {
logger.info(`electLastVisibleVideo: ${id}`);
2015-12-14 14:51:02 +00:00
return id;
}
2015-12-14 14:51:02 +00:00
// The RemoteVideo was removed (but the DOM elements may
// still exist).
}
2015-12-14 14:51:02 +00:00
// Go with local video
logger.info('Fallback to local video...');
const { id } = getLocalParticipant();
logger.info(`electLastVisibleVideo: ${id}`);
2015-12-14 14:51:02 +00:00
return id;
2015-12-14 12:26:50 +00:00
},
2015-11-30 11:54:54 +00:00
onRemoteStreamAdded(stream) {
const id = stream.getParticipantId();
const remoteVideo = remoteVideos[id];
2016-04-26 21:38:07 +00:00
if (!remoteVideo) {
2016-04-26 21:38:07 +00:00
return;
}
2016-04-26 21:38:07 +00:00
remoteVideo.addRemoteStreamElement(stream);
// Make sure track's muted state is reflected
if (stream.getType() === 'audio') {
this.onAudioMute(stream.getParticipantId(), stream.isMuted());
} else {
this.onVideoMute(stream.getParticipantId(), stream.isMuted());
}
2015-12-14 12:26:50 +00:00
},
onRemoteStreamRemoved(stream) {
const id = stream.getParticipantId();
const remoteVideo = remoteVideos[id];
// Remote stream may be removed after participant left the conference.
if (remoteVideo) {
remoteVideo.removeRemoteStreamElement(stream);
}
if (stream.isVideoTrack()) {
this._updateLargeVideoIfDisplayed(id);
}
this.updateMutedForNoTracks(id, stream.getType());
},
/**
* FIXME get rid of this method once muted indicator are reactified (by
* making sure that user with no tracks is displayed as muted )
*
* If participant has no tracks will make the UI display muted status.
* @param {string} participantId
* @param {string} mediaType 'audio' or 'video'
*/
updateMutedForNoTracks(participantId, mediaType) {
const participant = APP.conference.getParticipantById(participantId);
if (participant
&& !participant.getTracksByMediaType(mediaType).length) {
if (mediaType === 'audio') {
APP.UI.setAudioMuted(participantId, true);
} else if (mediaType === 'video') {
APP.UI.setVideoMuted(participantId, true);
} else {
logger.error(`Unsupported media type: ${mediaType}`);
}
}
},
/**
* Return the type of the remote video.
2015-12-14 12:26:50 +00:00
* @param id the id for the remote video
* @returns {String} the video type video or screen.
*/
getRemoteVideoType(id) {
const smallVideo = VideoLayout.getSmallVideo(id);
return smallVideo ? smallVideo.getVideoType() : null;
2015-12-14 12:26:50 +00:00
},
isPinned(id) {
return id === this.getPinnedId();
},
getPinnedId() {
const { id } = getPinnedParticipant(APP.store.getState()) || {};
return id || null;
},
2018-11-30 22:13:39 +00:00
/**
* Triggers a thumbnail to pin or unpin itself.
*
* @param {number} videoNumber - The index of the video to toggle pin on.
* @private
*/
togglePin(videoNumber) {
const videos = getAllThumbnails();
const videoView = videos[videoNumber];
videoView && videoView.togglePin();
},
/**
* Callback invoked to update display when the pin participant has changed.
*
* @paramn {string|null} pinnedParticipantID - The participant ID of the
* participant that is pinned or null if no one is pinned.
* @returns {void}
*/
onPinChange(pinnedParticipantID) {
if (interfaceConfig.filmStripOnly) {
return;
}
getAllThumbnails().forEach(thumbnail =>
thumbnail.focus(pinnedParticipantID === thumbnail.getId()));
if (pinnedParticipantID) {
this.updateLargeVideo(pinnedParticipantID);
} else {
const currentDominantSpeakerID
= getCurrentRemoteDominantSpeakerID();
if (currentDominantSpeakerID) {
this.updateLargeVideo(currentDominantSpeakerID);
} else {
// if there is no currentDominantSpeakerID, it can also be
// that local participant is the dominant speaker
// we should act as a participant has left and was on large
// and we should choose somebody (electLastVisibleVideo)
this.updateLargeVideo(this.electLastVisibleVideo());
}
}
2015-12-14 12:26:50 +00:00
},
/**
* Creates a participant container for the given id.
2016-04-26 21:38:07 +00:00
*
* @param {Object} participant - The redux representation of a remote
* participant.
* @returns {void}
*/
addRemoteParticipantContainer(participant) {
if (!participant || participant.local) {
return;
2018-06-22 18:59:54 +00:00
} else if (participant.isFakeParticipant) {
const sharedVideoThumb = new SharedVideoThumb(
participant,
SHARED_VIDEO_CONTAINER_TYPE,
VideoLayout);
this.addRemoteVideoContainer(participant.id, sharedVideoThumb);
return;
}
const id = participant.id;
const jitsiParticipant = APP.conference.getParticipantById(id);
const remoteVideo
= new RemoteVideo(jitsiParticipant, VideoLayout, eventEmitter);
this._setRemoteControlProperties(jitsiParticipant, remoteVideo);
this.addRemoteVideoContainer(id, remoteVideo);
this.updateMutedForNoTracks(id, 'audio');
this.updateMutedForNoTracks(id, 'video');
const remoteVideosCount = Object.keys(remoteVideos).length;
if (remoteVideosCount === 1) {
window.setTimeout(() => {
const updatedRemoteVideosCount
= Object.keys(remoteVideos).length;
if (updatedRemoteVideosCount === 1 && remoteVideos[id]) {
this._maybePlaceParticipantOnLargeVideo(id);
}
}, 3000);
}
},
/**
* Adds remote video container for the given id and <tt>SmallVideo</tt>.
*
* @param {string} the id of the video to add
* @param {SmallVideo} smallVideo the small video instance to add as a
* remote video
*/
addRemoteVideoContainer(id, remoteVideo) {
2015-12-14 12:26:50 +00:00
remoteVideos[id] = remoteVideo;
2016-11-08 02:39:28 +00:00
if (!remoteVideo.getVideoType()) {
// make video type the default one (camera)
// FIXME container type is not a video type
2016-11-08 02:39:28 +00:00
remoteVideo.setVideoType(VIDEO_CONTAINER_TYPE);
2015-12-14 12:26:50 +00:00
}
VideoLayout.resizeThumbnails(true);
// Initialize the view
remoteVideo.updateView();
2015-12-14 12:26:50 +00:00
},
// FIXME: what does this do???
remoteVideoActive(videoElement, resourceJid) {
logger.info(`${resourceJid} video is now active`, videoElement);
VideoLayout.resizeThumbnails(
false, () => {
if (videoElement) {
$(videoElement).show();
}
});
2015-06-23 08:00:46 +00:00
this._maybePlaceParticipantOnLargeVideo(resourceJid);
},
/**
* Update the large video to the last added video only if there's no current
* dominant, focused speaker or update it to the current dominant speaker.
*
* @params {string} resourceJid - The id of the user to maybe display on
* large video.
* @returns {void}
*/
_maybePlaceParticipantOnLargeVideo(resourceJid) {
const pinnedId = this.getPinnedId();
if ((!pinnedId
&& !getCurrentRemoteDominantSpeakerID()
&& this.isLargeContainerTypeVisible(VIDEO_CONTAINER_TYPE))
|| pinnedId === resourceJid
|| (!pinnedId && resourceJid
&& getCurrentRemoteDominantSpeakerID() === resourceJid)
/* Playback started while we're on the stage - may need to update
video source with the new stream */
|| this.isCurrentlyOnLarge(resourceJid)) {
2015-12-25 16:55:45 +00:00
this.updateLargeVideo(resourceJid, true);
}
2015-12-14 12:26:50 +00:00
},
/**
* Shows a visual indicator for the moderator of the conference.
2016-01-13 21:17:33 +00:00
* On local or remote participants.
*/
showModeratorIndicator() {
const isModerator = APP.conference.isModerator;
if (isModerator) {
2016-09-28 21:31:40 +00:00
localVideoThumbnail.addModeratorIndicator();
2016-07-08 01:44:04 +00:00
} else {
2016-09-28 21:31:40 +00:00
localVideoThumbnail.removeModeratorIndicator();
}
APP.conference.listMembers().forEach(member => {
const id = member.getId();
const remoteVideo = remoteVideos[id];
if (!remoteVideo) {
2016-04-27 20:52:07 +00:00
return;
}
2016-04-27 20:52:07 +00:00
2015-12-14 12:26:50 +00:00
if (member.isModerator()) {
2016-09-28 21:31:40 +00:00
remoteVideo.addModeratorIndicator();
}
remoteVideo.updateRemoteVideoMenu();
});
2015-12-14 12:26:50 +00:00
},
/*
* Shows or hides the audio muted indicator over the local thumbnail video.
* @param {boolean} isMuted
*/
showLocalAudioIndicator(isMuted) {
2015-06-30 11:34:11 +00:00
localVideoThumbnail.showAudioIndicator(isMuted);
2015-12-14 12:26:50 +00:00
},
/**
* Resizes thumbnails.
*/
resizeThumbnails(
forceUpdate = false,
onComplete = null) {
const { localVideo, remoteVideo }
= Filmstrip.calculateThumbnailSize();
Filmstrip.resizeThumbnails(localVideo, remoteVideo, forceUpdate);
if (shouldDisplayTileView(APP.store.getState())) {
const height
= (localVideo && localVideo.thumbHeight)
|| (remoteVideo && remoteVideo.thumbnHeight)
|| 0;
const qualityLevel = getNearestReceiverVideoQualityLevel(height);
APP.store.dispatch(setMaxReceiverVideoQuality(qualityLevel));
}
if (onComplete && typeof onComplete === 'function') {
onComplete();
}
2015-12-14 12:26:50 +00:00
},
/**
* On audio muted event.
*/
onAudioMute(id, isMuted) {
2016-01-06 22:39:13 +00:00
if (APP.conference.isLocalId(id)) {
2015-06-23 08:00:46 +00:00
localVideoThumbnail.showAudioIndicator(isMuted);
} else {
const remoteVideo = remoteVideos[id];
if (!remoteVideo) {
return;
}
remoteVideo.showAudioIndicator(isMuted);
remoteVideo.updateRemoteVideoMenu(isMuted);
}
2015-12-14 12:26:50 +00:00
},
/**
* On video muted event.
*/
onVideoMute(id, value) {
2016-01-06 22:39:13 +00:00
if (APP.conference.isLocalId(id)) {
2016-09-28 21:31:40 +00:00
localVideoThumbnail.setVideoMutedView(value);
} else {
const remoteVideo = remoteVideos[id];
if (remoteVideo) {
2016-09-28 21:31:40 +00:00
remoteVideo.setVideoMutedView(value);
}
}
2016-02-10 15:16:55 +00:00
if (this.isCurrentlyOnLarge(id)) {
// large video will show avatar instead of muted stream
this.updateLargeVideo(id, true);
}
2015-12-14 12:26:50 +00:00
},
/**
* Display name changed.
*/
onDisplayNameChanged(id, displayName, status) {
if (id === 'localVideoContainer'
|| APP.conference.isLocalId(id)) {
2015-06-23 08:00:46 +00:00
localVideoThumbnail.setDisplayName(displayName);
} else {
const remoteVideo = remoteVideos[id];
if (remoteVideo) {
remoteVideo.setDisplayName(displayName, status);
}
}
2015-12-14 12:26:50 +00:00
},
2016-06-20 21:13:17 +00:00
/**
* Sets the "raised hand" status for a participant identified by 'id'.
*/
setRaisedHandStatus(id, raisedHandStatus) {
const video
2016-06-20 21:13:17 +00:00
= APP.conference.isLocalId(id)
? localVideoThumbnail : remoteVideos[id];
2016-06-20 21:13:17 +00:00
if (video) {
video.showRaisedHandIndicator(raisedHandStatus);
if (raisedHandStatus) {
video.showDominantSpeakerIndicator(false);
}
2016-06-20 21:13:17 +00:00
}
},
/**
* On dominant speaker changed event.
*
* @param {string} id - The participant ID of the new dominant speaker.
* @returns {void}
*/
onDominantSpeakerChanged(id) {
getAllThumbnails().forEach(thumbnail =>
thumbnail.showDominantSpeakerIndicator(id === thumbnail.getId()));
if (!remoteVideos[id]) {
return;
2015-12-14 12:26:50 +00:00
}
// Local video will not have container found, but that's ok
// since we don't want to switch to local video.
if (!interfaceConfig.filmStripOnly && !this.getPinnedId()
&& !this.getCurrentlyOnLargeContainer().stayOnStage()) {
this.updateLargeVideo(id);
}
2015-12-14 12:26:50 +00:00
},
/**
* Shows/hides warning about a user's connectivity issues.
*
* @param {string} id - The ID of the remote participant(MUC nickname).
* @param {status} status - The new connection status.
* @returns {void}
*/
onParticipantConnectionStatusChanged(id, status) {
if (APP.conference.isLocalId(id)) {
// Maintain old logic of passing in either interrupted or active
// to updateConnectionStatus.
localVideoThumbnail.updateConnectionStatus(status);
if (status === JitsiParticipantConnectionStatus.INTERRUPTED) {
largeVideo && largeVideo.onVideoInterrupted();
} else {
largeVideo && largeVideo.onVideoRestored();
}
return;
}
// We have to trigger full large video update to transition from
// avatar to video on connectivity restored.
this._updateLargeVideoIfDisplayed(id, true);
const remoteVideo = remoteVideos[id];
if (remoteVideo) {
// Updating only connection status indicator is not enough, because
// when we the connection is restored while the avatar was displayed
// (due to 'muted while disconnected' condition) we may want to show
// the video stream again and in order to do that the display mode
// must be updated.
// remoteVideo.updateConnectionStatusIndicator(isActive);
remoteVideo.updateView();
}
},
/**
* On last N change event.
*
* @param endpointsLeavingLastN the list currently leaving last N
* endpoints
* @param endpointsEnteringLastN the list currently entering last N
* endpoints
*/
onLastNEndpointsChanged(endpointsLeavingLastN, endpointsEnteringLastN) {
if (endpointsLeavingLastN) {
endpointsLeavingLastN.forEach(this._updateRemoteVideo, this);
}
if (endpointsEnteringLastN) {
endpointsEnteringLastN.forEach(this._updateRemoteVideo, this);
}
},
/**
* Updates remote video by id if it exists.
* @param {string} id of the remote video
* @private
*/
_updateRemoteVideo(id) {
const remoteVideo = remoteVideos[id];
if (remoteVideo) {
remoteVideo.updateView();
if (remoteVideo.isCurrentlyOnLargeVideo()) {
this.updateLargeVideo(id);
}
}
2015-12-14 12:26:50 +00:00
},
/**
* Hides the connection indicator
2015-12-14 15:40:30 +00:00
* @param id
*/
hideConnectionIndicator(id) {
const remoteVideo = remoteVideos[id];
if (remoteVideo) {
remoteVideo.removeConnectionIndicator();
}
2015-12-14 12:26:50 +00:00
},
/**
* Hides all the indicators
*/
hideStats() {
for (const video in remoteVideos) { // eslint-disable-line guard-for-in
const remoteVideo = remoteVideos[video];
if (remoteVideo) {
remoteVideo.removeConnectionIndicator();
}
}
localVideoThumbnail.removeConnectionIndicator();
2015-12-14 12:26:50 +00:00
},
removeParticipantContainer(id) {
// Unlock large video
if (this.getPinnedId() === id) {
logger.info('Focused video owner has left the conference');
APP.store.dispatch(pinParticipant(null));
}
const remoteVideo = remoteVideos[id];
if (remoteVideo) {
// Remove remote video
logger.info(`Removing remote video: ${id}`);
2015-12-14 12:26:50 +00:00
delete remoteVideos[id];
remoteVideo.remove();
} else {
logger.warn(`No remote video for ${id}`);
}
VideoLayout.resizeThumbnails();
VideoLayout._updateAfterThumbRemoved(id);
2015-12-14 12:26:50 +00:00
},
2015-11-30 11:54:54 +00:00
onVideoTypeChanged(id, newVideoType) {
if (VideoLayout.getRemoteVideoType(id) === newVideoType) {
return;
}
logger.info('Peer video type changed: ', id, newVideoType);
let smallVideo;
2015-12-14 12:26:50 +00:00
if (APP.conference.isLocalId(id)) {
if (!localVideoThumbnail) {
logger.warn('Local video not ready yet');
return;
}
smallVideo = localVideoThumbnail;
2015-12-14 12:26:50 +00:00
} else if (remoteVideos[id]) {
smallVideo = remoteVideos[id];
} else {
return;
}
smallVideo.setVideoType(newVideoType);
2015-12-25 16:55:45 +00:00
if (this.isCurrentlyOnLarge(id)) {
this.updateLargeVideo(id, true);
}
2015-12-14 12:26:50 +00:00
},
2015-11-13 17:04:49 +00:00
2015-06-23 08:00:46 +00:00
/**
2015-11-13 17:04:49 +00:00
* Resizes the video area.
*
feat(recording): frontend logic can support live streaming and recording (#2952) * feat(recording): frontend logic can support live streaming and recording Instead of either live streaming or recording, now both can live together. The changes to facilitate such include the following: - Killing the state storing in Recording.js. Instead state is stored in the lib and updated in redux for labels to display the necessary state updates. - Creating a new container, Labels, for recording labels. Previously labels were manually created and positioned. The container can create a reasonable number of labels and only the container itself needs to be positioned with CSS. The VideoQualityLabel has been shoved into the container as well because it moves along with the recording labels. - The action for updating recording state has been modified to enable updating an array of recording sessions to support having multiple sessions. - Confirmation dialogs for stopping and starting a file recording session have been created, as they previously were jquery modals opened by Recording.js. - Toolbox.web displays live streaming and recording buttons based on configuration instead of recording availability. - VideoQualityLabel and RecordingLabel have been simplified to remove any positioning logic, as the Labels container handles such. - Previous recording state update logic has been moved into the RecordingLabel component. Each RecordingLabel is in charge of displaying state for a recording session. The display UX has been left alone. - Sipgw availability is no longer broadcast so remove logic depending on its state. Some moving around of code was necessary to get around linting errors about the existing code being too deeply nested (even though I didn't touch it). * work around lib-jitsi-meet circular dependency issues * refactor labels to use html base * pass in translation keys to video quality label * add video quality classnames for torture tests * break up, rearrange recorder session update listener * add comment about disabling startup resize animation * rename session to sessionData * chore(deps): update to latest lib for recording changes
2018-05-16 14:00:16 +00:00
* TODO: Remove the "animate" param as it is no longer passed in as true.
*
* @param forceUpdate indicates that hidden thumbnails will be shown
*/
resizeVideoArea(
forceUpdate = false,
animate = false) {
// Resize the thumbnails first.
this.resizeThumbnails(forceUpdate);
2015-12-25 16:55:45 +00:00
if (largeVideo) {
largeVideo.updateContainerSize();
2015-12-25 16:55:45 +00:00
largeVideo.resize(animate);
}
// Calculate available width and height.
const availableHeight = window.innerHeight;
const availableWidth = UIUtil.getAvailableVideoWidth();
2016-01-20 14:26:39 +00:00
if (availableWidth < 0 || availableHeight < 0) {
return;
}
2015-12-14 12:26:50 +00:00
},
getSmallVideo(id) {
2015-12-14 12:26:50 +00:00
if (APP.conference.isLocalId(id)) {
2015-06-23 08:00:46 +00:00
return localVideoThumbnail;
}
return remoteVideos[id];
2015-12-14 12:26:50 +00:00
},
changeUserAvatar(id, avatarUrl) {
const smallVideo = VideoLayout.getSmallVideo(id);
2015-12-02 15:24:57 +00:00
if (smallVideo) {
smallVideo.avatarChanged(avatarUrl);
2015-12-02 15:24:57 +00:00
} else {
2016-11-11 15:00:54 +00:00
logger.warn(
`Missed avatar update - no small video yet for ${id}`
2015-12-02 15:24:57 +00:00
);
}
2015-12-25 16:55:45 +00:00
if (this.isCurrentlyOnLarge(id)) {
largeVideo.updateAvatar(avatarUrl);
2015-12-25 16:55:45 +00:00
}
2015-12-14 12:26:50 +00:00
},
isLargeVideoVisible() {
return this.isLargeContainerTypeVisible(VIDEO_CONTAINER_TYPE);
2015-12-25 16:55:45 +00:00
},
/**
* @return {LargeContainer} the currently displayed container on large
* video.
*/
getCurrentlyOnLargeContainer() {
return largeVideo.getCurrentContainer();
},
isCurrentlyOnLarge(id) {
2015-12-25 16:55:45 +00:00
return largeVideo && largeVideo.id === id;
},
/**
* Triggers an update of remote video and large video displays so they may
* pick up any state changes that have occurred elsewhere.
*
* @returns {void}
*/
updateAllVideos() {
const displayedUserId = this.getLargeVideoID();
if (displayedUserId) {
this.updateLargeVideo(displayedUserId, true);
}
Object.keys(remoteVideos).forEach(video => {
remoteVideos[video].updateView();
});
},
updateLargeVideo(id, forceUpdate) {
2015-12-25 16:55:45 +00:00
if (!largeVideo) {
return;
}
const currentContainer = largeVideo.getCurrentContainer();
const currentContainerType = largeVideo.getCurrentContainerType();
const currentId = largeVideo.id;
const isOnLarge = this.isCurrentlyOnLarge(id);
const smallVideo = this.getSmallVideo(id);
if (isOnLarge && !forceUpdate
&& LargeVideoManager.isVideoContainer(currentContainerType)
&& smallVideo) {
const currentStreamId = currentContainer.getStreamID();
const newStreamId
= smallVideo.videoStream
? smallVideo.videoStream.getId() : null;
// FIXME it might be possible to get rid of 'forceUpdate' argument
if (currentStreamId !== newStreamId) {
logger.debug('Enforcing large video update for stream change');
forceUpdate = true; // eslint-disable-line no-param-reassign
}
}
2015-12-25 16:55:45 +00:00
if ((!isOnLarge || forceUpdate) && smallVideo) {
const videoType = this.getRemoteVideoType(id);
// FIXME video type is not the same thing as container type
if (id !== currentId && videoType === VIDEO_CONTAINER_TYPE) {
APP.API.notifyOnStageParticipantChanged(id);
2015-12-25 16:55:45 +00:00
}
2016-07-08 20:14:49 +00:00
let oldSmallVideo;
2015-12-25 16:55:45 +00:00
if (currentId) {
2016-07-08 20:14:49 +00:00
oldSmallVideo = this.getSmallVideo(currentId);
2015-12-25 16:55:45 +00:00
}
2016-07-08 20:14:49 +00:00
smallVideo.waitForResolutionChange();
if (oldSmallVideo) {
2016-07-08 20:14:49 +00:00
oldSmallVideo.waitForResolutionChange();
}
2015-12-25 16:55:45 +00:00
largeVideo.updateLargeVideo(
id,
smallVideo.videoStream,
videoType || VIDEO_TYPE.CAMERA
).then(() => {
// update current small video and the old one
smallVideo.updateView();
oldSmallVideo && oldSmallVideo.updateView();
}, () => {
// use clicked other video during update, nothing to do.
});
2015-12-25 16:55:45 +00:00
} else if (currentId) {
const currentSmallVideo = this.getSmallVideo(currentId);
currentSmallVideo.updateView();
2015-12-25 16:55:45 +00:00
}
},
addLargeVideoContainer(type, container) {
2015-12-25 16:55:45 +00:00
largeVideo && largeVideo.addContainer(type, container);
},
removeLargeVideoContainer(type) {
2015-12-25 16:55:45 +00:00
largeVideo && largeVideo.removeContainer(type);
},
/**
* @returns Promise
*/
showLargeVideoContainer(type, show) {
2015-12-25 16:55:45 +00:00
if (!largeVideo) {
return Promise.reject();
}
const isVisible = this.isLargeContainerTypeVisible(type);
2015-12-25 16:55:45 +00:00
if (isVisible === show) {
return Promise.resolve();
}
const currentId = largeVideo.id;
let oldSmallVideo;
if (currentId) {
oldSmallVideo = this.getSmallVideo(currentId);
}
let containerTypeToShow = type;
// if we are hiding a container and there is focusedVideo
// (pinned remote video) use its video type,
// if not then use default type - large video
if (!show) {
const pinnedId = this.getPinnedId();
if (pinnedId) {
2016-03-24 18:16:42 +00:00
containerTypeToShow = this.getRemoteVideoType(pinnedId);
} else {
containerTypeToShow = VIDEO_CONTAINER_TYPE;
}
}
return largeVideo.showContainer(containerTypeToShow)
.then(() => {
if (oldSmallVideo) {
oldSmallVideo && oldSmallVideo.updateView();
}
});
2015-12-25 16:55:45 +00:00
},
isLargeContainerTypeVisible(type) {
2015-12-25 16:55:45 +00:00
return largeVideo && largeVideo.state === type;
},
/**
* Returns the id of the current video shown on large.
* Currently used by tests (torture).
*/
getLargeVideoID() {
return largeVideo && largeVideo.id;
},
/**
* Returns the the current video shown on large.
* Currently used by tests (torture).
*/
getLargeVideo() {
return largeVideo;
},
/**
* Sets the flipX state of the local video.
* @param {boolean} true for flipped otherwise false;
*/
setLocalFlipX(val) {
this.localFlipX = val;
},
getEventEmitter() {
return eventEmitter;
},
/**
* Handles user's features changes.
*/
onUserFeaturesChanged(user) {
const video = this.getSmallVideo(user.getId());
if (!video) {
return;
}
this._setRemoteControlProperties(user, video);
2016-07-08 20:14:49 +00:00
},
/**
* Sets the remote control properties (checks whether remote control
* is supported and executes remoteVideo.setRemoteControlSupport).
* @param {JitsiParticipant} user the user that will be checked for remote
* control support.
* @param {RemoteVideo} remoteVideo the remoteVideo on which the properties
* will be set.
*/
_setRemoteControlProperties(user, remoteVideo) {
APP.remoteControl.checkUserRemoteControlSupport(user).then(result =>
remoteVideo.setRemoteControlSupport(result));
},
/**
* Returns the wrapper jquery selector for the largeVideo
* @returns {JQuerySelector} the wrapper jquery selector for the largeVideo
*/
getLargeVideoWrapper() {
return this.getCurrentlyOnLargeContainer().$wrapper;
},
/**
* Returns the number of remove video ids.
*
* @returns {number} The number of remote videos.
*/
getRemoteVideosCount() {
return Object.keys(remoteVideos).length;
},
/**
* Sets the remote control active status for a remote participant.
*
* @param {string} participantID - The id of the remote participant.
* @param {boolean} isActive - The new remote control active status.
* @returns {void}
*/
setRemoteControlActiveStatus(participantID, isActive) {
remoteVideos[participantID].setRemoteControlActiveStatus(isActive);
},
/**
* Sets the remote control active status for the local participant.
*
* @returns {void}
*/
setLocalRemoteControlActiveChanged() {
Object.values(remoteVideos).forEach(
remoteVideo => remoteVideo.updateRemoteVideoMenu()
);
},
/**
* Helper method to invoke when the video layout has changed and elements
* have to be re-arranged and resized.
*
* @returns {void}
*/
refreshLayout() {
localVideoThumbnail && localVideoThumbnail.updateDOMLocation();
VideoLayout.resizeVideoArea();
localVideoThumbnail && localVideoThumbnail.rerender();
Object.values(remoteVideos).forEach(
remoteVideo => remoteVideo.rerender()
);
},
/**
* Cleans up any existing largeVideo instance.
*
* @private
* @returns {void}
*/
_resetLargeVideo() {
if (largeVideo) {
largeVideo.destroy();
}
largeVideo = null;
},
/**
* Cleans up filmstrip state. While a separate {@code Filmstrip} exists, its
* implementation is mainly for querying and manipulating the DOM while
* state mostly remains in {@code VideoLayout}.
*
* @private
* @returns {void}
*/
_resetFilmstrip() {
Object.keys(remoteVideos).forEach(remoteVideoId => {
this.removeParticipantContainer(remoteVideoId);
delete remoteVideos[remoteVideoId];
});
if (localVideoThumbnail) {
localVideoThumbnail.remove();
localVideoThumbnail = null;
}
},
/**
* Triggers an update of large video if the passed in participant is
* currently displayed on large video.
*
* @param {string} participantId - The participant ID that should trigger an
* update of large video if displayed.
* @param {boolean} force - Whether or not the large video update should
* happen no matter what.
* @returns {void}
*/
_updateLargeVideoIfDisplayed(participantId, force = false) {
if (this.isCurrentlyOnLarge(participantId)) {
this.updateLargeVideo(participantId, force);
}
}
2015-12-14 12:26:50 +00:00
};
2015-01-07 14:54:03 +00:00
2015-12-14 12:26:50 +00:00
export default VideoLayout;