feat(pinning): move web pinning logic into redux

- Re-use the native redux pinning implementation for web
- Remove pinning logic from conference.js
- To the native pinning add a check for sharedVideo so
  youtube videos do not send a pin event
- Add shared videos as a participant to enable pinning and
  so they can eventually get added to the filmstrip
- Emit UIEvents.PINNED_ENDPOINT from middleware
This commit is contained in:
Leonard Kim 2017-06-27 15:56:55 -07:00 committed by Paweł Domas
parent 19d9b3f023
commit f1f46e0af5
8 changed files with 104 additions and 63 deletions

View File

@ -83,8 +83,6 @@ let room;
let connection; let connection;
let localAudio, localVideo; let localAudio, localVideo;
import {VIDEO_CONTAINER_TYPE} from "./modules/UI/videolayout/VideoContainer";
/* /*
* Logic to open a desktop picker put on the window global for * Logic to open a desktop picker put on the window global for
* lib-jitsi-meet to detect and invoke * lib-jitsi-meet to detect and invoke
@ -1777,37 +1775,9 @@ export default {
UIEvents.VIDEO_UNMUTING_WHILE_AUDIO_ONLY, UIEvents.VIDEO_UNMUTING_WHILE_AUDIO_ONLY,
() => this._displayAudioOnlyTooltip('videoMute')); () => this._displayAudioOnlyTooltip('videoMute'));
APP.UI.addListener(UIEvents.PINNED_ENDPOINT, APP.UI.addListener(
(smallVideo, isPinned) => { UIEvents.PINNED_ENDPOINT,
let smallVideoId = smallVideo.getId(); updateRemoteThumbnailsVisibility);
let isLocal = APP.conference.isLocalId(smallVideoId);
let eventName
= (isPinned ? "pinned" : "unpinned") + "." +
(isLocal ? "local" : "remote");
let participantCount = room.getParticipantCount();
JitsiMeetJS.analytics.sendEvent(
eventName,
{ value: participantCount });
// FIXME why VIDEO_CONTAINER_TYPE instead of checking if
// the participant is on the large video ?
if (smallVideo.getVideoType() === VIDEO_CONTAINER_TYPE
&& !isLocal) {
// When the library starts supporting multiple pins we
// would pass the isPinned parameter together with the
// identifier, but currently we send null to indicate that
// we unpin the last pinned.
try {
room.pinParticipant(isPinned ? smallVideoId : null);
} catch (e) {
reportError(e);
}
}
updateRemoteThumbnailsVisibility();
});
} }
room.on(ConferenceEvents.CONNECTION_INTERRUPTED, () => { room.on(ConferenceEvents.CONNECTION_INTERRUPTED, () => {

View File

@ -153,7 +153,7 @@ class FollowMe {
smallVideo = VideoLayout.getSmallVideo(pinnedId); smallVideo = VideoLayout.getSmallVideo(pinnedId);
} }
this._nextOnStage(smallVideo, isPinned); this._nextOnStage(smallVideo.getId(), isPinned);
// check whether shared document is enabled/initialized // check whether shared document is enabled/initialized
if(this._UI.getSharedDocumentManager()) if(this._UI.getSharedDocumentManager())
@ -174,8 +174,8 @@ class FollowMe {
this.filmstripEventHandler); this.filmstripEventHandler);
var self = this; var self = this;
this.pinnedEndpointEventHandler = function (smallVideo, isPinned) { this.pinnedEndpointEventHandler = function (videoId, isPinned) {
self._nextOnStage(smallVideo, isPinned); self._nextOnStage(videoId, isPinned);
}; };
this._UI.addListener(UIEvents.PINNED_ENDPOINT, this._UI.addListener(UIEvents.PINNED_ENDPOINT,
this.pinnedEndpointEventHandler); this.pinnedEndpointEventHandler);
@ -243,13 +243,13 @@ class FollowMe {
* unpinned * unpinned
* @private * @private
*/ */
_nextOnStage (smallVideo, isPinned) { _nextOnStage (videoId, isPinned) {
if (!this._conference.isModerator) if (!this._conference.isModerator)
return; return;
var nextOnStage = null; var nextOnStage = null;
if(isPinned) if(isPinned)
nextOnStage = smallVideo.getId(); nextOnStage = videoId;
this._local.nextOnStage = nextOnStage; this._local.nextOnStage = nextOnStage;
} }

