When user reconnected and the avatar is displayed we need to perform full large video update in order to transition correctly.
1171 lines
37 KiB
JavaScript
1171 lines
37 KiB
JavaScript
/* global config, APP, $, interfaceConfig, JitsiMeetJS */
|
|
/* jshint -W101 */
|
|
|
|
import AudioLevels from "../audio_levels/AudioLevels";
|
|
import Avatar from "../avatar/Avatar";
|
|
import FilmStrip from "./FilmStrip";
|
|
import UIEvents from "../../../service/UI/UIEvents";
|
|
import UIUtil from "../util/UIUtil";
|
|
|
|
import RemoteVideo from "./RemoteVideo";
|
|
import LargeVideoManager from "./LargeVideoManager";
|
|
import {VIDEO_CONTAINER_TYPE} from "./VideoContainer";
|
|
import {SHARED_VIDEO_CONTAINER_TYPE} from '../shared_video/SharedVideo';
|
|
import LocalVideo from "./LocalVideo";
|
|
|
|
const RTCUIUtil = JitsiMeetJS.util.RTCUIHelper;
|
|
|
|
var remoteVideos = {};
|
|
var localVideoThumbnail = null;
|
|
|
|
var currentDominantSpeaker = null;
|
|
var localLastNCount = config.channelLastN;
|
|
var localLastNSet = [];
|
|
var lastNEndpointsCache = [];
|
|
var lastNPickupId = null;
|
|
|
|
var eventEmitter = null;
|
|
|
|
/**
|
|
* Currently focused video jid
|
|
* @type {String}
|
|
*/
|
|
var pinnedId = null;
|
|
|
|
/**
|
|
* flipX state of the localVideo
|
|
*/
|
|
let localFlipX = null;
|
|
|
|
/**
|
|
* On contact list item clicked.
|
|
*/
|
|
function onContactClicked (id) {
|
|
if (APP.conference.isLocalId(id)) {
|
|
$("#localVideoContainer").click();
|
|
return;
|
|
}
|
|
|
|
let remoteVideo = remoteVideos[id];
|
|
if (remoteVideo && remoteVideo.hasVideo()) {
|
|
// It is not always the case that a videoThumb exists (if there is
|
|
// no actual video).
|
|
if (remoteVideo.hasVideoStarted()) {
|
|
// We have a video src, great! Let's update the large video
|
|
// now.
|
|
VideoLayout.handleVideoThumbClicked(id);
|
|
} else {
|
|
|
|
// If we don't have a video src for jid, there's absolutely
|
|
// no point in calling handleVideoThumbClicked; Quite
|
|
// simply, it won't work because it needs an src to attach
|
|
// to the large video.
|
|
//
|
|
// Instead, we trigger the pinned endpoint changed event to
|
|
// let the bridge adjust its lastN set for myjid and store
|
|
// the pinned user in the lastNPickupId variable to be
|
|
// picked up later by the lastN changed event handler.
|
|
|
|
lastNPickupId = id;
|
|
eventEmitter.emit(UIEvents.PINNED_ENDPOINT, remoteVideo, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
if (localVideoThumbnail.container === containerElement) {
|
|
return localVideoThumbnail.id;
|
|
}
|
|
|
|
let i = containerElement.id.indexOf('participant_');
|
|
|
|
if (i >= 0) {
|
|
return containerElement.id.substring(i + 12);
|
|
}
|
|
}
|
|
|
|
let largeVideo;
|
|
|
|
var VideoLayout = {
|
|
init (emitter) {
|
|
eventEmitter = emitter;
|
|
eventEmitter.addListener(UIEvents.LOCAL_FLIPX_CHANGED, function (val) {
|
|
localFlipX = val;
|
|
if(largeVideo)
|
|
largeVideo.onLocalFlipXChange(val);
|
|
});
|
|
localVideoThumbnail = new LocalVideo(VideoLayout, emitter);
|
|
// 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
|
|
let { localVideo } = this.resizeThumbnails(false, true);
|
|
AudioLevels.createAudioLevelCanvas(
|
|
"local", localVideo.thumbWidth, localVideo.thumbHeight);
|
|
|
|
emitter.addListener(UIEvents.CONTACT_CLICKED, onContactClicked);
|
|
this.lastNCount = config.channelLastN;
|
|
},
|
|
|
|
initLargeVideo () {
|
|
largeVideo = new LargeVideoManager(eventEmitter);
|
|
if(localFlipX) {
|
|
largeVideo.onLocalFlipXChange(localFlipX);
|
|
}
|
|
largeVideo.updateContainerSize();
|
|
AudioLevels.init();
|
|
},
|
|
|
|
setAudioLevel(id, lvl) {
|
|
if (!largeVideo) {
|
|
return;
|
|
}
|
|
AudioLevels.updateAudioLevel(
|
|
id, lvl, largeVideo.id
|
|
);
|
|
},
|
|
|
|
isInLastN (resource) {
|
|
return this.lastNCount < 0 || // lastN is disabled
|
|
// lastNEndpoints cache not built yet
|
|
(this.lastNCount > 0 && !lastNEndpointsCache.length) ||
|
|
(lastNEndpointsCache &&
|
|
lastNEndpointsCache.indexOf(resource) !== -1);
|
|
},
|
|
|
|
changeLocalAudio (stream) {
|
|
let localAudio = document.getElementById('localAudio');
|
|
localAudio = stream.attach(localAudio);
|
|
|
|
// Now when Temasys plugin is converting also <audio> elements to
|
|
// plugin's <object>s, in current layout it will capture click events
|
|
// before it reaches the local video object. We hide it here in order
|
|
// to prevent that.
|
|
//if (RTCBrowserType.isIExplorer()) {
|
|
// The issue is not present on Safari. Also if we hide it in Safari
|
|
// then the local audio track will have 'enabled' flag set to false
|
|
// which will result in audio mute issues
|
|
// $(localAudio).hide();
|
|
localAudio.width = 1;
|
|
localAudio.height = 1;
|
|
//}
|
|
},
|
|
|
|
changeLocalVideo (stream) {
|
|
let localId = APP.conference.getMyUserId();
|
|
this.onVideoTypeChanged(localId, stream.videoType);
|
|
|
|
if (!stream.isMuted()) {
|
|
localVideoThumbnail.changeVideo(stream);
|
|
}
|
|
|
|
/* force update if we're currently being displayed */
|
|
if (this.isCurrentlyOnLarge(localId)) {
|
|
this.updateLargeVideo(localId, true);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 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 () {
|
|
if (largeVideo && !largeVideo.id) {
|
|
this.updateLargeVideo(APP.conference.getMyUserId(), true);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Adds or removes icons for not available camera and microphone.
|
|
* @param resourceJid the jid of user
|
|
* @param devices available devices
|
|
*/
|
|
setDeviceAvailabilityIcons (id, devices) {
|
|
if (APP.conference.isLocalId(id)) {
|
|
localVideoThumbnail.setDeviceAvailabilityIcons(devices);
|
|
return;
|
|
}
|
|
|
|
let video = remoteVideos[id];
|
|
if (!video) {
|
|
return;
|
|
}
|
|
|
|
video.setDeviceAvailabilityIcons(devices);
|
|
},
|
|
|
|
/**
|
|
* Enables/disables device availability icons for the given participant id.
|
|
* The default value is {true}.
|
|
* @param id the identifier of the participant
|
|
* @param enable {true} to enable device availability icons
|
|
*/
|
|
enableDeviceAvailabilityIcons (id, enable) {
|
|
let video;
|
|
if (APP.conference.isLocalId(id)) {
|
|
video = localVideoThumbnail;
|
|
}
|
|
else {
|
|
video = remoteVideos[id];
|
|
}
|
|
|
|
if (video)
|
|
video.enableDeviceAvailabilityIcons(enable);
|
|
},
|
|
|
|
/**
|
|
* Shows/hides local video.
|
|
* @param {boolean} true to make the local video visible, false - otherwise
|
|
*/
|
|
setLocalVideoVisible(visible) {
|
|
localVideoThumbnail.setVisible(visible);
|
|
},
|
|
|
|
/**
|
|
* 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) {
|
|
if (!this.isCurrentlyOnLarge(id)) {
|
|
return;
|
|
}
|
|
|
|
let newId;
|
|
|
|
if (pinnedId)
|
|
newId = pinnedId;
|
|
else if (currentDominantSpeaker)
|
|
newId = currentDominantSpeaker;
|
|
else // Otherwise select last visible video
|
|
newId = this.electLastVisibleVideo();
|
|
|
|
this.updateLargeVideo(newId);
|
|
},
|
|
|
|
electLastVisibleVideo () {
|
|
// pick the last visible video in the row
|
|
// if nobody else is left, this picks the local video
|
|
let remoteThumbs = FilmStrip.getThumbs(true).remoteThumbs;
|
|
let thumbs = remoteThumbs.filter('[id!="mixedstream"]');
|
|
|
|
let lastVisible = thumbs.filter(':visible:last');
|
|
if (lastVisible.length) {
|
|
let id = getPeerContainerResourceId(lastVisible[0]);
|
|
if (remoteVideos[id]) {
|
|
console.info("electLastVisibleVideo: " + id);
|
|
return id;
|
|
}
|
|
// The RemoteVideo was removed (but the DOM elements may still
|
|
// exist).
|
|
}
|
|
|
|
console.info("Last visible video no longer exists");
|
|
thumbs = FilmStrip.getThumbs().remoteThumbs;
|
|
if (thumbs.length) {
|
|
let id = getPeerContainerResourceId(thumbs[0]);
|
|
if (remoteVideos[id]) {
|
|
console.info("electLastVisibleVideo: " + id);
|
|
return id;
|
|
}
|
|
// The RemoteVideo was removed (but the DOM elements may
|
|
// still exist).
|
|
}
|
|
|
|
// Go with local video
|
|
console.info("Fallback to local video...");
|
|
|
|
let id = APP.conference.getMyUserId();
|
|
console.info("electLastVisibleVideo: " + id);
|
|
|
|
return id;
|
|
},
|
|
|
|
onRemoteStreamAdded (stream) {
|
|
let id = stream.getParticipantId();
|
|
let remoteVideo = remoteVideos[id];
|
|
|
|
if (!remoteVideo)
|
|
return;
|
|
|
|
remoteVideo.addRemoteStreamElement(stream);
|
|
|
|
// if track is muted make sure we reflect that
|
|
if(stream.isMuted())
|
|
{
|
|
if(stream.getType() === "audio")
|
|
this.onAudioMute(stream.getParticipantId(), true);
|
|
else
|
|
this.onVideoMute(stream.getParticipantId(), true);
|
|
}
|
|
},
|
|
|
|
onRemoteStreamRemoved (stream) {
|
|
let id = stream.getParticipantId();
|
|
let remoteVideo = remoteVideos[id];
|
|
if (remoteVideo) { // remote stream may be removed after participant left the conference
|
|
remoteVideo.removeRemoteStreamElement(stream);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Return the type of the remote video.
|
|
* @param id the id for the remote video
|
|
* @returns {String} the video type video or screen.
|
|
*/
|
|
getRemoteVideoType (id) {
|
|
let smallVideo = VideoLayout.getSmallVideo(id);
|
|
return smallVideo ? smallVideo.getVideoType() : null;
|
|
},
|
|
|
|
isPinned (id) {
|
|
return (pinnedId) ? (id === pinnedId) : false;
|
|
},
|
|
|
|
getPinnedId () {
|
|
return pinnedId;
|
|
},
|
|
|
|
/**
|
|
* Handles the click on a video thumbnail.
|
|
*
|
|
* @param id the identifier of the video thumbnail
|
|
*/
|
|
handleVideoThumbClicked (id) {
|
|
if(pinnedId) {
|
|
var oldSmallVideo = VideoLayout.getSmallVideo(pinnedId);
|
|
if (oldSmallVideo && !interfaceConfig.filmStripOnly)
|
|
oldSmallVideo.focus(false);
|
|
}
|
|
|
|
var smallVideo = VideoLayout.getSmallVideo(id);
|
|
|
|
// Unpin if currently pinned.
|
|
if (pinnedId === id)
|
|
{
|
|
pinnedId = null;
|
|
// Enable the currently set dominant speaker.
|
|
if (currentDominantSpeaker) {
|
|
if(smallVideo && smallVideo.hasVideo()) {
|
|
this.updateLargeVideo(currentDominantSpeaker);
|
|
}
|
|
}
|
|
|
|
eventEmitter.emit(UIEvents.PINNED_ENDPOINT, smallVideo, false);
|
|
|
|
return;
|
|
}
|
|
|
|
// Lock new video
|
|
pinnedId = id;
|
|
|
|
// Update focused/pinned interface.
|
|
if (id) {
|
|
if (smallVideo && !interfaceConfig.filmStripOnly)
|
|
smallVideo.focus(true);
|
|
|
|
eventEmitter.emit(UIEvents.PINNED_ENDPOINT, smallVideo, true);
|
|
}
|
|
|
|
this.updateLargeVideo(id);
|
|
},
|
|
|
|
/**
|
|
* Creates or adds a participant container for the given id and smallVideo.
|
|
*
|
|
* @param {JitsiParticipant} user the participant to add
|
|
* @param {SmallVideo} smallVideo optional small video instance to add as a
|
|
* remote video, if undefined <tt>RemoteVideo</tt> will be created
|
|
*/
|
|
addParticipantContainer (user, smallVideo) {
|
|
let id = user.getId();
|
|
let remoteVideo;
|
|
if(smallVideo)
|
|
remoteVideo = smallVideo;
|
|
else
|
|
remoteVideo = new RemoteVideo(user, VideoLayout, eventEmitter);
|
|
this.addRemoteVideoContainer(id, remoteVideo);
|
|
},
|
|
|
|
/**
|
|
* 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) {
|
|
remoteVideos[id] = remoteVideo;
|
|
|
|
let videoType = VideoLayout.getRemoteVideoType(id);
|
|
if (!videoType) {
|
|
// make video type the default one (camera)
|
|
// FIXME container type is not a video type
|
|
videoType = VIDEO_CONTAINER_TYPE;
|
|
}
|
|
remoteVideo.setVideoType(videoType);
|
|
|
|
// In case this is not currently in the last n we don't show it.
|
|
if (localLastNCount && localLastNCount > 0 &&
|
|
FilmStrip.getThumbs().remoteThumbs.length >= localLastNCount + 2) {
|
|
remoteVideo.showPeerContainer('hide');
|
|
} else {
|
|
VideoLayout.resizeThumbnails(false, true);
|
|
}
|
|
// Initialize the view
|
|
remoteVideo.updateView();
|
|
},
|
|
|
|
videoactive (videoelem, resourceJid) {
|
|
|
|
console.info(resourceJid + " video is now active", videoelem);
|
|
|
|
VideoLayout.resizeThumbnails(
|
|
false, false, function() {$(videoelem).show();});
|
|
|
|
// 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.
|
|
if ((!pinnedId &&
|
|
!currentDominantSpeaker &&
|
|
this.isLargeContainerTypeVisible(VIDEO_CONTAINER_TYPE)) ||
|
|
pinnedId === resourceJid ||
|
|
(!pinnedId && resourceJid &&
|
|
currentDominantSpeaker === resourceJid) ||
|
|
/* Playback started while we're on the stage - may need to update
|
|
video source with the new stream */
|
|
this.isCurrentlyOnLarge(resourceJid)) {
|
|
|
|
this.updateLargeVideo(resourceJid, true);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Shows the presence status message for the given video.
|
|
*/
|
|
setPresenceStatus (id, statusMsg) {
|
|
let remoteVideo = remoteVideos[id];
|
|
if (remoteVideo)
|
|
remoteVideo.setPresenceStatus(statusMsg);
|
|
},
|
|
|
|
/**
|
|
* Shows a visual indicator for the moderator of the conference.
|
|
* On local or remote participants.
|
|
*/
|
|
showModeratorIndicator () {
|
|
let isModerator = APP.conference.isModerator;
|
|
if (isModerator) {
|
|
localVideoThumbnail.createModeratorIndicatorElement();
|
|
} else {
|
|
localVideoThumbnail.removeModeratorIndicatorElement();
|
|
}
|
|
|
|
APP.conference.listMembers().forEach(function (member) {
|
|
let id = member.getId();
|
|
let remoteVideo = remoteVideos[id];
|
|
if (!remoteVideo)
|
|
return;
|
|
|
|
if (member.isModerator()) {
|
|
remoteVideo.createModeratorIndicatorElement();
|
|
}
|
|
|
|
if (isModerator) {
|
|
// We are moderator, but user is not - add menu
|
|
if(!remoteVideo.hasRemoteVideoMenu) {
|
|
remoteVideo.addRemoteVideoMenu();
|
|
}
|
|
}
|
|
});
|
|
},
|
|
|
|
/*
|
|
* Shows or hides the audio muted indicator over the local thumbnail video.
|
|
* @param {boolean} isMuted
|
|
*/
|
|
showLocalAudioIndicator (isMuted) {
|
|
localVideoThumbnail.showAudioIndicator(isMuted);
|
|
},
|
|
|
|
/**
|
|
* Shows/hides the indication about local connection being interrupted.
|
|
*
|
|
* @param {boolean} isInterrupted <tt>true</tt> if local connection is
|
|
* currently in the interrupted state or <tt>false</tt> if the connection
|
|
* is fine.
|
|
*/
|
|
showLocalConnectionInterrupted (isInterrupted) {
|
|
localVideoThumbnail.connectionIndicator
|
|
.updateConnectionStatusIndicator(!isInterrupted);
|
|
},
|
|
|
|
/**
|
|
* Resizes thumbnails.
|
|
*/
|
|
resizeThumbnails ( animate = false,
|
|
forceUpdate = false,
|
|
onComplete = null) {
|
|
|
|
let { localVideo, remoteVideo }
|
|
= FilmStrip.calculateThumbnailSize();
|
|
|
|
let {thumbWidth, thumbHeight} = remoteVideo;
|
|
|
|
FilmStrip.resizeThumbnails(localVideo, remoteVideo,
|
|
animate, forceUpdate)
|
|
.then(function () {
|
|
AudioLevels.updateCanvasSize(localVideo, remoteVideo);
|
|
if (onComplete && typeof onComplete === "function")
|
|
onComplete();
|
|
});
|
|
return { localVideo, remoteVideo };
|
|
},
|
|
|
|
/**
|
|
* On audio muted event.
|
|
*/
|
|
onAudioMute (id, isMuted) {
|
|
if (APP.conference.isLocalId(id)) {
|
|
localVideoThumbnail.showAudioIndicator(isMuted);
|
|
} else {
|
|
let remoteVideo = remoteVideos[id];
|
|
if (!remoteVideo)
|
|
return;
|
|
|
|
remoteVideo.showAudioIndicator(isMuted);
|
|
if (APP.conference.isModerator) {
|
|
remoteVideo.updateRemoteVideoMenu(isMuted);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* On video muted event.
|
|
*/
|
|
onVideoMute (id, value) {
|
|
if (APP.conference.isLocalId(id)) {
|
|
localVideoThumbnail.setMutedView(value);
|
|
} else {
|
|
let remoteVideo = remoteVideos[id];
|
|
if (remoteVideo)
|
|
remoteVideo.setMutedView(value);
|
|
}
|
|
|
|
if (this.isCurrentlyOnLarge(id)) {
|
|
// large video will show avatar instead of muted stream
|
|
this.updateLargeVideo(id, true);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Display name changed.
|
|
*/
|
|
onDisplayNameChanged (id, displayName, status) {
|
|
if (id === 'localVideoContainer' ||
|
|
APP.conference.isLocalId(id)) {
|
|
localVideoThumbnail.setDisplayName(displayName);
|
|
} else {
|
|
let remoteVideo = remoteVideos[id];
|
|
if (remoteVideo)
|
|
remoteVideo.setDisplayName(displayName, status);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Sets the "raised hand" status for a participant identified by 'id'.
|
|
*/
|
|
setRaisedHandStatus(id, raisedHandStatus) {
|
|
var video
|
|
= APP.conference.isLocalId(id)
|
|
? localVideoThumbnail : remoteVideos[id];
|
|
if (video) {
|
|
video.showRaisedHandIndicator(raisedHandStatus);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* On dominant speaker changed event.
|
|
*/
|
|
onDominantSpeakerChanged (id) {
|
|
if (id === currentDominantSpeaker) {
|
|
return;
|
|
}
|
|
|
|
let oldSpeakerRemoteVideo = remoteVideos[currentDominantSpeaker];
|
|
// We ignore local user events, but just unmark remote user as dominant
|
|
// while we are talking
|
|
if (APP.conference.isLocalId(id)) {
|
|
if(oldSpeakerRemoteVideo)
|
|
{
|
|
oldSpeakerRemoteVideo.showDominantSpeakerIndicator(false);
|
|
currentDominantSpeaker = null;
|
|
}
|
|
localVideoThumbnail.showDominantSpeakerIndicator(true);
|
|
return;
|
|
}
|
|
|
|
let remoteVideo = remoteVideos[id];
|
|
if (!remoteVideo) {
|
|
return;
|
|
}
|
|
|
|
// Update the current dominant speaker.
|
|
remoteVideo.showDominantSpeakerIndicator(true);
|
|
localVideoThumbnail.showDominantSpeakerIndicator(false);
|
|
|
|
// let's remove the indications from the remote video if any
|
|
if (oldSpeakerRemoteVideo) {
|
|
oldSpeakerRemoteVideo.showDominantSpeakerIndicator(false);
|
|
}
|
|
currentDominantSpeaker = id;
|
|
|
|
// Local video will not have container found, but that's ok
|
|
// since we don't want to switch to local video.
|
|
// Update the large video if the video source is already available,
|
|
// otherwise wait for the "videoactive.jingle" event.
|
|
if (!pinnedId
|
|
&& remoteVideo.hasVideoStarted()
|
|
&& !this.getCurrentlyOnLargeContainer().stayOnStage()) {
|
|
this.updateLargeVideo(id);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Shows/hides warning about remote user's connectivity issues.
|
|
*
|
|
* @param {string} id the ID of the remote participant(MUC nickname)
|
|
* @param {boolean} isActive true if the connection is ok or false when
|
|
* the user is having connectivity issues.
|
|
*/
|
|
onParticipantConnectionStatusChanged (id, isActive) {
|
|
// Show/hide warning on the large video
|
|
if (this.isCurrentlyOnLarge(id)) {
|
|
if (largeVideo) {
|
|
// We have to trigger full large video update to transition from
|
|
// avatar to video on connectivity restored.
|
|
this.updateLargeVideo(id, true /* force update */);
|
|
}
|
|
}
|
|
// Show/hide warning on the thumbnail
|
|
let 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 lastNEndpoints the list of last N endpoints
|
|
* @param endpointsEnteringLastN the list currently entering last N
|
|
* endpoints
|
|
*/
|
|
onLastNEndpointsChanged (lastNEndpoints, endpointsEnteringLastN) {
|
|
if (this.lastNCount !== lastNEndpoints.length)
|
|
this.lastNCount = lastNEndpoints.length;
|
|
|
|
lastNEndpointsCache = lastNEndpoints;
|
|
|
|
// Say A, B, C, D, E, and F are in a conference and LastN = 3.
|
|
//
|
|
// If LastN drops to, say, 2, because of adaptivity, then E should see
|
|
// thumbnails for A, B and C. A and B are in E's server side LastN set,
|
|
// so E sees them. C is only in E's local LastN set.
|
|
//
|
|
// If F starts talking and LastN = 3, then E should see thumbnails for
|
|
// F, A, B. B gets "ejected" from E's server side LastN set, but it
|
|
// enters E's local LastN ejecting C.
|
|
|
|
// Increase the local LastN set size, if necessary.
|
|
if (this.lastNCount > localLastNCount) {
|
|
localLastNCount = this.lastNCount;
|
|
}
|
|
|
|
// Update the local LastN set preserving the order in which the
|
|
// endpoints appeared in the LastN/local LastN set.
|
|
var nextLocalLastNSet = lastNEndpoints.slice(0);
|
|
for (var i = 0; i < localLastNSet.length; i++) {
|
|
if (nextLocalLastNSet.length >= localLastNCount) {
|
|
break;
|
|
}
|
|
|
|
var resourceJid = localLastNSet[i];
|
|
if (nextLocalLastNSet.indexOf(resourceJid) === -1) {
|
|
nextLocalLastNSet.push(resourceJid);
|
|
}
|
|
}
|
|
|
|
localLastNSet = nextLocalLastNSet;
|
|
var updateLargeVideo = false;
|
|
|
|
// Handle LastN/local LastN changes.
|
|
FilmStrip.getThumbs().remoteThumbs.each(( index, element ) => {
|
|
var resourceJid = getPeerContainerResourceId(element);
|
|
var smallVideo = remoteVideos[resourceJid];
|
|
|
|
// We do not want to process any logic for our own(local) video
|
|
// because the local participant is never in the lastN set.
|
|
// The code of this function might detect that the local participant
|
|
// has been dropped out of the lastN set and will update the large
|
|
// video
|
|
// Detected from avatar tests, where lastN event override
|
|
// local video pinning
|
|
if(APP.conference.isLocalId(resourceJid))
|
|
return;
|
|
|
|
var isReceived = true;
|
|
if (resourceJid &&
|
|
lastNEndpoints.indexOf(resourceJid) < 0 &&
|
|
localLastNSet.indexOf(resourceJid) < 0) {
|
|
console.log("Remove from last N", resourceJid);
|
|
if (smallVideo)
|
|
smallVideo.showPeerContainer('hide');
|
|
else if (!APP.conference.isLocalId(resourceJid))
|
|
console.error("No remote video for: " + resourceJid);
|
|
isReceived = false;
|
|
} else if (resourceJid &&
|
|
//TOFIX: smallVideo may be undefined
|
|
smallVideo.isVisible() &&
|
|
lastNEndpoints.indexOf(resourceJid) < 0 &&
|
|
localLastNSet.indexOf(resourceJid) >= 0) {
|
|
|
|
// TOFIX: if we're here we already know that the smallVideo
|
|
// exists. Look at the previous FIX above.
|
|
if (smallVideo)
|
|
smallVideo.showPeerContainer('avatar');
|
|
else if (!APP.conference.isLocalId(resourceJid))
|
|
console.error("No remote video for: " + resourceJid);
|
|
isReceived = false;
|
|
}
|
|
|
|
if (!isReceived) {
|
|
// resourceJid has dropped out of the server side lastN set, so
|
|
// it is no longer being received. If resourceJid was being
|
|
// displayed in the large video we have to switch to another
|
|
// user.
|
|
if (!updateLargeVideo &&
|
|
this.isCurrentlyOnLarge(resourceJid)) {
|
|
updateLargeVideo = true;
|
|
}
|
|
}
|
|
});
|
|
|
|
if (!endpointsEnteringLastN || endpointsEnteringLastN.length < 0)
|
|
endpointsEnteringLastN = lastNEndpoints;
|
|
|
|
if (endpointsEnteringLastN && endpointsEnteringLastN.length > 0) {
|
|
endpointsEnteringLastN.forEach(function (resourceJid) {
|
|
|
|
var remoteVideo = remoteVideos[resourceJid];
|
|
if (remoteVideo)
|
|
remoteVideo.showPeerContainer('show');
|
|
|
|
if (!remoteVideo.isVisible()) {
|
|
console.log("Add to last N", resourceJid);
|
|
|
|
remoteVideo.addRemoteStreamElement(remoteVideo.videoStream);
|
|
|
|
if (lastNPickupId == resourceJid) {
|
|
// Clean up the lastN pickup id.
|
|
lastNPickupId = null;
|
|
|
|
VideoLayout.handleVideoThumbClicked(resourceJid);
|
|
|
|
updateLargeVideo = false;
|
|
}
|
|
remoteVideo.waitForPlayback(
|
|
remoteVideo.selectVideoElement()[0],
|
|
remoteVideo.videoStream);
|
|
}
|
|
});
|
|
}
|
|
|
|
// The endpoint that was being shown in the large video has dropped out
|
|
// of the lastN set and there was no lastN pickup jid. We need to update
|
|
// the large video now.
|
|
|
|
if (updateLargeVideo) {
|
|
var resource;
|
|
// Find out which endpoint to show in the large video.
|
|
for (i = 0; i < lastNEndpoints.length; i++) {
|
|
resource = lastNEndpoints[i];
|
|
if (!resource || APP.conference.isLocalId(resource))
|
|
continue;
|
|
|
|
// videoSrcToSsrc needs to be update for this call to succeed.
|
|
this.updateLargeVideo(resource);
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Updates local stats
|
|
* @param percent
|
|
* @param object
|
|
*/
|
|
updateLocalConnectionStats (percent, object) {
|
|
let resolutions = object.resolution;
|
|
|
|
object.resolution = resolutions[APP.conference.getMyUserId()];
|
|
localVideoThumbnail.updateStatsIndicator(percent, object);
|
|
|
|
Object.keys(resolutions).forEach(function (id) {
|
|
if (APP.conference.isLocalId(id)) {
|
|
return;
|
|
}
|
|
|
|
let resolution = resolutions[id];
|
|
let remoteVideo = remoteVideos[id];
|
|
|
|
if (resolution && remoteVideo) {
|
|
remoteVideo.updateResolution(resolution);
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Updates remote stats.
|
|
* @param id the id associated with the stats
|
|
* @param percent the connection quality percent
|
|
* @param object the stats data
|
|
*/
|
|
updateConnectionStats (id, percent, object) {
|
|
let remoteVideo = remoteVideos[id];
|
|
if (remoteVideo) {
|
|
remoteVideo.updateStatsIndicator(percent, object);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Hides the connection indicator
|
|
* @param id
|
|
*/
|
|
hideConnectionIndicator (id) {
|
|
let remoteVideo = remoteVideos[id];
|
|
if (remoteVideo)
|
|
remoteVideo.hideConnectionIndicator();
|
|
},
|
|
|
|
/**
|
|
* Hides all the indicators
|
|
*/
|
|
hideStats () {
|
|
for (var video in remoteVideos) {
|
|
let remoteVideo = remoteVideos[video];
|
|
if (remoteVideo)
|
|
remoteVideo.hideIndicator();
|
|
}
|
|
localVideoThumbnail.hideIndicator();
|
|
},
|
|
|
|
removeParticipantContainer (id) {
|
|
// Unlock large video
|
|
if (pinnedId === id) {
|
|
console.info("Focused video owner has left the conference");
|
|
pinnedId = null;
|
|
}
|
|
|
|
if (currentDominantSpeaker === id) {
|
|
console.info("Dominant speaker has left the conference");
|
|
currentDominantSpeaker = null;
|
|
}
|
|
|
|
var remoteVideo = remoteVideos[id];
|
|
if (remoteVideo) {
|
|
// Remove remote video
|
|
console.info("Removing remote video: " + id);
|
|
delete remoteVideos[id];
|
|
remoteVideo.remove();
|
|
} else {
|
|
console.warn("No remote video for " + id);
|
|
}
|
|
|
|
VideoLayout.resizeThumbnails();
|
|
},
|
|
|
|
onVideoTypeChanged (id, newVideoType) {
|
|
if (VideoLayout.getRemoteVideoType(id) === newVideoType) {
|
|
return;
|
|
}
|
|
|
|
console.info("Peer video type changed: ", id, newVideoType);
|
|
|
|
var smallVideo;
|
|
if (APP.conference.isLocalId(id)) {
|
|
if (!localVideoThumbnail) {
|
|
console.warn("Local video not ready yet");
|
|
return;
|
|
}
|
|
smallVideo = localVideoThumbnail;
|
|
} else if (remoteVideos[id]) {
|
|
smallVideo = remoteVideos[id];
|
|
} else {
|
|
return;
|
|
}
|
|
smallVideo.setVideoType(newVideoType);
|
|
|
|
if (this.isCurrentlyOnLarge(id)) {
|
|
this.updateLargeVideo(id, true);
|
|
}
|
|
},
|
|
|
|
showMore (id) {
|
|
if (id === 'local') {
|
|
localVideoThumbnail.connectionIndicator.showMore();
|
|
} else {
|
|
let remoteVideo = remoteVideos[id];
|
|
if (remoteVideo) {
|
|
remoteVideo.connectionIndicator.showMore();
|
|
} else {
|
|
console.info("Error - no remote video for id: " + id);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Resizes the video area.
|
|
*
|
|
* @param forceUpdate indicates that hidden thumbnails will be shown
|
|
* @param completeFunction a function to be called when the video area is
|
|
* resized.
|
|
*/
|
|
resizeVideoArea (forceUpdate = false,
|
|
animate = false,
|
|
completeFunction = null) {
|
|
|
|
if (largeVideo) {
|
|
largeVideo.updateContainerSize();
|
|
largeVideo.resize(animate);
|
|
}
|
|
|
|
// Calculate available width and height.
|
|
let availableHeight = window.innerHeight;
|
|
let availableWidth = UIUtil.getAvailableVideoWidth();
|
|
|
|
if (availableWidth < 0 || availableHeight < 0) {
|
|
return;
|
|
}
|
|
|
|
// Resize the thumbnails first.
|
|
this.resizeThumbnails(false, forceUpdate);
|
|
|
|
// Resize the video area element.
|
|
$('#videospace').animate({
|
|
right: window.innerWidth - availableWidth,
|
|
width: availableWidth,
|
|
height: availableHeight
|
|
}, {
|
|
queue: false,
|
|
duration: animate ? 500 : 1,
|
|
complete: completeFunction
|
|
});
|
|
},
|
|
|
|
getSmallVideo (id) {
|
|
if (APP.conference.isLocalId(id)) {
|
|
return localVideoThumbnail;
|
|
} else {
|
|
return remoteVideos[id];
|
|
}
|
|
},
|
|
|
|
changeUserAvatar (id, avatarUrl) {
|
|
var smallVideo = VideoLayout.getSmallVideo(id);
|
|
if (smallVideo) {
|
|
smallVideo.avatarChanged(avatarUrl);
|
|
} else {
|
|
console.warn(
|
|
"Missed avatar update - no small video yet for " + id
|
|
);
|
|
}
|
|
if (this.isCurrentlyOnLarge(id)) {
|
|
largeVideo.updateAvatar(avatarUrl);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Indicates that the video has been interrupted.
|
|
*/
|
|
onVideoInterrupted () {
|
|
if (largeVideo) {
|
|
largeVideo.onVideoInterrupted();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Indicates that the video has been restored.
|
|
*/
|
|
onVideoRestored () {
|
|
if (largeVideo) {
|
|
largeVideo.onVideoRestored();
|
|
}
|
|
},
|
|
|
|
isLargeVideoVisible () {
|
|
return this.isLargeContainerTypeVisible(VIDEO_CONTAINER_TYPE);
|
|
},
|
|
|
|
/**
|
|
* @return {LargeContainer} the currently displayed container on large
|
|
* video.
|
|
*/
|
|
getCurrentlyOnLargeContainer () {
|
|
return largeVideo.getContainer(largeVideo.state);
|
|
},
|
|
|
|
isCurrentlyOnLarge (id) {
|
|
return largeVideo && largeVideo.id === id;
|
|
},
|
|
|
|
updateLargeVideo (id, forceUpdate) {
|
|
if (!largeVideo) {
|
|
return;
|
|
}
|
|
let isOnLarge = this.isCurrentlyOnLarge(id);
|
|
let currentId = largeVideo.id;
|
|
|
|
if (!isOnLarge || forceUpdate) {
|
|
let videoType = this.getRemoteVideoType(id);
|
|
// FIXME video type is not the same thing as container type
|
|
if (id !== currentId && videoType === VIDEO_CONTAINER_TYPE) {
|
|
eventEmitter.emit(UIEvents.SELECTED_ENDPOINT, id);
|
|
}
|
|
|
|
let smallVideo = this.getSmallVideo(id);
|
|
let oldSmallVideo;
|
|
if (currentId) {
|
|
oldSmallVideo = this.getSmallVideo(currentId);
|
|
}
|
|
|
|
smallVideo.waitForResolutionChange();
|
|
if (oldSmallVideo)
|
|
oldSmallVideo.waitForResolutionChange();
|
|
|
|
largeVideo.updateLargeVideo(
|
|
id,
|
|
smallVideo.videoStream,
|
|
videoType
|
|
).then(function() {
|
|
// update current small video and the old one
|
|
smallVideo.updateView();
|
|
oldSmallVideo && oldSmallVideo.updateView();
|
|
}, function () {
|
|
// use clicked other video during update, nothing to do.
|
|
});
|
|
|
|
} else if (currentId) {
|
|
let currentSmallVideo = this.getSmallVideo(currentId);
|
|
currentSmallVideo.updateView();
|
|
}
|
|
},
|
|
|
|
addLargeVideoContainer (type, container) {
|
|
largeVideo && largeVideo.addContainer(type, container);
|
|
},
|
|
|
|
removeLargeVideoContainer (type) {
|
|
largeVideo && largeVideo.removeContainer(type);
|
|
},
|
|
|
|
/**
|
|
* @returns Promise
|
|
*/
|
|
showLargeVideoContainer (type, show) {
|
|
if (!largeVideo) {
|
|
return Promise.reject();
|
|
}
|
|
|
|
let isVisible = this.isLargeContainerTypeVisible(type);
|
|
if (isVisible === show) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
let currentId = largeVideo.id;
|
|
if(currentId) {
|
|
var 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) {
|
|
if(pinnedId)
|
|
containerTypeToShow = this.getRemoteVideoType(pinnedId);
|
|
else
|
|
containerTypeToShow = VIDEO_CONTAINER_TYPE;
|
|
}
|
|
|
|
return largeVideo.showContainer(containerTypeToShow)
|
|
.then(() => {
|
|
if(oldSmallVideo)
|
|
oldSmallVideo && oldSmallVideo.updateView();
|
|
});
|
|
},
|
|
|
|
isLargeContainerTypeVisible (type) {
|
|
return largeVideo && largeVideo.state === type;
|
|
},
|
|
|
|
/**
|
|
* Returns the id of the current video shown on large.
|
|
* Currently used by tests (torture).
|
|
*/
|
|
getLargeVideoID () {
|
|
return largeVideo.id;
|
|
},
|
|
|
|
/**
|
|
* Returns the the current video shown on large.
|
|
* Currently used by tests (torture).
|
|
*/
|
|
getLargeVideo () {
|
|
return largeVideo;
|
|
},
|
|
|
|
/**
|
|
* Updates the resolution label, indicating to the user that the large
|
|
* video stream is currently HD.
|
|
*/
|
|
updateResolutionLabel(isResolutionHD) {
|
|
let videoResolutionLabel = $("#videoResolutionLabel");
|
|
|
|
if (isResolutionHD && !videoResolutionLabel.is(":visible"))
|
|
videoResolutionLabel.css({display: "block"});
|
|
else if (!isResolutionHD && videoResolutionLabel.is(":visible"))
|
|
videoResolutionLabel.css({display: "none"});
|
|
},
|
|
|
|
/**
|
|
* Sets the flipX state of the local video.
|
|
* @param {boolean} true for flipped otherwise false;
|
|
*/
|
|
setLocalFlipX: function (val) {
|
|
this.localFlipX = val;
|
|
|
|
},
|
|
|
|
getEventEmitter: () => {return eventEmitter;}
|
|
};
|
|
|
|
export default VideoLayout;
|