Compare commits

...

12 Commits

Author SHA1 Message Date
George Politis
402eed5150 deps: lib-jitsi-meet@latest 2020-07-13 12:09:17 +02:00
George Politis
b3bb0725ad wip: Fixes lint errors 2020-07-13 12:06:51 +02:00
George Politis
7d845a4982 wip: Makes the max enabled resolution title a translateable string. 2020-07-13 12:06:51 +02:00
George Politis
9d1ea76162 wip: Fixes lint errors 2020-07-13 12:06:51 +02:00
George Politis
22b5bfcea4 feat: Exposes the max enabled resolution in the connection stats popover. 2020-07-13 12:06:50 +02:00
plokta
b0188a7184 config: Add option to set preferred audio bitrate. (#7072)
The maxaveragebitrate parameter to be used by Opus can be configured
through the new opusMaxAvgBitrate config option. Values are restricted
by Opus to integers between 6000 to 510000. Works for non-p2p only.

move option to Audio section, add documentation

Co-authored-by: plokta <dev@plokta.eu>
2020-07-11 10:33:49 +02:00
Saúl Ibarra Corretgé
53281c2d42 analytics: avoid error log when there are no handlers 2020-07-10 17:55:18 +02:00
damencho
3da1b65757 fix: Fixes import of moved function. 2020-07-10 08:22:22 -05:00
Tudor-Ovidiu Avram
0e5091adba feat(prejoin) Expose prejoin app 2020-07-10 14:24:06 +02:00
Tudor-Ovidiu Avram
f376542441 feat(prejoin) cache media start options when on prejoin screen 2020-07-10 14:24:06 +02:00
Mihai Uscat
bd65108692 fix(SecurityDialog): Fix password action spacing 2020-07-10 13:26:48 +02:00
Saúl Ibarra Corretgé
546b0abe32 misc: add script to update LJM to the latest commit 2020-07-10 12:44:29 +02:00
22 changed files with 468 additions and 85 deletions

View File

@@ -111,6 +111,11 @@ var config = {
// participants and to enable it back a reload is needed.
// startSilent: false
// Sets the preferred target bitrate for the Opus audio codec by setting its
// 'maxaveragebitrate' parameter. Currently not available in p2p mode.
// Valid values are in the range 6000 to 510000
// opusMaxAvgBitrate: 20000,
// Video
// Sets the preferred resolution (height) for local video. Defaults to 720.

View File

@@ -1,17 +1,21 @@
/**
* Shared style for full screen local track based dialogs/modals.
*/
.premeeting-screen {
.premeeting-screen,
.preview-overlay {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
}
.premeeting-screen {
align-items: stretch;
background: #1C2025;
bottom: 0;
background: radial-gradient(50% 50% at 50% 50%, #5D95C7 0%, #376288 100%), #FFFFFF;
display: flex;
flex-direction: column;
font-size: 1.3em;
left: 0;
position: absolute;
right: 0;
top: 0;
z-index: $toolbarZ + 1;
.action-btn {
@@ -74,9 +78,13 @@
}
}
.preview-overlay {
background-image: linear-gradient(transparent, black);
z-index: $toolbarZ + 1;
}
.content {
align-items: center;
background-image: linear-gradient(transparent, black);
display: flex;
flex: 1;
flex-direction: column;

View File

@@ -25,6 +25,10 @@
font-size: 14px;
color: #6FB1EA;
}
& > :first-child:not(:last-child) {
margin-right: 24px;
}
}
}
}

View File

@@ -8,7 +8,17 @@
<link rel="apple-touch-icon" href="images/apple-touch-icon.png">
<link rel="stylesheet" href="css/all.css">
<script>
document.addEventListener('DOMContentLoaded', () => {
if (!JitsiMeetJS.app) {
return;
}
JitsiMeetJS.app.renderEntryPoint({
Component: JitsiMeetJS.app.entryPoints.APP
})
})
</script>
<script>
// IE11 and earlier can be identified via their user agent and be
// redirected to a page that is known to have no newer js syntax.

View File

@@ -110,6 +110,7 @@
"localaddress_plural": "Local addresses:",
"localport": "Local port:",
"localport_plural": "Local ports:",
"maxEnabledResolution": "send max",
"more": "Show more",
"packetloss": "Packet loss:",
"quality": {

View File

@@ -56,7 +56,7 @@
"jquery-i18next": "1.2.1",
"js-md5": "0.6.1",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#cd008d726f1f57562eb5d8e6a3cd91c7e69826a0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#304b0a2b4e18216d792f499c74fc24bc3849303e",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.13",
"moment": "2.19.4",

View File

@@ -117,7 +117,9 @@ export function createHandlers({ getState }: { getState: Function }) {
})
.catch(e => {
analytics.dispose();
logger.error(e);
if (handlers.length !== 0) {
logger.error(e);
}
return [];
}));

