mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-05-20 17:37:47 +00:00
The local audio stream is attached to an audio element that is always muted. As such, local audio is not being rendered and the attaching may just not be needed at all.
1173 lines
36 KiB
JavaScript
1173 lines
36 KiB
JavaScript
/* global APP, $, interfaceConfig, JitsiMeetJS */
|
|
const logger = require("jitsi-meet-logger").getLogger(__filename);
|
|
|
|
import {
|
|
getPinnedParticipant,
|
|
pinParticipant
|
|
} from '../../../react/features/base/participants';
|
|
|
|
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 LocalVideo from "./LocalVideo";
|
|
|
|
const ParticipantConnectionStatus
|
|
= JitsiMeetJS.constants.participantConnectionStatus;
|
|
|
|
var remoteVideos = {};
|
|
var localVideoThumbnail = null;
|
|
|
|
var currentDominantSpeaker = null;
|
|
|
|
var eventEmitter = 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.
|
|
this.pinParticipant(remoteVideo.id);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handler for local flip X changed event.
|
|
* @param {Object} val
|
|
*/
|
|
function onLocalFlipXChanged (val) {
|
|
localFlipX = val;
|
|
if(largeVideo) {
|
|
largeVideo.onLocalFlipXChange(val);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
|
|
// Unregister listeners in case of reinitialization
|
|
this.unregisterListeners();
|
|
|
|
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
|
|
this.resizeThumbnails(false, true);
|
|
|
|
this.registerListeners();
|
|
},
|
|
|
|
/**
|
|
* Cleans up any existing largeVideo instance.
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
resetLargeVideo() {
|
|
if (largeVideo) {
|
|
largeVideo.destroy();
|
|
}
|
|
largeVideo = null;
|
|
},
|
|
|
|
/**
|
|
* Registering listeners for UI events in Video layout component.
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
registerListeners() {
|
|
eventEmitter.addListener(UIEvents.LOCAL_FLIPX_CHANGED,
|
|
onLocalFlipXChanged);
|
|
eventEmitter.addListener(UIEvents.CONTACT_CLICKED, onContactClicked);
|
|
},
|
|
|
|
/**
|
|
* Unregistering listeners for UI events in Video layout component.
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
unregisterListeners() {
|
|
eventEmitter.removeListener(UIEvents.CONTACT_CLICKED, onContactClicked);
|
|
},
|
|
|
|
initLargeVideo () {
|
|
this.resetLargeVideo();
|
|
|
|
largeVideo = new LargeVideoManager(eventEmitter);
|
|
if(localFlipX) {
|
|
largeVideo.onLocalFlipXChange(localFlipX);
|
|
}
|
|
largeVideo.updateContainerSize();
|
|
},
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
setAudioLevel(id, lvl) {
|
|
let smallVideo = this.getSmallVideo(id);
|
|
if (smallVideo)
|
|
smallVideo.updateAudioLevelIndicator(lvl);
|
|
|
|
if (largeVideo && id === largeVideo.id)
|
|
largeVideo.updateLargeVideoAudioLevel(lvl);
|
|
},
|
|
|
|
changeLocalVideo (stream) {
|
|
let localId = APP.conference.getMyUserId();
|
|
this.onVideoTypeChanged(localId, stream.videoType);
|
|
|
|
localVideoThumbnail.changeVideo(stream);
|
|
|
|
/* Update if we're currently being displayed */
|
|
if (this.isCurrentlyOnLarge(localId)) {
|
|
this.updateLargeVideo(localId);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
|
|
// 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();
|
|
},
|
|
|
|
/**
|
|
* 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) {
|
|
// Always trigger an update if large video is empty.
|
|
if (!largeVideo
|
|
|| (this.getLargeVideoID() && !this.isCurrentlyOnLarge(id))) {
|
|
return;
|
|
}
|
|
|
|
const pinnedId = this.getPinnedId();
|
|
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]) {
|
|
logger.info("electLastVisibleVideo: " + id);
|
|
return id;
|
|
}
|
|
// The RemoteVideo was removed (but the DOM elements may still
|
|
// exist).
|
|
}
|
|
|
|
logger.info("Last visible video no longer exists");
|
|
thumbs = Filmstrip.getThumbs().remoteThumbs;
|
|
if (thumbs.length) {
|
|
let id = getPeerContainerResourceId(thumbs[0]);
|
|
if (remoteVideos[id]) {
|
|
logger.info("electLastVisibleVideo: " + id);
|
|
return id;
|
|
}
|
|
// The RemoteVideo was removed (but the DOM elements may
|
|
// still exist).
|
|
}
|
|
|
|
// Go with local video
|
|
logger.info("Fallback to local video...");
|
|
|
|
let id = APP.conference.getMyUserId();
|
|
logger.info("electLastVisibleVideo: " + id);
|
|
|
|
return id;
|
|
},
|
|
|
|
onRemoteStreamAdded (stream) {
|
|
let id = stream.getParticipantId();
|
|
let remoteVideo = remoteVideos[id];
|
|
|
|
if (!remoteVideo)
|
|
return;
|
|
|
|
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());
|
|
}
|
|
},
|
|
|
|
onRemoteStreamRemoved (stream) {
|
|
let id = stream.getParticipantId();
|
|
let remoteVideo = remoteVideos[id];
|
|
// Remote stream may be removed after participant left the conference.
|
|
if (remoteVideo) {
|
|
remoteVideo.removeRemoteStreamElement(stream);
|
|
}
|
|
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.
|
|
* @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 id === this.getPinnedId();
|
|
},
|
|
|
|
getPinnedId () {
|
|
const { id } = getPinnedParticipant(APP.store.getState()) || {};
|
|
|
|
return id || null;
|
|
},
|
|
|
|
/**
|
|
* Updates the desired pinned participant and notifies web UI of the change.
|
|
*
|
|
* @param {string|null} id - The participant id of the participant to be
|
|
* pinned. Pass in null to unpin without pinning another participant.
|
|
* @returns {void}
|
|
*/
|
|
pinParticipant(id) {
|
|
APP.store.dispatch(pinParticipant(id));
|
|
APP.UI.emitEvent(UIEvents.PINNED_ENDPOINT, id, Boolean(id));
|
|
},
|
|
|
|
/**
|
|
* Handles the click on a video thumbnail.
|
|
*
|
|
* @param id the identifier of the video thumbnail
|
|
*/
|
|
handleVideoThumbClicked (id) {
|
|
var smallVideo = VideoLayout.getSmallVideo(id);
|
|
const pinnedId = this.getPinnedId();
|
|
|
|
if(pinnedId) {
|
|
var oldSmallVideo = VideoLayout.getSmallVideo(pinnedId);
|
|
if (oldSmallVideo && !interfaceConfig.filmStripOnly) {
|
|
oldSmallVideo.focus(false);
|
|
}
|
|
}
|
|
|
|
// Unpin if currently pinned.
|
|
if (pinnedId === id)
|
|
{
|
|
this.pinParticipant(null);
|
|
|
|
// Enable the currently set dominant speaker.
|
|
if (currentDominantSpeaker) {
|
|
if(smallVideo && smallVideo.hasVideo()) {
|
|
this.updateLargeVideo(currentDominantSpeaker);
|
|
}
|
|
} else {
|
|
// if there is no currentDominantSpeaker, 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());
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Update focused/pinned interface.
|
|
if (id) {
|
|
if (smallVideo && !interfaceConfig.filmStripOnly) {
|
|
smallVideo.focus(true);
|
|
this.pinParticipant(id);
|
|
}
|
|
}
|
|
|
|
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._setRemoteControlProperties(user, 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) {
|
|
remoteVideos[id] = remoteVideo;
|
|
|
|
if (!remoteVideo.getVideoType()) {
|
|
// make video type the default one (camera)
|
|
// FIXME container type is not a video type
|
|
remoteVideo.setVideoType(VIDEO_CONTAINER_TYPE);
|
|
}
|
|
|
|
VideoLayout.resizeThumbnails(false, true);
|
|
|
|
// Initialize the view
|
|
remoteVideo.updateView();
|
|
},
|
|
|
|
// FIXME: what does this do???
|
|
remoteVideoActive(videoElement, resourceJid) {
|
|
|
|
logger.info(resourceJid + " video is now active", videoElement);
|
|
|
|
VideoLayout.resizeThumbnails(
|
|
false, false, () => {
|
|
if (videoElement) {
|
|
$(videoElement).show();
|
|
}
|
|
});
|
|
|
|
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 &&
|
|
!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 a visual indicator for the moderator of the conference.
|
|
* On local or remote participants.
|
|
*/
|
|
showModeratorIndicator () {
|
|
let isModerator = APP.conference.isModerator;
|
|
if (isModerator) {
|
|
localVideoThumbnail.addModeratorIndicator();
|
|
} else {
|
|
localVideoThumbnail.removeModeratorIndicator();
|
|
}
|
|
|
|
APP.conference.listMembers().forEach(function (member) {
|
|
let id = member.getId();
|
|
let remoteVideo = remoteVideos[id];
|
|
if (!remoteVideo)
|
|
return;
|
|
|
|
if (member.isModerator()) {
|
|
remoteVideo.addModeratorIndicator();
|
|
}
|
|
|
|
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) {
|
|
// Currently local video thumbnail displays only "active" or
|
|
// "interrupted" despite the fact that ConnectionIndicator supports more
|
|
// states.
|
|
const status
|
|
= isInterrupted
|
|
? ParticipantConnectionStatus.INTERRUPTED
|
|
: ParticipantConnectionStatus.ACTIVE;
|
|
|
|
localVideoThumbnail.updateConnectionStatus(status);
|
|
},
|
|
|
|
/**
|
|
* Resizes thumbnails.
|
|
*/
|
|
resizeThumbnails ( animate = false,
|
|
forceUpdate = false,
|
|
onComplete = null) {
|
|
const { localVideo, remoteVideo }
|
|
= Filmstrip.calculateThumbnailSize();
|
|
|
|
Filmstrip.resizeThumbnails(localVideo, remoteVideo,
|
|
animate, forceUpdate)
|
|
.then(function () {
|
|
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);
|
|
remoteVideo.updateRemoteVideoMenu(isMuted);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* On video muted event.
|
|
*/
|
|
onVideoMute (id, value) {
|
|
if (APP.conference.isLocalId(id)) {
|
|
localVideoThumbnail.setVideoMutedView(value);
|
|
} else {
|
|
let remoteVideo = remoteVideos[id];
|
|
if (remoteVideo)
|
|
remoteVideo.setVideoMutedView(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);
|
|
if (raisedHandStatus) {
|
|
video.showDominantSpeakerIndicator(false);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 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.
|
|
// FIXME: there is no "videoactive.jingle" event.
|
|
if (!interfaceConfig.filmStripOnly && !this.getPinnedId()
|
|
&& 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)
|
|
*/
|
|
// eslint-disable-next-line no-unused-vars
|
|
onParticipantConnectionStatusChanged (id) {
|
|
// 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 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);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Hides the connection indicator
|
|
* @param id
|
|
*/
|
|
hideConnectionIndicator (id) {
|
|
let remoteVideo = remoteVideos[id];
|
|
if (remoteVideo)
|
|
remoteVideo.removeConnectionIndicator();
|
|
},
|
|
|
|
/**
|
|
* Hides all the indicators
|
|
*/
|
|
hideStats () {
|
|
for (var video in remoteVideos) {
|
|
let remoteVideo = remoteVideos[video];
|
|
if (remoteVideo)
|
|
remoteVideo.removeConnectionIndicator();
|
|
}
|
|
localVideoThumbnail.removeConnectionIndicator();
|
|
},
|
|
|
|
removeParticipantContainer (id) {
|
|
// Unlock large video
|
|
if (this.getPinnedId() === id) {
|
|
logger.info("Focused video owner has left the conference");
|
|
this.pinParticipant(null);
|
|
}
|
|
|
|
if (currentDominantSpeaker === id) {
|
|
logger.info("Dominant speaker has left the conference");
|
|
currentDominantSpeaker = null;
|
|
}
|
|
|
|
var remoteVideo = remoteVideos[id];
|
|
if (remoteVideo) {
|
|
// Remove remote video
|
|
logger.info("Removing remote video: " + id);
|
|
delete remoteVideos[id];
|
|
remoteVideo.remove();
|
|
} else {
|
|
logger.warn("No remote video for " + id);
|
|
}
|
|
|
|
VideoLayout.resizeThumbnails();
|
|
},
|
|
|
|
onVideoTypeChanged (id, newVideoType) {
|
|
if (VideoLayout.getRemoteVideoType(id) === newVideoType) {
|
|
return;
|
|
}
|
|
|
|
logger.info("Peer video type changed: ", id, newVideoType);
|
|
|
|
var smallVideo;
|
|
if (APP.conference.isLocalId(id)) {
|
|
if (!localVideoThumbnail) {
|
|
logger.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);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 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 {
|
|
logger.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.getCurrentContainer();
|
|
},
|
|
|
|
isCurrentlyOnLarge (id) {
|
|
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) {
|
|
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;
|
|
}
|
|
}
|
|
|
|
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 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) {
|
|
const pinnedId = this.getPinnedId();
|
|
|
|
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;
|
|
},
|
|
|
|
/**
|
|
* 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) {
|
|
let video = this.getSmallVideo(user.getId());
|
|
|
|
if (!video) {
|
|
return;
|
|
}
|
|
this._setRemoteControlProperties(user, video);
|
|
},
|
|
|
|
/**
|
|
* 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()
|
|
);
|
|
}
|
|
};
|
|
|
|
export default VideoLayout;
|