diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx index 6f3ff8f7d5..fec55ce8c4 100755 --- a/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx @@ -75,12 +75,12 @@ class NavBar extends Component { ghost={true} circle={true} hideLabel={true} - label={'Toggle User-List'} + label={'User-List Toggle'} icon={'user'} className={cx(toggleBtnClasses)} /> -
+
{this.renderPresentationTitle()}
From c510921509909bd8ca837f604dcbc17a3b04bab0 Mon Sep 17 00:00:00 2001 From: KDSBrowne Date: Wed, 15 Mar 2017 08:36:42 -0700 Subject: [PATCH 03/91] remove DropdownListSeparator --- .../imports/ui/components/actions-bar/emoji-menu/component.jsx | 2 -- bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx | 1 - .../ui/components/nav-bar/settings-dropdown/component.jsx | 1 - .../ui/components/user-list/user-list-item/component.jsx | 2 -- 4 files changed, 6 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/component.jsx index 3af018f7cf..f06f6e2af1 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/component.jsx @@ -7,7 +7,6 @@ import DropdownTrigger from '/imports/ui/components/dropdown/trigger/component'; import DropdownContent from '/imports/ui/components/dropdown/content/component'; import DropdownList from '/imports/ui/components/dropdown/list/component'; import DropdownListItem from '/imports/ui/components/dropdown/list/item/component'; -import DropdownListSeparator from '/imports/ui/components/dropdown/list/separator/component'; const propTypes = { // Emoji status of the current user @@ -101,7 +100,6 @@ class EmojiMenu extends Component { description={intl.formatMessage(intlMessages.applauseDesc)} onClick={() => actions.setEmojiHandler('applause')} /> - - ), - (), ].concat(actions) } From cafe64c0de03e7c236198706713fa37cea852d01 Mon Sep 17 00:00:00 2001 From: KDSBrowne Date: Wed, 15 Mar 2017 08:43:59 -0700 Subject: [PATCH 04/91] add aria-expanded to user-list button --- .../imports/ui/components/nav-bar/component.jsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx index 59375d2363..282c97385d 100755 --- a/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx @@ -50,6 +50,9 @@ class NavBar extends Component { } handleToggleUserList() { + let x = document.getElementById("UserListBtn").getAttribute("aria-expanded"); + let isExpanded = (x == "true") ? "false" : "true"; + document.getElementById("UserListBtn").setAttribute("aria-expanded", isExpanded); this.props.toggleUserList(); } @@ -77,6 +80,8 @@ class NavBar extends Component { label={'User-List Toggle'} icon={'user'} className={cx(toggleBtnClasses)} + aria-expanded="false" + id="UserListBtn" />
From 8be9f4ee26c57084cb293f2d21c32d0251e3fd0d Mon Sep 17 00:00:00 2001 From: KDSBrowne Date: Wed, 15 Mar 2017 08:53:12 -0700 Subject: [PATCH 05/91] add label and role to close chat link --- .../imports/ui/components/chat/component.jsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/chat/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/component.jsx index dd56c01987..55d1448c50 100755 --- a/bigbluebutton-html5/imports/ui/components/chat/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/component.jsx @@ -26,11 +26,17 @@ export default class Chat extends Component { actions, } = this.props; + let closeChatLabel = "close " + title; + return (
- - {title} + + {title}
Date: Mon, 20 Mar 2017 15:00:04 -0300 Subject: [PATCH 06/91] Merge conflicts --- .../imports/ui/components/settings/service.js | 3 ++ .../imports/ui/services/settings/index.js | 39 +++++++++++++++++++ .../private/config/public/app.yaml | 27 +++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 bigbluebutton-html5/imports/ui/services/settings/index.js diff --git a/bigbluebutton-html5/imports/ui/components/settings/service.js b/bigbluebutton-html5/imports/ui/components/settings/service.js index 8b701b0c9a..546acc2a6b 100644 --- a/bigbluebutton-html5/imports/ui/components/settings/service.js +++ b/bigbluebutton-html5/imports/ui/components/settings/service.js @@ -3,6 +3,7 @@ import Users from '/imports/api/users'; import Captions from '/imports/api/captions'; import Auth from '/imports/ui/services/auth'; import _ from 'lodash'; +import Settings from '/imports/ui/services/settings'; const updateSettings = (obj) => { Object.keys(obj).forEach(k => Storage.setItem(`settings_${k}`, obj[k])); @@ -35,6 +36,8 @@ const getUserRoles = () => { }; const setDefaultSettings = () => { + console.log(Settings); + const defaultSettings = { application: { chatAudioNotifications: false, diff --git a/bigbluebutton-html5/imports/ui/services/settings/index.js b/bigbluebutton-html5/imports/ui/services/settings/index.js new file mode 100644 index 0000000000..1406bb15f7 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/services/settings/index.js @@ -0,0 +1,39 @@ +import Storage from '/imports/ui/services/storage/session'; +import _ from 'underscore'; + +const SettingsCollection = new Mongo.Collection(null); + +class Settings { + constructor() { + console.log('constructor 4Head'); + const defaultSettings = Meteor.settings.public.app.defaultSettings; + + const savedSettings = { + application: this.getSettingsFor('application'), + audio: this.getSettingsFor('audio'), + video: this.getSettingsFor('video'), + cc: this.getSettingsFor('cc'), + participants: this.getSettingsFor('participants'), + }; + + Object.keys(defaultSettings).forEach(key => { + this[key] = _.extend(defaultSettings[key], savedSettings[key]); + }); + } + + // get achalaboy(key) { + // return SettingsCollection.findOne({ key }).properties; + // } + + // set setalaboy(key, object) { + // SettingsCollection.upsert({ key }, object); + // } + + getSettingsFor(key) { + const setting = Storage.getItem(`settings_${key}`); + return setting; + }; +} + +const SettingsSingleton = new Settings(); +export default SettingsSingleton; diff --git a/bigbluebutton-html5/private/config/public/app.yaml b/bigbluebutton-html5/private/config/public/app.yaml index 8625663d06..4bd5c47d79 100755 --- a/bigbluebutton-html5/private/config/public/app.yaml +++ b/bigbluebutton-html5/private/config/public/app.yaml @@ -25,3 +25,30 @@ app: basename: '/html5client' defaultLocale: 'en' + #default settings for session storage + defaultSettings: + application: + chatAudioNotifications: false + chatPushNotifications: false + fontSize: "16px" + audio: + inputDeviceId: undefined + outputDeviceId: undefined + video: + viewParticipantsWebcams: true + cc: + backgroundColor: "#FFFFFF" + fontColor: "#000000" + closedCaptions: false + fontFamily: "Calibri" + fontSize: '16px' + locale: undefined + takeOwnership: false + participants: + muteAll: false + lockAll: false + lockAll: false + microphone: false + publicChat: false + privateChat: false + layout: false From 918684b6c7161fc1cae4ba8ca7691fa69f3ecdba Mon Sep 17 00:00:00 2001 From: KDSBrowne Date: Thu, 23 Mar 2017 07:22:02 -0700 Subject: [PATCH 07/91] change variable name --- .../imports/ui/components/nav-bar/component.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx index 276d7d38e1..8acf1f1238 100755 --- a/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx @@ -50,8 +50,8 @@ class NavBar extends Component { } handleToggleUserList() { - let x = document.getElementById("UserListBtn").getAttribute("aria-expanded"); - let isExpanded = (x == "true") ? "false" : "true"; + let btn = document.getElementById("UserListBtn").getAttribute("aria-expanded"); + let isExpanded = (btn == "true") ? "false" : "true"; document.getElementById("UserListBtn").setAttribute("aria-expanded", isExpanded); this.props.toggleUserList(); } From c4c9ce557728dedadc8b003793963195ad4e21ea Mon Sep 17 00:00:00 2001 From: KDSBrowne Date: Thu, 23 Mar 2017 07:31:54 -0700 Subject: [PATCH 08/91] fix lint issues --- .../imports/ui/components/nav-bar/component.jsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx index 8acf1f1238..676ae1ea1a 100755 --- a/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx @@ -50,9 +50,14 @@ class NavBar extends Component { } handleToggleUserList() { - let btn = document.getElementById("UserListBtn").getAttribute("aria-expanded"); - let isExpanded = (btn == "true") ? "false" : "true"; - document.getElementById("UserListBtn").setAttribute("aria-expanded", isExpanded); + let btn = document.getElementById('UserListBtn') + .getAttribute('aria-expanded'); + + let isExpanded = (btn == 'true') ? 'false' : 'true'; + + document.getElementById('UserListBtn') + .setAttribute('aria-expanded', isExpanded); + this.props.toggleUserList(); } From e1bdeffa466a1466fbf62aeb9387b8e2bf8e8731 Mon Sep 17 00:00:00 2001 From: KDSBrowne Date: Thu, 23 Mar 2017 07:34:59 -0700 Subject: [PATCH 09/91] add missing import to fix JoinAudio Btn --- bigbluebutton-html5/imports/ui/components/actions-bar/service.js | 1 + 1 file changed, 1 insertion(+) diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/service.js b/bigbluebutton-html5/imports/ui/components/actions-bar/service.js index 05043c8900..17a52d2b8f 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/service.js +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/service.js @@ -1,3 +1,4 @@ +import React from 'react'; import AuthSingleton from '/imports/ui/services/auth/index.js'; import Users from '/imports/api/users'; import { joinListenOnly } from '/imports/api/phone'; From d6bdbeb448339e00a7f0ba396bec621e34508761 Mon Sep 17 00:00:00 2001 From: KDSBrowne Date: Thu, 23 Mar 2017 11:26:44 -0700 Subject: [PATCH 10/91] add aria-label for unread messages --- .../ui/components/user-list/chat-list-item/component.jsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/component.jsx index 872f4d5119..3187187b22 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/component.jsx @@ -55,7 +55,10 @@ class ChatListItem extends Component {
{(chat.unreadCounter > 0) ?
-

{chat.unreadCounter}

+

+ {chat.unreadCounter} + +

: null} From 4b2928f2fb0c481696aa1638ab56e5ec93f09786 Mon Sep 17 00:00:00 2001 From: KDSBrowne Date: Thu, 23 Mar 2017 12:13:09 -0700 Subject: [PATCH 11/91] remove span element --- .../imports/ui/components/user-list/chat-list-item/component.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/component.jsx index 3187187b22..7d36e2fa22 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/component.jsx @@ -57,7 +57,6 @@ class ChatListItem extends Component {

{chat.unreadCounter} -

: null} From c26965e794993cf4b29d5c5f80a7790cbaa64717 Mon Sep 17 00:00:00 2001 From: KDSBrowne Date: Fri, 24 Mar 2017 07:49:38 -0700 Subject: [PATCH 12/91] make actions button display only for Presenter --- .../components/actions-bar/actions-dropdown/component.jsx | 5 ++++- .../imports/ui/components/actions-bar/component.jsx | 7 +++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx index f818c41198..dc00a6fdd5 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx @@ -50,7 +50,10 @@ class ActionsDropdown extends Component { } render() { - const { intl } = this.props; + const { intl, isUserPresenter } = this.props; + + if (!isUserPresenter) return null; + return ( diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx index 15043eff45..2eedd5e341 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx @@ -13,10 +13,13 @@ export default class ActionsBar extends Component { } renderForPresenter() { + + const { isUserPresenter } = this.props; + return (
- +
@@ -29,7 +32,7 @@ export default class ActionsBar extends Component {
- +
); From a633492b98c743d44116124ec119f366a3ef9056 Mon Sep 17 00:00:00 2001 From: KDSBrowne Date: Fri, 24 Mar 2017 08:34:45 -0700 Subject: [PATCH 13/91] move user and presenter views to main render function --- .../actions-dropdown/component.jsx | 2 +- .../ui/components/actions-bar/component.jsx | 34 +++---------------- .../ui/components/actions-bar/service.js | 7 ++-- 3 files changed, 7 insertions(+), 36 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx index dc00a6fdd5..3226ea7463 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx @@ -51,7 +51,7 @@ class ActionsDropdown extends Component { render() { const { intl, isUserPresenter } = this.props; - + console.log(this.props); if (!isUserPresenter) return null; return ( diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx index 2eedd5e341..7f6d5dca41 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx @@ -12,14 +12,13 @@ export default class ActionsBar extends Component { super(props); } - renderForPresenter() { - + render() { const { isUserPresenter } = this.props; - + console.log(this.props); return (
- +
@@ -32,34 +31,9 @@ export default class ActionsBar extends Component {
- +
); } - - renderForUser() { - return ( -
-
- - - {/**/} - -
-
- ); - } - - render() { - const { isUserPresenter } = this.props; - - return isUserPresenter ? - this.renderForPresenter() : - this.renderForUser(); - } } diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/service.js b/bigbluebutton-html5/imports/ui/components/actions-bar/service.js index 05043c8900..3cd7200bfb 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/service.js +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/service.js @@ -1,3 +1,4 @@ +import React from 'react'; import AuthSingleton from '/imports/ui/services/auth/index.js'; import Users from '/imports/api/users'; import { joinListenOnly } from '/imports/api/phone'; @@ -8,13 +9,9 @@ import Audio from '/imports/ui/components/audio-modal/component'; let isUserPresenter = () => { // check if user is a presenter - let isPresenter = Users.findOne({ + return isPresenter = Users.findOne({ userId: AuthSingleton.userID, }).user.presenter; - - return { - isUserPresenter: isPresenter, - }; }; const handleExitAudio = () => { From 1edb4ec736c9861abecbfe6f5157e07afe0fea0b Mon Sep 17 00:00:00 2001 From: KDSBrowne Date: Fri, 24 Mar 2017 08:37:33 -0700 Subject: [PATCH 14/91] remove console log --- .../ui/components/actions-bar/actions-dropdown/component.jsx | 2 +- .../imports/ui/components/actions-bar/component.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx index 3226ea7463..dc00a6fdd5 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx @@ -51,7 +51,7 @@ class ActionsDropdown extends Component { render() { const { intl, isUserPresenter } = this.props; - console.log(this.props); + if (!isUserPresenter) return null; return ( diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx index 7f6d5dca41..ee1045a09b 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx @@ -14,7 +14,7 @@ export default class ActionsBar extends Component { render() { const { isUserPresenter } = this.props; - console.log(this.props); + return (
From 93d1aed4c3f71f16b65bc77eb20c5c446a326706 Mon Sep 17 00:00:00 2001 From: KDSBrowne Date: Fri, 24 Mar 2017 12:27:45 -0700 Subject: [PATCH 15/91] remove unneeded variable --- .../imports/ui/components/actions-bar/service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/service.js b/bigbluebutton-html5/imports/ui/components/actions-bar/service.js index 3cd7200bfb..1d3d44cf6e 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/service.js +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/service.js @@ -9,7 +9,7 @@ import Audio from '/imports/ui/components/audio-modal/component'; let isUserPresenter = () => { // check if user is a presenter - return isPresenter = Users.findOne({ + return Users.findOne({ userId: AuthSingleton.userID, }).user.presenter; }; From d802d26836444a0c7f7b2f9a2668c153f893ab42 Mon Sep 17 00:00:00 2001 From: KDSBrowne Date: Mon, 27 Mar 2017 11:10:24 -0700 Subject: [PATCH 16/91] remove direct DOM manipulation --- .../imports/ui/components/nav-bar/component.jsx | 17 ++++------------- .../imports/ui/components/nav-bar/container.jsx | 17 +++++++++++++++-- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx index 676ae1ea1a..fafe923d5c 100755 --- a/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx @@ -2,9 +2,7 @@ import React, { Component, PropTypes } from 'react'; import _ from 'lodash'; import cx from 'classnames'; import styles from './styles.scss'; - import { showModal } from '/imports/ui/components/app/service'; - import Button from '../button/component'; import RecordingIndicator from './recording-indicator/component'; import SettingsDropdownContainer from './settings-dropdown/container'; @@ -50,14 +48,8 @@ class NavBar extends Component { } handleToggleUserList() { - let btn = document.getElementById('UserListBtn') - .getAttribute('aria-expanded'); - - let isExpanded = (btn == 'true') ? 'false' : 'true'; - - document.getElementById('UserListBtn') - .setAttribute('aria-expanded', isExpanded); - + let toggled = (this.props.isExpanded == false) ? true : false; + this.props.setToggleState(toggled); this.props.toggleUserList(); } @@ -68,7 +60,7 @@ class NavBar extends Component { } render() { - const { hasUnreadMessages, beingRecorded } = this.props; + const { hasUnreadMessages, beingRecorded, isExpanded } = this.props; let toggleBtnClasses = {}; toggleBtnClasses[styles.btn] = true; @@ -85,8 +77,7 @@ class NavBar extends Component { label={'User-List Toggle'} icon={'user'} className={cx(toggleBtnClasses)} - aria-expanded="false" - id="UserListBtn" + aria-expanded={isExpanded} />
diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/container.jsx b/bigbluebutton-html5/imports/ui/components/nav-bar/container.jsx index b822921af8..aab3dd655f 100755 --- a/bigbluebutton-html5/imports/ui/components/nav-bar/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/nav-bar/container.jsx @@ -1,14 +1,13 @@ import React, { Component, PropTypes } from 'react'; import { createContainer } from 'meteor/react-meteor-data'; import { withRouter } from 'react-router'; - import Meetings from '/imports/api/meetings'; import Auth from '/imports/ui/services/auth'; import userListService from '../user-list/service'; import ChatService from '../chat/service'; import Service from './service'; import { meetingIsBreakout } from '/imports/ui/components/app/service'; - +import LocalStorage from '/imports/ui/services/storage/local.js'; import NavBar from './component'; const CHAT_CONFIG = Meteor.settings.public.chat; @@ -19,6 +18,10 @@ class NavBarContainer extends Component { super(props); } + componentWillUnmount() { + LocalStorage.removeItem('bbb.toggleUserList.isExpanded'); + } + render() { return ( @@ -29,6 +32,14 @@ class NavBarContainer extends Component { } export default withRouter(createContainer(({ location, router }) => { + + let toggleState = LocalStorage.getItem('bbb.toggleUserList.isExpanded'); + let isExpanded = (!toggleState) ? false : toggleState; + + const setToggleState = (state) => { + LocalStorage.setItem('bbb.toggleUserList.isExpanded', state); + }; + let meetingTitle; let meetingRecorded; @@ -60,6 +71,8 @@ export default withRouter(createContainer(({ location, router }) => { const currentUserId = Auth.userID; return { + setToggleState, + isExpanded, breakouts, currentUserId, meetingId, From 782deb6525698c5919c8b1e02864cdd531eb01cf Mon Sep 17 00:00:00 2001 From: KDSBrowne Date: Mon, 27 Mar 2017 12:49:46 -0700 Subject: [PATCH 17/91] add formatted messages and remove generic divs --- .../imports/ui/components/app/component.jsx | 67 +++++++++++++------ bigbluebutton-html5/private/locales/en.json | 4 ++ 2 files changed, 51 insertions(+), 20 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/app/component.jsx b/bigbluebutton-html5/imports/ui/components/app/component.jsx index ff039dc272..6f9ad85d76 100755 --- a/bigbluebutton-html5/imports/ui/components/app/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/app/component.jsx @@ -1,7 +1,7 @@ import React, { Component, PropTypes } from 'react'; import { FormattedMessage } from 'react-intl'; import _ from 'lodash'; - +import { defineMessages, injectIntl } from 'react-intl'; import NotificationsBarContainer from '../notifications-bar/container'; import AudioNotificationContainer from '../audio-notification/container'; import ChatNotificationContainer from '../chat/notification/container'; @@ -10,6 +10,21 @@ import Button from '../button/component'; import styles from './styles'; import cx from 'classnames'; +const intlMessages = defineMessages({ + userListLabel: { + id: 'app.userlist.Label', + }, + chatLabel: { + id: 'app.chat.Label', + }, + mediaLabel: { + id: 'app.media.Label', + }, + actionsbarLabel: { + id: 'app.actionsBar.Label', + }, +}); + const propTypes = { init: PropTypes.func.isRequired, fontSize: PropTypes.string, @@ -24,7 +39,7 @@ const defaultProps = { fontSize: '16px', }; -export default class App extends Component { +class App extends Component { constructor(props) { super(props); @@ -45,9 +60,9 @@ export default class App extends Component { if (!navbar) return null; return ( -
+
{navbar} -
+ ); } @@ -64,7 +79,7 @@ export default class App extends Component { } renderUserList() { - let { userList } = this.props; + let { userList, intl } = this.props; const { compactUserList } = this.state; if (!userList) return; @@ -76,45 +91,56 @@ export default class App extends Component { }); return ( -
- {userList} -
+ ); } renderChat() { - const { chat } = this.props; + const { chat, intl } = this.props; if (!chat) return null; return ( -
- {chat} -
+
+ {chat} +
); } renderMedia() { - const { media } = this.props; + const { media, intl } = this.props; if (!media) return null; return ( -
- {media} -
+
+ {media} +
); } renderActionsBar() { - const { actionsbar } = this.props; + const { actionsbar, intl } = this.props; if (!actionsbar) return null; return ( -
- {actionsbar} -
+
+ {actionsbar} +
); } @@ -145,3 +171,4 @@ export default class App extends Component { App.propTypes = propTypes; App.defaultProps = defaultProps; +export default injectIntl(App); diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json index fc7a3c5ea3..4bb823b86d 100755 --- a/bigbluebutton-html5/private/locales/en.json +++ b/bigbluebutton-html5/private/locales/en.json @@ -5,6 +5,7 @@ "app.userlist.messagesTitle": "Messages", "app.userlist.presenter": "Presenter", "app.userlist.you": "You", + "app.userlist.Label": "User List", "app.chat.submitLabel": "Send Message", "app.chat.inputLabel": "Message input for chat {name}", "app.chat.inputPlaceholder": "Message {name}", @@ -12,6 +13,8 @@ "app.chat.titlePrivate": "Private Chat with {name}", "app.chat.partnerDisconnected": "{name} has left the meeting", "app.chat.moreMessages": "More messages below", + "app.chat.Label": "Chat", + "app.media.Label": "Media", "app.presentation.presentationToolbar.prevSlideLabel": "Previous slide", "app.presentation.presentationToolbar.prevSlideDescrip": "Change the presentation to the previous slide", "app.presentation.presentationToolbar.nextSlideLabel": "Next slide", @@ -54,6 +57,7 @@ "app.actionsBar.muteLabel": "Mute", "app.actionsBar.camOffLabel": "Cam Off", "app.actionsBar.raiseLabel": "Raise", + "app.actionsBar.Label": "Actions Bar", "app.actionsBar.actionsDropdown.actionsLabel": "Actions", "app.actionsBar.actionsDropdown.presentationLabel": "Upload a presentation", "app.actionsBar.actionsDropdown.initPollLabel": "Initiate a poll", From 2fae315db2a4c8711e72e7c78b6ba5584d90857b Mon Sep 17 00:00:00 2001 From: KDSBrowne Date: Mon, 27 Mar 2017 14:41:10 -0700 Subject: [PATCH 18/91] add menu item separation NVDA fiendly --- .../actions-bar/emoji-menu/component.jsx | 1 + .../dropdown/list/item/component.jsx | 26 +++++++++++++++---- .../nav-bar/settings-dropdown/component.jsx | 1 + .../user-list/user-list-item/component.jsx | 1 + bigbluebutton-html5/private/locales/en.json | 2 ++ 5 files changed, 26 insertions(+), 5 deletions(-) mode change 100644 => 100755 bigbluebutton-html5/imports/ui/components/dropdown/list/item/component.jsx diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/component.jsx index f06f6e2af1..f282666019 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/component.jsx @@ -100,6 +100,7 @@ class EmojiMenu extends Component { description={intl.formatMessage(intlMessages.applauseDesc)} onClick={() => actions.setEmojiHandler('applause')} /> + + ), + (), ].concat(actions) } diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json index 4bb823b86d..17ab218785 100755 --- a/bigbluebutton-html5/private/locales/en.json +++ b/bigbluebutton-html5/private/locales/en.json @@ -14,6 +14,8 @@ "app.chat.partnerDisconnected": "{name} has left the meeting", "app.chat.moreMessages": "More messages below", "app.chat.Label": "Chat", + "app.dropdownListItem.separatorLabel": "separator", + "app.dropdownListItem.separatorDesc": "used to visually divide menu items", "app.media.Label": "Media", "app.presentation.presentationToolbar.prevSlideLabel": "Previous slide", "app.presentation.presentationToolbar.prevSlideDescrip": "Change the presentation to the previous slide", From f7cf86ddf0bf569bb4f81249cdb7d59d1e498436 Mon Sep 17 00:00:00 2001 From: KDSBrowne Date: Mon, 27 Mar 2017 14:57:05 -0700 Subject: [PATCH 19/91] change role from section to region --- bigbluebutton-html5/imports/ui/components/app/component.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bigbluebutton-html5/imports/ui/components/app/component.jsx b/bigbluebutton-html5/imports/ui/components/app/component.jsx index 6f9ad85d76..a8e1b5ae33 100755 --- a/bigbluebutton-html5/imports/ui/components/app/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/app/component.jsx @@ -107,7 +107,7 @@ class App extends Component { return (
{chat}
From 1a15f83690118c2b9ab182c7655d77a0b2d2e74f Mon Sep 17 00:00:00 2001 From: gcampes Date: Tue, 28 Mar 2017 15:41:48 -0300 Subject: [PATCH 20/91] adds settings service --- .../imports/ui/components/app/container.jsx | 3 - .../imports/ui/components/app/service.js | 8 +-- .../chat/notification/container.jsx | 4 +- .../ui/components/settings/component.jsx | 4 +- .../ui/components/settings/container.jsx | 15 ++-- .../imports/ui/components/settings/service.js | 69 ++----------------- .../imports/ui/services/settings/index.js | 65 ++++++++++------- 7 files changed, 61 insertions(+), 107 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/app/container.jsx b/bigbluebutton-html5/imports/ui/components/app/container.jsx index 491f2681c8..88c0062690 100755 --- a/bigbluebutton-html5/imports/ui/components/app/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/app/container.jsx @@ -10,8 +10,6 @@ import { getCaptionsStatus, } from './service'; -import { setDefaultSettings } from '../settings/service'; - import Auth from '/imports/ui/services/auth'; import Users from '/imports/api/users'; import Breakouts from '/imports/api/breakouts'; @@ -53,7 +51,6 @@ class AppContainer extends Component { const APP_CONFIG = Meteor.settings.public.app; const init = () => { - setDefaultSettings(); if (APP_CONFIG.autoJoinAudio) { showModal(); } diff --git a/bigbluebutton-html5/imports/ui/components/app/service.js b/bigbluebutton-html5/imports/ui/components/app/service.js index 6d96eee061..e6c7c983cc 100755 --- a/bigbluebutton-html5/imports/ui/components/app/service.js +++ b/bigbluebutton-html5/imports/ui/components/app/service.js @@ -1,5 +1,5 @@ import Breakouts from '/imports/api/breakouts'; -import SettingsService from '/imports/ui/components/settings/service'; +import Settings from '/imports/ui/services/settings'; let currentModal = { component: null, @@ -23,12 +23,12 @@ const clearModal = () => { }; const getCaptionsStatus = () => { - const settings = SettingsService.getSettingsFor('cc'); - return settings ? settings.closedCaptions : false; + const settings = Settings.cc; + return settings ? settings.cc : false; }; const getFontSize = () => { - const settings = SettingsService.getSettingsFor('application'); + const settings = Settings.application; return settings ? settings.fontSize : '16px'; }; diff --git a/bigbluebutton-html5/imports/ui/components/chat/notification/container.jsx b/bigbluebutton-html5/imports/ui/components/chat/notification/container.jsx index 624ead8f82..92db45757e 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/notification/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/notification/container.jsx @@ -4,7 +4,7 @@ import _ from 'lodash'; import Auth from '/imports/ui/services/auth'; import UserListService from '/imports/ui/components/user-list/service'; -import SettingsService from '/imports/ui/components/settings/service'; +import Settings from '/imports/ui/services/settings'; class ChatNotificationContainer extends Component { constructor(props) { @@ -30,7 +30,7 @@ class ChatNotificationContainer extends Component { } export default createContainer(({ currentChatID }) => { - const AppSettings = SettingsService.getSettingsFor('application'); + const AppSettings = Settings.application; const unreadMessagesCount = UserListService.getOpenChats() .map(chat => chat.unreadCounter) diff --git a/bigbluebutton-html5/imports/ui/components/settings/component.jsx b/bigbluebutton-html5/imports/ui/components/settings/component.jsx index 51aa3e4c5f..0a025badca 100644 --- a/bigbluebutton-html5/imports/ui/components/settings/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/settings/component.jsx @@ -42,7 +42,7 @@ export default class Settings extends Component { selectedTab: 0, }; - this.handleSettingsApply = props.updateSettings; + this.updateSettings = props.updateSettings; this.handleUpdateSettings = this.handleUpdateSettings.bind(this); this.handleSelectTab = this.handleSelectTab.bind(this); } @@ -57,7 +57,7 @@ export default class Settings extends Component { title="Settings" confirm={{ callback: (() => { - this.handleSettingsApply(this.state.current); + this.updateSettings(this.state.current); }), label: 'Save', description: 'Saves the changes and close the settings menu', diff --git a/bigbluebutton-html5/imports/ui/components/settings/container.jsx b/bigbluebutton-html5/imports/ui/components/settings/container.jsx index 39448763f5..a57058fb69 100644 --- a/bigbluebutton-html5/imports/ui/components/settings/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/settings/container.jsx @@ -2,11 +2,12 @@ import React, { Component, PropTypes } from 'react'; import { createContainer } from 'meteor/react-meteor-data'; import _ from 'lodash'; import Settings from './component'; +import SettingsService from '/imports/ui/services/settings'; + import { - getSettingsFor, - updateSettings, getClosedCaptionLocales, getUserRoles, + updateSettings, } from './service'; class SettingsContainer extends Component { @@ -19,11 +20,11 @@ class SettingsContainer extends Component { export default createContainer(() => { return { - audio: getSettingsFor('audio'), - video: getSettingsFor('video'), - application: getSettingsFor('application'), - cc: getSettingsFor('cc'), - participants: getSettingsFor('participants'), + audio: SettingsService.audio, + video: SettingsService.video, + application: SettingsService.application, + cc: SettingsService.cc, + participants: SettingsService.participants, updateSettings, locales: getClosedCaptionLocales(), isModerator: getUserRoles() === 'MODERATOR', diff --git a/bigbluebutton-html5/imports/ui/components/settings/service.js b/bigbluebutton-html5/imports/ui/components/settings/service.js index 546acc2a6b..b59adea85b 100644 --- a/bigbluebutton-html5/imports/ui/components/settings/service.js +++ b/bigbluebutton-html5/imports/ui/components/settings/service.js @@ -1,20 +1,9 @@ -import Storage from '/imports/ui/services/storage/session'; import Users from '/imports/api/users'; import Captions from '/imports/api/captions'; import Auth from '/imports/ui/services/auth'; import _ from 'lodash'; import Settings from '/imports/ui/services/settings'; -const updateSettings = (obj) => { - Object.keys(obj).forEach(k => Storage.setItem(`settings_${k}`, obj[k])); -}; - -const getSettingsFor = (key) => { - const setting = Storage.getItem(`settings_${key}`); - - return setting; -}; - const getClosedCaptionLocales = () => { //list of unique locales in the Captions Collection const locales = _.uniq(Captions.find({}, { @@ -35,63 +24,13 @@ const getUserRoles = () => { return user.role; }; -const setDefaultSettings = () => { - console.log(Settings); - - const defaultSettings = { - application: { - chatAudioNotifications: false, - chatPushNotifications: false, - fontSize: '16px', - }, - audio: { - inputDeviceId: undefined, - outputDeviceId: undefined, - }, - video: { - viewParticipantsWebcams: true, - }, - cc: { - backgroundColor: '#FFFFFF', - fontColor: '#000000', - closedCaptions: false, - fontFamily: 'Calibri', - fontSize: -1, - locale: undefined, - takeOwnership: false, - }, - participants: { - muteAll: false, - lockAll: false, - lockAll: false, - microphone: false, - publicChat: false, - privateChat: false, - layout: false, - }, - }; - - const savedSettings = { - application: getSettingsFor('application'), - audio: getSettingsFor('audio'), - video: getSettingsFor('video'), - cc: getSettingsFor('cc'), - participants: getSettingsFor('participants'), - }; - - let settings = {}; - - Object.keys(defaultSettings).forEach(key => { - settings[key] = _.extend(defaultSettings[key], savedSettings[key]); - }); - - updateSettings(settings); +const updateSettings = (obj) => { + Object.keys(obj).forEach(k => Settings[k] = obj[k]); + Settings.save(); }; export { - updateSettings, - getSettingsFor, getClosedCaptionLocales, getUserRoles, - setDefaultSettings, + updateSettings, }; diff --git a/bigbluebutton-html5/imports/ui/services/settings/index.js b/bigbluebutton-html5/imports/ui/services/settings/index.js index 1406bb15f7..7bc078e115 100644 --- a/bigbluebutton-html5/imports/ui/services/settings/index.js +++ b/bigbluebutton-html5/imports/ui/services/settings/index.js @@ -1,39 +1,56 @@ import Storage from '/imports/ui/services/storage/session'; import _ from 'underscore'; -const SettingsCollection = new Mongo.Collection(null); +const SETTINGS = [ + 'application', + 'audio', + 'video', + 'cc', + 'participants', +]; class Settings { - constructor() { - console.log('constructor 4Head'); - const defaultSettings = Meteor.settings.public.app.defaultSettings; + constructor(defaultValues = {}) { + SETTINGS.forEach(p => { + const privateProp = `_${p}`; + this[privateProp] = { + tracker: new Tracker.Dependency, + value: undefined, + }; - const savedSettings = { - application: this.getSettingsFor('application'), - audio: this.getSettingsFor('audio'), - video: this.getSettingsFor('video'), - cc: this.getSettingsFor('cc'), - participants: this.getSettingsFor('participants'), - }; + Object.defineProperty(this, p, { + get: () => { + this[privateProp].tracker.depend(); + return this[privateProp].value; + }, - Object.keys(defaultSettings).forEach(key => { - this[key] = _.extend(defaultSettings[key], savedSettings[key]); + set: v => { + this[privateProp].value = v; + this[privateProp].tracker.changed(); + }, + }); }); + this.setDefault(defaultValues); } - // get achalaboy(key) { - // return SettingsCollection.findOne({ key }).properties; - // } + setDefault(defaultValues) { + const savedSettings = {}; - // set setalaboy(key, object) { - // SettingsCollection.upsert({ key }, object); - // } + SETTINGS.forEach(s => { + savedSettings[s] = Storage.getItem(`settings_${s}`); + }); - getSettingsFor(key) { - const setting = Storage.getItem(`settings_${key}`); - return setting; - }; + Object.keys(defaultValues).forEach(key => { + this[key] = _.extend(defaultValues[key], savedSettings[key]); + }); + + this.save(); + }; + + save() { + Object.keys(this).forEach(k => Storage.setItem(`settings${k}`, this[k].value)); + } } -const SettingsSingleton = new Settings(); +const SettingsSingleton = new Settings(Meteor.settings.public.app.defaultSettings); export default SettingsSingleton; From 52578a2eea53580d2e3a9e12e52055b3445a4815 Mon Sep 17 00:00:00 2001 From: gcampes Date: Tue, 28 Mar 2017 17:16:57 -0300 Subject: [PATCH 21/91] Fix rollback settings issue --- .../imports/ui/components/settings/component.jsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/settings/component.jsx b/bigbluebutton-html5/imports/ui/components/settings/component.jsx index 0a025badca..339491d962 100644 --- a/bigbluebutton-html5/imports/ui/components/settings/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/settings/component.jsx @@ -26,11 +26,11 @@ export default class Settings extends Component { this.state = { current: { - audio, - video, - application, - cc, - participants, + audio: _.clone(audio), + video: _.clone(video), + application: _.clone(application), + cc: _.clone(cc), + participants: _.clone(participants), }, saved: { audio: _.clone(audio), From 67fbb8ddef400f3a019a56e9feffb8b015e5351c Mon Sep 17 00:00:00 2001 From: KDSBrowne Date: Wed, 29 Mar 2017 06:56:54 -0700 Subject: [PATCH 22/91] remove hidden duplicate component --- .../ui/components/actions-bar/component.jsx | 5 +---- .../imports/ui/components/actions-bar/styles.scss | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx index ee1045a09b..e6e2a36cc9 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx @@ -20,7 +20,7 @@ export default class ActionsBar extends Component {
-
+
*/}
-
- -
); } diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss b/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss index 86f45a5a1c..d1a75bebc7 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss @@ -7,7 +7,8 @@ .left, .right, -.center, +.relative, +.absolute, .hidden { display: flex; flex-direction: row; @@ -25,8 +26,18 @@ flex: 0; } -.center { +.relative { flex: 1; + position: relative; + left: 0; + right: 0; +} + +.absolute { + flex: 1; + position: absolute; + left: 0; + right: 0; } .hidden { From 396e7e4f0b05ab651892911ebbe9efe9c8f05b0f Mon Sep 17 00:00:00 2001 From: KDSBrowne Date: Wed, 29 Mar 2017 06:59:43 -0700 Subject: [PATCH 23/91] clean up css --- .../imports/ui/components/actions-bar/styles.scss | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss b/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss index d1a75bebc7..db90afdda2 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss @@ -26,18 +26,19 @@ flex: 0; } -.relative { +.relative, +.absolute { flex: 1; - position: relative; left: 0; right: 0; } +.relative { + position: relative +} + .absolute { - flex: 1; position: absolute; - left: 0; - right: 0; } .hidden { From 658c80147cba980a4bbffea789415b5bcef238e0 Mon Sep 17 00:00:00 2001 From: KDSBrowne Date: Wed, 29 Mar 2017 07:31:15 -0700 Subject: [PATCH 24/91] remove local storage usage and add separator --- .../actions-bar/emoji-menu/component.jsx | 3 ++- .../imports/ui/components/nav-bar/component.jsx | 2 -- .../imports/ui/components/nav-bar/container.jsx | 14 ++------------ .../nav-bar/settings-dropdown/component.jsx | 2 +- .../user-list/user-list-item/component.jsx | 3 ++- 5 files changed, 7 insertions(+), 17 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/component.jsx index f282666019..3af018f7cf 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/component.jsx @@ -7,6 +7,7 @@ import DropdownTrigger from '/imports/ui/components/dropdown/trigger/component'; import DropdownContent from '/imports/ui/components/dropdown/content/component'; import DropdownList from '/imports/ui/components/dropdown/list/component'; import DropdownListItem from '/imports/ui/components/dropdown/list/item/component'; +import DropdownListSeparator from '/imports/ui/components/dropdown/list/separator/component'; const propTypes = { // Emoji status of the current user @@ -100,7 +101,7 @@ class EmojiMenu extends Component { description={intl.formatMessage(intlMessages.applauseDesc)} onClick={() => actions.setEmojiHandler('applause')} /> - + @@ -33,13 +29,6 @@ class NavBarContainer extends Component { export default withRouter(createContainer(({ location, router }) => { - let toggleState = LocalStorage.getItem('bbb.toggleUserList.isExpanded'); - let isExpanded = (!toggleState) ? false : toggleState; - - const setToggleState = (state) => { - LocalStorage.setItem('bbb.toggleUserList.isExpanded', state); - }; - let meetingTitle; let meetingRecorded; @@ -70,8 +59,9 @@ export default withRouter(createContainer(({ location, router }) => { const breakouts = Service.getBreakouts(); const currentUserId = Auth.userID; + let isExpanded = location.pathname.indexOf('/users') !== -1; + return { - setToggleState, isExpanded, breakouts, currentUserId, diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/component.jsx index 33f6c73c1a..af0dd45102 100755 --- a/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/component.jsx @@ -121,7 +121,7 @@ class SettingsDropdown extends Component { description={intl.formatMessage(intlMessages.aboutDesc)} onClick={openAbout.bind(this)} /> - + ), - (), + (), ].concat(actions) } From 208383d3a55d4c9d6418a5bf04e3d84204973e77 Mon Sep 17 00:00:00 2001 From: KDSBrowne Date: Wed, 29 Mar 2017 08:09:47 -0700 Subject: [PATCH 25/91] fix DropdownListSeparator to work with NVDA --- .../actions-bar/emoji-menu/component.jsx | 2 +- .../dropdown/list/item/component.jsx | 22 ++----------------- .../dropdown/list/separator/component.jsx | 6 ++--- bigbluebutton-html5/private/locales/en.json | 2 -- 4 files changed, 5 insertions(+), 27 deletions(-) mode change 100644 => 100755 bigbluebutton-html5/imports/ui/components/dropdown/list/separator/component.jsx diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/component.jsx index 3af018f7cf..40fc41a48b 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/component.jsx @@ -101,7 +101,7 @@ class EmojiMenu extends Component { description={intl.formatMessage(intlMessages.applauseDesc)} onClick={() => actions.setEmojiHandler('applause')} /> - + ; + return
  • ; } } diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json index 17ab218785..4bb823b86d 100755 --- a/bigbluebutton-html5/private/locales/en.json +++ b/bigbluebutton-html5/private/locales/en.json @@ -14,8 +14,6 @@ "app.chat.partnerDisconnected": "{name} has left the meeting", "app.chat.moreMessages": "More messages below", "app.chat.Label": "Chat", - "app.dropdownListItem.separatorLabel": "separator", - "app.dropdownListItem.separatorDesc": "used to visually divide menu items", "app.media.Label": "Media", "app.presentation.presentationToolbar.prevSlideLabel": "Previous slide", "app.presentation.presentationToolbar.prevSlideDescrip": "Change the presentation to the previous slide", From 71fef7c86a6fd2a38f42880f502d16910dd382c1 Mon Sep 17 00:00:00 2001 From: KDSBrowne Date: Wed, 29 Mar 2017 08:12:07 -0700 Subject: [PATCH 26/91] remove unused import --- bigbluebutton-html5/imports/ui/components/nav-bar/container.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/container.jsx b/bigbluebutton-html5/imports/ui/components/nav-bar/container.jsx index 4e219ce7ac..c4de6ef3a1 100755 --- a/bigbluebutton-html5/imports/ui/components/nav-bar/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/nav-bar/container.jsx @@ -7,7 +7,6 @@ import userListService from '../user-list/service'; import ChatService from '../chat/service'; import Service from './service'; import { meetingIsBreakout } from '/imports/ui/components/app/service'; -import LocalStorage from '/imports/ui/services/storage/local.js'; import NavBar from './component'; const CHAT_CONFIG = Meteor.settings.public.chat; From 4cbf7bedbb945d83ed2194f2d10f631a5f1ffe8a Mon Sep 17 00:00:00 2001 From: KDSBrowne Date: Wed, 29 Mar 2017 08:22:26 -0700 Subject: [PATCH 27/91] add formatted message for userlist toggle btn label --- .../imports/ui/components/nav-bar/component.jsx | 15 +++++++++++---- bigbluebutton-html5/private/locales/en.json | 1 + 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx index 0e4cde02e6..c251605273 100755 --- a/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx @@ -13,6 +13,14 @@ import DropdownTrigger from '/imports/ui/components/dropdown/trigger/component'; import DropdownContent from '/imports/ui/components/dropdown/content/component'; import DropdownList from '/imports/ui/components/dropdown/list/component'; import DropdownListItem from '/imports/ui/components/dropdown/list/item/component'; +import { defineMessages, injectIntl } from 'react-intl'; + + +const intlMessages = defineMessages({ + toggleUserListLabel: { + id: 'app.navBar.userListToggleBtnLabel', + }, +}); const propTypes = { presentationTitle: PropTypes.string.isRequired, @@ -58,7 +66,7 @@ class NavBar extends Component { } render() { - const { hasUnreadMessages, beingRecorded, isExpanded } = this.props; + const { hasUnreadMessages, beingRecorded, isExpanded, intl } = this.props; let toggleBtnClasses = {}; toggleBtnClasses[styles.btn] = true; @@ -72,7 +80,7 @@ class NavBar extends Component { ghost={true} circle={true} hideLabel={true} - label={'User-List Toggle'} + label={intl.formatMessage(intlMessages.toggleUserListLabel)} icon={'user'} className={cx(toggleBtnClasses)} aria-expanded={isExpanded} @@ -166,5 +174,4 @@ class NavBar extends Component { NavBar.propTypes = propTypes; NavBar.defaultProps = defaultProps; - -export default NavBar; +export default injectIntl(NavBar); diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json index 4bb823b86d..04a0806d87 100755 --- a/bigbluebutton-html5/private/locales/en.json +++ b/bigbluebutton-html5/private/locales/en.json @@ -41,6 +41,7 @@ "app.navBar.settingsDropdown.leaveSessionDesc": "Leave the meeting", "app.navBar.settingsDropdown.exitFullScreenLabel": "Exit fullscreen", "app.navBar.settingsDropdown.exitFullScreenDesc": "Exit fullscreen mode", + "app.navBar.userListToggleBtnLabel": "User List Toggle", "app.leaveConfirmation.title": "Leave Session", "app.leaveConfirmation.message": "Do you want to leave this meeting?", "app.leaveConfirmation.confirmLabel": "Leave", From 00f77d1fd4fdfd4c5ed88ccf8e4011a545372978 Mon Sep 17 00:00:00 2001 From: KDSBrowne Date: Wed, 29 Mar 2017 08:35:07 -0700 Subject: [PATCH 28/91] add formatted message for close chat link --- .../imports/ui/components/chat/component.jsx | 22 ++++++++++++++----- bigbluebutton-html5/private/locales/en.json | 1 + 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/chat/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/component.jsx index 192898177a..ca1e0d85d1 100755 --- a/bigbluebutton-html5/imports/ui/components/chat/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/component.jsx @@ -1,14 +1,20 @@ import React, { Component } from 'react'; import { Link } from 'react-router'; import styles from './styles'; - +import { defineMessages, injectIntl } from 'react-intl'; import MessageForm from './message-form/component'; import MessageList from './message-list/component'; import Icon from '../icon/component'; const ELEMENT_ID = 'chat-messages'; -export default class Chat extends Component { +const intlMessages = defineMessages({ + closeChatLabel: { + id: 'app.chat.closeChatLabel', + }, +}); + +class Chat extends Component { constructor(props) { super(props); } @@ -24,17 +30,19 @@ export default class Chat extends Component { lastReadMessageTime, isChatLocked, actions, + intl, } = this.props; - let closeChatLabel = "close " + title; - return (
    - - {title} + + {title}
    @@ -69,3 +77,5 @@ export default class Chat extends Component { ); } } + +export default injectIntl(Chat); diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json index 04a0806d87..cc80390eec 100755 --- a/bigbluebutton-html5/private/locales/en.json +++ b/bigbluebutton-html5/private/locales/en.json @@ -12,6 +12,7 @@ "app.chat.titlePublic": "Public Chat", "app.chat.titlePrivate": "Private Chat with {name}", "app.chat.partnerDisconnected": "{name} has left the meeting", + "app.chat.closeChatLabel": "Close {title}", "app.chat.moreMessages": "More messages below", "app.chat.Label": "Chat", "app.media.Label": "Media", From 694baed72404d7bcfb022067be3a616547eca1b9 Mon Sep 17 00:00:00 2001 From: KDSBrowne Date: Wed, 29 Mar 2017 10:44:20 -0700 Subject: [PATCH 29/91] use cx import to handle styles --- .../ui/components/actions-bar/component.jsx | 39 ++++++++++++++----- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx index e6e2a36cc9..93737f3778 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx @@ -6,12 +6,40 @@ import ActionsDropdown from './actions-dropdown/component'; import JoinAudioOptionsContainer from './audio-menu/container'; import MuteAudioContainer from './mute-button/container'; import JoinVideo from './video-button/component'; +import cx from 'classnames'; export default class ActionsBar extends Component { constructor(props) { super(props); } + renderCenterBtns() { + const { isUserPresenter } = this.props; + + let centerContent; + + let positionClasses = {}; + positionClasses[styles.relative] = !isUserPresenter; + positionClasses[styles.absolute] = isUserPresenter; + + if (isUserPresenter || !isUserPresenter) { + centerContent = ( +
    + + + {/**/} + +
    + ); + } + + return centerContent; + } + render() { const { isUserPresenter } = this.props; @@ -20,16 +48,7 @@ export default class ActionsBar extends Component {
    -
    - - - {/**/} - -
    + {this.renderCenterBtns()}
    ); } From 767c9181f99993f1be6f11e5178d88725d87d150 Mon Sep 17 00:00:00 2001 From: KDSBrowne Date: Thu, 30 Mar 2017 06:42:11 -0700 Subject: [PATCH 30/91] add desc to give context to unread message count --- .../user-list/chat-list-item/component.jsx | 178 ++++++++++-------- bigbluebutton-html5/private/locales/en.json | 2 + 2 files changed, 98 insertions(+), 82 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/component.jsx index 872f4d5119..f33092fe2e 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/component.jsx @@ -1,82 +1,96 @@ -import React, { Component } from 'react'; -import UserAvatar from '/imports/ui/components/user-avatar/component'; -import Icon from '/imports/ui/components/icon/component'; -import styles from './styles.scss'; -import { withRouter } from 'react-router'; -import { Link } from 'react-router'; -import cx from 'classnames'; -import { defineMessages, injectIntl } from 'react-intl'; - -const intlMessages = defineMessages({ - titlePublic: { - id: 'app.chat.titlePublic', - defaultMessage: "Public Chat", - }, -}); - -const CHAT_CONFIG = Meteor.settings.public.chat; -const PRIVATE_CHAT_PATH = CHAT_CONFIG.path_route; - -const propTypes = { - chat: React.PropTypes.shape({ - id: React.PropTypes.string.isRequired, - name: React.PropTypes.string.isRequired, - unreadCounter: React.PropTypes.number.isRequired, - }).isRequired, -}; - -const defaultProps = { -}; - -class ChatListItem extends Component { - render() { - const { - chat, - openChat, - compact, - intl, - } = this.props; - - const linkPath = [PRIVATE_CHAT_PATH, chat.id].join(''); - - let linkClasses = {}; - linkClasses[styles.active] = chat.id === openChat; - - if (chat.name === 'Public Chat') { - chat.name = intl.formatMessage(intlMessages.titlePublic); - } - - return ( -
  • - - {chat.icon ? this.renderChatIcon() : this.renderChatAvatar()} -
    - {!compact ?

    {chat.name}

    : null } -
    - {(chat.unreadCounter > 0) ? -
    -

    {chat.unreadCounter}

    -
    - : null} - -
  • - ); - } - - renderChatAvatar() { - return ; - } - - renderChatIcon() { - return ( -
    - -
    - ); - } -} - -ChatListItem.propTypes = propTypes; -ChatListItem.defaultProps = defaultProps; - -export default withRouter(injectIntl(ChatListItem)); +import React, { Component } from 'react'; +import UserAvatar from '/imports/ui/components/user-avatar/component'; +import Icon from '/imports/ui/components/icon/component'; +import styles from './styles.scss'; +import { withRouter } from 'react-router'; +import { Link } from 'react-router'; +import cx from 'classnames'; +import { defineMessages, injectIntl } from 'react-intl'; + +const intlMessages = defineMessages({ + titlePublic: { + id: 'app.chat.titlePublic', + defaultMessage: "Public Chat", + }, + unreadPlural: { + id: 'app.chatlistitem.unreadPlural', + }, + unreadSingular: { + id: 'app.chatlistitem.unreadSingular' + }, +}); + +const CHAT_CONFIG = Meteor.settings.public.chat; +const PRIVATE_CHAT_PATH = CHAT_CONFIG.path_route; + +const propTypes = { + chat: React.PropTypes.shape({ + id: React.PropTypes.string.isRequired, + name: React.PropTypes.string.isRequired, + unreadCounter: React.PropTypes.number.isRequired, + }).isRequired, +}; + +const defaultProps = { +}; + +class ChatListItem extends Component { + render() { + const { + chat, + openChat, + compact, + intl, + } = this.props; + + const linkPath = [PRIVATE_CHAT_PATH, chat.id].join(''); + + let linkClasses = {}; + linkClasses[styles.active] = chat.id === openChat; + + if (chat.name === 'Public Chat') { + chat.name = intl.formatMessage(intlMessages.titlePublic); + } + + return ( +
  • + + {chat.icon ? this.renderChatIcon() : this.renderChatAvatar()} +
    + {!compact ?

    {chat.name}

    : null } +
    + {(chat.unreadCounter > 0) ? +
    +
    + {chat.unreadCounter} + {(chat.unreadCounter == 1) ? +
    + :
    } +
    +
    + : null} + +
  • + ); + } + + renderChatAvatar() { + return ; + } + + renderChatIcon() { + return ( +
    + +
    + ); + } +} + +ChatListItem.propTypes = propTypes; +ChatListItem.defaultProps = defaultProps; + +export default withRouter(injectIntl(ChatListItem)); diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json index fc7a3c5ea3..f1b24df57e 100755 --- a/bigbluebutton-html5/private/locales/en.json +++ b/bigbluebutton-html5/private/locales/en.json @@ -12,6 +12,8 @@ "app.chat.titlePrivate": "Private Chat with {name}", "app.chat.partnerDisconnected": "{name} has left the meeting", "app.chat.moreMessages": "More messages below", + "app.chatlistitem.unreadSingular": "Unread Message", + "app.chatlistitem.unreadPlural": "Unread Messages", "app.presentation.presentationToolbar.prevSlideLabel": "Previous slide", "app.presentation.presentationToolbar.prevSlideDescrip": "Change the presentation to the previous slide", "app.presentation.presentationToolbar.nextSlideLabel": "Next slide", From 8dc0942ac782a139c5f1f815c73943a0f4ea7730 Mon Sep 17 00:00:00 2001 From: KDSBrowne Date: Thu, 30 Mar 2017 06:44:06 -0700 Subject: [PATCH 31/91] change line endings --- .../user-list/chat-list-item/component.jsx | 192 +++++++++--------- 1 file changed, 96 insertions(+), 96 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/component.jsx index f33092fe2e..3155a1e38b 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/component.jsx @@ -1,96 +1,96 @@ -import React, { Component } from 'react'; -import UserAvatar from '/imports/ui/components/user-avatar/component'; -import Icon from '/imports/ui/components/icon/component'; -import styles from './styles.scss'; -import { withRouter } from 'react-router'; -import { Link } from 'react-router'; -import cx from 'classnames'; -import { defineMessages, injectIntl } from 'react-intl'; - -const intlMessages = defineMessages({ - titlePublic: { - id: 'app.chat.titlePublic', - defaultMessage: "Public Chat", - }, - unreadPlural: { - id: 'app.chatlistitem.unreadPlural', - }, - unreadSingular: { - id: 'app.chatlistitem.unreadSingular' - }, -}); - -const CHAT_CONFIG = Meteor.settings.public.chat; -const PRIVATE_CHAT_PATH = CHAT_CONFIG.path_route; - -const propTypes = { - chat: React.PropTypes.shape({ - id: React.PropTypes.string.isRequired, - name: React.PropTypes.string.isRequired, - unreadCounter: React.PropTypes.number.isRequired, - }).isRequired, -}; - -const defaultProps = { -}; - -class ChatListItem extends Component { - render() { - const { - chat, - openChat, - compact, - intl, - } = this.props; - - const linkPath = [PRIVATE_CHAT_PATH, chat.id].join(''); - - let linkClasses = {}; - linkClasses[styles.active] = chat.id === openChat; - - if (chat.name === 'Public Chat') { - chat.name = intl.formatMessage(intlMessages.titlePublic); - } - - return ( -
  • - - {chat.icon ? this.renderChatIcon() : this.renderChatAvatar()} -
    - {!compact ?

    {chat.name}

    : null } -
    - {(chat.unreadCounter > 0) ? -
    -
    - {chat.unreadCounter} - {(chat.unreadCounter == 1) ? -
    - :
    } -
    -
    - : null} - -
  • - ); - } - - renderChatAvatar() { - return ; - } - - renderChatIcon() { - return ( -
    - -
    - ); - } -} - -ChatListItem.propTypes = propTypes; -ChatListItem.defaultProps = defaultProps; - -export default withRouter(injectIntl(ChatListItem)); +import React, { Component } from 'react'; +import UserAvatar from '/imports/ui/components/user-avatar/component'; +import Icon from '/imports/ui/components/icon/component'; +import styles from './styles.scss'; +import { withRouter } from 'react-router'; +import { Link } from 'react-router'; +import cx from 'classnames'; +import { defineMessages, injectIntl } from 'react-intl'; + +const intlMessages = defineMessages({ + titlePublic: { + id: 'app.chat.titlePublic', + defaultMessage: "Public Chat", + }, + unreadPlural: { + id: 'app.chatlistitem.unreadPlural', + }, + unreadSingular: { + id: 'app.chatlistitem.unreadSingular' + }, +}); + +const CHAT_CONFIG = Meteor.settings.public.chat; +const PRIVATE_CHAT_PATH = CHAT_CONFIG.path_route; + +const propTypes = { + chat: React.PropTypes.shape({ + id: React.PropTypes.string.isRequired, + name: React.PropTypes.string.isRequired, + unreadCounter: React.PropTypes.number.isRequired, + }).isRequired, +}; + +const defaultProps = { +}; + +class ChatListItem extends Component { + render() { + const { + chat, + openChat, + compact, + intl, + } = this.props; + + const linkPath = [PRIVATE_CHAT_PATH, chat.id].join(''); + + let linkClasses = {}; + linkClasses[styles.active] = chat.id === openChat; + + if (chat.name === 'Public Chat') { + chat.name = intl.formatMessage(intlMessages.titlePublic); + } + + return ( +
  • + + {chat.icon ? this.renderChatIcon() : this.renderChatAvatar()} +
    + {!compact ?

    {chat.name}

    : null } +
    + {(chat.unreadCounter > 0) ? +
    +
    + {chat.unreadCounter} + {(chat.unreadCounter == 1) ? +
    + :
    } +
    +
    + : null} + +
  • + ); + } + + renderChatAvatar() { + return ; + } + + renderChatIcon() { + return ( +
    + +
    + ); + } +} + +ChatListItem.propTypes = propTypes; +ChatListItem.defaultProps = defaultProps; + +export default withRouter(injectIntl(ChatListItem)); From 18c97fafe717d63d11a101f36f12c373585cf549 Mon Sep 17 00:00:00 2001 From: KDSBrowne Date: Thu, 30 Mar 2017 07:20:12 -0700 Subject: [PATCH 32/91] add aria-expanded to chat-list-item --- .../ui/components/user-list/chat-list-item/component.jsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/component.jsx index 3155a1e38b..1e240cc39e 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/component.jsx @@ -45,6 +45,7 @@ class ChatListItem extends Component { const linkPath = [PRIVATE_CHAT_PATH, chat.id].join(''); + let isExpanded; let linkClasses = {}; linkClasses[styles.active] = chat.id === openChat; @@ -54,10 +55,12 @@ class ChatListItem extends Component { return (
  • + {isExpanded = !openChat ? false : true} + role="button" + aria-expanded={isExpanded}> {chat.icon ? this.renderChatIcon() : this.renderChatAvatar()}
    {!compact ?

    {chat.name}

    : null } From 317e474d77a11763f4804ecea7a7bc1a7ce57f5e Mon Sep 17 00:00:00 2001 From: KDSBrowne Date: Thu, 30 Mar 2017 07:45:56 -0700 Subject: [PATCH 33/91] fix lint issues --- .../user-list/chat-list-item/component.jsx | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/component.jsx index 1e240cc39e..41b10c99c7 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/component.jsx @@ -10,14 +10,13 @@ import { defineMessages, injectIntl } from 'react-intl'; const intlMessages = defineMessages({ titlePublic: { id: 'app.chat.titlePublic', - defaultMessage: "Public Chat", }, unreadPlural: { - id: 'app.chatlistitem.unreadPlural', - }, - unreadSingular: { - id: 'app.chatlistitem.unreadSingular' - }, + id: 'app.chatlistitem.unreadPlural', + }, + unreadSingular: { + id: 'app.chatlistitem.unreadSingular', + }, }); const CHAT_CONFIG = Meteor.settings.public.chat; @@ -70,8 +69,12 @@ class ChatListItem extends Component {
    {chat.unreadCounter} {(chat.unreadCounter == 1) ? -
    - :
    } +
    + :
    }
    : null} From 8ec0c6bb04fcd7b8b8806b686bda0e6ed05311a9 Mon Sep 17 00:00:00 2001 From: KDSBrowne Date: Thu, 30 Mar 2017 10:25:29 -0700 Subject: [PATCH 34/91] change formatted msg and remove duplicate div --- .../user-list/chat-list-item/component.jsx | 22 +++++++++---------- bigbluebutton-html5/private/locales/en.json | 4 ++-- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/component.jsx index 41b10c99c7..2c53a09774 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/component.jsx @@ -43,10 +43,10 @@ class ChatListItem extends Component { } = this.props; const linkPath = [PRIVATE_CHAT_PATH, chat.id].join(''); + const isCurrentChat = chat.id === openChat; - let isExpanded; let linkClasses = {}; - linkClasses[styles.active] = chat.id === openChat; + linkClasses[styles.active] = isCurrentChat; if (chat.name === 'Public Chat') { chat.name = intl.formatMessage(intlMessages.titlePublic); @@ -54,27 +54,25 @@ class ChatListItem extends Component { return (
  • - {isExpanded = !openChat ? false : true} + aria-expanded={isCurrentChat}> {chat.icon ? this.renderChatIcon() : this.renderChatAvatar()}
    {!compact ?

    {chat.name}

    : null }
    {(chat.unreadCounter > 0) ?
    -
    +
    {chat.unreadCounter} - {(chat.unreadCounter == 1) ? -
    - :
    } +
    : null} diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json index f1b24df57e..b6940e2906 100755 --- a/bigbluebutton-html5/private/locales/en.json +++ b/bigbluebutton-html5/private/locales/en.json @@ -12,8 +12,8 @@ "app.chat.titlePrivate": "Private Chat with {name}", "app.chat.partnerDisconnected": "{name} has left the meeting", "app.chat.moreMessages": "More messages below", - "app.chatlistitem.unreadSingular": "Unread Message", - "app.chatlistitem.unreadPlural": "Unread Messages", + "app.chatlistitem.unreadSingular": "New Message", + "app.chatlistitem.unreadPlural": "New Messages", "app.presentation.presentationToolbar.prevSlideLabel": "Previous slide", "app.presentation.presentationToolbar.prevSlideDescrip": "Change the presentation to the previous slide", "app.presentation.presentationToolbar.nextSlideLabel": "Next slide", From c99de6c4db35bc5348405024027edc032e741727 Mon Sep 17 00:00:00 2001 From: Richard Alam Date: Fri, 31 Mar 2017 19:54:51 +0000 Subject: [PATCH 35/91] - display length of recording in minutes instead of milliseconds --- .../api/domain/RecordingMetadata.java | 9 +++++++ .../api/domain/RecordingMetadataPlayback.java | 9 +++++++ .../src/test/resources/include-recording.ftlx | 6 ++++- .../WEB-INF/freemarker/include-recording.ftlx | 27 +++++++++++-------- 4 files changed, 39 insertions(+), 12 deletions(-) diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/RecordingMetadata.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/RecordingMetadata.java index 72eb72deee..74972db585 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/RecordingMetadata.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/RecordingMetadata.java @@ -195,4 +195,13 @@ public class RecordingMetadata { public Boolean hasError() { return processingError; } + + public Integer calculateDuration() { + if ((endTime == null) || (endTime == "") || (startTime == null) || (startTime == "")) return 0; + + int start = (int) Math.ceil((Long.parseLong(startTime)) / 60000.0); + int end = (int) Math.ceil((Long.parseLong(endTime)) / 60000.0); + + return end - start; + } } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/RecordingMetadataPlayback.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/RecordingMetadataPlayback.java index dde38a006b..765f196a26 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/RecordingMetadataPlayback.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/RecordingMetadataPlayback.java @@ -47,6 +47,15 @@ public class RecordingMetadataPlayback { return duration; } + public Long calculateDuration() { + if (duration > 0) { + // convert to minutes + return duration / 60000; + } else { + return 0L; + } + } + public void setExtensions(Extensions extensions) { this.extensions = extensions; } diff --git a/bbb-common-web/src/test/resources/include-recording.ftlx b/bbb-common-web/src/test/resources/include-recording.ftlx index d57f74fb11..4bfdbcd1e8 100755 --- a/bbb-common-web/src/test/resources/include-recording.ftlx +++ b/bbb-common-web/src/test/resources/include-recording.ftlx @@ -48,7 +48,11 @@ ${pb.getFormat()} ${pb.getLink()} ${pb.getProcessingTime()?c} - ${pb.getDuration()?c} + <#if pb.getDuration() == 0> + ${r.calculateDuration()?c} + <#else> + ${pb.calculateDuration()?c} + <#if pb.getExtensions()??> <#if pb.getExtensions().getPreview()??> diff --git a/bigbluebutton-web/web-app/WEB-INF/freemarker/include-recording.ftlx b/bigbluebutton-web/web-app/WEB-INF/freemarker/include-recording.ftlx index c6648a4ffb..6f0c23c217 100755 --- a/bigbluebutton-web/web-app/WEB-INF/freemarker/include-recording.ftlx +++ b/bigbluebutton-web/web-app/WEB-INF/freemarker/include-recording.ftlx @@ -48,7 +48,12 @@ ${pb.getFormat()} ${pb.getLink()} ${pb.getProcessingTime()?c} - ${pb.getDuration()?c} + + <#if pb.getDuration() == 0> + ${r.calculateDuration()?c} + <#else> + ${pb.calculateDuration()?c} + <#if pb.getExtensions()??> <#if pb.getExtensions().getPreview()??> @@ -59,14 +64,14 @@ <#items as image> <#if image??> - ${image.getAlt()?html}${image.getValue()!"Link not found."} + ${image.getAlt()?html}${image.getValue()!"Link not found."} + + + + - - - - - - - - - + + + + + From 90695dd5d78e24df62bca41eebd75f3726359850 Mon Sep 17 00:00:00 2001 From: KDSBrowne Date: Mon, 3 Apr 2017 10:19:21 -0700 Subject: [PATCH 36/91] make NVDA ignore Avatar text --- .../imports/ui/components/user-avatar/component.jsx | 6 ++---- .../components/user-list/user-list-item/component.jsx | 10 ++++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/user-avatar/component.jsx b/bigbluebutton-html5/imports/ui/components/user-avatar/component.jsx index d1cea8d785..b51790089a 100755 --- a/bigbluebutton-html5/imports/ui/components/user-avatar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-avatar/component.jsx @@ -32,10 +32,8 @@ export default class UserAvatar extends Component { return (
    - - {this.renderAvatarContent()} - + style={avatarStyles} aria-hidden="true"> + {this.renderAvatarContent()} {this.renderUserStatus()} {this.renderUserMediaStatus()}
    diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-item/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-item/component.jsx index 0eb6a5375b..81c56ac59d 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-item/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-item/component.jsx @@ -166,6 +166,8 @@ class UserListItem extends Component { return (
  • {this.renderUserContents()}
  • @@ -249,12 +251,12 @@ class UserListItem extends Component { return (
    -

    + {user.name} -

    -

    + + {userNameSub} -

    +
    ); } From 940905e4b70c0c1721c6b7b025462b0311077a32 Mon Sep 17 00:00:00 2001 From: KDSBrowne Date: Mon, 3 Apr 2017 10:34:37 -0700 Subject: [PATCH 37/91] add aria-label to status btn --- .../actions-bar/emoji-menu/component.jsx | 25 +++---------------- bigbluebutton-html5/private/locales/en.json | 1 + 2 files changed, 5 insertions(+), 21 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/component.jsx index 3af018f7cf..62ea927ed6 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/component.jsx @@ -33,6 +33,7 @@ class EmojiMenu extends Component {
  • - ); } From 7e4bf6058815529cf8f67a4bcbedd1c2a8a5717d Mon Sep 17 00:00:00 2001 From: KDSBrowne Date: Wed, 5 Apr 2017 15:49:05 -0700 Subject: [PATCH 56/91] remove FormattedMessage import --- .../imports/ui/components/user-list/component.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bigbluebutton-html5/imports/ui/components/user-list/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/component.jsx index 86059a3ac3..4a44136b1d 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/component.jsx @@ -2,7 +2,6 @@ import React, { Component, PropTypes } from 'react'; import { withRouter } from 'react-router'; import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; import styles from './styles.scss'; -import { FormattedMessage } from 'react-intl'; import cx from 'classnames'; import { defineMessages, injectIntl } from 'react-intl'; import UserListItem from './user-list-item/component.jsx'; @@ -154,12 +153,15 @@ class UserList extends Component { const intlMessages = defineMessages({ usersTitle: { id: 'app.userlist.usersTitle', + description: 'Title for the Header', }, messagesTitle: { id: 'app.userlist.messagesTitle', + description: 'Title for the messages list', }, participantsTitle: { id: 'app.userlist.participantsTitle', + description: 'Title for the Users list', }, }); From 800c47f462d184d8344a49bc6ffcad61bd4b3a14 Mon Sep 17 00:00:00 2001 From: KDSBrowne Date: Wed, 5 Apr 2017 17:02:07 -0700 Subject: [PATCH 57/91] remove default messages --- .../imports/ui/components/about/component.jsx | 6 ------ .../components/actions-bar/audio-menu/component.jsx | 2 -- .../breakout-join-confirmation/component.jsx | 6 ------ .../ui/components/chat/message-form/component.jsx | 3 --- .../ui/components/chat/message-list/component.jsx | 1 - .../imports/ui/components/dropdown/component.jsx | 1 - .../imports/ui/components/error-screen/component.jsx | 4 ---- .../ui/components/logout-confirmation/component.jsx | 6 ------ .../nav-bar/settings-dropdown/component.jsx | 11 ----------- .../components/user-list/user-list-item/component.jsx | 2 -- 10 files changed, 42 deletions(-) mode change 100644 => 100755 bigbluebutton-html5/imports/ui/components/about/component.jsx mode change 100644 => 100755 bigbluebutton-html5/imports/ui/components/breakout-join-confirmation/component.jsx mode change 100644 => 100755 bigbluebutton-html5/imports/ui/components/error-screen/component.jsx diff --git a/bigbluebutton-html5/imports/ui/components/about/component.jsx b/bigbluebutton-html5/imports/ui/components/about/component.jsx old mode 100644 new mode 100755 index 02d7944750..4b72d9a9d8 --- a/bigbluebutton-html5/imports/ui/components/about/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/about/component.jsx @@ -5,11 +5,9 @@ import Modal from '/imports/ui/components/modal/component'; const intlMessages = defineMessages({ title: { id: 'app.about.title', - defaultMessage: 'About', }, version: { id: 'app.about.version', - defaultMessage: 'Client Build:', }, copyright: { id: 'app.about.copyright', @@ -17,19 +15,15 @@ const intlMessages = defineMessages({ }, confirmLabel: { id: 'app.about.confirmLabel', - defaultMessage: 'OK', }, confirmDesc: { id: 'app.about.confirmDesc', - defaultMessage: 'OK', }, dismissLabel: { id: 'app.about.dismissLabel', - defaultMessage: 'Cancel', }, dismissDesc: { id: 'app.about.dismissDesc', - defaultMessage: 'Close about client information', }, }); diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/audio-menu/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/audio-menu/component.jsx index 963d3ff003..c4c3f2d77c 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/audio-menu/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/audio-menu/component.jsx @@ -7,11 +7,9 @@ import { defineMessages, injectIntl } from 'react-intl'; const intlMessages = defineMessages({ joinAudio: { id: 'app.audio.joinAudio', - defaultMessage: 'Join Audio', }, leaveAudio: { id: 'app.audio.leaveAudio', - defaultMessage: 'Leave Audio', } }); diff --git a/bigbluebutton-html5/imports/ui/components/breakout-join-confirmation/component.jsx b/bigbluebutton-html5/imports/ui/components/breakout-join-confirmation/component.jsx old mode 100644 new mode 100755 index e23f29fd11..edd4065ee9 --- a/bigbluebutton-html5/imports/ui/components/breakout-join-confirmation/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/breakout-join-confirmation/component.jsx @@ -7,27 +7,21 @@ import Modal from '/imports/ui/components/modal/component'; const intlMessages = defineMessages({ title: { id: 'app.breakoutJoinConfirmation.title', - defaultMessage: 'Join Breakout Room', }, message: { id: 'app.breakoutJoinConfirmation.message', - defaultMessage: 'Do you want to join', }, confirmLabel: { id: 'app.breakoutJoinConfirmation.confirmLabel', - defaultMessage: 'Join', }, confirmDesc: { id: 'app.breakoutJoinConfirmation.confirmDesc', - defaultMessage: 'Join you to the Breakout Room', }, dismissLabel: { id: 'app.breakoutJoinConfirmation.dismissLabel', - defaultMessage: 'Cancel', }, dismissDesc: { id: 'app.breakoutJoinConfirmation.dismissDesc', - defaultMessage: 'Closes and rejects Joining the Breakout Room', }, }); diff --git a/bigbluebutton-html5/imports/ui/components/chat/message-form/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/message-form/component.jsx index 73fa059d8c..448c293d62 100755 --- a/bigbluebutton-html5/imports/ui/components/chat/message-form/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/message-form/component.jsx @@ -16,17 +16,14 @@ const defaultProps = { const messages = defineMessages({ submitLabel: { id: 'app.chat.submitLabel', - defaultMessage: 'Send Message', description: 'Chat submit button label', }, inputLabel: { id: 'app.chat.inputLabel', - defaultMessage: 'Message input for chat {name}', description: 'Chat message input label', }, inputPlaceholder: { id: 'app.chat.inputPlaceholder', - defaultMessage: 'Message {name}', description: 'Chat message input placeholder', }, }); diff --git a/bigbluebutton-html5/imports/ui/components/chat/message-list/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/message-list/component.jsx index 5042a0b2e6..8fab674bf9 100755 --- a/bigbluebutton-html5/imports/ui/components/chat/message-list/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/message-list/component.jsx @@ -14,7 +14,6 @@ const propTypes = { const intlMessages = defineMessages({ moreMessages: { id: 'app.chat.moreMessages', - defaultMessage: 'More messages below', description: 'Chat message when the user has unread messages below the scroll', }, }); diff --git a/bigbluebutton-html5/imports/ui/components/dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/dropdown/component.jsx index 6cab4489c3..a72ce7f6a6 100755 --- a/bigbluebutton-html5/imports/ui/components/dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/dropdown/component.jsx @@ -12,7 +12,6 @@ const FOCUSABLE_CHILDREN = `[tabindex]:not([tabindex="-1"]), a, input, button`; const intlMessages = defineMessages({ close: { id: 'app.dropdown.close', - defaultMessage: 'Close', }, }); diff --git a/bigbluebutton-html5/imports/ui/components/error-screen/component.jsx b/bigbluebutton-html5/imports/ui/components/error-screen/component.jsx old mode 100644 new mode 100755 index 2be616666a..85d5decc1f --- a/bigbluebutton-html5/imports/ui/components/error-screen/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/error-screen/component.jsx @@ -6,19 +6,15 @@ import styles from './styles.scss'; const intlMessages = defineMessages({ 500: { id: 'app.error.500', - defaultMessage: 'Ops, something went wrong', }, 404: { id: 'app.error.404', - defaultMessage: 'Not Found', }, 401: { id: 'app.about.401', - defaultMessage: 'Unauthorized', }, 403: { id: 'app.about.403', - defaultMessage: 'Forbidden', }, }); diff --git a/bigbluebutton-html5/imports/ui/components/logout-confirmation/component.jsx b/bigbluebutton-html5/imports/ui/components/logout-confirmation/component.jsx index 7bcfd43d4a..5f6533649c 100755 --- a/bigbluebutton-html5/imports/ui/components/logout-confirmation/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/logout-confirmation/component.jsx @@ -6,27 +6,21 @@ import Modal from '/imports/ui/components/modal/component'; const intlMessages = defineMessages({ title: { id: 'app.leaveConfirmation.title', - defaultMessage: 'Leave Session', }, message: { id: 'app.leaveConfirmation.message', - defaultMessage: 'Do you want to leave this meeting?', }, confirmLabel: { id: 'app.leaveConfirmation.confirmLabel', - defaultMessage: 'Leave', }, confirmDesc: { id: 'app.leaveConfirmation.confirmDesc', - defaultMessage: 'Logs you out of the meeting', }, dismissLabel: { id: 'app.leaveConfirmation.dismissLabel', - defaultMessage: 'Cancel', }, dismissDesc: { id: 'app.leaveConfirmation.dismissDesc', - defaultMessage: 'Closes and rejects the leave confirmation', }, }); diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/component.jsx index af0dd45102..a32cef655b 100755 --- a/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/component.jsx @@ -19,47 +19,36 @@ import DropdownListSeparator from '/imports/ui/components/dropdown/list/separato const intlMessages = defineMessages({ optionsLabel: { id: 'app.navBar.settingsDropdown.optionsLabel', - defaultMessage: 'Options', }, fullscreenLabel: { id: 'app.navBar.settingsDropdown.fullscreenLabel', - defaultMessage: 'Make fullscreen', }, settingsLabel: { id: 'app.navBar.settingsDropdown.settingsLabel', - defaultMessage: 'Open settings', }, aboutLabel: { id: 'app.navBar.settingsDropdown.aboutLabel', - defaultMessage: 'About', }, aboutDesc: { id: 'app.navBar.settingsDropdown.aboutDesc', - defaultMessage: 'About', }, leaveSessionLabel: { id: 'app.navBar.settingsDropdown.leaveSessionLabel', - defaultMessage: 'Logout', }, fullscreenDesc: { id: 'app.navBar.settingsDropdown.fullscreenDesc', - defaultMessage: 'Make the settings menu fullscreen', }, settingsDesc: { id: 'app.navBar.settingsDropdown.settingsDesc', - defaultMessage: 'Change the general settings', }, leaveSessionDesc: { id: 'app.navBar.settingsDropdown.leaveSessionDesc', - defaultMessage: 'Leave the meeting', }, exitFullScreenDesc: { id: 'app.navBar.settingsDropdown.exitFullScreenDesc', - defaultMessage: 'exit fullscreen mode', }, exitFullScreenLabel: { id: 'app.navBar.settingsDropdown.exitFullScreenLabel', - defaultMessage: 'Exit fullscreen', }, }); diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-item/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-item/component.jsx index 32fd59d711..a362af6c2c 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-item/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-item/component.jsx @@ -40,12 +40,10 @@ const messages = defineMessages({ presenter: { id: 'app.userlist.presenter', description: 'Text for identifying presenter user', - defaultMessage: 'Presenter', }, you: { id: 'app.userlist.you', description: 'Text for identifying your user', - defaultMessage: 'You', }, }); From 3ea21df973cac58718944441a02af242051515f2 Mon Sep 17 00:00:00 2001 From: gcampes Date: Thu, 6 Apr 2017 08:46:15 -0300 Subject: [PATCH 58/91] Fix closed captions settings issue --- bigbluebutton-html5/imports/ui/components/app/service.js | 8 ++++---- .../imports/ui/components/closed-captions/service.js | 6 +++--- .../settings/submenus/closed-captions/component.jsx | 6 +++--- bigbluebutton-html5/private/config/public/app.yaml | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/app/service.js b/bigbluebutton-html5/imports/ui/components/app/service.js index e6c7c983cc..9bf3bc5952 100755 --- a/bigbluebutton-html5/imports/ui/components/app/service.js +++ b/bigbluebutton-html5/imports/ui/components/app/service.js @@ -23,13 +23,13 @@ const clearModal = () => { }; const getCaptionsStatus = () => { - const settings = Settings.cc; - return settings ? settings.cc : false; + const ccSettings = Settings.cc; + return ccSettings ? ccSettings.enabled : false; }; const getFontSize = () => { - const settings = Settings.application; - return settings ? settings.fontSize : '16px'; + const applicationSettings = Settings.application; + return applicationSettings ? applicationSettings.fontSize : '16px'; }; function meetingIsBreakout() { diff --git a/bigbluebutton-html5/imports/ui/components/closed-captions/service.js b/bigbluebutton-html5/imports/ui/components/closed-captions/service.js index 2b3c443704..b4fc7e3dcc 100755 --- a/bigbluebutton-html5/imports/ui/components/closed-captions/service.js +++ b/bigbluebutton-html5/imports/ui/components/closed-captions/service.js @@ -1,13 +1,13 @@ import Captions from '/imports/api/captions'; import Auth from '/imports/ui/services/auth'; -import Storage from '/imports/ui/services/storage/session'; +import Settings from '/imports/ui/services/settings'; let getCCData = () => { const meetingID = Auth.meetingID; - const ccSettings = Storage.getItem('settings_cc'); + const ccSettings = Settings.cc; - let CCEnabled = ccSettings.closedCaptions; + let CCEnabled = ccSettings.enabled; //associative array that keeps locales with arrays of string objects related to those locales let captions = []; diff --git a/bigbluebutton-html5/imports/ui/components/settings/submenus/closed-captions/component.jsx b/bigbluebutton-html5/imports/ui/components/settings/submenus/closed-captions/component.jsx index 01675fbbf3..dfca166a71 100644 --- a/bigbluebutton-html5/imports/ui/components/settings/submenus/closed-captions/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/settings/submenus/closed-captions/component.jsx @@ -28,7 +28,7 @@ export default class ClosedCaptionsMenu extends BaseMenu { settings: { backgroundColor: props.settings ? props.settings.backgroundColor : '#f3f6f9', fontColor: props.settings ? props.settings.fontColor : '#000000', - closedCaptions: props.settings ? props.settings.closedCaptions : false, + enabled: props.settings ? props.settings.enabled : false, fontFamily: props.settings ? props.settings.fontFamily : 'Calibri', fontSize: props.settings ? props.settings.fontSize : -1, locale: props.settings ? props.settings.locale : -1, @@ -99,8 +99,8 @@ export default class ClosedCaptionsMenu extends BaseMenu {
    this.handleToggle('closedCaptions')} /> + defaultChecked={this.state.settings.enabled} + onChange={() => this.handleToggle('enabled')} />
    diff --git a/bigbluebutton-html5/private/config/public/app.yaml b/bigbluebutton-html5/private/config/public/app.yaml index 4bd5c47d79..cbb670a9b5 100755 --- a/bigbluebutton-html5/private/config/public/app.yaml +++ b/bigbluebutton-html5/private/config/public/app.yaml @@ -39,10 +39,10 @@ app: cc: backgroundColor: "#FFFFFF" fontColor: "#000000" - closedCaptions: false + enabled: false fontFamily: "Calibri" fontSize: '16px' - locale: undefined + # locale: undefined takeOwnership: false participants: muteAll: false From b87ba1f3ea4ceb360175be20b3981bacc9697fc8 Mon Sep 17 00:00:00 2001 From: Anton Georgiev Date: Thu, 6 Apr 2017 10:42:01 -0400 Subject: [PATCH 59/91] adjust emoji statuses and icon names --- .../ui/components/user-avatar/component.jsx | 23 ++++++++++++++++++- bigbluebutton-html5/imports/utils/statuses.js | 4 ++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/user-avatar/component.jsx b/bigbluebutton-html5/imports/ui/components/user-avatar/component.jsx index d1cea8d785..512333a37c 100755 --- a/bigbluebutton-html5/imports/ui/components/user-avatar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-avatar/component.jsx @@ -48,7 +48,28 @@ export default class UserAvatar extends Component { let content = user.name.slice(0, 2); if (user.emoji.status !== 'none') { - content = ; + let iconEmoji = undefined; + + switch (user.emoji.status) { + case 'thumbsUp': + iconEmoji = 'thumbs_up'; + break; + case 'thumbsDown': + iconEmoji = 'thumbs_down'; + break; + case 'raiseHand': + iconEmoji = 'hand'; + break; + case 'away': + iconEmoji = 'time'; + break; + case 'neutral': + iconEmoji = 'undecided'; + break; + default: + iconEmoji = user.emoji.status; + } + content = ; } return content; diff --git a/bigbluebutton-html5/imports/utils/statuses.js b/bigbluebutton-html5/imports/utils/statuses.js index ab074110dc..a62ca1f0ee 100755 --- a/bigbluebutton-html5/imports/utils/statuses.js +++ b/bigbluebutton-html5/imports/utils/statuses.js @@ -1,4 +1,4 @@ -const EMOJI_STATUSES = ['time', 'hand', 'undecided', 'confused', 'sad', - 'happy', 'applause', 'thumbs_up', 'thumbs_down']; +const EMOJI_STATUSES = ['away', 'raiseHand', 'neutral', 'confused', 'sad', + 'happy', 'applause', 'thumbsUp', 'thumbsDown']; export { EMOJI_STATUSES }; From 8469691256a56cc9dc60376849350dfc6028422d Mon Sep 17 00:00:00 2001 From: KDSBrowne Date: Thu, 6 Apr 2017 08:13:28 -0700 Subject: [PATCH 60/91] fix slide control and actionbar btn overlap --- .../imports/ui/components/app/styles.scss | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/app/styles.scss b/bigbluebutton-html5/imports/ui/components/app/styles.scss index 5361f93962..25994873cc 100755 --- a/bigbluebutton-html5/imports/ui/components/app/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/app/styles.scss @@ -51,8 +51,6 @@ $bars-padding: $lg-padding-x - .45rem; // -.45 so user-list and chat title is al } } - - .content { @extend %full-page; @@ -149,11 +147,13 @@ $bars-padding: $lg-padding-x - .45rem; // -.45 so user-list and chat title is al .media { @extend %full-page; - flex: 1; - order: 1; + flex: 1 100%; + order: 2; @include mq($small-only) { padding-bottom: $actionsbar-height; + margin-bottom: $actionsbar-height; + position: absolute; } } @@ -164,6 +164,7 @@ $bars-padding: $lg-padding-x - .45rem; // -.45 so user-list and chat title is al } .actionsbar { + flex: 1; padding: $bars-padding; position: relative; order: 3; From 82a301817161526f557f89df0b9a9234aeeaec6b Mon Sep 17 00:00:00 2001 From: Gabriel Carvalho de Campes Date: Thu, 6 Apr 2017 14:06:03 -0300 Subject: [PATCH 61/91] [HTML5] - Fix user not being removed on tab close --- .../imports/api/users/server/methods/userLeaving.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bigbluebutton-html5/imports/api/users/server/methods/userLeaving.js b/bigbluebutton-html5/imports/api/users/server/methods/userLeaving.js index 8e4144e346..a9b02dc837 100755 --- a/bigbluebutton-html5/imports/api/users/server/methods/userLeaving.js +++ b/bigbluebutton-html5/imports/api/users/server/methods/userLeaving.js @@ -57,7 +57,7 @@ export default function userLeaving(credentials, userId) { } }; - return Users.update(selector, modifier, cb); + Users.update(selector, modifier, cb); } let payload = { From 0a1667f2933b6fac9e5d2e754af8570f6fe29a60 Mon Sep 17 00:00:00 2001 From: Calvin Walton Date: Thu, 6 Apr 2017 14:33:38 -0400 Subject: [PATCH 62/91] recording presentation: Correctly calculate width of bar in poll results The code was previously dividing by the total number of votes rather than the max number of votes, which meant it was underestimating the width of the bars. In some cases, this meant that the label for number of votes would overlap the percentages. Fixes #3725 --- .../presentation/scripts/publish/presentation.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/record-and-playback/presentation/scripts/publish/presentation.rb b/record-and-playback/presentation/scripts/publish/presentation.rb index 3335020ae0..906d225c69 100755 --- a/record-and-playback/presentation/scripts/publish/presentation.rb +++ b/record-and-playback/presentation/scripts/publish/presentation.rb @@ -466,6 +466,7 @@ def storePollResultShape(xml, shape) result = JSON.load(shape.at_xpath('result').text) num_responders = shape.at_xpath('num_responders').text.to_i presentation = shape.at_xpath('presentation').text + max_num_votes = result.map{ |r| r['num_votes'] }.max $global_shape_count += 1 $poll_result_count += 1 @@ -503,7 +504,7 @@ def storePollResultShape(xml, shape) end g.puts('set linetype 1 linewidth 1 linecolor rgb "black"') result.each do |r| - if r['num_votes'] == 0 or r['num_votes'].to_f / num_responders <= 0.5 + if r['num_votes'] == 0 or r['num_votes'].to_f / max_num_votes <= 0.5 g.puts("set label \"#{r['num_votes']}\" at #{r['id']},#{r['num_votes']} left rotate by 90 offset 0,character 0.5 front") else g.puts("set label \"#{r['num_votes']}\" at #{r['id']},#{r['num_votes']} right rotate by 90 offset 0,character -0.5 textcolor rgb \"white\" front") From 1c22f38adddd0f77847f6ce22edf2534f245ab51 Mon Sep 17 00:00:00 2001 From: Klaus Klein Date: Thu, 6 Apr 2017 17:41:08 -0300 Subject: [PATCH 63/91] Fix user drop down --- .../user-list/user-list-item/component.jsx | 68 +++++++++++++++---- 1 file changed, 54 insertions(+), 14 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-item/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-item/component.jsx index 0eb6a5375b..d8810ba01b 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-item/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-item/component.jsx @@ -83,6 +83,9 @@ class UserListItem extends Component { this.state = { isActionsOpen: false, + dropdownOffset: 0, + dropdownDirection: 'top', + dropdownVisible: false, }; this.handleScroll = this.handleScroll.bind(this); @@ -135,12 +138,46 @@ class UserListItem extends Component { ]); } + componentDidUpdate(prevProps, prevState) { + const { isActionsOpen, dropdownVisible } = this.state; + + console.log('CARAIO', isActionsOpen, dropdownVisible); + if(isActionsOpen && !dropdownVisible) { + const dropdown = findDOMNode(this.refs.dropdown); + const dropdownTrigger = dropdown.children[0]; + const dropdownContent = dropdown.children[1]; + + const scrollContainer = dropdown.parentElement.parentElement; + + let nextState = { + dropdownVisible: true, + }; + + const isDropdownVisible = dropdownContent.offsetTop + dropdownContent.offsetHeight + scrollContainer.scrollTop< window.innerHeight; + + //console.log('isVisible', dropdownContent.offsetTop, dropdownContent.offsetHeight, window.innerHeight); + + if (!isDropdownVisible) { + //console.log('EU DEVERIA SER BOTTOM'); + nextState.dropdownOffset = window.innerHeight - (dropdownTrigger.offsetTop + dropdownTrigger.offsetHeight - scrollContainer.scrollTop); + nextState.dropdownDirection = 'bottom'; + } + + this.setState(nextState); + //console.log(nextState) + } + } + onActionsShow() { const dropdown = findDOMNode(this.refs.dropdown); + const scrollContainer = dropdown.parentElement.parentElement; + const dropdownTrigger = dropdown.children[0]; + this.setState({ - contentTop: `${dropdown.offsetTop - dropdown.parentElement.parentElement.scrollTop}px`, isActionsOpen: true, - active: true, + dropdownVisible: false, + dropdownOffset: dropdownTrigger.offsetTop - scrollContainer.scrollTop, + dropdownDirection: 'top', }); findDOMNode(this).parentElement.addEventListener('scroll', this.handleScroll, false); @@ -148,8 +185,8 @@ class UserListItem extends Component { onActionsHide() { this.setState({ - active: false, isActionsOpen: false, + dropdownVisible: false, }); findDOMNode(this).parentElement.removeEventListener('scroll', this.handleScroll, false); @@ -162,7 +199,7 @@ class UserListItem extends Component { let userItemContentsStyle = {}; userItemContentsStyle[styles.userItemContentsCompact] = compact; - userItemContentsStyle[styles.active] = this.state.active; + userItemContentsStyle[styles.active] = this.state.isActionsOpen; return (
  • - + {this.renderUserName()} {this.renderUserIcons()} @@ -190,10 +227,12 @@ class UserListItem extends Component { return contents; } + const { dropdownOffset, dropdownDirection, dropdownVisible, } = this.state; + return ( @@ -202,10 +241,11 @@ class UserListItem extends Component { + placement={`right ${dropdownDirection}`}> { @@ -215,7 +255,7 @@ class UserListItem extends Component { key={_.uniqueId('action-header')} label={user.name} style={{ fontWeight: 600 }} - defaultMessage={user.name}/>), + defaultMessage={user.name} />), (), ].concat(actions) } @@ -294,16 +334,16 @@ class UserListItem extends Component { { user.isSharingWebcam ? - + : null } { audioChatIcon ? - - - - : null + + + + : null } ); From e3f66da499a41b5412f8890e753c63fdfa946fe8 Mon Sep 17 00:00:00 2001 From: Anton Georgiev Date: Thu, 6 Apr 2017 17:08:20 -0400 Subject: [PATCH 64/91] update chat unless user just left --- .../imports/ui/components/chat/component.jsx | 2 ++ .../imports/ui/components/chat/container.jsx | 6 +++- .../chat/message-list/component.jsx | 28 +++++++++++++------ 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/chat/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/component.jsx index ca1e0d85d1..72efd5a92a 100755 --- a/bigbluebutton-html5/imports/ui/components/chat/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/component.jsx @@ -28,6 +28,7 @@ class Chat extends Component { scrollPosition, hasUnreadMessages, lastReadMessageTime, + partnerIsLoggedOut, isChatLocked, actions, intl, @@ -65,6 +66,7 @@ class Chat extends Component { handleScrollUpdate={actions.handleScrollUpdate} handleReadMessage={actions.handleReadMessage} lastReadMessageTime={lastReadMessageTime} + partnerIsLoggedOut={partnerIsLoggedOut} /> { messages = ChatService.getPrivateMessages(chatID); } + let user = ChatService.getUser(chatID, '{{NAME}}'); + const partnerIsLoggedOut = user.isLoggedOut; + if (messages && chatID !== PUBLIC_CHAT_KEY) { let userMessage = messages.find(m => m.sender !== null); let user = ChatService.getUser(chatID, '{{NAME}}'); @@ -62,7 +65,7 @@ export default injectIntl(createContainer(({ params, intl }) => { title = intl.formatMessage(intlMessages.titlePrivate, { name: user.name }); chatName = user.name; - if (user.isLoggedOut) { + if (partnerIsLoggedOut) { let time = Date.now(); let id = `partner-disconnected-${time}`; let messagePartnerLoggedOut = { @@ -92,6 +95,7 @@ export default injectIntl(createContainer(({ params, intl }) => { messages, lastReadMessageTime, hasUnreadMessages, + partnerIsLoggedOut, isChatLocked, scrollPosition, actions: { diff --git a/bigbluebutton-html5/imports/ui/components/chat/message-list/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/message-list/component.jsx index 5042a0b2e6..adf9d0e1c4 100755 --- a/bigbluebutton-html5/imports/ui/components/chat/message-list/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/message-list/component.jsx @@ -1,4 +1,3 @@ - import React, { Component, PropTypes } from 'react'; import { defineMessages, injectIntl } from 'react-intl'; import _ from 'lodash'; @@ -112,14 +111,27 @@ class MessageList extends Component { } shouldComponentUpdate(nextProps) { - if (this.props.chatId !== nextProps.chatId - || this.props.hasUnreadMessages !== nextProps.hasUnreadMessages - || this.props.messages.length !== nextProps.messages.length - || !_.isEqual(this.props.messages, nextProps.messages)) { - return true; - } + const { + chatId, + hasUnreadMessages, + partnerIsLoggedOut, + } = this.props; - return false; + const switchingCorrespondent = chatId !== nextProps.chatId; + const hasNewUnreadMessages = hasUnreadMessages !== nextProps.hasUnreadMessages; + + // console.log('switchingCorrespondent=' + switchingCorrespondent); + // console.log('hasNewUnreadMessages=' + hasNewUnreadMessages); + + // check if the messages include + const lastMessageId = nextProps.messages[nextProps.messages.length - 1].id; + const userLeftIsDisplayed = lastMessageId.includes('partner-disconnected'); + + if (switchingCorrespondent || hasNewUnreadMessages) return true; + + if (partnerIsLoggedOut && userLeftIsDisplayed) return false; // update leads to endless loop + + return true; } render() { From e77b50d2c6c6d8c9aa6b055d805301f5f1c8630b Mon Sep 17 00:00:00 2001 From: Calvin Walton Date: Thu, 6 Apr 2017 18:04:10 -0400 Subject: [PATCH 65/91] recording presentation: Reduce the top/bottom margin so bars fit better MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gnuplot's auto-margins don't work well on this graph, so manually specify the top/bottom (actually left/right - the graph is rotated 90°) to make the bars fit better, particularly when using wide aspect ratio slides. --- .../presentation/scripts/publish/presentation.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/record-and-playback/presentation/scripts/publish/presentation.rb b/record-and-playback/presentation/scripts/publish/presentation.rb index 906d225c69..b08d7d455d 100755 --- a/record-and-playback/presentation/scripts/publish/presentation.rb +++ b/record-and-playback/presentation/scripts/publish/presentation.rb @@ -489,6 +489,8 @@ def storePollResultShape(xml, shape) File.open(gpl_file, 'w') do |g| g.puts('reset') g.puts("set term pdfcairo size #{height / 72}, #{width / 72} font \"Arial,48\" noenhanced") + g.puts('set lmargin 0.5') + g.puts('set rmargin 0.5') g.puts('unset key') g.puts('set style data boxes') g.puts('set style fill solid border -1') From d9592aba04602f67c05514d4e7a313d39a3b80b9 Mon Sep 17 00:00:00 2001 From: Anton Georgiev Date: Fri, 7 Apr 2017 11:00:44 -0400 Subject: [PATCH 66/91] fix issue with new chat session opened --- .../ui/components/chat/message-list/component.jsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/chat/message-list/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/message-list/component.jsx index adf9d0e1c4..e504e5c937 100755 --- a/bigbluebutton-html5/imports/ui/components/chat/message-list/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/message-list/component.jsx @@ -124,13 +124,14 @@ class MessageList extends Component { // console.log('hasNewUnreadMessages=' + hasNewUnreadMessages); // check if the messages include - const lastMessageId = nextProps.messages[nextProps.messages.length - 1].id; - const userLeftIsDisplayed = lastMessageId.includes('partner-disconnected'); + const lastMessage = nextProps.messages[nextProps.messages.length - 1]; + if (lastMessage) { + const userLeftIsDisplayed = lastMessage.id.includes('partner-disconnected'); + if (partnerIsLoggedOut && userLeftIsDisplayed) return false; // update leads to endless loop + } if (switchingCorrespondent || hasNewUnreadMessages) return true; - if (partnerIsLoggedOut && userLeftIsDisplayed) return false; // update leads to endless loop - return true; } From c35e36f58243122f48293c40f06a4801915a620a Mon Sep 17 00:00:00 2001 From: Klaus Klein Date: Fri, 7 Apr 2017 14:12:29 -0300 Subject: [PATCH 67/91] Change code to better readability --- .../user-list/user-list-item/component.jsx | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-item/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-item/component.jsx index d8810ba01b..7a3a18edf5 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-item/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-item/component.jsx @@ -138,40 +138,48 @@ class UserListItem extends Component { ]); } + componentDidUpdate(prevProps, prevState) { const { isActionsOpen, dropdownVisible } = this.state; - console.log('CARAIO', isActionsOpen, dropdownVisible); - if(isActionsOpen && !dropdownVisible) { - const dropdown = findDOMNode(this.refs.dropdown); + if (isActionsOpen && !dropdownVisible) { + const dropdown = findDOMNode(this.refs.dropdown); const dropdownTrigger = dropdown.children[0]; const dropdownContent = dropdown.children[1]; - const scrollContainer = dropdown.parentElement.parentElement; + const scrollContainer = dropdown.parentElement.parentElement; let nextState = { dropdownVisible: true, }; - const isDropdownVisible = dropdownContent.offsetTop + dropdownContent.offsetHeight + scrollContainer.scrollTop< window.innerHeight; + const isDropdownVisible = this.checkIfDropdownIsVisible(dropdownContent.offsetTop, dropdownContent.offsetHeight); - //console.log('isVisible', dropdownContent.offsetTop, dropdownContent.offsetHeight, window.innerHeight); - if (!isDropdownVisible) { - //console.log('EU DEVERIA SER BOTTOM'); nextState.dropdownOffset = window.innerHeight - (dropdownTrigger.offsetTop + dropdownTrigger.offsetHeight - scrollContainer.scrollTop); nextState.dropdownDirection = 'bottom'; } this.setState(nextState); - //console.log(nextState) } } + /** + * Return true if the content fit on the screen, false otherwise. + * + * @param {number} contentOffSetTop + * @param {number} contentOffsetHeight + * @return True if the content fit on the screen, false otherwise. + */ + checkIfDropdownIsVisible(contentOffSetTop, contentOffsetHeight) { + return (contentOffSetTop + contentOffsetHeight) < window.innerHeight; + } + + onActionsShow() { const dropdown = findDOMNode(this.refs.dropdown); const scrollContainer = dropdown.parentElement.parentElement; - const dropdownTrigger = dropdown.children[0]; + const dropdownTrigger = dropdown.children[0]; this.setState({ isActionsOpen: true, From 94c5151e445eefaf9addc508388b012936c3ddb7 Mon Sep 17 00:00:00 2001 From: Klaus Klein Date: Fri, 7 Apr 2017 15:49:43 -0300 Subject: [PATCH 68/91] Adjust code for readability, issue #3764 --- .../user-list/user-list-item/component.jsx | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-item/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-item/component.jsx index 7a3a18edf5..165b69b03d 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-item/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-item/component.jsx @@ -140,9 +140,14 @@ class UserListItem extends Component { componentDidUpdate(prevProps, prevState) { - const { isActionsOpen, dropdownVisible } = this.state; + this.checkDropdownDirection(); + } - if (isActionsOpen && !dropdownVisible) { + /** + * Check if the dropdown is visible, if so, check if should be draw on top or bottom direction. + */ + checkDropdownDirection() { + if (this.isDropdownActivedByUser()) { const dropdown = findDOMNode(this.refs.dropdown); const dropdownTrigger = dropdown.children[0]; const dropdownContent = dropdown.children[1]; @@ -153,10 +158,14 @@ class UserListItem extends Component { dropdownVisible: true, }; - const isDropdownVisible = this.checkIfDropdownIsVisible(dropdownContent.offsetTop, dropdownContent.offsetHeight); + const isDropdownVisible = + this.checkIfDropdownIsVisible(dropdownContent.offsetTop, dropdownContent.offsetHeight); if (!isDropdownVisible) { - nextState.dropdownOffset = window.innerHeight - (dropdownTrigger.offsetTop + dropdownTrigger.offsetHeight - scrollContainer.scrollTop); + const offsetPageTop = + (dropdownTrigger.offsetTop + dropdownTrigger.offsetHeight - scrollContainer.scrollTop); + + nextState.dropdownOffset = window.innerHeight - offsetPageTop; nextState.dropdownDirection = 'bottom'; } @@ -164,6 +173,16 @@ class UserListItem extends Component { } } + /** + * Check if the dropdown is visible and is opened by the user + * + * @return True if is visible and opened by the user. + */ + isDropdownActivedByUser() { + const { isActionsOpen, dropdownVisible } = this.state; + return isActionsOpen && !dropdownVisible; + } + /** * Return true if the content fit on the screen, false otherwise. * @@ -188,7 +207,7 @@ class UserListItem extends Component { dropdownDirection: 'top', }); - findDOMNode(this).parentElement.addEventListener('scroll', this.handleScroll, false); + scrollContainer.addEventListener('scroll', this.handleScroll, false); } onActionsHide() { From 52a74d00637e22e40684b1254a52c56252168b82 Mon Sep 17 00:00:00 2001 From: Fred Dixon Date: Sat, 8 Apr 2017 17:41:20 -0500 Subject: [PATCH 69/91] Fixed output of free memory + minor code cleanup --- bigbluebutton-config/bin/bbb-conf | 116 ++++++++++++------------------ 1 file changed, 47 insertions(+), 69 deletions(-) diff --git a/bigbluebutton-config/bin/bbb-conf b/bigbluebutton-config/bin/bbb-conf index 689b90fb06..03851f4fb4 100755 --- a/bigbluebutton-config/bin/bbb-conf +++ b/bigbluebutton-config/bin/bbb-conf @@ -55,6 +55,7 @@ # 2016-05-28 FFD Initial updates for 1.1-dev # 2016-08-15 GTR Archive more logs with zip option and show more applications with status # 2016-10-17 GTR Added redis to checked server components & added ownership check for video and freeswitch recording directories +# 2017-04-08 FFD Cleanup for 1.1-beta #set -x #set -e @@ -118,7 +119,7 @@ if [ ! -f /var/www/bigbluebutton/client/conf/config.xml ]; then echo "# BigBlueButton does not appear to be installed. Could not" echo "# locate:" echo "#" - echo "# /usr/share/red5/webapps/bigbluebutton/WEB-INF/red5-web.xml" + echo "# /var/www/bigbluebutton/client/conf/config.xml" exit 1 fi @@ -131,7 +132,7 @@ else fi # -# We're going to give ^bigbluebutton.web.logoutURL a default value so bbb-conf does not give a warning +# We're going to give ^bigbluebutton.web.logoutURL a default value (if undefined) so bbb-conf does not give a warning # if [ -f $SERVLET_DIR/bigbluebutton/WEB-INF/classes/bigbluebutton.properties ]; then if cat $SERVLET_DIR/bigbluebutton/WEB-INF/classes/bigbluebutton.properties | grep -q ^bigbluebutton.web.logoutURL=$; then @@ -154,8 +155,11 @@ else IP=$(echo "$(LANG=c ifconfig | awk -v RS="" '{gsub (/\n[ ]*inet /," ")}1' | grep ^et.* | head -n1 | sed 's/.*addr://g' | sed 's/ .*//g')$(LANG=c ifconfig | awk -v RS="" '{gsub (/\n[ ]*inet /," ")}1' | grep ^en.* | head -n1 | sed 's/.*addr://g' | sed 's/ .*//g')" | head -n1) fi +# +# Calculate total memory on this server +# MEM=`grep MemTotal /proc/meminfo | awk '{print $2}'` - +MEM=$((MEM/1000)) # @@ -213,13 +217,14 @@ usage() { echo echo "Configuration:" echo " --version Display BigBlueButton version (packages)" - echo " --setip Set IP/hostname for BigBlueButton" + echo " --setip Set IP/hostname for BigBlueButton" echo " --setsecret Change the shared secret in bigbluebutton.properties" echo echo "Monitoring:" echo " --check Check configuration files and processes for problems" echo " --debug Scan the log files for error messages" echo " --watch Scan the log files for error messages every 2 seconds" + echo " --network View network connections on 80 and 1935 by IP address" echo " --secret View the URL and shared secret for the server" echo " --lti View the URL and secret for LTI (if installed)" echo @@ -230,10 +235,6 @@ usage() { echo " --clean Restart and clean all log files" echo " --status Display running status of components" echo " --zip Zip up log files for reporting an error" - echo - echo "Testing:" - echo " --enablewebrtc Enables WebRTC audio in the server" - echo " --disablewebrtc Disables WebRTC audio in the server" echo } @@ -398,15 +399,6 @@ start_bigbluebutton () { NGINX_IP=$(cat /etc/nginx/sites-available/bigbluebutton | grep -v '#' | sed -n '/server_name/{s/.*name[ ]*//;s/;//;p}' | cut -d' ' -f1) check_no_value server_name /etc/nginx/sites-available/bigbluebutton $NGINX_IP - #if ! nc -z -w 1 127.0.0.1 9123 > /dev/null; then - # while ! nc -z -w 1 127.0.0.1 9123 > /dev/null; do - # echo -n "." - # sleep 1 - # done - #fi - - #sleep 5 - #if ! wget http://$BBB_WEB/bigbluebutton/api -O - --quiet | grep -q SUCCESS; then # echo "Startup unsuccessful: could not connect to http://$BBB_WEB/bigbluebutton/api" # exit 1 @@ -471,6 +463,9 @@ display_bigbluebutton_status () { fi } +# +# Depreciated -- WebRTC is always enabled by default +# enable_webrtc(){ # Set server ip address in FreeSWITCH sed -i "s@@@g" $FREESWITCH_VARS @@ -549,12 +544,6 @@ while [ $# -gt 0 ]; do continue fi - if [ "$1" = "--setup-samba" -o "$1" = "-setup-samba" ]; then - SAMBA=1 - shift - continue - fi - if [ "$1" = "--version" -o "$1" = "-version" -o "$1" = "-v" ]; then VERSION=1 shift @@ -602,18 +591,6 @@ while [ $# -gt 0 ]; do continue fi - if [ "$1" = "--enablewebrtc" -o "$1" = "-enablewebrtc" ]; then - need_root - enable_webrtc - exit 0 - fi - - if [ "$1" = "--disablewebrtc" -o "$1" = "-disablewebrtc" ]; then - need_root - disable_webrtc - exit 0 - fi - # # all other parameters requires at least 1 argument # @@ -632,8 +609,8 @@ while [ $# -gt 0 ]; do fi if [ "$1" = "--salt" -o "$1" = "-salt" -o "$1" = "--setsalt" -o "$1" = "--secret" -o "$1" = "-secret" -o "$1" = "--setsecret" ]; then - SALT="${2}" - if [ -z "$SALT" ]; then + SECRET="${2}" + if [ -z "$SECRET" ]; then BBB_WEB_URL=$(cat ${SERVLET_DIR}/bigbluebutton/WEB-INF/classes/bigbluebutton.properties | grep -v '#' | sed -n '/^bigbluebutton.web.serverURL/{s/.*=//;p}') SECRET=$(cat ${SERVLET_DIR}/bigbluebutton/WEB-INF/classes/bigbluebutton.properties | grep -v '#' | grep securitySalt | cut -d= -f2); echo @@ -647,7 +624,7 @@ while [ $# -gt 0 ]; do fi if [ "$1" = "--lti" -o "$1" = "-lti" ]; then - if [ -z "$SALT" ]; then + if [ -z "$SECRET" ]; then if [ -f ${SERVLET_DIR}/lti/WEB-INF/classes/lti-config.properties ]; then LTI_URL='http://'$(cat ${SERVLET_DIR}/lti/WEB-INF/classes/lti-config.properties | grep -v '#' | sed -n '/^ltiEndPoint/{s/^.*=//;p}')'/lti/tool' CUSTOMER=$(cat ${SERVLET_DIR}/lti/WEB-INF/classes/lti-config.properties | grep -v '#' | sed -n '/^ltiConsumer/{s/^.*=//;s/:.*//p}') @@ -684,28 +661,30 @@ fi # -# Set Security Salt -# - Legacy +# Set Shared Secret # -if [ $SALT ]; then +if [ $SECRET ]; then need_root - change_var_salt ${SERVLET_DIR}/bigbluebutton/WEB-INF/classes/bigbluebutton.properties securitySalt $SALT + change_var_salt ${SERVLET_DIR}/bigbluebutton/WEB-INF/classes/bigbluebutton.properties securitySalt $SECRET if [ -f /usr/local/bigbluebutton/bbb-webhooks/config_local.coffee ]; then - sed -i "s|\(^[ \t]*config.bbb.sharedSecret[ =]*\).*|\1\"$SALT\"|g" /usr/local/bigbluebutton/bbb-webhooks/config_local.coffee + sed -i "s|\(^[ \t]*config.bbb.sharedSecret[ =]*\).*|\1\"$SECRET\"|g" /usr/local/bigbluebutton/bbb-webhooks/config_local.coffee fi if [ -f /usr/local/bigbluebutton/bbb-webhooks/extra/post_catcher.js ]; then - sed -i "s|\(^[ \t]*var shared_secret[ =]*\)[^;]*|\1\"$SALT\"|g" /usr/local/bigbluebutton/bbb-webhooks/extra/post_catcher.js + sed -i "s|\(^[ \t]*var shared_secret[ =]*\)[^;]*|\1\"$SECRET\"|g" /usr/local/bigbluebutton/bbb-webhooks/extra/post_catcher.js fi if [ -f /usr/share/bbb-apps-akka/conf/application.conf ]; then - sed -i "s/sharedSecret[ ]*=[ ]*\"[^\"]*\"/sharedSecret=\"$SALT\"/g" \ + sed -i "s/sharedSecret[ ]*=[ ]*\"[^\"]*\"/sharedSecret=\"$SECRET\"/g" \ /usr/share/bbb-apps-akka/conf/application.conf fi - echo "Changed BigBlueButton's shared secret to $SALT" + echo "Changed BigBlueButton's shared secret to $SECRET" + echo + echo "You must restart BigBlueButton for the changes to take effect" + echo " sudo bbb-conf --restart" echo fi @@ -855,13 +834,13 @@ check_configuration() { # # Make sure the salt for the API matches the server # - SALT_PROPERTIES=$(cat ${SERVLET_DIR}/bigbluebutton/WEB-INF/classes/bigbluebutton.properties | grep -v '#' | tr -d '\r' | sed -n '/securitySalt/{s/.*=//;p}') - SALT_DEMO=$(cat ${SERVLET_DIR}/demo/bbb_api_conf.jsp | grep -v '^//' | tr -d '\r' | sed -n '/salt[ ]*=/{s/.*=[ ]*"//;s/".*//g;p}') + SECRET_PROPERTIES=$(cat ${SERVLET_DIR}/bigbluebutton/WEB-INF/classes/bigbluebutton.properties | grep -v '#' | tr -d '\r' | sed -n '/securitySalt/{s/.*=//;p}') + SECRET_DEMO=$(cat ${SERVLET_DIR}/demo/bbb_api_conf.jsp | grep -v '^//' | tr -d '\r' | sed -n '/salt[ ]*=/{s/.*=[ ]*"//;s/".*//g;p}') - if [ "$SALT_PROPERTIES" != "$SALT_DEMO" ]; then - echo "# Warning: API Salt mismatch: " - echo "# ${SERVLET_DIR}/bigbluebutton/WEB-INF/classes/bigbluebutton.properties = $SALT_PROPERTIES" - echo "# ${SERVLET_DIR}/demo/bbb_api_conf.jsp = $SALT_DEMO" + if [ "$SECRET_PROPERTIES" != "$SECRET_DEMO" ]; then + echo "# Warning: API Shared Secret mismatch: " + echo "# ${SERVLET_DIR}/bigbluebutton/WEB-INF/classes/bigbluebutton.properties = $SECRET_PROPERTIES" + echo "# ${SERVLET_DIR}/demo/bbb_api_conf.jsp = $SECRET_DEMO" echo fi @@ -875,16 +854,16 @@ check_configuration() { fi fi - BBB_SALT=$(cat ${SERVLET_DIR}/bigbluebutton/WEB-INF/classes/bigbluebutton.properties | grep -v '#' | tr -d '\r' | sed -n '/securitySalt/{s/.*=//;p}') + BBB_SECRET=$(cat ${SERVLET_DIR}/bigbluebutton/WEB-INF/classes/bigbluebutton.properties | grep -v '#' | tr -d '\r' | sed -n '/securitySalt/{s/.*=//;p}') NGINX_IP=$(cat /etc/nginx/sites-available/bigbluebutton | grep -v '#' | sed -n '/server_name/{s/.*name[ ]*//;s/;//;p}' | cut -d' ' -f1) if [ -f /usr/local/bigbluebutton/bbb-webhooks/config_local.coffee ]; then - WEBHOOKS_SALT=$(cat /usr/local/bigbluebutton/bbb-webhooks/config_local.coffee | grep '^[ \t]*config.bbb.sharedSecret[ =]*' | cut -d '"' -f2) + WEBHOOKS_SECRET=$(cat /usr/local/bigbluebutton/bbb-webhooks/config_local.coffee | grep '^[ \t]*config.bbb.sharedSecret[ =]*' | cut -d '"' -f2) - if [ "$BBB_SALT" != "$WEBHOOKS_SALT" ]; then - echo "# Warning: Webhooks API Salt mismatch: " - echo "# ${SERVLET_DIR}/bigbluebutton/WEB-INF/classes/bigbluebutton.properties = $BBB_SALT" - echo "# /usr/local/bigbluebutton/bbb-webhooks/config_local.coffee = $WEBHOOKS_SALT" + if [ "$BBB_SECRET" != "$WEBHOOKS_SECRET" ]; then + echo "# Warning: Webhooks API Shared Secret mismatch: " + echo "# ${SERVLET_DIR}/bigbluebutton/WEB-INF/classes/bigbluebutton.properties = $BBB_SECRET" + echo "# /usr/local/bigbluebutton/bbb-webhooks/config_local.coffee = $WEBHOOKS_SECRET" echo fi @@ -900,13 +879,13 @@ check_configuration() { fi if [ -f ${SERVLET_DIR}/lti/WEB-INF/classes/lti-config.properties ]; then - LTI_SALT=$(cat ${SERVLET_DIR}/lti/WEB-INF/classes/lti-config.properties | grep -v '#' | tr -d '\r' | sed -n '/^bigbluebuttonSalt/{s/.*=//;p}') - BBB_SALT=$(cat ${SERVLET_DIR}/bigbluebutton/WEB-INF/classes/bigbluebutton.properties | grep -v '#' | tr -d '\r' | sed -n '/securitySalt/{s/.*=//;p}') + LTI_SECRET=$(cat ${SERVLET_DIR}/lti/WEB-INF/classes/lti-config.properties | grep -v '#' | tr -d '\r' | sed -n '/^bigbluebuttonSalt/{s/.*=//;p}') + BBB_SECRET=$(cat ${SERVLET_DIR}/bigbluebutton/WEB-INF/classes/bigbluebutton.properties | grep -v '#' | tr -d '\r' | sed -n '/securitySalt/{s/.*=//;p}') - if [ "$LTI_SALT" != "$BBB_SALT" ]; then + if [ "$LTI_SECRET" != "$BBB_SECRET" ]; then echo "# Warning: LTI shared secret (salt) mismatch:" - echo "# ${SERVLET_DIR}/lti/WEB-INF/classes/lti-config.properties = $LTI_SALT" - echo "# ${SERVLET_DIR}/bigbluebutton/WEB-INF/classes/bigbluebutton.properties = $BBB_SALT" + echo "# ${SERVLET_DIR}/lti/WEB-INF/classes/lti-config.properties = $LTI_SECRET" + echo "# ${SERVLET_DIR}/bigbluebutton/WEB-INF/classes/bigbluebutton.properties = $BBB_SECRET" echo fi fi @@ -1374,7 +1353,7 @@ check_state() { echo fi - if (( $MEM < 3940000 )); then + if (( $MEM < 3940 )); then echo "# Warning: You are running BigBlueButton on a server with less than 4G of memory. Your" echo "# performance may suffer." echo @@ -1785,7 +1764,7 @@ if [ -n "$HOST" ]; then /usr/share/bbb-apps-akka/conf/application.conf sed -i "s/defaultPresentationURL[ ]*=[ ]*\"[^\"]*\"/defaultPresentationURL=\"${PROTOCOL_HTTP}:\/\/$HOST\/default.pdf\"/g" \ /usr/share/bbb-apps-akka/conf/application.conf - # XXX Temporary fix to ensure application.conf has the latest shared secret + # Fix to ensure application.conf has the latest shared secret SECRET=$(cat ${SERVLET_DIR}/bigbluebutton/WEB-INF/classes/bigbluebutton.properties | grep -v '#' | grep securitySalt | cut -d= -f2); sed -i "s/sharedSecret[ ]*=[ ]*\"[^\"]*\"/sharedSecret=\"$SECRET\"/g" \ /usr/share/bbb-apps-akka/conf/application.conf @@ -1916,17 +1895,16 @@ if [ $CLEAN ]; then fi if [ $NETWORK ]; then - netstat -ant | egrep ":1935|:9123|:80\ " | egrep -v ":::|0.0.0.0" > /tmp/t_net + netstat -ant | egrep ":1935|:80\ " | egrep -v ":::|0.0.0.0" > /tmp/t_net REMOTE=$(cat /tmp/t_net | cut -c 45-68 | cut -d ":" -f1 | sort | uniq) if [ "$REMOTE" != "" ]; then - echo -e "netstat\t\t\t80\t1935\t9123" + echo -e "netstat\t\t\t80\t1935" for IP in $REMOTE ; do PORT_1935=$(cat /tmp/t_net | grep :1935 | cut -c 45-68 | cut -d ":" -f1 | grep $IP | wc -l) - PORT_9123=$(cat /tmp/t_net | grep :9123 | cut -c 45-68 | cut -d ":" -f1 | grep $IP | wc -l ) PORT_80=$(cat /tmp/t_net | grep :80 | cut -c 45-68 | cut -d ":" -f1 | grep $IP | wc -l ) - echo -e "$IP\t\t$PORT_80\t$PORT_1935\t$PORT_9123" + echo -e "$IP\t\t$PORT_80\t$PORT_1935" done fi fi From 92554f8b3e39918a88af0ae9f5b85057cb0556eb Mon Sep 17 00:00:00 2001 From: Ghazi Triki Date: Mon, 10 Apr 2017 11:16:46 +0100 Subject: [PATCH 70/91] Squashed commits for displaying WebRTC stats inside the Flex client. --- .../branding/default/style/css/BBBDefault.css | 7 + .../images/almost_strong_audio_status.png | Bin 0 -> 104 bytes .../images/almost_weak_audio_status.png | Bin 0 -> 103 bytes .../css/assets/images/strong_audio_status.png | Bin 0 -> 102 bytes .../css/assets/images/weak_audio_status.png | Bin 0 -> 100 bytes .../locale/en_US/bbbResources.properties | 4 + .../resources/config.xml.template | 1 + .../resources/prod/BigBlueButton.html | 4 + .../resources/prod/lib/adapter.js | 3201 +++++++++++++++++ .../resources/prod/lib/bbb_api_bridge.js | 11 +- .../prod/lib/bbb_webrtc_bridge_sip.js | 3 + .../resources/prod/lib/getStats.js | 297 ++ .../resources/prod/lib/webrtc_stats_bridge.js | 305 ++ .../main/api/ExternalApiCallbacks.as | 8 +- .../org/bigbluebutton/main/events/BBBEvent.as | 1 + .../bigbluebutton/main/views/MainToolbar.mxml | 3 +- .../main/views/WebRTCAudioStatus.mxml | 111 + .../modules/phone/PhoneOptions.as | 6 + 18 files changed, 3958 insertions(+), 4 deletions(-) create mode 100644 bigbluebutton-client/branding/default/style/css/assets/images/almost_strong_audio_status.png create mode 100644 bigbluebutton-client/branding/default/style/css/assets/images/almost_weak_audio_status.png create mode 100644 bigbluebutton-client/branding/default/style/css/assets/images/strong_audio_status.png create mode 100644 bigbluebutton-client/branding/default/style/css/assets/images/weak_audio_status.png create mode 100644 bigbluebutton-client/resources/prod/lib/adapter.js create mode 100644 bigbluebutton-client/resources/prod/lib/getStats.js create mode 100644 bigbluebutton-client/resources/prod/lib/webrtc_stats_bridge.js create mode 100644 bigbluebutton-client/src/org/bigbluebutton/main/views/WebRTCAudioStatus.mxml diff --git a/bigbluebutton-client/branding/default/style/css/BBBDefault.css b/bigbluebutton-client/branding/default/style/css/BBBDefault.css index cec394e23b..7a15010a76 100755 --- a/bigbluebutton-client/branding/default/style/css/BBBDefault.css +++ b/bigbluebutton-client/branding/default/style/css/BBBDefault.css @@ -1037,6 +1037,13 @@ AlertForm { refreshImage: Embed(source='assets/images/status_refresh.png'); } +.webRTCAudioStatusStyle { + strongAudioStatus: Embed(source='assets/images/strong_audio_status.png'); + almostStrongAudioStatus: Embed(source='assets/images/almost_strong_audio_status.png'); + almostWeakAudioStatus: Embed(source='assets/images/almost_weak_audio_status.png'); + weakAudioStatus: Embed(source='assets/images/weak_audio_status.png'); +} + .warningButtonStyle { icon: Embed('assets/images/status_warning_20.png'); } diff --git a/bigbluebutton-client/branding/default/style/css/assets/images/almost_strong_audio_status.png b/bigbluebutton-client/branding/default/style/css/assets/images/almost_strong_audio_status.png new file mode 100644 index 0000000000000000000000000000000000000000..af067f7abf218f358dd784146f99f7a0b19edb5f GIT binary patch literal 104 zcmeAS@N?(olHy`uVBq!ia0vp^Nq`WTzQu>}Qjv*Ddk_~P>`YJD;*1(jt z@*uaHtkJ6pf$cq3DOVI1^GTMTSm6v35_kjD;5XIFzVpCNAdA7%)z4*}Q$iB}o?IV> literal 0 HcmV?d00001 diff --git a/bigbluebutton-client/branding/default/style/css/assets/images/almost_weak_audio_status.png b/bigbluebutton-client/branding/default/style/css/assets/images/almost_weak_audio_status.png new file mode 100644 index 0000000000000000000000000000000000000000..ca769d83e4a552fb79daa54383a8bb937a755061 GIT binary patch literal 103 zcmeAS@N?(olHy`uVBq!ia0vp^N?8y8M*)f literal 0 HcmV?d00001 diff --git a/bigbluebutton-client/branding/default/style/css/assets/images/strong_audio_status.png b/bigbluebutton-client/branding/default/style/css/assets/images/strong_audio_status.png new file mode 100644 index 0000000000000000000000000000000000000000..5f7b11d30b0f4e8c3781facc0ab0ed51a0679692 GIT binary patch literal 102 zcmeAS@N?(olHy`uVBq!ia0vp^Nq`WTzQo5cljv*Ddk~yM3J+Rk1!@yb+ yX#8qIV0({M$`!@Me3GRnRye!K8i9lojx#Jtx8^>^WUUL-$l&Sf=d#Wzp$Py=H69`W literal 0 HcmV?d00001 diff --git a/bigbluebutton-client/branding/default/style/css/assets/images/weak_audio_status.png b/bigbluebutton-client/branding/default/style/css/assets/images/weak_audio_status.png new file mode 100644 index 0000000000000000000000000000000000000000..4f482fd65075fed2502716c728765488b6a36dbb GIT binary patch literal 100 zcmeAS@N?(olHy`uVBq!ia0vp^Nq`WTzQrey_jv*Ddl7Hl diff --git a/bigbluebutton-client/resources/prod/BigBlueButton.html b/bigbluebutton-client/resources/prod/BigBlueButton.html index 794943c6d9..e8b99b8839 100755 --- a/bigbluebutton-client/resources/prod/BigBlueButton.html +++ b/bigbluebutton-client/resources/prod/BigBlueButton.html @@ -120,6 +120,10 @@ + + + + diff --git a/bigbluebutton-client/resources/prod/lib/adapter.js b/bigbluebutton-client/resources/prod/lib/adapter.js new file mode 100644 index 0000000000..57ba9599b8 --- /dev/null +++ b/bigbluebutton-client/resources/prod/lib/adapter.js @@ -0,0 +1,3201 @@ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.adapter = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 0 ? 'm=' + part : part).trim() + '\r\n'; + }); +}; + +// Returns lines that start with a certain prefix. +SDPUtils.matchPrefix = function(blob, prefix) { + return SDPUtils.splitLines(blob).filter(function(line) { + return line.indexOf(prefix) === 0; + }); +}; + +// Parses an ICE candidate line. Sample input: +// candidate:702786350 2 udp 41819902 8.8.8.8 60769 typ relay raddr 8.8.8.8 +// rport 55996" +SDPUtils.parseCandidate = function(line) { + var parts; + // Parse both variants. + if (line.indexOf('a=candidate:') === 0) { + parts = line.substring(12).split(' '); + } else { + parts = line.substring(10).split(' '); + } + + var candidate = { + foundation: parts[0], + component: parts[1], + protocol: parts[2].toLowerCase(), + priority: parseInt(parts[3], 10), + ip: parts[4], + port: parseInt(parts[5], 10), + // skip parts[6] == 'typ' + type: parts[7] + }; + + for (var i = 8; i < parts.length; i += 2) { + switch (parts[i]) { + case 'raddr': + candidate.relatedAddress = parts[i + 1]; + break; + case 'rport': + candidate.relatedPort = parseInt(parts[i + 1], 10); + break; + case 'tcptype': + candidate.tcpType = parts[i + 1]; + break; + default: // Unknown extensions are silently ignored. + break; + } + } + return candidate; +}; + +// Translates a candidate object into SDP candidate attribute. +SDPUtils.writeCandidate = function(candidate) { + var sdp = []; + sdp.push(candidate.foundation); + sdp.push(candidate.component); + sdp.push(candidate.protocol.toUpperCase()); + sdp.push(candidate.priority); + sdp.push(candidate.ip); + sdp.push(candidate.port); + + var type = candidate.type; + sdp.push('typ'); + sdp.push(type); + if (type !== 'host' && candidate.relatedAddress && + candidate.relatedPort) { + sdp.push('raddr'); + sdp.push(candidate.relatedAddress); // was: relAddr + sdp.push('rport'); + sdp.push(candidate.relatedPort); // was: relPort + } + if (candidate.tcpType && candidate.protocol.toLowerCase() === 'tcp') { + sdp.push('tcptype'); + sdp.push(candidate.tcpType); + } + return 'candidate:' + sdp.join(' '); +}; + +// Parses an rtpmap line, returns RTCRtpCoddecParameters. Sample input: +// a=rtpmap:111 opus/48000/2 +SDPUtils.parseRtpMap = function(line) { + var parts = line.substr(9).split(' '); + var parsed = { + payloadType: parseInt(parts.shift(), 10) // was: id + }; + + parts = parts[0].split('/'); + + parsed.name = parts[0]; + parsed.clockRate = parseInt(parts[1], 10); // was: clockrate + // was: channels + parsed.numChannels = parts.length === 3 ? parseInt(parts[2], 10) : 1; + return parsed; +}; + +// Generate an a=rtpmap line from RTCRtpCodecCapability or +// RTCRtpCodecParameters. +SDPUtils.writeRtpMap = function(codec) { + var pt = codec.payloadType; + if (codec.preferredPayloadType !== undefined) { + pt = codec.preferredPayloadType; + } + return 'a=rtpmap:' + pt + ' ' + codec.name + '/' + codec.clockRate + + (codec.numChannels !== 1 ? '/' + codec.numChannels : '') + '\r\n'; +}; + +// Parses an a=extmap line (headerextension from RFC 5285). Sample input: +// a=extmap:2 urn:ietf:params:rtp-hdrext:toffset +SDPUtils.parseExtmap = function(line) { + var parts = line.substr(9).split(' '); + return { + id: parseInt(parts[0], 10), + uri: parts[1] + }; +}; + +// Generates a=extmap line from RTCRtpHeaderExtensionParameters or +// RTCRtpHeaderExtension. +SDPUtils.writeExtmap = function(headerExtension) { + return 'a=extmap:' + (headerExtension.id || headerExtension.preferredId) + + ' ' + headerExtension.uri + '\r\n'; +}; + +// Parses an ftmp line, returns dictionary. Sample input: +// a=fmtp:96 vbr=on;cng=on +// Also deals with vbr=on; cng=on +SDPUtils.parseFmtp = function(line) { + var parsed = {}; + var kv; + var parts = line.substr(line.indexOf(' ') + 1).split(';'); + for (var j = 0; j < parts.length; j++) { + kv = parts[j].trim().split('='); + parsed[kv[0].trim()] = kv[1]; + } + return parsed; +}; + +// Generates an a=ftmp line from RTCRtpCodecCapability or RTCRtpCodecParameters. +SDPUtils.writeFmtp = function(codec) { + var line = ''; + var pt = codec.payloadType; + if (codec.preferredPayloadType !== undefined) { + pt = codec.preferredPayloadType; + } + if (codec.parameters && Object.keys(codec.parameters).length) { + var params = []; + Object.keys(codec.parameters).forEach(function(param) { + params.push(param + '=' + codec.parameters[param]); + }); + line += 'a=fmtp:' + pt + ' ' + params.join(';') + '\r\n'; + } + return line; +}; + +// Parses an rtcp-fb line, returns RTCPRtcpFeedback object. Sample input: +// a=rtcp-fb:98 nack rpsi +SDPUtils.parseRtcpFb = function(line) { + var parts = line.substr(line.indexOf(' ') + 1).split(' '); + return { + type: parts.shift(), + parameter: parts.join(' ') + }; +}; +// Generate a=rtcp-fb lines from RTCRtpCodecCapability or RTCRtpCodecParameters. +SDPUtils.writeRtcpFb = function(codec) { + var lines = ''; + var pt = codec.payloadType; + if (codec.preferredPayloadType !== undefined) { + pt = codec.preferredPayloadType; + } + if (codec.rtcpFeedback && codec.rtcpFeedback.length) { + // FIXME: special handling for trr-int? + codec.rtcpFeedback.forEach(function(fb) { + lines += 'a=rtcp-fb:' + pt + ' ' + fb.type + + (fb.parameter && fb.parameter.length ? ' ' + fb.parameter : '') + + '\r\n'; + }); + } + return lines; +}; + +// Parses an RFC 5576 ssrc media attribute. Sample input: +// a=ssrc:3735928559 cname:something +SDPUtils.parseSsrcMedia = function(line) { + var sp = line.indexOf(' '); + var parts = { + ssrc: parseInt(line.substr(7, sp - 7), 10) + }; + var colon = line.indexOf(':', sp); + if (colon > -1) { + parts.attribute = line.substr(sp + 1, colon - sp - 1); + parts.value = line.substr(colon + 1); + } else { + parts.attribute = line.substr(sp + 1); + } + return parts; +}; + +// Extracts DTLS parameters from SDP media section or sessionpart. +// FIXME: for consistency with other functions this should only +// get the fingerprint line as input. See also getIceParameters. +SDPUtils.getDtlsParameters = function(mediaSection, sessionpart) { + var lines = SDPUtils.splitLines(mediaSection); + // Search in session part, too. + lines = lines.concat(SDPUtils.splitLines(sessionpart)); + var fpLine = lines.filter(function(line) { + return line.indexOf('a=fingerprint:') === 0; + })[0].substr(14); + // Note: a=setup line is ignored since we use the 'auto' role. + // Note2: 'algorithm' is not case sensitive except in Edge. + var dtlsParameters = { + role: 'auto', + fingerprints: [{ + algorithm: fpLine.split(' ')[0].toLowerCase(), + value: fpLine.split(' ')[1] + }] + }; + return dtlsParameters; +}; + +// Serializes DTLS parameters to SDP. +SDPUtils.writeDtlsParameters = function(params, setupType) { + var sdp = 'a=setup:' + setupType + '\r\n'; + params.fingerprints.forEach(function(fp) { + sdp += 'a=fingerprint:' + fp.algorithm + ' ' + fp.value + '\r\n'; + }); + return sdp; +}; +// Parses ICE information from SDP media section or sessionpart. +// FIXME: for consistency with other functions this should only +// get the ice-ufrag and ice-pwd lines as input. +SDPUtils.getIceParameters = function(mediaSection, sessionpart) { + var lines = SDPUtils.splitLines(mediaSection); + // Search in session part, too. + lines = lines.concat(SDPUtils.splitLines(sessionpart)); + var iceParameters = { + usernameFragment: lines.filter(function(line) { + return line.indexOf('a=ice-ufrag:') === 0; + })[0].substr(12), + password: lines.filter(function(line) { + return line.indexOf('a=ice-pwd:') === 0; + })[0].substr(10) + }; + return iceParameters; +}; + +// Serializes ICE parameters to SDP. +SDPUtils.writeIceParameters = function(params) { + return 'a=ice-ufrag:' + params.usernameFragment + '\r\n' + + 'a=ice-pwd:' + params.password + '\r\n'; +}; + +// Parses the SDP media section and returns RTCRtpParameters. +SDPUtils.parseRtpParameters = function(mediaSection) { + var description = { + codecs: [], + headerExtensions: [], + fecMechanisms: [], + rtcp: [] + }; + var lines = SDPUtils.splitLines(mediaSection); + var mline = lines[0].split(' '); + for (var i = 3; i < mline.length; i++) { // find all codecs from mline[3..] + var pt = mline[i]; + var rtpmapline = SDPUtils.matchPrefix( + mediaSection, 'a=rtpmap:' + pt + ' ')[0]; + if (rtpmapline) { + var codec = SDPUtils.parseRtpMap(rtpmapline); + var fmtps = SDPUtils.matchPrefix( + mediaSection, 'a=fmtp:' + pt + ' '); + // Only the first a=fmtp: is considered. + codec.parameters = fmtps.length ? SDPUtils.parseFmtp(fmtps[0]) : {}; + codec.rtcpFeedback = SDPUtils.matchPrefix( + mediaSection, 'a=rtcp-fb:' + pt + ' ') + .map(SDPUtils.parseRtcpFb); + description.codecs.push(codec); + // parse FEC mechanisms from rtpmap lines. + switch (codec.name.toUpperCase()) { + case 'RED': + case 'ULPFEC': + description.fecMechanisms.push(codec.name.toUpperCase()); + break; + default: // only RED and ULPFEC are recognized as FEC mechanisms. + break; + } + } + } + SDPUtils.matchPrefix(mediaSection, 'a=extmap:').forEach(function(line) { + description.headerExtensions.push(SDPUtils.parseExtmap(line)); + }); + // FIXME: parse rtcp. + return description; +}; + +// Generates parts of the SDP media section describing the capabilities / +// parameters. +SDPUtils.writeRtpDescription = function(kind, caps) { + var sdp = ''; + + // Build the mline. + sdp += 'm=' + kind + ' '; + sdp += caps.codecs.length > 0 ? '9' : '0'; // reject if no codecs. + sdp += ' UDP/TLS/RTP/SAVPF '; + sdp += caps.codecs.map(function(codec) { + if (codec.preferredPayloadType !== undefined) { + return codec.preferredPayloadType; + } + return codec.payloadType; + }).join(' ') + '\r\n'; + + sdp += 'c=IN IP4 0.0.0.0\r\n'; + sdp += 'a=rtcp:9 IN IP4 0.0.0.0\r\n'; + + // Add a=rtpmap lines for each codec. Also fmtp and rtcp-fb. + caps.codecs.forEach(function(codec) { + sdp += SDPUtils.writeRtpMap(codec); + sdp += SDPUtils.writeFmtp(codec); + sdp += SDPUtils.writeRtcpFb(codec); + }); + var maxptime = 0; + caps.codecs.forEach(function(codec) { + if (codec.maxptime > maxptime) { + maxptime = codec.maxptime; + } + }); + if (maxptime > 0) { + sdp += 'a=maxptime:' + maxptime + '\r\n'; + } + sdp += 'a=rtcp-mux\r\n'; + + caps.headerExtensions.forEach(function(extension) { + sdp += SDPUtils.writeExtmap(extension); + }); + // FIXME: write fecMechanisms. + return sdp; +}; + +// Parses the SDP media section and returns an array of +// RTCRtpEncodingParameters. +SDPUtils.parseRtpEncodingParameters = function(mediaSection) { + var encodingParameters = []; + var description = SDPUtils.parseRtpParameters(mediaSection); + var hasRed = description.fecMechanisms.indexOf('RED') !== -1; + var hasUlpfec = description.fecMechanisms.indexOf('ULPFEC') !== -1; + + // filter a=ssrc:... cname:, ignore PlanB-msid + var ssrcs = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:') + .map(function(line) { + return SDPUtils.parseSsrcMedia(line); + }) + .filter(function(parts) { + return parts.attribute === 'cname'; + }); + var primarySsrc = ssrcs.length > 0 && ssrcs[0].ssrc; + var secondarySsrc; + + var flows = SDPUtils.matchPrefix(mediaSection, 'a=ssrc-group:FID') + .map(function(line) { + var parts = line.split(' '); + parts.shift(); + return parts.map(function(part) { + return parseInt(part, 10); + }); + }); + if (flows.length > 0 && flows[0].length > 1 && flows[0][0] === primarySsrc) { + secondarySsrc = flows[0][1]; + } + + description.codecs.forEach(function(codec) { + if (codec.name.toUpperCase() === 'RTX' && codec.parameters.apt) { + var encParam = { + ssrc: primarySsrc, + codecPayloadType: parseInt(codec.parameters.apt, 10), + rtx: { + ssrc: secondarySsrc + } + }; + encodingParameters.push(encParam); + if (hasRed) { + encParam = JSON.parse(JSON.stringify(encParam)); + encParam.fec = { + ssrc: secondarySsrc, + mechanism: hasUlpfec ? 'red+ulpfec' : 'red' + }; + encodingParameters.push(encParam); + } + } + }); + if (encodingParameters.length === 0 && primarySsrc) { + encodingParameters.push({ + ssrc: primarySsrc + }); + } + + // we support both b=AS and b=TIAS but interpret AS as TIAS. + var bandwidth = SDPUtils.matchPrefix(mediaSection, 'b='); + if (bandwidth.length) { + if (bandwidth[0].indexOf('b=TIAS:') === 0) { + bandwidth = parseInt(bandwidth[0].substr(7), 10); + } else if (bandwidth[0].indexOf('b=AS:') === 0) { + bandwidth = parseInt(bandwidth[0].substr(5), 10); + } + encodingParameters.forEach(function(params) { + params.maxBitrate = bandwidth; + }); + } + return encodingParameters; +}; + +// parses http://draft.ortc.org/#rtcrtcpparameters* +SDPUtils.parseRtcpParameters = function(mediaSection) { + var rtcpParameters = {}; + + var cname; + // Gets the first SSRC. Note that with RTX there might be multiple + // SSRCs. + var remoteSsrc = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:') + .map(function(line) { + return SDPUtils.parseSsrcMedia(line); + }) + .filter(function(obj) { + return obj.attribute === 'cname'; + })[0]; + if (remoteSsrc) { + rtcpParameters.cname = remoteSsrc.value; + rtcpParameters.ssrc = remoteSsrc.ssrc; + } + + // Edge uses the compound attribute instead of reducedSize + // compound is !reducedSize + var rsize = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-rsize'); + rtcpParameters.reducedSize = rsize.length > 0; + rtcpParameters.compound = rsize.length === 0; + + // parses the rtcp-mux attrÑ–bute. + // Note that Edge does not support unmuxed RTCP. + var mux = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-mux'); + rtcpParameters.mux = mux.length > 0; + + return rtcpParameters; +}; + +// parses either a=msid: or a=ssrc:... msid lines an returns +// the id of the MediaStream and MediaStreamTrack. +SDPUtils.parseMsid = function(mediaSection) { + var parts; + var spec = SDPUtils.matchPrefix(mediaSection, 'a=msid:'); + if (spec.length === 1) { + parts = spec[0].substr(7).split(' '); + return {stream: parts[0], track: parts[1]}; + } + var planB = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:') + .map(function(line) { + return SDPUtils.parseSsrcMedia(line); + }) + .filter(function(parts) { + return parts.attribute === 'msid'; + }); + if (planB.length > 0) { + parts = planB[0].value.split(' '); + return {stream: parts[0], track: parts[1]}; + } +}; + +SDPUtils.writeSessionBoilerplate = function() { + // FIXME: sess-id should be an NTP timestamp. + return 'v=0\r\n' + + 'o=thisisadapterortc 8169639915646943137 2 IN IP4 127.0.0.1\r\n' + + 's=-\r\n' + + 't=0 0\r\n'; +}; + +SDPUtils.writeMediaSection = function(transceiver, caps, type, stream) { + var sdp = SDPUtils.writeRtpDescription(transceiver.kind, caps); + + // Map ICE parameters (ufrag, pwd) to SDP. + sdp += SDPUtils.writeIceParameters( + transceiver.iceGatherer.getLocalParameters()); + + // Map DTLS parameters to SDP. + sdp += SDPUtils.writeDtlsParameters( + transceiver.dtlsTransport.getLocalParameters(), + type === 'offer' ? 'actpass' : 'active'); + + sdp += 'a=mid:' + transceiver.mid + '\r\n'; + + if (transceiver.rtpSender && transceiver.rtpReceiver) { + sdp += 'a=sendrecv\r\n'; + } else if (transceiver.rtpSender) { + sdp += 'a=sendonly\r\n'; + } else if (transceiver.rtpReceiver) { + sdp += 'a=recvonly\r\n'; + } else { + sdp += 'a=inactive\r\n'; + } + + if (transceiver.rtpSender) { + // spec. + var msid = 'msid:' + stream.id + ' ' + + transceiver.rtpSender.track.id + '\r\n'; + sdp += 'a=' + msid; + + // for Chrome. + sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc + + ' ' + msid; + if (transceiver.sendEncodingParameters[0].rtx) { + sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc + + ' ' + msid; + sdp += 'a=ssrc-group:FID ' + + transceiver.sendEncodingParameters[0].ssrc + ' ' + + transceiver.sendEncodingParameters[0].rtx.ssrc + + '\r\n'; + } + } + // FIXME: this should be written by writeRtpDescription. + sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc + + ' cname:' + SDPUtils.localCName + '\r\n'; + if (transceiver.rtpSender && transceiver.sendEncodingParameters[0].rtx) { + sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc + + ' cname:' + SDPUtils.localCName + '\r\n'; + } + return sdp; +}; + +// Gets the direction from the mediaSection or the sessionpart. +SDPUtils.getDirection = function(mediaSection, sessionpart) { + // Look for sendrecv, sendonly, recvonly, inactive, default to sendrecv. + var lines = SDPUtils.splitLines(mediaSection); + for (var i = 0; i < lines.length; i++) { + switch (lines[i]) { + case 'a=sendrecv': + case 'a=sendonly': + case 'a=recvonly': + case 'a=inactive': + return lines[i].substr(2); + default: + // FIXME: What should happen here? + } + } + if (sessionpart) { + return SDPUtils.getDirection(sessionpart); + } + return 'sendrecv'; +}; + +SDPUtils.getKind = function(mediaSection) { + var lines = SDPUtils.splitLines(mediaSection); + var mline = lines[0].split(' '); + return mline[0].substr(2); +}; + +SDPUtils.isRejected = function(mediaSection) { + return mediaSection.split(' ', 2)[1] === '0'; +}; + +// Expose public methods. +module.exports = SDPUtils; + +},{}],2:[function(require,module,exports){ +/* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + /* eslint-env node */ + +'use strict'; + +// Shimming starts here. +(function() { + // Utils. + var utils = require('./utils'); + var logging = utils.log; + var browserDetails = utils.browserDetails; + // Export to the adapter global object visible in the browser. + module.exports.browserDetails = browserDetails; + module.exports.extractVersion = utils.extractVersion; + module.exports.disableLog = utils.disableLog; + + // Uncomment the line below if you want logging to occur, including logging + // for the switch statement below. Can also be turned on in the browser via + // adapter.disableLog(false), but then logging from the switch statement below + // will not appear. + // require('./utils').disableLog(false); + + // Browser shims. + var chromeShim = require('./chrome/chrome_shim') || null; + var edgeShim = require('./edge/edge_shim') || null; + var firefoxShim = require('./firefox/firefox_shim') || null; + var safariShim = require('./safari/safari_shim') || null; + + // Shim browser if found. + switch (browserDetails.browser) { + case 'chrome': + if (!chromeShim || !chromeShim.shimPeerConnection) { + logging('Chrome shim is not included in this adapter release.'); + return; + } + logging('adapter.js shimming chrome.'); + // Export to the adapter global object visible in the browser. + module.exports.browserShim = chromeShim; + + chromeShim.shimGetUserMedia(); + chromeShim.shimMediaStream(); + utils.shimCreateObjectURL(); + chromeShim.shimSourceObject(); + chromeShim.shimPeerConnection(); + chromeShim.shimOnTrack(); + chromeShim.shimGetSendersWithDtmf(); + break; + case 'firefox': + if (!firefoxShim || !firefoxShim.shimPeerConnection) { + logging('Firefox shim is not included in this adapter release.'); + return; + } + logging('adapter.js shimming firefox.'); + // Export to the adapter global object visible in the browser. + module.exports.browserShim = firefoxShim; + + firefoxShim.shimGetUserMedia(); + utils.shimCreateObjectURL(); + firefoxShim.shimSourceObject(); + firefoxShim.shimPeerConnection(); + firefoxShim.shimOnTrack(); + break; + case 'edge': + if (!edgeShim || !edgeShim.shimPeerConnection) { + logging('MS edge shim is not included in this adapter release.'); + return; + } + logging('adapter.js shimming edge.'); + // Export to the adapter global object visible in the browser. + module.exports.browserShim = edgeShim; + + edgeShim.shimGetUserMedia(); + utils.shimCreateObjectURL(); + edgeShim.shimPeerConnection(); + edgeShim.shimReplaceTrack(); + break; + case 'safari': + if (!safariShim) { + logging('Safari shim is not included in this adapter release.'); + return; + } + logging('adapter.js shimming safari.'); + // Export to the adapter global object visible in the browser. + module.exports.browserShim = safariShim; + + safariShim.shimOnAddStream(); + safariShim.shimGetUserMedia(); + break; + default: + logging('Unsupported browser!'); + } +})(); + +},{"./chrome/chrome_shim":3,"./edge/edge_shim":5,"./firefox/firefox_shim":7,"./safari/safari_shim":9,"./utils":10}],3:[function(require,module,exports){ + +/* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + /* eslint-env node */ +'use strict'; +var logging = require('../utils.js').log; +var browserDetails = require('../utils.js').browserDetails; + +var chromeShim = { + shimMediaStream: function() { + window.MediaStream = window.MediaStream || window.webkitMediaStream; + }, + + shimOnTrack: function() { + if (typeof window === 'object' && window.RTCPeerConnection && !('ontrack' in + window.RTCPeerConnection.prototype)) { + Object.defineProperty(window.RTCPeerConnection.prototype, 'ontrack', { + get: function() { + return this._ontrack; + }, + set: function(f) { + var self = this; + if (this._ontrack) { + this.removeEventListener('track', this._ontrack); + this.removeEventListener('addstream', this._ontrackpoly); + } + this.addEventListener('track', this._ontrack = f); + this.addEventListener('addstream', this._ontrackpoly = function(e) { + // onaddstream does not fire when a track is added to an existing + // stream. But stream.onaddtrack is implemented so we use that. + e.stream.addEventListener('addtrack', function(te) { + var event = new Event('track'); + event.track = te.track; + event.receiver = {track: te.track}; + event.streams = [e.stream]; + self.dispatchEvent(event); + }); + e.stream.getTracks().forEach(function(track) { + var event = new Event('track'); + event.track = track; + event.receiver = {track: track}; + event.streams = [e.stream]; + this.dispatchEvent(event); + }.bind(this)); + }.bind(this)); + } + }); + } + }, + + shimGetSendersWithDtmf: function() { + if (typeof window === 'object' && window.RTCPeerConnection && + !('getSenders' in RTCPeerConnection.prototype) && + 'createDTMFSender' in RTCPeerConnection.prototype) { + RTCPeerConnection.prototype.getSenders = function() { + return this._senders; + }; + var origAddStream = RTCPeerConnection.prototype.addStream; + var origRemoveStream = RTCPeerConnection.prototype.removeStream; + + RTCPeerConnection.prototype.addStream = function(stream) { + var pc = this; + pc._senders = pc._senders || []; + origAddStream.apply(pc, [stream]); + stream.getTracks().forEach(function(track) { + pc._senders.push({ + track: track, + get dtmf() { + if (this._dtmf === undefined) { + if (track.kind === 'audio') { + this._dtmf = pc.createDTMFSender(track); + } else { + this._dtmf = null; + } + } + return this._dtmf; + } + }); + }); + }; + + RTCPeerConnection.prototype.removeStream = function(stream) { + var pc = this; + pc._senders = pc._senders || []; + origRemoveStream.apply(pc, [stream]); + stream.getTracks().forEach(function(track) { + var sender = pc._senders.find(function(s) { + return s.track === track; + }); + if (sender) { + pc._senders.splice(pc._senders.indexOf(sender), 1); // remove sender + } + }); + }; + } + }, + + shimSourceObject: function() { + if (typeof window === 'object') { + if (window.HTMLMediaElement && + !('srcObject' in window.HTMLMediaElement.prototype)) { + // Shim the srcObject property, once, when HTMLMediaElement is found. + Object.defineProperty(window.HTMLMediaElement.prototype, 'srcObject', { + get: function() { + return this._srcObject; + }, + set: function(stream) { + var self = this; + // Use _srcObject as a private property for this shim + this._srcObject = stream; + if (this.src) { + URL.revokeObjectURL(this.src); + } + + if (!stream) { + this.src = ''; + return undefined; + } + this.src = URL.createObjectURL(stream); + // We need to recreate the blob url when a track is added or + // removed. Doing it manually since we want to avoid a recursion. + stream.addEventListener('addtrack', function() { + if (self.src) { + URL.revokeObjectURL(self.src); + } + self.src = URL.createObjectURL(stream); + }); + stream.addEventListener('removetrack', function() { + if (self.src) { + URL.revokeObjectURL(self.src); + } + self.src = URL.createObjectURL(stream); + }); + } + }); + } + } + }, + + shimPeerConnection: function() { + // The RTCPeerConnection object. + if (!window.RTCPeerConnection) { + window.RTCPeerConnection = function(pcConfig, pcConstraints) { + // Translate iceTransportPolicy to iceTransports, + // see https://code.google.com/p/webrtc/issues/detail?id=4869 + // this was fixed in M56 along with unprefixing RTCPeerConnection. + logging('PeerConnection'); + if (pcConfig && pcConfig.iceTransportPolicy) { + pcConfig.iceTransports = pcConfig.iceTransportPolicy; + } + + return new webkitRTCPeerConnection(pcConfig, pcConstraints); + }; + window.RTCPeerConnection.prototype = webkitRTCPeerConnection.prototype; + // wrap static methods. Currently just generateCertificate. + if (webkitRTCPeerConnection.generateCertificate) { + Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', { + get: function() { + return webkitRTCPeerConnection.generateCertificate; + } + }); + } + } else { + // migrate from non-spec RTCIceServer.url to RTCIceServer.urls + var OrigPeerConnection = RTCPeerConnection; + window.RTCPeerConnection = function(pcConfig, pcConstraints) { + if (pcConfig && pcConfig.iceServers) { + var newIceServers = []; + for (var i = 0; i < pcConfig.iceServers.length; i++) { + var server = pcConfig.iceServers[i]; + if (!server.hasOwnProperty('urls') && + server.hasOwnProperty('url')) { + console.warn('RTCIceServer.url is deprecated! Use urls instead.'); + server = JSON.parse(JSON.stringify(server)); + server.urls = server.url; + newIceServers.push(server); + } else { + newIceServers.push(pcConfig.iceServers[i]); + } + } + pcConfig.iceServers = newIceServers; + } + return new OrigPeerConnection(pcConfig, pcConstraints); + }; + window.RTCPeerConnection.prototype = OrigPeerConnection.prototype; + // wrap static methods. Currently just generateCertificate. + Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', { + get: function() { + return OrigPeerConnection.generateCertificate; + } + }); + } + + var origGetStats = RTCPeerConnection.prototype.getStats; + RTCPeerConnection.prototype.getStats = function(selector, + successCallback, errorCallback) { + var self = this; + var args = arguments; + + // If selector is a function then we are in the old style stats so just + // pass back the original getStats format to avoid breaking old users. + if (arguments.length > 0 && typeof selector === 'function') { + return origGetStats.apply(this, arguments); + } + + // When spec-style getStats is supported, return those when called with + // either no arguments or the selector argument is null. + if (origGetStats.length === 0 && (arguments.length === 0 || + typeof arguments[0] !== 'function')) { + return origGetStats.apply(this, []); + } + + var fixChromeStats_ = function(response) { + var standardReport = {}; + var reports = response.result(); + reports.forEach(function(report) { + var standardStats = { + id: report.id, + timestamp: report.timestamp, + type: { + localcandidate: 'local-candidate', + remotecandidate: 'remote-candidate' + }[report.type] || report.type + }; + report.names().forEach(function(name) { + standardStats[name] = report.stat(name); + }); + standardReport[standardStats.id] = standardStats; + }); + + return standardReport; + }; + + // shim getStats with maplike support + var makeMapStats = function(stats) { + return new Map(Object.keys(stats).map(function(key) { + return[key, stats[key]]; + })); + }; + + if (arguments.length >= 2) { + var successCallbackWrapper_ = function(response) { + args[1](makeMapStats(fixChromeStats_(response))); + }; + + return origGetStats.apply(this, [successCallbackWrapper_, + arguments[0]]); + } + + // promise-support + return new Promise(function(resolve, reject) { + origGetStats.apply(self, [ + function(response) { + resolve(makeMapStats(fixChromeStats_(response))); + }, reject]); + }).then(successCallback, errorCallback); + }; + + // add promise support -- natively available in Chrome 51 + if (browserDetails.version < 51) { + ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate'] + .forEach(function(method) { + var nativeMethod = RTCPeerConnection.prototype[method]; + RTCPeerConnection.prototype[method] = function() { + var args = arguments; + var self = this; + var promise = new Promise(function(resolve, reject) { + nativeMethod.apply(self, [args[0], resolve, reject]); + }); + if (args.length < 2) { + return promise; + } + return promise.then(function() { + args[1].apply(null, []); + }, + function(err) { + if (args.length >= 3) { + args[2].apply(null, [err]); + } + }); + }; + }); + } + + // promise support for createOffer and createAnswer. Available (without + // bugs) since M52: crbug/619289 + if (browserDetails.version < 52) { + ['createOffer', 'createAnswer'].forEach(function(method) { + var nativeMethod = RTCPeerConnection.prototype[method]; + RTCPeerConnection.prototype[method] = function() { + var self = this; + if (arguments.length < 1 || (arguments.length === 1 && + typeof arguments[0] === 'object')) { + var opts = arguments.length === 1 ? arguments[0] : undefined; + return new Promise(function(resolve, reject) { + nativeMethod.apply(self, [resolve, reject, opts]); + }); + } + return nativeMethod.apply(this, arguments); + }; + }); + } + + // shim implicit creation of RTCSessionDescription/RTCIceCandidate + ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate'] + .forEach(function(method) { + var nativeMethod = RTCPeerConnection.prototype[method]; + RTCPeerConnection.prototype[method] = function() { + arguments[0] = new ((method === 'addIceCandidate') ? + RTCIceCandidate : RTCSessionDescription)(arguments[0]); + return nativeMethod.apply(this, arguments); + }; + }); + + // support for addIceCandidate(null or undefined) + var nativeAddIceCandidate = + RTCPeerConnection.prototype.addIceCandidate; + RTCPeerConnection.prototype.addIceCandidate = function() { + if (!arguments[0]) { + if (arguments[1]) { + arguments[1].apply(null); + } + return Promise.resolve(); + } + return nativeAddIceCandidate.apply(this, arguments); + }; + } +}; + + +// Expose public methods. +module.exports = { + shimMediaStream: chromeShim.shimMediaStream, + shimOnTrack: chromeShim.shimOnTrack, + shimGetSendersWithDtmf: chromeShim.shimGetSendersWithDtmf, + shimSourceObject: chromeShim.shimSourceObject, + shimPeerConnection: chromeShim.shimPeerConnection, + shimGetUserMedia: require('./getusermedia') +}; + +},{"../utils.js":10,"./getusermedia":4}],4:[function(require,module,exports){ +/* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + /* eslint-env node */ +'use strict'; +var logging = require('../utils.js').log; +var browserDetails = require('../utils.js').browserDetails; + +// Expose public methods. +module.exports = function() { + var constraintsToChrome_ = function(c) { + if (typeof c !== 'object' || c.mandatory || c.optional) { + return c; + } + var cc = {}; + Object.keys(c).forEach(function(key) { + if (key === 'require' || key === 'advanced' || key === 'mediaSource') { + return; + } + var r = (typeof c[key] === 'object') ? c[key] : {ideal: c[key]}; + if (r.exact !== undefined && typeof r.exact === 'number') { + r.min = r.max = r.exact; + } + var oldname_ = function(prefix, name) { + if (prefix) { + return prefix + name.charAt(0).toUpperCase() + name.slice(1); + } + return (name === 'deviceId') ? 'sourceId' : name; + }; + if (r.ideal !== undefined) { + cc.optional = cc.optional || []; + var oc = {}; + if (typeof r.ideal === 'number') { + oc[oldname_('min', key)] = r.ideal; + cc.optional.push(oc); + oc = {}; + oc[oldname_('max', key)] = r.ideal; + cc.optional.push(oc); + } else { + oc[oldname_('', key)] = r.ideal; + cc.optional.push(oc); + } + } + if (r.exact !== undefined && typeof r.exact !== 'number') { + cc.mandatory = cc.mandatory || {}; + cc.mandatory[oldname_('', key)] = r.exact; + } else { + ['min', 'max'].forEach(function(mix) { + if (r[mix] !== undefined) { + cc.mandatory = cc.mandatory || {}; + cc.mandatory[oldname_(mix, key)] = r[mix]; + } + }); + } + }); + if (c.advanced) { + cc.optional = (cc.optional || []).concat(c.advanced); + } + return cc; + }; + + var shimConstraints_ = function(constraints, func) { + constraints = JSON.parse(JSON.stringify(constraints)); + if (constraints && constraints.audio) { + constraints.audio = constraintsToChrome_(constraints.audio); + } + if (constraints && typeof constraints.video === 'object') { + // Shim facingMode for mobile & surface pro. + var face = constraints.video.facingMode; + face = face && ((typeof face === 'object') ? face : {ideal: face}); + var getSupportedFacingModeLies = browserDetails.version < 61; + + if ((face && (face.exact === 'user' || face.exact === 'environment' || + face.ideal === 'user' || face.ideal === 'environment')) && + !(navigator.mediaDevices.getSupportedConstraints && + navigator.mediaDevices.getSupportedConstraints().facingMode && + !getSupportedFacingModeLies)) { + delete constraints.video.facingMode; + var match; + if (face.exact === 'environment' || face.ideal === 'environment') { + match = 'back'; + } else if (face.exact === 'user' || face.ideal === 'user') { + match = 'front'; + } + if (match) { + // Look for match in label. + return navigator.mediaDevices.enumerateDevices() + .then(function(devices) { + devices = devices.filter(function(d) { + return d.kind === 'videoinput'; + }); + var dev = devices.find(function(d) { + return d.label.toLowerCase().indexOf(match) !== -1; + }); + if (dev) { + constraints.video.deviceId = face.exact ? {exact: dev.deviceId} : + {ideal: dev.deviceId}; + } + constraints.video = constraintsToChrome_(constraints.video); + logging('chrome: ' + JSON.stringify(constraints)); + return func(constraints); + }); + } + } + constraints.video = constraintsToChrome_(constraints.video); + } + logging('chrome: ' + JSON.stringify(constraints)); + return func(constraints); + }; + + var shimError_ = function(e) { + return { + name: { + PermissionDeniedError: 'NotAllowedError', + ConstraintNotSatisfiedError: 'OverconstrainedError' + }[e.name] || e.name, + message: e.message, + constraint: e.constraintName, + toString: function() { + return this.name + (this.message && ': ') + this.message; + } + }; + }; + + var getUserMedia_ = function(constraints, onSuccess, onError) { + shimConstraints_(constraints, function(c) { + navigator.webkitGetUserMedia(c, onSuccess, function(e) { + onError(shimError_(e)); + }); + }); + }; + + navigator.getUserMedia = getUserMedia_; + + // Returns the result of getUserMedia as a Promise. + var getUserMediaPromise_ = function(constraints) { + return new Promise(function(resolve, reject) { + navigator.getUserMedia(constraints, resolve, reject); + }); + }; + + if (!navigator.mediaDevices) { + navigator.mediaDevices = { + getUserMedia: getUserMediaPromise_, + enumerateDevices: function() { + return new Promise(function(resolve) { + var kinds = {audio: 'audioinput', video: 'videoinput'}; + return MediaStreamTrack.getSources(function(devices) { + resolve(devices.map(function(device) { + return {label: device.label, + kind: kinds[device.kind], + deviceId: device.id, + groupId: ''}; + })); + }); + }); + }, + getSupportedConstraints: function() { + return { + deviceId: true, echoCancellation: true, facingMode: true, + frameRate: true, height: true, width: true + }; + } + }; + } + + // A shim for getUserMedia method on the mediaDevices object. + // TODO(KaptenJansson) remove once implemented in Chrome stable. + if (!navigator.mediaDevices.getUserMedia) { + navigator.mediaDevices.getUserMedia = function(constraints) { + return getUserMediaPromise_(constraints); + }; + } else { + // Even though Chrome 45 has navigator.mediaDevices and a getUserMedia + // function which returns a Promise, it does not accept spec-style + // constraints. + var origGetUserMedia = navigator.mediaDevices.getUserMedia. + bind(navigator.mediaDevices); + navigator.mediaDevices.getUserMedia = function(cs) { + return shimConstraints_(cs, function(c) { + return origGetUserMedia(c).then(function(stream) { + if (c.audio && !stream.getAudioTracks().length || + c.video && !stream.getVideoTracks().length) { + stream.getTracks().forEach(function(track) { + track.stop(); + }); + throw new DOMException('', 'NotFoundError'); + } + return stream; + }, function(e) { + return Promise.reject(shimError_(e)); + }); + }); + }; + } + + // Dummy devicechange event methods. + // TODO(KaptenJansson) remove once implemented in Chrome stable. + if (typeof navigator.mediaDevices.addEventListener === 'undefined') { + navigator.mediaDevices.addEventListener = function() { + logging('Dummy mediaDevices.addEventListener called.'); + }; + } + if (typeof navigator.mediaDevices.removeEventListener === 'undefined') { + navigator.mediaDevices.removeEventListener = function() { + logging('Dummy mediaDevices.removeEventListener called.'); + }; + } +}; + +},{"../utils.js":10}],5:[function(require,module,exports){ +/* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + /* eslint-env node */ +'use strict'; + +var SDPUtils = require('sdp'); +var browserDetails = require('../utils').browserDetails; + +// sort tracks such that they follow an a-v-a-v... +// pattern. +function sortTracks(tracks) { + var audioTracks = tracks.filter(function(track) { + return track.kind === 'audio'; + }); + var videoTracks = tracks.filter(function(track) { + return track.kind === 'video'; + }); + tracks = []; + while (audioTracks.length || videoTracks.length) { + if (audioTracks.length) { + tracks.push(audioTracks.shift()); + } + if (videoTracks.length) { + tracks.push(videoTracks.shift()); + } + } + return tracks; +} + +// Edge does not like +// 1) stun: +// 2) turn: that does not have all of turn:host:port?transport=udp +// 3) turn: with ipv6 addresses +// 4) turn: occurring muliple times +function filterIceServers(iceServers) { + var hasTurn = false; + iceServers = JSON.parse(JSON.stringify(iceServers)); + return iceServers.filter(function(server) { + if (server && (server.urls || server.url)) { + var urls = server.urls || server.url; + if (server.url && !server.urls) { + console.warn('RTCIceServer.url is deprecated! Use urls instead.'); + } + var isString = typeof urls === 'string'; + if (isString) { + urls = [urls]; + } + urls = urls.filter(function(url) { + var validTurn = url.indexOf('turn:') === 0 && + url.indexOf('transport=udp') !== -1 && + url.indexOf('turn:[') === -1 && + !hasTurn; + + if (validTurn) { + hasTurn = true; + return true; + } + return url.indexOf('stun:') === 0 && + browserDetails.version >= 14393; + }); + + delete server.url; + server.urls = isString ? urls[0] : urls; + return !!urls.length; + } + return false; + }); +} + +var edgeShim = { + shimPeerConnection: function() { + if (window.RTCIceGatherer) { + // ORTC defines an RTCIceCandidate object but no constructor. + // Not implemented in Edge. + if (!window.RTCIceCandidate) { + window.RTCIceCandidate = function(args) { + return args; + }; + } + // ORTC does not have a session description object but + // other browsers (i.e. Chrome) that will support both PC and ORTC + // in the future might have this defined already. + if (!window.RTCSessionDescription) { + window.RTCSessionDescription = function(args) { + return args; + }; + } + // this adds an additional event listener to MediaStrackTrack that signals + // when a tracks enabled property was changed. Workaround for a bug in + // addStream, see below. No longer required in 15025+ + if (browserDetails.version < 15025) { + var origMSTEnabled = Object.getOwnPropertyDescriptor( + MediaStreamTrack.prototype, 'enabled'); + Object.defineProperty(MediaStreamTrack.prototype, 'enabled', { + set: function(value) { + origMSTEnabled.set.call(this, value); + var ev = new Event('enabled'); + ev.enabled = value; + this.dispatchEvent(ev); + } + }); + } + } + + window.RTCPeerConnection = function(config) { + var self = this; + + var _eventTarget = document.createDocumentFragment(); + ['addEventListener', 'removeEventListener', 'dispatchEvent'] + .forEach(function(method) { + self[method] = _eventTarget[method].bind(_eventTarget); + }); + + this.onicecandidate = null; + this.onaddstream = null; + this.ontrack = null; + this.onremovestream = null; + this.onsignalingstatechange = null; + this.oniceconnectionstatechange = null; + this.onicegatheringstatechange = null; + this.onnegotiationneeded = null; + this.ondatachannel = null; + this.canTrickleIceCandidates = null; + + this.localStreams = []; + this.remoteStreams = []; + this.getLocalStreams = function() { + return self.localStreams; + }; + this.getRemoteStreams = function() { + return self.remoteStreams; + }; + + this.localDescription = new RTCSessionDescription({ + type: '', + sdp: '' + }); + this.remoteDescription = new RTCSessionDescription({ + type: '', + sdp: '' + }); + this.signalingState = 'stable'; + this.iceConnectionState = 'new'; + this.iceGatheringState = 'new'; + + this.iceOptions = { + gatherPolicy: 'all', + iceServers: [] + }; + if (config && config.iceTransportPolicy) { + switch (config.iceTransportPolicy) { + case 'all': + case 'relay': + this.iceOptions.gatherPolicy = config.iceTransportPolicy; + break; + default: + // don't set iceTransportPolicy. + break; + } + } + this.usingBundle = config && config.bundlePolicy === 'max-bundle'; + + if (config && config.iceServers) { + this.iceOptions.iceServers = filterIceServers(config.iceServers); + } + this._config = config || {}; + + // per-track iceGathers, iceTransports, dtlsTransports, rtpSenders, ... + // everything that is needed to describe a SDP m-line. + this.transceivers = []; + + // since the iceGatherer is currently created in createOffer but we + // must not emit candidates until after setLocalDescription we buffer + // them in this array. + this._localIceCandidatesBuffer = []; + }; + + window.RTCPeerConnection.prototype._emitGatheringStateChange = function() { + var event = new Event('icegatheringstatechange'); + this.dispatchEvent(event); + if (this.onicegatheringstatechange !== null) { + this.onicegatheringstatechange(event); + } + }; + + window.RTCPeerConnection.prototype._emitBufferedCandidates = function() { + var self = this; + var sections = SDPUtils.splitSections(self.localDescription.sdp); + // FIXME: need to apply ice candidates in a way which is async but + // in-order + this._localIceCandidatesBuffer.forEach(function(event) { + var end = !event.candidate || Object.keys(event.candidate).length === 0; + if (end) { + for (var j = 1; j < sections.length; j++) { + if (sections[j].indexOf('\r\na=end-of-candidates\r\n') === -1) { + sections[j] += 'a=end-of-candidates\r\n'; + } + } + } else { + sections[event.candidate.sdpMLineIndex + 1] += + 'a=' + event.candidate.candidate + '\r\n'; + } + self.localDescription.sdp = sections.join(''); + self.dispatchEvent(event); + if (self.onicecandidate !== null) { + self.onicecandidate(event); + } + if (!event.candidate && self.iceGatheringState !== 'complete') { + var complete = self.transceivers.every(function(transceiver) { + return transceiver.iceGatherer && + transceiver.iceGatherer.state === 'completed'; + }); + if (complete && self.iceGatheringStateChange !== 'complete') { + self.iceGatheringState = 'complete'; + self._emitGatheringStateChange(); + } + } + }); + this._localIceCandidatesBuffer = []; + }; + + window.RTCPeerConnection.prototype.getConfiguration = function() { + return this._config; + }; + + window.RTCPeerConnection.prototype.addStream = function(stream) { + if (browserDetails.version >= 15025) { + this.localStreams.push(stream); + } else { + // Clone is necessary for local demos mostly, attaching directly + // to two different senders does not work (build 10547). + // Fixed in 15025 (or earlier) + var clonedStream = stream.clone(); + stream.getTracks().forEach(function(track, idx) { + var clonedTrack = clonedStream.getTracks()[idx]; + track.addEventListener('enabled', function(event) { + clonedTrack.enabled = event.enabled; + }); + }); + this.localStreams.push(clonedStream); + } + this._maybeFireNegotiationNeeded(); + }; + + window.RTCPeerConnection.prototype.removeStream = function(stream) { + var idx = this.localStreams.indexOf(stream); + if (idx > -1) { + this.localStreams.splice(idx, 1); + this._maybeFireNegotiationNeeded(); + } + }; + + window.RTCPeerConnection.prototype.getSenders = function() { + return this.transceivers.filter(function(transceiver) { + return !!transceiver.rtpSender; + }) + .map(function(transceiver) { + return transceiver.rtpSender; + }); + }; + + window.RTCPeerConnection.prototype.getReceivers = function() { + return this.transceivers.filter(function(transceiver) { + return !!transceiver.rtpReceiver; + }) + .map(function(transceiver) { + return transceiver.rtpReceiver; + }); + }; + + // Determines the intersection of local and remote capabilities. + window.RTCPeerConnection.prototype._getCommonCapabilities = + function(localCapabilities, remoteCapabilities) { + var commonCapabilities = { + codecs: [], + headerExtensions: [], + fecMechanisms: [] + }; + + var findCodecByPayloadType = function(pt, codecs) { + pt = parseInt(pt, 10); + for (var i = 0; i < codecs.length; i++) { + if (codecs[i].payloadType === pt || + codecs[i].preferredPayloadType === pt) { + return codecs[i]; + } + } + }; + + var rtxCapabilityMatches = function(lRtx, rRtx, lCodecs, rCodecs) { + var lCodec = findCodecByPayloadType(lRtx.parameters.apt, lCodecs); + var rCodec = findCodecByPayloadType(rRtx.parameters.apt, rCodecs); + return lCodec && rCodec && + lCodec.name.toLowerCase() === rCodec.name.toLowerCase(); + }; + + localCapabilities.codecs.forEach(function(lCodec) { + for (var i = 0; i < remoteCapabilities.codecs.length; i++) { + var rCodec = remoteCapabilities.codecs[i]; + if (lCodec.name.toLowerCase() === rCodec.name.toLowerCase() && + lCodec.clockRate === rCodec.clockRate) { + if (lCodec.name.toLowerCase() === 'rtx' && + lCodec.parameters && rCodec.parameters.apt) { + // for RTX we need to find the local rtx that has a apt + // which points to the same local codec as the remote one. + if (!rtxCapabilityMatches(lCodec, rCodec, + localCapabilities.codecs, remoteCapabilities.codecs)) { + continue; + } + } + rCodec = JSON.parse(JSON.stringify(rCodec)); // deepcopy + // number of channels is the highest common number of channels + rCodec.numChannels = Math.min(lCodec.numChannels, + rCodec.numChannels); + // push rCodec so we reply with offerer payload type + commonCapabilities.codecs.push(rCodec); + + // determine common feedback mechanisms + rCodec.rtcpFeedback = rCodec.rtcpFeedback.filter(function(fb) { + for (var j = 0; j < lCodec.rtcpFeedback.length; j++) { + if (lCodec.rtcpFeedback[j].type === fb.type && + lCodec.rtcpFeedback[j].parameter === fb.parameter) { + return true; + } + } + return false; + }); + // FIXME: also need to determine .parameters + // see https://github.com/openpeer/ortc/issues/569 + break; + } + } + }); + + localCapabilities.headerExtensions + .forEach(function(lHeaderExtension) { + for (var i = 0; i < remoteCapabilities.headerExtensions.length; + i++) { + var rHeaderExtension = remoteCapabilities.headerExtensions[i]; + if (lHeaderExtension.uri === rHeaderExtension.uri) { + commonCapabilities.headerExtensions.push(rHeaderExtension); + break; + } + } + }); + + // FIXME: fecMechanisms + return commonCapabilities; + }; + + // Create ICE gatherer, ICE transport and DTLS transport. + window.RTCPeerConnection.prototype._createIceAndDtlsTransports = + function(mid, sdpMLineIndex) { + var self = this; + var iceGatherer = new RTCIceGatherer(self.iceOptions); + var iceTransport = new RTCIceTransport(iceGatherer); + iceGatherer.onlocalcandidate = function(evt) { + var event = new Event('icecandidate'); + event.candidate = {sdpMid: mid, sdpMLineIndex: sdpMLineIndex}; + + var cand = evt.candidate; + var end = !cand || Object.keys(cand).length === 0; + // Edge emits an empty object for RTCIceCandidateComplete‥ + if (end) { + // polyfill since RTCIceGatherer.state is not implemented in + // Edge 10547 yet. + if (iceGatherer.state === undefined) { + iceGatherer.state = 'completed'; + } + } else { + // RTCIceCandidate doesn't have a component, needs to be added + cand.component = iceTransport.component === 'RTCP' ? 2 : 1; + event.candidate.candidate = SDPUtils.writeCandidate(cand); + } + + // update local description. + var sections = SDPUtils.splitSections(self.localDescription.sdp); + if (!end) { + sections[event.candidate.sdpMLineIndex + 1] += + 'a=' + event.candidate.candidate + '\r\n'; + } else { + sections[event.candidate.sdpMLineIndex + 1] += + 'a=end-of-candidates\r\n'; + } + self.localDescription.sdp = sections.join(''); + var transceivers = self._pendingOffer ? self._pendingOffer : + self.transceivers; + var complete = transceivers.every(function(transceiver) { + return transceiver.iceGatherer && + transceiver.iceGatherer.state === 'completed'; + }); + + // Emit candidate if localDescription is set. + // Also emits null candidate when all gatherers are complete. + switch (self.iceGatheringState) { + case 'new': + if (!end) { + self._localIceCandidatesBuffer.push(event); + } + if (end && complete) { + self._localIceCandidatesBuffer.push( + new Event('icecandidate')); + } + break; + case 'gathering': + self._emitBufferedCandidates(); + if (!end) { + self.dispatchEvent(event); + if (self.onicecandidate !== null) { + self.onicecandidate(event); + } + } + if (complete) { + self.dispatchEvent(new Event('icecandidate')); + if (self.onicecandidate !== null) { + self.onicecandidate(new Event('icecandidate')); + } + self.iceGatheringState = 'complete'; + self._emitGatheringStateChange(); + } + break; + case 'complete': + // should not happen... currently! + break; + default: // no-op. + break; + } + }; + iceTransport.onicestatechange = function() { + self._updateConnectionState(); + }; + + var dtlsTransport = new RTCDtlsTransport(iceTransport); + dtlsTransport.ondtlsstatechange = function() { + self._updateConnectionState(); + }; + dtlsTransport.onerror = function() { + // onerror does not set state to failed by itself. + dtlsTransport.state = 'failed'; + self._updateConnectionState(); + }; + + return { + iceGatherer: iceGatherer, + iceTransport: iceTransport, + dtlsTransport: dtlsTransport + }; + }; + + // Destroy ICE gatherer, ICE transport and DTLS transport. + // Without triggering the callbacks. + window.RTCPeerConnection.prototype._disposeIceAndDtlsTransports = + function(sdpMLineIndex) { + var iceGatherer = this.transceivers[sdpMLineIndex].iceGatherer; + if (iceGatherer) { + delete iceGatherer.onlocalcandidate; + delete this.transceivers[sdpMLineIndex].iceGatherer; + } + var iceTransport = this.transceivers[sdpMLineIndex].iceTransport; + if (iceTransport) { + delete iceTransport.onicestatechange; + delete this.transceivers[sdpMLineIndex].iceTransport; + } + var dtlsTransport = this.transceivers[sdpMLineIndex].dtlsTransport; + if (dtlsTransport) { + delete dtlsTransport.ondtlssttatechange; + delete dtlsTransport.onerror; + delete this.transceivers[sdpMLineIndex].dtlsTransport; + } + }; + + + // Start the RTP Sender and Receiver for a transceiver. + window.RTCPeerConnection.prototype._transceive = function(transceiver, + send, recv) { + var params = this._getCommonCapabilities(transceiver.localCapabilities, + transceiver.remoteCapabilities); + if (send && transceiver.rtpSender) { + params.encodings = transceiver.sendEncodingParameters; + params.rtcp = { + cname: SDPUtils.localCName, + compound: transceiver.rtcpParameters.compound + }; + if (transceiver.recvEncodingParameters.length) { + params.rtcp.ssrc = transceiver.recvEncodingParameters[0].ssrc; + } + transceiver.rtpSender.send(params); + } + if (recv && transceiver.rtpReceiver) { + // remove RTX field in Edge 14942 + if (transceiver.kind === 'video' + && transceiver.recvEncodingParameters + && browserDetails.version < 15019) { + transceiver.recvEncodingParameters.forEach(function(p) { + delete p.rtx; + }); + } + params.encodings = transceiver.recvEncodingParameters; + params.rtcp = { + cname: transceiver.rtcpParameters.cname, + compound: transceiver.rtcpParameters.compound + }; + if (transceiver.sendEncodingParameters.length) { + params.rtcp.ssrc = transceiver.sendEncodingParameters[0].ssrc; + } + transceiver.rtpReceiver.receive(params); + } + }; + + window.RTCPeerConnection.prototype.setLocalDescription = + function(description) { + var self = this; + var sections; + var sessionpart; + if (description.type === 'offer') { + // FIXME: What was the purpose of this empty if statement? + // if (!this._pendingOffer) { + // } else { + if (this._pendingOffer) { + // VERY limited support for SDP munging. Limited to: + // * changing the order of codecs + sections = SDPUtils.splitSections(description.sdp); + sessionpart = sections.shift(); + sections.forEach(function(mediaSection, sdpMLineIndex) { + var caps = SDPUtils.parseRtpParameters(mediaSection); + self._pendingOffer[sdpMLineIndex].localCapabilities = caps; + }); + this.transceivers = this._pendingOffer; + delete this._pendingOffer; + } + } else if (description.type === 'answer') { + sections = SDPUtils.splitSections(self.remoteDescription.sdp); + sessionpart = sections.shift(); + var isIceLite = SDPUtils.matchPrefix(sessionpart, + 'a=ice-lite').length > 0; + sections.forEach(function(mediaSection, sdpMLineIndex) { + var transceiver = self.transceivers[sdpMLineIndex]; + var iceGatherer = transceiver.iceGatherer; + var iceTransport = transceiver.iceTransport; + var dtlsTransport = transceiver.dtlsTransport; + var localCapabilities = transceiver.localCapabilities; + var remoteCapabilities = transceiver.remoteCapabilities; + + var rejected = mediaSection.split('\n', 1)[0] + .split(' ', 2)[1] === '0'; + + if (!rejected && !transceiver.isDatachannel) { + var remoteIceParameters = SDPUtils.getIceParameters( + mediaSection, sessionpart); + var remoteDtlsParameters = SDPUtils.getDtlsParameters( + mediaSection, sessionpart); + if (isIceLite) { + remoteDtlsParameters.role = 'server'; + } + + if (!self.usingBundle || sdpMLineIndex === 0) { + iceTransport.start(iceGatherer, remoteIceParameters, + isIceLite ? 'controlling' : 'controlled'); + dtlsTransport.start(remoteDtlsParameters); + } + + // Calculate intersection of capabilities. + var params = self._getCommonCapabilities(localCapabilities, + remoteCapabilities); + + // Start the RTCRtpSender. The RTCRtpReceiver for this + // transceiver has already been started in setRemoteDescription. + self._transceive(transceiver, + params.codecs.length > 0, + false); + } + }); + } + + this.localDescription = { + type: description.type, + sdp: description.sdp + }; + switch (description.type) { + case 'offer': + this._updateSignalingState('have-local-offer'); + break; + case 'answer': + this._updateSignalingState('stable'); + break; + default: + throw new TypeError('unsupported type "' + description.type + + '"'); + } + + // If a success callback was provided, emit ICE candidates after it + // has been executed. Otherwise, emit callback after the Promise is + // resolved. + var hasCallback = arguments.length > 1 && + typeof arguments[1] === 'function'; + if (hasCallback) { + var cb = arguments[1]; + window.setTimeout(function() { + cb(); + if (self.iceGatheringState === 'new') { + self.iceGatheringState = 'gathering'; + self._emitGatheringStateChange(); + } + self._emitBufferedCandidates(); + }, 0); + } + var p = Promise.resolve(); + p.then(function() { + if (!hasCallback) { + if (self.iceGatheringState === 'new') { + self.iceGatheringState = 'gathering'; + self._emitGatheringStateChange(); + } + // Usually candidates will be emitted earlier. + window.setTimeout(self._emitBufferedCandidates.bind(self), 500); + } + }); + return p; + }; + + window.RTCPeerConnection.prototype.setRemoteDescription = + function(description) { + var self = this; + var streams = {}; + var receiverList = []; + var sections = SDPUtils.splitSections(description.sdp); + var sessionpart = sections.shift(); + var isIceLite = SDPUtils.matchPrefix(sessionpart, + 'a=ice-lite').length > 0; + var usingBundle = SDPUtils.matchPrefix(sessionpart, + 'a=group:BUNDLE ').length > 0; + var iceOptions = SDPUtils.matchPrefix(sessionpart, + 'a=ice-options:')[0]; + if (iceOptions) { + this.canTrickleIceCandidates = iceOptions.substr(14).split(' ') + .indexOf('trickle') >= 0; + } else { + this.canTrickleIceCandidates = false; + } + + sections.forEach(function(mediaSection, sdpMLineIndex) { + var lines = SDPUtils.splitLines(mediaSection); + var mline = lines[0].substr(2).split(' '); + var kind = mline[0]; + var rejected = mline[1] === '0'; + var direction = SDPUtils.getDirection(mediaSection, sessionpart); + var remoteMsid = SDPUtils.parseMsid(mediaSection); + + var mid = SDPUtils.matchPrefix(mediaSection, 'a=mid:'); + if (mid.length) { + mid = mid[0].substr(6); + } else { + mid = SDPUtils.generateIdentifier(); + } + + // Reject datachannels which are not implemented yet. + if (kind === 'application' && mline[2] === 'DTLS/SCTP') { + self.transceivers[sdpMLineIndex] = { + mid: mid, + isDatachannel: true + }; + return; + } + + var transceiver; + var iceGatherer; + var iceTransport; + var dtlsTransport; + var rtpSender; + var rtpReceiver; + var sendEncodingParameters; + var recvEncodingParameters; + var localCapabilities; + + var track; + // FIXME: ensure the mediaSection has rtcp-mux set. + var remoteCapabilities = SDPUtils.parseRtpParameters(mediaSection); + var remoteIceParameters; + var remoteDtlsParameters; + if (!rejected) { + remoteIceParameters = SDPUtils.getIceParameters(mediaSection, + sessionpart); + remoteDtlsParameters = SDPUtils.getDtlsParameters(mediaSection, + sessionpart); + remoteDtlsParameters.role = 'client'; + } + recvEncodingParameters = + SDPUtils.parseRtpEncodingParameters(mediaSection); + + var rtcpParameters = SDPUtils.parseRtcpParameters(mediaSection); + + var isComplete = SDPUtils.matchPrefix(mediaSection, + 'a=end-of-candidates', sessionpart).length > 0; + var cands = SDPUtils.matchPrefix(mediaSection, 'a=candidate:') + .map(function(cand) { + return SDPUtils.parseCandidate(cand); + }) + .filter(function(cand) { + return cand.component === '1'; + }); + if (description.type === 'offer' && !rejected) { + var transports = usingBundle && sdpMLineIndex > 0 ? { + iceGatherer: self.transceivers[0].iceGatherer, + iceTransport: self.transceivers[0].iceTransport, + dtlsTransport: self.transceivers[0].dtlsTransport + } : self._createIceAndDtlsTransports(mid, sdpMLineIndex); + + if (isComplete && (!usingBundle || sdpMLineIndex === 0)) { + transports.iceTransport.setRemoteCandidates(cands); + } + + localCapabilities = RTCRtpReceiver.getCapabilities(kind); + + // filter RTX until additional stuff needed for RTX is implemented + // in adapter.js + if (browserDetails.version < 15019) { + localCapabilities.codecs = localCapabilities.codecs.filter( + function(codec) { + return codec.name !== 'rtx'; + }); + } + + sendEncodingParameters = [{ + ssrc: (2 * sdpMLineIndex + 2) * 1001 + }]; + + if (direction === 'sendrecv' || direction === 'sendonly') { + rtpReceiver = new RTCRtpReceiver(transports.dtlsTransport, + kind); + + track = rtpReceiver.track; + // FIXME: does not work with Plan B. + if (remoteMsid) { + if (!streams[remoteMsid.stream]) { + streams[remoteMsid.stream] = new MediaStream(); + Object.defineProperty(streams[remoteMsid.stream], 'id', { + get: function() { + return remoteMsid.stream; + } + }); + } + Object.defineProperty(track, 'id', { + get: function() { + return remoteMsid.track; + } + }); + streams[remoteMsid.stream].addTrack(track); + receiverList.push([track, rtpReceiver, + streams[remoteMsid.stream]]); + } else { + if (!streams.default) { + streams.default = new MediaStream(); + } + streams.default.addTrack(track); + receiverList.push([track, rtpReceiver, streams.default]); + } + } + + // FIXME: look at direction. + if (self.localStreams.length > 0 && + self.localStreams[0].getTracks().length >= sdpMLineIndex) { + var localTrack; + if (kind === 'audio') { + localTrack = self.localStreams[0].getAudioTracks()[0]; + } else if (kind === 'video') { + localTrack = self.localStreams[0].getVideoTracks()[0]; + } + if (localTrack) { + // add RTX + if (browserDetails.version >= 15019 && kind === 'video') { + sendEncodingParameters[0].rtx = { + ssrc: (2 * sdpMLineIndex + 2) * 1001 + 1 + }; + } + rtpSender = new RTCRtpSender(localTrack, + transports.dtlsTransport); + } + } + + self.transceivers[sdpMLineIndex] = { + iceGatherer: transports.iceGatherer, + iceTransport: transports.iceTransport, + dtlsTransport: transports.dtlsTransport, + localCapabilities: localCapabilities, + remoteCapabilities: remoteCapabilities, + rtpSender: rtpSender, + rtpReceiver: rtpReceiver, + kind: kind, + mid: mid, + rtcpParameters: rtcpParameters, + sendEncodingParameters: sendEncodingParameters, + recvEncodingParameters: recvEncodingParameters + }; + // Start the RTCRtpReceiver now. The RTPSender is started in + // setLocalDescription. + self._transceive(self.transceivers[sdpMLineIndex], + false, + direction === 'sendrecv' || direction === 'sendonly'); + } else if (description.type === 'answer' && !rejected) { + if (usingBundle && sdpMLineIndex > 0) { + self._disposeIceAndDtlsTransports(sdpMLineIndex); + self.transceivers[sdpMLineIndex].iceGatherer = + self.transceivers[0].iceGatherer; + self.transceivers[sdpMLineIndex].iceTransport = + self.transceivers[0].iceTransport; + self.transceivers[sdpMLineIndex].dtlsTransport = + self.transceivers[0].dtlsTransport; + } + transceiver = self.transceivers[sdpMLineIndex]; + iceGatherer = transceiver.iceGatherer; + iceTransport = transceiver.iceTransport; + dtlsTransport = transceiver.dtlsTransport; + rtpSender = transceiver.rtpSender; + rtpReceiver = transceiver.rtpReceiver; + sendEncodingParameters = transceiver.sendEncodingParameters; + localCapabilities = transceiver.localCapabilities; + + self.transceivers[sdpMLineIndex].recvEncodingParameters = + recvEncodingParameters; + self.transceivers[sdpMLineIndex].remoteCapabilities = + remoteCapabilities; + self.transceivers[sdpMLineIndex].rtcpParameters = rtcpParameters; + + if ((isIceLite || isComplete) && cands.length) { + iceTransport.setRemoteCandidates(cands); + } + if (!usingBundle || sdpMLineIndex === 0) { + iceTransport.start(iceGatherer, remoteIceParameters, + 'controlling'); + dtlsTransport.start(remoteDtlsParameters); + } + + self._transceive(transceiver, + direction === 'sendrecv' || direction === 'recvonly', + direction === 'sendrecv' || direction === 'sendonly'); + + if (rtpReceiver && + (direction === 'sendrecv' || direction === 'sendonly')) { + track = rtpReceiver.track; + receiverList.push([track, rtpReceiver]); + if (remoteMsid) { + if (!streams[remoteMsid.stream]) { + streams[remoteMsid.stream] = new MediaStream(); + } + streams[remoteMsid.stream].addTrack(track); + } else { + if (!streams.default) { + streams.default = new MediaStream(); + } + streams.default.addTrack(track); + } + } else { + // FIXME: actually the receiver should be created later. + delete transceiver.rtpReceiver; + } + } + }); + this.usingBundle = usingBundle; + + this.remoteDescription = { + type: description.type, + sdp: description.sdp + }; + switch (description.type) { + case 'offer': + this._updateSignalingState('have-remote-offer'); + break; + case 'answer': + this._updateSignalingState('stable'); + break; + default: + throw new TypeError('unsupported type "' + description.type + + '"'); + } + Object.keys(streams).forEach(function(sid) { + var stream = streams[sid]; + if (stream.getTracks().length) { + self.remoteStreams.push(stream); + var event = new Event('addstream'); + event.stream = stream; + self.dispatchEvent(event); + if (self.onaddstream !== null) { + window.setTimeout(function() { + self.onaddstream(event); + }, 0); + } + + receiverList.forEach(function(item) { + var track = item[0]; + var receiver = item[1]; + if (stream.id !== item[2].id) { + return; + } + var trackEvent = new Event('track'); + trackEvent.track = track; + trackEvent.receiver = receiver; + trackEvent.streams = [stream]; + self.dispatchEvent(trackEvent); + if (self.ontrack !== null) { + window.setTimeout(function() { + self.ontrack(trackEvent); + }, 0); + } + }); + } + }); + if (arguments.length > 1 && typeof arguments[1] === 'function') { + window.setTimeout(arguments[1], 0); + } + return Promise.resolve(); + }; + + window.RTCPeerConnection.prototype.close = function() { + this.transceivers.forEach(function(transceiver) { + /* not yet + if (transceiver.iceGatherer) { + transceiver.iceGatherer.close(); + } + */ + if (transceiver.iceTransport) { + transceiver.iceTransport.stop(); + } + if (transceiver.dtlsTransport) { + transceiver.dtlsTransport.stop(); + } + if (transceiver.rtpSender) { + transceiver.rtpSender.stop(); + } + if (transceiver.rtpReceiver) { + transceiver.rtpReceiver.stop(); + } + }); + // FIXME: clean up tracks, local streams, remote streams, etc + this._updateSignalingState('closed'); + }; + + // Update the signaling state. + window.RTCPeerConnection.prototype._updateSignalingState = + function(newState) { + this.signalingState = newState; + var event = new Event('signalingstatechange'); + this.dispatchEvent(event); + if (this.onsignalingstatechange !== null) { + this.onsignalingstatechange(event); + } + }; + + // Determine whether to fire the negotiationneeded event. + window.RTCPeerConnection.prototype._maybeFireNegotiationNeeded = + function() { + // Fire away (for now). + var event = new Event('negotiationneeded'); + this.dispatchEvent(event); + if (this.onnegotiationneeded !== null) { + this.onnegotiationneeded(event); + } + }; + + // Update the connection state. + window.RTCPeerConnection.prototype._updateConnectionState = function() { + var self = this; + var newState; + var states = { + 'new': 0, + closed: 0, + connecting: 0, + checking: 0, + connected: 0, + completed: 0, + failed: 0 + }; + this.transceivers.forEach(function(transceiver) { + states[transceiver.iceTransport.state]++; + states[transceiver.dtlsTransport.state]++; + }); + // ICETransport.completed and connected are the same for this purpose. + states.connected += states.completed; + + newState = 'new'; + if (states.failed > 0) { + newState = 'failed'; + } else if (states.connecting > 0 || states.checking > 0) { + newState = 'connecting'; + } else if (states.disconnected > 0) { + newState = 'disconnected'; + } else if (states.new > 0) { + newState = 'new'; + } else if (states.connected > 0 || states.completed > 0) { + newState = 'connected'; + } + + if (newState !== self.iceConnectionState) { + self.iceConnectionState = newState; + var event = new Event('iceconnectionstatechange'); + this.dispatchEvent(event); + if (this.oniceconnectionstatechange !== null) { + this.oniceconnectionstatechange(event); + } + } + }; + + window.RTCPeerConnection.prototype.createOffer = function() { + var self = this; + if (this._pendingOffer) { + throw new Error('createOffer called while there is a pending offer.'); + } + var offerOptions; + if (arguments.length === 1 && typeof arguments[0] !== 'function') { + offerOptions = arguments[0]; + } else if (arguments.length === 3) { + offerOptions = arguments[2]; + } + + var tracks = []; + var numAudioTracks = 0; + var numVideoTracks = 0; + // Default to sendrecv. + if (this.localStreams.length) { + numAudioTracks = this.localStreams.reduce(function(numTracks, stream) { + return numTracks + stream.getAudioTracks().length; + }, 0); + numVideoTracks = this.localStreams.reduce(function(numTracks, stream) { + return numTracks + stream.getVideoTracks().length; + }, 0); + } + // Determine number of audio and video tracks we need to send/recv. + if (offerOptions) { + // Reject Chrome legacy constraints. + if (offerOptions.mandatory || offerOptions.optional) { + throw new TypeError( + 'Legacy mandatory/optional constraints not supported.'); + } + if (offerOptions.offerToReceiveAudio !== undefined) { + numAudioTracks = offerOptions.offerToReceiveAudio; + } + if (offerOptions.offerToReceiveVideo !== undefined) { + numVideoTracks = offerOptions.offerToReceiveVideo; + } + } + + // Push local streams. + this.localStreams.forEach(function(localStream) { + localStream.getTracks().forEach(function(track) { + tracks.push({ + kind: track.kind, + track: track, + stream: localStream, + wantReceive: track.kind === 'audio' ? + numAudioTracks > 0 : numVideoTracks > 0 + }); + if (track.kind === 'audio') { + numAudioTracks--; + } else if (track.kind === 'video') { + numVideoTracks--; + } + }); + }); + + // Create M-lines for recvonly streams. + while (numAudioTracks > 0 || numVideoTracks > 0) { + if (numAudioTracks > 0) { + tracks.push({ + kind: 'audio', + wantReceive: true + }); + numAudioTracks--; + } + if (numVideoTracks > 0) { + tracks.push({ + kind: 'video', + wantReceive: true + }); + numVideoTracks--; + } + } + // reorder tracks + tracks = sortTracks(tracks); + + var sdp = SDPUtils.writeSessionBoilerplate(); + var transceivers = []; + tracks.forEach(function(mline, sdpMLineIndex) { + // For each track, create an ice gatherer, ice transport, + // dtls transport, potentially rtpsender and rtpreceiver. + var track = mline.track; + var kind = mline.kind; + var mid = SDPUtils.generateIdentifier(); + + var transports = self.usingBundle && sdpMLineIndex > 0 ? { + iceGatherer: transceivers[0].iceGatherer, + iceTransport: transceivers[0].iceTransport, + dtlsTransport: transceivers[0].dtlsTransport + } : self._createIceAndDtlsTransports(mid, sdpMLineIndex); + + var localCapabilities = RTCRtpSender.getCapabilities(kind); + // filter RTX until additional stuff needed for RTX is implemented + // in adapter.js + if (browserDetails.version < 15019) { + localCapabilities.codecs = localCapabilities.codecs.filter( + function(codec) { + return codec.name !== 'rtx'; + }); + } + localCapabilities.codecs.forEach(function(codec) { + // work around https://bugs.chromium.org/p/webrtc/issues/detail?id=6552 + // by adding level-asymmetry-allowed=1 + if (codec.name === 'H264' && + codec.parameters['level-asymmetry-allowed'] === undefined) { + codec.parameters['level-asymmetry-allowed'] = '1'; + } + }); + + var rtpSender; + var rtpReceiver; + + // generate an ssrc now, to be used later in rtpSender.send + var sendEncodingParameters = [{ + ssrc: (2 * sdpMLineIndex + 1) * 1001 + }]; + if (track) { + // add RTX + if (browserDetails.version >= 15019 && kind === 'video') { + sendEncodingParameters[0].rtx = { + ssrc: (2 * sdpMLineIndex + 1) * 1001 + 1 + }; + } + rtpSender = new RTCRtpSender(track, transports.dtlsTransport); + } + + if (mline.wantReceive) { + rtpReceiver = new RTCRtpReceiver(transports.dtlsTransport, kind); + } + + transceivers[sdpMLineIndex] = { + iceGatherer: transports.iceGatherer, + iceTransport: transports.iceTransport, + dtlsTransport: transports.dtlsTransport, + localCapabilities: localCapabilities, + remoteCapabilities: null, + rtpSender: rtpSender, + rtpReceiver: rtpReceiver, + kind: kind, + mid: mid, + sendEncodingParameters: sendEncodingParameters, + recvEncodingParameters: null + }; + }); + + // always offer BUNDLE and dispose on return if not supported. + if (this._config.bundlePolicy !== 'max-compat') { + sdp += 'a=group:BUNDLE ' + transceivers.map(function(t) { + return t.mid; + }).join(' ') + '\r\n'; + } + sdp += 'a=ice-options:trickle\r\n'; + + tracks.forEach(function(mline, sdpMLineIndex) { + var transceiver = transceivers[sdpMLineIndex]; + sdp += SDPUtils.writeMediaSection(transceiver, + transceiver.localCapabilities, 'offer', mline.stream); + sdp += 'a=rtcp-rsize\r\n'; + }); + + this._pendingOffer = transceivers; + var desc = new RTCSessionDescription({ + type: 'offer', + sdp: sdp + }); + if (arguments.length && typeof arguments[0] === 'function') { + window.setTimeout(arguments[0], 0, desc); + } + return Promise.resolve(desc); + }; + + window.RTCPeerConnection.prototype.createAnswer = function() { + var self = this; + + var sdp = SDPUtils.writeSessionBoilerplate(); + if (this.usingBundle) { + sdp += 'a=group:BUNDLE ' + this.transceivers.map(function(t) { + return t.mid; + }).join(' ') + '\r\n'; + } + this.transceivers.forEach(function(transceiver) { + if (transceiver.isDatachannel) { + sdp += 'm=application 0 DTLS/SCTP 5000\r\n' + + 'c=IN IP4 0.0.0.0\r\n' + + 'a=mid:' + transceiver.mid + '\r\n'; + return; + } + // Calculate intersection of capabilities. + var commonCapabilities = self._getCommonCapabilities( + transceiver.localCapabilities, + transceiver.remoteCapabilities); + + var hasRtx = commonCapabilities.codecs.filter(function(c) { + return c.name.toLowerCase() === 'rtx'; + }).length; + if (!hasRtx && transceiver.sendEncodingParameters[0].rtx) { + delete transceiver.sendEncodingParameters[0].rtx; + } + + sdp += SDPUtils.writeMediaSection(transceiver, commonCapabilities, + 'answer', self.localStreams[0]); + if (transceiver.rtcpParameters && + transceiver.rtcpParameters.reducedSize) { + sdp += 'a=rtcp-rsize\r\n'; + } + }); + + var desc = new RTCSessionDescription({ + type: 'answer', + sdp: sdp + }); + if (arguments.length && typeof arguments[0] === 'function') { + window.setTimeout(arguments[0], 0, desc); + } + return Promise.resolve(desc); + }; + + window.RTCPeerConnection.prototype.addIceCandidate = function(candidate) { + if (!candidate) { + for (var j = 0; j < this.transceivers.length; j++) { + this.transceivers[j].iceTransport.addRemoteCandidate({}); + if (this.usingBundle) { + return Promise.resolve(); + } + } + } else { + var mLineIndex = candidate.sdpMLineIndex; + if (candidate.sdpMid) { + for (var i = 0; i < this.transceivers.length; i++) { + if (this.transceivers[i].mid === candidate.sdpMid) { + mLineIndex = i; + break; + } + } + } + var transceiver = this.transceivers[mLineIndex]; + if (transceiver) { + var cand = Object.keys(candidate.candidate).length > 0 ? + SDPUtils.parseCandidate(candidate.candidate) : {}; + // Ignore Chrome's invalid candidates since Edge does not like them. + if (cand.protocol === 'tcp' && (cand.port === 0 || cand.port === 9)) { + return Promise.resolve(); + } + // Ignore RTCP candidates, we assume RTCP-MUX. + if (cand.component !== '1') { + return Promise.resolve(); + } + transceiver.iceTransport.addRemoteCandidate(cand); + + // update the remoteDescription. + var sections = SDPUtils.splitSections(this.remoteDescription.sdp); + sections[mLineIndex + 1] += (cand.type ? candidate.candidate.trim() + : 'a=end-of-candidates') + '\r\n'; + this.remoteDescription.sdp = sections.join(''); + } + } + if (arguments.length > 1 && typeof arguments[1] === 'function') { + window.setTimeout(arguments[1], 0); + } + return Promise.resolve(); + }; + + window.RTCPeerConnection.prototype.getStats = function() { + var promises = []; + this.transceivers.forEach(function(transceiver) { + ['rtpSender', 'rtpReceiver', 'iceGatherer', 'iceTransport', + 'dtlsTransport'].forEach(function(method) { + if (transceiver[method]) { + promises.push(transceiver[method].getStats()); + } + }); + }); + var cb = arguments.length > 1 && typeof arguments[1] === 'function' && + arguments[1]; + var fixStatsType = function(stat) { + return { + inboundrtp: 'inbound-rtp', + outboundrtp: 'outbound-rtp', + candidatepair: 'candidate-pair', + localcandidate: 'local-candidate', + remotecandidate: 'remote-candidate' + }[stat.type] || stat.type; + }; + return new Promise(function(resolve) { + // shim getStats with maplike support + var results = new Map(); + Promise.all(promises).then(function(res) { + res.forEach(function(result) { + Object.keys(result).forEach(function(id) { + result[id].type = fixStatsType(result[id]); + results.set(id, result[id]); + }); + }); + if (cb) { + window.setTimeout(cb, 0, results); + } + resolve(results); + }); + }); + }; + }, + shimReplaceTrack: function() { + // ORTC has replaceTrack -- https://github.com/w3c/ortc/issues/614 + if (window.RTCRtpSender && !('replaceTrack' in RTCRtpSender.prototype)) { + RTCRtpSender.prototype.replaceTrack = RTCRtpSender.prototype.setTrack; + } + } +}; + +// Expose public methods. +module.exports = { + shimPeerConnection: edgeShim.shimPeerConnection, + shimGetUserMedia: require('./getusermedia'), + shimReplaceTrack: edgeShim.shimReplaceTrack +}; + +},{"../utils":10,"./getusermedia":6,"sdp":1}],6:[function(require,module,exports){ +/* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + /* eslint-env node */ +'use strict'; + +// Expose public methods. +module.exports = function() { + var shimError_ = function(e) { + return { + name: {PermissionDeniedError: 'NotAllowedError'}[e.name] || e.name, + message: e.message, + constraint: e.constraint, + toString: function() { + return this.name; + } + }; + }; + + // getUserMedia error shim. + var origGetUserMedia = navigator.mediaDevices.getUserMedia. + bind(navigator.mediaDevices); + navigator.mediaDevices.getUserMedia = function(c) { + return origGetUserMedia(c).catch(function(e) { + return Promise.reject(shimError_(e)); + }); + }; +}; + +},{}],7:[function(require,module,exports){ +/* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + /* eslint-env node */ +'use strict'; + +var browserDetails = require('../utils').browserDetails; + +var firefoxShim = { + shimOnTrack: function() { + if (typeof window === 'object' && window.RTCPeerConnection && !('ontrack' in + window.RTCPeerConnection.prototype)) { + Object.defineProperty(window.RTCPeerConnection.prototype, 'ontrack', { + get: function() { + return this._ontrack; + }, + set: function(f) { + if (this._ontrack) { + this.removeEventListener('track', this._ontrack); + this.removeEventListener('addstream', this._ontrackpoly); + } + this.addEventListener('track', this._ontrack = f); + this.addEventListener('addstream', this._ontrackpoly = function(e) { + e.stream.getTracks().forEach(function(track) { + var event = new Event('track'); + event.track = track; + event.receiver = {track: track}; + event.streams = [e.stream]; + this.dispatchEvent(event); + }.bind(this)); + }.bind(this)); + } + }); + } + }, + + shimSourceObject: function() { + // Firefox has supported mozSrcObject since FF22, unprefixed in 42. + if (typeof window === 'object') { + if (window.HTMLMediaElement && + !('srcObject' in window.HTMLMediaElement.prototype)) { + // Shim the srcObject property, once, when HTMLMediaElement is found. + Object.defineProperty(window.HTMLMediaElement.prototype, 'srcObject', { + get: function() { + return this.mozSrcObject; + }, + set: function(stream) { + this.mozSrcObject = stream; + } + }); + } + } + }, + + shimPeerConnection: function() { + if (typeof window !== 'object' || !(window.RTCPeerConnection || + window.mozRTCPeerConnection)) { + return; // probably media.peerconnection.enabled=false in about:config + } + // The RTCPeerConnection object. + if (!window.RTCPeerConnection) { + window.RTCPeerConnection = function(pcConfig, pcConstraints) { + if (browserDetails.version < 38) { + // .urls is not supported in FF < 38. + // create RTCIceServers with a single url. + if (pcConfig && pcConfig.iceServers) { + var newIceServers = []; + for (var i = 0; i < pcConfig.iceServers.length; i++) { + var server = pcConfig.iceServers[i]; + if (server.hasOwnProperty('urls')) { + for (var j = 0; j < server.urls.length; j++) { + var newServer = { + url: server.urls[j] + }; + if (server.urls[j].indexOf('turn') === 0) { + newServer.username = server.username; + newServer.credential = server.credential; + } + newIceServers.push(newServer); + } + } else { + newIceServers.push(pcConfig.iceServers[i]); + } + } + pcConfig.iceServers = newIceServers; + } + } + return new mozRTCPeerConnection(pcConfig, pcConstraints); + }; + window.RTCPeerConnection.prototype = mozRTCPeerConnection.prototype; + + // wrap static methods. Currently just generateCertificate. + if (mozRTCPeerConnection.generateCertificate) { + Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', { + get: function() { + return mozRTCPeerConnection.generateCertificate; + } + }); + } + + window.RTCSessionDescription = mozRTCSessionDescription; + window.RTCIceCandidate = mozRTCIceCandidate; + } + + // shim away need for obsolete RTCIceCandidate/RTCSessionDescription. + ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate'] + .forEach(function(method) { + var nativeMethod = RTCPeerConnection.prototype[method]; + RTCPeerConnection.prototype[method] = function() { + arguments[0] = new ((method === 'addIceCandidate') ? + RTCIceCandidate : RTCSessionDescription)(arguments[0]); + return nativeMethod.apply(this, arguments); + }; + }); + + // support for addIceCandidate(null or undefined) + var nativeAddIceCandidate = + RTCPeerConnection.prototype.addIceCandidate; + RTCPeerConnection.prototype.addIceCandidate = function() { + if (!arguments[0]) { + if (arguments[1]) { + arguments[1].apply(null); + } + return Promise.resolve(); + } + return nativeAddIceCandidate.apply(this, arguments); + }; + + // shim getStats with maplike support + var makeMapStats = function(stats) { + var map = new Map(); + Object.keys(stats).forEach(function(key) { + map.set(key, stats[key]); + map[key] = stats[key]; + }); + return map; + }; + + var modernStatsTypes = { + inboundrtp: 'inbound-rtp', + outboundrtp: 'outbound-rtp', + candidatepair: 'candidate-pair', + localcandidate: 'local-candidate', + remotecandidate: 'remote-candidate' + }; + + var nativeGetStats = RTCPeerConnection.prototype.getStats; + RTCPeerConnection.prototype.getStats = function(selector, onSucc, onErr) { + return nativeGetStats.apply(this, [selector || null]) + .then(function(stats) { + if (browserDetails.version < 48) { + stats = makeMapStats(stats); + } + if (browserDetails.version < 53 && !onSucc) { + // Shim only promise getStats with spec-hyphens in type names + // Leave callback version alone; misc old uses of forEach before Map + try { + stats.forEach(function(stat) { + stat.type = modernStatsTypes[stat.type] || stat.type; + }); + } catch (e) { + if (e.name !== 'TypeError') { + throw e; + } + // Avoid TypeError: "type" is read-only, in old versions. 34-43ish + stats.forEach(function(stat, i) { + stats.set(i, Object.assign({}, stat, { + type: modernStatsTypes[stat.type] || stat.type + })); + }); + } + } + return stats; + }) + .then(onSucc, onErr); + }; + } +}; + +// Expose public methods. +module.exports = { + shimOnTrack: firefoxShim.shimOnTrack, + shimSourceObject: firefoxShim.shimSourceObject, + shimPeerConnection: firefoxShim.shimPeerConnection, + shimGetUserMedia: require('./getusermedia') +}; + +},{"../utils":10,"./getusermedia":8}],8:[function(require,module,exports){ +/* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + /* eslint-env node */ +'use strict'; + +var logging = require('../utils').log; +var browserDetails = require('../utils').browserDetails; + +// Expose public methods. +module.exports = function() { + var shimError_ = function(e) { + return { + name: { + NotSupportedError: 'TypeError', + SecurityError: 'NotAllowedError', + PermissionDeniedError: 'NotAllowedError' + }[e.name] || e.name, + message: { + 'The operation is insecure.': 'The request is not allowed by the ' + + 'user agent or the platform in the current context.' + }[e.message] || e.message, + constraint: e.constraint, + toString: function() { + return this.name + (this.message && ': ') + this.message; + } + }; + }; + + // getUserMedia constraints shim. + var getUserMedia_ = function(constraints, onSuccess, onError) { + var constraintsToFF37_ = function(c) { + if (typeof c !== 'object' || c.require) { + return c; + } + var require = []; + Object.keys(c).forEach(function(key) { + if (key === 'require' || key === 'advanced' || key === 'mediaSource') { + return; + } + var r = c[key] = (typeof c[key] === 'object') ? + c[key] : {ideal: c[key]}; + if (r.min !== undefined || + r.max !== undefined || r.exact !== undefined) { + require.push(key); + } + if (r.exact !== undefined) { + if (typeof r.exact === 'number') { + r. min = r.max = r.exact; + } else { + c[key] = r.exact; + } + delete r.exact; + } + if (r.ideal !== undefined) { + c.advanced = c.advanced || []; + var oc = {}; + if (typeof r.ideal === 'number') { + oc[key] = {min: r.ideal, max: r.ideal}; + } else { + oc[key] = r.ideal; + } + c.advanced.push(oc); + delete r.ideal; + if (!Object.keys(r).length) { + delete c[key]; + } + } + }); + if (require.length) { + c.require = require; + } + return c; + }; + constraints = JSON.parse(JSON.stringify(constraints)); + if (browserDetails.version < 38) { + logging('spec: ' + JSON.stringify(constraints)); + if (constraints.audio) { + constraints.audio = constraintsToFF37_(constraints.audio); + } + if (constraints.video) { + constraints.video = constraintsToFF37_(constraints.video); + } + logging('ff37: ' + JSON.stringify(constraints)); + } + return navigator.mozGetUserMedia(constraints, onSuccess, function(e) { + onError(shimError_(e)); + }); + }; + + // Returns the result of getUserMedia as a Promise. + var getUserMediaPromise_ = function(constraints) { + return new Promise(function(resolve, reject) { + getUserMedia_(constraints, resolve, reject); + }); + }; + + // Shim for mediaDevices on older versions. + if (!navigator.mediaDevices) { + navigator.mediaDevices = {getUserMedia: getUserMediaPromise_, + addEventListener: function() { }, + removeEventListener: function() { } + }; + } + navigator.mediaDevices.enumerateDevices = + navigator.mediaDevices.enumerateDevices || function() { + return new Promise(function(resolve) { + var infos = [ + {kind: 'audioinput', deviceId: 'default', label: '', groupId: ''}, + {kind: 'videoinput', deviceId: 'default', label: '', groupId: ''} + ]; + resolve(infos); + }); + }; + + if (browserDetails.version < 41) { + // Work around http://bugzil.la/1169665 + var orgEnumerateDevices = + navigator.mediaDevices.enumerateDevices.bind(navigator.mediaDevices); + navigator.mediaDevices.enumerateDevices = function() { + return orgEnumerateDevices().then(undefined, function(e) { + if (e.name === 'NotFoundError') { + return []; + } + throw e; + }); + }; + } + if (browserDetails.version < 49) { + var origGetUserMedia = navigator.mediaDevices.getUserMedia. + bind(navigator.mediaDevices); + navigator.mediaDevices.getUserMedia = function(c) { + return origGetUserMedia(c).then(function(stream) { + // Work around https://bugzil.la/802326 + if (c.audio && !stream.getAudioTracks().length || + c.video && !stream.getVideoTracks().length) { + stream.getTracks().forEach(function(track) { + track.stop(); + }); + throw new DOMException('The object can not be found here.', + 'NotFoundError'); + } + return stream; + }, function(e) { + return Promise.reject(shimError_(e)); + }); + }; + } + navigator.getUserMedia = function(constraints, onSuccess, onError) { + if (browserDetails.version < 44) { + return getUserMedia_(constraints, onSuccess, onError); + } + // Replace Firefox 44+'s deprecation warning with unprefixed version. + console.warn('navigator.getUserMedia has been replaced by ' + + 'navigator.mediaDevices.getUserMedia'); + navigator.mediaDevices.getUserMedia(constraints).then(onSuccess, onError); + }; +}; + +},{"../utils":10}],9:[function(require,module,exports){ +/* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ +'use strict'; +var safariShim = { + // TODO: DrAlex, should be here, double check against LayoutTests + + // TODO: once the back-end for the mac port is done, add. + // TODO: check for webkitGTK+ + // shimPeerConnection: function() { }, + + shimOnAddStream: function() { + if (typeof window === 'object' && window.RTCPeerConnection && + !('onaddstream' in window.RTCPeerConnection.prototype)) { + Object.defineProperty(window.RTCPeerConnection.prototype, 'onaddstream', { + get: function() { + return this._onaddstream; + }, + set: function(f) { + if (this._onaddstream) { + this.removeEventListener('addstream', this._onaddstream); + this.removeEventListener('track', this._onaddstreampoly); + } + this.addEventListener('addstream', this._onaddstream = f); + this.addEventListener('track', this._onaddstreampoly = function(e) { + var stream = e.streams[0]; + if (!this._streams) { + this._streams = []; + } + if (this._streams.indexOf(stream) >= 0) { + return; + } + this._streams.push(stream); + var event = new Event('addstream'); + event.stream = e.streams[0]; + this.dispatchEvent(event); + }.bind(this)); + } + }); + } + }, + + shimGetUserMedia: function() { + if (!navigator.getUserMedia) { + if (navigator.webkitGetUserMedia) { + navigator.getUserMedia = navigator.webkitGetUserMedia.bind(navigator); + } else if (navigator.mediaDevices && + navigator.mediaDevices.getUserMedia) { + navigator.getUserMedia = function(constraints, cb, errcb) { + navigator.mediaDevices.getUserMedia(constraints) + .then(cb, errcb); + }.bind(navigator); + } + } + } +}; + +// Expose public methods. +module.exports = { + shimOnAddStream: safariShim.shimOnAddStream, + shimGetUserMedia: safariShim.shimGetUserMedia + // TODO + // shimPeerConnection: safariShim.shimPeerConnection +}; + +},{}],10:[function(require,module,exports){ +/* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + /* eslint-env node */ +'use strict'; + +var logDisabled_ = true; + +// Utility methods. +var utils = { + disableLog: function(bool) { + if (typeof bool !== 'boolean') { + return new Error('Argument type: ' + typeof bool + + '. Please use a boolean.'); + } + logDisabled_ = bool; + return (bool) ? 'adapter.js logging disabled' : + 'adapter.js logging enabled'; + }, + + log: function() { + if (typeof window === 'object') { + if (logDisabled_) { + return; + } + if (typeof console !== 'undefined' && typeof console.log === 'function') { + console.log.apply(console, arguments); + } + } + }, + + /** + * Extract browser version out of the provided user agent string. + * + * @param {!string} uastring userAgent string. + * @param {!string} expr Regular expression used as match criteria. + * @param {!number} pos position in the version string to be returned. + * @return {!number} browser version. + */ + extractVersion: function(uastring, expr, pos) { + var match = uastring.match(expr); + return match && match.length >= pos && parseInt(match[pos], 10); + }, + + /** + * Browser detector. + * + * @return {object} result containing browser and version + * properties. + */ + detectBrowser: function() { + // Returned result object. + var result = {}; + result.browser = null; + result.version = null; + + // Fail early if it's not a browser + if (typeof window === 'undefined' || !window.navigator) { + result.browser = 'Not a browser.'; + return result; + } + + // Firefox. + if (navigator.mozGetUserMedia) { + result.browser = 'firefox'; + result.version = this.extractVersion(navigator.userAgent, + /Firefox\/(\d+)\./, 1); + } else if (navigator.webkitGetUserMedia) { + // Chrome, Chromium, Webview, Opera, all use the chrome shim for now + if (window.webkitRTCPeerConnection) { + result.browser = 'chrome'; + result.version = this.extractVersion(navigator.userAgent, + /Chrom(e|ium)\/(\d+)\./, 2); + } else { // Safari (in an unpublished version) or unknown webkit-based. + if (navigator.userAgent.match(/Version\/(\d+).(\d+)/)) { + result.browser = 'safari'; + result.version = this.extractVersion(navigator.userAgent, + /AppleWebKit\/(\d+)\./, 1); + } else { // unknown webkit-based browser. + result.browser = 'Unsupported webkit-based browser ' + + 'with GUM support but no WebRTC support.'; + return result; + } + } + } else if (navigator.mediaDevices && + navigator.userAgent.match(/Edge\/(\d+).(\d+)$/)) { // Edge. + result.browser = 'edge'; + result.version = this.extractVersion(navigator.userAgent, + /Edge\/(\d+).(\d+)$/, 2); + } else if (navigator.mediaDevices && + navigator.userAgent.match(/AppleWebKit\/(\d+)\./)) { + // Safari, with webkitGetUserMedia removed. + result.browser = 'safari'; + result.version = this.extractVersion(navigator.userAgent, + /AppleWebKit\/(\d+)\./, 1); + } else { // Default fallthrough: not supported. + result.browser = 'Not a supported browser.'; + return result; + } + + return result; + }, + + // shimCreateObjectURL must be called before shimSourceObject to avoid loop. + + shimCreateObjectURL: function() { + if (!(typeof window === 'object' && window.HTMLMediaElement && + 'srcObject' in window.HTMLMediaElement.prototype)) { + // Only shim CreateObjectURL using srcObject if srcObject exists. + return undefined; + } + + var nativeCreateObjectURL = URL.createObjectURL.bind(URL); + var nativeRevokeObjectURL = URL.revokeObjectURL.bind(URL); + var streams = new Map(), newId = 0; + + URL.createObjectURL = function(stream) { + if ('getTracks' in stream) { + var url = 'polyblob:' + (++newId); + streams.set(url, stream); + console.log('URL.createObjectURL(stream) is deprecated! ' + + 'Use elem.srcObject = stream instead!'); + return url; + } + return nativeCreateObjectURL(stream); + }; + URL.revokeObjectURL = function(url) { + nativeRevokeObjectURL(url); + streams.delete(url); + }; + + var dsc = Object.getOwnPropertyDescriptor(window.HTMLMediaElement.prototype, + 'src'); + Object.defineProperty(window.HTMLMediaElement.prototype, 'src', { + get: function() { + return dsc.get.apply(this); + }, + set: function(url) { + this.srcObject = streams.get(url) || null; + return dsc.set.apply(this, [url]); + } + }); + + var nativeSetAttribute = HTMLMediaElement.prototype.setAttribute; + HTMLMediaElement.prototype.setAttribute = function() { + if (arguments.length === 2 && + ('' + arguments[0]).toLowerCase() === 'src') { + this.srcObject = streams.get(arguments[1]) || null; + } + return nativeSetAttribute.apply(this, arguments); + }; + } +}; + +// Export. +module.exports = { + log: utils.log, + disableLog: utils.disableLog, + browserDetails: utils.detectBrowser(), + extractVersion: utils.extractVersion, + shimCreateObjectURL: utils.shimCreateObjectURL, + detectBrowser: utils.detectBrowser.bind(utils) +}; + +},{}]},{},[2])(2) +}); \ No newline at end of file diff --git a/bigbluebutton-client/resources/prod/lib/bbb_api_bridge.js b/bigbluebutton-client/resources/prod/lib/bbb_api_bridge.js index c3e8e668fe..10b33330e4 100755 --- a/bigbluebutton-client/resources/prod/lib/bbb_api_bridge.js +++ b/bigbluebutton-client/resources/prod/lib/bbb_api_bridge.js @@ -504,7 +504,15 @@ swfObj.webRTCMediaFail(); } } - + + BBB.webRTCMonitorUpdate = function(result) { + var swfObj = getSwfObj(); + if (swfObj) { + swfObj.webRTCMonitorUpdate(result); + } + } + + // Third-party JS apps should use this to query if the BBB SWF file is ready to handle calls. BBB.isSwfClientReady = function() { return swfReady; @@ -657,4 +665,3 @@ window.BBB = BBB; })(this); - diff --git a/bigbluebutton-client/resources/prod/lib/bbb_webrtc_bridge_sip.js b/bigbluebutton-client/resources/prod/lib/bbb_webrtc_bridge_sip.js index edbb820b07..780f9b68cd 100755 --- a/bigbluebutton-client/resources/prod/lib/bbb_webrtc_bridge_sip.js +++ b/bigbluebutton-client/resources/prod/lib/bbb_webrtc_bridge_sip.js @@ -9,12 +9,15 @@ function webRTCCallback(message) { if (message.errorcode !== 1004) { message.cause = null; } + monitorTracksStop(); BBB.webRTCCallFailed(inEchoTest, message.errorcode, message.cause); break; case 'ended': + monitorTracksStop(); BBB.webRTCCallEnded(inEchoTest); break; case 'started': + monitorTracksStart(); BBB.webRTCCallStarted(inEchoTest); break; case 'connecting': diff --git a/bigbluebutton-client/resources/prod/lib/getStats.js b/bigbluebutton-client/resources/prod/lib/getStats.js new file mode 100644 index 0000000000..98474b96c2 --- /dev/null +++ b/bigbluebutton-client/resources/prod/lib/getStats.js @@ -0,0 +1,297 @@ +// Last time updated at May 23, 2015, 08:32:23 +// Latest file can be found here: https://cdn.webrtc-experiment.com/getStats.js +// Muaz Khan - www.MuazKhan.com +// MIT License - www.WebRTC-Experiment.com/licence +// Source Code - https://github.com/muaz-khan/getStats +// ___________ +// getStats.js +// an abstraction layer runs top over RTCPeerConnection.getStats API +// cross-browser compatible solution +// http://dev.w3.org/2011/webrtc/editor/webrtc.html#dom-peerconnection-getstats +/* +getStats(function(result) { + result.connectionType.remote.ipAddress + result.connectionType.remote.candidateType + result.connectionType.transport +}); +*/ +(function() { + + var RTCPeerConnection; + if (typeof webkitRTCPeerConnection !== 'undefined') { + RTCPeerConnection = webkitRTCPeerConnection; + } + + if (isFirefox()) { + RTCPeerConnection = mozRTCPeerConnection; + } + + if (typeof MediaStreamTrack === 'undefined') { + MediaStreamTrack = {}; // todo? + } + + function isFirefox() { + return typeof mozRTCPeerConnection !== 'undefined'; + } + + function arrayAverage(array) { + var sum = 0; + for( var i = 0; i < array.length; i++ ){ + sum += array[i]; + } + return sum/array.length; + } + + function getStats(mediaStreamTrack, callback, interval) { + var peer = this; + + if (arguments[0] instanceof RTCPeerConnection) { + peer = arguments[0]; + mediaStreamTrack = arguments[1]; + callback = arguments[2]; + interval = arguments[3]; + + if (!(mediaStreamTrack instanceof MediaStreamTrack) && !!navigator.mozGetUserMedia) { + throw '2nd argument is not instance of MediaStreamTrack.'; + } + } else if (!(mediaStreamTrack instanceof MediaStreamTrack) && !!navigator.mozGetUserMedia) { + throw '1st argument is not instance of MediaStreamTrack.'; + } + + var globalObject = { + audio: {}, + video: {} + }; + + var nomore = false; + + (function getPrivateStats() { + var promise = _getStats(peer, mediaStreamTrack); + promise.then(function(results) { + var result = { + audio: {}, + video: {}, + // TODO remove the raw results + results: results, + nomore: function() { + nomore = true; + } + }; + + for (var key in results) { + var res = results[key]; + + res.timestamp = typeof res.timestamp === 'object'? res.timestamp.getTime(): res.timestamp; + res.packetsLost = typeof res.packetsLost === 'string'? parseInt(res.packetsLost): res.packetsLost; + res.packetsReceived = typeof res.packetsReceived === 'string'? parseInt(res.packetsReceived): res.packetsReceived; + res.bytesReceived = typeof res.bytesReceived === 'string'? parseInt(res.bytesReceived): res.bytesReceived; + + if ((res.mediaType == 'audio' || (res.type == 'ssrc' && res.googCodecName == 'opus')) && typeof res.bytesSent !== 'undefined') { + if (typeof globalObject.audio.prevSent === 'undefined') { + globalObject.audio.prevSent = res; + } + + var bytes = res.bytesSent - globalObject.audio.prevSent.bytesSent; + var diffTimestamp = res.timestamp - globalObject.audio.prevSent.timestamp; + + var kilobytes = bytes / 1024; + var kbitsSentPerSecond = (kilobytes * 8) / (diffTimestamp / 1000.0); + + result.audio = merge(result.audio, { + availableBandwidth: kilobytes, + inputLevel: res.audioInputLevel, + packetsSent: res.packetsSent, + bytesSent: res.bytesSent, + kbitsSentPerSecond: kbitsSentPerSecond + }); + + globalObject.audio.prevSent = res; + } + if ((res.mediaType == 'audio' || (res.type == 'ssrc' && res.googCodecName == 'opus')) && typeof res.bytesReceived !== 'undefined') { + if (typeof globalObject.audio.prevReceived === 'undefined') { + globalObject.audio.prevReceived = res; + globalObject.audio.consecutiveFlaws = 0; + globalObject.audio.globalBitrateArray = [ ]; + } + + var intervalPacketsLost = res.packetsLost - globalObject.audio.prevReceived.packetsLost; + var intervalPacketsReceived = res.packetsReceived - globalObject.audio.prevReceived.packetsReceived; + var intervalBytesReceived = res.bytesReceived - globalObject.audio.prevReceived.bytesReceived; + var intervalLossRate = intervalPacketsLost + intervalPacketsReceived == 0? 1: intervalPacketsLost / (intervalPacketsLost + intervalPacketsReceived); + var intervalBitrate = (intervalBytesReceived / interval) * 8; + var globalBitrate = arrayAverage(globalObject.audio.globalBitrateArray); + var intervalEstimatedLossRate; + if (isFirefox()) { + intervalEstimatedLossRate = Math.max(0, globalBitrate - intervalBitrate) / globalBitrate; + } else { + intervalEstimatedLossRate = intervalLossRate; + } + + var flaws = intervalPacketsLost; + if (flaws > 0) { + if (globalObject.audio.consecutiveFlaws > 0) { + flaws *= 2; + } + ++globalObject.audio.consecutiveFlaws; + } else { + globalObject.audio.consecutiveFlaws = 0; + } + var packetsLost = res.packetsLost + flaws; + var r = (Math.max(0, res.packetsReceived - packetsLost) / res.packetsReceived) * 100; + if (r > 100) { + r = 100; + } + // https://freeswitch.org/stash/projects/FS/repos/freeswitch/browse/src/switch_rtp.c?at=refs%2Fheads%2Fv1.4#1671 + var mos = 1 + (0.035) * r + (0.000007) * r * (r-60) * (100-r); + + var bytes = res.bytesReceived - globalObject.audio.prevReceived.bytesReceived; + var diffTimestamp = res.timestamp - globalObject.audio.prevReceived.timestamp; + var packetDuration = diffTimestamp / (res.packetsReceived - globalObject.audio.prevReceived.packetsReceived); + var kilobytes = bytes / 1024; + var kbitsReceivedPerSecond = (kilobytes * 8) / (diffTimestamp / 1000.0); + + result.audio = merge(result.audio, { + availableBandwidth: kilobytes, + outputLevel: res.audioOutputLevel, + packetsLost: res.packetsLost, + jitter: typeof res.googJitterReceived !== 'undefined'? res.googJitterReceived: res.jitter, + jitterBuffer: res.googJitterBufferMs, + delay: res.googCurrentDelayMs, + packetsReceived: res.packetsReceived, + bytesReceived: res.bytesReceived, + kbitsReceivedPerSecond: kbitsReceivedPerSecond, + packetDuration: packetDuration, + r: r, + mos: mos, + intervalLossRate: intervalLossRate, + intervalEstimatedLossRate: intervalEstimatedLossRate, + globalBitrate: globalBitrate + }); + + globalObject.audio.prevReceived = res; + globalObject.audio.globalBitrateArray.push(intervalBitrate); + // 12 is the number of seconds we use to calculate the global bitrate + if (globalObject.audio.globalBitrateArray.length > 12 / (interval / 1000)) { + globalObject.audio.globalBitrateArray.shift(); + } + } + + // TODO make it work on Firefox + if (res.googCodecName == 'VP8') { + if (!globalObject.video.prevBytesSent) + globalObject.video.prevBytesSent = res.bytesSent; + + var bytes = res.bytesSent - globalObject.video.prevBytesSent; + globalObject.video.prevBytesSent = res.bytesSent; + + var kilobytes = bytes / 1024; + + result.video = merge(result.video, { + availableBandwidth: kilobytes.toFixed(1), + googFrameHeightInput: res.googFrameHeightInput, + googFrameWidthInput: res.googFrameWidthInput, + googCaptureQueueDelayMsPerS: res.googCaptureQueueDelayMsPerS, + rtt: res.googRtt, + packetsLost: res.packetsLost, + packetsSent: res.packetsSent, + googEncodeUsagePercent: res.googEncodeUsagePercent, + googCpuLimitedResolution: res.googCpuLimitedResolution, + googNacksReceived: res.googNacksReceived, + googFrameRateInput: res.googFrameRateInput, + googPlisReceived: res.googPlisReceived, + googViewLimitedResolution: res.googViewLimitedResolution, + googCaptureJitterMs: res.googCaptureJitterMs, + googAvgEncodeMs: res.googAvgEncodeMs, + googFrameHeightSent: res.googFrameHeightSent, + googFrameRateSent: res.googFrameRateSent, + googBandwidthLimitedResolution: res.googBandwidthLimitedResolution, + googFrameWidthSent: res.googFrameWidthSent, + googFirsReceived: res.googFirsReceived, + bytesSent: res.bytesSent + }); + } + + if (res.type == 'VideoBwe') { + result.video.bandwidth = { + googActualEncBitrate: res.googActualEncBitrate, + googAvailableSendBandwidth: res.googAvailableSendBandwidth, + googAvailableReceiveBandwidth: res.googAvailableReceiveBandwidth, + googRetransmitBitrate: res.googRetransmitBitrate, + googTargetEncBitrate: res.googTargetEncBitrate, + googBucketDelay: res.googBucketDelay, + googTransmitBitrate: res.googTransmitBitrate + }; + } + + // res.googActiveConnection means either STUN or TURN is used. + + if (res.type == 'googCandidatePair' && res.googActiveConnection == 'true') { + result.connectionType = { + local: { + candidateType: res.googLocalCandidateType, + ipAddress: res.googLocalAddress + }, + remote: { + candidateType: res.googRemoteCandidateType, + ipAddress: res.googRemoteAddress + }, + transport: res.googTransportType + }; + } + } + + callback(result); + + // second argument checks to see, if target-user is still connected. + if (!nomore) { + typeof interval != undefined && interval && setTimeout(getPrivateStats, interval || 1000); + } + }, function(exception) { + console.log("Promise rejected: " + exception.message); + callback(null); + }); + })(); + + // taken from http://blog.telenor.io/webrtc/2015/06/11/getstats-chrome-vs-firefox.html + function _getStats(pc, selector) { + if (navigator.mozGetUserMedia) { + return pc.getStats(selector); + } + return new Promise(function(resolve, reject) { + pc.getStats(function(response) { + var standardReport = {}; + response.result().forEach(function(report) { + var standardStats = { + id: report.id, + timestamp: report.timestamp, + type: report.type + }; + report.names().forEach(function(name) { + standardStats[name] = report.stat(name); + }); + standardReport[standardStats.id] = standardStats; + }); + resolve(standardReport); + }, selector, reject); + }); + } + } + + function merge(mergein, mergeto) { + if (!mergein) mergein = {}; + if (!mergeto) return mergein; + + for (var item in mergeto) { + mergein[item] = mergeto[item]; + } + return mergein; + } + + if (typeof module !== 'undefined'/* && !!module.exports*/) { + module.exports = getStats; + } + + if(typeof window !== 'undefined') { + window.getStats = getStats; + } +})(); diff --git a/bigbluebutton-client/resources/prod/lib/webrtc_stats_bridge.js b/bigbluebutton-client/resources/prod/lib/webrtc_stats_bridge.js new file mode 100644 index 0000000000..7d02df3859 --- /dev/null +++ b/bigbluebutton-client/resources/prod/lib/webrtc_stats_bridge.js @@ -0,0 +1,305 @@ +var monitoredTracks = {}; + +function isFirefox() { + return typeof mozRTCPeerConnection !== 'undefined'; +} + +function arrayAverage(array) { + var sum = 0; + for( var i = 0; i < array.length; i++ ){ + sum += array[i]; + } + return sum/array.length; +} + +function customGetStats(peer, mediaStreamTrack, callback, interval) { + var globalObject = { + audio: {} + //audio: {}, + //video: {} + }; + + var nomore = false; + + (function getPrivateStats() { + var promise = _getStats(peer, mediaStreamTrack); + promise.then(function(results) { + var result = { + audio: {}, + //video: {}, + // TODO remove the raw results + results: results, + nomore: function() { + nomore = true; + } + }; + + for (var key in results) { + var res = results[key]; + + res.timestamp = typeof res.timestamp === 'object'? res.timestamp.getTime(): res.timestamp; + res.packetsLost = typeof res.packetsLost === 'string'? parseInt(res.packetsLost): res.packetsLost; + res.packetsReceived = typeof res.packetsReceived === 'string'? parseInt(res.packetsReceived): res.packetsReceived; + res.bytesReceived = typeof res.bytesReceived === 'string'? parseInt(res.bytesReceived): res.bytesReceived; + + if ((res.mediaType == 'audio' || (res.type == 'ssrc' && res.googCodecName == 'opus')) && typeof res.bytesSent !== 'undefined') { + if (typeof globalObject.audio.prevSent === 'undefined') { + globalObject.audio.prevSent = res; + } + + var bytes = res.bytesSent - globalObject.audio.prevSent.bytesSent; + var diffTimestamp = res.timestamp - globalObject.audio.prevSent.timestamp; + + var kilobytes = bytes / 1024; + var kbitsSentPerSecond = (kilobytes * 8) / (diffTimestamp / 1000.0); + + result.audio = merge(result.audio, { + availableBandwidth: kilobytes, + inputLevel: res.audioInputLevel, + packetsSent: res.packetsSent, + bytesSent: res.bytesSent, + kbitsSentPerSecond: kbitsSentPerSecond + }); + + globalObject.audio.prevSent = res; + } + if ((res.mediaType == 'audio' || (res.type == 'ssrc' && res.googCodecName == 'opus')) && typeof res.bytesReceived !== 'undefined') { + if (typeof globalObject.audio.prevReceived === 'undefined') { + globalObject.audio.prevReceived = res; + globalObject.audio.consecutiveFlaws = 0; + globalObject.audio.globalBitrateArray = [ ]; + } + + var intervalPacketsLost = res.packetsLost - globalObject.audio.prevReceived.packetsLost; + var intervalPacketsReceived = res.packetsReceived - globalObject.audio.prevReceived.packetsReceived; + var intervalBytesReceived = res.bytesReceived - globalObject.audio.prevReceived.bytesReceived; + var intervalLossRate = intervalPacketsLost + intervalPacketsReceived == 0? 1: intervalPacketsLost / (intervalPacketsLost + intervalPacketsReceived); + var intervalBitrate = (intervalBytesReceived / interval) * 8; + var globalBitrate = arrayAverage(globalObject.audio.globalBitrateArray); + var intervalEstimatedLossRate; + if (isFirefox()) { + intervalEstimatedLossRate = Math.max(0, globalBitrate - intervalBitrate) / globalBitrate; + } else { + intervalEstimatedLossRate = intervalLossRate; + } + + var flaws = intervalPacketsLost; + if (flaws > 0) { + if (globalObject.audio.consecutiveFlaws > 0) { + flaws *= 2; + } + ++globalObject.audio.consecutiveFlaws; + } else { + globalObject.audio.consecutiveFlaws = 0; + } + var packetsLost = res.packetsLost + flaws; + var r = (Math.max(0, res.packetsReceived - packetsLost) / res.packetsReceived) * 100; + if (r > 100) { + r = 100; + } + // https://freeswitch.org/stash/projects/FS/repos/freeswitch/browse/src/switch_rtp.c?at=refs%2Fheads%2Fv1.4#1671 + var mos = 1 + (0.035) * r + (0.000007) * r * (r-60) * (100-r); + + var bytes = res.bytesReceived - globalObject.audio.prevReceived.bytesReceived; + var diffTimestamp = res.timestamp - globalObject.audio.prevReceived.timestamp; + var packetDuration = diffTimestamp / (res.packetsReceived - globalObject.audio.prevReceived.packetsReceived); + var kilobytes = bytes / 1024; + var kbitsReceivedPerSecond = (kilobytes * 8) / (diffTimestamp / 1000.0); + + result.audio = merge(result.audio, { + availableBandwidth: kilobytes, + outputLevel: res.audioOutputLevel, + packetsLost: res.packetsLost, + jitter: typeof res.googJitterReceived !== 'undefined'? res.googJitterReceived: res.jitter, + jitterBuffer: res.googJitterBufferMs, + delay: res.googCurrentDelayMs, + packetsReceived: res.packetsReceived, + bytesReceived: res.bytesReceived, + kbitsReceivedPerSecond: kbitsReceivedPerSecond, + packetDuration: packetDuration, + r: r, + mos: mos, + intervalLossRate: intervalLossRate, + intervalEstimatedLossRate: intervalEstimatedLossRate, + globalBitrate: globalBitrate + }); + + globalObject.audio.prevReceived = res; + globalObject.audio.globalBitrateArray.push(intervalBitrate); + // 12 is the number of seconds we use to calculate the global bitrate + if (globalObject.audio.globalBitrateArray.length > 12 / (interval / 1000)) { + globalObject.audio.globalBitrateArray.shift(); + } + } + + /* + // TODO make it work on Firefox + if (res.googCodecName == 'VP8') { + if (!globalObject.video.prevBytesSent) + globalObject.video.prevBytesSent = res.bytesSent; + + var bytes = res.bytesSent - globalObject.video.prevBytesSent; + globalObject.video.prevBytesSent = res.bytesSent; + + var kilobytes = bytes / 1024; + + result.video = merge(result.video, { + availableBandwidth: kilobytes.toFixed(1), + googFrameHeightInput: res.googFrameHeightInput, + googFrameWidthInput: res.googFrameWidthInput, + googCaptureQueueDelayMsPerS: res.googCaptureQueueDelayMsPerS, + rtt: res.googRtt, + packetsLost: res.packetsLost, + packetsSent: res.packetsSent, + googEncodeUsagePercent: res.googEncodeUsagePercent, + googCpuLimitedResolution: res.googCpuLimitedResolution, + googNacksReceived: res.googNacksReceived, + googFrameRateInput: res.googFrameRateInput, + googPlisReceived: res.googPlisReceived, + googViewLimitedResolution: res.googViewLimitedResolution, + googCaptureJitterMs: res.googCaptureJitterMs, + googAvgEncodeMs: res.googAvgEncodeMs, + googFrameHeightSent: res.googFrameHeightSent, + googFrameRateSent: res.googFrameRateSent, + googBandwidthLimitedResolution: res.googBandwidthLimitedResolution, + googFrameWidthSent: res.googFrameWidthSent, + googFirsReceived: res.googFirsReceived, + bytesSent: res.bytesSent + }); + } + + if (res.type == 'VideoBwe') { + result.video.bandwidth = { + googActualEncBitrate: res.googActualEncBitrate, + googAvailableSendBandwidth: res.googAvailableSendBandwidth, + googAvailableReceiveBandwidth: res.googAvailableReceiveBandwidth, + googRetransmitBitrate: res.googRetransmitBitrate, + googTargetEncBitrate: res.googTargetEncBitrate, + googBucketDelay: res.googBucketDelay, + googTransmitBitrate: res.googTransmitBitrate + }; + } + */ + + // res.googActiveConnection means either STUN or TURN is used. + + if (res.type == 'googCandidatePair' && res.googActiveConnection == 'true') { + result.connectionType = { + local: { + candidateType: res.googLocalCandidateType, + ipAddress: res.googLocalAddress + }, + remote: { + candidateType: res.googRemoteCandidateType, + ipAddress: res.googRemoteAddress + }, + transport: res.googTransportType + }; + } + } + + callback(result); + + // second argument checks to see, if target-user is still connected. + if (!nomore) { + typeof interval != undefined && interval && setTimeout(getPrivateStats, interval || 1000); + } + }, function(exception) { + console.log("Promise rejected: " + exception.message); + callback(null); + }); + })(); + + // taken from http://blog.telenor.io/webrtc/2015/06/11/getstats-chrome-vs-firefox.html + function _getStats(pc, selector) { + if (navigator.mozGetUserMedia) { + return pc.getStats(selector); + } + return new Promise(function(resolve, reject) { + pc.getStats(function(response) { + var standardReport = {}; + response.result().forEach(function(report) { + var standardStats = { + id: report.id, + timestamp: report.timestamp, + type: report.type + }; + report.names().forEach(function(name) { + standardStats[name] = report.stat(name); + }); + standardReport[standardStats.id] = standardStats; + }); + resolve(standardReport); + }, selector, reject); + }); + } +} + +function merge(mergein, mergeto) { + if (!mergein) mergein = {}; + if (!mergeto) return mergein; + + for (var item in mergeto) { + mergein[item] = mergeto[item]; + } + return mergein; +} + +function monitorTrackStart(peer, track, local) { + if (! monitoredTracks.hasOwnProperty(track.id)) { + monitoredTracks[track.id] = function() { console.log("Still didn't have any report for this track"); }; + customGetStats( + peer, + track, + function(results) { + if (results == null) { + monitorTrackStop(track.id); + } else { + monitoredTracks[track.id] = results.nomore; + results.audio.type = local? "local": "remote", + delete results.results; + BBB.webRTCMonitorUpdate(JSON.stringify(results)); + console.log(JSON.stringify(results)); + } + }, + 2000 + ); + } else { + console.log("Already monitoring this track"); + } +} + +function monitorTrackStop(trackId) { + monitoredTracks[trackId](); + delete monitoredTracks[trackId]; + console.log("Track removed, monitoredTracks.length = " + Object.keys(monitoredTracks).length); +} + +function monitorTracksStart() { + setTimeout( function() { + if (currentSession == null) { + console.log("Doing nothing because currentSession is null"); + return; + } + var peer = currentSession.mediaHandler.peerConnection; + + for (var streamId = 0; streamId < peer.getLocalStreams().length; ++streamId) { + for (var trackId = 0; trackId < peer.getLocalStreams()[streamId].getAudioTracks().length; ++trackId) { + var track = peer.getLocalStreams()[streamId].getAudioTracks()[trackId]; + monitorTrackStart(peer, track, true); + } + } + for (var streamId = 0; streamId < peer.getRemoteStreams().length; ++streamId) { + for (var trackId = 0; trackId < peer.getRemoteStreams()[streamId].getAudioTracks().length; ++trackId) { + var track = peer.getRemoteStreams()[streamId].getAudioTracks()[trackId]; + monitorTrackStart(peer, track, false); + } + } + }, 2000); +} + +function monitorTracksStop() { + for (var id in monitoredTracks) { + monitorTrackStop(id); + } +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/api/ExternalApiCallbacks.as b/bigbluebutton-client/src/org/bigbluebutton/main/api/ExternalApiCallbacks.as index d3862708b5..1d29c7b8e1 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/main/api/ExternalApiCallbacks.as +++ b/bigbluebutton-client/src/org/bigbluebutton/main/api/ExternalApiCallbacks.as @@ -105,7 +105,7 @@ package org.bigbluebutton.main.api ExternalInterface.addCallback("webRTCMediaSuccess", handleWebRTCMediaSuccess); ExternalInterface.addCallback("webRTCMediaFail", handleWebRTCMediaFail); ExternalInterface.addCallback("getSessionToken", handleGetSessionToken); - + ExternalInterface.addCallback("webRTCMonitorUpdate", handleWebRTCMonitorUpdate); } // Tell out JS counterpart that we are ready. @@ -444,5 +444,11 @@ package org.bigbluebutton.main.api private function handleWebRTCMediaFail():void { _dispatcher.dispatchEvent(new WebRTCMediaEvent(WebRTCMediaEvent.WEBRTC_MEDIA_FAIL)); } + + private function handleWebRTCMonitorUpdate(results:String):void { + var e:BBBEvent = new BBBEvent(BBBEvent.WEBRTC_MONITOR_UPDATE_EVENT); + e.payload.results = results; + _dispatcher.dispatchEvent(e); + } } } diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/events/BBBEvent.as b/bigbluebutton-client/src/org/bigbluebutton/main/events/BBBEvent.as index b4bf6048b0..39594a9fe2 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/main/events/BBBEvent.as +++ b/bigbluebutton-client/src/org/bigbluebutton/main/events/BBBEvent.as @@ -54,6 +54,7 @@ package org.bigbluebutton.main.events { public static const RECONNECT_DESKSHARE_SUCCEEDED_EVENT:String = "RECONNECT_DESKSHARE_SUCCEEDED_EVENT"; public static const CANCEL_RECONNECTION_EVENT:String = "CANCEL_RECONNECTION_EVENT"; + public static const WEBRTC_MONITOR_UPDATE_EVENT:String = "WEBRTC_MONITOR_UPDATE_EVENT"; public var message:String; public var payload:Object = new Object(); diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/views/MainToolbar.mxml b/bigbluebutton-client/src/org/bigbluebutton/main/views/MainToolbar.mxml index c42ee071fb..31f7c8c9a6 100644 --- a/bigbluebutton-client/src/org/bigbluebutton/main/views/MainToolbar.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/main/views/MainToolbar.mxml @@ -380,7 +380,7 @@ with BigBlueButton; if not, see . + tabIndices="{[recordBtn, muteMeBtn, webRTCAudioStatus, shortcutKeysBtn, helpBtn, btnLogout]}"/> @@ -404,6 +404,7 @@ with BigBlueButton; if not, see . + + + + + + + + + 0.50: + audioStatus.source = getStyle("weakAudioStatus"); + toolTip = ResourceUtil.getInstance().getString('bbb.clientstatus.webrtc.weakStatus'); + LOGGER.debug("Audio interval estimated loss rate: {0}", [rate]); + break; + case rate > 0.25: + audioStatus.source = getStyle("almostWeakAudioStatus"); + toolTip = ResourceUtil.getInstance().getString('bbb.clientstatus.webrtc.almostWeakStatus'); + LOGGER.debug("Audio interval estimated loss rate: {0}", [rate]); + break; + case rate > 0.1: + audioStatus.source = getStyle("almostStrongAudioStatus"); + toolTip = ResourceUtil.getInstance().getString('bbb.clientstatus.webrtc.almostStrongStatus'); + break; + default: + audioStatus.source = getStyle("strongAudioStatus"); + toolTip = ResourceUtil.getInstance().getString('bbb.clientstatus.webrtc.strongStatus'); + break; + } + validateNow(); + } + } + + public function handleWebRTCMonitor(e:BBBEvent):void { + var results:String = e.payload.results; + var object:Object = JSON.parse(results); + if (object.hasOwnProperty("audio")) { + audioMonitor(object.audio); + } + } + + ]]> + + + diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/phone/PhoneOptions.as b/bigbluebutton-client/src/org/bigbluebutton/modules/phone/PhoneOptions.as index 6cc32b3ec8..f468aba7e4 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/phone/PhoneOptions.as +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/phone/PhoneOptions.as @@ -49,6 +49,9 @@ package org.bigbluebutton.modules.phone [Bindable] public var showPhoneOption:Boolean = false; + + [Bindable] + public var showWebRTCStats:Boolean = false; public function PhoneOptions() { parseOptions(); @@ -87,6 +90,9 @@ package org.bigbluebutton.modules.phone if (vxml.@showPhoneOption != undefined) { showPhoneOption = (vxml.@showPhoneOption.toString().toUpperCase() == "TRUE"); } + if (vxml.@showWebRTCStats != undefined) { + showWebRTCStats = (vxml.@showWebRTCStats.toString().toUpperCase() == "TRUE"); + } } } } From 6727f14ab77539925b146d1ccd13c7f44310b6ac Mon Sep 17 00:00:00 2001 From: Klaus Klein Date: Mon, 10 Apr 2017 09:55:22 -0300 Subject: [PATCH 71/91] Fix multiple welcome messages, issue #3792 HTML5 --- .../api/chat/server/modifiers/removeChat.js | 20 +++++++++++++++++++ .../server/handlers/validateAuthToken.js | 17 ++++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) create mode 100755 bigbluebutton-html5/imports/api/chat/server/modifiers/removeChat.js diff --git a/bigbluebutton-html5/imports/api/chat/server/modifiers/removeChat.js b/bigbluebutton-html5/imports/api/chat/server/modifiers/removeChat.js new file mode 100755 index 0000000000..4e0ccad675 --- /dev/null +++ b/bigbluebutton-html5/imports/api/chat/server/modifiers/removeChat.js @@ -0,0 +1,20 @@ +import Chat from '/imports/api/chat'; +import Logger from '/imports/startup/server/logger'; +import { check } from 'meteor/check'; +import { BREAK_LINE } from '/imports/utils/lineEndings.js'; + +/** + * Remove from chat a message that match the regex 'message' param. + * + * @param {string} meetingId + * @param {string} userId + * @param {string} message + */ +export default function removeChat(meetingId, userId, message) { + if (meetingId && userId && message) { + const modifiers = { meetingId: meetingId, "message.to_userid": userId ,"message.message":{ '$regex': message, '$options': 'i' }}; + + return Chat.remove(modifiers + , Logger.info(`Removing messages that match: (${message}) `)); + } +}; diff --git a/bigbluebutton-html5/imports/api/users/server/handlers/validateAuthToken.js b/bigbluebutton-html5/imports/api/users/server/handlers/validateAuthToken.js index a83a236834..fb65f516d1 100644 --- a/bigbluebutton-html5/imports/api/users/server/handlers/validateAuthToken.js +++ b/bigbluebutton-html5/imports/api/users/server/handlers/validateAuthToken.js @@ -4,6 +4,7 @@ import Meetings from '/imports/api/meetings'; import Users from '/imports/api/users'; import addChat from '/imports/api/chat/server/modifiers/addChat'; +import removeChat from '/imports/api/chat/server/modifiers/removeChat'; export default function handleValidateAuthToken({ payload }) { const meetingId = payload.meeting_id; @@ -40,6 +41,7 @@ export default function handleValidateAuthToken({ payload }) { if (numChanged) { if (validStatus) { + clearPastWelcomeMessage(meetingId, userId); addWelcomeChatMessage(meetingId, userId); } @@ -50,6 +52,17 @@ export default function handleValidateAuthToken({ payload }) { return Users.update(selector, modifier, cb); }; +/** + * Prevent the chat message having multiple welcome message showing to user, removing the past ones. + * @param {string} meetingId + * @param {string} userId + */ +const clearPastWelcomeMessage = (meetingId, userId) => { + const APP_CONFIG = Meteor.settings.public.app; + + return removeChat(meetingId, userId,APP_CONFIG.defaultWelcomeMessageFooter); +}; + const addWelcomeChatMessage = (meetingId, userId) => { const APP_CONFIG = Meteor.settings.public.app; const CHAT_CONFIG = Meteor.settings.public.chat; @@ -57,8 +70,8 @@ const addWelcomeChatMessage = (meetingId, userId) => { const Meeting = Meetings.findOne({ meetingId }); let welcomeMessage = APP_CONFIG.defaultWelcomeMessage - .concat(APP_CONFIG.defaultWelcomeMessageFooter) - .replace(/%%CONFNAME%%/, Meeting.meetingName); + .concat(APP_CONFIG.defaultWelcomeMessageFooter) + .replace(/%%CONFNAME%%/, Meeting.meetingName); const message = { chat_type: CHAT_CONFIG.type_system, From b432ffd68b60a4bf0caa1ccd123582612a3ed4f0 Mon Sep 17 00:00:00 2001 From: Klaus Klein Date: Mon, 10 Apr 2017 11:04:27 -0300 Subject: [PATCH 72/91] Change to fetch from chat system id, issue #3792 HTML5 --- .../api/chat/server/methods/sendChat.js | 2 +- .../modifiers/clearUserSystemMessages.js | 26 +++++++++++++++++++ .../api/chat/server/modifiers/removeChat.js | 20 -------------- .../server/handlers/validateAuthToken.js | 15 ++--------- 4 files changed, 29 insertions(+), 34 deletions(-) create mode 100755 bigbluebutton-html5/imports/api/chat/server/modifiers/clearUserSystemMessages.js delete mode 100755 bigbluebutton-html5/imports/api/chat/server/modifiers/removeChat.js diff --git a/bigbluebutton-html5/imports/api/chat/server/methods/sendChat.js b/bigbluebutton-html5/imports/api/chat/server/methods/sendChat.js index a53f5743f8..5d56026ad6 100755 --- a/bigbluebutton-html5/imports/api/chat/server/methods/sendChat.js +++ b/bigbluebutton-html5/imports/api/chat/server/methods/sendChat.js @@ -54,7 +54,7 @@ export default function sendChat(credentials, message) { } if (!isAllowedTo(actionName, credentials) - && message.from_userid !== requesterUserId) { + && message.from_userid !== requesterUserId) { throw new Meteor.Error('not-allowed', `You are not allowed to sendChat`); } diff --git a/bigbluebutton-html5/imports/api/chat/server/modifiers/clearUserSystemMessages.js b/bigbluebutton-html5/imports/api/chat/server/modifiers/clearUserSystemMessages.js new file mode 100755 index 0000000000..28360789ac --- /dev/null +++ b/bigbluebutton-html5/imports/api/chat/server/modifiers/clearUserSystemMessages.js @@ -0,0 +1,26 @@ +import Chat from '/imports/api/chat'; +import Logger from '/imports/startup/server/logger'; +import { check } from 'meteor/check'; +import { BREAK_LINE } from '/imports/utils/lineEndings.js'; + +/** + * Remove any system message from the user with userId. + * + * @param {string} meetingId + * @param {string} userId + */ +export default function clearUserSystemMessages(meetingId, userId) { + + check(meetingId, String); + check(userId, String); + + const CHAT_CONFIG = Meteor.settings.public.chat; + + const selector = { + meetingId, + "message.from_userid": CHAT_CONFIG.type_system, + "message.to_userid": userId, + } + + return Chat.remove(selector, Logger.info(`Removing system messages from: (${userId})`)); +}; diff --git a/bigbluebutton-html5/imports/api/chat/server/modifiers/removeChat.js b/bigbluebutton-html5/imports/api/chat/server/modifiers/removeChat.js deleted file mode 100755 index 4e0ccad675..0000000000 --- a/bigbluebutton-html5/imports/api/chat/server/modifiers/removeChat.js +++ /dev/null @@ -1,20 +0,0 @@ -import Chat from '/imports/api/chat'; -import Logger from '/imports/startup/server/logger'; -import { check } from 'meteor/check'; -import { BREAK_LINE } from '/imports/utils/lineEndings.js'; - -/** - * Remove from chat a message that match the regex 'message' param. - * - * @param {string} meetingId - * @param {string} userId - * @param {string} message - */ -export default function removeChat(meetingId, userId, message) { - if (meetingId && userId && message) { - const modifiers = { meetingId: meetingId, "message.to_userid": userId ,"message.message":{ '$regex': message, '$options': 'i' }}; - - return Chat.remove(modifiers - , Logger.info(`Removing messages that match: (${message}) `)); - } -}; diff --git a/bigbluebutton-html5/imports/api/users/server/handlers/validateAuthToken.js b/bigbluebutton-html5/imports/api/users/server/handlers/validateAuthToken.js index fb65f516d1..b1fdda6ace 100644 --- a/bigbluebutton-html5/imports/api/users/server/handlers/validateAuthToken.js +++ b/bigbluebutton-html5/imports/api/users/server/handlers/validateAuthToken.js @@ -4,7 +4,7 @@ import Meetings from '/imports/api/meetings'; import Users from '/imports/api/users'; import addChat from '/imports/api/chat/server/modifiers/addChat'; -import removeChat from '/imports/api/chat/server/modifiers/removeChat'; +import clearUserSystemMessages from '/imports/api/chat/server/modifiers/clearUserSystemMessages'; export default function handleValidateAuthToken({ payload }) { const meetingId = payload.meeting_id; @@ -41,7 +41,7 @@ export default function handleValidateAuthToken({ payload }) { if (numChanged) { if (validStatus) { - clearPastWelcomeMessage(meetingId, userId); + clearUserSystemMessages(meetingId, userId); addWelcomeChatMessage(meetingId, userId); } @@ -52,17 +52,6 @@ export default function handleValidateAuthToken({ payload }) { return Users.update(selector, modifier, cb); }; -/** - * Prevent the chat message having multiple welcome message showing to user, removing the past ones. - * @param {string} meetingId - * @param {string} userId - */ -const clearPastWelcomeMessage = (meetingId, userId) => { - const APP_CONFIG = Meteor.settings.public.app; - - return removeChat(meetingId, userId,APP_CONFIG.defaultWelcomeMessageFooter); -}; - const addWelcomeChatMessage = (meetingId, userId) => { const APP_CONFIG = Meteor.settings.public.app; const CHAT_CONFIG = Meteor.settings.public.chat; From 28165f25842645f6b29d3fcabe2145d283d40713 Mon Sep 17 00:00:00 2001 From: Anton Georgiev Date: Mon, 10 Apr 2017 11:02:58 -0400 Subject: [PATCH 73/91] improved logic for updating message-list --- .../imports/ui/components/chat/message-list/component.jsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/chat/message-list/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/message-list/component.jsx index e504e5c937..96a8eeff1d 100755 --- a/bigbluebutton-html5/imports/ui/components/chat/message-list/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/message-list/component.jsx @@ -120,19 +120,16 @@ class MessageList extends Component { const switchingCorrespondent = chatId !== nextProps.chatId; const hasNewUnreadMessages = hasUnreadMessages !== nextProps.hasUnreadMessages; - // console.log('switchingCorrespondent=' + switchingCorrespondent); - // console.log('hasNewUnreadMessages=' + hasNewUnreadMessages); - // check if the messages include const lastMessage = nextProps.messages[nextProps.messages.length - 1]; if (lastMessage) { const userLeftIsDisplayed = lastMessage.id.includes('partner-disconnected'); - if (partnerIsLoggedOut && userLeftIsDisplayed) return false; // update leads to endless loop + if (!(partnerIsLoggedOut && userLeftIsDisplayed)) return true; } if (switchingCorrespondent || hasNewUnreadMessages) return true; - return true; + return false; } render() { From a40deced614206c98f67649df890b3d28ebf7b75 Mon Sep 17 00:00:00 2001 From: Ghazi Triki Date: Mon, 10 Apr 2017 20:22:30 +0100 Subject: [PATCH 74/91] Display "Recommend using either Firefox or Chrome for better audio" when any of other browser is used. --- .../branding/default/style/css/BBBDefault.css | 13 ++++++++++--- .../main/views/AudioSelectionWindow.mxml | 13 +++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/bigbluebutton-client/branding/default/style/css/BBBDefault.css b/bigbluebutton-client/branding/default/style/css/BBBDefault.css index cec394e23b..2905906ef8 100755 --- a/bigbluebutton-client/branding/default/style/css/BBBDefault.css +++ b/bigbluebutton-client/branding/default/style/css/BBBDefault.css @@ -503,17 +503,24 @@ TitleWindow { } -.presentationUploadFileFormatHintBoxStyle { +.presentationUploadFileFormatHintBoxStyle, .audioBroswerHintBoxStyle { backgroundColor: #D4D4D4; dropShadowEnabled: false; paddingLeft: 10; - paddingRight: 10 + paddingRight: 10; } -.presentationUploadFileFormatHintTextStyle { +.presentationUploadFileFormatHintTextStyle, .audioBroswerHintTextStyle { fontWeight: bold; } +.audioBroswerHintBoxStyle { + paddingLeft: 5; + paddingRight: 5; + paddingBottom : 8; + paddingTop : 8; +} + .desktopShareViewStyle { backgroundColor: #FFFFFF; paddingTop: 15; diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/views/AudioSelectionWindow.mxml b/bigbluebutton-client/src/org/bigbluebutton/main/views/AudioSelectionWindow.mxml index 91274f99c4..8f66dacc77 100644 --- a/bigbluebutton-client/src/org/bigbluebutton/main/views/AudioSelectionWindow.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/main/views/AudioSelectionWindow.mxml @@ -76,6 +76,12 @@ with BigBlueButton; if not, see . vboxListen.percentWidth = 50; } + var browser:String = ExternalInterface.call("determineBrowser")[0]; + if (browser != "Firefox" && browser == "Chrome") { + audioBrowserHint.visible = audioBrowserHint.includeInLayout = true; + this.height += 72; + } + // If Puffin browser is deteted and version is less than 4.6 if (browserInfo[0] == "Puffin" && String(browserInfo[2]).substr(0, 3) < "4.6") { vruleListen.visible = vruleListen.includeInLayout = vboxMic.visible = vboxMic.includeInLayout = false; @@ -139,6 +145,13 @@ with BigBlueButton; if not, see . styleName="micSettingsWindowTitleStyle" width="100%" height="100%" /> + + + From 11871d7d0b7a35da18389b1bff7dd482cdfbf0fc Mon Sep 17 00:00:00 2001 From: Anton Georgiev Date: Mon, 10 Apr 2017 16:18:05 -0400 Subject: [PATCH 75/91] fix event name causing issues with listenOnly status --- bigbluebutton-html5/imports/api/phone/index.js | 4 ++-- .../imports/api/users/server/methods/listenOnlyToggle.js | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) mode change 100755 => 100644 bigbluebutton-html5/imports/api/phone/index.js mode change 100755 => 100644 bigbluebutton-html5/imports/api/users/server/methods/listenOnlyToggle.js diff --git a/bigbluebutton-html5/imports/api/phone/index.js b/bigbluebutton-html5/imports/api/phone/index.js old mode 100755 new mode 100644 index 05101454fb..cbfbe6e940 --- a/bigbluebutton-html5/imports/api/phone/index.js +++ b/bigbluebutton-html5/imports/api/phone/index.js @@ -47,7 +47,7 @@ function exitAudio(afterExitCall = () => {}) { // notify BBB-apps we are leaving the call call if we are listen only if (amIListenOnly()) { - callServer('listenOnlyRequestToggle', false); + callServer('listenOnlyToggle', false); } window.webrtc_hangup(hangupCallback); @@ -132,7 +132,7 @@ function joinVoiceCallSIP(options) { } function joinListenOnly() { - callServer('listenOnlyRequestToggle', true); + callServer('listenOnlyToggle', true); if (MEDIA_CONFIG.useSIPAudio) { joinVoiceCallSIP({ isListenOnly: true }); } else { diff --git a/bigbluebutton-html5/imports/api/users/server/methods/listenOnlyToggle.js b/bigbluebutton-html5/imports/api/users/server/methods/listenOnlyToggle.js old mode 100755 new mode 100644 index 6bbc5237a1..3bd3efcfa5 --- a/bigbluebutton-html5/imports/api/users/server/methods/listenOnlyToggle.js +++ b/bigbluebutton-html5/imports/api/users/server/methods/listenOnlyToggle.js @@ -16,13 +16,15 @@ export default function listenOnlyToggle(credentials, isJoining = true) { check(requesterUserId, String); check(isJoining, Boolean); + let EVENT_NAME = undefined; + if (isJoining) { - let EVENT_NAME = 'user_connected_to_global_audio'; + EVENT_NAME = 'user_connected_to_global_audio'; if (!isAllowedTo('joinListenOnly', credentials)) { throw new Meteor.Error('not-allowed', `You are not allowed to joinListenOnly`); } } else { - let EVENT_NAME = 'user_disconnected_from_global_audio'; + EVENT_NAME = 'user_disconnected_from_global_audio'; if (!isAllowedTo('leaveListenOnly', credentials)) { throw new Meteor.Error('not-allowed', `You are not allowed to leaveListenOnly`); } @@ -40,6 +42,7 @@ export default function listenOnlyToggle(credentials, isJoining = true) { meetingId, userId: requesterUserId, }); + if (!User) { throw new Meteor.Error( 'user-not-found', `You need a valid user to be able to toggle audio`); From 01b4a5f6e40b67e12cff00cbf880680b1b1b04fa Mon Sep 17 00:00:00 2001 From: Ghazi Triki Date: Tue, 11 Apr 2017 08:29:00 +0100 Subject: [PATCH 76/91] Check for WebRTC availability instead of checking for the browser name to display the AudioSelectionWindow browser hint. --- .../src/org/bigbluebutton/main/views/AudioSelectionWindow.mxml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/views/AudioSelectionWindow.mxml b/bigbluebutton-client/src/org/bigbluebutton/main/views/AudioSelectionWindow.mxml index 8f66dacc77..d30b0e3a04 100644 --- a/bigbluebutton-client/src/org/bigbluebutton/main/views/AudioSelectionWindow.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/main/views/AudioSelectionWindow.mxml @@ -76,8 +76,7 @@ with BigBlueButton; if not, see . vboxListen.percentWidth = 50; } - var browser:String = ExternalInterface.call("determineBrowser")[0]; - if (browser != "Firefox" && browser == "Chrome") { + if (!JSAPI.getInstance().isWebRTCAvailable()) { audioBrowserHint.visible = audioBrowserHint.includeInLayout = true; this.height += 72; } From 7c9c09b5b0fadfe5271ea56656990ad9ad09d647 Mon Sep 17 00:00:00 2001 From: Richard Alam Date: Tue, 11 Apr 2017 17:22:54 +0000 Subject: [PATCH 77/91] Cleanup - remove extra > on meeting name on getRecording API response --- bbb-common-web/src/test/resources/include-recording.ftlx | 2 +- .../web-app/WEB-INF/freemarker/include-recording.ftlx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bbb-common-web/src/test/resources/include-recording.ftlx b/bbb-common-web/src/test/resources/include-recording.ftlx index 803b3d9438..11bd77df86 100755 --- a/bbb-common-web/src/test/resources/include-recording.ftlx +++ b/bbb-common-web/src/test/resources/include-recording.ftlx @@ -7,7 +7,7 @@ ${r.getMeeting().isBreakout()?c} <#else> ${r.getMeetingId()?html} - ${r.getMeetingName()?html}> + ${r.getMeetingName()?html} ${r.isBreakout()?c} diff --git a/bigbluebutton-web/web-app/WEB-INF/freemarker/include-recording.ftlx b/bigbluebutton-web/web-app/WEB-INF/freemarker/include-recording.ftlx index 803b3d9438..11bd77df86 100755 --- a/bigbluebutton-web/web-app/WEB-INF/freemarker/include-recording.ftlx +++ b/bigbluebutton-web/web-app/WEB-INF/freemarker/include-recording.ftlx @@ -7,7 +7,7 @@ ${r.getMeeting().isBreakout()?c} <#else> ${r.getMeetingId()?html} - ${r.getMeetingName()?html}> + ${r.getMeetingName()?html} ${r.isBreakout()?c} From 6717308530543ee4df49fb4175b6a88fc93e3d95 Mon Sep 17 00:00:00 2001 From: Richard Alam Date: Tue, 11 Apr 2017 19:32:18 +0000 Subject: [PATCH 78/91] Check userId when validating token - check if the client validating the token mathes the userId of the registered user. If we don't, the client can pass any token and impersonate other users. --- .../org/bigbluebutton/core/apps/UsersApp.scala | 4 ++-- .../bigbluebutton/core/apps/UsersModel.scala | 17 +++++++++++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/UsersApp.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/UsersApp.scala index c903af8277..9000a7afbd 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/UsersApp.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/UsersApp.scala @@ -86,7 +86,7 @@ trait UsersApp { def handleValidateAuthToken(msg: ValidateAuthToken) { log.info("Got ValidateAuthToken message. meetingId=" + msg.meetingID + " userId=" + msg.userId) - usersModel.getRegisteredUserWithToken(msg.token) match { + usersModel.getRegisteredUserWithToken(msg.token, msg.userId) match { case Some(u) => { val replyTo = mProps.meetingID + '/' + msg.userId @@ -319,7 +319,7 @@ trait UsersApp { def handleUserJoin(msg: UserJoining): Unit = { log.debug("Received user joined meeting. metingId=" + mProps.meetingID + " userId=" + msg.userID) - val regUser = usersModel.getRegisteredUserWithToken(msg.authToken) + val regUser = usersModel.getRegisteredUserWithToken(msg.authToken, msg.userID) regUser foreach { ru => log.debug("Found registered user. metingId=" + mProps.meetingID + " userId=" + msg.userID + " ru=" + ru) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/UsersModel.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/UsersModel.scala index 13d911e920..4dc495a5a8 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/UsersModel.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/UsersModel.scala @@ -38,8 +38,21 @@ class UsersModel { regUsers += token -> regUser } - def getRegisteredUserWithToken(token: String): Option[RegisteredUser] = { - regUsers.get(token) + def getRegisteredUserWithToken(token: String, userId: String): Option[RegisteredUser] = { + + def isSameUserId(ru: RegisteredUser, userId: String): Option[RegisteredUser] = { + if (userId.startsWith(ru.id)) { + Some(ru) + } else { + None + } + } + + for { + ru <- regUsers.get(token) + user <- isSameUserId(ru, userId) + } yield user + } def generateWebUserId: String = { From b66fbf4bc49fa1e269122d71503395f37350b0e0 Mon Sep 17 00:00:00 2001 From: Anton Georgiev Date: Tue, 11 Apr 2017 15:39:50 -0400 Subject: [PATCH 79/91] align emojis horizontally --- .../imports/ui/components/user-avatar/styles.scss | 1 + 1 file changed, 1 insertion(+) mode change 100755 => 100644 bigbluebutton-html5/imports/ui/components/user-avatar/styles.scss diff --git a/bigbluebutton-html5/imports/ui/components/user-avatar/styles.scss b/bigbluebutton-html5/imports/ui/components/user-avatar/styles.scss old mode 100755 new mode 100644 index 27b98d79b5..c78fb3798f --- a/bigbluebutton-html5/imports/ui/components/user-avatar/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/user-avatar/styles.scss @@ -19,6 +19,7 @@ $moderator-bg: $color-primary; flex-shrink: 0; line-height: 2.2rem; justify-content: center; + align-items: center; position: relative; display: flex; flex-flow: column; From 419902cc8b2890a61b94f235753119e155176c59 Mon Sep 17 00:00:00 2001 From: Klaus Klein Date: Tue, 11 Apr 2017 16:46:51 -0300 Subject: [PATCH 80/91] Fixed user leave problem on private chat, close #3799 HTML5 --- .../api/users/server/modifiers/removeUser.js | 4 +-- .../imports/api/users/server/publishers.js | 5 +-- .../imports/startup/server/userPermissions.js | 1 + .../imports/ui/components/chat/container.jsx | 2 +- .../message-list-item/component.jsx | 5 +-- .../imports/ui/components/chat/service.js | 35 ++++++------------- .../ui/components/user-avatar/component.jsx | 5 +-- .../ui/components/user-list/service.js | 4 +-- 8 files changed, 23 insertions(+), 38 deletions(-) diff --git a/bigbluebutton-html5/imports/api/users/server/modifiers/removeUser.js b/bigbluebutton-html5/imports/api/users/server/modifiers/removeUser.js index f0b5d2e941..54ec71ad10 100644 --- a/bigbluebutton-html5/imports/api/users/server/modifiers/removeUser.js +++ b/bigbluebutton-html5/imports/api/users/server/modifiers/removeUser.js @@ -17,7 +17,7 @@ export default function removeUser(meetingId, userId) { const User = Users.findOne(selector); - if (User && User.clientType !== CLIENT_TYPE_HTML) { + /*if (User && User.clientType !== CLIENT_TYPE_HTML) { const cb = (err, numChanged) => { if (err) { return Logger.error(`Removing user from collection: ${err}`); @@ -29,7 +29,7 @@ export default function removeUser(meetingId, userId) { }; return Users.remove(selector, cb); - } + }*/ const modifier = { $set: { diff --git a/bigbluebutton-html5/imports/api/users/server/publishers.js b/bigbluebutton-html5/imports/api/users/server/publishers.js index d37df26103..454f87323a 100644 --- a/bigbluebutton-html5/imports/api/users/server/publishers.js +++ b/bigbluebutton-html5/imports/api/users/server/publishers.js @@ -44,10 +44,7 @@ Meteor.publish('users', function (credentials) { }); const selector = { - meetingId, - 'user.connection_status': { - $in: ['online', ''], - }, + meetingId }; const options = { diff --git a/bigbluebutton-html5/imports/startup/server/userPermissions.js b/bigbluebutton-html5/imports/startup/server/userPermissions.js index c792ab3d1b..4fea3ba0ea 100755 --- a/bigbluebutton-html5/imports/startup/server/userPermissions.js +++ b/bigbluebutton-html5/imports/startup/server/userPermissions.js @@ -132,6 +132,7 @@ export function isAllowedTo(action, credentials) { null != user && authToken === user.authToken && user.validated && + user.user.connection_status === 'online' && 'HTML5' === user.clientType && null != user.user; diff --git a/bigbluebutton-html5/imports/ui/components/chat/container.jsx b/bigbluebutton-html5/imports/ui/components/chat/container.jsx index 7934eac7a3..5eea4f6a0c 100755 --- a/bigbluebutton-html5/imports/ui/components/chat/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/container.jsx @@ -62,7 +62,7 @@ export default injectIntl(createContainer(({ params, intl }) => { title = intl.formatMessage(intlMessages.titlePrivate, { name: user.name }); chatName = user.name; - if (user.isLoggedOut) { + if (!user.isOnline) { let time = Date.now(); let id = `partner-disconnected-${time}`; let messagePartnerLoggedOut = { diff --git a/bigbluebutton-html5/imports/ui/components/chat/message-list/message-list-item/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/message-list/message-list-item/component.jsx index bc9027b1c2..41d3a46632 100755 --- a/bigbluebutton-html5/imports/ui/components/chat/message-list/message-list-item/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/message-list/message-list-item/component.jsx @@ -19,6 +19,7 @@ const defaultProps = { export default class MessageListItem extends Component { constructor(props) { super(props); + } render() { @@ -41,9 +42,9 @@ export default class MessageListItem extends Component {
    -
    +
    {user.name} - {user.isLoggedOut ? (offline) : null} + {user.isOnline ? null : (offline)}