View File

@ -10,6 +10,10 @@ import LargeContainer from '../videolayout/LargeContainer';
import SmallVideo from '../videolayout/SmallVideo'; import SmallVideo from '../videolayout/SmallVideo';
import Filmstrip from '../videolayout/Filmstrip'; import Filmstrip from '../videolayout/Filmstrip';
import {
participantJoined,
participantLeft
} from '../../../react/features/base/participants';
import { dockToolbox, showToolbox } from '../../../react/features/toolbox'; import { dockToolbox, showToolbox } from '../../../react/features/toolbox';
export const SHARED_VIDEO_CONTAINER_TYPE = "sharedvideo"; export const SHARED_VIDEO_CONTAINER_TYPE = "sharedvideo";
@ -267,6 +271,13 @@ export default class SharedVideoManager {
VideoLayout.addLargeVideoContainer( VideoLayout.addLargeVideoContainer(
SHARED_VIDEO_CONTAINER_TYPE, self.sharedVideo); SHARED_VIDEO_CONTAINER_TYPE, self.sharedVideo);
APP.store.dispatch(participantJoined({
id: self.url,
isBot: true,
name: player.getVideoData().title
}));
VideoLayout.handleVideoThumbClicked(self.url); VideoLayout.handleVideoThumbClicked(self.url);
// If we are sending the command and we are starting the player // If we are sending the command and we are starting the player
@ -461,6 +472,8 @@ export default class SharedVideoManager {
UIEvents.UPDATE_SHARED_VIDEO, null, 'removed'); UIEvents.UPDATE_SHARED_VIDEO, null, 'removed');
}); });
APP.store.dispatch(participantLeft(this.url));
this.url = null; this.url = null;
this.isSharedVideoShown = false; this.isSharedVideoShown = false;
this.initialAttributes = null; this.initialAttributes = null;

View File

