feat(alwaysontop): Toolbar.

This commit is contained in:
hristoterezov 2017-08-04 11:15:11 +03:00 committed by virtuacoplenny
parent 382b328262
commit 1782030936
10 changed files with 562 additions and 40 deletions

View File

@ -727,12 +727,12 @@ export default {
// so that the user can try unmute later on and add audio/video
// to the conference
if (!tracks.find((t) => t.isAudioTrack())) {
this.audioMuted = true;
this.setAudioMuteStatus(true);
APP.UI.setAudioMuted(this.getMyUserId(), this.audioMuted);
}
if (!tracks.find((t) => t.isVideoTrack())) {
this.videoMuted = true;
this.setVideoMuteStatus(true);
APP.UI.setVideoMuted(this.getMyUserId(), this.videoMuted);
}
@ -765,7 +765,7 @@ export default {
muteAudio(mute, showUI = true) {
// Not ready to modify track's state yet
if (!this._localTracksInitialized) {
this.audioMuted = mute;
this.setAudioMuteStatus(mute);
return;
} else if (localAudio && localAudio.isMuted() === mute) {
// NO-OP
@ -794,7 +794,7 @@ export default {
muteLocalAudio(mute)
.catch(error => {
maybeShowErrorDialog(error);
this.audioMuted = oldMutedStatus;
this.setAudioMuteStatus(oldMutedStatus);
APP.UI.setAudioMuted(this.getMyUserId(), this.audioMuted);
});
}
@ -824,7 +824,7 @@ export default {
muteVideo(mute, showUI = true) {
// Not ready to modify track's state yet
if (!this._localTracksInitialized) {
this.videoMuted = mute;
this.setVideoMuteStatus(mute);
return;
} else if (localVideo && localVideo.isMuted() === mute) {
@ -863,7 +863,7 @@ export default {
muteLocalVideo(mute)
.catch(error => {
maybeShowErrorDialog(error);
this.videoMuted = oldMutedStatus;
this.setVideoMuteStatus(oldMutedStatus);
APP.UI.setVideoMuted(this.getMyUserId(), this.videoMuted);
});
}
@ -1220,13 +1220,13 @@ export default {
.then(() => {
localVideo = newStream;
if (newStream) {
this.videoMuted = newStream.isMuted();
this.setVideoMuteStatus(newStream.isMuted());
this.isSharingScreen = newStream.videoType === 'desktop';
APP.UI.addLocalStream(newStream);
} else {
// No video is treated the same way as being video muted
this.videoMuted = true;
this.setVideoMuteStatus(true);
this.isSharingScreen = false;
}
APP.UI.setVideoMuted(this.getMyUserId(), this.videoMuted);
@ -1245,12 +1245,13 @@ export default {
replaceLocalTrack(localAudio, newStream, room))
.then(() => {
localAudio = newStream;
if (newStream) {
this.audioMuted = newStream.isMuted();
this.setAudioMuteStatus(newStream.isMuted());
APP.UI.addLocalStream(newStream);
} else {
// No audio is treated the same way as being audio muted
this.audioMuted = true;
this.setAudioMuteStatus(true);
}
APP.UI.setAudioMuted(this.getMyUserId(), this.audioMuted);
});
@ -2310,6 +2311,7 @@ export default {
'device count: ' + audioDeviceCount);
APP.store.dispatch(setAudioAvailable(available));
APP.API.notifyAudioAvailabilityChanged(available);
},
/**
@ -2334,6 +2336,7 @@ export default {
'device count: ' + videoDeviceCount);
APP.store.dispatch(setVideoAvailable(available));
APP.API.notifyVideoAvailabilityChanged(available);
},
/**
@ -2541,5 +2544,29 @@ export default {
*/
getDesktopSharingSourceType() {
return localVideo.sourceType;
}
},
/**
* Sets the video muted status.
*
* @param {boolean} muted - New muted status.
*/
setVideoMuteStatus(muted) {
if (this.videoMuted !== muted) {
this.videoMuted = muted;
APP.API.notifyVideoMutedStatusChanged(muted);
}
},
/**
* Sets the audio muted status.
*
* @param {boolean} muted - New muted status.
*/
setAudioMuteStatus(muted) {
if (this.audioMuted !== muted) {
this.audioMuted = muted;
APP.API.notifyAudioMutedStatusChanged(muted);
}
},
};

View File

@ -25,7 +25,7 @@ Its constructor gets a number of options:
* **parentNode**: (optional) HTML DOM Element where the iframe will be added as a child.
* **configOverwrite**: (optional) JS object with overrides for options defined in [config.js].
* **interfaceConfigOverwrite**: (optional) JS object with overrides for options defined in [interface_config.js].
* **noSsl**: (optional, defaults to true) Boolean indicating if the server should be contacted using HTTP or HTTPS.
* **noSSL**: (optional, defaults to true) Boolean indicating if the server should be contacted using HTTP or HTTPS.
* **jwt**: (optional) [JWT](https://jwt.io/) token.
Example:
@ -141,6 +141,20 @@ The `listener` parameter is a Function object with one argument that will be not
The following events are currently supported:
* **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
}
```
* **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
}
```
* **incomingMessage** - Event notifications about incoming
messages. The listener will receive an object with the following structure:
```javascript
@ -196,6 +210,20 @@ changes. The listener will receive an object with the following structure:
}
```
* **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
}
```
* **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
}
```
* **readyToClose** - event notification fired when Jitsi Meet is ready to be closed (hangup operations are completed).
You can also add multiple event listeners by using `addEventListeners`.
@ -241,6 +269,34 @@ You can get the iframe HTML element where Jitsi Meet is loaded with the followin
var iframe = api.getIFrame();
```
You can check whether the audio is muted with the following API function:
```javascript
isAudioMuted().then(function(muted) {
...
});
```
You can check whether the video is muted with the following API function:
```javascript
isVideoMuted().then(function(muted) {
...
});
```
You can check whether the audio is available with the following API function:
```javascript
isAudioAvailable().then(function(available) {
...
});
```
You can check whether the video is available with the following API function:
```javascript
isVideoAvailable().then(function(available) {
...
});
```
You can remove the embedded Jitsi Meet Conference with the following API function:
```javascript
api.dispose()

View File

@ -26,6 +26,20 @@ let initialScreenSharingState = false;
*/
const transport = getJitsiMeetTransport();
/**
* The current audio availability.
*
* @type {boolean}
*/
let audioAvailable = true;
/**
* The current video availability.
*
* @type {boolean}
*/
let videoAvailable = true;
/**
* Initializes supported commands.
*
@ -58,6 +72,26 @@ function initCommands() {
return false;
});
transport.on('request', ({ data, name }, callback) => {
switch (name) {
case 'is-audio-muted':
callback(APP.conference.audioMuted);
break;
case 'is-video-muted':
callback(APP.conference.videoMuted);
break;
case 'is-audio-available':
callback(audioAvailable);
break;
case 'is-video-available':
callback(videoAvailable);
break;
default:
return false;
}
return true;
});
}
/**
@ -265,6 +299,65 @@ class API {
this._sendEvent({ name: 'video-ready-to-close' });
}
/**
* Notify external application (if API is enabled) for audio muted status
* changed.
*
* @param {boolean} muted - The new muted status.
* @returns {void}
*/
notifyAudioMutedStatusChanged(muted) {
this._sendEvent({
name: 'audio-mute-status-changed',
muted
});
}
/**
* Notify external application (if API is enabled) for video muted status
* changed.
*
* @param {boolean} muted - The new muted status.
* @returns {void}
*/
notifyVideoMutedStatusChanged(muted) {
this._sendEvent({
name: 'video-mute-status-changed',
muted
});
}
/**
* Notify external application (if API is enabled) for audio availability
* changed.
*
* @param {boolean} available - True if available and false otherwise.
* @returns {void}
*/
notifyAudioAvailabilityChanged(available) {
audioAvailable = available;
this._sendEvent({
name: 'audio-availability-changed',
available
});
}
/**
* Notify external application (if API is enabled) for video available
* status changed.
*
* @param {boolean} available - True if available and false otherwise.
* @returns {void}
*/
notifyVideoAvailabilityChanged(available) {
videoAvailable = available;
this._sendEvent({
name: 'video-availability-changed',
available
});
}
/**
* Disposes the allocated resources.
*

View File

@ -9,7 +9,7 @@ import {
const logger = require('jitsi-meet-logger').getLogger(__filename);
const ALWAYS_ON_TOP_FILENAMES = [
'css/alwaysontop.css', 'libs/alwaysontop.bundle.min.js'
'css/all.css', 'libs/alwaysontop.min.js'
];
/**
@ -34,6 +34,8 @@ const commands = {
* events expected by jitsi-meet
*/
const events = {
'audio-availability-changed': 'audioAvailabilityChanged',
'audio-mute-status-changed': 'audioMuteStatusChanged',
'display-name-change': 'displayNameChange',
'incoming-message': 'incomingMessage',
'outgoing-message': 'outgoingMessage',
@ -41,7 +43,9 @@ const events = {
'participant-left': 'participantLeft',
'video-ready-to-close': 'readyToClose',
'video-conference-joined': 'videoConferenceJoined',
'video-conference-left': 'videoConferenceLeft'
'video-conference-left': 'videoConferenceLeft',
'video-availability-changed': 'videoAvailabilityChanged',
'video-mute-status-changed': 'videoMuteStatusChanged'
};
/**
@ -211,9 +215,7 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
noSSL,
roomName
});
this._baseUrl = generateURL(domain, {
noSSL
});
this._baseUrl = `${noSSL ? 'http' : 'https'}://${domain}/`;
this._createIFrame(height, width);
this._transport = new Transport({
backend: new PostMessageTransportBackend({
@ -448,6 +450,30 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
}
}
/**
* Check if the audio is available.
*
* @returns {Promise} - Resolves with true if the audio available, with
* false if not and rejects on failure.
*/
isAudioAvailable() {
return this._transport.sendRequest({
name: 'is-audio-available'
});
}
/**
* Returns the audio mute status.
*
* @returns {Promise} - Resolves with the audio mute status and rejects on
* failure.
*/
isAudioMuted() {
return this._transport.sendRequest({
name: 'is-audio-muted'
});
}
/**
* Returns the iframe that loads Jitsi Meet.
*
@ -467,6 +493,30 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
return this._numberOfParticipants;
}
/**
* Check if the video is available.
*
* @returns {Promise} - Resolves with true if the video available, with
* false if not and rejects on failure.
*/
isVideoAvailable() {
return this._transport.sendRequest({
name: 'is-video-available'
});
}
/**
* Returns the audio mute status.
*
* @returns {Promise} - Resolves with the audio mute status and rejects on
* failure.
*/
isVideoMuted() {
return this._transport.sendRequest({
name: 'is-video-muted'
});
}
/**
* Removes event listener.
*

View File

@ -0,0 +1,297 @@
import React, { Component } from 'react';
import StatelessToolbar from '../toolbox/components/StatelessToolbar';
import StatelessToolbarButton
from '../toolbox/components/StatelessToolbarButton';
const { api } = window.alwaysOnTop;
/**
* The timeout in ms for hidding the toolbar.
*/
const TOOLBAR_TIMEOUT = 4000;
/**
* Map with toolbar button descriptors.
*/
const toolbarButtons = {
/**
* The descriptor of the camera toolbar button.
*/
camera: {
classNames: [ 'button', 'icon-camera' ],
enabled: true,
id: 'toolbar_button_camera',
onClick() {
api.executeCommand('toggleVideo');
}
},
/**
* The descriptor of the toolbar button which hangs up the call/conference.
*/
hangup: {
classNames: [ 'button', 'icon-hangup', 'button_hangup' ],
enabled: true,
id: 'toolbar_button_hangup',
onClick() {
api.executeCommand('hangup');
window.close();
}
},
/**
* The descriptor of the microphone toolbar button.
*/
microphone: {
classNames: [ 'button', 'icon-microphone' ],
enabled: true,
id: 'toolbar_button_mute',
onClick() {
api.executeCommand('toggleAudio');
}
}
};
/**
* Represents the always on top page.
*
* @class AlwaysOnTop
* @extends Component
*/
export default class AlwaysOnTop extends Component {
/**
* Initializes new AlwaysOnTop instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props) {
super(props);
this.state = {
visible: true,
audioMuted: false,
videoMuted: false,
audioAvailable: false,
videoAvailable: false
};
this._hovered = false;
this._audioAvailabilityListener
= this._audioAvailabilityListener.bind(this);
this._audioMutedListener = this._audioMutedListener.bind(this);
this._mouseMove = this._mouseMove.bind(this);
this._onMouseOver = this._onMouseOver.bind(this);
this._onMouseOut = this._onMouseOut.bind(this);
this._videoAvailabilityListener
= this._videoAvailabilityListener.bind(this);
this._videoMutedListener = this._videoMutedListener.bind(this);
}
/**
* Handles audio available api events.
*
* @param {{ available: boolean }} status - The new available status.
* @returns {void}
*/
_audioAvailabilityListener({ available }) {
this.setState({ audioAvailable: available });
}
/**
* Handles audio muted api events.
*
* @param {{ muted: boolean }} status - The new muted status.
* @returns {void}
*/
_audioMutedListener({ muted }) {
this.setState({ audioMuted: muted });
}
/**
* Hides the toolbar after a timeout.
*
* @returns {void}
*/
_hideToolbarAfterTimeout() {
setTimeout(() => {
if (this._hovered) {
this._hideToolbarAfterTimeout();
return;
}
this.setState({ visible: false });
}, TOOLBAR_TIMEOUT);
}
/**
* Handles mouse move events.
*
* @returns {void}
*/
_mouseMove() {
if (!this.state.visible) {
this.setState({ visible: true });
}
}
/**
* Toolbar mouse over handler.
*
* @returns {void}
*/
_onMouseOver() {
this._hovered = true;
}
/**
* Toolbar mouse out handler.
*
* @returns {void}
*/
_onMouseOut() {
this._hovered = false;
}
/**
* Handles audio available api events.
*
* @param {{ available: boolean }} status - The new available status.
* @returns {void}
*/
_videoAvailabilityListener({ available }) {
this.setState({ videoAvailable: available });
}
/**
* Handles video muted api events.
*
* @param {{ muted: boolean }} status - The new muted status.
* @returns {void}
*/
_videoMutedListener({ muted }) {
this.setState({ videoMuted: muted });
}
/**
* Sets mouse move listener and initial toolbar timeout.
*
* @inheritdoc
* @returns {void}
*/
componentDidMount() {
api.on('audioMuteStatusChanged', this._audioMutedListener);
api.on('videoMuteStatusChanged', this._videoMutedListener);
api.on('audioAvailabilityChanged', this._audioAvailabilityListener);
api.on('videoAvailabilityChanged', this._videoAvailabilityListener);
Promise.all([
api.isAudioMuted(),
api.isVideoMuted(),
api.isAudioAvailable(),
api.isVideoAvailable()
])
.then(([
audioMuted = false,
videoMuted = false,
audioAvailable = false,
videoAvailable = false
]) =>
this.setState({
audioMuted,
videoMuted,
audioAvailable,
videoAvailable
})
)
.catch(console.error);
window.addEventListener('mousemove', this._mouseMove);
this._hideToolbarAfterTimeout();
}
/**
* Removes all listeners.
*
* @inheritdoc
* @returns {void}
*/
componentWillUnmount() {
api.removeListener('audioMuteStatusChanged',
this._audioMutedListener);
api.removeListener('videoMuteStatusChanged',
this._videoMutedListener);
api.removeListener('audioAvailabilityChanged',
this._audioAvailabilityListener);
api.removeListener('videoAvailabilityChanged',
this._videoAvailabilityListener);
window.removeEventListener('mousemove', this._mouseMove);
}
/**
* Sets a timeout to hide the toolbar when the toolbar is shown.
*
* @inheritdoc
* @returns {void}
*/
componentWillUpdate(nextProps, nextState) {
if (!this.state.visible && nextState.visible) {
this._hideToolbarAfterTimeout();
}
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const className
= `toolbar_primary ${this.state.visible ? 'fadeIn' : 'fadeOut'}`;
return (
<StatelessToolbar
className = { className }
onMouseOut = { this._onMouseOut }
onMouseOver = { this._onMouseOver }>
{
Object.entries(toolbarButtons).map(([ key, button ]) => {
const { onClick } = button;
let enabled = false, 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>
);
}
}

View File

@ -0,0 +1,10 @@
import React from 'react';
import ReactDOM from 'react-dom';
import AlwaysOnTop from './AlwaysOnTop';
// Render the main/root Component.
ReactDOM.render(
<AlwaysOnTop />,
document.getElementById('react')
);

View File

@ -110,9 +110,9 @@ MiddlewareRegistry.register(store => next => action => {
if (jitsiTrack.isLocal()) {
if (isVideoTrack) {
APP.conference.videoMuted = muted;
APP.conference.setVideoMuteStatus(muted);
} else {
APP.conference.audioMuted = muted;
APP.conference.setAudioMuteStatus(muted);
}
}

View File

@ -4,10 +4,9 @@ import React, { Component } from 'react';
/**
* Implements a toolbar in React/Web. It is a strip that contains a set of
* toolbar items such as buttons. Toolbar is commonly placed inside of a
* Toolbox.
* toolbar items such as buttons.
*
* @class Toolbar
* @class StatelessToolbar
* @extends Component
*/
export default class StatelessToolbar extends Component {

View File

@ -76,24 +76,10 @@ class Toolbar extends Component {
* @returns {ReactElement}
*/
render(): ReactElement<*> {
const toolbarButtons = new Map();
this.props.toolbarButtons
.forEach((button, key) => {
const { onClick } = button;
toolbarButtons.set(key, {
...button,
onClick: (...args) =>
onClick && onClick(this.props.dispatch, ...args)
});
});
const props = {
...this.props,
className: this.props.className,
onMouseOut: this._onMouseOut,
onMouseOver: this._onMouseOver,
toolbarButtons
onMouseOver: this._onMouseOver
};
return (
@ -150,14 +136,15 @@ class Toolbar extends Component {
}
const { tooltipPosition } = this.props;
const { onClick, onMount, onUnmount } = button;
const onClickWithDispatch = (...args) =>
onClick && onClick(this.props.dispatch, ...args);
return (
<ToolbarButton
button = { button }
key = { key }
onClick = { onClick }
onClick = { onClickWithDispatch }
onMount = { onMount }
onUnmount = { onUnmount }
tooltipPosition = { tooltipPosition } />

View File

@ -176,6 +176,9 @@ module.exports = [
'device_selection_popup_bundle':
'./react/features/device-selection/popup.js',
'alwaysontop':
'./react/features/always-on-top/index.js',
'do_external_connect':
'./connection_optimization/do_external_connect.js'
}