feat(aot): Handle video not available use cases (#2242)

This commit is contained in:
hristoterezov 2017-12-04 21:27:17 -06:00 committed by virtuacoplenny
parent 40df5f97d4
commit 5ffcaca649
12 changed files with 472 additions and 76 deletions

View File

@ -78,6 +78,7 @@ import {
import { getLocationContextRoot } from './react/features/base/util';
import { statsEmitter } from './react/features/connection-indicator';
import { showDesktopPicker } from './react/features/desktop-picker';
import { appendSuffix } from './react/features/display-name';
import { maybeOpenFeedbackDialog } from './react/features/feedback';
import {
mediaPermissionPromptVisibilityChanged,
@ -1726,15 +1727,20 @@ export default {
if (user.isHidden()) {
return;
}
const displayName = user.getDisplayName();
APP.store.dispatch(participantJoined({
id,
name: user.getDisplayName(),
name: displayName,
role: user.getRole()
}));
logger.log('USER %s connnected', id, user);
APP.API.notifyUserJoined(id);
APP.API.notifyUserJoined(id, {
displayName,
formattedDisplayName: appendSuffix(
displayName || interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME)
});
APP.UI.addUser(user);
// check the roles for the new user and reflect them
@ -1892,6 +1898,7 @@ export default {
}
APP.UI.addListener(UIEvents.SELECTED_ENDPOINT, id => {
APP.API.notifyOnStageParticipantChanged(id);
try {
// do not try to select participant if there is none (we
// are alone in the room), otherwise an error will be
@ -1938,7 +1945,13 @@ export default {
id,
name: formattedDisplayName
}));
APP.API.notifyDisplayNameChanged(id, formattedDisplayName);
APP.API.notifyDisplayNameChanged(id, {
displayName: formattedDisplayName,
formattedDisplayName:
appendSuffix(
formattedDisplayName
|| interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME)
});
APP.UI.changeDisplayName(id, formattedDisplayName);
}
);
@ -2377,7 +2390,19 @@ export default {
APP.store.dispatch(conferenceJoined(room));
APP.UI.mucJoined();
APP.API.notifyConferenceJoined(APP.conference.roomName);
const displayName = APP.settings.getDisplayName();
APP.API.notifyConferenceJoined(
this.roomName,
this._room.myUserId(),
{
displayName,
formattedDisplayName: appendSuffix(
displayName,
interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME),
avatarURL: APP.UI.getAvatarUrl()
}
);
APP.UI.markVideoInterrupted(false);
},
@ -2748,6 +2773,14 @@ export default {
}));
APP.settings.setDisplayName(formattedNickname);
APP.API.notifyDisplayNameChanged(id, {
displayName: formattedNickname,
formattedDisplayName:
appendSuffix(
formattedNickname,
interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME)
});
if (room) {
room.setDisplayName(formattedNickname);
APP.UI.changeDisplayName(id, formattedNickname);

View File

@ -250,6 +250,7 @@
/**
* Positions video thumbnail display name and editor.
*/
#alwaysOnTop .displayname,
.videocontainer .displayname,
.videocontainer .editdisplayname {
display: inline-block;
@ -269,6 +270,15 @@
z-index: $zindex2;
}
#alwaysOnTop .displayname {
font-size: 15px;
position: inherit;
width: 100%;
left: 0px;
top: 0px;
margin-top: 10px;
}
/**
* Positions video thumbnail display name editor.
*/
@ -507,6 +517,20 @@
width: auto;
}
#videoNotAvailableScreen {
text-align: center;
#avatarContainer {
height: 50vh;
display:inline-block;
margin-top: 25vh;
#avatar {
border-radius: 50%;
height: 100%;
}
}
}
.sharedVideoAvatar {
height: 100%;
width: 100%;

View File

@ -140,18 +140,26 @@ The `event` parameter is a String object with the name of the event.
The `listener` parameter is a Function object with one argument that will be notified when the event occurs with data related to the event.
The following events are currently supported:
* **avatarChanged** - event notifications about avatar
changes. The listener will receive an object with the following structure:
```javascript
{
"id": id, // the id of the participant that changed his avatar.
"avatarURL": avatarURL // the new avatar URL.
}
```
* **audioAvailabilityChanged** - event notifications about audio availability status changes. The listener will receive an object with the following structure:
```javascript
{
"available": available // new available status - boolean
"available": available // new available status - boolean
}
```
* **audioMuteStatusChanged** - event notifications about audio mute status changes. The listener will receive an object with the following structure:
```javascript
{
"muted": muted // new muted status - boolean
"muted": muted // new muted status - boolean
}
```
@ -159,9 +167,9 @@ The following events are currently supported:
messages. The listener will receive an object with the following structure:
```javascript
{
"from": from, // JID of the user that sent the message
"nick": nick, // the nickname of the user that sent the message
"message": txt // the text of the message
"from": from, // The id of the user that sent the message
"nick": nick, // the nickname of the user that sent the message
"message": txt // the text of the message
}
```
@ -169,7 +177,7 @@ messages. The listener will receive an object with the following structure:
messages. The listener will receive an object with the following structure:
```javascript
{
"message": txt // the text of the message
"message": txt // the text of the message
}
```
@ -177,50 +185,54 @@ messages. The listener will receive an object with the following structure:
changes. The listener will receive an object with the following structure:
```javascript
{
"jid": jid, // the JID of the participant that changed his display name
"displayname": displayName // the new display name
"id": id, // the id of the participant that changed his display name
"displayname": displayName // the new display name
}
```
* **participantJoined** - event notifications about new participants who join the room. The listener will receive an object with the following structure:
```javascript
{
"jid": jid // the JID of the participant
"id": id, // the id of the participant
"displayName": displayName // the display name of the participant
}
```
* **participantLeft** - event notifications about participants that leave the room. The listener will receive an object with the following structure:
```javascript
{
"jid": jid // the JID of the participant
"id": id // the id of the participant
}
```
* **videoConferenceJoined** - event notifications fired when the local user has joined the video conference. The listener will receive an object with the following structure:
```javascript
{
"roomName": room // the room name of the conference
"roomName": room, // the room name of the conference
"id": id, // the id of the local participant
"displayName": displayName, // the display name of the local participant
"avatarURL": avatarURL // the avatar URL of the local participant
}
```
* **videoConferenceLeft** - event notifications fired when the local user has left the video conference. The listener will receive an object with the following structure:
```javascript
{
"roomName": room // the room name of the conference
"roomName": room // the room name of the conference
}
```
* **videoAvailabilityChanged** - event notifications about video availability status changes. The listener will receive an object with the following structure:
```javascript
{
"available": available // new available status - boolean
"available": available // new available status - boolean
}
```
* **videoMuteStatusChanged** - event notifications about video mute status changes. The listener will receive an object with the following structure:
```javascript
{
"muted": muted // new muted status - boolean
"muted": muted // new muted status - boolean
}
```
@ -264,6 +276,16 @@ You can get the number of participants in the conference with the following API
var numberOfParticipants = api.getNumberOfParticipants();
```
You can get the avatar URL of a participant in the conference with the following API function:
```javascript
var avatarURL = api.getAvatarURL(participantId);
```
You can get the display name of a participant in the conference with the following API function:
```javascript
var displayName = api.getDisplayName(participantId);
```
You can get the iframe HTML element where Jitsi Meet is loaded with the following API function:
```javascript
var iframe = api.getIFrame();

View File

@ -184,6 +184,21 @@ class API {
initCommands();
}
/**
* Notify external application (if API is enabled) that the large video
* visibility changed.
*
* @param {boolean} isHidden - True if the large video is hidden and false
* otherwise.
* @returns {void}
*/
notifyLargeVideoVisibilityChanged(isHidden: boolean) {
this._sendEvent({
name: 'large-video-visibility-changed',
isVisible: !isHidden
});
}
/**
* Sends event to the external application.
*
@ -238,12 +253,14 @@ class API {
* conference.
*
* @param {string} id - User id.
* @param {Object} props - The display name of the user.
* @returns {void}
*/
notifyUserJoined(id: string) {
notifyUserJoined(id: string, props: Object) {
this._sendEvent({
name: 'participant-joined',
id
id,
...props
});
}
@ -261,18 +278,39 @@ class API {
});
}
/**
* Notify external application (if API is enabled) that user changed their
* avatar.
*
* @param {string} id - User id.
* @param {string} avatarURL - The new avatar URL of the participant.
* @returns {void}
*/
notifyAvatarChanged(id: string, avatarURL: string) {
this._sendEvent({
name: 'avatar-changed',
avatarURL,
id
});
}
/**
* Notify external application (if API is enabled) that user changed their
* nickname.
*
* @param {string} id - User id.
* @param {string} displayname - User nickname.
* @param {string} formattedDisplayName - The display name shown in Jitsi
* meet's UI for the user.
* @returns {void}
*/
notifyDisplayNameChanged(id: string, displayname: string) {
notifyDisplayNameChanged(
id: string,
{ displayName, formattedDisplayName }: Object) {
this._sendEvent({
name: 'display-name-change',
displayname,
displayname: displayName,
formattedDisplayName,
id
});
}
@ -282,12 +320,17 @@ class API {
* been joined.
*
* @param {string} roomName - The room name.
* @param {string} id - The id of the local user.
* @param {Object} props - The display name and avatar URL of the local
* user.
* @returns {void}
*/
notifyConferenceJoined(roomName: string) {
notifyConferenceJoined(roomName: string, id: string, props: Object) {
this._sendEvent({
name: 'video-conference-joined',
roomName
roomName,
id,
...props
});
}
@ -373,6 +416,20 @@ class API {
});
}
/**
* Notify external application (if API is enabled) that the on stage
* participant has changed.
*
* @param {string} id - User id of the new on stage participant.
* @returns {void}
*/
notifyOnStageParticipantChanged(id: string) {
this._sendEvent({
name: 'on-stage-participant-changed',
id
});
}
/**
* Disposes the allocated resources.

View File

@ -34,6 +34,7 @@ const commands = {
* events expected by jitsi-meet
*/
const events = {
'avatar-changed': 'avatarChanged',
'audio-availability-changed': 'audioAvailabilityChanged',
'audio-mute-status-changed': 'audioMuteStatusChanged',
'display-name-change': 'displayNameChange',
@ -224,7 +225,11 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
}
})
});
this._numberOfParticipants = 1;
this._isLargeVideoVisible = true;
this._numberOfParticipants = 0;
this._participants = {};
this._myUserID = undefined;
this._onStageParticipant = undefined;
this._setupListeners();
id++;
}
@ -278,6 +283,34 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
);
}
/**
* Returns the id of the on stage participant.
*
* @returns {string} - The id of the on stage participant.
*/
_getOnStageParticipant() {
return this._onStageParticipant;
}
/**
* Getter for the large video element in Jitsi Meet.
*
* @returns {HTMLElement|undefined} - The large video.
*/
_getLargeVideo() {
const iframe = this.getIFrame();
if (!this._isLargeVideoVisible
|| !iframe
|| !iframe.contentWindow
|| !iframe.contentWindow.document) {
return;
}
return iframe.contentWindow.document.getElementById('largeVideo');
}
/**
* Sets the size of the iframe element.
*
@ -308,12 +341,58 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
* @private
*/
_setupListeners() {
this._transport.on('event', ({ name, ...data }) => {
if (name === 'participant-joined') {
const userID = data.id;
switch (name) {
case 'video-conference-joined':
this._myUserID = userID;
this._participants[userID] = {
avatarURL: data.avatarURL
};
// eslint-disable-next-line no-fallthrough
case 'participant-joined': {
this._participants[userID] = this._participants[userID] || {};
this._participants[userID].displayName = data.displayName;
this._participants[userID].formattedDisplayName
= data.formattedDisplayName;
changeParticipantNumber(this, 1);
} else if (name === 'participant-left') {
break;
}
case 'participant-left':
changeParticipantNumber(this, -1);
delete this._participants[userID];
break;
case 'display-name-change': {
const user = this._participants[userID];
if (user) {
user.displayName = data.displayname;
user.formattedDisplayName = data.formattedDisplayName;
}
break;
}
case 'avatar-changed': {
const user = this._participants[userID];
if (user) {
user.avatarURL = data.avatarURL;
}
break;
}
case 'on-stage-participant-changed':
this._onStageParticipant = userID;
this.emit('largeVideoChanged');
break;
case 'large-video-visibility-changed':
this._isLargeVideoVisible = data.isVisible;
this.emit('largeVideoChanged');
break;
case 'video-conference-left':
changeParticipantNumber(this, -1);
delete this._participants[this._myUserID];
break;
}
const eventName = events[name];
@ -487,6 +566,43 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
});
}
/**
* Returns the avatar URL of a participant.
*
* @param {string} participantId - The id of the participant.
* @returns {string} The avatar URL.
*/
getAvatarURL(participantId) {
const { avatarURL } = this._participants[participantId] || {};
return avatarURL;
}
/**
* Returns the display name of a participant.
*
* @param {string} participantId - The id of the participant.
* @returns {string} The display name.
*/
getDisplayName(participantId) {
const { displayName } = this._participants[participantId] || {};
return displayName;
}
/**
* Returns the formatted display name of a participant.
*
* @param {string} participantId - The id of the participant.
* @returns {string} The formatted display name.
*/
_getFormattedDisplayName(participantId) {
const { formattedDisplayName }
= this._participants[participantId] || {};
return formattedDisplayName;
}
/**
* Returns the iframe that loads Jitsi Meet.
*

View File

@ -801,6 +801,16 @@ function changeAvatar(id, avatarUrl) {
}
}
/**
* Returns the avatar URL for a given user.
*
* @param {string} id - The id of the user.
* @returns {string} The avatar URL.
*/
UI.getAvatarUrl = function(id) {
return Avatar.getAvatarUrl(id);
};
/**
* Update user email.
* @param {string} id user id

View File

@ -48,6 +48,10 @@ export default {
users[id] = {};
}
users[id][prop] = val;
APP.API.notifyAvatarChanged(
id === 'local' ? APP.conference.getMyUserId() : id,
this.getAvatarUrl(id)
);
},
/**

View File

@ -1,4 +1,4 @@
/* global $, interfaceConfig */
/* global $, APP, interfaceConfig */
import Filmstrip from './Filmstrip';
import LargeContainer from './LargeContainer';
@ -545,6 +545,7 @@ export class VideoContainer extends LargeContainer {
this.avatarDisplayed = show;
this.emitter.emit(UIEvents.LARGE_VIDEO_AVATAR_VISIBLE, show);
APP.API.notifyLargeVideoVisibilityChanged(show);
}
/**

View File

@ -61,6 +61,9 @@ const TOOLBAR_TIMEOUT = 4000;
type State = {
audioAvailable: boolean,
audioMuted: boolean,
avatarURL: string,
displayName: string,
isVideoDisplayed: boolean,
videoAvailable: boolean,
videoMuted: boolean,
visible: boolean
@ -89,13 +92,21 @@ export default class AlwaysOnTop extends Component<*, State> {
audioMuted: false,
videoMuted: false,
audioAvailable: false,
videoAvailable: false
videoAvailable: false,
displayName: '',
isVideoDisplayed: true,
avatarURL: ''
};
// Bind event handlers so they are only bound once per instance.
this._audioAvailabilityListener
= this._audioAvailabilityListener.bind(this);
this._audioMutedListener = this._audioMutedListener.bind(this);
this._avatarChangedListener = this._avatarChangedListener.bind(this);
this._largeVideoChangedListener
= this._largeVideoChangedListener.bind(this);
this._displayNameChangedListener
= this._displayNameChangedListener.bind(this);
this._mouseMove = this._mouseMove.bind(this);
this._onMouseOut = this._onMouseOut.bind(this);
this._onMouseOver = this._onMouseOver.bind(this);
@ -128,6 +139,40 @@ export default class AlwaysOnTop extends Component<*, State> {
this.setState({ audioMuted: muted });
}
_avatarChangedListener: () => void;
/**
* Handles avatar changed api events.
*
* @returns {void}
*/
_avatarChangedListener({ avatarURL, id }) {
if (api._getOnStageParticipant() !== id) {
return;
}
if (avatarURL !== this.state.avatarURL) {
this.setState({ avatarURL });
}
}
_displayNameChangedListener: () => void;
/**
* Handles display name changed api events.
*
* @returns {void}
*/
_displayNameChangedListener({ formattedDisplayName, id }) {
if (api._getOnStageParticipant() !== id) {
return;
}
if (formattedDisplayName !== this.state.displayName) {
this.setState({ displayName: formattedDisplayName });
}
}
/**
* Hides the toolbar after a timeout.
*
@ -144,6 +189,26 @@ export default class AlwaysOnTop extends Component<*, State> {
}, TOOLBAR_TIMEOUT);
}
_largeVideoChangedListener: () => void;
/**
* Handles large video changed api events.
*
* @returns {void}
*/
_largeVideoChangedListener() {
const userID = api._getOnStageParticipant();
const displayName = api._getFormattedDisplayName(userID);
const avatarURL = api.getAvatarURL(userID);
const isVideoDisplayed = Boolean(api._getLargeVideo());
this.setState({
avatarURL,
displayName,
isVideoDisplayed
});
}
_mouseMove: () => void;
/**
@ -181,6 +246,38 @@ export default class AlwaysOnTop extends Component<*, State> {
_videoAvailabilityListener: ({ available: boolean }) => void;
/**
* Renders display name and avatar for the on stage participant.
*
* @returns {ReactElement}
*/
_renderVideoNotAvailableScreen() {
const { avatarURL, displayName, isVideoDisplayed } = this.state;
if (isVideoDisplayed) {
return null;
}
return (
<div id = 'videoNotAvailableScreen'>
{
avatarURL
? <div id = 'avatarContainer'>
<img
id = 'avatar'
src = { avatarURL } />
</div>
: null
}
<div
className = 'displayname'
id = 'displayname'>
{ displayName }
</div>
</div>
);
}
/**
* Handles audio available api events.
*
@ -214,6 +311,11 @@ export default class AlwaysOnTop extends Component<*, State> {
api.on('videoMuteStatusChanged', this._videoMutedListener);
api.on('audioAvailabilityChanged', this._audioAvailabilityListener);
api.on('videoAvailabilityChanged', this._videoAvailabilityListener);
api.on('largeVideoChanged', this._largeVideoChangedListener);
api.on('displayNameChange', this._displayNameChangedListener);
api.on('avatarChanged', this._avatarChangedListener);
this._largeVideoChangedListener();
Promise.all([
api.isAudioMuted(),
@ -256,6 +358,11 @@ export default class AlwaysOnTop extends Component<*, State> {
this._audioAvailabilityListener);
api.removeListener('videoAvailabilityChanged',
this._videoAvailabilityListener);
api.removeListener('largeVideoChanged',
this._largeVideoChangedListener);
api.removeListener('displayNameChange',
this._displayNameChangedListener);
api.removeListener('avatarChanged', this._avatarChangedListener);
window.removeEventListener('mousemove', this._mouseMove);
}
@ -283,50 +390,61 @@ export default class AlwaysOnTop extends Component<*, State> {
this.state.visible ? 'fadeIn' : 'fadeOut'}`;
return (
<StatelessToolbar
className = { className }
onMouseOut = { this._onMouseOut }
onMouseOver = { this._onMouseOver }>
<div id = 'alwaysOnTop'>
<StatelessToolbar
className = { className }
onMouseOut = { this._onMouseOut }
onMouseOver = { this._onMouseOver }>
{
Object.entries(TOOLBAR_BUTTONS).map(
([ key, button ]) => {
// XXX The following silences a couple of flow
// errors:
if (button === null
|| typeof button !== 'object') {
return null;
}
const { onClick } = button;
let enabled = false;
let toggled = false;
switch (key) {
case 'microphone':
enabled = this.state.audioAvailable;
toggled = enabled
? this.state.audioMuted : true;
break;
case 'camera':
enabled = this.state.videoAvailable;
toggled = enabled
? this.state.videoMuted : true;
break;
default: // hangup button
toggled = false;
enabled = true;
}
const updatedButton = {
...button,
enabled,
toggled
};
return (
<StatelessToolbarButton
button = { updatedButton }
key = { key }
onClick = { onClick } />
);
}
)
}
</StatelessToolbar>
{
Object.entries(TOOLBAR_BUTTONS).map(([ key, button ]) => {
// XXX The following silences a couple of flow errors:
if (button === null || typeof button !== 'object') {
return null;
}
const { onClick } = button;
let enabled = false;
let toggled = false;
switch (key) {
case 'microphone':
enabled = this.state.audioAvailable;
toggled = enabled ? this.state.audioMuted : true;
break;
case 'camera':
enabled = this.state.videoAvailable;
toggled = enabled ? this.state.videoMuted : true;
break;
default: // hangup button
toggled = false;
enabled = true;
}
const updatedButton = {
...button,
enabled,
toggled
};
return (
<StatelessToolbarButton
button = { updatedButton }
key = { key }
onClick = { onClick } />
);
})
this._renderVideoNotAvailableScreen()
}
</StatelessToolbar>
</div>
);
}
}

View File

@ -2,6 +2,8 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { appendSuffix } from '../functions';
import { translate } from '../../base/i18n';
import { participantDisplayNameChanged } from '../../base/participants';
@ -144,15 +146,12 @@ class DisplayName extends Component {
);
}
const suffix
= displayName && displayNameSuffix ? ` (${displayNameSuffix})` : '';
return (
<span
className = 'displayname'
id = { elementID }
onClick = { this._onStartEditing }>
{ `${displayName || displayNameSuffix || ''}${suffix}` }
{ `${appendSuffix(displayName, displayNameSuffix)}` }
</span>
);
}

View File

@ -0,0 +1,11 @@
/**
* Appends a suffix to the display name.
*
* @param {string} displayName - The display name.
* @param {string} suffix - Suffix that will be appended.
* @returns {string} The formatted display name.
*/
export function appendSuffix(displayName, suffix) {
return `${displayName || suffix || ''}${
displayName && suffix ? ` (${suffix})` : ''}`;
}

View File

@ -1,2 +1,3 @@
export * from './actions';
export * from './components';
export * from './functions';