@ -1,6 +1,8 @@
/* global APP, $, interfaceConfig, JitsiMeetJS */ /* global APP, $, interfaceConfig, JitsiMeetJS */
const logger = require("jitsi-meet-logger").getLogger(__filename); const logger = require("jitsi-meet-logger").getLogger(__filename);
import { pinParticipant } from '../../../react/features/base/participants';
import Filmstrip from "./Filmstrip"; import Filmstrip from "./Filmstrip";
import UIEvents from "../../../service/UI/UIEvents"; import UIEvents from "../../../service/UI/UIEvents";
import UIUtil from "../util/UIUtil"; import UIUtil from "../util/UIUtil";
@ -20,6 +22,8 @@ var currentDominantSpeaker = null;
var eventEmitter = null; var eventEmitter = null;
// TODO Remove this private reference to pinnedId once other components
// interested in its updates are moved to react/redux.
/** /**
* Currently focused video jid * Currently focused video jid
* @type {String} * @type {String}
@ -59,7 +63,7 @@ function onContactClicked (id) {
// let the bridge adjust its lastN set for myjid and store // let the bridge adjust its lastN set for myjid and store
// the pinned user in the lastNPickupId variable to be // the pinned user in the lastNPickupId variable to be
// picked up later by the lastN changed event handler. // picked up later by the lastN changed event handler.
eventEmitter.emit(UIEvents.PINNED_ENDPOINT, remoteVideo, true); APP.store.dispatch(pinParticipant(remoteVideo.id));
} }
} }
} }
@ -406,12 +410,6 @@ var VideoLayout = {
var oldSmallVideo = VideoLayout.getSmallVideo(pinnedId); var oldSmallVideo = VideoLayout.getSmallVideo(pinnedId);
if (oldSmallVideo && !interfaceConfig.filmStripOnly) { if (oldSmallVideo && !interfaceConfig.filmStripOnly) {
oldSmallVideo.focus(false); oldSmallVideo.focus(false);
// as no pinned event will be sent for local video
// and we will unpin old one, lets signal it
// otherwise we will just send the new pinned one
if (smallVideo.isLocal)
eventEmitter.emit(
UIEvents.PINNED_ENDPOINT, oldSmallVideo, false);
} }
} }
@ -419,6 +417,9 @@ var VideoLayout = {
if (pinnedId === id) if (pinnedId === id)
{ {
pinnedId = null; pinnedId = null;
APP.store.dispatch(pinParticipant(null));
// Enable the currently set dominant speaker. // Enable the currently set dominant speaker.
if (currentDominantSpeaker) { if (currentDominantSpeaker) {
if(smallVideo && smallVideo.hasVideo()) { if(smallVideo && smallVideo.hasVideo()) {
@ -432,8 +433,6 @@ var VideoLayout = {
this.updateLargeVideo(this.electLastVisibleVideo()); this.updateLargeVideo(this.electLastVisibleVideo());
} }
eventEmitter.emit(UIEvents.PINNED_ENDPOINT, smallVideo, false);
return; return;
} }
@ -442,10 +441,10 @@ var VideoLayout = {
// Update focused/pinned interface. // Update focused/pinned interface.
if (id) { if (id) {
if (smallVideo && !interfaceConfig.filmStripOnly) if (smallVideo && !interfaceConfig.filmStripOnly) {
smallVideo.focus(true); smallVideo.focus(true);
APP.store.dispatch(pinParticipant(id));
eventEmitter.emit(UIEvents.PINNED_ENDPOINT, smallVideo, true); }
} }
this.updateLargeVideo(id); this.updateLargeVideo(id);
@ -823,6 +822,7 @@ var VideoLayout = {
if (pinnedId === id) { if (pinnedId === id) {
logger.info("Focused video owner has left the conference"); logger.info("Focused video owner has left the conference");
pinnedId = null; pinnedId = null;
APP.store.dispatch(pinParticipant(null));
} }
if (currentDominantSpeaker === id) { if (currentDominantSpeaker === id) {

View File

@ -2,10 +2,12 @@
import UIEvents from '../../../../service/UI/UIEvents'; import UIEvents from '../../../../service/UI/UIEvents';
import { CONNECTION_ESTABLISHED } from '../connection'; import { CONNECTION_ESTABLISHED } from '../connection';
import JitsiMeetJS from '../lib-jitsi-meet';
import { setVideoMuted, VIDEO_MUTISM_AUTHORITY } from '../media'; import { setVideoMuted, VIDEO_MUTISM_AUTHORITY } from '../media';
import { import {
getLocalParticipant, getLocalParticipant,
getParticipantById, getParticipantById,
getPinnedParticipant,
PIN_PARTICIPANT PIN_PARTICIPANT
} from '../participants'; } from '../participants';
import { MiddlewareRegistry } from '../redux'; import { MiddlewareRegistry } from '../redux';
@ -168,27 +170,46 @@ function _conferenceJoined(store, next, action) {
*/ */
function _pinParticipant(store, next, action) { function _pinParticipant(store, next, action) {
const state = store.getState(); const state = store.getState();
const { conference } = state['features/base/conference'];
const participants = state['features/base/participants']; const participants = state['features/base/participants'];
const id = action.participant.id; const id = action.participant.id;
const participantById = getParticipantById(participants, id); const participantById = getParticipantById(participants, id);
let pin; let pin;
// The following condition prevents signaling to pin local participant. The const shouldEmitToLegacyApp = typeof APP !== 'undefined';
// logic is:
if (shouldEmitToLegacyApp) {
const pinnedParticipant = getPinnedParticipant(participants);
const actionName = action.participant.id ? 'pinned' : 'unpinned';
let videoType;
if ((participantById && participantById.local)
|| (!id && pinnedParticipant && pinnedParticipant.local)) {
videoType = 'local';
} else {
videoType = 'remote';
}
JitsiMeetJS.analytics.sendEvent(
`${actionName}.${videoType}`,
{ value: conference.getParticipantCount() });
}
// The following condition prevents signaling to pin local participant and
// shared videos. The logic is:
// - If we have an ID, we check if the participant identified by that ID is // - If we have an ID, we check if the participant identified by that ID is
// local. // local or a bot/fake participant (such as with shared video).
// - If we don't have an ID (i.e. no participant identified by an ID), we // - If we don't have an ID (i.e. no participant identified by an ID), we
// check for local participant. If she's currently pinned, then this // check for local participant. If she's currently pinned, then this
// action will unpin her and that's why we won't signal here too. // action will unpin her and that's why we won't signal here too.
if (participantById) { if (participantById) {
pin = !participantById.local; pin = !participantById.local && !participantById.isBot;
} else { } else {
const localParticipant = getLocalParticipant(participants); const localParticipant = getLocalParticipant(participants);
pin = !localParticipant || !localParticipant.pinned; pin = !localParticipant || !localParticipant.pinned;
} }
if (pin) { if (pin) {
const { conference } = state['features/base/conference'];
try { try {
conference.pinParticipant(id); conference.pinParticipant(id);
@ -197,6 +218,10 @@ function _pinParticipant(store, next, action) {
} }
} }
if (shouldEmitToLegacyApp) {
APP.UI.emitEvent(UIEvents.PINNED_ENDPOINT, id, Boolean(id));
}
return next(action); return next(action);
} }

View File

@ -98,6 +98,38 @@ export function getParticipantById(stateOrGetState, id) {
return participants.find(p => p.id === id); return participants.find(p => p.id === id);
} }
/**
* Returns a count of the known participants in the passed in redux state,
* excluding any fake participants.
*
* @param {(Function|Object|Participant[])} stateOrGetState - The redux state
* features/base/participants, the (whole) redux state, or redux's
* {@code getState} function to be used to retrieve the
* features/base/participants state.
* @returns {number}
*/
export function getParticipantCount(stateOrGetState) {
const participants = _getParticipants(stateOrGetState);
const realParticipants = participants.filter(p => !p.isBot);
return realParticipants.length;
}
/**
* Returns the participant which has its pinned state set to truthy.
*
* @param {(Function|Object|Participant[])} stateOrGetState - The redux state
* features/base/participants, the (whole) redux state, or redux's
* {@code getState} function to be used to retrieve the
* features/base/participants state.
* @returns {(Participant|undefined)}
*/
export function getPinnedParticipant(stateOrGetState) {
const participants = _getParticipants(stateOrGetState);
return participants.find(p => p.pinned);
}
/** /**
* Returns array of participants from Redux state. * Returns array of participants from Redux state.
* *

View File

@ -73,6 +73,7 @@ function _participant(state, action) {
connectionStatus, connectionStatus,
dominantSpeaker, dominantSpeaker,
email, email,
isBot,
local, local,
pinned, pinned,
role role
@ -108,6 +109,7 @@ function _participant(state, action) {
dominantSpeaker: dominantSpeaker || false, dominantSpeaker: dominantSpeaker || false,
email, email,
id, id,
isBot,
local: local || false, local: local || false,
name, name,
pinned: pinned || false, pinned: pinned || false,

View File

@ -9,7 +9,11 @@ import {
import { SET_CONFIG } from '../base/config'; import { SET_CONFIG } from '../base/config';
import { SET_LOCATION_URL } from '../base/connection'; import { SET_LOCATION_URL } from '../base/connection';
import { LIB_INIT_ERROR } from '../base/lib-jitsi-meet'; import { LIB_INIT_ERROR } from '../base/lib-jitsi-meet';
import { PARTICIPANT_JOINED } from '../base/participants'; import {
getLocalParticipant,
getParticipantCount,
PARTICIPANT_JOINED
} from '../base/participants';
import { MiddlewareRegistry } from '../base/redux'; import { MiddlewareRegistry } from '../base/redux';
import { setCallOverlayVisible, setJWT } from './actions'; import { setCallOverlayVisible, setJWT } from './actions';
@ -96,13 +100,8 @@ function _maybeSetCallOverlayVisible({ dispatch, getState }, next, action) {
default: { default: {
// The CallOverlay it to no longer be displayed/visible as soon // The CallOverlay it to no longer be displayed/visible as soon
// as another participant joins. // as another participant joins.
const participants = state['features/base/participants']; callOverlayVisible = getParticipantCount(state) === 1
&& Boolean(getLocalParticipant(state));
callOverlayVisible
= Boolean(
participants
&& participants.length === 1
&& participants[0].local);
// However, the CallDialog is not to be displayed/visible again // However, the CallDialog is not to be displayed/visible again
// after all remote participants leave. // after all remote participants leave.