Compare commits

...

15 Commits

Author SHA1 Message Date
Hristo Terezov
625c38fb04 fix(external-api): Set ifame.src before adding it.
Revert a7b25d6
2023-04-03 10:05:33 -05:00
damencho
0b729205aa fix(ScreenObtainer) Request high resolutions by default.
Request higher capture resolutions when desktopSharingFrameRate is not defined in config.js.
2023-03-28 08:45:40 -05:00
Hristo Terezov
a973ceee56 fix(remote-control):Pin the control participant SS 2023-03-17 14:18:11 -05:00
damencho
06fd8ce702 fix: Returns source names only for existing participants.
There are cases when participant is left and still we receive a track added. In this occasions for screensharing sources a virtual participant is created for non-existing participant.
2023-03-17 14:17:53 -05:00
Hristo Terezov
c6484f6fe0 fix(large-video):Dont elect participants that left 2023-03-17 10:16:08 -05:00
Robert Pintilii
a64c640879 fix(speaker-stats) Change icon (#13074) 2023-03-17 14:21:19 +02:00
Robert Pintilii
8d3cb4dd93 fix(audio-picker) Fix max height (#13069) 2023-03-17 12:25:12 +02:00
Robert Pintilii
d644fd4e1a fix(conference-timer) Show correct time (#13070)
Show meeting time after returning from breakout room
2023-03-17 12:24:54 +02:00
Bogdan Duduman
765d801760 feat(webhid) - add webhid feature flag (#13071) 2023-03-17 10:59:38 +02:00
Horatiu Muresan
de4d6a8744 fix(prejoin) Fix prejoin toolbar buttons 2023-03-14 13:11:31 +02:00
Jaya Allamsetty
992820afbb fix(AudioTrack): Reattach the track to the audio element on error.
Audio playback for a remote participant doesn't happen when the browser fires an error event on the audio element that the audio track is attached to.
'[modules/RTC/JitsiRemoteTrack.js] <._containerEventHandler>:  error handler was called for a container with attached RemoteTrack'
Log an error when that happens and try to re-attach the audio track and execute play on it as a potential fix.
2023-03-10 14:20:41 -06:00
damencho
e4d6792d31 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1585.0.0+362d1b2c...v1592.0.0+6afb0a56
2023-03-10 14:13:25 -06:00
damencho
6419f90c5e fix: Updates gapi to use new google identity service.
fix: Updates gapi to use new google identity service.
2023-03-10 11:54:47 -06:00
Horatiu Muresan
271f5ec4c7 fix(always-on-top) Show participant`s avatar (#12967) 2023-03-07 08:37:38 -06:00
Horatiu Muresan
689d0e98af fix(numbers-list) Re-add sip svg 2023-02-28 11:57:13 -06:00
23 changed files with 214 additions and 87 deletions

View File

@@ -561,6 +561,9 @@ var config = {
// Require users to always specify a display name.
// requireDisplayName: true,
// Enables webhid functionality for Audio.
// enableWebHIDFeature: false,
// DEPRECATED! Use 'welcomePage.disabled' instead.
// Whether to use a welcome page or not. In case it's false a random room
// will be joined when no room is specified.

View File

@@ -5,7 +5,7 @@
position: relative;
right: auto;
margin-bottom: 4px;
max-height: 456px;
max-height: calc(100vh - 100px);
overflow: auto;
width: 300px;

View File

@@ -3,6 +3,7 @@ $sidePanelWidth: 300px;
.prejoin-third-party {
flex-direction: column-reverse;
z-index: auto;
align-items: center;
.content {
height: auto;

View File

@@ -87,7 +87,7 @@
.toolbox-content-wrapper,
.toolbox-content-items {
box-sizing: border-box;
width: 100%;
width: auto;
}
}

View File

@@ -401,10 +401,9 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
// and fires event when it is done
this._frame.onload = onload;
}
this._frame.src = this._url;
this._frame = this._parentNode.appendChild(this._frame);
this._frame.src = this._url;
}
/**

11
package-lock.json generated
View File

@@ -72,7 +72,7 @@
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1585.0.0+362d1b2c/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#e0ebef7a915bd219c43defe58f7ed43cd170771c",
"lodash": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
@@ -13405,8 +13405,8 @@
},
"node_modules/lib-jitsi-meet": {
"version": "0.0.0",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1585.0.0+362d1b2c/lib-jitsi-meet.tgz",
"integrity": "sha512-g7JVvBfZixl1fKZI4ZMm3nvMasEz5sdapMzZdc76kA/eZSej2QuNK+W9cB8IypB7dqeTM4yzbfzi9rDipyWn+w==",
"resolved": "git+ssh://git@github.com/jitsi/lib-jitsi-meet.git#e0ebef7a915bd219c43defe58f7ed43cd170771c",
"integrity": "sha512-DqRJLh085DVitQssOOeF2LdVYkQy+zzk+r+woJzWG9IzX9kw1sKqsegEWGzSeAu4NC+25fJPEUNn67ntdVteEQ==",
"license": "Apache-2.0",
"dependencies": {
"@jitsi/js-utils": "2.0.0",
@@ -30289,8 +30289,9 @@
}
},
"lib-jitsi-meet": {
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1585.0.0+362d1b2c/lib-jitsi-meet.tgz",
"integrity": "sha512-g7JVvBfZixl1fKZI4ZMm3nvMasEz5sdapMzZdc76kA/eZSej2QuNK+W9cB8IypB7dqeTM4yzbfzi9rDipyWn+w==",
"version": "git+ssh://git@github.com/jitsi/lib-jitsi-meet.git#e0ebef7a915bd219c43defe58f7ed43cd170771c",
"integrity": "sha512-DqRJLh085DVitQssOOeF2LdVYkQy+zzk+r+woJzWG9IzX9kw1sKqsegEWGzSeAu4NC+25fJPEUNn67ntdVteEQ==",
"from": "lib-jitsi-meet@github:jitsi/lib-jitsi-meet#e0ebef7a915bd219c43defe58f7ed43cd170771c",
"requires": {
"@jitsi/js-utils": "2.0.0",
"@jitsi/logger": "2.0.0",

View File

@@ -77,7 +77,7 @@
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1585.0.0+362d1b2c/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#e0ebef7a915bd219c43defe58f7ed43cd170771c",
"lodash": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",

View File

@@ -199,7 +199,7 @@ export default class AlwaysOnTop extends Component<*, State> {
color = { getAvatarColor(displayName, customAvatarBackgrounds) }
id = 'avatar'
initials = { getInitials(displayName) }
url = { displayName ? null : avatarURL } />)
url = { avatarURL } />)
</div>
<div
className = 'displayname'

View File

@@ -316,6 +316,7 @@ export interface IConfig {
enableSaveLogs?: boolean;
enableTcc?: boolean;
enableUnifiedOnChrome?: boolean;
enableWebHIDFeature?: boolean;
enableWelcomePage?: boolean;
etherpad_base?: string;
faceLandmarks?: {

View File

@@ -44,6 +44,16 @@ export function getToolbarButtons(state: IReduxState): Array<string> {
return buttons;
}
/**
* Returns the configuration value of web-hid feature.
*
* @param {Object} state - The state of the app.
* @returns {boolean} True if web-hid feature should be enabled, otherwise false.
*/
export function getWebHIDFeatureConfig(state: IReduxState): boolean {
return state['features/base/config'].enableWebHIDFeature || false;
}
/**
* Checks if the specified button is enabled.
*

View File

@@ -81,6 +81,7 @@ export { default as IconSend } from './send.svg';
export { default as IconShare } from './share.svg';
export { default as IconShareDoc } from './share-doc.svg';
export { default as IconShortcuts } from './shortcuts.svg';
export { default as IconSip } from './sip.svg';
export { default as IconSites } from './sites.svg';
export { default as IconStop } from './stop.svg';
export { default as IconStopScreenshare } from './stop-screenshare.svg';

View File

@@ -0,0 +1,3 @@
<svg width="20" height="14" viewBox="0 0 20 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.41201 1.90767C3.50689 3.37865 5.24438 7.52348 7.36096 9.64007C7.36096 9.64004 7.36175 9.63923 7.36329 9.63765C9.48 11.7541 13.6215 13.4932 15.0923 12.5882C16.1022 11.9668 16.0078 9.51337 15.2427 8.76783C14.7369 8.2749 13.1882 8.01994 12.5497 8.14762C12.3496 8.18763 11.7907 8.76515 11.4793 9.08696C11.4184 9.14994 11.3669 9.20313 11.3295 9.24058C11.1007 9.46937 9.63912 8.22168 9.20588 7.78845L7.60102 9.39838C8.10053 8.89635 9.2057 7.78701 9.2057 7.78701C8.77247 7.35377 7.53081 5.89935 7.7596 5.67056C7.79705 5.63311 7.85024 5.58164 7.91322 5.5207C8.23503 5.20928 8.81255 4.65041 8.85256 4.45033C8.98024 3.81178 8.72528 2.26311 8.23236 1.75727C7.48681 0.992193 5.03342 0.897765 4.41201 1.90767Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 868 B

View File

@@ -79,6 +79,7 @@ class AudioTrack extends Component<Props> {
super(props);
// Bind event handlers so they are only bound once for every instance.
this._errorHandler = this._errorHandler.bind(this);
this._setRef = this._setRef.bind(this);
this._play = this._play.bind(this);
}
@@ -103,6 +104,8 @@ class AudioTrack extends Component<Props> {
if (typeof _muted === 'boolean') {
this._ref.muted = _muted;
}
this._ref.addEventListener('error', this._errorHandler);
}
}
@@ -115,6 +118,7 @@ class AudioTrack extends Component<Props> {
*/
componentWillUnmount() {
this._detachTrack(this.props.audioTrack);
this._ref.removeEventListener('error', this._errorHandler);
}
/**
@@ -202,10 +206,25 @@ class AudioTrack extends Component<Props> {
}
}
_errorHandler: (?Error) => void;
/**
* Reattaches the audio track to the underlying HTMLAudioElement when an 'error' event is fired.
*
* @param {Error} error - The error event fired on the HTMLAudioElement.
* @returns {void}
*/
_errorHandler(error) {
logger.error(`Error ${error?.message} called on audio track ${this.props.audioTrack?.jitsiTrack}. `
+ 'Attempting to reattach the audio track to the element and execute play on it');
this._detachTrack(this.props.audioTrack);
this._attachTrack(this.props.audioTrack);
}
_play: ?number => void;
/**
* Plays the uderlying HTMLAudioElement.
* Plays the underlying HTMLAudioElement.
*
* @param {number} retries - The number of previously failed retries.
* @returns {void}

View File

@@ -10,7 +10,11 @@ import { VIDEO_TYPE } from '../media/constants';
import StateListenerRegistry from '../redux/StateListenerRegistry';
import { createVirtualScreenshareParticipant, participantLeft } from './actions';
import { getRemoteScreensharesBasedOnPresence } from './functions';
import {
getParticipantById,
getRemoteScreensharesBasedOnPresence,
getVirtualScreenshareParticipantOwnerId
} from './functions';
import { FakeParticipant } from './types';
StateListenerRegistry.register(
@@ -76,7 +80,7 @@ function _updateScreenshareParticipants(store: IStore): void {
if (track.local) {
newLocalSceenshareSourceName = sourceName;
} else {
} else if (getParticipantById(state, getVirtualScreenshareParticipantOwnerId(sourceName))) {
acc.push(sourceName);
}
}

View File

@@ -82,6 +82,7 @@ const ConferenceTimer = ({ textStyle }: IProps) => {
const stopTimer = useCallback(() => {
if (interval.current) {
clearInterval(interval.current);
interval.current = undefined;
}
setTimerValue(getLocalizedDurationFormatter(0));

View File

@@ -3,7 +3,7 @@ import { useDispatch, useSelector } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { openDialog } from '../../../base/dialog/actions';
import { IconConnection } from '../../../base/icons/svg';
import { IconUsers } from '../../../base/icons/svg';
import Label from '../../../base/label/components/web/Label';
import { COLORS } from '../../../base/label/constants';
import { getParticipantCount } from '../../../base/participants/functions';
@@ -33,7 +33,7 @@ function SpeakerStatsLabel() {
return (
<Label
color = { COLORS.white }
icon = { IconConnection }
icon = { IconUsers }
iconColor = '#fff'
// eslint-disable-next-line react/jsx-no-bind
onClick = { onClick }

View File

@@ -1,5 +1,6 @@
import { IStore } from '../app/types';
import { IStateful } from '../base/app/types';
import { getWebHIDFeatureConfig } from '../base/config/functions.web';
import {
addPendingDeviceRequest,
getAvailableDevices,
@@ -44,7 +45,7 @@ export function getDeviceSelectionDialogProps(stateful: IStateful, isDisplayedOn
const speakerChangeSupported = JitsiMeetJS.mediaDevices.isDeviceChangeAvailable('output');
const userSelectedCamera = getUserSelectedCameraDeviceId(state);
const userSelectedMic = getUserSelectedMicDeviceId(state);
const deviceHidSupported = isDeviceHidSupported();
const deviceHidSupported = isDeviceHidSupported() && getWebHIDFeatureConfig(state);
// When the previews are disabled we don't need multiple audio input support in order to change the mic. This is the
// case for Safari on iOS.

View File

@@ -52,6 +52,7 @@ export function loadGoogleAPI() {
return Promise.resolve();
})
.then(() => dispatch(setGoogleAPIState(GOOGLE_API_STATES.LOADED)))
.then(() => googleApi.signInIfNotSignedIn())
.then(() => googleApi.isSignedIn())
.then(isSignedIn => {
if (isSignedIn) {
@@ -150,8 +151,7 @@ export function setGoogleAPIState(
* selectedBoundStreamID: *} | never>)}
*/
export function showAccountSelection() {
return () =>
googleApi.showAccountSelection();
return () => googleApi.showAccountSelection(true);
}
/**
@@ -161,7 +161,7 @@ export function showAccountSelection() {
*/
export function signIn() {
return (dispatch: Dispatch<any>) => googleApi.get()
.then(() => googleApi.signInIfNotSignedIn())
.then(() => googleApi.signInIfNotSignedIn(true))
.then(() => dispatch({
type: SET_GOOGLE_API_STATE,
googleAPIState: GOOGLE_API_STATES.SIGNED_IN
@@ -205,10 +205,10 @@ export function updateProfile() {
.then(profile => {
dispatch({
type: SET_GOOGLE_API_PROFILE,
profileEmail: profile.getEmail()
profileEmail: profile.email
});
return profile.getEmail();
return profile.email;
});
}

View File

@@ -20,10 +20,9 @@ export const API_URL_LIVE_BROADCASTS = 'https://content.googleapis.com/youtube/v
/**
* Array of API discovery doc URLs for APIs used by the googleApi.
*
* @type {string[]}
* @type {string}
*/
export const DISCOVERY_DOCS
= [ 'https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest' ];
export const DISCOVERY_DOCS = 'https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest';
/**
* An enumeration of the different states the Google API can be in.
@@ -61,6 +60,13 @@ export const GOOGLE_API_STATES = {
*/
export const GOOGLE_SCOPE_CALENDAR = 'https://www.googleapis.com/auth/calendar';
/**
* Google API auth scope to access user email.
*
* @type {string}
*/
export const GOOGLE_SCOPE_USERINFO = 'https://www.googleapis.com/auth/userinfo.email';
/**
* Google API auth scope to access YouTube streams.
*

View File

@@ -3,10 +3,13 @@ import {
API_URL_LIVE_BROADCASTS,
DISCOVERY_DOCS,
GOOGLE_SCOPE_CALENDAR,
GOOGLE_SCOPE_USERINFO,
GOOGLE_SCOPE_YOUTUBE
} from './constants';
import logger from './logger';
const GOOGLE_API_CLIENT_LIBRARY_URL = 'https://apis.google.com/js/api.js';
const GOOGLE_GIS_LIBRARY_URL = 'https://accounts.google.com/gsi/client';
/**
* A promise for dynamically loading the Google API Client Library.
@@ -50,9 +53,9 @@ const googleApi = {
}
return this._getGoogleApiClient()
.auth2.getAuthInstance()
.currentUser.get()
.getBasicProfile();
.client.oauth2
.userinfo.get().getPromise()
.then(r => r.result);
});
},
@@ -68,23 +71,40 @@ const googleApi = {
initializeClient(clientId, enableYoutube, enableCalendar) {
return this.get()
.then(api => new Promise((resolve, reject) => {
const scope
= `${enableYoutube ? GOOGLE_SCOPE_YOUTUBE : ''} ${enableCalendar ? GOOGLE_SCOPE_CALENDAR : ''}`
.trim();
// setTimeout is used as a workaround for api.client.init not
// resolving consistently when the Google API Client Library is
// loaded asynchronously. See:
// github.com/google/google-api-javascript-client/issues/399
setTimeout(() => {
api.client.init({
clientId,
discoveryDocs: DISCOVERY_DOCS,
scope
api.client.init({})
.then(() => {
if (enableCalendar) {
api.client.load(DISCOVERY_DOCS);
}
})
.then(() => {
api.client.load('https://www.googleapis.com/discovery/v1/apis/oauth2/v1/rest');
})
.then(resolve)
.catch(reject);
}, 500);
}))
.then(() => new Promise((resolve, reject) => {
try {
const scope
= `${enableYoutube ? GOOGLE_SCOPE_YOUTUBE : ''} ${enableCalendar ? GOOGLE_SCOPE_CALENDAR : ''}`
.trim();
this.tokenClient = this._getGoogleGISApiClient().accounts.oauth2.initTokenClient({
// eslint-disable-next-line camelcase
client_id: clientId,
scope: `${scope} ${GOOGLE_SCOPE_USERINFO}`,
callback: '' // defined at request time in await/promise scope.
});
resolve();
} catch (err) {
reject(err);
}
}));
},
@@ -95,13 +115,38 @@ const googleApi = {
* @returns {Promise}
*/
isSignedIn() {
return this.get()
.then(api => Boolean(api
&& api.auth2
&& api.auth2.getAuthInstance
&& api.auth2.getAuthInstance()
&& api.auth2.getAuthInstance().isSignedIn
&& api.auth2.getAuthInstance().isSignedIn.get()));
return new Promise((resolve, _) => {
const te = parseInt(this.tokenExpires, 10);
const isExpired = isNaN(this.tokenExpires) ? true : new Date().getTime() > te;
resolve(Boolean(!isExpired));
});
},
/**
* Generates a script tag.
*
* @param {string} src - The source for the script tag.
* @returns {Promise<unknown>}
* @private
*/
_loadScriptTag(src) {
return new Promise((resolve, reject) => {
const scriptTag = document.createElement('script');
scriptTag.async = true;
scriptTag.addEventListener('error', () => {
scriptTag.remove();
reject();
});
scriptTag.addEventListener('load', resolve);
scriptTag.type = 'text/javascript';
scriptTag.src = src;
document.head.appendChild(scriptTag);
});
},
/**
@@ -114,29 +159,19 @@ const googleApi = {
return googleClientLoadPromise;
}
googleClientLoadPromise = new Promise((resolve, reject) => {
const scriptTag = document.createElement('script');
scriptTag.async = true;
scriptTag.addEventListener('error', () => {
scriptTag.remove();
googleClientLoadPromise = this._loadScriptTag(GOOGLE_API_CLIENT_LIBRARY_URL)
.catch(() => {
googleClientLoadPromise = null;
reject();
});
scriptTag.addEventListener('load', resolve);
scriptTag.type = 'text/javascript';
scriptTag.src = GOOGLE_API_CLIENT_LIBRARY_URL;
document.head.appendChild(scriptTag);
})
})
.then(() => new Promise((resolve, reject) =>
this._getGoogleApiClient().load('client:auth2', {
this._getGoogleApiClient().load('client', {
callback: resolve,
onerror: reject
})))
.then(this._loadScriptTag(GOOGLE_GIS_LIBRARY_URL))
.catch(() => {
googleClientLoadPromise = null;
})
.then(() => this._getGoogleApiClient());
return googleClientLoadPromise;
@@ -171,25 +206,50 @@ const googleApi = {
* Prompts the participant to sign in to the Google API Client Library, even
* if already signed in.
*
* @param {boolean} consent - Whether to show account selection dialog.
* @returns {Promise}
*/
showAccountSelection() {
showAccountSelection(consent: boolean) {
return this.get()
.then(api => api.auth2.getAuthInstance().signIn());
.then(api => new Promise((resolve, reject) => {
try {
// Settle this promise in the response callback for requestAccessToken()
this.tokenClient.callback = resp => {
if (resp.error !== undefined) {
reject(resp);
}
// Get the number of seconds the token is valid for, subtract 5 minutes
// to account for differences in clock settings and convert to ms.
const expiresIn = (parseInt(api.client.getToken().expires_in, 10) - 300) * 1000;
const now = new Date();
const expireDate = new Date(now.getTime() + expiresIn);
this.tokenExpires = expireDate.getTime().toString();
resolve(resp);
};
this.tokenClient.requestAccessToken({ prompt: consent ? 'consent' : '' });
} catch (err) {
logger.error('Error requesting token', err);
}
}));
},
/**
* Prompts the participant to sign in to the Google API Client Library, if
* not already signed in.
*
* @param {boolean} consent - Whether to show account selection dialog.
* @returns {Promise}
*/
signInIfNotSignedIn() {
signInIfNotSignedIn(consent: boolean) {
return this.get()
.then(() => this.isSignedIn())
.then(isSignedIn => {
if (!isSignedIn) {
return this.showAccountSelection();
return this.showAccountSelection(consent);
}
});
},
@@ -201,11 +261,10 @@ const googleApi = {
*/
signOut() {
return this.get()
.then(api =>
api.auth2
&& api.auth2.getAuthInstance
&& api.auth2.getAuthInstance()
&& api.auth2.getAuthInstance().signOut());
.then(() => {
this.tokenClient = undefined;
this.tokenExpires = undefined;
});
},
/**
@@ -387,6 +446,17 @@ const googleApi = {
*/
_getGoogleApiClient() {
return window.gapi;
},
/**
* Returns the global Google Identity Services Library object.
*
* @private
* @returns {Object|undefined}
*/
_getGoogleGISApiClient() {
return window.google;
}
};

View File

@@ -0,0 +1,3 @@
import { getLogger } from '../base/logging/functions';
export default getLogger('features/base/redux');

View File

@@ -1,15 +1,16 @@
import { IReduxState, IStore } from '../app/types';
import { getSsrcRewritingFeatureFlag } from '../base/config/functions.any';
import { IStateful } from '../base/app/types';
import { MEDIA_TYPE } from '../base/media/constants';
import {
getDominantSpeakerParticipant,
getLocalParticipant,
getLocalScreenShareParticipant,
getParticipantById,
getPinnedParticipant,
getRemoteParticipants,
getVirtualScreenshareParticipantByOwnerId
} from '../base/participants/functions';
import { ITrack } from '../base/tracks/types';
import { toState } from '../base/redux/functions';
import { isStageFilmstripAvailable } from '../filmstrip/functions';
import { getAutoPinSetting } from '../video-layout/functions';
@@ -104,17 +105,24 @@ export function setLargeVideoDimensions(height: number, width: number) {
/**
* Returns the most recent existing remote video track.
*
* @param {Track[]} tracks - All current tracks.
* @param {Function|Object} stateful - The redux store or {@code getState} function.
* @private
* @returns {(Track|undefined)}
*/
function _electLastVisibleRemoteVideo(tracks: ITrack[]) {
function _electLastVisibleRemoteParticipant(stateful: IStateful) {
const state = toState(stateful);
const tracks = state['features/base/tracks'];
// First we try to get most recent remote video track.
for (let i = tracks.length - 1; i >= 0; --i) {
const track = tracks[i];
if (!track.local && track.mediaType === MEDIA_TYPE.VIDEO) {
return track;
if (!track.local && track.mediaType === MEDIA_TYPE.VIDEO && track.participantId) {
const participant = getParticipantById(state, track.participantId);
if (participant) {
return participant;
}
}
}
}
@@ -171,14 +179,10 @@ function _electParticipantInLargeVideo(state: IReduxState) {
participant = undefined;
// Next, pick the most recent participant with video.
// (Skip this if rewriting, tracks may be detached from any owner.)
if (!getSsrcRewritingFeatureFlag(state)) {
const tracks = state['features/base/tracks'];
const videoTrack = _electLastVisibleRemoteVideo(tracks);
const lastVisibleRemoteParticipant = _electLastVisibleRemoteParticipant(state);
if (videoTrack) {
return videoTrack.participantId;
}
if (lastVisibleRemoteParticipant) {
return lastVisibleRemoteParticipant.id;
}
// Last, select the participant that joined last (other than poltergist or other bot type participants).

View File

@@ -207,14 +207,14 @@ export function processPermissionRequestReply(participantId: string, event: any)
// the remote control permissions has been granted
// pin the controlled participant
const pinnedParticipant = getPinnedParticipant(state);
const virtualScreenshareParticipantId = getVirtualScreenshareParticipantByOwnerId(state, participantId);
const virtualScreenshareParticipant = getVirtualScreenshareParticipantByOwnerId(state, participantId);
const pinnedId = pinnedParticipant?.id;
// @ts-ignore
if (virtualScreenshareParticipantId && pinnedId !== virtualScreenshareParticipantId) {
if (virtualScreenshareParticipant?.id && pinnedId !== virtualScreenshareParticipant?.id) {
// @ts-ignore
dispatch(pinParticipant(virtualScreenshareParticipantId));
} else if (!virtualScreenshareParticipantId && pinnedId !== participantId) {
dispatch(pinParticipant(virtualScreenshareParticipant?.id));
} else if (!virtualScreenshareParticipant?.id && pinnedId !== participantId) {
dispatch(pinParticipant(participantId));
}
}