View File

@@ -122,14 +122,14 @@ export default class BaseApp extends Component<*, State> {
* @returns {ReactElement}
*/
render() {
const { route: { component }, store } = this.state;
const { route: { component, props }, store } = this.state;
if (store) {
return (
<I18nextProvider i18n = { i18next }>
<Provider store = { store }>
<Fragment>
{ this._createMainElement(component) }
{ this._createMainElement(component, props) }
<SoundCollection />
{ this._createExtraElement() }
{ this._renderDialogContainer() }

View File

@@ -125,6 +125,7 @@ export default [
'minParticipants',
'nick',
'openBridgeChannel',
'opusMaxAvgBitrate',
'p2p',
'pcStatsInterval',
'preferH264',

View File

@@ -24,6 +24,16 @@ type Props = {
*/
name?: string,
/**
* Indicates whether the avatar should be shown when video is off
*/
showAvatar: boolean,
/**
* Indicates whether the label and copy url action should be shown
*/
showConferenceInfo: boolean,
/**
* Title of the screen.
*/
@@ -45,13 +55,23 @@ type Props = {
* on the prejoin screen (pre-connection) or lobby (post-connection).
*/
export default class PreMeetingScreen extends PureComponent<Props> {
/**
* Default values for {@code Prejoin} component's properties.
*
* @static
*/
static defaultProps = {
showAvatar: true,
showConferenceInfo: true
};
/**
* Implements {@code PureComponent#render}.
*
* @inheritdoc
*/
render() {
const { name, title, videoMuted, videoTrack } = this.props;
const { name, showAvatar, showConferenceInfo, title, videoMuted, videoTrack } = this.props;
return (
<div
@@ -59,13 +79,19 @@ export default class PreMeetingScreen extends PureComponent<Props> {
id = 'lobby-screen'>
<Preview
name = { name }
showAvatar = { showAvatar }
videoMuted = { videoMuted }
videoTrack = { videoTrack } />
{!videoMuted && <div className = 'preview-overlay' />}
<div className = 'content'>
<div className = 'title'>
{ title }
</div>
<CopyMeetingUrl />
{showConferenceInfo && (
<>
<div className = 'title'>
{ title }
</div>
<CopyMeetingUrl />
</>
)}
{ this.props.children }
<div className = 'media-btn-container'>
<AudioSettingsButton visible = { true } />

View File

@@ -14,6 +14,11 @@ export type Props = {
*/
name: string,
/**
* Indicates whether the avatar should be shown when video is off
*/
showAvatar: boolean,
/**
* Flag signaling the visibility of camera preview.
*/
@@ -32,7 +37,7 @@ export type Props = {
* @returns {ReactElement}
*/
function Preview(props: Props) {
const { name, videoMuted, videoTrack } = props;
const { name, showAvatar, videoMuted, videoTrack } = props;
if (!videoMuted && videoTrack) {
return (
@@ -44,19 +49,27 @@ function Preview(props: Props) {
);
}
return (
<div
className = 'no-video'
id = 'preview'>
<Avatar
className = 'preview-avatar'
displayName = { name }
participantId = 'local'
size = { 200 } />
</div>
);
if (showAvatar) {
return (
<div
className = 'no-video'
id = 'preview'>
<Avatar
className = 'preview-avatar'
displayName = { name }
participantId = 'local'
size = { 200 } />
</div>
);
}
return null;
}
Preview.defaultProps = {
showAvatar: true
};
/**
* Maps part of the Redux state to the props of this component.
*

View File

@@ -1,7 +1,7 @@
/* global APP */
import JitsiMeetJS, { JitsiTrackErrors, browser } from '../lib-jitsi-meet';
import { MEDIA_TYPE } from '../media';
import { MEDIA_TYPE, setAudioMuted } from '../media';
import {
getUserSelectedCameraDeviceId,
getUserSelectedMicDeviceId
@@ -125,6 +125,89 @@ export function createLocalTracksF(options = {}, firePermissionPromptIsShownEven
}));
}
/**
* Returns an object containing a promise which resolves with the created tracks &
* the errors resulting from that process.
*
* @returns {Promise<JitsiLocalTrack>}
*
* @todo Refactor to not use APP
*/
export function createPrejoinTracks() {
const errors = {};
const initialDevices = [ 'audio' ];
const requestedAudio = true;
let requestedVideo = false;
const { startAudioOnly, startWithAudioMuted, startWithVideoMuted } = APP.store.getState()['features/base/settings'];
// Always get a handle on the audio input device so that we have statistics even if the user joins the
// conference muted. Previous implementation would only acquire the handle when the user first unmuted,
// which would results in statistics ( such as "No audio input" or "Are you trying to speak?") being available
// only after that point.
if (startWithAudioMuted) {
APP.store.dispatch(setAudioMuted(true));
}
if (!startWithVideoMuted && !startAudioOnly) {
initialDevices.push('video');
requestedVideo = true;
}
let tryCreateLocalTracks;
if (!requestedAudio && !requestedVideo) {
// Resolve with no tracks
tryCreateLocalTracks = Promise.resolve([]);
} else {
tryCreateLocalTracks = createLocalTracksF({ devices: initialDevices }, true)
.catch(err => {
if (requestedAudio && requestedVideo) {
// Try audio only...
errors.audioAndVideoError = err;
return (
createLocalTracksF({ devices: [ 'audio' ] }, true));
} else if (requestedAudio && !requestedVideo) {
errors.audioOnlyError = err;
return [];
} else if (requestedVideo && !requestedAudio) {
errors.videoOnlyError = err;
return [];
}
logger.error('Should never happen');
})
.catch(err => {
// Log this just in case...
if (!requestedAudio) {
logger.error('The impossible just happened', err);
}
errors.audioOnlyError = err;
// Try video only...
return requestedVideo
? createLocalTracksF({ devices: [ 'video' ] }, true)
: [];
})
.catch(err => {
// Log this just in case...
if (!requestedVideo) {
logger.error('The impossible just happened', err);
}
errors.videoOnlyError = err;
return [];
});
}
return {
tryCreateLocalTracks,
errors
};
}
/**
* Returns local audio track.
*

View File

@@ -143,6 +143,10 @@ MiddlewareRegistry.register(store => next => action => {
if (typeof APP !== 'undefined') {
const result = next(action);
if (isPrejoinPageVisible(store.getState())) {
return result;
}
const { jitsiTrack } = action.track;
const muted = jitsiTrack.isMuted();
const participantID = jitsiTrack.getParticipantId();

View File

@@ -342,6 +342,7 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
bridgeCount,
e2eRtt,
framerate,
maxEnabledResolution,
packetLoss,
region,
resolution,
@@ -358,6 +359,7 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
e2eRtt = { e2eRtt }
framerate = { framerate }
isLocalVideo = { this.props.isLocalVideo }
maxEnabledResolution = { maxEnabledResolution }
onShowMore = { this._onToggleShowMore }
packetLoss = { packetLoss }
region = { region }

View File

@@ -57,6 +57,12 @@ type Props = {
*/
isLocalVideo: boolean,
/**
* The send-side max enabled resolution (aka the highest layer that is not
* suspended on the send-side).
*/
maxEnabledResolution: number,
/**
* Callback to invoke when the show additional stats link is clicked.
*/
@@ -380,8 +386,8 @@ class ConnectionStatsTable extends Component<Props> {
* @returns {ReactElement}
*/
_renderResolution() {
const { resolution, t } = this.props;
const resolutionString = Object.keys(resolution || {})
const { resolution, maxEnabledResolution, t } = this.props;
let resolutionString = Object.keys(resolution || {})
.map(ssrc => {
const { width, height } = resolution[ssrc];
@@ -389,6 +395,12 @@ class ConnectionStatsTable extends Component<Props> {
})
.join(', ') || 'N/A';
if (maxEnabledResolution && maxEnabledResolution < 720) {
const maxEnabledResolutionTitle = t('connectionindicator.maxEnabledResolution');
resolutionString += ` (${maxEnabledResolutionTitle} ${maxEnabledResolution}p)`;
}
return (
<tr>
<td>

View File

@@ -79,11 +79,21 @@ type Props = {
*/
setJoinByPhoneDialogVisiblity: Function,
/**
* Indicates whether the avatar should be shown when video is off
*/
showAvatar: boolean,
/**
* Flag signaling the visibility of camera preview.
*/
showCameraPreview: boolean,
/**
* Flag signaling the visibility of join label, input and buttons
*/
showJoinActions: boolean,
/**
* If 'JoinByPhoneDialog' is visible or not.
*/
@@ -112,6 +122,15 @@ type State = {
* This component is displayed before joining a meeting.
*/
class Prejoin extends Component<Props, State> {
/**
* Default values for {@code Prejoin} component's properties.
*
* @static
*/
static defaultProps = {
showJoinActions: true
};
/**
* Initializes a new {@code Prejoin} instance.
*
@@ -223,8 +242,10 @@ class Prejoin extends Component<Props, State> {
joinConference,
joinConferenceWithoutAudio,
name,
showAvatar,
showCameraPreview,
showDialog,
showJoinActions,
t,
videoTrack
} = this.props;
@@ -236,61 +257,65 @@ class Prejoin extends Component<Props, State> {
<PreMeetingScreen
footer = { this._renderFooter() }
name = { name }
showAvatar = { showAvatar }
showConferenceInfo = { showJoinActions }
title = { t('prejoin.joinMeeting') }
videoMuted = { !showCameraPreview }
videoTrack = { videoTrack }>
<div className = 'prejoin-input-area-container'>
<div className = 'prejoin-input-area'>
<InputField
onChange = { _setName }
onSubmit = { joinConference }
placeHolder = { t('dialog.enterDisplayName') }
value = { name } />
{showJoinActions && (
<div className = 'prejoin-input-area-container'>
<div className = 'prejoin-input-area'>
<InputField
onChange = { _setName }
onSubmit = { joinConference }
placeHolder = { t('dialog.enterDisplayName') }
value = { name } />
<div className = 'prejoin-preview-dropdown-container'>
<InlineDialog
content = { <div className = 'prejoin-preview-dropdown-btns'>
<div
className = 'prejoin-preview-dropdown-btn'
onClick = { joinConferenceWithoutAudio }>
<Icon
className = 'prejoin-preview-dropdown-icon'
size = { 24 }
src = { IconVolumeOff } />
{ t('prejoin.joinWithoutAudio') }
</div>
{hasJoinByPhoneButton && <div
className = 'prejoin-preview-dropdown-btn'
onClick = { _showDialog }>
<Icon
className = 'prejoin-preview-dropdown-icon'
size = { 24 }
src = { IconPhone } />
{ t('prejoin.joinAudioByPhone') }
</div>}
</div> }
isOpen = { showJoinByPhoneButtons }
onClose = { _onDropdownClose }>
<ActionButton
disabled = { joinButtonDisabled }
hasOptions = { true }
onClick = { joinConference }
onOptionsClick = { _onOptionsClick }
type = 'primary'>
{ t('prejoin.joinMeeting') }
</ActionButton>
</InlineDialog>
<div className = 'prejoin-preview-dropdown-container'>
<InlineDialog
content = { <div className = 'prejoin-preview-dropdown-btns'>
<div
className = 'prejoin-preview-dropdown-btn'
onClick = { joinConferenceWithoutAudio }>
<Icon
className = 'prejoin-preview-dropdown-icon'
size = { 24 }
src = { IconVolumeOff } />
{ t('prejoin.joinWithoutAudio') }
</div>
{hasJoinByPhoneButton && <div
className = 'prejoin-preview-dropdown-btn'
onClick = { _showDialog }>
<Icon
className = 'prejoin-preview-dropdown-icon'
size = { 24 }
src = { IconPhone } />
{ t('prejoin.joinAudioByPhone') }
</div>}
</div> }
isOpen = { showJoinByPhoneButtons }
onClose = { _onDropdownClose }>
<ActionButton
disabled = { joinButtonDisabled }
hasOptions = { true }
onClick = { joinConference }
onOptionsClick = { _onOptionsClick }
type = 'primary'>
{ t('prejoin.joinMeeting') }
</ActionButton>
</InlineDialog>
</div>
</div>
<div className = 'prejoin-checkbox-container'>
<input
className = 'prejoin-checkbox'
onChange = { _onCheckboxChange }
type = 'checkbox' />
<span>{t('prejoin.doNotShow')}</span>
</div>
</div>
<div className = 'prejoin-checkbox-container'>
<input
className = 'prejoin-checkbox'
onChange = { _onCheckboxChange }
type = 'checkbox' />
<span>{t('prejoin.doNotShow')}</span>
</div>
</div>
)}
{ showDialog && (
<JoinByPhoneDialog
joinConferenceWithoutAudio = { joinConferenceWithoutAudio }

View File

@@ -0,0 +1,93 @@
// @flow
import { AtlasKitThemeProvider } from '@atlaskit/theme';
import React from 'react';
import { BaseApp } from '../../../features/base/app';
import { setConfig } from '../../base/config';
import { createPrejoinTracks } from '../../base/tracks';
import { initPrejoin } from '../actions';
import Prejoin from './Prejoin';
type Props = {
/**
* Indicates whether the avatar should be shown when video is off
*/
showAvatar: boolean,
/**
* Flag signaling the visibility of join label, input and buttons
*/
showJoinActions: boolean,
};
/**
* Wrapper application for prejoin.
*
* @extends BaseApp
*/
export default class PrejoinApp extends BaseApp<Props> {
_init: Promise<*>;
/**
* Navigates to {@link Prejoin} upon mount.
*
* @returns {void}
*/
componentDidMount() {
super.componentDidMount();
this._init.then(async () => {
const { store } = this.state;
const { dispatch } = store;
const { showAvatar, showJoinActions } = this.props;
super._navigate({
component: Prejoin,
props: {
showAvatar,
showJoinActions
}
});
const { startWithAudioMuted, startWithVideoMuted } = store.getState()['features/base/settings'];
dispatch(setConfig({
prejoinPageEnabled: true,
startWithAudioMuted,
startWithVideoMuted
}));
const { tryCreateLocalTracks, errors } = createPrejoinTracks();
const tracks = await tryCreateLocalTracks;
dispatch(initPrejoin(tracks, errors));
});
}
/**
* Overrides the parent method to inject {@link AtlasKitThemeProvider} as
* the top most component.
*
* @override
*/
_createMainElement(component, props) {
return (
<AtlasKitThemeProvider mode = 'dark'>
{ super._createMainElement(component, props) }
</AtlasKitThemeProvider>
);
}
/**
* Renders the platform specific dialog container.
*
* @returns {React$Element}
*/
_renderDialogContainer() {
return null;
}
}

View File

@@ -1,11 +1,13 @@
// @flow
import { SET_AUDIO_MUTED, SET_VIDEO_MUTED } from '../base/media';
import { MiddlewareRegistry } from '../base/redux';
import { updateSettings } from '../base/settings';
import { getLocalVideoTrack, replaceLocalTrack } from '../base/tracks';
import { PREJOIN_START_CONFERENCE } from './actionTypes';
import { setPrejoinPageVisibility } from './actions';
import { isPrejoinPageVisible } from './functions';
declare var APP: Object;
@@ -38,8 +40,26 @@ MiddlewareRegistry.register(store => next => async action => {
break;
}
case SET_AUDIO_MUTED: {
if (isPrejoinPageVisible(store.getState())) {
store.dispatch(updateSettings({
startWithAudioMuted: Boolean(action.muted)
}));
}
break;
}
case SET_VIDEO_MUTED: {
if (isPrejoinPageVisible(store.getState())) {
store.dispatch(updateSettings({
startWithVideoMuted: Boolean(action.muted)
}));
}
break;
}
}
return next(action);
});

View File

@@ -4,7 +4,7 @@
import React, { useRef } from 'react';
import { translate } from '../../../base/i18n';
import { copyText } from '../../../invite';
import { copyText } from '../../../base/util';
import PasswordForm from './PasswordForm';

View File

@@ -8,6 +8,8 @@ import { getJitsiMeetTransport } from '../modules/transport';
import { App } from './features/app/components';
import { getLogger } from './features/base/logging/functions';
import { Platform } from './features/base/react';
import { getJitsiMeetGlobalNS } from './features/base/util';
import PrejoinApp from './features/prejoin/components/PrejoinApp';
const logger = getLogger('index.web');
const OS = Platform.OS;
@@ -20,9 +22,6 @@ document.addEventListener('DOMContentLoaded', () => {
APP.connectionTimes['document.ready'] = now;
logger.log('(TIME) document ready:\t', now);
// Render the main/root Component.
ReactDOM.render(<App />, document.getElementById('react'));
});
// Workaround for the issue when returning to a page with the back button and
@@ -56,3 +55,21 @@ window.addEventListener('beforeunload', () => {
APP.API.dispose();
getJitsiMeetTransport().dispose();
});
const globalNS = getJitsiMeetGlobalNS();
globalNS.entryPoints = {
APP: App,
PREJOIN: PrejoinApp
};
globalNS.renderEntryPoint = ({
Component,
props = {},
elementId = 'react'
}) => {
ReactDOM.render(
<Component { ...props } />,
document.getElementById(elementId)
);
};

16
resources/update-ljm.sh Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/bash
set -e -u
THIS_DIR=$(cd -P "$(dirname "$(readlink "${BASH_SOURCE[0]}" || echo "${BASH_SOURCE[0]}")")" && pwd)
LATEST_LJM_COMMIT=$(git ls-remote https://github.com/jitsi/lib-jitsi-meet.git HEAD | awk '{ print $1 }')
pushd ${THIS_DIR}/..
npm install github:jitsi/lib-jitsi-meet#${LATEST_LJM_COMMIT}
git add package.json package-lock.json
git commit -m "deps: lib-jitsi-meet@latest"
popd
echo "Done! Now push your branch to GH and open a PR!"

41
static/prejoin.html Normal file
View File

@@ -0,0 +1,41 @@
<html xmlns="http://www.w3.org/1999/html">
<head>
<meta charset="utf-8">
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!--#include virtual="/base.html" -->
<link rel="stylesheet" href="../css/all.css">
<script>
document.addEventListener('DOMContentLoaded', () => {
if (!JitsiMeetJS.app) {
return;
}
const url = new URL(window.location.href);
const params = new URLSearchParams(url.search);
const showAvatar = params.get('showAvatar') === 'true';
const showJoinActions = params.get('showJoinActions') === 'true';
const css = params.get('style');
const style = document.createElement('style');
style.appendChild(document.createTextNode(css));
document.head.appendChild(style);
JitsiMeetJS.app.renderEntryPoint({
Component: JitsiMeetJS.app.entryPoints.PREJOIN,
props: {
showAvatar,
showJoinActions
}
})
})
</script>
<!--#include virtual="/title.html" -->
<script>var config = {}</script><!-- adapt to your needs, i.e. set hosts and bosh path -->
<script>var interfaceConfig = {}</script>
<script src="../libs/lib-jitsi-meet.min.js?v=139"></script>
<script src="../libs/app.bundle.min.js?v=139"></script>
</head>
<body>
<div id="react"></div>
</body>
</html>