瀏覽代碼

Merge branch 'master' into talk-muted

master
Lyubomir Marinov 8 年之前
父節點
當前提交
c95a8e058c
共有 72 個檔案被更改,包括 3519 行新增1870 行删除
  1. 1
    0
      .gitignore
  2. 11
    2
      app.js
  3. 8
    0
      authError.html
  4. 8
    0
      close.html
  5. 230
    31
      conference.js
  6. 2
    0
      config.js
  7. 二進制
      css/.DS_Store
  8. 12
    15
      css/_base.scss
  9. 0
    5
      css/_chat.scss
  10. 4
    5
      css/_contact_list.scss
  11. 0
    6
      css/_font-awesome.scss
  12. 12
    7
      css/_font.scss
  13. 2
    1
      css/_jquery-impromptu.scss
  14. 13
    0
      css/_mixins.scss
  15. 12
    0
      css/_redirect_page.scss
  16. 8
    7
      css/_side_toolbar_container.scss
  17. 36
    8
      css/_toolbars.scss
  18. 32
    6
      css/_variables.scss
  19. 212
    105
      css/_videolayout_default.scss
  20. 4
    1
      css/main.scss
  21. 53
    0
      css/modals/_dialog.scss
  22. 44
    43
      css/modals/feedback/_feedback.scss
  23. 6
    1
      css/ringing/_ringing.scss
  24. 二進制
      fonts/jitsi.eot
  25. 2
    0
      fonts/jitsi.svg
  26. 二進制
      fonts/jitsi.ttf
  27. 二進制
      fonts/jitsi.woff
  28. 52
    0
      fonts/selection.json
  29. 26
    22
      index.html
  30. 10
    1
      interface_config.js
  31. 2
    0
      lang/languages-de.json
  32. 15
    0
      lang/languages-pl.json
  33. 2
    0
      lang/languages-ptBR.json
  34. 2
    0
      lang/languages.json
  35. 57
    40
      lang/main-de.json
  36. 344
    0
      lang/main-pl.json
  37. 43
    26
      lang/main-ptBR.json
  38. 17
    15
      lang/main.json
  39. 1
    0
      modules/TokenData/TokenData.js
  40. 0
    321
      modules/UI/Feedback.js
  41. 88
    79
      modules/UI/UI.js
  42. 121
    216
      modules/UI/audio_levels/AudioLevels.js
  43. 0
    108
      modules/UI/audio_levels/CanvasUtils.js
  44. 30
    2
      modules/UI/authentication/RoomLocker.js
  45. 128
    0
      modules/UI/feedback/Feedback.js
  46. 193
    0
      modules/UI/feedback/FeedbackWindow.js
  47. 4
    1
      modules/UI/recording/Recording.js
  48. 57
    15
      modules/UI/ring_overlay/RingOverlay.js
  49. 1
    5
      modules/UI/shared_video/SharedVideo.js
  50. 40
    31
      modules/UI/side_pannels/chat/Chat.js
  51. 5
    6
      modules/UI/side_pannels/chat/Replacement.js
  52. 47
    0
      modules/UI/side_pannels/chat/smileys.js
  53. 0
    48
      modules/UI/side_pannels/chat/smileys.json
  54. 10
    18
      modules/UI/side_pannels/contactlist/ContactList.js
  55. 2
    6
      modules/UI/side_pannels/settings/SettingsMenu.js
  56. 67
    19
      modules/UI/toolbars/Toolbar.js
  57. 100
    6
      modules/UI/util/UIUtil.js
  58. 37
    12
      modules/UI/videolayout/ConnectionIndicator.js
  59. 138
    42
      modules/UI/videolayout/FilmStrip.js
  60. 501
    0
      modules/UI/videolayout/LargeVideoManager.js
  61. 25
    19
      modules/UI/videolayout/LocalVideo.js
  62. 171
    30
      modules/UI/videolayout/RemoteVideo.js
  63. 210
    136
      modules/UI/videolayout/SmallVideo.js
  64. 111
    325
      modules/UI/videolayout/VideoContainer.js
  65. 102
    49
      modules/UI/videolayout/VideoLayout.js
  66. 7
    5
      modules/UI/welcome_page/WelcomePage.js
  67. 1
    8
      modules/keyboardshortcut/keyboardshortcut.js
  68. 8
    4
      modules/settings/Settings.js
  69. 14
    6
      package.json
  70. 1
    1
      prosody-plugins/mod_token_verification.lua
  71. 11
    2
      service/UI/UIEvents.js
  72. 6
    3
      service/translation/languages.js

+ 1
- 0
.gitignore 查看文件

1
 node_modules
1
 node_modules
2
+.DS_Store
2
 *.swp
3
 *.swp
3
 .idea/
4
 .idea/
4
 *.iml
5
 *.iml

+ 11
- 2
app.js 查看文件

8
 import "strophe";
8
 import "strophe";
9
 import "strophe-disco";
9
 import "strophe-disco";
10
 import "strophe-caps";
10
 import "strophe-caps";
11
-import "tooltip";
12
-import "popover";
13
 import "jQuery-Impromptu";
11
 import "jQuery-Impromptu";
14
 import "autosize";
12
 import "autosize";
13
+
14
+import 'aui';
15
+import 'aui-experimental';
16
+import 'aui-css';
17
+import 'aui-experimental-css';
18
+
15
 window.toastr = require("toastr");
19
 window.toastr = require("toastr");
16
 
20
 
17
 import URLProcessor from "./modules/config/URLProcessor";
21
 import URLProcessor from "./modules/config/URLProcessor";
106
     var isUIReady = APP.UI.start();
110
     var isUIReady = APP.UI.start();
107
     if (isUIReady) {
111
     if (isUIReady) {
108
         APP.conference.init({roomName: buildRoomName()}).then(function () {
112
         APP.conference.init({roomName: buildRoomName()}).then(function () {
113
+            let server = APP.tokenData.server;
114
+            if(server) {
115
+                APP.conference.logEvent("server." + server, 1);
116
+            }
117
+
109
             APP.UI.initConference();
118
             APP.UI.initConference();
110
 
119
 
111
             APP.UI.addListener(UIEvents.LANG_CHANGED, function (language) {
120
             APP.UI.addListener(UIEvents.LANG_CHANGED, function (language) {

+ 8
- 0
authError.html 查看文件

1
+<html>
2
+<head>
3
+    <link rel="stylesheet" href="css/all.css"/>
4
+</head>
5
+<body>
6
+    <div class="redirectPageMessage">Sorry! You are not allowed to be here :(</div>
7
+</body>
8
+</html>

+ 8
- 0
close.html 查看文件

1
+<html>
2
+<head>
3
+    <link rel="stylesheet" href="css/all.css"/>
4
+</head>
5
+<body>
6
+    <div class="redirectPageMessage">Thank you for your feedback!</div>
7
+</body>
8
+</html>

+ 230
- 31
conference.js 查看文件

40
  */
40
  */
41
 let DSExternalInstallationInProgress = false;
41
 let DSExternalInstallationInProgress = false;
42
 
42
 
43
-import {VIDEO_CONTAINER_TYPE} from "./modules/UI/videolayout/LargeVideo";
43
+/**
44
+ * Listens whether conference had been left from local user when we are trying
45
+ * to navigate away from current page.
46
+ * @type {ConferenceLeftListener}
47
+ */
48
+let conferenceLeftListener = null;
49
+
50
+import {VIDEO_CONTAINER_TYPE} from "./modules/UI/videolayout/VideoContainer";
44
 
51
 
45
 /**
52
 /**
46
  * Known custom conference commands.
53
  * Known custom conference commands.
203
 
210
 
204
 /**
211
 /**
205
  * Check if the welcome page is enabled and redirects to it.
212
  * Check if the welcome page is enabled and redirects to it.
213
+ * If requested show a thank you dialog before that.
214
+ * If we have a close page enabled, redirect to it without
215
+ * showing any other dialog.
216
+ * @param {boolean} showThankYou whether we should show a thank you dialog
206
  */
217
  */
207
-function maybeRedirectToWelcomePage() {
218
+function maybeRedirectToWelcomePage(showThankYou) {
219
+
220
+    // if close page is enabled redirect to it, without further action
221
+    if (config.enableClosePage) {
222
+        window.location.pathname = "close.html";
223
+        return;
224
+    }
225
+
226
+    if (showThankYou) {
227
+        APP.UI.messageHandler.openMessageDialog(
228
+            null, null, null,
229
+            APP.translation.translateString(
230
+                "dialog.thankYou", {appName:interfaceConfig.APP_NAME}
231
+            )
232
+        );
233
+    }
234
+
208
     if (!config.enableWelcomePage) {
235
     if (!config.enableWelcomePage) {
209
         return;
236
         return;
210
     }
237
     }
236
  * @param {boolean} [requestFeedback=false] if user feedback should be requested
263
  * @param {boolean} [requestFeedback=false] if user feedback should be requested
237
  */
264
  */
238
 function hangup (requestFeedback = false) {
265
 function hangup (requestFeedback = false) {
239
-    const errCallback = (f, err) => {
266
+    const errCallback = (err) => {
240
 
267
 
241
         // If we want to break out the chain in our error handler, it needs
268
         // If we want to break out the chain in our error handler, it needs
242
         // to return a rejected promise. In the case of feedback request
269
         // to return a rejected promise. In the case of feedback request
251
         }
278
         }
252
     };
279
     };
253
     const disconnect = disconnectAndShowFeedback.bind(null, requestFeedback);
280
     const disconnect = disconnectAndShowFeedback.bind(null, requestFeedback);
281
+
282
+    if (!conferenceLeftListener)
283
+        conferenceLeftListener = new ConferenceLeftListener();
284
+
285
+    // Make sure that leave is resolved successfully and the set the handlers
286
+    // to be invoked once conference had been left
254
     APP.conference._room.leave()
287
     APP.conference._room.leave()
255
-    .then(disconnect)
256
-    .catch(errCallback.bind(null, disconnect))
257
-    .then(maybeRedirectToWelcomePage)
258
-    .catch(function(err){
259
-            console.log(err);
260
-        });
288
+        .then(conferenceLeftListener.setHandler(disconnect, errCallback))
289
+        .catch(errCallback);
290
+}
291
+
292
+/**
293
+ * Listens for CONFERENCE_LEFT event so we can check whether it has finished.
294
+ * The handler will be called once the conference had been left or if it
295
+ * was already left when we are adding the handler.
296
+ */
297
+class ConferenceLeftListener {
298
+    /**
299
+     * Creates ConferenceLeftListener and start listening for conference
300
+     * failed event.
301
+     */
302
+    constructor() {
303
+        room.on(ConferenceEvents.CONFERENCE_LEFT,
304
+            this._handleConferenceLeft.bind(this));
305
+    }
261
 
306
 
307
+    /**
308
+     * Handles the conference left event, if we have a handler we invoke it.
309
+     * @private
310
+     */
311
+    _handleConferenceLeft() {
312
+        this.conferenceLeft = true;
313
+
314
+        if (this.handler)
315
+            this._handleLeave();
316
+    }
317
+
318
+    /**
319
+     * Sets the handlers. If we already left the conference invoke them.
320
+     * @param handler
321
+     * @param errCallback
322
+     */
323
+    setHandler (handler, errCallback) {
324
+        this.handler = handler;
325
+        this.errCallback = errCallback;
326
+
327
+        if (this.conferenceLeft)
328
+            this._handleLeave();
329
+    }
330
+
331
+    /**
332
+     * Invokes the handlers.
333
+     * @private
334
+     */
335
+    _handleLeave()
336
+    {
337
+        this.handler()
338
+            .catch(this.errCallback)
339
+            .then(maybeRedirectToWelcomePage)
340
+            .catch(function(err){
341
+                console.log(err);
342
+            });
343
+    }
262
 }
344
 }
263
 
345
 
264
 /**
346
 /**
294
             firefox_fake_device: config.firefox_fake_device,
376
             firefox_fake_device: config.firefox_fake_device,
295
             desktopSharingExtensionExternalInstallation:
377
             desktopSharingExtensionExternalInstallation:
296
                 options.desktopSharingExtensionExternalInstallation
378
                 options.desktopSharingExtensionExternalInstallation
297
-        }, checkForPermissionPrompt)
298
-        .catch(function (err) {
379
+        }, checkForPermissionPrompt).then( (tracks) => {
380
+            tracks.forEach((track) => {
381
+                track.on(TrackEvents.NO_DATA_FROM_SOURCE,
382
+                    APP.UI.showTrackNotWorkingDialog.bind(null, track));
383
+            });
384
+            return tracks;
385
+        }).catch(function (err) {
299
             console.error(
386
             console.error(
300
                 'failed to create local tracks', options.devices, err);
387
                 'failed to create local tracks', options.devices, err);
301
             return Promise.reject(err);
388
             return Promise.reject(err);
358
         case ConferenceErrors.PASSWORD_REQUIRED:
445
         case ConferenceErrors.PASSWORD_REQUIRED:
359
             APP.UI.markRoomLocked(true);
446
             APP.UI.markRoomLocked(true);
360
             roomLocker.requirePassword().then(function () {
447
             roomLocker.requirePassword().then(function () {
448
+                let pass = roomLocker.password;
449
+                // we received that password is required, but user is trying
450
+                // anyway to login without a password, mark room as not locked
451
+                // in case he succeeds (maybe someone removed the password
452
+                // meanwhile), if it is still locked another password required
453
+                // will be received and the room again will be marked as locked
454
+                if (!pass)
455
+                    APP.UI.markRoomLocked(false);
361
                 room.join(roomLocker.password);
456
                 room.join(roomLocker.password);
362
             });
457
             });
363
             break;
458
             break;
369
             }
464
             }
370
             break;
465
             break;
371
 
466
 
467
+        case ConferenceErrors.NOT_ALLOWED_ERROR:
468
+            {
469
+                // let's show some auth not allowed page
470
+                window.location.pathname = "authError.html";
471
+            }
472
+            break;
473
+
372
         case ConferenceErrors.VIDEOBRIDGE_NOT_AVAILABLE:
474
         case ConferenceErrors.VIDEOBRIDGE_NOT_AVAILABLE:
373
             APP.UI.notifyBridgeDown();
475
             APP.UI.notifyBridgeDown();
374
             break;
476
             break;
649
         return this._room
751
         return this._room
650
             && this._room.getConnectionState();
752
             && this._room.getConnectionState();
651
     },
753
     },
754
+    /**
755
+     * Checks whether or not our connection is currently in interrupted and
756
+     * reconnect attempts are in progress.
757
+     *
758
+     * @returns {boolean} true if the connection is in interrupted state or
759
+     * false otherwise.
760
+     */
761
+    isConnectionInterrupted () {
762
+        return connectionIsInterrupted;
763
+    },
764
+    /**
765
+     * Finds JitsiParticipant for given id.
766
+     *
767
+     * @param {string} id participant's identifier(MUC nickname).
768
+     *
769
+     * @returns {JitsiParticipant|null} participant instance for given id or
770
+     * null if not found.
771
+     */
772
+    getParticipantById (id) {
773
+        return room ? room.getParticipantById(id) : null;
774
+    },
775
+    /**
776
+     * Checks whether the user identified by given id is currently connected.
777
+     *
778
+     * @param {string} id participant's identifier(MUC nickname)
779
+     *
780
+     * @returns {boolean|null} true if participant's connection is ok or false
781
+     * if the user is having connectivity issues.
782
+     */
783
+    isParticipantConnectionActive (id) {
784
+        let participant = this.getParticipantById(id);
785
+        return participant ? participant.isConnectionActive() : null;
786
+    },
787
+    /**
788
+     * Gets the display name foe the <tt>JitsiParticipant</tt> identified by
789
+     * the given <tt>id</tt>.
790
+     *
791
+     * @param id {string} the participant's id(MUC nickname/JVB endpoint id)
792
+     *
793
+     * @return {string} the participant's display name or the default string if
794
+     * absent.
795
+     */
796
+    getParticipantDisplayName (id) {
797
+        let displayName = getDisplayName(id);
798
+        if (displayName) {
799
+            return displayName;
800
+        } else {
801
+            if (APP.conference.isLocalId(id)) {
802
+                return APP.translation.generateTranslationHTML(
803
+                    interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME);
804
+            } else {
805
+                return interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME;
806
+            }
807
+        }
808
+    },
652
     getMyUserId () {
809
     getMyUserId () {
653
         return this._room
810
         return this._room
654
             && this._room.myUserId();
811
             && this._room.myUserId();
699
         return room.getLogs();
856
         return room.getLogs();
700
     },
857
     },
701
 
858
 
859
+    /**
860
+     * Download logs, a function that can be called from console while
861
+     * debugging.
862
+     * @param filename (optional) specify target filename
863
+     */
864
+    saveLogs (filename = 'meetlog.json') {
865
+        // this can be called from console and will not have reference to this
866
+        // that's why we reference the global var
867
+        let logs = APP.conference.getLogs();
868
+        let data = encodeURIComponent(JSON.stringify(logs, null, '  '));
869
+
870
+        let elem = document.createElement('a');
871
+
872
+        elem.download = filename;
873
+        elem.href = 'data:application/json;charset=utf-8,\n' + data;
874
+        elem.dataset.downloadurl
875
+            = ['text/json', elem.download, elem.href].join(':');
876
+        elem.dispatchEvent(new MouseEvent('click', {
877
+            view: window,
878
+            bubbles: true,
879
+            cancelable: false
880
+        }));
881
+    },
882
+
702
     /**
883
     /**
703
      * Exposes a Command(s) API on this instance. It is necessitated by (1) the
884
      * Exposes a Command(s) API on this instance. It is necessitated by (1) the
704
      * desire to keep room private to this instance and (2) the need of other
885
      * desire to keep room private to this instance and (2) the need of other
858
 
1039
 
859
         return promise.then(function () {
1040
         return promise.then(function () {
860
             if (stream) {
1041
             if (stream) {
861
-                stream.on(TrackEvents.TRACK_AUDIO_NOT_WORKING,
862
-                    APP.UI.showAudioNotWorkingDialog);
863
                 return room.addTrack(stream);
1042
                 return room.addTrack(stream);
864
             }
1043
             }
865
         }).then(() => {
1044
         }).then(() => {
1021
 
1200
 
1022
             console.log('USER %s connnected', id, user);
1201
             console.log('USER %s connnected', id, user);
1023
             APP.API.notifyUserJoined(id);
1202
             APP.API.notifyUserJoined(id);
1024
-            APP.UI.addUser(id, user.getDisplayName());
1203
+            APP.UI.addUser(user);
1025
 
1204
 
1026
             // check the roles for the new user and reflect them
1205
             // check the roles for the new user and reflect them
1027
             APP.UI.updateUserRole(user);
1206
             APP.UI.updateUserRole(user);
1037
         room.on(ConferenceEvents.USER_ROLE_CHANGED, (id, role) => {
1216
         room.on(ConferenceEvents.USER_ROLE_CHANGED, (id, role) => {
1038
             if (this.isLocalId(id)) {
1217
             if (this.isLocalId(id)) {
1039
                 console.info(`My role changed, new role: ${role}`);
1218
                 console.info(`My role changed, new role: ${role}`);
1040
-                this.isModerator = room.isModerator();
1041
-                APP.UI.updateLocalRole(room.isModerator());
1219
+                if (this.isModerator !== room.isModerator()) {
1220
+                    this.isModerator = room.isModerator();
1221
+                    APP.UI.updateLocalRole(room.isModerator());
1222
+                }
1042
             } else {
1223
             } else {
1043
                 let user = room.getParticipantById(id);
1224
                 let user = room.getParticipantById(id);
1044
                 if (user) {
1225
                 if (user) {
1115
             ConferenceEvents.LAST_N_ENDPOINTS_CHANGED, (ids, enteringIds) => {
1296
             ConferenceEvents.LAST_N_ENDPOINTS_CHANGED, (ids, enteringIds) => {
1116
             APP.UI.handleLastNEndpoints(ids, enteringIds);
1297
             APP.UI.handleLastNEndpoints(ids, enteringIds);
1117
         });
1298
         });
1299
+        room.on(
1300
+            ConferenceEvents.PARTICIPANT_CONN_STATUS_CHANGED,
1301
+            (id, isActive) => {
1302
+                APP.UI.participantConnectionStatusChanged(id, isActive);
1303
+        });
1118
         room.on(ConferenceEvents.DOMINANT_SPEAKER_CHANGED, (id) => {
1304
         room.on(ConferenceEvents.DOMINANT_SPEAKER_CHANGED, (id) => {
1119
             if (this.isLocalId(id)) {
1305
             if (this.isLocalId(id)) {
1120
                 this.isDominantSpeaker = true;
1306
                 this.isDominantSpeaker = true;
1146
         room.on(ConferenceEvents.CONNECTION_INTERRUPTED, () => {
1332
         room.on(ConferenceEvents.CONNECTION_INTERRUPTED, () => {
1147
             connectionIsInterrupted = true;
1333
             connectionIsInterrupted = true;
1148
             ConnectionQuality.updateLocalConnectionQuality(0);
1334
             ConnectionQuality.updateLocalConnectionQuality(0);
1335
+            APP.UI.showLocalConnectionInterrupted(true);
1149
         });
1336
         });
1150
 
1337
 
1151
         room.on(ConferenceEvents.CONNECTION_RESTORED, () => {
1338
         room.on(ConferenceEvents.CONNECTION_RESTORED, () => {
1152
             connectionIsInterrupted = false;
1339
             connectionIsInterrupted = false;
1340
+            APP.UI.showLocalConnectionInterrupted(false);
1153
         });
1341
         });
1154
 
1342
 
1155
         room.on(ConferenceEvents.DISPLAY_NAME_CHANGED, (id, displayName) => {
1343
         room.on(ConferenceEvents.DISPLAY_NAME_CHANGED, (id, displayName) => {
1173
             console.log("Received channel password lock change: ", state,
1361
             console.log("Received channel password lock change: ", state,
1174
                 error);
1362
                 error);
1175
             APP.UI.markRoomLocked(state);
1363
             APP.UI.markRoomLocked(state);
1364
+            roomLocker.lockedElsewhere = state;
1176
         });
1365
         });
1177
 
1366
 
1178
         room.on(ConferenceEvents.USER_STATUS_CHANGED, function (id, status) {
1367
         room.on(ConferenceEvents.USER_STATUS_CHANGED, function (id, status) {
1300
                 && APP.UI.notifyInitiallyMuted();
1489
                 && APP.UI.notifyInitiallyMuted();
1301
         });
1490
         });
1302
 
1491
 
1303
-        APP.UI.addListener(UIEvents.USER_INVITED, (roomUrl) => {
1304
-            APP.UI.inviteParticipants(
1305
-                roomUrl,
1306
-                APP.conference.roomName,
1307
-                roomLocker.password,
1308
-                APP.settings.getDisplayName()
1309
-            );
1310
-        });
1311
-
1312
         room.on(
1492
         room.on(
1313
             ConferenceEvents.AVAILABLE_DEVICES_CHANGED, function (id, devices) {
1493
             ConferenceEvents.AVAILABLE_DEVICES_CHANGED, function (id, devices) {
1314
                 APP.UI.updateDevicesAvailability(id, devices);
1494
                 APP.UI.updateDevicesAvailability(id, devices);
1395
 
1575
 
1396
         APP.UI.addListener(UIEvents.PINNED_ENDPOINT, (smallVideo, isPinned) => {
1576
         APP.UI.addListener(UIEvents.PINNED_ENDPOINT, (smallVideo, isPinned) => {
1397
             var smallVideoId = smallVideo.getId();
1577
             var smallVideoId = smallVideo.getId();
1578
+            // FIXME why VIDEO_CONTAINER_TYPE instead of checking if
1579
+            // the participant is on the large video ?
1398
             if (smallVideo.getVideoType() === VIDEO_CONTAINER_TYPE
1580
             if (smallVideo.getVideoType() === VIDEO_CONTAINER_TYPE
1399
                 && !APP.conference.isLocalId(smallVideoId)) {
1581
                 && !APP.conference.isLocalId(smallVideoId)) {
1400
 
1582
 
1422
                 .then(([stream]) => {
1604
                 .then(([stream]) => {
1423
                     this.useVideoStream(stream);
1605
                     this.useVideoStream(stream);
1424
                     console.log('switched local video device');
1606
                     console.log('switched local video device');
1425
-                    APP.settings.setCameraDeviceId(cameraDeviceId);
1607
+                    APP.settings.setCameraDeviceId(cameraDeviceId, true);
1426
                 })
1608
                 })
1427
                 .catch((err) => {
1609
                 .catch((err) => {
1428
                     APP.UI.showDeviceErrorDialog(null, err);
1610
                     APP.UI.showDeviceErrorDialog(null, err);
1444
                 .then(([stream]) => {
1626
                 .then(([stream]) => {
1445
                     this.useAudioStream(stream);
1627
                     this.useAudioStream(stream);
1446
                     console.log('switched local audio device');
1628
                     console.log('switched local audio device');
1447
-                    APP.settings.setMicDeviceId(micDeviceId);
1629
+                    APP.settings.setMicDeviceId(micDeviceId, true);
1448
                 })
1630
                 })
1449
                 .catch((err) => {
1631
                 .catch((err) => {
1450
                     APP.UI.showDeviceErrorDialog(err, null);
1632
                     APP.UI.showDeviceErrorDialog(err, null);
1539
                 // storage and settings menu. This is a workaround until
1721
                 // storage and settings menu. This is a workaround until
1540
                 // getConstraints() method will be implemented in browsers.
1722
                 // getConstraints() method will be implemented in browsers.
1541
                 if (localAudio) {
1723
                 if (localAudio) {
1542
-                    localAudio._setRealDeviceIdFromDeviceList(devices);
1543
-                    APP.settings.setMicDeviceId(localAudio.getDeviceId());
1724
+                    APP.settings.setMicDeviceId(
1725
+                        localAudio.getDeviceId(), false);
1544
                 }
1726
                 }
1545
 
1727
 
1546
                 if (localVideo) {
1728
                 if (localVideo) {
1547
-                    localVideo._setRealDeviceIdFromDeviceList(devices);
1548
-                    APP.settings.setCameraDeviceId(localVideo.getDeviceId());
1729
+                    APP.settings.setCameraDeviceId(
1730
+                        localVideo.getDeviceId(), false);
1549
                 }
1731
                 }
1550
 
1732
 
1551
                 mediaDeviceHelper.setCurrentMediaDevices(devices);
1733
                 mediaDeviceHelper.setCurrentMediaDevices(devices);
1646
     setRaisedHand(raisedHand) {
1828
     setRaisedHand(raisedHand) {
1647
         if (raisedHand !== this.isHandRaised)
1829
         if (raisedHand !== this.isHandRaised)
1648
         {
1830
         {
1831
+            APP.UI.onLocalRaiseHandChanged(raisedHand);
1832
+
1649
             this.isHandRaised = raisedHand;
1833
             this.isHandRaised = raisedHand;
1650
             // Advertise the updated status
1834
             // Advertise the updated status
1651
             room.setLocalParticipantProperty("raisedHand", raisedHand);
1835
             room.setLocalParticipantProperty("raisedHand", raisedHand);
1652
             // Update the view
1836
             // Update the view
1653
             APP.UI.setLocalRaisedHandStatus(raisedHand);
1837
             APP.UI.setLocalRaisedHandStatus(raisedHand);
1654
         }
1838
         }
1839
+    },
1840
+    /**
1841
+     * Log event to callstats and analytics.
1842
+     * @param {string} name the event name
1843
+     * @param {int} value the value (it's int because google analytics supports
1844
+     * only int).
1845
+     * NOTE: Should be used after conference.init
1846
+     */
1847
+    logEvent(name, value) {
1848
+        if(JitsiMeetJS.analytics) {
1849
+            JitsiMeetJS.analytics.sendEvent(name, value);
1850
+        }
1851
+        if(room) {
1852
+            room.sendApplicationLog(JSON.stringify({name, value}));
1853
+        }
1655
     }
1854
     }
1656
 };
1855
 };

+ 2
- 0
config.js 查看文件

56
     //disableAdaptiveSimulcast: false,
56
     //disableAdaptiveSimulcast: false,
57
     enableRecording: false,
57
     enableRecording: false,
58
     enableWelcomePage: true,
58
     enableWelcomePage: true,
59
+    //enableClosePage: false, // enabling the close page will ignore the welcome
60
+                              // page redirection when call is hangup
59
     disableSimulcast: false,
61
     disableSimulcast: false,
60
     logStats: false, // Enable logging of PeerConnection stats via the focus
62
     logStats: false, // Enable logging of PeerConnection stats via the focus
61
 //    requireDisplayName: true, // Forces the participants that doesn't have display name to enter it when they enter the room.
63
 //    requireDisplayName: true, // Forces the participants that doesn't have display name to enter it when they enter the room.

二進制
css/.DS_Store 查看文件


+ 12
- 15
css/_base.scss 查看文件

81
     display: block;
81
     display: block;
82
 }
82
 }
83
 
83
 
84
-#downloadlog {
85
-    display: none;
86
-    position: absolute;
87
-    bottom: 5;
88
-    left: 5;
89
-    overflow: visible;
90
-    color: rgba(255,255,255,.50);
91
-}
92
-
93
 .active {
84
 .active {
94
     background-color: #00ccff;
85
     background-color: #00ccff;
95
 }
86
 }
96
 
87
 
97
-.glow
98
-{
99
-    text-shadow: 0px 0px 30px #06a5df, 0px 0px 10px #06a5df, 0px 0px 5px #06a5df,0px 0px 3px #06a5df;
100
-}
101
-
102
 .watermark {
88
 .watermark {
103
     display: block;
89
     display: block;
104
     position: absolute;
90
     position: absolute;
175
     display: -ms-flexbox !important;
161
     display: -ms-flexbox !important;
176
     display: -webkit-flex !important;
162
     display: -webkit-flex !important;
177
     display: flex !important;
163
     display: flex !important;
178
-}
164
+}
165
+
166
+.tipsy {
167
+    z-index: $tooltipsZ;
168
+    &-inner {
169
+        background-color: $tooltipBg;
170
+    }
171
+
172
+    &-arrow {
173
+        border-color: $tooltipBg;
174
+    }
175
+}

+ 0
- 5
css/_chat.scss 查看文件

104
     color: #a7a7a7;
104
     color: #a7a7a7;
105
 }
105
 }
106
 
106
 
107
-#unreadMessages {
108
-    font-size: 8px;
109
-    position: absolute;
110
-}
111
-
112
 #chat_container .username {
107
 #chat_container .username {
113
     float: left;
108
     float: left;
114
     padding-left: 5px;
109
     padding-left: 5px;

+ 4
- 5
css/_contact_list.scss 查看文件

2
     cursor: default;
2
     cursor: default;
3
 
3
 
4
     > ul#contacts {
4
     > ul#contacts {
5
-        position: absolute;
6
-        top: 31px;
5
+        font-size: 12px;
7
         bottom: 0px;
6
         bottom: 0px;
8
-        width: 100%;
9
         margin: 0px;
7
         margin: 0px;
10
         padding: 0px;
8
         padding: 0px;
9
+        width: 100%;
11
         overflow-y: scroll;
10
         overflow-y: scroll;
12
         overflow-x: hidden;
11
         overflow-x: hidden;
13
     }
12
     }
20
 #contacts {
19
 #contacts {
21
 
20
 
22
     >li {
21
     >li {
22
+        color: $defaultSideBarFontColor;
23
         list-style-type: none;
23
         list-style-type: none;
24
         text-align: left;
24
         text-align: left;
25
         white-space: nowrap;
25
         white-space: nowrap;
26
         color: #FFF;
26
         color: #FFF;
27
         font-size: 10pt;
27
         font-size: 10pt;
28
-        padding: 7px 10px;
29
-        margin: 2px;
28
+        padding: 6px 10%;
30
 
29
 
31
         &:hover,
30
         &:hover,
32
         &:active {
31
         &:active {

+ 0
- 6
css/_font-awesome.scss 查看文件

255
 .fa-road:before {
255
 .fa-road:before {
256
   content: "\f018";
256
   content: "\f018";
257
 }
257
 }
258
-.fa-download:before {
259
-  content: "\f019";
260
-}
261
 .fa-arrow-circle-o-down:before {
258
 .fa-arrow-circle-o-down:before {
262
   content: "\f01a";
259
   content: "\f01a";
263
 }
260
 }
842
 .fa-exchange:before {
839
 .fa-exchange:before {
843
   content: "\f0ec";
840
   content: "\f0ec";
844
 }
841
 }
845
-.fa-cloud-download:before {
846
-  content: "\f0ed";
847
-}
848
 .fa-cloud-upload:before {
842
 .fa-cloud-upload:before {
849
   content: "\f0ee";
843
   content: "\f0ee";
850
 }
844
 }

+ 12
- 7
css/_font.scss 查看文件

16
     font-weight: normal;
16
     font-weight: normal;
17
     font-variant: normal;
17
     font-variant: normal;
18
     text-transform: none;
18
     text-transform: none;
19
-    line-height: 0.75em;
19
+    line-height: 1.22em;
20
     font-size: 1.22em;
20
     font-size: 1.22em;
21
+    cursor: default;
21
 
22
 
22
     /* Better Font Rendering =========== */
23
     /* Better Font Rendering =========== */
23
     -webkit-font-smoothing: antialiased;
24
     -webkit-font-smoothing: antialiased;
42
 .icon-chat:before {
43
 .icon-chat:before {
43
   content: "\e906";
44
   content: "\e906";
44
 }
45
 }
45
-.icon-download:before {
46
-  content: "\e902";
47
-}
48
 .icon-edit:before {
46
 .icon-edit:before {
49
   content: "\e907";
47
   content: "\e907";
50
 }
48
 }
57
 .icon-kick:before {
55
 .icon-kick:before {
58
   content: "\e904";
56
   content: "\e904";
59
 }
57
 }
58
+.icon-menu-up:before {
59
+  content: "\e91f";
60
+}
61
+.icon-menu-down:before {
62
+  content: "\e920";
63
+}
60
 .icon-full-screen:before {
64
 .icon-full-screen:before {
61
   content: "\e90b";
65
   content: "\e90b";
62
 }
66
 }
63
 .icon-exit-full-screen:before {
67
 .icon-exit-full-screen:before {
64
   content: "\e90c";
68
   content: "\e90c";
65
 }
69
 }
70
+.icon-star:before {
71
+    content: "\e916";
72
+}
66
 .icon-star-full:before {
73
 .icon-star-full:before {
67
   content: "\e90a";
74
   content: "\e90a";
68
 }
75
 }
99
 .icon-settings:before {
106
 .icon-settings:before {
100
   content: "\e915";
107
   content: "\e915";
101
 }
108
 }
102
-.icon-star:before {
103
-  content: "\e916";
104
-}
105
 .icon-share-desktop:before {
109
 .icon-share-desktop:before {
106
   content: "\e917";
110
   content: "\e917";
107
 }
111
 }
126
 .icon-recEnable:before {
130
 .icon-recEnable:before {
127
   content: "\e614";
131
   content: "\e614";
128
 }
132
 }
133
+// FIXME not used anymore - consider removing in the next font update
129
 .icon-presentation:before {
134
 .icon-presentation:before {
130
   content: "\e603";
135
   content: "\e603";
131
 }
136
 }

+ 2
- 1
css/_jquery-impromptu.scss 查看文件

9
 }
9
 }
10
 div.jqi{ 
10
 div.jqi{ 
11
 	width: 400px;
11
 	width: 400px;
12
-	position: absolute; 
12
+	position: absolute;
13
+    color: #3a3a3a;
13
 	background-color: #ffffff; 
14
 	background-color: #ffffff; 
14
 	font-size: 11px; 
15
 	font-size: 11px; 
15
 	text-align: left; 
16
 	text-align: left; 

+ 13
- 0
css/_mixins.scss 查看文件

36
   }
36
   }
37
 }
37
 }
38
 
38
 
39
+@mixin circle($diameter) {
40
+    width: $diameter;
41
+    height: $diameter;
42
+    border-radius: 50%;
43
+}
44
+
45
+@mixin absoluteAligning($sizeX, $sizeY) {
46
+    top: 50%;
47
+    left: 50%;
48
+    position: absolute;
49
+    @include transform(translate(-#{$sizeX / 2}, -#{$sizeY / 2}))
50
+}
51
+
39
 @mixin transform($func) {
52
 @mixin transform($func) {
40
     -moz-transform: $func;
53
     -moz-transform: $func;
41
     -ms-transform: $func;
54
     -ms-transform: $func;

+ 12
- 0
css/_redirect_page.scss 查看文件

1
+html, body {
2
+    width: 100%;
3
+    height:100%;
4
+    color: $defaultColor;
5
+    background: $defaultBackground;
6
+}
7
+
8
+.redirectPageMessage {
9
+    text-align: center;
10
+    font-size: 36px;
11
+    margin-top: 20%;
12
+}

css/_side_toolbar_container.css → css/_side_toolbar_container.scss 查看文件

12
     background-color: rgba(0,0,0,0.8);
12
     background-color: rgba(0,0,0,0.8);
13
     z-index: 800;
13
     z-index: 800;
14
     overflow: hidden;
14
     overflow: hidden;
15
+    letter-spacing: 1px;
15
 
16
 
16
     /**
17
     /**
17
      * Labels inside the side panel.
18
      * Labels inside the side panel.
18
      */
19
      */
19
     label {
20
     label {
20
-        color: $defaultSemiDarkColor;
21
+        color: $defaultColor;
21
     }
22
     }
22
 
23
 
23
     /**
24
     /**
70
          */
71
          */
71
         > div.title,
72
         > div.title,
72
           div.subTitle {
73
           div.subTitle {
73
-            color: $defaultColor !important;
74
             text-align: left;
74
             text-align: left;
75
             margin: 10px 0px 10px 0px;
75
             margin: 10px 0px 10px 0px;
76
-            padding: 5px 10px 5px 10px;
77
         }
76
         }
78
 
77
 
79
         /**
78
         /**
80
          * Main title size.
79
          * Main title size.
81
          */
80
          */
82
         > div.title {
81
         > div.title {
82
+            color: $defaultColor !important;
83
+            text-align: center;
83
             font-size: 16px;
84
             font-size: 16px;
84
         }
85
         }
85
 
86
 
87
          * Subtitle specific properties.
88
          * Subtitle specific properties.
88
          */
89
          */
89
         > div.subTitle {
90
         > div.subTitle {
90
-            font-size: 12px;
91
-            background: $inputSemiBackground !important;
92
-            margin-top: 20px !important;
93
-            margin-bottom: 8px !important;
91
+            font-size: 11px;
92
+            font-weight: 500;
93
+            color: $defaultSideBarFontColor !important;
94
+            margin-left: 10%;
94
         }
95
         }
95
 
96
 
96
         /**
97
         /**

+ 36
- 8
css/_toolbars.scss 查看文件

83
     display: none;
83
     display: none;
84
 }
84
 }
85
 
85
 
86
-#numberOfParticipants {
87
-  position: absolute;
88
-  top: 5px;
89
-  line-height: 13px;
90
-  font-weight: bold;
91
-  font-size: 11px;
92
-}
93
-
94
 #mainToolbar a.button:last-child::after {
86
 #mainToolbar a.button:last-child::after {
95
     content: none;
87
     content: none;
96
 }
88
 }
118
     cursor: default;
110
     cursor: default;
119
 }
111
 }
120
 
112
 
113
+.button.glow
114
+{
115
+    text-shadow: 0px 0px 5px $toolbarToggleBackground;
116
+}
117
+
121
 a.button.unclickable:hover,
118
 a.button.unclickable:hover,
122
 a.button.unclickable:active,
119
 a.button.unclickable:active,
123
 a.button.unclickable.selected{
120
 a.button.unclickable.selected{
129
 a.button:active,
126
 a.button:active,
130
 a.button.selected {
127
 a.button.selected {
131
     cursor: pointer;
128
     cursor: pointer;
129
+    text-decoration: none;
132
     // sum opacity with background layer should give us 0.8
130
     // sum opacity with background layer should give us 0.8
133
     background: $toolbarSelectBackground;
131
     background: $toolbarSelectBackground;
134
 }
132
 }
144
     margin-top: auto;
142
     margin-top: auto;
145
 }
143
 }
146
 
144
 
145
+/**
146
+ * Round badge.
147
+ */
148
+.badge-round {
149
+    background-color: $toolbarBadgeBackground;
150
+    color: $toolbarBadgeColor;
151
+    font-size: 9px;
152
+    line-height: 13px;
153
+    font-weight: 700;
154
+    text-align: center;
155
+    border-radius: 50%;
156
+    min-width: 13px;
157
+    overflow: hidden;
158
+    text-overflow: ellipsis;
159
+    box-sizing: border-box;
160
+    vertical-align: middle;
161
+    // Do not inherit the font-family from the toolbar button, because it's an
162
+    // icon style.
163
+    font-family: $baseFontFamily;
164
+}
165
+
166
+/**
167
+ * Toolbar specific round badge.
168
+ */
169
+.toolbar .badge-round {
170
+    position: absolute;
171
+    right: 9px;
172
+    bottom: 9px;
173
+}
174
+
147
 /**
175
 /**
148
  * START of slide in animation for extended toolbar.
176
  * START of slide in animation for extended toolbar.
149
  */
177
  */

+ 32
- 6
css/_variables.scss 查看文件

10
  */
10
  */
11
 $defaultToolbarSize: 50px;
11
 $defaultToolbarSize: 50px;
12
 
12
 
13
+// Video layout.
14
+$thumbnailIndicatorSize: 23px;
15
+$thumbnailIndicatorBorder: 0px;
16
+$thumbnailVideoMargin: 2px;
17
+$thumbnailToolbarHeight: 25px;
18
+
13
 /**
19
 /**
14
  * Color variables.
20
  * Color variables.
15
  */
21
  */
16
 $defaultColor: #F1F1F1;
22
 $defaultColor: #F1F1F1;
17
-$defaultSemiDarkColor: #ACACAC;
23
+$defaultSideBarFontColor: #44A5FF;
18
 $defaultDarkColor: #4F4F4F;
24
 $defaultDarkColor: #4F4F4F;
19
 $defaultBackground: #474747;
25
 $defaultBackground: #474747;
26
+$tooltipBg: rgba(0,0,0, 0.7);
27
+
28
+// Toolbar
20
 $toolbarSelectBackground: rgba(0, 0, 0, .6);
29
 $toolbarSelectBackground: rgba(0, 0, 0, .6);
30
+
31
+$toolbarBadgeBackground: #165ECC;
32
+$toolbarBadgeColor: #FFFFFF;
33
+$toolbarToggleBackground: #165ECC;
34
+
35
+// Main controls
21
 $inputBackground: rgba(132, 132, 132, .5);
36
 $inputBackground: rgba(132, 132, 132, .5);
22
 $inputSemiBackground: rgba(132, 132, 132, .8);
37
 $inputSemiBackground: rgba(132, 132, 132, .8);
23
 $inputLightBackground: #EBEBEB;
38
 $inputLightBackground: #EBEBEB;
24
 $inputBorderColor: #EBEBEB;
39
 $inputBorderColor: #EBEBEB;
25
 $buttonBackground: #44A5FF;
40
 $buttonBackground: #44A5FF;
26
 
41
 
42
+// Video layout.
43
+$videoThumbnailHovered: #BFEBFF;
44
+$videoThumbnailSelected: #165ECC;
45
+$participantNameColor: #fff;
46
+$thumbnailPictogramColor: #fff;
47
+$dominantSpeakerBg: #165ecc;
48
+$raiseHandBg: #D6D61E;
49
+$audioLevelBg: #44A5FF;
50
+$audioLevelShadow: rgba(9, 36, 77, 0.9);
51
+
52
+$rateStarDefault: #ccc;
53
+$rateStarActivity: #165ecc;
54
+$rateStarLabelColor: #333;
55
+
27
 /**
56
 /**
28
  * Misc.
57
  * Misc.
29
  */
58
  */
33
 /**
62
 /**
34
  * Z-indexes. TODO: Replace this by a function.
63
  * Z-indexes. TODO: Replace this by a function.
35
  */
64
  */
65
+$tooltipsZ: 901;
36
 $toolbarZ: 900;
66
 $toolbarZ: 900;
37
-$overlayZ: 800;
38
-
39
-$rateStarDefault: #ccc;
40
-$rateStarActivity: #f6c342;
41
-$rateStarLabelColor: #333;
67
+$overlayZ: 800;

+ 212
- 105
css/_videolayout_default.scss 查看文件

1
+#videoconference_page {
2
+    min-height: 100%;
3
+}
4
+
1
 #videospace {
5
 #videospace {
2
     display: block;
6
     display: block;
7
+    min-height: 100%;
3
     position: absolute;
8
     position: absolute;
4
     top: 0px;
9
     top: 0px;
5
     left: 0px;
10
     left: 0px;
13
     display: -ms-flexbox;
18
     display: -ms-flexbox;
14
     display: -webkit-flex;
19
     display: -webkit-flex;
15
     display: flex;
20
     display: flex;
16
-    flex-direction: row;
21
+    flex-direction: row-reverse;
17
     flex-wrap: nowrap;
22
     flex-wrap: nowrap;
18
-    justify-content: flex-end;
23
+    justify-content: flex-start;
19
 
24
 
20
     position:absolute;
25
     position:absolute;
21
     text-align:right;
26
     text-align:right;
22
     height:196px;
27
     height:196px;
23
-    padding: 18px;
28
+    padding: 10px 10px 10px 5px;
24
     bottom: 0;
29
     bottom: 0;
25
     left: 0;
30
     left: 0;
26
-    right: 20px;
31
+    right: 0;
27
     width:auto;
32
     width:auto;
28
-    border:1px solid transparent;
33
+    border: 2px solid transparent;
29
     z-index: 5;
34
     z-index: 5;
30
     transition: bottom 2s;
35
     transition: bottom 2s;
31
     overflow: visible !important;
36
     overflow: visible !important;
43
 
48
 
44
 #remoteVideos .videocontainer {
49
 #remoteVideos .videocontainer {
45
     display: none;
50
     display: none;
51
+    position: relative;
46
     background-color: black;
52
     background-color: black;
47
     background-size: contain;
53
     background-size: contain;
48
     border-radius:1px;
54
     border-radius:1px;
49
-    border: 1px solid #212425;
55
+    margin: 0 $thumbnailVideoMargin;
50
 }
56
 }
51
 
57
 
52
-#remoteVideos .videocontainer.videoContainerFocused {
58
+/**
59
+ * The toolbar of the video thumbnail.
60
+ */
61
+.videocontainer__toolbar {
62
+    position: absolute;
63
+    bottom: 0;
64
+    left: 0;
65
+    z-index: 1;
66
+    width: 100%;
67
+    box-sizing: border-box; // Includes the padding in the 100% width.
68
+    height: $thumbnailToolbarHeight;
69
+    max-height: 100%;
70
+    background-color: rgba(0, 0, 0, 0.5);
71
+    padding: 0 5px 0 5px;
72
+}
73
+
74
+#remoteVideos .videocontainer.videoContainerFocused,
75
+#remoteVideos .videocontainer:hover {
53
     cursor: hand;
76
     cursor: hand;
77
+    margin-right: $thumbnailVideoMargin - 2;
78
+    margin-left: $thumbnailVideoMargin - 2;
79
+    margin-top: -2px;
80
+}
81
+/**
82
+ * Focused video thumbnail.
83
+ */
84
+#remoteVideos .videocontainer.videoContainerFocused {
54
     transition-duration: 0.5s;
85
     transition-duration: 0.5s;
55
     -webkit-transition-duration: 0.5s;
86
     -webkit-transition-duration: 0.5s;
56
     -webkit-animation-name: greyPulse;
87
     -webkit-animation-name: greyPulse;
57
     -webkit-animation-duration: 2s;
88
     -webkit-animation-duration: 2s;
58
     -webkit-animation-iteration-count: 1;
89
     -webkit-animation-iteration-count: 1;
90
+    border: 2px solid $videoThumbnailSelected !important;
91
+    box-shadow: inset 0 0 3px $videoThumbnailSelected,
92
+                0 0 3px $videoThumbnailSelected !important;
59
 }
93
 }
60
 
94
 
95
+/**
96
+ * Hovered video thumbnail.
97
+ */
61
 #remoteVideos .videocontainer:hover {
98
 #remoteVideos .videocontainer:hover {
62
-    border: 1px solid #c1c1c1;
63
-}
64
-
65
-#remoteVideos .videocontainer.videoContainerFocused {
66
-    box-shadow: inset 0 0 28px #006d91;
67
-    border: 1px solid #006d91;
68
-}
69
-
70
-#remoteVideos .videocontainer.videoContainerFocused:hover {
71
-    box-shadow: inset 0 0 5px #c1c1c1, 0 0 10px #c1c1c1, inset 0 0 60px #006d91;
72
-    border: 1px solid #c1c1c1;
99
+    cursor: hand;
100
+    border: 2px solid $videoThumbnailHovered;
101
+    box-shadow: inset 0 0 3px $videoThumbnailHovered,
102
+              0 0 3px $videoThumbnailHovered;
73
 }
103
 }
74
 
104
 
75
 #localVideoWrapper {
105
 #localVideoWrapper {
113
     object-fit: cover;
143
     object-fit: cover;
114
 }
144
 }
115
 
145
 
116
-#presentation,
117
 #sharedVideo,
146
 #sharedVideo,
118
 #etherpad,
147
 #etherpad,
119
 #localVideoWrapper>video,
148
 #localVideoWrapper>video,
132
     height: 100%;
161
     height: 100%;
133
 }
162
 }
134
 
163
 
135
-#etherpad,
136
-#presentation {
164
+#etherpad {
137
     text-align: center;
165
     text-align: center;
138
 }
166
 }
139
 
167
 
141
     z-index: 0;
169
     z-index: 0;
142
 }
170
 }
143
 
171
 
144
-#remoteVideos .videocontainer>span.focusindicator,
145
-#remoteVideos .videocontainer>div.remotevideomenu {
146
-    position: absolute;
147
-    color: #FFFFFF;
148
-    top: 0;
149
-    left: 0;
150
-    padding: 5px 0px;
151
-    width: 25px;
152
-    font-size: 11pt;
153
-    text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7);
154
-    border: 0px;
155
-    z-index: 2;
156
-    text-align: center;
157
-}
158
-
159
-#remoteVideos .videocontainer>span.focusindicator {
172
+/**
173
+ * Positions video thumbnail display name and editor.
174
+ */
175
+.videocontainer .displayname,
176
+.videocontainer .editdisplayname {
160
     display: inline-block;
177
     display: inline-block;
161
-}
162
-
163
-#remoteVideos .videocontainer>div.remotevideomenu {
164
-    display: block;
165
-}
166
-
167
-.videocontainer>span.displayname,
168
-.videocontainer>input.displayname {
169
-    display: none;
170
     position: absolute;
178
     position: absolute;
171
-    color: #FFFFFF;
172
-    background: rgba(0,0,0,.7);
179
+    left: 30%;
180
+    width: 40%;
181
+    color: $participantNameColor;
173
     text-align: center;
182
     text-align: center;
174
     text-overflow: ellipsis;
183
     text-overflow: ellipsis;
175
-    width: 70%;
176
-    height: 20%;
177
-    left: 15%;
178
-    top: 40%;
179
-    padding: 5px;
180
-    font-size: 11pt;
184
+    font-size: 12px;
185
+    font-weight: 100;
186
+    letter-spacing: 1px;
181
     overflow: hidden;
187
     overflow: hidden;
182
     white-space: nowrap;
188
     white-space: nowrap;
189
+    line-height: $thumbnailToolbarHeight;
183
     z-index: 2;
190
     z-index: 2;
184
-    border-radius:3px;
191
+}
192
+
193
+/**
194
+ * Positions video thumbnail display name editor.
195
+ */
196
+.videocontainer .editdisplayname {
197
+    outline: none;
198
+    border: none;
199
+    background: none;
200
+    box-shadow: none;
201
+    padding: 0;
185
 }
202
 }
186
 
203
 
187
 .videocontainer>span.status {
204
 .videocontainer>span.status {
221
     overflow: hidden;
238
     overflow: hidden;
222
 }
239
 }
223
 
240
 
241
+.connection.connection_lost
242
+{
243
+    color: #8B8B8B;
244
+    overflow: visible;
245
+}
246
+
224
 .connection.connection_full
247
 .connection.connection_full
225
 {
248
 {
226
     color: #FFFFFF;/*#15A1ED*/
249
     color: #FFFFFF;/*#15A1ED*/
257
 }
280
 }
258
 
281
 
259
 #localVideoContainer>span.status:hover,
282
 #localVideoContainer>span.status:hover,
260
-#localVideoContainer>span.displayname:hover {
283
+#localVideoContainer .displayname:hover {
261
     cursor: text;
284
     cursor: text;
262
 }
285
 }
263
 
286
 
264
 .videocontainer>span.status,
287
 .videocontainer>span.status,
265
-.videocontainer>span.displayname {
288
+.videocontainer .displayname {
266
     pointer-events: none;
289
     pointer-events: none;
267
 }
290
 }
268
 
291
 
269
-.videocontainer>input.displayname {
292
+.videocontainer .editdisplayname {
270
     height: auto;
293
     height: auto;
271
 }
294
 }
272
 
295
 
287
     z-index: 2;
310
     z-index: 2;
288
 }
311
 }
289
 
312
 
290
-.videocontainer>span.audioMuted {
291
-    display: inline-block;
292
-    position: absolute;
293
-    color: #FFFFFF;
294
-    top: 0;
295
-    padding: 8px 5px;
296
-    width: 25px;
313
+/**
314
+ * Video thumbnail toolbar icon.
315
+ */
316
+.videocontainer .toolbar-icon {
297
     font-size: 8pt;
317
     font-size: 8pt;
298
-    text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7);
299
-    border: 0px;
300
-    z-index: 3;
301
     text-align: center;
318
     text-align: center;
319
+    text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7);
320
+    color: #FFFFFF;
321
+    width: 12px;
322
+    line-height: $thumbnailToolbarHeight;
323
+    height: $thumbnailToolbarHeight;
324
+    padding: 0;
325
+    border: 0;
326
+    margin: 0px 5px 0px 0px;
327
+    float: left;
302
 }
328
 }
303
 
329
 
304
-.videocontainer>span.videoMuted {
305
-    display: inline-block;
306
-    position: absolute;
307
-    color: #FFFFFF;
308
-    top: 0;
309
-    right: 0;
310
-    padding: 8px 5px;
311
-    width: 25px;
312
-    font-size: 8pt;
313
-    text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7);
314
-    border: 0px;
315
-    z-index: 3;
330
+/**
331
+ * Toolbar icon internal i elements (font icons).
332
+ */
333
+.toolbar-icon>i {
334
+    line-height: $thumbnailToolbarHeight;
335
+}
336
+
337
+/**
338
+ * Toolbar icons positioned on the right.
339
+ */
340
+.toolbar-icon.right {
341
+  float: right;
342
+  margin: 0px 0px 0px 5px;
316
 }
343
 }
317
 
344
 
318
 .videocontainer>span.indicator {
345
 .videocontainer>span.indicator {
319
-    bottom: 0px;
346
+    position: absolute;
347
+    top: 0px;
320
     left: 0px;
348
     left: 0px;
321
-    width: 25px;
322
-    height: 25px;
349
+    @include circle($thumbnailIndicatorSize);
350
+    box-sizing: border-box;
351
+    line-height: $thumbnailIndicatorSize - 2*$thumbnailIndicatorBorder;
323
     z-index: 3;
352
     z-index: 3;
324
     text-align: center;
353
     text-align: center;
325
-    border-radius: 50%;
326
-    background: #21B9FC;
327
-    margin: 5px;
354
+    background: $dominantSpeakerBg;
355
+    margin: 7px;
328
     display: inline-block;
356
     display: inline-block;
357
+    color: $thumbnailPictogramColor;
358
+    font-size: 8pt;
359
+    border: $thumbnailIndicatorBorder solid $thumbnailPictogramColor;
360
+}
361
+
362
+.videocontainer>#raisehandindicator {
363
+    background: $raiseHandBg;
364
+}
365
+
366
+/**
367
+ * Audio indicator on video thumbnails.
368
+ */
369
+.videocontainer>span.audioindicator {
329
     position: absolute;
370
     position: absolute;
330
-    color: #FFFFFF;
331
-    font-size: 11pt;
332
-    border: 0px;
371
+    display: inline-block;
372
+    left: 6px;
373
+    top: 50%;
374
+    margin-top: -17px;
375
+    width: 6px;
376
+    height: 35px;
377
+    z-index: 2;
378
+    border: none;
379
+
380
+    .audiodot-top,
381
+    .audiodot-bottom,
382
+    .audiodot-middle {
383
+        opacity: 0;
384
+        display: inline-block;
385
+        @include circle(5px);
386
+        background: $audioLevelShadow;
387
+        margin: 1px 0 1px 0;
388
+        transition: opacity .25s ease-in-out;
389
+        -moz-transition: opacity .25s ease-in-out;
390
+    }
391
+
392
+    span.audiodot-top::after,
393
+    span.audiodot-bottom::after,
394
+    span.audiodot-middle::after {
395
+        content: "";
396
+        display: inline-block;
397
+        width: 5px;
398
+        height: 5px;
399
+        border-radius: 50%;
400
+        -webkit-filter: blur(0.5px);
401
+        filter: blur(0.5px);
402
+        background: $audioLevelBg;
403
+    }
333
 }
404
 }
334
 
405
 
335
 #indicatoricon {
406
 #indicatoricon {
336
-    padding-top: 5px;
407
+    width: $thumbnailIndicatorSize - 2*$thumbnailIndicatorBorder;
408
+    height: $thumbnailIndicatorSize - 2*$thumbnailIndicatorBorder;
409
+    line-height: $thumbnailIndicatorSize - 2*$thumbnailIndicatorBorder;
337
 }
410
 }
338
 
411
 
339
 #reloadPresentation {
412
 #reloadPresentation {
366
     width: 300px;
439
     width: 300px;
367
     height: 300px;
440
     height: 300px;
368
     margin: auto;
441
     margin: auto;
369
-    overflow: hidden;
370
     position: relative;
442
     position: relative;
371
 }
443
 }
372
 
444
 
373
-#dominantSpeakerAudioLevel {
374
-    position: absolute;
375
-    top: 0px;
376
-    left: 0px;
377
-    z-index: 2;
378
-    visibility: inherit;
379
-}
380
-
381
 #mixedstream {
445
 #mixedstream {
382
     display:none !important;
446
     display:none !important;
383
 }
447
 }
384
 
448
 
385
-#dominantSpeakerAvatar {
449
+#dominantSpeakerAvatar,
450
+.dynamic-shadow {
386
     width: 200px;
451
     width: 200px;
387
     height: 200px;
452
     height: 200px;
453
+}
454
+
455
+#dominantSpeakerAvatar {
388
     top: 50px;
456
     top: 50px;
389
     margin: auto;
457
     margin: auto;
390
     position: relative;
458
     position: relative;
394
     background-color: #000000;
462
     background-color: #000000;
395
 }
463
 }
396
 
464
 
397
-.userAvatar {
398
-    height: 100%;
465
+.dynamic-shadow {
466
+    border-radius: 50%;
399
     position: absolute;
467
     position: absolute;
400
-    left: 0;
401
-    border-radius: 2px;
468
+    top: 50%;
469
+    left: 50%;
470
+    margin: -100px 0 0 -100px;
471
+    transition: box-shadow 0.3s ease;
472
+}
473
+
474
+.userAvatar {
475
+    @include circle(60px);
476
+    @include absoluteAligning(60px, 60px);
402
 }
477
 }
403
 
478
 
404
 .sharedVideoAvatar {
479
 .sharedVideoAvatar {
436
     filter: grayscale(.5) opacity(0.8);
511
     filter: grayscale(.5) opacity(0.8);
437
 }
512
 }
438
 
513
 
514
+.remoteVideoProblemFilter {
515
+    -webkit-filter: grayscale(100%);
516
+    filter: grayscale(100%);
517
+}
518
+
439
 .videoProblemFilter {
519
 .videoProblemFilter {
440
     -webkit-filter: blur(10px) grayscale(.5) opacity(0.8);
520
     -webkit-filter: blur(10px) grayscale(.5) opacity(0.8);
441
     filter: blur(10px) grayscale(.5) opacity(0.8);
521
     filter: blur(10px) grayscale(.5) opacity(0.8);
442
 }
522
 }
443
 
523
 
444
-#videoConnectionMessage {
524
+.videoThumbnailProblemFilter {
525
+    -webkit-filter: grayscale(100%);
526
+    filter: grayscale(100%);
527
+}
528
+
529
+#remoteConnectionMessage {
530
+    display: none;
531
+    position: absolute;
532
+    width: auto;
533
+    z-index: 1011;
534
+    font-weight: 600;
535
+    font-size: 14px;
536
+    text-align: center;
537
+    color: #FFF;
538
+    opacity: .80;
539
+    text-shadow:    0px 0px 1px rgba(0,0,0,0.3),
540
+                    0px 1px 1px rgba(0,0,0,0.3),
541
+                    1px 0px 1px rgba(0,0,0,0.3),
542
+                    0px 0px 1px rgba(0,0,0,0.3);
543
+
544
+    background: rgba(0,0,0,.5);
545
+    border-radius: 5px;
546
+    padding: 5px;
547
+    padding-left: 10px;
548
+    padding-right: 10px;
549
+}
550
+
551
+#localConnectionMessage {
445
     display: none;
552
     display: none;
446
     position: absolute;
553
     position: absolute;
447
     width: 100%;
554
     width: 100%;

+ 4
- 1
css/main.scss 查看文件

22
 @import 'toastr';
22
 @import 'toastr';
23
 @import 'base';
23
 @import 'base';
24
 @import 'overlay/overlay';
24
 @import 'overlay/overlay';
25
+@import 'modals/dialog';
26
+@import 'modals/feedback/feedback';
25
 @import 'videolayout_default';
27
 @import 'videolayout_default';
26
 @import 'jquery-impromptu';
28
 @import 'jquery-impromptu';
27
 @import 'modaldialog';
29
 @import 'modaldialog';
38
 @import 'toolbars';
40
 @import 'toolbars';
39
 @import 'side_toolbar_container';
41
 @import 'side_toolbar_container';
40
 @import 'device_settings_dialog';
42
 @import 'device_settings_dialog';
41
-@import 'feedback';
42
 @import 'jquery.contextMenu';
43
 @import 'jquery.contextMenu';
43
 @import 'keyboard-shortcuts';
44
 @import 'keyboard-shortcuts';
45
+@import 'redirect_page';
46
+
44
 
47
 
45
 /* Modules END */
48
 /* Modules END */

+ 53
- 0
css/modals/_dialog.scss 查看文件

1
+.dialog{
2
+    visibility: visible;
3
+    height: auto;
4
+
5
+    p {
6
+        color: $defaultDarkColor;
7
+    }
8
+    textarea {
9
+        background: none;
10
+        border: 1px solid $inputBorderColor;
11
+    }
12
+    .aui-dialog2-content:last-child {
13
+        border-bottom-right-radius: 5px;
14
+        border-bottom-left-radius: 5px;
15
+    }
16
+    .aui-dialog2-content:first-child {
17
+        border-top-right-radius: 5px;
18
+        border-top-left-radius: 5px;
19
+    }
20
+    .aui-dialog2-footer{
21
+        border-top: 0;
22
+        border-radius: 0;
23
+        padding-top: 0;
24
+        background: none;
25
+        border: none;
26
+        height: auto;
27
+        margin-top: 10px;
28
+    }
29
+    .aui-button {
30
+        height: 28px;
31
+        font-size: 12px;
32
+        padding: 3px 6px 3px 6px;
33
+        border: none;
34
+        box-shadow: none;
35
+        outline: none;
36
+
37
+        &_close {
38
+            font-weight: 400 !important;
39
+            color: $buttonBackground;
40
+            background: none !important;
41
+
42
+            :hover {
43
+                text-decoration: underline;
44
+            }
45
+        }
46
+        &_submit {
47
+            font-weight: 700 !important;
48
+            color: $defaultColor;
49
+            background: $buttonBackground;
50
+            border-radius: 3px;
51
+        }
52
+    }
53
+}

css/_feedback.scss → css/modals/feedback/_feedback.scss 查看文件

33
 }
33
 }
34
 
34
 
35
 .shake-rotate {
35
 .shake-rotate {
36
+    display: inline-block;
37
+
36
     -webkit-animation-duration: .4s;
38
     -webkit-animation-duration: .4s;
37
     animation-duration: .4s;
39
     animation-duration: .4s;
38
     -webkit-animation-iteration-count: infinite;
40
     -webkit-animation-iteration-count: infinite;
43
     animation-timing-function: ease-in-out
45
     animation-timing-function: ease-in-out
44
 }
46
 }
45
 
47
 
46
-.text-center {
47
-    text-align: center;
48
-}
49
-
50
-.feedbackDetails textarea {
51
-    resize: vertical;
52
-    min-height: 100px;
53
-}
54
-
55
-.feedback-rating {
56
-    line-height: 1.2;
57
-    padding: 20px 0;
58
-
48
+.feedback {
59
     h2 {
49
     h2 {
60
         font-weight: 400;
50
         font-weight: 400;
61
         font-size: 24px;
51
         font-size: 24px;
62
         line-height: 1.2;
52
         line-height: 1.2;
63
-        padding: auto;
64
-        margin: auto;
65
-        border: none;
66
     }
53
     }
67
-
68
     p {
54
     p {
69
-        margin-top: 10px;
70
-        margin-left: 0px;
71
-        margin-bottom: 0px;
72
-        margin-right: 0px;
55
+        font-weight: 400;
56
+        font-size: 14px;
73
     }
57
     }
74
 
58
 
75
-    .star-label {
76
-        font-size: 16px;
77
-        color: $rateStarLabelColor;
59
+    &__content {
60
+        text-align: center;
61
+
62
+        textarea {
63
+            text-align: left;
64
+            min-height: 80px;
65
+            width: 100%;
66
+        }
78
     }
67
     }
68
+    &__footer {
79
 
69
 
80
-    .star-btn {
81
-        color: $rateStarDefault;
82
-        font-size: 36px;
83
-        position: relative;
84
-        cursor: pointer;
85
-        outline: none;
86
-        text-decoration: none;
87
-        @include transition(all .2s ease);
88
-
89
-        &.starHover,
90
-        &.active,
91
         &:hover {
70
         &:hover {
92
-            color: $rateStarActivity;
71
+            color: #287ade;
72
+            outline: 0;
73
+        }
74
+    }
75
+    &__rating {
76
+        line-height: 1.2;
77
+        padding: 20px 0;
93
 
78
 
94
-            .fa {
95
-                top: -6px;
96
-            }
97
-        };
79
+        p {
80
+            margin: 10px 0 0;
81
+        }
98
 
82
 
99
-        &.rated:hover .fa {
100
-            top: 0;
83
+        .star-label {
84
+            font-size: 16px;
85
+            color: $rateStarLabelColor;
101
         }
86
         }
102
 
87
 
103
-        .fa {
88
+        .star-btn {
89
+            color: $rateStarDefault;
90
+            font-size: 36px;
104
             position: relative;
91
             position: relative;
92
+            cursor: pointer;
93
+            outline: none;
94
+            text-decoration: none;
95
+            @include transition(all .2s ease);
96
+
97
+            &.starHover,
98
+            &.active,
99
+            &:hover {
100
+                color: $rateStarActivity;
101
+                > i:before {
102
+                    content: "\e90a";
103
+                }
104
+            };
105
+
105
         }
106
         }
106
     }
107
     }
107
 }
108
 }

+ 6
- 1
css/ringing/_ringing.scss 查看文件

9
     background: linear-gradient(transparent, #000);
9
     background: linear-gradient(transparent, #000);
10
     opacity: 0.8;
10
     opacity: 0.8;
11
 
11
 
12
+    &.solidBG {
13
+        background: $defaultBackground;
14
+        opacity: 1;
15
+    }
16
+
12
     &__content {
17
     &__content {
13
         position: absolute;
18
         position: absolute;
14
         width: 400px;
19
         width: 400px;
33
             color: #333;
38
             color: #333;
34
         }
39
         }
35
     }
40
     }
36
-}
41
+}

二進制
fonts/jitsi.eot 查看文件


+ 2
- 0
fonts/jitsi.svg 查看文件

42
 <glyph unicode="&#xe91c;" glyph-name="toggle-filmstrip" d="M896 896h-768c-46.933 0-85.333-38.4-85.333-85.333v-597.333c0-46.933 38.4-85.333 85.333-85.333h768c46.933 0 85.333 38.4 85.333 85.333v597.333c0 46.933-38.4 85.333-85.333 85.333zM896 213.333h-768v128h768v-128z" />
42
 <glyph unicode="&#xe91c;" glyph-name="toggle-filmstrip" d="M896 896h-768c-46.933 0-85.333-38.4-85.333-85.333v-597.333c0-46.933 38.4-85.333 85.333-85.333h768c46.933 0 85.333 38.4 85.333 85.333v597.333c0 46.933-38.4 85.333-85.333 85.333zM896 213.333h-768v128h768v-128z" />
43
 <glyph unicode="&#xe91d;" glyph-name="feedback" d="M42.667 128h170.667v512h-170.667v-512zM981.333 597.333c0 46.933-38.4 85.333-85.333 85.333h-269.227l40.533 194.987 1.28 13.653c0 17.493-7.253 33.707-18.773 45.227l-45.227 44.8-280.747-281.173c-15.787-15.36-25.173-36.693-25.173-60.16v-426.667c0-46.933 38.4-85.333 85.333-85.333h384c35.413 0 65.707 21.333 78.507 52.053l128.853 300.8c3.84 9.813 5.973 20.053 5.973 31.147v81.493l-0.427 0.427 0.427 3.413z" />
43
 <glyph unicode="&#xe91d;" glyph-name="feedback" d="M42.667 128h170.667v512h-170.667v-512zM981.333 597.333c0 46.933-38.4 85.333-85.333 85.333h-269.227l40.533 194.987 1.28 13.653c0 17.493-7.253 33.707-18.773 45.227l-45.227 44.8-280.747-281.173c-15.787-15.36-25.173-36.693-25.173-60.16v-426.667c0-46.933 38.4-85.333 85.333-85.333h384c35.413 0 65.707 21.333 78.507 52.053l128.853 300.8c3.84 9.813 5.973 20.053 5.973 31.147v81.493l-0.427 0.427 0.427 3.413z" />
44
 <glyph unicode="&#xe91e;" glyph-name="raised-hand" d="M982 790v-620c0-94-78-170-172-170h-310c-46 0-90 18-122 50l-336 342s54 52 56 52c10 8 22 12 34 12 10 0 18-2 26-6 2 0 184-104 184-104v508c0 36 28 64 64 64s64-28 64-64v-300h42v406c0 36 28 64 64 64s64-28 64-64v-406h42v364c0 36 28 64 64 64s64-28 64-64v-364h44v236c0 36 28 64 64 64s64-28 64-64z" />
44
 <glyph unicode="&#xe91e;" glyph-name="raised-hand" d="M982 790v-620c0-94-78-170-172-170h-310c-46 0-90 18-122 50l-336 342s54 52 56 52c10 8 22 12 34 12 10 0 18-2 26-6 2 0 184-104 184-104v508c0 36 28 64 64 64s64-28 64-64v-300h42v406c0 36 28 64 64 64s64-28 64-64v-406h42v364c0 36 28 64 64 64s64-28 64-64v-364h44v236c0 36 28 64 64 64s64-28 64-64z" />
45
+<glyph unicode="&#xe91f;" glyph-name="menu-up" d="M512 682l256-256-60-60-196 196-196-196-60 60z" />
46
+<glyph unicode="&#xe920;" glyph-name="menu-down" d="M708 658l60-60-256-256-256 256 60 60 196-196z" />
45
 </font></defs></svg>
47
 </font></defs></svg>

二進制
fonts/jitsi.ttf 查看文件


二進制
fonts/jitsi.woff 查看文件


+ 52
- 0
fonts/selection.json 查看文件

293
       "setId": 2,
293
       "setId": 2,
294
       "iconIdx": 243
294
       "iconIdx": 243
295
     },
295
     },
296
+    {
297
+      "icon": {
298
+        "paths": [
299
+          "M512 342l256 256-60 60-196-196-196 196-60-60z"
300
+        ],
301
+        "isMulticolor": false,
302
+        "isMulticolor2": false,
303
+        "tags": [
304
+          "expand_less"
305
+        ],
306
+        "grid": 0,
307
+        "attrs": []
308
+      },
309
+      "attrs": [],
310
+      "properties": {
311
+        "id": 256,
312
+        "order": 106,
313
+        "ligatures": "expand_less",
314
+        "prevSize": 32,
315
+        "code": 59679,
316
+        "name": "menu-up"
317
+      },
318
+      "setIdx": 0,
319
+      "setId": 2,
320
+      "iconIdx": 257
321
+    },
322
+    {
323
+      "icon": {
324
+        "paths": [
325
+          "M708 366l60 60-256 256-256-256 60-60 196 196z"
326
+        ],
327
+        "isMulticolor": false,
328
+        "isMulticolor2": false,
329
+        "tags": [
330
+          "expand_more"
331
+        ],
332
+        "grid": 0,
333
+        "attrs": []
334
+      },
335
+      "attrs": [],
336
+      "properties": {
337
+        "id": 257,
338
+        "order": 107,
339
+        "ligatures": "expand_more",
340
+        "prevSize": 32,
341
+        "code": 59680,
342
+        "name": "menu-down"
343
+      },
344
+      "setIdx": 0,
345
+      "setId": 2,
346
+      "iconIdx": 258
347
+    },
296
     {
348
     {
297
       "icon": {
349
       "icon": {
298
         "paths": [
350
         "paths": [

+ 26
- 22
index.html 查看文件

120
                     </li>
120
                     </li>
121
                 </ul>
121
                 </ul>
122
             </span>
122
             </span>
123
-            <a class="button icon-contactList" id="toolbar_contact_list" data-container="body" data-toggle="popover" data-placement="right" shortcut="contactlistpopover"  data-i18n="[content]bottomtoolbar.contactlist" content="Open / close contact list">
124
-                <span id="numberOfParticipants"></span>
123
+            <a class="button icon-contactList" id="toolbar_contact_list" shortcut="contactlistpopover">
124
+                <span class="badge-round">
125
+                    <span id="numberOfParticipants"></span>
126
+                </span>
125
             </a>
127
             </a>
126
-            <!--a class="button icon-link" id="toolbar_button_link" data-container="body" data-toggle="popover" data-placement="right" data-i18n="[content]toolbar.invite" content="Invite others"></a-->
127
-            <a class="button icon-chat" id="toolbar_button_chat" data-container="body" data-toggle="popover" shortcut="toggleChatPopover" data-placement="right" data-i18n="[content]toolbar.chat" content="Open / close chat">
128
-                <span id="unreadMessages"></span>
128
+            <!--a class="button icon-link" id="toolbar_button_link"></a-->
129
+            <a class="button icon-chat" id="toolbar_button_chat" shortcut="toggleChatPopover">
130
+                <span class="badge-round">
131
+                    <span id="unreadMessages"></span>
132
+                </span>
129
             </a>
133
             </a>
130
-            <a class="button" id="toolbar_button_record" data-container="body" data-toggle="popover" data-placement="right" style="display: none"></a>
131
-            <a class="button icon-security" id="toolbar_button_security" data-container="body" data-toggle="popover" data-placement="right" data-i18n="[content]toolbar.lock" content="Lock / unlock room"></a>
132
-            <a class="button icon-share-doc" id="toolbar_button_etherpad" data-container="body" data-toggle="popover" data-placement="right" content="Shared document" data-i18n="[content]toolbar.etherpad"></a>
133
-            <a class="button icon-shared-video" id="toolbar_button_sharedvideo" data-container="body" data-toggle="popover" data-placement="right" content="Share a YouTube video" data-i18n="[content]toolbar.sharedvideo" style="display: none">
134
+            <a class="button" id="toolbar_button_record" style="display: none"></a>
135
+            <a class="button icon-security" id="toolbar_button_security"></a>
136
+            <a class="button icon-share-doc" id="toolbar_button_etherpad"></a>
137
+            <a class="button icon-shared-video" id="toolbar_button_sharedvideo" style="display: none">
134
                 <ul id="sharedVideoMutedPopup" class="loginmenu extendedToolbarPopup">
138
                 <ul id="sharedVideoMutedPopup" class="loginmenu extendedToolbarPopup">
135
                     <li data-i18n="[html]toolbar.sharedVideoMutedPopup"></li>
139
                     <li data-i18n="[html]toolbar.sharedVideoMutedPopup"></li>
136
                 </ul>
140
                 </ul>
137
             </a>
141
             </a>
138
-            <a class="button icon-telephone" id="toolbar_button_sip" data-container="body" data-toggle="popover" data-placement="right" content="Call SIP number" data-i18n="[content]toolbar.sip" style="display: none"></a>
139
-            <a class="button icon-dialpad" id="toolbar_button_dialpad" data-container="body" data-toggle="popover" data-placement="right" content="Open dialpad" data-i18n="[content]toolbar.dialpad" style="display: none"></a>
140
-            <a class="button icon-settings" id="toolbar_button_settings" data-container="body" data-toggle="popover" data-placement="right" content="Settings" data-i18n="[content]toolbar.Settings"></a>
141
-            <a class="button icon-raised-hand" id="toolbar_button_raisehand" data-container="body" data-toggle="popover" data-placement="right" data-i18n="[content]toolbar.raiseHand" content="Raise Hand" shortcut="raiseHandPopover"></a>
142
-            <a class="button icon-full-screen" id="toolbar_button_fullScreen" data-container="body" data-toggle="popover" data-placement="right" shortcut="toggleFullscreenPopover" data-i18n="[content]toolbar.fullscreen" content="Enter / Exit Full Screen"></a>
143
-            <a class="button icon-toggle-filmstrip" id="toolbar_film_strip" data-container="body" data-toggle="popover" shortcut="filmstripPopover" data-placement="right" data-i18n="[content]toolbar.filmstrip" content="Show / hide videos"></a>
144
-            <a class="button icon-feedback" id="feedbackButton" data-container="body" data-toggle="popover" data-placement="right" data-i18n="[content]feedback"></a>
142
+            <a class="button icon-telephone" id="toolbar_button_sip" style="display: none"></a>
143
+            <a class="button icon-dialpad" id="toolbar_button_dialpad" style="display: none"></a>
144
+            <a class="button icon-settings" id="toolbar_button_settings"></a>
145
+            <a class="button icon-raised-hand" id="toolbar_button_raisehand" shortcut="raiseHandPopover"></a>
146
+            <a class="button icon-full-screen" id="toolbar_button_fullScreen" shortcut="toggleFullscreenPopover"></a>
147
+            <a class="button icon-toggle-filmstrip" id="toolbar_film_strip" data-container="body" shortcut="filmstripPopover"></a>
148
+            <a class="button icon-feedback" id="feedbackButton"></a>
145
             <div id="sideToolbarContainer">
149
             <div id="sideToolbarContainer">
146
                 <div id="profile_container" class="sideToolbarContainer__inner">
150
                 <div id="profile_container" class="sideToolbarContainer__inner">
147
                     <div class="title" data-i18n="profile.title"></div>
151
                     <div class="title" data-i18n="profile.title"></div>
208
                         <input type="checkbox" id="followMeCheckBox">
212
                         <input type="checkbox" id="followMeCheckBox">
209
                         <label class="followMeLabel" for="followMeCheckBox" data-i18n="settings.followMe"></label>
213
                         <label class="followMeLabel" for="followMeCheckBox" data-i18n="settings.followMe"></label>
210
                     </div>
214
                     </div>
211
-                    <a id="downloadlog" data-container="body" data-toggle="popover" data-placement="right" data-i18n="[data-content]downloadlogs" ><i class="icon-download"></i></a>
212
                 </div>
215
                 </div>
213
             </div>
216
             </div>
214
         </div>
217
         </div>
215
         <div id="videospace">
218
         <div id="videospace">
216
             <div id="largeVideoContainer" class="videocontainer">
219
             <div id="largeVideoContainer" class="videocontainer">
217
-                <div id="presentation"></div>
218
                 <div id="sharedVideo"><div id="sharedVideoIFrame"></div></div>
220
                 <div id="sharedVideo"><div id="sharedVideoIFrame"></div></div>
219
                 <div id="etherpad"></div>
221
                 <div id="etherpad"></div>
220
                 <a target="_new"><div class="watermark leftwatermark"></div></a>
222
                 <a target="_new"><div class="watermark leftwatermark"></div></a>
223
                     <span data-i18n="poweredby"></span> jitsi.org
225
                     <span data-i18n="poweredby"></span> jitsi.org
224
                 </a>
226
                 </a>
225
                 <div id="dominantSpeaker">
227
                 <div id="dominantSpeaker">
228
+                    <div class="dynamic-shadow"></div>
226
                     <img id="dominantSpeakerAvatar" src=""/>
229
                     <img id="dominantSpeakerAvatar" src=""/>
227
-                    <canvas id="dominantSpeakerAudioLevel"></canvas>
228
                 </div>
230
                 </div>
231
+                <span id="remoteConnectionMessage"></span>
229
                 <div id="largeVideoWrapper">
232
                 <div id="largeVideoWrapper">
230
                     <video id="largeVideo" muted="true" autoplay></video>
233
                     <video id="largeVideo" muted="true" autoplay></video>
231
                 </div>
234
                 </div>
232
-                <span id="videoConnectionMessage"></span>
235
+                <span id="localConnectionMessage"></span>
233
                 <span id="videoResolutionLabel">HD</span>
236
                 <span id="videoResolutionLabel">HD</span>
234
                 <span id="recordingLabel" class="centeredVideoLabel">
237
                 <span id="recordingLabel" class="centeredVideoLabel">
235
                     <span id="recordingLabelText"></span>
238
                     <span id="recordingLabelText"></span>
238
             </div>
241
             </div>
239
 
242
 
240
             <div id="remoteVideos">
243
             <div id="remoteVideos">
241
-                <span id="localVideoContainer" class="videocontainer">
244
+                <span id="localVideoContainer" class="videocontainer videocontainer_small">
242
                     <span id="localVideoWrapper">
245
                     <span id="localVideoWrapper">
243
                         <!--<video id="localVideo" autoplay muted></video> - is now per stream generated -->
246
                         <!--<video id="localVideo" autoplay muted></video> - is now per stream generated -->
244
                     </span>
247
                     </span>
245
                     <audio id="localAudio" autoplay muted></audio>
248
                     <audio id="localAudio" autoplay muted></audio>
246
-                    <span class="focusindicator"></span>
249
+                    <div class="videocontainer__toolbar"></div>
247
                 </span>
250
                 </span>
248
                 <audio id="userJoined" src="sounds/joined.wav" preload="auto"></audio>
251
                 <audio id="userJoined" src="sounds/joined.wav" preload="auto"></audio>
249
                 <audio id="userLeft" src="sounds/left.wav" preload="auto"></audio>
252
                 <audio id="userLeft" src="sounds/left.wav" preload="auto"></audio>
257
             </ul>
260
             </ul>
258
         </div>
261
         </div>
259
     </div>
262
     </div>
263
+    <div id="aui-feedback-dialog" class="dialog feedback aui-layer aui-dialog2 aui-dialog2-medium" style="display: none;"></div>
260
   </body>
264
   </body>
261
 </html>
265
 </html>

+ 10
- 1
interface_config.js 查看文件

34
     filmStripOnly: false,
34
     filmStripOnly: false,
35
     RANDOM_AVATAR_URL_PREFIX: false,
35
     RANDOM_AVATAR_URL_PREFIX: false,
36
     RANDOM_AVATAR_URL_SUFFIX: false,
36
     RANDOM_AVATAR_URL_SUFFIX: false,
37
-    FILM_STRIP_MAX_HEIGHT: 120
37
+    FILM_STRIP_MAX_HEIGHT: 120,
38
+    LOCAL_THUMBNAIL_RATIO_WIDTH: 16,
39
+    LOCAL_THUMBNAIL_RATIO_HEIGHT: 9,
40
+    REMOTE_THUMBNAIL_RATIO_WIDTH: 1,
41
+    REMOTE_THUMBNAIL_RATIO_HEIGHT: 1,
42
+    // Enables feedback star animation.
43
+    ENABLE_FEEDBACK_ANIMATION: false,
44
+    DISABLE_FOCUS_INDICATOR: false,
45
+    AUDIO_LEVEL_PRIMARY_COLOR: "rgba(255,255,255,0.7)",
46
+    AUDIO_LEVEL_SECONDARY_COLOR: "rgba(255,255,255,0.4)"
38
 };
47
 };

+ 2
- 0
lang/languages-de.json 查看文件

7
     "hy": "Armenisch",
7
     "hy": "Armenisch",
8
     "it": "Italienisch",
8
     "it": "Italienisch",
9
     "oc": "Okzitanisch",
9
     "oc": "Okzitanisch",
10
+    "pl": "Polnisch",
10
     "ptBR": "Portugiesisch (Brasilien)",
11
     "ptBR": "Portugiesisch (Brasilien)",
12
+    "ru": "Russisch",
11
     "sk": "Slowakisch",
13
     "sk": "Slowakisch",
12
     "sl": "Slowenisch",
14
     "sl": "Slowenisch",
13
     "sv": "Schwedisch",
15
     "sv": "Schwedisch",

+ 15
- 0
lang/languages-pl.json 查看文件

1
+{
2
+    "en": "Angielski",
3
+    "bg": "Bułgarski",
4
+    "de": "Niemiecki",
5
+    "es": "Hiszpański",
6
+    "fr": "Francuski",
7
+    "hy": "Ormiański",
8
+    "it": "Włoski",
9
+    "oc": "Prowansalski",
10
+    "ptBR": "portugalski (brazylijski)",
11
+    "sk": "Słowacki",
12
+    "sl": "Słoweński",
13
+    "sv": "Szwedzki",
14
+    "tr": "Turecki"
15
+}

+ 2
- 0
lang/languages-ptBR.json 查看文件

7
     "hy": "Armênio",
7
     "hy": "Armênio",
8
     "it": "Italiano",
8
     "it": "Italiano",
9
     "oc": "Provençal",
9
     "oc": "Provençal",
10
+    "pl": "Polonês",
10
     "ptBR": "Português (Brasil)",
11
     "ptBR": "Português (Brasil)",
12
+    "ru": "Russo",
11
     "sk": "Eslovaco",
13
     "sk": "Eslovaco",
12
     "sl": "Esloveno",
14
     "sl": "Esloveno",
13
     "sv": "Sueco",
15
     "sv": "Sueco",

+ 2
- 0
lang/languages.json 查看文件

8
     "hy": "Armenian",
8
     "hy": "Armenian",
9
     "it": "Italian",
9
     "it": "Italian",
10
     "oc": "Occitan",
10
     "oc": "Occitan",
11
+    "pl": "Polish",
11
     "ptBR": "Portuguese (Brazil)",
12
     "ptBR": "Portuguese (Brazil)",
13
+    "ru": "Russian",
12
     "sk": "Slovak",
14
     "sk": "Slovak",
13
     "sl": "Slovenian",
15
     "sl": "Slovenian",
14
     "sv": "Swedish",
16
     "sv": "Swedish",

+ 57
- 40
lang/main-de.json 查看文件

1
 {
1
 {
2
-    "contactlist": "Kontaktliste",
2
+    "contactlist": "Im Gespräch",
3
     "connectionsettings": "Verbindungseinstellungen",
3
     "connectionsettings": "Verbindungseinstellungen",
4
     "poweredby": "Betrieben von",
4
     "poweredby": "Betrieben von",
5
-    "downloadlogs": "Log herunterladen",
6
     "feedback": "Wir freuen uns auf Ihr Feedback!",
5
     "feedback": "Wir freuen uns auf Ihr Feedback!",
7
     "roomUrlDefaultMsg": "Die Konferenz wird erstellt...",
6
     "roomUrlDefaultMsg": "Die Konferenz wird erstellt...",
8
-    "participant": "Teilnehmer",
9
     "me": "ich",
7
     "me": "ich",
10
     "speaker": "Sprecher",
8
     "speaker": "Sprecher",
11
     "raisedHand": "Möchte sprechen",
9
     "raisedHand": "Möchte sprechen",
12
     "defaultNickname": "Bsp: Heidi Blau",
10
     "defaultNickname": "Bsp: Heidi Blau",
13
     "defaultLink": "Bsp.: __url__",
11
     "defaultLink": "Bsp.: __url__",
14
-    "calling": "Rufe __name__ an...",
12
+    "callingName": "__name__",
15
     "userMedia": {
13
     "userMedia": {
16
         "react-nativeGrantPermissions": "Bitte Berechtigungen zur Verwendung der Kamera und des Mikrofons durch anwählen von <b><i>Erlauben</i></b>",
14
         "react-nativeGrantPermissions": "Bitte Berechtigungen zur Verwendung der Kamera und des Mikrofons durch anwählen von <b><i>Erlauben</i></b>",
17
         "chromeGrantPermissions": "Bitte Berechtigungen zur Verwendung der Kamera und des Mikrofons durch anwählen von <b><i>Erlauben</i></b>",
15
         "chromeGrantPermissions": "Bitte Berechtigungen zur Verwendung der Kamera und des Mikrofons durch anwählen von <b><i>Erlauben</i></b>",
27
         "raiseHand": "Heben Sie Ihre Hand.",
25
         "raiseHand": "Heben Sie Ihre Hand.",
28
         "pushToTalk": "Drücken um zu sprechen.",
26
         "pushToTalk": "Drücken um zu sprechen.",
29
         "toggleScreensharing": "Zwischen Kamera und Bildschirmfreigabe wechseln.",
27
         "toggleScreensharing": "Zwischen Kamera und Bildschirmfreigabe wechseln.",
30
-        "toggleFilmstrip": "Videovorschau anzeigen oder verstecken.",
28
+        "toggleFilmstrip": "Videos anzeigen oder verbergen.",
31
         "toggleShortcuts": "Hilfe-Menu anzeigen oder verdecken.",
29
         "toggleShortcuts": "Hilfe-Menu anzeigen oder verdecken.",
32
         "focusLocal": "Lokales Video fokussieren.",
30
         "focusLocal": "Lokales Video fokussieren.",
33
         "focusRemote": "Andere Videos fokussieren.",
31
         "focusRemote": "Andere Videos fokussieren.",
37
     },
35
     },
38
     "welcomepage": {
36
     "welcomepage": {
39
         "go": "Los",
37
         "go": "Los",
40
-        "roomname": "Raumnamen eingeben",
38
+        "roomname": "Konferenzname eingeben",
41
         "disable": "Diesen Hinweis nicht mehr anzeigen",
39
         "disable": "Diesen Hinweis nicht mehr anzeigen",
42
         "feature1": {
40
         "feature1": {
43
             "title": "Einfach zu benutzen",
41
             "title": "Einfach zu benutzen",
49
         },
47
         },
50
         "feature3": {
48
         "feature3": {
51
             "title": "Open Source",
49
             "title": "Open Source",
52
-            "content": "__app__ steht unter der Apache Lizenz. Es steht ihnen frei __app__ gemäß dieser Lizenz herunterzuladen, zu verändern oder zu verbreiten."
50
+            "content": "__app__ steht unter der Apache Lizenz. Es steht ihnen frei __app__ gemäss dieser Lizenz herunterzuladen, zu verändern oder zu verbreiten."
53
         },
51
         },
54
         "feature4": {
52
         "feature4": {
55
             "title": "Unbegrenzte Anzahl Benutzer",
53
             "title": "Unbegrenzte Anzahl Benutzer",
76
         "mute": "Stummschaltung aktivieren / deaktivieren",
74
         "mute": "Stummschaltung aktivieren / deaktivieren",
77
         "videomute": "Kamera starten / stoppen",
75
         "videomute": "Kamera starten / stoppen",
78
         "authenticate": "Anmelden",
76
         "authenticate": "Anmelden",
79
-        "lock": "Raum schützen / Schutz aufheben",
77
+        "lock": "Konferenz schützen / Schutz aufheben",
80
         "invite": "Andere einladen",
78
         "invite": "Andere einladen",
81
-        "chat": "Chat öffnen / schließen",
82
-        "etherpad": "Geteiltes Dokument",
83
-        "sharedvideo": "Ein YouTube-Video teilen",
79
+        "chat": "Chat öffnen / schliessen",
80
+        "etherpad": "Dokument teilen",
81
+        "sharedvideo": "YouTube-Video teilen",
84
         "sharescreen": "Bildschirm freigeben",
82
         "sharescreen": "Bildschirm freigeben",
85
         "fullscreen": "Vollbildmodus aktivieren / deaktivieren",
83
         "fullscreen": "Vollbildmodus aktivieren / deaktivieren",
86
         "sip": "SIP Nummer anrufen",
84
         "sip": "SIP Nummer anrufen",
87
         "Settings": "Einstellungen",
85
         "Settings": "Einstellungen",
88
-        "hangup": "Auflegen",
86
+        "hangup": "Konferenz verlassen",
89
         "login": "Anmelden",
87
         "login": "Anmelden",
90
         "logout": "Abmelden",
88
         "logout": "Abmelden",
91
         "dialpad": "Tastenblock anzeigen",
89
         "dialpad": "Tastenblock anzeigen",
93
         "micMutedPopup": "Ihr Mikrofon wurde stumm geschaltet damit das<br/>geteilte Video genossen werden kann.",
91
         "micMutedPopup": "Ihr Mikrofon wurde stumm geschaltet damit das<br/>geteilte Video genossen werden kann.",
94
         "unableToUnmutePopup": "Die Stummschaltung kann nicht aufgehoben werden während das geteilte Video abgespielt wird.",
92
         "unableToUnmutePopup": "Die Stummschaltung kann nicht aufgehoben werden während das geteilte Video abgespielt wird.",
95
         "cameraDisabled": "Keine Kamera verfügbar",
93
         "cameraDisabled": "Keine Kamera verfügbar",
96
-        "micDisabled": "Kein Mikrofon verfügbar"
94
+        "micDisabled": "Kein Mikrofon verfügbar",
95
+        "filmstrip": "Videos anzeigen / verbergen",
96
+        "raiseHand": "Hand erheben um zu sprechen"
97
     },
97
     },
98
     "bottomtoolbar": {
98
     "bottomtoolbar": {
99
-        "chat": "Chat öffnen / schließen",
100
-        "filmstrip": "Videovorschau anzeigen / verstecken",
101
-        "contactlist": "Kontaktliste öffnen / schließen"
99
+        "chat": "Chat öffnen / schliessen",
100
+        "filmstrip": "Videos anzeigen / verbergen",
101
+        "contactlist": "Kontaktliste öffnen / schliessen"
102
     },
102
     },
103
     "chat": {
103
     "chat": {
104
         "nickname": {
104
         "nickname": {
105
-            "title": "Nickname im Eingabefeld eingeben",
106
-            "popover": "Einen Namen auswählen"
105
+            "title": "Name eingeben",
106
+            "popover": "Name"
107
         },
107
         },
108
         "messagebox": "Text eingeben..."
108
         "messagebox": "Text eingeben..."
109
     },
109
     },
111
         "title": "Einstellungen",
111
         "title": "Einstellungen",
112
         "update": "Aktualisieren",
112
         "update": "Aktualisieren",
113
         "name": "Name",
113
         "name": "Name",
114
-        "startAudioMuted": "Stumm beitreten",
115
-        "startVideoMuted": "Ohne Video beitreten",
116
-        "selectCamera": "Kamera auswählen",
117
-        "selectMic": "Mikrofon auswählen",
118
-        "selectAudioOutput": "Audio-Ausgabe auswählen",
119
-        "followMe": "Follow-me aktivieren",
114
+        "startAudioMuted": "Alle Teilnehmer treten stumm geschaltet bei",
115
+        "startVideoMuted": "Alle Teilnehmer treten ohne Video bei",
116
+        "selectCamera": "Kamera",
117
+        "selectMic": "Mikrofon",
118
+        "selectAudioOutput": "Audioausgabe",
119
+        "followMe": "Follow-me für alle Teilnehmer",
120
         "noDevice": "Kein",
120
         "noDevice": "Kein",
121
         "noPermission": "Keine Berechtigung um das Gerät zu verwenden",
121
         "noPermission": "Keine Berechtigung um das Gerät zu verwenden",
122
-        "avatarUrl": "Avatar URL"
122
+        "cameraAndMic": "Kamera und Mikrofon",
123
+        "moderator": "MODERATOR",
124
+        "password": "PASSWORT SETZEN",
125
+        "audioVideo": "AUDIO UND VIDEO",
126
+        "setPasswordLabel": "Konferenz mit einem Passwort schützen."
127
+    },
128
+    "profile": {
129
+        "title": "PROFIL",
130
+        "setDisplayNameLabel": "Anzeigename festlegen",
131
+        "setEmailLabel": "E-Mail Adresse für Gravatar"
123
     },
132
     },
124
     "videothumbnail": {
133
     "videothumbnail": {
125
         "editnickname": "Klicken, um den Anzeigenamen zu bearbeiten",
134
         "editnickname": "Klicken, um den Anzeigenamen zu bearbeiten",
126
         "moderator": "Besitzer dieser Konferenz",
135
         "moderator": "Besitzer dieser Konferenz",
127
-        "videomute": "Teilnehmer hat die Kamera pausiert.",
136
+        "videomute": "Teilnehmer hat die Kamera pausiert",
128
         "mute": "Teilnehmer ist stumm geschaltet",
137
         "mute": "Teilnehmer ist stumm geschaltet",
129
         "kick": "Hinauswerfen",
138
         "kick": "Hinauswerfen",
130
         "muted": "Stummgeschaltet",
139
         "muted": "Stummgeschaltet",
172
         "connectError": "Oh! Es hat etwas nicht geklappt und der Konferenz konnte nicht beigetreten werden.",
181
         "connectError": "Oh! Es hat etwas nicht geklappt und der Konferenz konnte nicht beigetreten werden.",
173
         "connectErrorWithMsg": "Oh! Es hat etwas nicht geklappt und der Konferenz konnte nicht beigetreten werden: __msg__",
182
         "connectErrorWithMsg": "Oh! Es hat etwas nicht geklappt und der Konferenz konnte nicht beigetreten werden: __msg__",
174
         "connecting": "Verbindung wird hergestellt",
183
         "connecting": "Verbindung wird hergestellt",
184
+        "copy": "Kopieren",
175
         "error": "Fehler",
185
         "error": "Fehler",
176
         "detectext": "Fehler bei der Erkennung der Bildschirmfreigabeerweiterung.",
186
         "detectext": "Fehler bei der Erkennung der Bildschirmfreigabeerweiterung.",
177
         "failtoinstall": "Die Bildschirmfreigabeerweiterung konnte nicht installiert werden.",
187
         "failtoinstall": "Die Bildschirmfreigabeerweiterung konnte nicht installiert werden.",
183
         "lockMessage": "Die Konferenz konnte nicht gesperrt werden.",
193
         "lockMessage": "Die Konferenz konnte nicht gesperrt werden.",
184
         "warning": "Warnung",
194
         "warning": "Warnung",
185
         "passwordNotSupported": "Passwörter für Räume werden nicht unterstützt.",
195
         "passwordNotSupported": "Passwörter für Räume werden nicht unterstützt.",
186
-        "sorry": "Entschuldigung",
187
-        "internalError": "Interner Anwendungsfehler [setRemoteDescription]",
196
+        "internalErrorTitle": "Interner Fehler",
197
+        "internalError": "Ups! Es ist etwas schiefgegangen. Der Fehler [setRemoteDescription] ist aufgetreten.",
188
         "unableToSwitch": "Der Videodatenstrom kann nicht gewechselt werden.",
198
         "unableToSwitch": "Der Videodatenstrom kann nicht gewechselt werden.",
189
         "SLDFailure": "Oh! Die Stummschaltung konnte nicht aktiviert werden. (SLD Fehler)",
199
         "SLDFailure": "Oh! Die Stummschaltung konnte nicht aktiviert werden. (SLD Fehler)",
190
         "SRDFailure": "Oh! Das Video konnte nicht gestoppt werden. (SRD Fehler)",
200
         "SRDFailure": "Oh! Das Video konnte nicht gestoppt werden. (SRD Fehler)",
206
         "logoutTitle": "Abmelden",
216
         "logoutTitle": "Abmelden",
207
         "logoutQuestion": "Sind Sie sicher, dass Sie sich abmelden und die Konferenz verlassen möchten?",
217
         "logoutQuestion": "Sind Sie sicher, dass Sie sich abmelden und die Konferenz verlassen möchten?",
208
         "sessTerminated": "Sitzung beendet",
218
         "sessTerminated": "Sitzung beendet",
209
-        "hungUp": "Anruf beendet",
219
+        "hungUp": "Konferenz beendet",
210
         "joinAgain": "Erneut beitreten",
220
         "joinAgain": "Erneut beitreten",
211
         "Share": "Teilen",
221
         "Share": "Teilen",
212
         "Save": "Speichern",
222
         "Save": "Speichern",
215
         "Dial": "Wählen",
225
         "Dial": "Wählen",
216
         "sipMsg": "Geben Sie eine SIP Nummer ein",
226
         "sipMsg": "Geben Sie eine SIP Nummer ein",
217
         "passwordCheck": "Sind Sie sicher, dass Sie das Passwort entfernen möchten?",
227
         "passwordCheck": "Sind Sie sicher, dass Sie das Passwort entfernen möchten?",
218
-        "passwordMsg": "Passwort setzen, um den Raum zu schützen",
219
-        "Invite": "Einladen",
220
-        "shareLink": "Teilen Sie diesen Link mit jedem den Sie einladen möchten",
228
+        "passwordMsg": "Passwort setzen um die Konferenz zu schützen",
229
+        "shareLink": "Diesen Link kopieren und teilen",
221
         "settings1": "Konferenz einrichten",
230
         "settings1": "Konferenz einrichten",
222
         "settings2": "Teilnehmer treten stummgeschaltet bei",
231
         "settings2": "Teilnehmer treten stummgeschaltet bei",
223
-        "settings3": "Nickname erforderlich<br/><br/>Setzen Sie ein Passwort, um den Raum zu schützen:",
224
-        "yourPassword": "Ihr Passwort",
232
+        "settings3": "Name erforderlich<br/><br/>Setzen Sie ein Passwort, um die Konferenz zu schützen:",
233
+        "yourPassword": "Neues Passwort eingeben",
225
         "Back": "Zurück",
234
         "Back": "Zurück",
226
         "serviceUnavailable": "Dienst nicht verfügbar",
235
         "serviceUnavailable": "Dienst nicht verfügbar",
227
         "gracefulShutdown": "Der Dienst steht momentan wegen Wartungsarbeiten nicht zur Verfügung. Bitte versuchen Sie es später noch einmal.",
236
         "gracefulShutdown": "Der Dienst steht momentan wegen Wartungsarbeiten nicht zur Verfügung. Bitte versuchen Sie es später noch einmal.",
228
         "Yes": "Ja",
237
         "Yes": "Ja",
229
         "reservationError": "Fehler im Reservationssystem",
238
         "reservationError": "Fehler im Reservationssystem",
230
         "reservationErrorMsg": "Fehler, Nummer: __code__, Nachricht: __msg__",
239
         "reservationErrorMsg": "Fehler, Nummer: __code__, Nachricht: __msg__",
231
-        "password": "Passwort",
240
+        "password": "Passwort eingeben",
232
         "userPassword": "Benutzerpasswort",
241
         "userPassword": "Benutzerpasswort",
233
         "token": "Token",
242
         "token": "Token",
234
-        "tokenAuthFailed": "Anmeldung am XMPP-Server fehlgeschlagen: ungültiges Token",
243
+        "tokenAuthFailedTitle": "Authentifizierungsfehler",
244
+        "tokenAuthFailed": "Sie sind nicht berechtigt dieser Konferenz beizutreten.",
235
         "displayNameRequired": "Geben Sie Ihren Anzeigenamen ein",
245
         "displayNameRequired": "Geben Sie Ihren Anzeigenamen ein",
236
         "extensionRequired": "Erweiterung erforderlich:",
246
         "extensionRequired": "Erweiterung erforderlich:",
237
         "firefoxExtensionPrompt": "Um die Bildschirmfreigabe nutzen zu können, muss eine Firefox-Erweiterung installiert werden. Bitte versuchen Sie es erneut nachdem die <a href='__url__'>Erweiterung installiert</a> wurde.",
247
         "firefoxExtensionPrompt": "Um die Bildschirmfreigabe nutzen zu können, muss eine Firefox-Erweiterung installiert werden. Bitte versuchen Sie es erneut nachdem die <a href='__url__'>Erweiterung installiert</a> wurde.",
238
-        "feedbackQuestion": "Wie war der Anruf?",
248
+        "rateExperience": "Bitte bewerten Sie diese Konferenz.",
249
+        "feedbackHelp": "Ihr Feedback hilft uns die Qualität der Konferenzen zu verbessern.",
250
+        "feedbackQuestion": "Anmerkungen zur Konferenz.",
239
         "thankYou": "Danke für die Verwendung von __appName__!",
251
         "thankYou": "Danke für die Verwendung von __appName__!",
240
         "sorryFeedback": "Tut uns leid. Möchten Sie uns mehr mitteilen?",
252
         "sorryFeedback": "Tut uns leid. Möchten Sie uns mehr mitteilen?",
241
         "liveStreaming": "Live-Streaming",
253
         "liveStreaming": "Live-Streaming",
253
         "cameraUnsupportedResolutionError": "Die Kamera unterstützt die erforderliche Auflösung nicht.",
265
         "cameraUnsupportedResolutionError": "Die Kamera unterstützt die erforderliche Auflösung nicht.",
254
         "cameraUnknownError": "Die Kamera kann aus einem unbekannten Grund nicht verwendet werden.",
266
         "cameraUnknownError": "Die Kamera kann aus einem unbekannten Grund nicht verwendet werden.",
255
         "cameraPermissionDeniedError": "Die Berechtigung zur Verwendung der Kamera wurde nicht erteilt. Sie können trotzdem an der Konferenz teilnehmen, aber die anderen Teilnehmer können Sie nicht sehen. Verwenden Sie die Kamera-Schaltfläche in der Adressleiste um die Berechtigungen zu erteilen.",
267
         "cameraPermissionDeniedError": "Die Berechtigung zur Verwendung der Kamera wurde nicht erteilt. Sie können trotzdem an der Konferenz teilnehmen, aber die anderen Teilnehmer können Sie nicht sehen. Verwenden Sie die Kamera-Schaltfläche in der Adressleiste um die Berechtigungen zu erteilen.",
256
-        "cameraNotFoundError": "Die Berechtigung zur Verwendung der Kamera wurde nicht erteilt. Sie können trotzdem an der Konferenz teilnehmen, aber die anderen Teilnehmer können Sie nicht sehen. Verwenden Sie die Kamera-Schaltfläche in der Adressleiste um die Berechtigungen zu erteilen.",
268
+        "cameraNotFoundError": "Kamera nicht gefunden.",
257
         "cameraConstraintFailedError": "Ihre Kamera erfüllt die notwendigen Anforderungen nicht.",
269
         "cameraConstraintFailedError": "Ihre Kamera erfüllt die notwendigen Anforderungen nicht.",
258
         "micUnknownError": "Das Mikrofon kann aus einem unbekannten Grund nicht verwendet werden.",
270
         "micUnknownError": "Das Mikrofon kann aus einem unbekannten Grund nicht verwendet werden.",
259
         "micPermissionDeniedError": "Die Berechtigung zur Verwendung des Mikrofons wurde nicht erteilt. Sie können trotzdem an der Konferenz teilnehmen, aber die anderen Teilnehmer können Sie nicht hören. Verwenden Sie die Kamera-Schaltfläche in der Adressleiste um die Berechtigungen zu erteilen.",
271
         "micPermissionDeniedError": "Die Berechtigung zur Verwendung des Mikrofons wurde nicht erteilt. Sie können trotzdem an der Konferenz teilnehmen, aber die anderen Teilnehmer können Sie nicht hören. Verwenden Sie die Kamera-Schaltfläche in der Adressleiste um die Berechtigungen zu erteilen.",
260
-        "micNotFoundError": "Das angeforderte Mikrofon konnte nicht gefunden werden.",
261
-        "micConstraintFailedError": "Ihr Mikrofon erfüllt die notwendigen Anforderungen nicht."
272
+        "micNotFoundError": "Mikrofon nicht gefunden.",
273
+        "micConstraintFailedError": "Ihr Mikrofon erfüllt die notwendigen Anforderungen nicht.",
274
+        "micNotSendingData": "Das Mikrofon kann nicht verwendet werden. Bitte wählen Sie ein anderes Mikrofon in den Einstellungen oder laden Sie die Konferenz neu.",
275
+        "cameraNotSendingData": "Die Kamera kann nicht verwendet werden. Bitte wählen Sie eine andere Kamera in den Einstellungen oder laden Sie die Konferenz neu.",
276
+        "goToStore": "Zum Store",
277
+        "externalInstallationTitle": "Erweiterung erforderlich",
278
+        "externalInstallationMsg": "Die Bildschirmfreigabeerweiterung muss installiert werden."
262
     },
279
     },
263
     "\u0005dialog": {},
280
     "\u0005dialog": {},
264
     "email": {
281
     "email": {

+ 344
- 0
lang/main-pl.json 查看文件

1
+{
2
+    "contactlist": "w trakcie rozmowy",
3
+    "connectionsettings": "ustawienia połączenia",
4
+    "poweredby": "Uruchomiono",
5
+    "feedback": "jaka jest twoja opinia ?",
6
+    "roomUrlDefaultMsg": "otwarto twoją konferencję",
7
+    "me": "to ja",
8
+    "speaker": "głośnik",
9
+    "raisedHand": "Chcesz się odezwać ?",
10
+    "defaultNickname": "np. Ziutek Kowalski",
11
+    "defaultLink": "np. _url_",
12
+    "callingName": "_nazwa_",
13
+    "userMedia": {
14
+        "react-nativeGrantPermissions": "",
15
+        "chromeGrantPermissions": "",
16
+        "androidGrantPermissions": "",
17
+        "firefoxGrantPermissions": "wyraź zgodę na użycie kamery i mikrofonu naciskając <b><i>Share Selected Device</i></b> przycisk",
18
+        "operaGrantPermissions": "wyraź zgodę na użycie kamery i mikrofonu naciskając <b><i>Allow</i></b> przycisk",
19
+        "iexplorerGrantPermissions": "",
20
+        "safariGrantPermissions": "wyraź zgodę na użycie kamery i mikrofonu naciskając <b><i>OK</i></b> przycisk",
21
+        "nwjsGrantPermissions": "wyraź zgodę na użycie kamery i mikrofonu"
22
+    },
23
+    "keyboardShortcuts": {
24
+        "keyboardShortcuts": "Skróty klawiaturowe:",
25
+        "raiseHand": "Unieś rękę.",
26
+        "pushToTalk": "naciśnij i mów",
27
+        "toggleScreensharing": "Przełączanie pomiędzy kamerą i wspóldzieleniem ekranu",
28
+        "toggleFilmstrip": "Pokaż lub ukryj klipy wideo",
29
+        "toggleShortcuts": "Pokaż lub ukryj pasek pomocy.",
30
+        "focusLocal": "Przełącz na lokalne wideo.",
31
+        "focusRemote": "Przełącz na któreś ze zdalnych wideo.",
32
+        "toggleChat": "Otwórz lub zamknij panel czat.",
33
+        "mute": "Wyłącz lub włącz mikrofon.",
34
+        "videoMute": "Start lub stop lokalne wideo."
35
+    },
36
+    "welcomepage": {
37
+        "go": "IDŹ",
38
+        "roomname": "Podaj nazwę sali konferencyjnej",
39
+        "disable": "nie pokazuj ponownie",
40
+        "feature1": {
41
+            "title": "użyj",
42
+            "content": "nie musisz nic pobierać. _app_ jest gotowa do użycia bezpośrednio w przeglądarce. Zaproś innych do udziału w konferencji podając adres URL"
43
+        },
44
+        "feature2": {
45
+            "title": "za mała przepustowość",
46
+            "content": "Dla konferencji video potrzeba nie więcej niż 128 kbit/sek. Konferencje dzielenia ekranu lub tylko audio są możliwe przy mniejszej przepustowości. "
47
+        },
48
+        "feature3": {
49
+            "title": "Open source",
50
+            "content": "_app_ oparta jest na Apache License. Możesz swobodnie pobierać ją, używać, modyfikować i dzielić się nią."
51
+        },
52
+        "feature4": {
53
+            "title": "Nieograniczona liczba użytkowników",
54
+            "content": "Liczba użytkowników czy uczestników konferencji nie jest ograniczona.  Determinuje ją moc serwera i dostępna przepustowość lącza."
55
+        },
56
+        "feature5": {
57
+            "title": "Współdzielenie ekranu",
58
+            "content": "Z łatwością podzielisz się ekranem z innymi. _app_ jest idealnym narzędziem do prezentacji, nauczania i udzielania zdalnej pomocy technicznej."
59
+        },
60
+        "feature6": {
61
+            "title": "Sale bezpieczne.",
62
+            "content": "Potrzebujesz prywatności? _app_ sale konferencyjne mogą być zabezpieczone hasłami niedopuszczającymi niezaproszonych uczestników czy też osoby chcące zakłócić konferencję."
63
+        },
64
+        "feature7": {
65
+            "title": "Współdzielenie uwag.",
66
+            "content": "_app_ zawiera Etherpad, współdzielony edytor tekstu doskonały dla redakcji zespołowych artykułów czy komentarzy."
67
+        },
68
+        "feature8": {
69
+            "title": "Statystyki użycia.",
70
+            "content": "Analizuj uczestników konferencji z łatwościa integrując dane z Piwik i Google Analitics i innymi systemami monitorującymi."
71
+        }
72
+    },
73
+    "toolbar": {
74
+        "mute": "Wycisz / Pogłośnij",
75
+        "videomute": "Kamera start / stop ",
76
+        "authenticate": "Uwierzytelnianie",
77
+        "lock": "Zamknij / Otwórz salę",
78
+        "invite": "Zaproś innych",
79
+        "chat": "",
80
+        "etherpad": "Udostępniaj dokument",
81
+        "sharedvideo": "Udostępniaj wideo w Youtube",
82
+        "sharescreen": "Udostępnij ekran",
83
+        "fullscreen": "Otwórz / Zamknij pełny ekran",
84
+        "sip": "Wykręć numer SIP",
85
+        "Settings": "",
86
+        "hangup": "Rozłącz",
87
+        "login": "Zaloguj",
88
+        "logout": "",
89
+        "dialpad": "Wyświetl panel wybierania",
90
+        "sharedVideoMutedPopup": "Współdzielone wideo zostało wyciszone i <br/> możesz zacząć rozmawiać z innymi.",
91
+        "micMutedPopup": "Mikrofon został wyłączony i <br/> możesz spokojnie konsumować współdzielone wideo",
92
+        "unableToUnmutePopup": "Nie możesz pogłośnić audio podczas współużytkowania wideo",
93
+        "cameraDisabled": "Kamera nie jest dostępna",
94
+        "micDisabled": "Mikrofon nie jest dostępny",
95
+        "filmstrip": "",
96
+        "raiseHand": "Podnieś rękę chcąc zabrać głos"
97
+    },
98
+    "bottomtoolbar": {
99
+        "chat": "Otwórz / Zamknij Czat",
100
+        "filmstrip": "Pokaż / Ukryj klipy wideo",
101
+        "contactlist": "Otwórz / Zamknij spis kontaktów"
102
+    },
103
+    "chat": {
104
+        "nickname": {
105
+            "title": "Podaj swój nick poniżej",
106
+            "popover": "Wybierz swój nick"
107
+        },
108
+        "messagebox": "Umieść tekst...."
109
+    },
110
+    "settings": {
111
+        "title": "Ustawienia",
112
+        "update": "Aktualizacja",
113
+        "name": "Nazwa",
114
+        "startAudioMuted": "Wszyscy się wyciszyli",
115
+        "startVideoMuted": "Wszyscy się ukryli",
116
+        "selectCamera": "Kamera",
117
+        "selectMic": "Mikrofon",
118
+        "selectAudioOutput": "Wyjście audio",
119
+        "followMe": "Wszyscy za mną",
120
+        "noDevice": "Brak",
121
+        "noPermission": "Nie ma zgody na użycie urządzenia",
122
+        "cameraAndMic": "Kamera i Mikrofon",
123
+        "moderator": "MODERATOR",
124
+        "password": "USTAW HASŁO",
125
+        "audioVideo": "AUDIO I WIDEO",
126
+        "setPasswordLabel": "Zamknij salę konferencyjną z hasłem"
127
+    },
128
+    "profile": {
129
+        "title": "PROFIL",
130
+        "setDisplayNameLabel": "Podaj swoją wyświetlaną nazwę",
131
+        "setEmailLabel": "Ustaw email swojego gravatara"
132
+    },
133
+    "videothumbnail": {
134
+        "editnickname": "Kliknij <br/>celem edycji swojej nazwy",
135
+        "moderator": "Gospodarz <br/>tej konferencji",
136
+        "videomute": "Uczestnik <br/>wyłączyl kamerę",
137
+        "mute": "Uczestnik ma wyciszone audio",
138
+        "kick": "Kick out",
139
+        "muted": "Wyciszony",
140
+        "domute": "Wyciszenie",
141
+        "flip": "Odwrócenie"
142
+    },
143
+    "connectionindicator": {
144
+        "bitrate": "Szybkość transmisji:",
145
+        "packetloss": "Strata pakietów:",
146
+        "resolution": "Rozdzielczość:",
147
+        "less": "Pokaż mniej",
148
+        "more": "Pokaż więcej",
149
+        "address": "Adres:",
150
+        "remoteport": "Zdalny port:Zdalne porty:",
151
+        "remoteport_plural_2": "",
152
+        "remoteport_plural_5": "",
153
+        "localport": "Lokalny port:Lokalne porty:",
154
+        "localport_plural_2": "",
155
+        "localport_plural_5": "",
156
+        "localaddress": "Lokalny adres:Lokalne Adresy:",
157
+        "localaddress_plural_2": "",
158
+        "localaddress_plural_5": "",
159
+        "remoteaddress": "Zdalny adres:Zdalne adresy:",
160
+        "remoteaddress_plural_2": "",
161
+        "remoteaddress_plural_5": "",
162
+        "transport": "Przekazywanie:",
163
+        "bandwidth": "Zakładana przepustowość:",
164
+        "na": "Po informację o połączeniu wróć gdy wystartuje konferencja"
165
+    },
166
+    "notify": {
167
+        "disconnected": "rozłączone",
168
+        "moderator": "Prawa moderatora przydzielone!",
169
+        "connected": "połączono",
170
+        "somebody": "Ktoś",
171
+        "me": "To ja",
172
+        "focus": "Fokus konferencji",
173
+        "focusFail": "_składnik_nie dostępny - zastosuj w _ms_sek",
174
+        "grantedTo": "Prawa moderatora przyznane _to_!",
175
+        "grantedToUnknown": "Prawa Moderatora przyznane $t(somebody)!",
176
+        "muted": "Masz wyciszony mikrofon",
177
+        "mutedTitle": "Jesteś wyciszony!",
178
+        "raisedHand": "Możesz mówić."
179
+    },
180
+    "dialog": {
181
+        "kickMessage": "Ocho! Zostałeś wyproszony z konferencji!",
182
+        "popupError": "Twoja przeglądarka blokuje wyskakujące okienka z tej witryny. Proszę, zmień w ustawieniach przeglądarki.",
183
+        "passwordError": "Ta konwersacja aktualnie jest zabezpieczona hasłem. Tylko gospodarz konferencji może zakładać hasło.",
184
+        "passwordError2": "Ta rozmowa nie jest zabezpieczona hasłem. Tylko gospodarz konferencji może ustanowić hasło zabezpieczające.",
185
+        "connectError": "Ocho! Cos poszło nie tak, nie można podłaczyć się do tej konferencji.",
186
+        "connectErrorWithMsg": "Ocho! Coś poszło nie tak i nie można podłączyć się do tej konferencji:_msg_",
187
+        "connecting": "",
188
+        "copy": "Kopiuj",
189
+        "error": "",
190
+        "detectext": "Błąd podczas rozpoznania rozszerzenia wspóldzielenia ekranu.",
191
+        "failtoinstall": "Instalacja współdzielenia ekranu nie powiodła się.",
192
+        "failedpermissions": "Brak akceptacji dla użycia kamery i mikrofonu",
193
+        "bridgeUnavailable": "Jitsi Videobridge aktualnie jest niedostępne. Proszę, spróbuj później!",
194
+        "jicofoUnavailable": "Jicofo jest aktualnie niedostępne. Proszę, spróbuj później!",
195
+        "maxUsersLimitReached": "Osiągnięto max liczbę uczestników konferencji. Proszę spróbuj później! ",
196
+        "lockTitle": "Nie powiodło się zabezpieczenie konferencji",
197
+        "lockMessage": "Zabezpieczenie konferencji nie powiodło się.",
198
+        "warning": "Uwaga",
199
+        "passwordNotSupported": "Hasła sali konferencyjnych są aktualnie niedostępne.",
200
+        "internalErrorTitle": "Błąd wewnętrzny",
201
+        "internalError": "Ocho! coś poszło nie tak. Wystąpił błąd:  [setRemoteDescription]",
202
+        "unableToSwitch": "Nie można przełaczyć na strumień wideo",
203
+        "SLDFailure": "Ocho! Coś poszło nie tak i nie można wyciszyć! (SLD Failure)",
204
+        "SRDFailure": "Ocho! Coś poszło nie tak i nie można zatrzymać wideo! (SRD Failure)",
205
+        "oops": "Ups",
206
+        "defaultError": "Wystąpił jakiś błąd",
207
+        "passwordRequired": "Wymagane hasło",
208
+        "Ok": "Ok",
209
+        "Remove": "Usuń",
210
+        "shareVideoTitle": "Współdziel wideo",
211
+        "shareVideoLinkError": "Podaj proszę prawidłowy link youtube.",
212
+        "removeSharedVideoTitle": "Usuń wideo współdzielone",
213
+        "removeSharedVideoMsg": "Na pewno chcesz usunąć współdzielone wideo?",
214
+        "alreadySharedVideoMsg": "Inny uczestnik aktualnie współdzieli wideo. W tej konferencji tylko jedno wideo może być współdzielone.",
215
+        "WaitingForHost": "Oczekiwanie na komputer",
216
+        "WaitForHostMsg": "Konferencja <b>_room_</b> jeszcze nie wystartowała. Jeśli jesteś gospodarzem podaj dane autentykacji. Jeśli nie czekaj na gospodarza.",
217
+        "IamHost": "Jestem gospodarzem",
218
+        "Cancel": "Anuluj",
219
+        "retry": "Ponów",
220
+        "logoutTitle": "Wyloguj",
221
+        "logoutQuestion": "Na pewno chcesz się wylogować i zakończyć konferencję?",
222
+        "sessTerminated": "Sesja zakończona",
223
+        "hungUp": "Przerwałeś połączenie",
224
+        "joinAgain": "Ponownie przystąp",
225
+        "Share": "Współdziel",
226
+        "Save": "Zapisz",
227
+        "recording": "",
228
+        "recordingToken": "Proszę podać token nagrywania",
229
+        "Dial": "Dzwoń",
230
+        "sipMsg": "Podaj numer SIP",
231
+        "passwordCheck": "Czy na pewno chcesz usunąć swoje hasło ?",
232
+        "passwordMsg": "Podaj hasło aby zabezpieczyć salę konferencyjną",
233
+        "shareLink": "Skopiuj i udostępnij ten link",
234
+        "settings1": "Skonfiguruj swoją konferencję",
235
+        "settings2": "Wyciszenie współuczestników",
236
+        "settings3": "Wymagane nicki <br/><br/>Wprowadź hasło dla zabezpieczenia sali konferencyjnej:",
237
+        "yourPassword": "Proszę wprowadzić nowe hasło",
238
+        "Back": "Wstecz",
239
+        "serviceUnavailable": "Usługa jest niedostępna",
240
+        "gracefulShutdown": "Aktualnie serwis jest konserwowany. Prosze spróbować później.",
241
+        "Yes": "Tak",
242
+        "reservationError": "Błąd systemu rezerwacji",
243
+        "reservationErrorMsg": "Kod błędu: _code_, treść: _msg_",
244
+        "password": "Podaj hasło",
245
+        "userPassword": "hasło użytkownika",
246
+        "token": "token",
247
+        "tokenAuthFailedTitle": "Problem uwierzytelnienia",
248
+        "tokenAuthFailed": "Przepraszam, ale nie jesteś upoważniony do uczestnictwa w tym połączeniu",
249
+        "displayNameRequired": "Wprowadź swoją nazwę użytkownika",
250
+        "extensionRequired": "Wymagane jest rozszerzenie:",
251
+        "firefoxExtensionPrompt": "Potrzebujesz zainstalować rozszerzenie firefox aby móc współdzielić ekran. Spróbuj ponownie później <a href='__url__'>weź z</a>!",
252
+        "rateExperience": "Oceń proszę swoje doświadczenia z konferencji.",
253
+        "feedbackHelp": "Twoja opinia będzie pomocna w usprawnieniu naszego serwisu.",
254
+        "feedbackQuestion": "Powiedz nam o twoim połączeniu!",
255
+        "thankYou": "Dziękujemy Ci za używanie _appName_!",
256
+        "sorryFeedback": "Przykro nam to słyszeć. Czy możesz powiedzieć więcej na ten temat?",
257
+        "liveStreaming": "",
258
+        "streamKey": "Nazwa strumienia/klucz",
259
+        "startLiveStreaming": "Uruchom strumień live",
260
+        "stopStreamingWarning": "Czy jesteś pewny, że chcesz zatrzymać ten strumień live?",
261
+        "stopRecordingWarning": "Naprawdę chcesz zatrzymać nagrywanie?",
262
+        "stopLiveStreaming": "Zatrzymaj transmisję live",
263
+        "stopRecording": "Zatrzymaj nagrywanie",
264
+        "doNotShowWarningAgain": "Nie pokazuj tego ostrzeżenia ponownie",
265
+        "permissionDenied": "Brak uprawnień",
266
+        "screenSharingPermissionDeniedError": "Nie posiadasz uprawnień do współdzielenia ekranu.",
267
+        "micErrorPresent": "Wystąpił błąd w dostępie do mikrofonu.",
268
+        "cameraErrorPresent": "Wystąpił błąd w dostępie do twojej kamery.",
269
+        "cameraUnsupportedResolutionError": "Twoja kamera nie obsługuje wymaganej rozdzielczości.",
270
+        "cameraUnknownError": "Z nieznanej przyczyny nie można użyć kamery ",
271
+        "cameraPermissionDeniedError": "Nie udzieliłeś pozwolenia na użycie twojej kamery. Nadal możesz włączyć się do konferencji ale inni nie będą cię widzieli. Naciśnij przycisk kamera w pasku menu aby użyć właściwą kamerę. ",
272
+        "cameraNotFoundError": "Kamera nie znaleziona.",
273
+        "cameraConstraintFailedError": "Twoja kamera nie spełnia wymagań.",
274
+        "micUnknownError": "Z przyczyn nieznanych nie można użyć mikrofonu. ",
275
+        "micPermissionDeniedError": "Nie udzieliłeś pozwolenia na użycie twojego mikrofonu. Nadal możesz uczestniczyc w konferencji ale inni nie będą cię słyszeli. Użyj przycisku kamera aby to naprawić.",
276
+        "micNotFoundError": "Mikrofon nie jest odnaleziony.",
277
+        "micConstraintFailedError": "Twój mikrofon nie obsługuje wymaganych parametrów.",
278
+        "micNotSendingData": "Nie możemy mieć dostępu do twojego mikrofonu. Proszę, wskaż inne urządzenie lub przeładuj aplikację.",
279
+        "cameraNotSendingData": "Nie możemy mieć dostępu do twojej kamery. Sprawdź czy inna aplikacja nie używa twojej kamery, wybierz inne urządzenie lub ponownie uruchom aplikację.",
280
+        "goToStore": "Idź do sklepu",
281
+        "externalInstallationTitle": "Wymagane rozszerzenie",
282
+        "externalInstallationMsg": "Zainstaluj rozszerzenie naszego współdzielenia ekranu."
283
+    },
284
+    "email": {
285
+        "sharedKey": [
286
+            "Ta konferencja jest zabezpieczona hasłem. Aby się podłączyć proszę zastosuj następujący pin:",
287
+            "",
288
+            "",
289
+            "_sharedKey_",
290
+            "",
291
+            " "
292
+        ],
293
+        "subject": "Zaproszenie do a_appName_(_conferenceName_)",
294
+        "body": [
295
+            "Witaj, I%27 zaprasza cię do udziału w konferencji_appName_.",
296
+            "",
297
+            "",
298
+            "Kliknij na poniższy link aby uczestniczyć w konferencji.",
299
+            "",
300
+            "",
301
+            "_roomUrl_",
302
+            "",
303
+            "",
304
+            "_sharedKeyTex_",
305
+            "Zauważ, że -appName_ możesz używać tylko przy pomocy _supportedBrowsers_.",
306
+            "",
307
+            "",
308
+            "Polączymy się błyskawicznie! "
309
+        ],
310
+        "and": "i"
311
+    },
312
+    "connection": {
313
+        "ERROR": "Błąd",
314
+        "CONNECTING": "Nawiązywanie połączenia",
315
+        "RECONNECTING": "Wystąpił problem w sieci. Ponowienie połaczenia....",
316
+        "CONNFAIL": "Połączenie się nie powiodło",
317
+        "AUTHENTICATING": "Uwierzytelnianie",
318
+        "AUTHFAIL": "Uwierzytelnianie nie powiodło się",
319
+        "CONNECTED": "Połączono",
320
+        "DISCONNECTED": "Rozłączony",
321
+        "DISCONNECTING": "Rozłączanie",
322
+        "ATTACHED": "Załącznik"
323
+    },
324
+    "recording": {
325
+        "pending": "Nagrywanie oczekiwanie na uczestników konferencji.....",
326
+        "on": "Nagrywanie",
327
+        "off": "Nagrywanie zatrzymane",
328
+        "failedToStart": "Nagrywanie nie jest możliwe",
329
+        "buttonTooltip": "Nagrywanie start / stop",
330
+        "error": "Nagranie się nie powiodło. Proszę, spróbuj ponownie.",
331
+        "unavailable": "Serwis nagrywania jest aktualnie niedostępny. Proszę, spróbować później."
332
+    },
333
+    "liveStreaming": {
334
+        "pending": "Start strumieniowania live...",
335
+        "on": "Strumień live",
336
+        "off": "Strumieniowanie live zastopowane",
337
+        "unavailable": "Strumieniowanie live aktualnie jest niedostepne. Proszę spróbować później.",
338
+        "failedToStart": "Strumieniowanie live nie powiodło się",
339
+        "buttonTooltip": "Strumieniowanie live start / stop",
340
+        "streamIdRequired": "Proszę podaj id strumieniowania aby uruchomić live.",
341
+        "error": "Strumieniowanie live nie powiodło się. Spróbuj później.",
342
+        "busy": "Wszystkie nagrywarki są zajęte. Proszę, sprawdź ponownie później."
343
+    }
344
+}

+ 43
- 26
lang/main-ptBR.json 查看文件

1
 {
1
 {
2
-    "contactlist": "LISTA DE CONTATO",
2
+    "contactlist": "Na chamada",
3
     "connectionsettings": "Configurações de conexão",
3
     "connectionsettings": "Configurações de conexão",
4
     "poweredby": "distribuído por",
4
     "poweredby": "distribuído por",
5
-    "downloadlogs": "Baixar registros",
6
     "feedback": "Dê seus comentários",
5
     "feedback": "Dê seus comentários",
7
     "roomUrlDefaultMsg": "Sua conferência está sendo criado...",
6
     "roomUrlDefaultMsg": "Sua conferência está sendo criado...",
8
-    "participant": "Participante",
9
     "me": "eu",
7
     "me": "eu",
10
     "speaker": "Orador",
8
     "speaker": "Orador",
11
     "raisedHand": "Gostaria de falar",
9
     "raisedHand": "Gostaria de falar",
12
     "defaultNickname": "ex. João Pedro",
10
     "defaultNickname": "ex. João Pedro",
13
     "defaultLink": "i.e. __url__",
11
     "defaultLink": "i.e. __url__",
14
-    "calling": "Chamando __name__ ...",
12
+    "callingName": "__name__",
15
     "userMedia": {
13
     "userMedia": {
16
         "react-nativeGrantPermissions": "Dê as permissões para usar sua câmera e microfone pressionando o botão <b> <i>Permitir</i> </b>",
14
         "react-nativeGrantPermissions": "Dê as permissões para usar sua câmera e microfone pressionando o botão <b> <i>Permitir</i> </b>",
17
         "chromeGrantPermissions": "Dê as permissões para usar sua câmera e microfone pressionando o botão <b> <i>Permitir</i> </b>",
15
         "chromeGrantPermissions": "Dê as permissões para usar sua câmera e microfone pressionando o botão <b> <i>Permitir</i> </b>",
27
         "raiseHand": "Erguer sua mão.",
25
         "raiseHand": "Erguer sua mão.",
28
         "pushToTalk": "Pressione para falar.",
26
         "pushToTalk": "Pressione para falar.",
29
         "toggleScreensharing": "Trocar entre câmera e compartilhamento de tela.",
27
         "toggleScreensharing": "Trocar entre câmera e compartilhamento de tela.",
30
-        "toggleFilmstrip": "Mostrar ou ocultar a tira de filme.",
28
+        "toggleFilmstrip": "Mostrar ou ocultar os vídeos.",
31
         "toggleShortcuts": "Mostrar ou ocultar este menu de ajuda.",
29
         "toggleShortcuts": "Mostrar ou ocultar este menu de ajuda.",
32
         "focusLocal": "Foco no vídeo local.",
30
         "focusLocal": "Foco no vídeo local.",
33
         "focusRemote": "Foco em um dos vídeos remotos.",
31
         "focusRemote": "Foco em um dos vídeos remotos.",
93
         "micMutedPopup": "Seu microfone está mudo assim que você<br/>pode curtir plenamente seu vídeo compartilhado.",
91
         "micMutedPopup": "Seu microfone está mudo assim que você<br/>pode curtir plenamente seu vídeo compartilhado.",
94
         "unableToUnmutePopup": "Você não pode sair do mudo enquanto seu vídeo compartilhado está ativo.",
92
         "unableToUnmutePopup": "Você não pode sair do mudo enquanto seu vídeo compartilhado está ativo.",
95
         "cameraDisabled": "A câmera não está disponível",
93
         "cameraDisabled": "A câmera não está disponível",
96
-        "micDisabled": "O microfone não está disponível"
94
+        "micDisabled": "O microfone não está disponível",
95
+        "filmstrip": "",
96
+        "raiseHand": "Levantar a mão para falar"
97
     },
97
     },
98
     "bottomtoolbar": {
98
     "bottomtoolbar": {
99
         "chat": "Abrir / fechar bate-papo",
99
         "chat": "Abrir / fechar bate-papo",
100
-        "filmstrip": "Mostrar / ocultar a tira de usuários",
100
+        "filmstrip": "Mostrar/ocultar vídeos",
101
         "contactlist": "Abrir / fechar a lista de contatos"
101
         "contactlist": "Abrir / fechar a lista de contatos"
102
     },
102
     },
103
     "chat": {
103
     "chat": {
108
         "messagebox": "Digite um texto..."
108
         "messagebox": "Digite um texto..."
109
     },
109
     },
110
     "settings": {
110
     "settings": {
111
-        "title": "CONFIGURAÇÕES",
111
+        "title": "Configurações",
112
         "update": "Atualizar",
112
         "update": "Atualizar",
113
         "name": "Nome",
113
         "name": "Nome",
114
-        "startAudioMuted": "Iniciar sem áudio",
115
-        "startVideoMuted": "Iniciar sem vídeo",
116
-        "selectCamera": "Selecione a câmera",
117
-        "selectMic": "Selecionar o microfone",
118
-        "selectAudioOutput": "Selecionar a saída de áudio",
119
-        "followMe": "Habilitar o siga-me",
114
+        "startAudioMuted": "Todos iniciam mudos",
115
+        "startVideoMuted": "Todos iniciam ocultos",
116
+        "selectCamera": "Câmera",
117
+        "selectMic": "Microfone",
118
+        "selectAudioOutput": "Saída de áudio",
119
+        "followMe": "Todos me seguem",
120
         "noDevice": "Nenhum",
120
         "noDevice": "Nenhum",
121
         "noPermission": "Permissão para usar o dispositivo não concedida",
121
         "noPermission": "Permissão para usar o dispositivo não concedida",
122
-        "avatarUrl": "URL do Avatar"
122
+        "cameraAndMic": "Câmera e microfone",
123
+        "moderator": "MODERADOR",
124
+        "password": "DEFINIR SENHA",
125
+        "audioVideo": "ÁUDIO E VÍDEO",
126
+        "setPasswordLabel": "Trancar sua sala com uma senha."
127
+    },
128
+    "profile": {
129
+        "title": "PERFIL",
130
+        "setDisplayNameLabel": "Definir seu nome de exibição",
131
+        "setEmailLabel": "Definir seu email de gravatar"
123
     },
132
     },
124
     "videothumbnail": {
133
     "videothumbnail": {
125
         "editnickname": "Clique para editar o seu <br/>nome de exibição",
134
         "editnickname": "Clique para editar o seu <br/>nome de exibição",
172
         "connectError": "Oops! Alguma coisa está errada e nós não pudemos conectar à conferência.",
181
         "connectError": "Oops! Alguma coisa está errada e nós não pudemos conectar à conferência.",
173
         "connectErrorWithMsg": "Oops! Alguma coisa está errada e não podemos conectar à conferência: __msg__",
182
         "connectErrorWithMsg": "Oops! Alguma coisa está errada e não podemos conectar à conferência: __msg__",
174
         "connecting": "Conectando",
183
         "connecting": "Conectando",
184
+        "copy": "Copiar",
175
         "error": "Erro",
185
         "error": "Erro",
176
         "detectext": "Erro enquanto tenta detectar a extensão de compartilhamento de tela.",
186
         "detectext": "Erro enquanto tenta detectar a extensão de compartilhamento de tela.",
177
         "failtoinstall": "Falhou a instalação da extensão de compartilhamento de tela",
187
         "failtoinstall": "Falhou a instalação da extensão de compartilhamento de tela",
183
         "lockMessage": "Falha ao travar a conferência.",
193
         "lockMessage": "Falha ao travar a conferência.",
184
         "warning": "Atenção",
194
         "warning": "Atenção",
185
         "passwordNotSupported": "Senhas de salas não são suportadas atualmente.",
195
         "passwordNotSupported": "Senhas de salas não são suportadas atualmente.",
186
-        "sorry": "Desculpe",
187
-        "internalError": "Erro interno de aplicação [setRemoteDescription]",
196
+        "internalErrorTitle": "Erro interno",
197
+        "internalError": "Ops! Alguma coisa está errada. Ocorreu o seguinte erro: [setRemoteDescriptio]",
188
         "unableToSwitch": "Impossível trocar o fluxo de vídeo.",
198
         "unableToSwitch": "Impossível trocar o fluxo de vídeo.",
189
         "SLDFailure": "Oops! Alguma coisa está errada e nós falhamos em silenciar! (Falha do SLD)",
199
         "SLDFailure": "Oops! Alguma coisa está errada e nós falhamos em silenciar! (Falha do SLD)",
190
         "SRDFailure": "Oops! Alguma coisa está errada e nós falhamos em parar o vídeo! (Falha do SRD)",
200
         "SRDFailure": "Oops! Alguma coisa está errada e nós falhamos em parar o vídeo! (Falha do SRD)",
216
         "sipMsg": "Digite o número SIP",
226
         "sipMsg": "Digite o número SIP",
217
         "passwordCheck": "Você tem certeza que deseja remover sua senha?",
227
         "passwordCheck": "Você tem certeza que deseja remover sua senha?",
218
         "passwordMsg": "Definir uma senha para trancar sua sala",
228
         "passwordMsg": "Definir uma senha para trancar sua sala",
219
-        "Invite": "Convidar",
220
-        "shareLink": "Compartilhar este link com quem você espera convidar",
229
+        "shareLink": "Copiar e compartilhar este link",
221
         "settings1": "Configure sua conferência",
230
         "settings1": "Configure sua conferência",
222
         "settings2": "Participantes entram mudos",
231
         "settings2": "Participantes entram mudos",
223
         "settings3": "Requer apelidos<br/><br/>Defina uma senha para trancar sua sala:",
232
         "settings3": "Requer apelidos<br/><br/>Defina uma senha para trancar sua sala:",
224
-        "yourPassword": "sua Senha",
233
+        "yourPassword": "Digite a nova senha",
225
         "Back": "Voltar",
234
         "Back": "Voltar",
226
         "serviceUnavailable": "Serviço indisponível",
235
         "serviceUnavailable": "Serviço indisponível",
227
         "gracefulShutdown": "Nosso serviço está desligado para manutenção. Por favor, tente mais tarde.",
236
         "gracefulShutdown": "Nosso serviço está desligado para manutenção. Por favor, tente mais tarde.",
228
         "Yes": "Sim",
237
         "Yes": "Sim",
229
         "reservationError": "Erro de sistema de reserva",
238
         "reservationError": "Erro de sistema de reserva",
230
         "reservationErrorMsg": "Código do erro: __code__, mensagem: __msg__",
239
         "reservationErrorMsg": "Código do erro: __code__, mensagem: __msg__",
231
-        "password": "senha",
240
+        "password": "Insira a senha",
232
         "userPassword": "senha do usuário",
241
         "userPassword": "senha do usuário",
233
         "token": "token",
242
         "token": "token",
234
-        "tokenAuthFailed": "Falha em autenticar com o servidor XMPP: token inválido",
243
+        "tokenAuthFailedTitle": "Problema na autenticação",
244
+        "tokenAuthFailed": "Desculpe, você não está autorizado a entrar nesta chamada.",
235
         "displayNameRequired": "Digite seu nome de exibição",
245
         "displayNameRequired": "Digite seu nome de exibição",
236
         "extensionRequired": "Extensão requerida:",
246
         "extensionRequired": "Extensão requerida:",
237
         "firefoxExtensionPrompt": "Você precisa instalar uma extensão do Firefox para compartilhar a tela. Tente novamente depois que você <a href='__url__'>pegá-lo aqui</a>!",
247
         "firefoxExtensionPrompt": "Você precisa instalar uma extensão do Firefox para compartilhar a tela. Tente novamente depois que você <a href='__url__'>pegá-lo aqui</a>!",
238
-        "feedbackQuestion": "Como foi a chamada?",
248
+        "rateExperience": "Por favor, avalie sua experiência na reunião.",
249
+        "feedbackHelp": "Seu retorno nos ajudará a melhorar nossa experiência de vídeo.",
250
+        "feedbackQuestion": "Nos conte sobre sua chamada!",
239
         "thankYou": "Obrigado por usar o __appName__!",
251
         "thankYou": "Obrigado por usar o __appName__!",
240
         "sorryFeedback": "Lamentamos escutar isso. Gostaria de nos contar mais?",
252
         "sorryFeedback": "Lamentamos escutar isso. Gostaria de nos contar mais?",
241
         "liveStreaming": "Live Streaming",
253
         "liveStreaming": "Live Streaming",
253
         "cameraUnsupportedResolutionError": "Sua câmera não suporta a resolução de vídeo requerida.",
265
         "cameraUnsupportedResolutionError": "Sua câmera não suporta a resolução de vídeo requerida.",
254
         "cameraUnknownError": "Não pode usar a câmera por uma razão desconhecida.",
266
         "cameraUnknownError": "Não pode usar a câmera por uma razão desconhecida.",
255
         "cameraPermissionDeniedError": "Você não tem permissão para usar sua câmera. Você ainda pode entrar na conferência, mas os outros não verão você. Use o botão da câmera na barra de endereço para fixar isto.",
267
         "cameraPermissionDeniedError": "Você não tem permissão para usar sua câmera. Você ainda pode entrar na conferência, mas os outros não verão você. Use o botão da câmera na barra de endereço para fixar isto.",
256
-        "cameraNotFoundError": "Câmera solicitada não foi encontrada.",
268
+        "cameraNotFoundError": "A câmera não foi encontrada.",
257
         "cameraConstraintFailedError": "Sua câmera não satisfaz algumas condições requeridas.",
269
         "cameraConstraintFailedError": "Sua câmera não satisfaz algumas condições requeridas.",
258
         "micUnknownError": "Não pode usar o microfone por uma razão desconhecida.",
270
         "micUnknownError": "Não pode usar o microfone por uma razão desconhecida.",
259
         "micPermissionDeniedError": "Você não tem permissão para usar seu microfone. Você ainda pode entrar na conferência, mas os outros não ouvirão você. Use o botão da câmera na barra de endereço para fixar isto.",
271
         "micPermissionDeniedError": "Você não tem permissão para usar seu microfone. Você ainda pode entrar na conferência, mas os outros não ouvirão você. Use o botão da câmera na barra de endereço para fixar isto.",
260
-        "micNotFoundError": "O microfone solicitado não foi encontrado.",
261
-        "micConstraintFailedError": "Seu microfone não satisfaz algumas condições requeridas."
272
+        "micNotFoundError": "O microfone não foi encontrado.",
273
+        "micConstraintFailedError": "Seu microfone não satisfaz algumas condições requeridas.",
274
+        "micNotSendingData": "Seu microfone está inacessível. Selecione outro dispositivo do menu de configurações ou tente reiniciar a aplicação.",
275
+        "cameraNotSendingData": "Sua câmera está inacessível. Verifique se outra aplicação está usando este dispositivo, selecione outro dispositivo do menu de configurações ou tente reiniciar a aplicação.",
276
+        "goToStore": "Vá para a loja virtual",
277
+        "externalInstallationTitle": "Extensão requerida",
278
+        "externalInstallationMsg": "Você precisa instalar nossa extensão de compartilhamento de tela."
262
     },
279
     },
263
     "email": {
280
     "email": {
264
         "sharedKey": [
281
         "sharedKey": [

+ 17
- 15
lang/main.json 查看文件

1
 {
1
 {
2
-    "contactlist": "ON CALL (__participants__)",
2
+    "contactlist": "On Call",
3
     "connectionsettings": "Connection Settings",
3
     "connectionsettings": "Connection Settings",
4
     "poweredby": "powered by",
4
     "poweredby": "powered by",
5
-    "downloadlogs": "Download logs",
6
     "feedback": "Give us your feedback",
5
     "feedback": "Give us your feedback",
7
     "roomUrlDefaultMsg": "Your conference is currently being created...",
6
     "roomUrlDefaultMsg": "Your conference is currently being created...",
8
-    "participant": "Participant",
9
     "me": "me",
7
     "me": "me",
10
     "speaker": "Speaker",
8
     "speaker": "Speaker",
11
     "raisedHand": "Would like to speak",
9
     "raisedHand": "Would like to speak",
33
         "focusRemote": "Focus on one of the remote videos.",
31
         "focusRemote": "Focus on one of the remote videos.",
34
         "toggleChat": "Open or close the chat panel.",
32
         "toggleChat": "Open or close the chat panel.",
35
         "mute": "Mute or unmute the microphone.",
33
         "mute": "Mute or unmute the microphone.",
34
+        "fullScreen": "Enter or exit full screen mode.",
36
         "videoMute": "Stop or start the local video."
35
         "videoMute": "Stop or start the local video."
37
     },
36
     },
38
     "welcomepage":{
37
     "welcomepage":{
112
     },
111
     },
113
     "settings":
112
     "settings":
114
     {
113
     {
115
-        "title": "SETTINGS",
114
+        "title": "Settings",
116
         "update": "Update",
115
         "update": "Update",
117
         "name": "Name",
116
         "name": "Name",
118
         "startAudioMuted": "Everyone starts muted",
117
         "startAudioMuted": "Everyone starts muted",
119
         "startVideoMuted": "Everyone starts hidden",
118
         "startVideoMuted": "Everyone starts hidden",
120
-        "selectCamera": "Select camera",
121
-        "selectMic": "Select microphone",
122
-        "selectAudioOutput": "Select audio output",
119
+        "selectCamera": "Camera",
120
+        "selectMic": "Microphone",
121
+        "selectAudioOutput": "Audio output",
123
         "followMe": "Everyone follows me",
122
         "followMe": "Everyone follows me",
124
         "noDevice": "None",
123
         "noDevice": "None",
125
         "noPermission": "Permission to use device is not granted",
124
         "noPermission": "Permission to use device is not granted",
126
         "cameraAndMic": "Camera and microphone",
125
         "cameraAndMic": "Camera and microphone",
127
         "moderator": "MODERATOR",
126
         "moderator": "MODERATOR",
128
         "password": "SET PASSWORD",
127
         "password": "SET PASSWORD",
129
-        "audioVideo": "AUDIO / VIDEO",
128
+        "audioVideo": "AUDIO AND VIDEO",
130
         "setPasswordLabel": "Lock your room with a password."
129
         "setPasswordLabel": "Lock your room with a password."
131
     },
130
     },
132
     "profile": {
131
     "profile": {
138
     {
137
     {
139
         "editnickname": "Click to edit your<br/>display name",
138
         "editnickname": "Click to edit your<br/>display name",
140
         "moderator": "The owner of<br/>this conference",
139
         "moderator": "The owner of<br/>this conference",
141
-        "videomute": "Participant has<br/>stopped the camera.",
140
+        "videomute": "Participant has<br/>stopped the camera",
142
         "mute": "Participant is muted",
141
         "mute": "Participant is muted",
143
         "kick": "Kick out",
142
         "kick": "Kick out",
144
         "muted": "Muted",
143
         "muted": "Muted",
188
         "connectError": "Oops! Something went wrong and we couldn't connect to the conference.",
187
         "connectError": "Oops! Something went wrong and we couldn't connect to the conference.",
189
         "connectErrorWithMsg": "Oops! Something went wrong and we couldn't connect to the conference: __msg__",
188
         "connectErrorWithMsg": "Oops! Something went wrong and we couldn't connect to the conference: __msg__",
190
         "connecting": "Connecting",
189
         "connecting": "Connecting",
190
+        "copy": "Copy",
191
         "error": "Error",
191
         "error": "Error",
192
         "detectext": "Error when trying to detect desktopsharing extension.",
192
         "detectext": "Error when trying to detect desktopsharing extension.",
193
         "failtoinstall": "Failed to install desktop sharing extension",
193
         "failtoinstall": "Failed to install desktop sharing extension",
199
         "lockMessage": "Failed to lock the conference.",
199
         "lockMessage": "Failed to lock the conference.",
200
         "warning": "Warning",
200
         "warning": "Warning",
201
         "passwordNotSupported": "Room passwords are currently not supported.",
201
         "passwordNotSupported": "Room passwords are currently not supported.",
202
-        "sorry": "Sorry",
203
-        "internalError": "Internal application error [setRemoteDescription]",
202
+        "internalErrorTitle": "Internal error",
203
+        "internalError": "Oups! Something went wrong. The following error occurred: [setRemoteDescription]",
204
         "unableToSwitch": "Unable to switch video stream.",
204
         "unableToSwitch": "Unable to switch video stream.",
205
         "SLDFailure": "Oops! Something went wrong and we failed to mute! (SLD Failure)",
205
         "SLDFailure": "Oops! Something went wrong and we failed to mute! (SLD Failure)",
206
         "SRDFailure": "Oops! Something went wrong and we failed to stop video! (SRD Failure)",
206
         "SRDFailure": "Oops! Something went wrong and we failed to stop video! (SRD Failure)",
233
         "passwordCheck": "Are you sure you would like to remove your password?",
233
         "passwordCheck": "Are you sure you would like to remove your password?",
234
         "Remove": "Remove",
234
         "Remove": "Remove",
235
         "passwordMsg": "Set a password to lock your room",
235
         "passwordMsg": "Set a password to lock your room",
236
-        "Invite": "Invite",
237
         "shareLink": "Copy and share this link",
236
         "shareLink": "Copy and share this link",
238
         "settings1": "Configure your conference",
237
         "settings1": "Configure your conference",
239
         "settings2": "Participants join muted",
238
         "settings2": "Participants join muted",
248
         "password": "Enter password",
247
         "password": "Enter password",
249
         "userPassword": "user password",
248
         "userPassword": "user password",
250
         "token": "token",
249
         "token": "token",
251
-        "tokenAuthFailed": "Failed to authenticate with XMPP server: invalid token",
250
+        "tokenAuthFailedTitle": "Authentication problem",
251
+        "tokenAuthFailed": "Sorry, you're not allowed to join this call.",
252
         "displayNameRequired": "Please enter your display name",
252
         "displayNameRequired": "Please enter your display name",
253
         "extensionRequired": "Extension required:",
253
         "extensionRequired": "Extension required:",
254
         "firefoxExtensionPrompt": "You need to install a Firefox extension in order to use screen sharing. Please try again after you <a href='__url__'>get it from here</a>!",
254
         "firefoxExtensionPrompt": "You need to install a Firefox extension in order to use screen sharing. Please try again after you <a href='__url__'>get it from here</a>!",
255
         "rateExperience": "Please rate your meeting experience.",
255
         "rateExperience": "Please rate your meeting experience.",
256
         "feedbackHelp": "Your feedback will help us to improve our video experience.",
256
         "feedbackHelp": "Your feedback will help us to improve our video experience.",
257
-        "feedbackQuestion": "How was your call?",
257
+        "feedbackQuestion": "Tell us about your call!",
258
         "thankYou": "Thank you for using __appName__!",
258
         "thankYou": "Thank you for using __appName__!",
259
         "sorryFeedback": "We're sorry to hear that. Would you like to tell us more?",
259
         "sorryFeedback": "We're sorry to hear that. Would you like to tell us more?",
260
         "liveStreaming": "Live Streaming",
260
         "liveStreaming": "Live Streaming",
279
         "micNotFoundError": "Microphone was not found.",
279
         "micNotFoundError": "Microphone was not found.",
280
         "micConstraintFailedError": "Yor microphone does not satisfy some of required constraints.",
280
         "micConstraintFailedError": "Yor microphone does not satisfy some of required constraints.",
281
         "micNotSendingData": "We are unable to access your microphone. Please select another device from the settings menu or try to restart the application.",
281
         "micNotSendingData": "We are unable to access your microphone. Please select another device from the settings menu or try to restart the application.",
282
+        "cameraNotSendingData": "We are unable to access your camera. Please check if another application is using this device, select another device from the settings menu or try to restart the application.",
282
         "goToStore": "Go to the webstore",
283
         "goToStore": "Go to the webstore",
283
         "externalInstallationTitle": "Extension required",
284
         "externalInstallationTitle": "Extension required",
284
         "externalInstallationMsg": "You need to install our desktop sharing extension."
285
         "externalInstallationMsg": "You need to install our desktop sharing extension."
325
         "ATTACHED": "Attached",
326
         "ATTACHED": "Attached",
326
         "FETCH_SESSION_ID": "Obtaining session-id...",
327
         "FETCH_SESSION_ID": "Obtaining session-id...",
327
         "GOT_SESSION_ID": "Obtaining session-id... Done",
328
         "GOT_SESSION_ID": "Obtaining session-id... Done",
328
-        "GET_SESSION_ID_ERROR": "Get session-id error: "
329
+        "GET_SESSION_ID_ERROR": "Get session-id error: ",
330
+        "USER_CONNECTION_INTERRUPTED": "__displayName__ is having connectivity issues..."
329
     },
331
     },
330
     "recording":
332
     "recording":
331
     {
333
     {

+ 1
- 0
modules/TokenData/TokenData.js 查看文件

97
         this.payload = this.decodedJWT.payload;
97
         this.payload = this.decodedJWT.payload;
98
         if(!this.payload.context)
98
         if(!this.payload.context)
99
             return;
99
             return;
100
+        this.server = this.payload.context.server;
100
         let callerData = this.payload.context.user;
101
         let callerData = this.payload.context.user;
101
         let calleeData = this.payload.context.callee;
102
         let calleeData = this.payload.context.callee;
102
         if(callerData)
103
         if(callerData)

+ 0
- 321
modules/UI/Feedback.js 查看文件

1
-/* global $, APP, config, interfaceConfig, JitsiMeetJS */
2
-import UIEvents from "../../service/UI/UIEvents";
3
-
4
-/**
5
- * Constructs the html for the overall feedback window.
6
- *
7
- * @returns {string} the constructed html string
8
- */
9
-var constructOverallFeedbackHtml = function() {
10
-    var feedbackQuestion = (Feedback.feedbackScore < 0)
11
-        ? '<br/><br/>' + APP.translation
12
-        .translateString("dialog.feedbackQuestion")
13
-        : '';
14
-
15
-    var message = '<div class="feedback"><div>' +
16
-        '<div class="feedbackTitle">' +
17
-        APP.translation.translateString("dialog.thankYou",
18
-                                        {appName:interfaceConfig.APP_NAME}) +
19
-        '</div>' +
20
-        feedbackQuestion +
21
-        '</div><br/><br/>' +
22
-        '<div id="stars">' +
23
-        '<a><i class="icon-star icon-star-full"></i></a>' +
24
-        '<a><i class="icon-star icon-star-full"></i></a>' +
25
-        '<a><i class="icon-star icon-star-full"></i></a>' +
26
-        '<a><i class="icon-star icon-star-full"></i></a>' +
27
-        '<a><i class="icon-star icon-star-full"></i></a>' +
28
-        '</div></div>';
29
-
30
-    return message;
31
-};
32
-
33
-/**
34
- * Constructs the html for the detailed feedback window.
35
- *
36
- * @returns {string} the contructed html string
37
- */
38
-var constructDetailedFeedbackHtml = function() {
39
-    // Construct the html, which will be served as a dialog message.
40
-    var message = '<div class="feedback">' +
41
-        '<div class="feedbackTitle">' +
42
-        APP.translation.translateString("dialog.sorryFeedback") +
43
-        '</div><br/><br/>' +
44
-        '<div class="feedbackDetails">' +
45
-        '<textarea id="feedbackTextArea" rows="10" cols="50" autofocus>' +
46
-        '</textarea>' +
47
-        '</div></div>';
48
-
49
-    return message;
50
-};
51
-
52
-var createRateFeedbackHTML = function () {
53
-    var rate = APP.translation.translateString('dialog.rateExperience'),
54
-        help = APP.translation.translateString('dialog.feedbackHelp');
55
-
56
-    return `
57
-        <div class="feedback-rating text-center">
58
-            <h2>${ rate }</h2>
59
-            <p class="star-label">&nbsp;</p>
60
-            <div id="stars" class="feedback-stars">
61
-                <a class="star-btn">
62
-                    <i class="fa fa-star shake-rotate"></i>
63
-                </a>
64
-                <a class="star-btn">
65
-                    <i class="fa fa-star shake-rotate"></i>
66
-                </a>
67
-                <a class="star-btn">
68
-                    <i class="fa fa-star shake-rotate"></i>
69
-                </a>
70
-                <a class="star-btn">
71
-                    <i class="fa fa-star shake-rotate"></i>
72
-                </a>
73
-                <a class="star-btn">
74
-                    <i class="fa fa-star shake-rotate"></i>
75
-                </a>
76
-            </div>
77
-            <p>&nbsp;</p>
78
-            <p>${ help }</p>
79
-        </div>
80
-    `;
81
-};
82
-
83
-/**
84
- * The callback function corresponding to the openFeedbackWindow parameter.
85
- *
86
- * @type {function}
87
- */
88
-var feedbackWindowCallback = null;
89
-
90
-/**
91
- * Shows / hides the feedback button.
92
- * @private
93
- */
94
-function _toggleFeedbackIcon() {
95
-    $('#feedbackButtonDiv').toggleClass("hidden");
96
-}
97
-
98
-/**
99
- * Shows / hides the feedback button.
100
- * @param {show} set to {true} to show the feedback button or to  {false}
101
- * to hide it
102
- * @private
103
- */
104
-function _showFeedbackButton (show) {
105
-    var feedbackButton = $("#feedbackButtonDiv");
106
-
107
-    if (show)
108
-        feedbackButton.css("display", "block");
109
-    else
110
-        feedbackButton.css("display", "none");
111
-}
112
-
113
-/**
114
- * Defines all methods in connection to the Feedback window.
115
- *
116
- * @type {{feedbackScore: number, openFeedbackWindow: Function,
117
- * toggleStars: Function, hoverStars: Function, unhoverStars: Function}}
118
- */
119
-var Feedback = {
120
-    /**
121
-     * The feedback score. -1 indicates no score has been given for now.
122
-     */
123
-    feedbackScore: -1,
124
-
125
-    /**
126
-     * Initialise the Feedback functionality.
127
-     * @param emitter the EventEmitter to associate with the Feedback.
128
-     */
129
-    init: function (emitter) {
130
-        // CallStats is the way we send feedback, so we don't have to initialise
131
-        // if callstats isn't enabled.
132
-        if (!APP.conference.isCallstatsEnabled())
133
-            return;
134
-
135
-        // If enabled property is still undefined, i.e. it hasn't been set from
136
-        // some other module already, we set it to true by default.
137
-        if (typeof this.enabled == "undefined")
138
-            this.enabled = true;
139
-
140
-        _showFeedbackButton(this.enabled);
141
-
142
-        $("#feedbackButton").click(function (event) {
143
-            Feedback.openFeedbackWindow();
144
-        });
145
-
146
-        // Show / hide the feedback button whenever the film strip is
147
-        // shown / hidden.
148
-        emitter.addListener(UIEvents.TOGGLE_FILM_STRIP, function () {
149
-            _toggleFeedbackIcon();
150
-        });
151
-    },
152
-    /**
153
-     * Enables/ disabled the feedback feature.
154
-     */
155
-    enableFeedback: function (enable) {
156
-        if (this.enabled !== enable)
157
-            _showFeedbackButton(enable);
158
-        this.enabled = enable;
159
-    },
160
-
161
-    /**
162
-     * Indicates if the feedback functionality is enabled.
163
-     *
164
-     * @return true if the feedback functionality is enabled, false otherwise.
165
-     */
166
-    isEnabled: function() {
167
-        return this.enabled && APP.conference.isCallstatsEnabled();
168
-    },
169
-
170
-    /**
171
-     * Returns true if the feedback window is currently visible and false
172
-     * otherwise.
173
-     * @return {boolean} true if the feedback window is visible, false
174
-     * otherwise
175
-     */
176
-    isVisible: function() {
177
-        return $(".feedback").is(":visible");
178
-    },
179
-
180
-    /**
181
-     * Opens the feedback window.
182
-     */
183
-    openFeedbackWindow: function (callback) {
184
-        feedbackWindowCallback = callback;
185
-        // Add all mouse and click listeners.
186
-        var onLoadFunction = function (event) {
187
-            $('#stars >a').each(function(index) {
188
-                // On star mouse over.
189
-                $(this).get(0).onmouseover = function(){
190
-                    Feedback.hoverStars(index);
191
-                };
192
-                // On star mouse leave.
193
-                $(this).get(0).onmouseleave = function(){
194
-                    Feedback.unhoverStars(index);
195
-                };
196
-                // On star click.
197
-                $(this).get(0).onclick = function(){
198
-                    Feedback.toggleStars(index);
199
-                    Feedback.feedbackScore = index+1;
200
-
201
-                    // If the feedback is less than 3 stars we're going to
202
-                    // ask the user for more information.
203
-                    if (Feedback.feedbackScore > 3) {
204
-                        APP.conference.sendFeedback(Feedback.feedbackScore, "");
205
-                        if (feedbackWindowCallback)
206
-                            feedbackWindowCallback();
207
-                        else
208
-                            APP.UI.messageHandler.closeDialog();
209
-                    }
210
-                    else {
211
-                        feedbackDialog.goToState('detailed_feedback');
212
-                    }
213
-                };
214
-                // Init stars to correspond to previously entered feedback.
215
-                if (Feedback.feedbackScore > 0
216
-                    && index < Feedback.feedbackScore) {
217
-                    Feedback.hoverStars(index);
218
-                    Feedback.toggleStars(index);
219
-                }
220
-            });
221
-        };
222
-
223
-        // Defines the different states of the feedback window.
224
-        var states = {
225
-            overall_feedback: {
226
-                html: createRateFeedbackHTML(),
227
-                persistent: false,
228
-                buttons: {},
229
-                closeText: '',
230
-                focus: "div[id='stars']",
231
-                position: {width: 500}
232
-            },
233
-            detailed_feedback: {
234
-                html: constructDetailedFeedbackHtml(),
235
-                buttons: {"Submit": true, "Cancel": false},
236
-                closeText: '',
237
-                focus: "textarea[id='feedbackTextArea']",
238
-                position: {width: 500},
239
-                submit: function(e,v,m,f) {
240
-                    e.preventDefault();
241
-                    if (v) {
242
-                        var feedbackDetails
243
-                            = document.getElementById("feedbackTextArea").value;
244
-
245
-                        if (feedbackDetails && feedbackDetails.length > 0) {
246
-                            APP.conference.sendFeedback( Feedback.feedbackScore,
247
-                                                    feedbackDetails);
248
-                        }
249
-
250
-                        if (feedbackWindowCallback)
251
-                            feedbackWindowCallback();
252
-                        else
253
-                            APP.UI.messageHandler.closeDialog();
254
-                    } else {
255
-                        // User cancelled
256
-                        if (feedbackWindowCallback)
257
-                            feedbackWindowCallback();
258
-                        else
259
-                            APP.UI.messageHandler.closeDialog();
260
-                    }
261
-                }
262
-            }
263
-        };
264
-
265
-        // Create the feedback dialog.
266
-        var feedbackDialog
267
-            = APP.UI.messageHandler.openDialogWithStates(
268
-                states,
269
-                {   persistent: false,
270
-                    buttons: {},
271
-                    closeText: '',
272
-                    loaded: onLoadFunction,
273
-                    position: {width: 500}}, null);
274
-        JitsiMeetJS.analytics.sendEvent('feedback.open');
275
-    },
276
-    /**
277
-     * Toggles the appropriate css class for the given number of stars, to
278
-     * indicate that those stars have been clicked/selected.
279
-     *
280
-     * @param starCount the number of stars, for which to toggle the css class
281
-     */
282
-    toggleStars: function (starCount)
283
-    {
284
-        $('#stars >a >i').each(function(index) {
285
-            if (index <= starCount) {
286
-                $(this).removeClass("icon-star");
287
-            }
288
-            else
289
-                $(this).addClass("icon-star");
290
-        });
291
-    },
292
-    /**
293
-     * Toggles the appropriate css class for the given number of stars, to
294
-     * indicate that those stars have been hovered.
295
-     *
296
-     * @param starCount the number of stars, for which to toggle the css class
297
-     */
298
-    hoverStars: function (starCount)
299
-    {
300
-        $('#stars >a >i').each(function(index) {
301
-            if (index <= starCount)
302
-                $(this).addClass("starHover");
303
-        });
304
-    },
305
-    /**
306
-     * Toggles the appropriate css class for the given number of stars, to
307
-     * indicate that those stars have been un-hovered.
308
-     *
309
-     * @param starCount the number of stars, for which to toggle the css class
310
-     */
311
-    unhoverStars: function (starCount)
312
-    {
313
-        $('#stars >a >i').each(function(index) {
314
-            if (index <= starCount && $(this).hasClass("icon-star"))
315
-                $(this).removeClass("starHover");
316
-        });
317
-    }
318
-};
319
-
320
-// Exports the Feedback class.
321
-module.exports = Feedback;

+ 88
- 79
modules/UI/UI.js 查看文件

29
 UI.messageHandler = require("./util/MessageHandler");
29
 UI.messageHandler = require("./util/MessageHandler");
30
 var messageHandler = UI.messageHandler;
30
 var messageHandler = UI.messageHandler;
31
 var JitsiPopover = require("./util/JitsiPopover");
31
 var JitsiPopover = require("./util/JitsiPopover");
32
-var Feedback = require("./Feedback");
32
+var Feedback = require("./feedback/Feedback");
33
 
33
 
34
 import FollowMe from "../FollowMe";
34
 import FollowMe from "../FollowMe";
35
 
35
 
60
     = "dialog.cameraNotFoundError";
60
     = "dialog.cameraNotFoundError";
61
 JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[TrackErrors.CONSTRAINT_FAILED]
61
 JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[TrackErrors.CONSTRAINT_FAILED]
62
     = "dialog.cameraConstraintFailedError";
62
     = "dialog.cameraConstraintFailedError";
63
+JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[TrackErrors.NO_DATA_FROM_SOURCE]
64
+    = "dialog.cameraNotSendingData";
63
 JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[TrackErrors.GENERAL]
65
 JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[TrackErrors.GENERAL]
64
     = "dialog.micUnknownError";
66
     = "dialog.micUnknownError";
65
 JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[TrackErrors.PERMISSION_DENIED]
67
 JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[TrackErrors.PERMISSION_DENIED]
68
     = "dialog.micNotFoundError";
70
     = "dialog.micNotFoundError";
69
 JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[TrackErrors.CONSTRAINT_FAILED]
71
 JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[TrackErrors.CONSTRAINT_FAILED]
70
     = "dialog.micConstraintFailedError";
72
     = "dialog.micConstraintFailedError";
73
+JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[TrackErrors.NO_DATA_FROM_SOURCE]
74
+    = "dialog.micNotSendingData";
71
 
75
 
72
 /**
76
 /**
73
  * Prompt user for nickname.
77
  * Prompt user for nickname.
257
     }
261
     }
258
 };
262
 };
259
 
263
 
264
+/**
265
+ * Shows/hides the indication about local connection being interrupted.
266
+ *
267
+ * @param {boolean} isInterrupted <tt>true</tt> if local connection is
268
+ * currently in the interrupted state or <tt>false</tt> if the connection
269
+ * is fine.
270
+ */
271
+UI.showLocalConnectionInterrupted = function (isInterrupted) {
272
+    VideoLayout.showLocalConnectionInterrupted(isInterrupted);
273
+};
274
+
260
 /**
275
 /**
261
  * Sets the "raised hand" status for a participant.
276
  * Sets the "raised hand" status for a participant.
262
  */
277
  */
292
     }
307
     }
293
 
308
 
294
     // Add myself to the contact list.
309
     // Add myself to the contact list.
295
-    ContactList.addContact(id);
310
+    ContactList.addContact(id, true);
296
 
311
 
297
-    //update default button states before showing the toolbar
298
-    //if local role changes buttons state will be again updated
299
-    UI.updateLocalRole(false);
312
+    // Update default button states before showing the toolbar
313
+    // if local role changes buttons state will be again updated.
314
+    UI.updateLocalRole(APP.conference.isModerator);
300
 
315
 
301
     UI.showToolbar();
316
     UI.showToolbar();
302
 
317
 
325
     // to the UI (depending on the moderator role of the local participant) and
340
     // to the UI (depending on the moderator role of the local participant) and
326
     // (2) APP.conference as means of communication between the participants.
341
     // (2) APP.conference as means of communication between the participants.
327
     followMeHandler = new FollowMe(APP.conference, UI);
342
     followMeHandler = new FollowMe(APP.conference, UI);
343
+
344
+    UIUtil.activateTooltips();
328
 };
345
 };
329
 
346
 
330
 UI.mucJoined = function () {
347
 UI.mucJoined = function () {
339
     VideoLayout.resizeVideoArea(true, false);
356
     VideoLayout.resizeVideoArea(true, false);
340
 };
357
 };
341
 
358
 
359
+/**
360
+ * Sets tooltip defaults.
361
+ *
362
+ * @private
363
+ */
364
+function _setTooltipDefaults() {
365
+    $.fn.tooltip.defaults = {
366
+        opacity: 1, //defaults to 1
367
+        offset: 1,
368
+        delayIn: 0, //defaults to 500
369
+        hoverable: true,
370
+        hideOnClick: true,
371
+        aria: true
372
+    };
373
+}
374
+
342
 /**
375
 /**
343
  * Setup some UI event listeners.
376
  * Setup some UI event listeners.
344
  */
377
  */
431
     // Set the defaults for prompt dialogs.
464
     // Set the defaults for prompt dialogs.
432
     $.prompt.setDefaults({persistent: false});
465
     $.prompt.setDefaults({persistent: false});
433
 
466
 
467
+    // Set the defaults for tooltips.
468
+    _setTooltipDefaults();
469
+
434
     registerListeners();
470
     registerListeners();
435
 
471
 
436
     ToolbarToggler.init();
472
     ToolbarToggler.init();
463
             $('#noticeText').text(config.noticeMessage);
499
             $('#noticeText').text(config.noticeMessage);
464
             $('#notice').css({display: 'block'});
500
             $('#notice').css({display: 'block'});
465
         }
501
         }
466
-        $("#downloadlog").click(function (event) {
467
-            let logs = APP.conference.getLogs();
468
-            let data = encodeURIComponent(JSON.stringify(logs, null, '  '));
469
-
470
-            let elem = event.target.parentNode;
471
-            elem.download = 'meetlog.json';
472
-            elem.href = 'data:application/json;charset=utf-8,\n' + data;
473
-        });
474
     } else {
502
     } else {
475
         $("#mainToolbarContainer").css("display", "none");
503
         $("#mainToolbarContainer").css("display", "none");
476
-        $("#downloadlog").css("display", "none");
477
         FilmStrip.setupFilmStripOnly();
504
         FilmStrip.setupFilmStripOnly();
478
         messageHandler.enableNotifications(false);
505
         messageHandler.enableNotifications(false);
479
-        $('body').popover("disable");
480
         JitsiPopover.enabled = false;
506
         JitsiPopover.enabled = false;
481
     }
507
     }
482
 
508
 
589
 
615
 
590
 /**
616
 /**
591
  * Show user on UI.
617
  * Show user on UI.
592
- * @param {string} id user id
593
- * @param {string} displayName user nickname
618
+ * @param {JitsiParticipant} user
594
  */
619
  */
595
-UI.addUser = function (id, displayName) {
620
+UI.addUser = function (user) {
621
+    var id = user.getId();
622
+    var displayName = user.getDisplayName();
596
     UI.hideRingOverLay();
623
     UI.hideRingOverLay();
597
     ContactList.addContact(id);
624
     ContactList.addContact(id);
598
 
625
 
605
         UIUtil.playSoundNotification('userJoined');
632
         UIUtil.playSoundNotification('userJoined');
606
 
633
 
607
     // Add Peer's container
634
     // Add Peer's container
608
-    VideoLayout.addParticipantContainer(id);
635
+    VideoLayout.addParticipantContainer(user);
609
 
636
 
610
     // Configure avatar
637
     // Configure avatar
611
     UI.setUserEmail(id);
638
     UI.setUserEmail(id);
662
     SettingsMenu.showFollowMeOptions(isModerator);
689
     SettingsMenu.showFollowMeOptions(isModerator);
663
 
690
 
664
     if (isModerator) {
691
     if (isModerator) {
665
-        messageHandler.notify(null, "notify.me", 'connected', "notify.moderator");
692
+        if (!interfaceConfig.DISABLE_FOCUS_INDICATOR)
693
+            messageHandler
694
+                .notify(null, "notify.me", 'connected', "notify.moderator");
666
 
695
 
667
         Recording.checkAutoRecord();
696
         Recording.checkAutoRecord();
668
     }
697
     }
676
 UI.updateUserRole = function (user) {
705
 UI.updateUserRole = function (user) {
677
     VideoLayout.showModeratorIndicator();
706
     VideoLayout.showModeratorIndicator();
678
 
707
 
679
-    if (!user.isModerator()) {
708
+    // We don't need to show moderator notifications when the focus (moderator)
709
+    // indicator is disabled.
710
+    if (!user.isModerator() || interfaceConfig.DISABLE_FOCUS_INDICATOR) {
680
         return;
711
         return;
681
     }
712
     }
682
 
713
 
970
     VideoLayout.onLastNEndpointsChanged(ids, enteringIds);
1001
     VideoLayout.onLastNEndpointsChanged(ids, enteringIds);
971
 };
1002
 };
972
 
1003
 
1004
+/**
1005
+ * Will handle notification about participant's connectivity status change.
1006
+ *
1007
+ * @param {string} id the id of remote participant(MUC jid)
1008
+ * @param {boolean} isActive true if the connection is ok or false if the user
1009
+ * is having connectivity issues.
1010
+ */
1011
+UI.participantConnectionStatusChanged = function (id, isActive) {
1012
+    VideoLayout.onParticipantConnectionStatusChanged(id, isActive);
1013
+};
1014
+
973
 /**
1015
 /**
974
  * Update audio level visualization for specified user.
1016
  * Update audio level visualization for specified user.
975
  * @param {string} id user id
1017
  * @param {string} id user id
1052
     //Toolbar.showDialPadButton(dtmfSupport);
1094
     //Toolbar.showDialPadButton(dtmfSupport);
1053
 };
1095
 };
1054
 
1096
 
1055
-/**
1056
- * Invite participants to conference.
1057
- * @param {string} roomUrl
1058
- * @param {string} conferenceName
1059
- * @param {string} key
1060
- * @param {string} nick
1061
- */
1062
-UI.inviteParticipants = function (roomUrl, conferenceName, key, nick) {
1063
-    let keyText = "";
1064
-    if (key) {
1065
-        keyText = APP.translation.translateString(
1066
-            "email.sharedKey", {sharedKey: key}
1067
-        );
1068
-    }
1069
-
1070
-    let and = APP.translation.translateString("email.and");
1071
-    let supportedBrowsers = `Chromium, Google Chrome, Firefox ${and} Opera`;
1072
-
1073
-    let subject = APP.translation.translateString(
1074
-        "email.subject", {appName:interfaceConfig.APP_NAME, conferenceName}
1075
-    );
1076
-
1077
-    let body = APP.translation.translateString(
1078
-        "email.body", {
1079
-            appName:interfaceConfig.APP_NAME,
1080
-            sharedKeyText: keyText,
1081
-            roomUrl,
1082
-            supportedBrowsers
1083
-        }
1084
-    );
1085
-
1086
-    body = body.replace(/\n/g, "%0D%0A");
1087
-
1088
-    if (nick) {
1089
-        body += "%0D%0A%0D%0A" + UIUtil.escapeHtml(nick);
1090
-    }
1091
-
1092
-    if (interfaceConfig.INVITATION_POWERED_BY) {
1093
-        body += "%0D%0A%0D%0A--%0D%0Apowered by jitsi.org";
1094
-    }
1095
-
1096
-    window.open(`mailto:?subject=${subject}&body=${body}`, '_blank');
1097
-};
1098
-
1099
 /**
1097
 /**
1100
  * Show user feedback dialog if its required or just show "thank you" dialog.
1098
  * Show user feedback dialog if its required or just show "thank you" dialog.
1101
  * @returns {Promise} when dialog is closed.
1099
  * @returns {Promise} when dialog is closed.
1103
 UI.requestFeedback = function () {
1101
 UI.requestFeedback = function () {
1104
     if (Feedback.isVisible())
1102
     if (Feedback.isVisible())
1105
         return Promise.reject(UIErrors.FEEDBACK_REQUEST_IN_PROGRESS);
1103
         return Promise.reject(UIErrors.FEEDBACK_REQUEST_IN_PROGRESS);
1104
+    // Feedback has been submitted already.
1105
+    else if (Feedback.isEnabled() && Feedback.isSubmitted())
1106
+        return Promise.resolve();
1106
     else
1107
     else
1107
         return new Promise(function (resolve, reject) {
1108
         return new Promise(function (resolve, reject) {
1108
             if (Feedback.isEnabled()) {
1109
             if (Feedback.isEnabled()) {
1109
                 // If the user has already entered feedback, we'll show the
1110
                 // If the user has already entered feedback, we'll show the
1110
                 // window and immidiately start the conference dispose timeout.
1111
                 // window and immidiately start the conference dispose timeout.
1111
-                if (Feedback.feedbackScore > 0) {
1112
+                if (Feedback.getFeedbackScore() > 0) {
1112
                     Feedback.openFeedbackWindow();
1113
                     Feedback.openFeedbackWindow();
1113
                     resolve();
1114
                     resolve();
1114
 
1115
 
1117
                 }
1118
                 }
1118
             } else {
1119
             } else {
1119
                 // If the feedback functionality isn't enabled we show a thank
1120
                 // If the feedback functionality isn't enabled we show a thank
1120
-                // you dialog.
1121
-                messageHandler.openMessageDialog(
1122
-                    null, null, null,
1123
-                    APP.translation.translateString(
1124
-                        "dialog.thankYou", {appName:interfaceConfig.APP_NAME}
1125
-                    )
1126
-                );
1127
-                resolve();
1121
+                // you dialog. Signaling it (true), so the caller
1122
+                // of requestFeedback can act on it
1123
+                resolve(true);
1128
             }
1124
             }
1129
         });
1125
         });
1130
 };
1126
 };
1134
 };
1130
 };
1135
 
1131
 
1136
 UI.notifyTokenAuthFailed = function () {
1132
 UI.notifyTokenAuthFailed = function () {
1137
-    messageHandler.showError("dialog.error", "dialog.tokenAuthFailed");
1133
+    messageHandler.showError(   "dialog.tokenAuthFailedTitle",
1134
+                                "dialog.tokenAuthFailed");
1138
 };
1135
 };
1139
 
1136
 
1140
 UI.notifyInternalError = function () {
1137
 UI.notifyInternalError = function () {
1141
-    messageHandler.showError("dialog.sorry", "dialog.internalError");
1138
+    messageHandler.showError(   "dialog.internalErrorTitle",
1139
+                                "dialog.internalError");
1142
 };
1140
 };
1143
 
1141
 
1144
 UI.notifyFocusDisconnected = function (focus, retrySec) {
1142
 UI.notifyFocusDisconnected = function (focus, retrySec) {
1193
     SettingsMenu.updateStartMutedBox(startAudioMuted, startVideoMuted);
1191
     SettingsMenu.updateStartMutedBox(startAudioMuted, startVideoMuted);
1194
 };
1192
 };
1195
 
1193
 
1194
+/**
1195
+ * Notifies interested listeners that the raise hand property has changed.
1196
+ *
1197
+ * @param {boolean} isRaisedHand indicates the current state of the
1198
+ * "raised hand"
1199
+ */
1200
+UI.onLocalRaiseHandChanged = function (isRaisedHand) {
1201
+    eventEmitter.emit(UIEvents.LOCAL_RAISE_HAND_CHANGED, isRaisedHand);
1202
+};
1203
+
1196
 /**
1204
 /**
1197
  * Update list of available physical devices.
1205
  * Update list of available physical devices.
1198
  * @param {object[]} devices new list of available devices
1206
  * @param {object[]} devices new list of available devices
1415
 
1423
 
1416
 /**
1424
 /**
1417
  * Shows error dialog that informs the user that no data is received from the
1425
  * Shows error dialog that informs the user that no data is received from the
1418
- * microphone.
1426
+ * device.
1419
  */
1427
  */
1420
-UI.showAudioNotWorkingDialog = function () {
1428
+UI.showTrackNotWorkingDialog = function (stream) {
1421
     messageHandler.openMessageDialog(
1429
     messageHandler.openMessageDialog(
1422
         "dialog.error",
1430
         "dialog.error",
1423
-        "dialog.micNotSendingData",
1431
+        stream.isAudioTrack()? "dialog.micNotSendingData" :
1432
+            "dialog.cameraNotSendingData",
1424
         null,
1433
         null,
1425
         null);
1434
         null);
1426
 };
1435
 };

+ 121
- 216
modules/UI/audio_levels/AudioLevels.js 查看文件

1
-/* global APP, interfaceConfig, $ */
2
-/* jshint -W101 */
1
+/* global interfaceConfig */
3
 
2
 
4
-import CanvasUtil from './CanvasUtils';
5
-import FilmStrip from '../videolayout/FilmStrip';
6
-
7
-const LOCAL_LEVEL = 'local';
8
-
9
-let ASDrawContext = null;
10
-let audioLevelCanvasCache = {};
11
-let dominantSpeakerAudioElement = null;
12
-
13
-function initDominantSpeakerAudioLevels(dominantSpeakerAvatarSize) {
14
-    let ASRadius = dominantSpeakerAvatarSize / 2;
15
-    let ASCenter = (dominantSpeakerAvatarSize + ASRadius) / 2;
16
-
17
-    // Draw a circle.
18
-    ASDrawContext.beginPath();
19
-    ASDrawContext.arc(ASCenter, ASCenter, ASRadius, 0, 2 * Math.PI);
20
-    ASDrawContext.closePath();
21
-
22
-    // Add a shadow around the circle
23
-    ASDrawContext.shadowColor = interfaceConfig.SHADOW_COLOR;
24
-    ASDrawContext.shadowOffsetX = 0;
25
-    ASDrawContext.shadowOffsetY = 0;
26
-}
3
+import UIUtil from "../util/UIUtil";
27
 
4
 
28
 /**
5
 /**
29
- * Resizes the given audio level canvas to match the given thumbnail size.
6
+ * Responsible for drawing audio levels.
30
  */
7
  */
31
-function resizeAudioLevelCanvas(audioLevelCanvas, thumbnailWidth, thumbnailHeight) {
32
-    audioLevelCanvas.width = thumbnailWidth + interfaceConfig.CANVAS_EXTRA;
33
-    audioLevelCanvas.height = thumbnailHeight + interfaceConfig.CANVAS_EXTRA;
34
-}
35
-
36
-/**
37
- * Draws the audio level canvas into the cached canvas object.
38
- *
39
- * @param id of the user for whom we draw the audio level
40
- * @param audioLevel the newAudio level to render
41
- */
42
-function drawAudioLevelCanvas(id, audioLevel) {
43
-    if (!audioLevelCanvasCache[id]) {
44
-
45
-        let videoSpanId = getVideoSpanId(id);
46
-
47
-        let audioLevelCanvasOrig = $(`#${videoSpanId}>canvas`).get(0);
48
-
49
-        /*
50
-         * FIXME Testing has shown that audioLevelCanvasOrig may not exist.
51
-         * In such a case, the method CanvasUtil.cloneCanvas may throw an
52
-         * error. Since audio levels are frequently updated, the errors have
53
-         * been observed to pile into the console, strain the CPU.
54
-         */
55
-        if (audioLevelCanvasOrig) {
56
-            audioLevelCanvasCache[id]
57
-                = CanvasUtil.cloneCanvas(audioLevelCanvasOrig);
58
-        }
59
-    }
60
-
61
-    let canvas = audioLevelCanvasCache[id];
62
-
63
-    if (!canvas) {
64
-        return;
65
-    }
66
-
67
-    let drawContext = canvas.getContext('2d');
68
-
69
-    drawContext.clearRect(0, 0, canvas.width, canvas.height);
70
-
71
-    let shadowLevel = getShadowLevel(audioLevel);
72
-
73
-    if (shadowLevel > 0) {
74
-        // drawContext, x, y, w, h, r, shadowColor, shadowLevel
75
-        CanvasUtil.drawRoundRectGlow(
76
-            drawContext,
77
-            interfaceConfig.CANVAS_EXTRA / 2, interfaceConfig.CANVAS_EXTRA / 2,
78
-            canvas.width - interfaceConfig.CANVAS_EXTRA,
79
-            canvas.height - interfaceConfig.CANVAS_EXTRA,
80
-            interfaceConfig.CANVAS_RADIUS,
81
-            interfaceConfig.SHADOW_COLOR,
82
-            shadowLevel);
83
-    }
84
-}
85
-
86
-/**
87
- * Returns the shadow/glow level for the given audio level.
88
- *
89
- * @param audioLevel the audio level from which we determine the shadow
90
- * level
91
- */
92
-function getShadowLevel (audioLevel) {
93
-    let shadowLevel = 0;
94
-
95
-    if (audioLevel <= 0.3) {
96
-        shadowLevel = Math.round(
97
-            interfaceConfig.CANVAS_EXTRA/2*(audioLevel/0.3));
98
-    } else if (audioLevel <= 0.6) {
99
-        shadowLevel = Math.round(
100
-            interfaceConfig.CANVAS_EXTRA/2*((audioLevel - 0.3) / 0.3));
101
-    } else {
102
-        shadowLevel = Math.round(
103
-            interfaceConfig.CANVAS_EXTRA/2*((audioLevel - 0.6) / 0.4));
104
-    }
105
-
106
-    return shadowLevel;
107
-}
108
-
109
-/**
110
- * Returns the video span id corresponding to the given user id
111
- */
112
-function getVideoSpanId(id) {
113
-    let videoSpanId = null;
8
+const AudioLevels = {
114
 
9
 
115
-    if (id === LOCAL_LEVEL || APP.conference.isLocalId(id)) {
116
-        videoSpanId = 'localVideoContainer';
117
-    } else {
118
-        videoSpanId = `participant_${id}`;
119
-    }
10
+    /**
11
+     * The number of dots.
12
+     *
13
+     * IMPORTANT: functions below assume that this is an odd number.
14
+     */
15
+    _AUDIO_LEVEL_DOTS: 5,
120
 
16
 
121
-    return videoSpanId;
122
-}
17
+    /**
18
+     * Creates the audio level indicator span element.
19
+     *
20
+     * IMPORTANT: This function assumes that the number of dots is an
21
+     * odd number.
22
+     *
23
+     * @return {Element} the document element representing audio levels
24
+     */
25
+    createThumbnailAudioLevelIndicator() {
123
 
26
 
124
-/**
125
- * The audio Levels plugin.
126
- */
127
-const AudioLevels = {
27
+        let audioSpan = document.createElement('span');
28
+        audioSpan.className = 'audioindicator';
128
 
29
 
129
-    init () {
130
-        dominantSpeakerAudioElement =  $('#dominantSpeakerAudioLevel')[0];
131
-        ASDrawContext = dominantSpeakerAudioElement.getContext('2d');
30
+        this.sideDotsCount = Math.floor(this._AUDIO_LEVEL_DOTS/2);
132
 
31
 
133
-        let parentContainer = $("#dominantSpeaker");
134
-        let dominantSpeakerWidth = parentContainer.width();
135
-        let dominantSpeakerHeight = parentContainer.height();
32
+        for (let i = 0; i < this._AUDIO_LEVEL_DOTS; i++) {
33
+            let audioDot = document.createElement('span');
136
 
34
 
137
-        dominantSpeakerAudioElement.width = dominantSpeakerWidth;
138
-        dominantSpeakerAudioElement.height = dominantSpeakerHeight;
35
+            // The median index will be equal to the number of dots on each
36
+            // side.
37
+            if (i === this.sideDotsCount)
38
+                audioDot.className = "audiodot-middle";
39
+            else
40
+                audioDot.className = (i < this.sideDotsCount)
41
+                                    ? "audiodot-top"
42
+                                    : "audiodot-bottom";
139
 
43
 
140
-        let dominantSpeakerAvatar = $("#dominantSpeakerAvatar");
141
-        initDominantSpeakerAudioLevels(dominantSpeakerAvatar.width());
44
+            audioSpan.appendChild(audioDot);
45
+        }
46
+        return audioSpan;
142
     },
47
     },
143
 
48
 
144
     /**
49
     /**
145
-     * Updates the audio level canvas for the given id. If the canvas
146
-     * didn't exist we create it.
50
+     * Updates the audio level UI for the given id.
51
+     *
52
+     * @param {string} id id of the user for whom we draw the audio level
53
+     * @param {number} audioLevel the newAudio level to render
147
      */
54
      */
148
-    updateAudioLevelCanvas (id, thumbWidth, thumbHeight) {
149
-        let videoSpanId = 'localVideoContainer';
150
-        if (id) {
151
-            videoSpanId = `participant_${id}`;
152
-        }
55
+    updateThumbnailAudioLevel (id, audioLevel) {
153
 
56
 
154
-        let videoSpan = document.getElementById(videoSpanId);
57
+        // First make sure we are sensitive enough.
58
+        audioLevel *= 1.2;
59
+        audioLevel = Math.min(audioLevel, 1);
155
 
60
 
156
-        if (!videoSpan) {
157
-            if (id) {
158
-                console.error("No video element for id", id);
159
-            } else {
160
-                console.error("No video element for local video.");
161
-            }
162
-            return;
163
-        }
164
-
165
-        let audioLevelCanvas = $(`#${videoSpanId}>canvas`);
166
-
167
-        if (!audioLevelCanvas || audioLevelCanvas.length === 0) {
61
+        // Let's now stretch the audio level over the number of dots we have.
62
+        let stretchedAudioLevel = (this.sideDotsCount + 1) * audioLevel;
63
+        let dotLevel = 0.0;
168
 
64
 
169
-            audioLevelCanvas = document.createElement('canvas');
170
-            audioLevelCanvas.className = "audiolevel";
171
-            audioLevelCanvas.style.bottom
172
-                = `-${interfaceConfig.CANVAS_EXTRA/2}px`;
173
-            audioLevelCanvas.style.left
174
-                = `-${interfaceConfig.CANVAS_EXTRA/2}px`;
175
-            resizeAudioLevelCanvas(audioLevelCanvas, thumbWidth, thumbHeight);
65
+        for (let i = 0; i < (this.sideDotsCount + 1); i++) {
176
 
66
 
177
-            videoSpan.appendChild(audioLevelCanvas);
178
-        } else {
179
-            audioLevelCanvas = audioLevelCanvas.get(0);
180
-
181
-            resizeAudioLevelCanvas(audioLevelCanvas, thumbWidth, thumbHeight);
67
+            dotLevel = Math.min(1, Math.max(0, (stretchedAudioLevel - i)));
68
+            this._setDotLevel(id, i, dotLevel);
182
         }
69
         }
183
     },
70
     },
184
 
71
 
185
     /**
72
     /**
186
-     * Updates the audio level UI for the given id.
73
+     * Fills the dot(s) with the specified "index", with as much opacity as
74
+     * indicated by "opacity".
187
      *
75
      *
188
-     * @param id id of the user for whom we draw the audio level
189
-     * @param audioLevel the newAudio level to render
76
+     * @param {string} elementID the parent audio indicator span element
77
+     * @param {number} index the index of the dots to fill, where 0 indicates
78
+     * the middle dot and the following increments point toward the
79
+     * corresponding pair of dots.
80
+     * @param {number} opacity the opacity to set for the specified dot.
190
      */
81
      */
191
-    updateAudioLevel (id, audioLevel, largeVideoId) {
192
-        drawAudioLevelCanvas(id, audioLevel);
82
+    _setDotLevel(elementID, index, opacity) {
193
 
83
 
194
-        let videoSpanId = getVideoSpanId(id);
84
+        let audioSpan = document.getElementById(elementID)
85
+            .getElementsByClassName("audioindicator");
195
 
86
 
196
-        let audioLevelCanvas = $(`#${videoSpanId}>canvas`).get(0);
197
-
198
-        if (!audioLevelCanvas) {
87
+        // Make sure the audio span is still around.
88
+        if (audioSpan && audioSpan.length > 0)
89
+            audioSpan = audioSpan[0];
90
+        else
199
             return;
91
             return;
200
-        }
201
 
92
 
202
-        let drawContext = audioLevelCanvas.getContext('2d');
93
+        let audioTopDots
94
+            = audioSpan.getElementsByClassName("audiodot-top");
95
+        let audioDotMiddle
96
+            = audioSpan.getElementsByClassName("audiodot-middle");
97
+        let audioBottomDots
98
+            = audioSpan.getElementsByClassName("audiodot-bottom");
203
 
99
 
204
-        let canvasCache = audioLevelCanvasCache[id];
205
-
206
-        drawContext.clearRect(
207
-            0, 0, audioLevelCanvas.width, audioLevelCanvas.height
208
-        );
209
-        drawContext.drawImage(canvasCache, 0, 0);
210
-
211
-        if (id === LOCAL_LEVEL) {
212
-            id = APP.conference.getMyUserId();
213
-            if (!id) {
214
-                return;
215
-            }
100
+        // First take care of the middle dot case.
101
+        if (index === 0){
102
+            audioDotMiddle[0].style.opacity = opacity;
103
+            return;
216
         }
104
         }
217
 
105
 
218
-        if(id === largeVideoId) {
219
-            window.requestAnimationFrame(function () {
220
-                AudioLevels.updateDominantSpeakerAudioLevel(audioLevel);
221
-            });
222
-        }
106
+        // Index > 0 : we are setting non-middle dots.
107
+        index--;
108
+        audioBottomDots[index].style.opacity = opacity;
109
+        audioTopDots[this.sideDotsCount - index - 1].style.opacity = opacity;
223
     },
110
     },
224
 
111
 
225
-    updateDominantSpeakerAudioLevel (audioLevel) {
226
-        if($("#dominantSpeaker").css("visibility") == "hidden"
227
-            || ASDrawContext === null) {
228
-            return;
229
-        }
230
-
231
-        ASDrawContext.clearRect(0, 0,
232
-            dominantSpeakerAudioElement.width,
233
-            dominantSpeakerAudioElement.height);
112
+    /**
113
+     * Updates the audio level of the large video.
114
+     *
115
+     * @param audioLevel the new audio level to set.
116
+     */
117
+    updateLargeVideoAudioLevel(elementId, audioLevel) {
118
+        let element = document.getElementById(elementId);
234
 
119
 
235
-        if (!audioLevel) {
120
+        if(!UIUtil.isVisible(element))
236
             return;
121
             return;
237
-        }
238
 
122
 
239
-        ASDrawContext.shadowBlur = getShadowLevel(audioLevel);
123
+        let level = parseFloat(audioLevel);
240
 
124
 
241
-        // Fill the shape.
242
-        ASDrawContext.fill();
243
-    },
125
+        level = isNaN(level) ? 0 : level;
244
 
126
 
245
-    updateCanvasSize (thumbWidth, thumbHeight) {
246
-        let canvasWidth = thumbWidth + interfaceConfig.CANVAS_EXTRA;
247
-        let canvasHeight = thumbHeight + interfaceConfig.CANVAS_EXTRA;
127
+        let shadowElement = element.getElementsByClassName("dynamic-shadow");
248
 
128
 
249
-        FilmStrip.getThumbs().children('canvas').each(function () {
250
-            $(this).attr('width', canvasWidth);
251
-            $(this).attr('height', canvasHeight);
252
-        });
129
+        if (shadowElement && shadowElement.length > 0)
130
+            shadowElement = shadowElement[0];
253
 
131
 
254
-        Object.keys(audioLevelCanvasCache).forEach(function (id) {
255
-            audioLevelCanvasCache[id].width = canvasWidth;
256
-            audioLevelCanvasCache[id].height = canvasHeight;
257
-        });
132
+        shadowElement.style.boxShadow = this._updateLargeVideoShadow(level);
133
+    },
134
+
135
+    /**
136
+     * Updates the large video shadow effect.
137
+     */
138
+    _updateLargeVideoShadow (level) {
139
+        var scale = 2,
140
+
141
+        // Internal circle audio level.
142
+        int = {
143
+            level: level > 0.15 ? 20 : 0,
144
+            color: interfaceConfig.AUDIO_LEVEL_PRIMARY_COLOR
145
+        },
146
+
147
+        // External circle audio level.
148
+        ext = {
149
+            level: (int.level * scale * level + int.level).toFixed(0),
150
+            color: interfaceConfig.AUDIO_LEVEL_SECONDARY_COLOR
151
+        };
152
+
153
+        // Internal blur.
154
+        int.blur = int.level ? 2 : 0;
155
+
156
+        // External blur.
157
+        ext.blur = ext.level ? 6 : 0;
158
+
159
+        return [
160
+            `0 0 ${ int.blur }px ${ int.level }px ${ int.color }`,
161
+            `0 0 ${ ext.blur }px ${ ext.level }px ${ ext.color }`
162
+        ].join(', ');
258
     }
163
     }
259
 };
164
 };
260
 
165
 

+ 0
- 108
modules/UI/audio_levels/CanvasUtils.js 查看文件

1
-/**
2
- * Utility class for drawing canvas shapes.
3
- */
4
-const CanvasUtil = {
5
-
6
-    /**
7
-     * Draws a round rectangle with a glow. The glowWidth indicates the depth
8
-     * of the glow.
9
-     *
10
-     * @param drawContext the context of the canvas to draw to
11
-     * @param x the x coordinate of the round rectangle
12
-     * @param y the y coordinate of the round rectangle
13
-     * @param w the width of the round rectangle
14
-     * @param h the height of the round rectangle
15
-     * @param glowColor the color of the glow
16
-     * @param glowWidth the width of the glow
17
-     */
18
-    drawRoundRectGlow (drawContext, x, y, w, h, r, glowColor, glowWidth) {
19
-
20
-        // Save the previous state of the context.
21
-        drawContext.save();
22
-
23
-        if (w < 2 * r) r = w / 2;
24
-        if (h < 2 * r) r = h / 2;
25
-
26
-        // Draw a round rectangle.
27
-        drawContext.beginPath();
28
-        drawContext.moveTo(x+r, y);
29
-        drawContext.arcTo(x+w, y,   x+w, y+h, r);
30
-        drawContext.arcTo(x+w, y+h, x,   y+h, r);
31
-        drawContext.arcTo(x,   y+h, x,   y,   r);
32
-        drawContext.arcTo(x,   y,   x+w, y,   r);
33
-        drawContext.closePath();
34
-
35
-        // Add a shadow around the rectangle
36
-        drawContext.shadowColor = glowColor;
37
-        drawContext.shadowBlur = glowWidth;
38
-        drawContext.shadowOffsetX = 0;
39
-        drawContext.shadowOffsetY = 0;
40
-
41
-        // Fill the shape.
42
-        drawContext.fill();
43
-
44
-        drawContext.save();
45
-
46
-        drawContext.restore();
47
-
48
-//      1) Uncomment this line to use Composite Operation, which is doing the
49
-//      same as the clip function below and is also antialiasing the round
50
-//      border, but is said to be less fast performance wise.
51
-
52
-//      drawContext.globalCompositeOperation='destination-out';
53
-
54
-        drawContext.beginPath();
55
-        drawContext.moveTo(x+r, y);
56
-        drawContext.arcTo(x+w, y,   x+w, y+h, r);
57
-        drawContext.arcTo(x+w, y+h, x,   y+h, r);
58
-        drawContext.arcTo(x,   y+h, x,   y,   r);
59
-        drawContext.arcTo(x,   y,   x+w, y,   r);
60
-        drawContext.closePath();
61
-
62
-//      2) Uncomment this line to use Composite Operation, which is doing the
63
-//      same as the clip function below and is also antialiasing the round
64
-//      border, but is said to be less fast performance wise.
65
-
66
-//      drawContext.fill();
67
-
68
-        // Comment these two lines if choosing to do the same with composite
69
-        // operation above 1 and 2.
70
-        drawContext.clip();
71
-        drawContext.clearRect(0, 0, 277, 200);
72
-
73
-        // Restore the previous context state.
74
-        drawContext.restore();
75
-    },
76
-
77
-    /**
78
-     * Clones the given canvas.
79
-     *
80
-     * @return the new cloned canvas.
81
-     */
82
-    cloneCanvas (oldCanvas) {
83
-        /*
84
-         * FIXME Testing has shown that oldCanvas may not exist. In such a case,
85
-         * the method CanvasUtil.cloneCanvas may throw an error. Since audio
86
-         * levels are frequently updated, the errors have been observed to pile
87
-         * into the console, strain the CPU.
88
-         */
89
-        if (!oldCanvas)
90
-            return oldCanvas;
91
-
92
-        //create a new canvas
93
-        var newCanvas = document.createElement('canvas');
94
-        var context = newCanvas.getContext('2d');
95
-
96
-        //set dimensions
97
-        newCanvas.width = oldCanvas.width;
98
-        newCanvas.height = oldCanvas.height;
99
-
100
-        //apply the old canvas to the new one
101
-        context.drawImage(oldCanvas, 0, 0);
102
-
103
-        //return the new canvas
104
-        return newCanvas;
105
-    }
106
-};
107
-
108
-export default CanvasUtil;

+ 30
- 2
modules/UI/authentication/RoomLocker.js 查看文件

116
     let password;
116
     let password;
117
     let dialog = null;
117
     let dialog = null;
118
 
118
 
119
+    /**
120
+     * If the room was locked from someone other than us, we indicate it with
121
+     * this property in order to have correct roomLocker state of isLocked.
122
+     * @type {boolean} whether room is locked, but not from us.
123
+     */
124
+    let lockedElsewhere = false;
125
+
119
     function lock (newPass) {
126
     function lock (newPass) {
120
         return room.lock(newPass).then(function () {
127
         return room.lock(newPass).then(function () {
121
             password = newPass;
128
             password = newPass;
135
      */
142
      */
136
     return {
143
     return {
137
         get isLocked () {
144
         get isLocked () {
138
-            return !!password;
145
+            return !!password || lockedElsewhere;
139
         },
146
         },
140
 
147
 
141
         get password () {
148
         get password () {
142
             return password;
149
             return password;
143
         },
150
         },
144
 
151
 
152
+        /**
153
+         * Sets that the room is locked from another user, not us.
154
+         * @param {boolean} value locked/unlocked state
155
+         */
156
+        set lockedElsewhere (value) {
157
+            lockedElsewhere = value;
158
+        },
159
+
160
+        /**
161
+         * Whether room is locked from someone else.
162
+         * @returns {boolean} whether room is not locked locally,
163
+         * but it is still locked.
164
+         */
165
+        get lockedElsewhere () {
166
+            return lockedElsewhere;
167
+        },
168
+
145
         /**
169
         /**
146
          * Allows to remove password from the conference (asks user first).
170
          * Allows to remove password from the conference (asks user first).
147
          * @returns {Promise}
171
          * @returns {Promise}
185
                 newPass => { password = newPass; }
209
                 newPass => { password = newPass; }
186
             ).catch(
210
             ).catch(
187
                 reason => {
211
                 reason => {
212
+                    // user canceled, no pass was entered.
213
+                    // clear, as if we use the same instance several times
214
+                    // pass stays between attempts
215
+                    password = null;
188
                     if (reason !== APP.UI.messageHandler.CANCEL)
216
                     if (reason !== APP.UI.messageHandler.CANCEL)
189
                         console.error(reason);
217
                         console.error(reason);
190
                 }
218
                 }
202
                 dialog = null;
230
                 dialog = null;
203
             };
231
             };
204
 
232
 
205
-            if (password) {
233
+            if (this.isLocked) {
206
                 dialog = APP.UI.messageHandler
234
                 dialog = APP.UI.messageHandler
207
                     .openMessageDialog(null, "dialog.passwordError",
235
                     .openMessageDialog(null, "dialog.passwordError",
208
                         null, null, closeCallback);
236
                         null, null, closeCallback);

+ 128
- 0
modules/UI/feedback/Feedback.js 查看文件

1
+/* global $, APP, config, interfaceConfig, JitsiMeetJS */
2
+import UIEvents from "../../../service/UI/UIEvents";
3
+import FeedabckWindow from "./FeedbackWindow";
4
+
5
+/**
6
+ * Shows / hides the feedback button.
7
+ * @private
8
+ */
9
+function _toggleFeedbackIcon() {
10
+    $('#feedbackButtonDiv').toggleClass("hidden");
11
+}
12
+
13
+/**
14
+ * Shows / hides the feedback button.
15
+ * @param {show} set to {true} to show the feedback button or to  {false}
16
+ * to hide it
17
+ * @private
18
+ */
19
+function _showFeedbackButton (show) {
20
+    var feedbackButton = $("#feedbackButtonDiv");
21
+
22
+    if (show)
23
+        feedbackButton.css("display", "block");
24
+    else
25
+        feedbackButton.css("display", "none");
26
+}
27
+
28
+/**
29
+ * Defines all methods in connection to the Feedback window.
30
+ *
31
+ * @type {{openFeedbackWindow: Function}}
32
+ */
33
+var Feedback = {
34
+
35
+    /**
36
+     * Initialise the Feedback functionality.
37
+     * @param emitter the EventEmitter to associate with the Feedback.
38
+     */
39
+    init: function (emitter) {
40
+        // CallStats is the way we send feedback, so we don't have to initialise
41
+        // if callstats isn't enabled.
42
+        if (!APP.conference.isCallstatsEnabled())
43
+            return;
44
+
45
+        // If enabled property is still undefined, i.e. it hasn't been set from
46
+        // some other module already, we set it to true by default.
47
+        if (typeof this.enabled == "undefined")
48
+            this.enabled = true;
49
+
50
+        _showFeedbackButton(this.enabled);
51
+
52
+        this.window = new FeedabckWindow({});
53
+
54
+        $("#feedbackButton").click(Feedback.openFeedbackWindow);
55
+
56
+        // Show / hide the feedback button whenever the film strip is
57
+        // shown / hidden.
58
+        emitter.addListener(UIEvents.TOGGLE_FILM_STRIP, function () {
59
+            _toggleFeedbackIcon();
60
+        });
61
+    },
62
+    /**
63
+     * Enables/ disabled the feedback feature.
64
+     */
65
+    enableFeedback: function (enable) {
66
+        if (this.enabled !== enable)
67
+            _showFeedbackButton(enable);
68
+        this.enabled = enable;
69
+    },
70
+
71
+    /**
72
+     * Indicates if the feedback functionality is enabled.
73
+     *
74
+     * @return true if the feedback functionality is enabled, false otherwise.
75
+     */
76
+    isEnabled: function() {
77
+        return this.enabled && APP.conference.isCallstatsEnabled();
78
+    },
79
+
80
+    /**
81
+     * Returns true if the feedback window is currently visible and false
82
+     * otherwise.
83
+     * @return {boolean} true if the feedback window is visible, false
84
+     * otherwise
85
+     */
86
+    isVisible: function() {
87
+        return $(".feedback").is(":visible");
88
+    },
89
+
90
+    /**
91
+     * Indicates if the feedback is submitted.
92
+     *
93
+     * @return {boolean} {true} to indicate if the feedback is submitted,
94
+     * {false} - otherwise
95
+     */
96
+    isSubmitted: function() {
97
+        return Feedback.window.submitted;
98
+    },
99
+
100
+    /**
101
+     * Opens the feedback window.
102
+     */
103
+    openFeedbackWindow: function (callback) {
104
+        Feedback.window.show(callback);
105
+
106
+        JitsiMeetJS.analytics.sendEvent('feedback.open');
107
+    },
108
+
109
+    /**
110
+     * Returns the feedback score.
111
+     *
112
+     * @returns {*}
113
+     */
114
+    getFeedbackScore: function() {
115
+        return Feedback.window.feedbackScore;
116
+    },
117
+
118
+    /**
119
+     * Returns the feedback free text.
120
+     *
121
+     * @returns {null|*|message}
122
+     */
123
+    getFeedbackText: function() {
124
+        return Feedback.window.feedbackText;
125
+    }
126
+};
127
+
128
+module.exports = Feedback;

+ 193
- 0
modules/UI/feedback/FeedbackWindow.js 查看文件

1
+/* global $, APP, interfaceConfig, AJS */
2
+/* jshint -W101 */
3
+
4
+const selector = '#aui-feedback-dialog';
5
+
6
+/**
7
+ * Toggles the appropriate css class for the given number of stars, to
8
+ * indicate that those stars have been clicked/selected.
9
+ *
10
+ * @param starCount the number of stars, for which to toggle the css class
11
+ */
12
+let toggleStars = function(starCount) {
13
+    $('#stars > a').each(function(index, el) {
14
+        if (index <= starCount) {
15
+            el.classList.add("starHover");
16
+        } else
17
+            el.classList.remove("starHover");
18
+    });
19
+};
20
+
21
+/**
22
+ * Constructs the html for the rated feedback window.
23
+ *
24
+ * @returns {string} the contructed html string
25
+ */
26
+let createRateFeedbackHTML = function (Feedback) {
27
+    let rateExperience
28
+            = APP.translation.translateString('dialog.rateExperience'),
29
+        feedbackHelp = APP.translation.translateString('dialog.feedbackHelp');
30
+
31
+    let starClassName = (interfaceConfig.ENABLE_FEEDBACK_ANIMATION)
32
+                            ? "icon-star shake-rotate"
33
+                            : "icon-star";
34
+
35
+    return `
36
+        <div class="aui-dialog2-content feedback__content">
37
+            <form action="javascript:false;" onsubmit="return false;">
38
+                <div class="feedback__rating">
39
+                    <h2>${ rateExperience }</h2>
40
+                    <p class="star-label">&nbsp;</p>
41
+                    <div id="stars" class="feedback-stars">
42
+                        <a class="star-btn">
43
+                            <i class=${ starClassName }></i>
44
+                        </a>
45
+                        <a class="star-btn">
46
+                            <i class=${ starClassName }></i>
47
+                        </a>
48
+                        <a class="star-btn">
49
+                            <i class=${ starClassName }></i>
50
+                        </a>
51
+                        <a class="star-btn">
52
+                            <i class=${ starClassName }></i>
53
+                        </a>
54
+                        <a class="star-btn">
55
+                            <i class=${ starClassName }></i>
56
+                        </a>
57
+                    </div>
58
+                    <p>&nbsp;</p>
59
+                    <p>${ feedbackHelp }</p>
60
+                </div>
61
+                <textarea id="feedbackTextArea" rows="10" cols="40" autofocus></textarea>
62
+            </form>
63
+            <footer class="aui-dialog2-footer feedback__footer">
64
+                <div class="aui-dialog2-footer-actions">
65
+                    <button id="dialog-close-button" class="aui-button aui-button_close">Close</button>
66
+                    <button id="dialog-submit-button" class="aui-button aui-button_submit">Submit</button>
67
+                </div>
68
+            </footer>
69
+        </div>
70
+`;
71
+};
72
+
73
+/**
74
+ * Callback for Rate Feedback
75
+ *
76
+ * @param Feedback
77
+ */
78
+let onLoadRateFunction = function (Feedback) {
79
+    $('#stars > a').each((index, el) => {
80
+        el.onmouseover = function(){
81
+            toggleStars(index);
82
+        };
83
+        el.onmouseleave = function(){
84
+            toggleStars(Feedback.feedbackScore - 1);
85
+        };
86
+        el.onclick = function(){
87
+            Feedback.feedbackScore = index + 1;
88
+        };
89
+    });
90
+
91
+    // Init stars to correspond to previously entered feedback.
92
+    if (Feedback.feedbackScore > 0) {
93
+        toggleStars(Feedback.feedbackScore - 1);
94
+    }
95
+
96
+    if (Feedback.feedbackText && Feedback.feedbackText.length > 0)
97
+        $('#feedbackTextArea').text(Feedback.feedbackText);
98
+
99
+    let submitBtn = Feedback.$el.find('#dialog-submit-button');
100
+    let closeBtn = Feedback.$el.find('#dialog-close-button');
101
+
102
+    if (submitBtn && submitBtn.length) {
103
+        submitBtn.on('click', (e) => {
104
+            e.preventDefault();
105
+            Feedback.onFeedbackSubmitted();
106
+        });
107
+    }
108
+    if (closeBtn && closeBtn.length) {
109
+        closeBtn.on('click', (e) => {
110
+            e.preventDefault();
111
+            Feedback.hide();
112
+        });
113
+    }
114
+
115
+    $('#feedbackTextArea').focus();
116
+};
117
+
118
+/**
119
+ * @class Dialog
120
+ *
121
+ */
122
+export default class Dialog {
123
+
124
+    constructor(options) {
125
+        this.feedbackScore = -1;
126
+        this.feedbackText = null;
127
+        this.submitted = false;
128
+        this.onCloseCallback = null;
129
+
130
+        this.states = {
131
+            rate_feedback: {
132
+                getHtml: createRateFeedbackHTML,
133
+                onLoad: onLoadRateFunction
134
+            }
135
+        };
136
+        this.state = options.state || 'rate_feedback';
137
+
138
+        this.window = AJS.dialog2(selector, {
139
+            closeOnOutsideClick: true
140
+        });
141
+        this.$el = this.window.$el;
142
+
143
+        AJS.dialog2(selector).on("hide", function() {
144
+            if (this.onCloseCallback) {
145
+                this.onCloseCallback();
146
+                this.onCloseCallback = null;
147
+            }
148
+        }.bind(this));
149
+
150
+        this.setState();
151
+    }
152
+
153
+    setState(state) {
154
+        let newState = state || this.state;
155
+
156
+        let htmlStr = this.states[newState].getHtml(this);
157
+
158
+        this.$el.html(htmlStr);
159
+
160
+        this.states[newState].onLoad(this);
161
+    }
162
+
163
+    show(cb) {
164
+        this.setState('rate_feedback');
165
+        if (typeof cb == 'function') {
166
+            this.onCloseCallback = cb;
167
+        }
168
+
169
+        this.window.show();
170
+
171
+    }
172
+
173
+    hide() {
174
+        this.window.hide();
175
+    }
176
+
177
+    onFeedbackSubmitted() {
178
+        let message = this.$el.find('textarea').val();
179
+        let self = this;
180
+
181
+        if (message && message.length > 0) {
182
+            self.feedbackText = message;
183
+        }
184
+
185
+        APP.conference.sendFeedback(self.feedbackScore,
186
+                                    self.feedbackText);
187
+
188
+        // TO DO: make sendFeedback return true or false.
189
+        self.submitted = true;
190
+
191
+        this.hide();
192
+    }
193
+}

+ 4
- 1
modules/UI/recording/Recording.js 查看文件

17
 import UIEvents from "../../../service/UI/UIEvents";
17
 import UIEvents from "../../../service/UI/UIEvents";
18
 import UIUtil from '../util/UIUtil';
18
 import UIUtil from '../util/UIUtil';
19
 import VideoLayout from '../videolayout/VideoLayout';
19
 import VideoLayout from '../videolayout/VideoLayout';
20
-import Feedback from '../Feedback.js';
20
+import Feedback from '../feedback/Feedback.js';
21
 import Toolbar from '../toolbars/Toolbar';
21
 import Toolbar from '../toolbars/Toolbar';
22
 
22
 
23
 /**
23
 /**
270
     initRecordingButton(recordingType) {
270
     initRecordingButton(recordingType) {
271
         let selector = $('#toolbar_button_record');
271
         let selector = $('#toolbar_button_record');
272
 
272
 
273
+        let button = selector.get(0);
274
+        UIUtil.setTooltip(button, 'liveStreaming.buttonTooltip', 'right');
275
+
273
         if (recordingType === 'jibri') {
276
         if (recordingType === 'jibri') {
274
             this.baseClass = "fa fa-play-circle";
277
             this.baseClass = "fa fa-play-circle";
275
             this.recordingTitle = "dialog.liveStreaming";
278
             this.recordingTitle = "dialog.liveStreaming";

+ 57
- 15
modules/UI/ring_overlay/RingOverlay.js 查看文件

1
-/* global $ */
1
+/* global $, APP */
2
 /* jshint -W101 */
2
 /* jshint -W101 */
3
+import UIEvents from "../../../service/UI/UIEvents";
4
+
5
+/**
6
+ * Store the current ring overlay instance.
7
+ * Note: We want to have only 1 instance at a time.
8
+ */
9
+let overlay = null;
10
+
11
+/**
12
+ * Handler for UIEvents.LARGE_VIDEO_AVATAR_DISPLAYED event.
13
+ * @param {boolean} shown indicates whether the avatar on the large video is
14
+ *  currently displayed or not.
15
+ */
16
+function onAvatarDisplayed(shown) {
17
+    overlay._changeBackground(shown);
18
+}
3
 
19
 
4
 /**
20
 /**
5
  * Shows ring overlay
21
  * Shows ring overlay
11
     constructor(callee) {
27
     constructor(callee) {
12
         this._containerId = 'ringOverlay';
28
         this._containerId = 'ringOverlay';
13
         this._audioContainerId = 'ringOverlayRinging';
29
         this._audioContainerId = 'ringOverlayRinging';
14
-
30
+        this.isRinging = true;
15
         this.callee = callee;
31
         this.callee = callee;
16
         this.render();
32
         this.render();
17
         this.audio = document.getElementById(this._audioContainerId);
33
         this.audio = document.getElementById(this._audioContainerId);
18
         this.audio.play();
34
         this.audio.play();
19
         this._setAudioTimeout();
35
         this._setAudioTimeout();
36
+        this._timeout = setTimeout(() => {
37
+            this.destroy();
38
+            this.render();
39
+        }, 30000);
40
+    }
41
+
42
+    /**
43
+     * Chagnes the background of the ring overlay.
44
+     * @param {boolean} solid - if true the new background will be the solid
45
+     * one, otherwise the background will be default one.
46
+     * NOTE: The method just toggles solidBG css class.
47
+     */
48
+    _changeBackground(solid) {
49
+        const container = $("#" + this._containerId);
50
+        if(solid) {
51
+            container.addClass("solidBG");
52
+        } else {
53
+            container.removeClass("solidBG");
54
+        }
20
     }
55
     }
21
 
56
 
22
     /**
57
     /**
23
      * Builds and appends the ring overlay to the html document
58
      * Builds and appends the ring overlay to the html document
24
      */
59
      */
25
     _getHtmlStr(callee) {
60
     _getHtmlStr(callee) {
61
+        let callingLabel = this.isRinging? "<p>Calling...</p>" : "";
62
+        let callerStateLabel =  this.isRinging? "" : " isn't available";
26
         return `
63
         return `
27
             <div id="${this._containerId}" class='ringing' >
64
             <div id="${this._containerId}" class='ringing' >
28
                 <div class='ringing__content'>
65
                 <div class='ringing__content'>
29
-                    <p>Calling...</p>
66
+                    ${callingLabel}
30
                     <img class='ringing__avatar' src="${callee.getAvatarUrl()}" />
67
                     <img class='ringing__avatar' src="${callee.getAvatarUrl()}" />
31
                     <div class="ringing__caller-info">
68
                     <div class="ringing__caller-info">
32
-                        <p>${callee.getName()}</p>
69
+                        <p>${callee.getName()}${callerStateLabel}</p>
33
                     </div>
70
                     </div>
34
                 </div>
71
                 </div>
35
-                <audio id="${this._audioContainerId}" src="/sounds/ring.ogg" />
72
+                <audio id="${this._audioContainerId}" src="./sounds/ring.ogg" />
36
             </div>`;
73
             </div>`;
37
     }
74
     }
38
 
75
 
49
      * related to the ring overlay.
86
      * related to the ring overlay.
50
      */
87
      */
51
     destroy() {
88
     destroy() {
52
-        if (this.interval) {
53
-            clearInterval(this.interval);
54
-        }
55
-
89
+        this._stopAudio();
56
         this._detach();
90
         this._detach();
57
     }
91
     }
58
 
92
 
64
         $(`#${this._containerId}`).remove();
98
         $(`#${this._containerId}`).remove();
65
     }
99
     }
66
 
100
 
101
+    _stopAudio() {
102
+        this.isRinging = false;
103
+        if (this.interval) {
104
+            clearInterval(this.interval);
105
+        }
106
+        if(this._timeout) {
107
+            clearTimeout(this._timeout);
108
+        }
109
+    }
110
+
67
     /**
111
     /**
68
      * Sets the interval that is going to play the ringing sound.
112
      * Sets the interval that is going to play the ringing sound.
69
      */
113
      */
74
     }
118
     }
75
 }
119
 }
76
 
120
 
77
-/**
78
- * Store the current ring overlay instance.
79
- * Note: We want to have only 1 instance at a time.
80
- */
81
-let overlay = null;
82
-
83
 export default {
121
 export default {
84
     /**
122
     /**
85
      * Shows the ring overlay for the passed callee.
123
      * Shows the ring overlay for the passed callee.
92
         }
130
         }
93
 
131
 
94
         overlay = new RingOverlay(callee);
132
         overlay = new RingOverlay(callee);
133
+        APP.UI.addListener(UIEvents.LARGE_VIDEO_AVATAR_DISPLAYED,
134
+            onAvatarDisplayed);
95
     },
135
     },
96
 
136
 
97
     /**
137
     /**
104
         }
144
         }
105
         overlay.destroy();
145
         overlay.destroy();
106
         overlay = null;
146
         overlay = null;
147
+        APP.UI.removeListener(UIEvents.LARGE_VIDEO_AVATAR_DISPLAYED,
148
+            onAvatarDisplayed);
107
         return true;
149
         return true;
108
     },
150
     },
109
 
151
 

+ 1
- 5
modules/UI/shared_video/SharedVideo.js 查看文件

243
 
243
 
244
             let thumb = new SharedVideoThumb(self.url);
244
             let thumb = new SharedVideoThumb(self.url);
245
             thumb.setDisplayName(player.getVideoData().title);
245
             thumb.setDisplayName(player.getVideoData().title);
246
-            VideoLayout.addParticipantContainer(self.url, thumb);
246
+            VideoLayout.addRemoteVideoContainer(self.url, thumb);
247
 
247
 
248
             let iframe = player.getIframe();
248
             let iframe = player.getIframe();
249
             self.sharedVideo = new SharedVideoContainer(
249
             self.sharedVideo = new SharedVideoContainer(
567
         this.player = player;
567
         this.player = player;
568
     }
568
     }
569
 
569
 
570
-    get $video () {
571
-        return this.$iframe;
572
-    }
573
-
574
     show () {
570
     show () {
575
         let self = this;
571
         let self = this;
576
         return new Promise(resolve => {
572
         return new Promise(resolve => {

+ 40
- 31
modules/UI/side_pannels/chat/Chat.js 查看文件

3
 import {processReplacements, linkify} from './Replacement';
3
 import {processReplacements, linkify} from './Replacement';
4
 import CommandsProcessor from './Commands';
4
 import CommandsProcessor from './Commands';
5
 import ToolbarToggler from '../../toolbars/ToolbarToggler';
5
 import ToolbarToggler from '../../toolbars/ToolbarToggler';
6
+import VideoLayout from "../../videolayout/VideoLayout";
6
 
7
 
7
 import UIUtil from '../../util/UIUtil';
8
 import UIUtil from '../../util/UIUtil';
8
 import UIEvents from '../../../../service/UI/UIEvents';
9
 import UIEvents from '../../../../service/UI/UIEvents';
9
 
10
 
10
-var smileys = require("./smileys.json").smileys;
11
+import { smileys } from './smileys';
11
 
12
 
12
-var notificationInterval = false;
13
 var unreadMessages = 0;
13
 var unreadMessages = 0;
14
 
14
 
15
+/**
16
+ * The container id, which is and the element id.
17
+ */
18
+var CHAT_CONTAINER_ID = "chat_container";
15
 
19
 
16
 /**
20
 /**
17
- * Shows/hides a visual notification, indicating that a message has arrived.
21
+ *  Updates visual notification, indicating that a message has arrived.
18
  */
22
  */
19
-function setVisualNotification(show) {
23
+function updateVisualNotification() {
20
     var unreadMsgElement = document.getElementById('unreadMessages');
24
     var unreadMsgElement = document.getElementById('unreadMessages');
21
 
25
 
22
-    var glower = $('#toolbar_button_chat');
23
-
24
     if (unreadMessages) {
26
     if (unreadMessages) {
25
         unreadMsgElement.innerHTML = unreadMessages.toString();
27
         unreadMsgElement.innerHTML = unreadMessages.toString();
26
 
28
 
37
             'style',
39
             'style',
38
                 'top:' + topIndent +
40
                 'top:' + topIndent +
39
                 '; left:' + leftIndent + ';');
41
                 '; left:' + leftIndent + ';');
40
-
41
-        if (!glower.hasClass('icon-chat-simple')) {
42
-            glower.removeClass('icon-chat');
43
-            glower.addClass('icon-chat-simple');
44
-        }
45
     }
42
     }
46
     else {
43
     else {
47
         unreadMsgElement.innerHTML = '';
44
         unreadMsgElement.innerHTML = '';
48
-        glower.removeClass('icon-chat-simple');
49
-        glower.addClass('icon-chat');
50
     }
45
     }
51
 
46
 
52
-    if (show && !notificationInterval) {
53
-        notificationInterval = window.setInterval(function () {
54
-            glower.toggleClass('active');
55
-        }, 800);
56
-    }
57
-    else if (!show && notificationInterval) {
58
-        window.clearInterval(notificationInterval);
59
-        notificationInterval = false;
60
-        glower.removeClass('active');
61
-    }
47
+    $(unreadMsgElement).parent()[unreadMessages > 0 ? 'show' : 'hide']();
62
 }
48
 }
63
 
49
 
64
 
50
 
131
  */
117
  */
132
 function resizeChatConversation() {
118
 function resizeChatConversation() {
133
     var msgareaHeight = $('#usermsg').outerHeight();
119
     var msgareaHeight = $('#usermsg').outerHeight();
134
-    var chatspace = $('#chat_container');
120
+    var chatspace = $('#' + CHAT_CONTAINER_ID);
135
     var width = chatspace.width();
121
     var width = chatspace.width();
136
     var chat = $('#chatconversation');
122
     var chat = $('#chatconversation');
137
     var smileys = $('#smileysarea');
123
     var smileys = $('#smileysarea');
187
         };
173
         };
188
         usermsg.autosize({callback: onTextAreaResize});
174
         usermsg.autosize({callback: onTextAreaResize});
189
 
175
 
190
-        $("#chat_container").bind("shown",
191
-            function () {
176
+        eventEmitter.on(UIEvents.SIDE_TOOLBAR_CONTAINER_TOGGLED,
177
+            function(containerId, isVisible) {
178
+                if (containerId !== CHAT_CONTAINER_ID || !isVisible)
179
+                    return;
180
+
192
                 unreadMessages = 0;
181
                 unreadMessages = 0;
193
-                setVisualNotification(false);
182
+                updateVisualNotification();
183
+
184
+                // Undock the toolbar when the chat is shown and if we're in a
185
+                // video mode.
186
+                if (VideoLayout.isLargeVideoVisible()) {
187
+                    ToolbarToggler.dockToolbar(false);
188
+                }
189
+
190
+                // if we are in conversation mode focus on the text input
191
+                // if we are not, focus on the display name input
192
+                if (APP.settings.getDisplayName())
193
+                    $('#usermsg').focus();
194
+                else
195
+                    $('#nickinput').focus();
194
             });
196
             });
195
 
197
 
196
         addSmileys();
198
         addSmileys();
199
+        updateVisualNotification();
197
     },
200
     },
198
 
201
 
199
     /**
202
     /**
210
             if (!Chat.isVisible()) {
213
             if (!Chat.isVisible()) {
211
                 unreadMessages++;
214
                 unreadMessages++;
212
                 UIUtil.playSoundNotification('chatNotification');
215
                 UIUtil.playSoundNotification('chatNotification');
213
-                setVisualNotification(true);
216
+                updateVisualNotification();
214
             }
217
             }
215
         }
218
         }
216
 
219
 
271
 
274
 
272
     /**
275
     /**
273
      * Sets the chat conversation mode.
276
      * Sets the chat conversation mode.
277
+     * Conversation mode is the normal chat mode, non conversation mode is
278
+     * where we ask user to input its display name.
274
      * @param {boolean} isConversationMode if chat should be in
279
      * @param {boolean} isConversationMode if chat should be in
275
      * conversation mode or not.
280
      * conversation mode or not.
276
      */
281
      */
277
     setChatConversationMode (isConversationMode) {
282
     setChatConversationMode (isConversationMode) {
278
-        $('#chat_container')
283
+        $('#' + CHAT_CONTAINER_ID)
279
             .toggleClass('is-conversation-mode', isConversationMode);
284
             .toggleClass('is-conversation-mode', isConversationMode);
285
+
286
+        // this is needed when we transition from no conversation mode to
287
+        // conversation mode. When user enters his nickname and hits enter,
288
+        // to focus on the write area.
280
         if (isConversationMode) {
289
         if (isConversationMode) {
281
             $('#usermsg').focus();
290
             $('#usermsg').focus();
282
         }
291
         }
286
      * Resizes the chat area.
295
      * Resizes the chat area.
287
      */
296
      */
288
     resizeChat (width, height) {
297
     resizeChat (width, height) {
289
-        $('#chat_container').width(width).height(height);
298
+        $('#' + CHAT_CONTAINER_ID).width(width).height(height);
290
 
299
 
291
         resizeChatConversation();
300
         resizeChatConversation();
292
     },
301
     },
296
      */
305
      */
297
     isVisible () {
306
     isVisible () {
298
         return UIUtil.isVisible(
307
         return UIUtil.isVisible(
299
-            document.getElementById("chat_container"));
308
+            document.getElementById(CHAT_CONTAINER_ID));
300
     },
309
     },
301
     /**
310
     /**
302
      * Shows and hides the window with the smileys
311
      * Shows and hides the window with the smileys

+ 5
- 6
modules/UI/side_pannels/chat/Replacement.js 查看文件

1
 /* jshint -W101 */
1
 /* jshint -W101 */
2
-var Smileys = require("./smileys.json");
2
+import { regexes } from './smileys';
3
 
3
 
4
 /**
4
 /**
5
  * Processes links and smileys in "body"
5
  * Processes links and smileys in "body"
29
     replacePattern2 = /(^|[^\/])(www\.[\S]+(\b|$))/gim;
29
     replacePattern2 = /(^|[^\/])(www\.[\S]+(\b|$))/gim;
30
     replacedText = replacedText.replace(replacePattern2, '$1<a href="http://$2" target="_blank">$2</a>');
30
     replacedText = replacedText.replace(replacePattern2, '$1<a href="http://$2" target="_blank">$2</a>');
31
 
31
 
32
-    //Change email addresses to mailto:: links.
32
+    //Change email addresses to mailto: links.
33
     replacePattern3 = /(([a-zA-Z0-9\-\_\.])+@[a-zA-Z\_]+?(\.[a-zA-Z]{2,6})+)/gim;
33
     replacePattern3 = /(([a-zA-Z0-9\-\_\.])+@[a-zA-Z\_]+?(\.[a-zA-Z]{2,6})+)/gim;
34
     replacedText = replacedText.replace(replacePattern3, '<a href="mailto:$1">$1</a>');
34
     replacedText = replacedText.replace(replacePattern3, '<a href="mailto:$1">$1</a>');
35
 
35
 
44
         return body;
44
         return body;
45
     }
45
     }
46
 
46
 
47
-    var regexs = Smileys.regexs;
48
-    for(var smiley in regexs) {
49
-        if(regexs.hasOwnProperty(smiley)) {
50
-            body = body.replace(regexs[smiley],
47
+    for(var smiley in regexes) {
48
+        if(regexes.hasOwnProperty(smiley)) {
49
+            body = body.replace(regexes[smiley],
51
                     '<img class="smiley" src="images/smileys/' + smiley + '.svg">');
50
                     '<img class="smiley" src="images/smileys/' + smiley + '.svg">');
52
         }
51
         }
53
     }
52
     }

+ 47
- 0
modules/UI/side_pannels/chat/smileys.js 查看文件

1
+export const smileys = {
2
+    smiley1: ":)",
3
+    smiley2: ":(",
4
+    smiley3: ":D",
5
+    smiley4: "(y)",
6
+    smiley5: " :P",
7
+    smiley6: "(wave)",
8
+    smiley7: "(blush)",
9
+    smiley8: "(chuckle)",
10
+    smiley9: "(shocked)",
11
+    smiley10: ":*",
12
+    smiley11: "(n)",
13
+    smiley12: "(search)",
14
+    smiley13: " <3",
15
+    smiley14: "(oops)",
16
+    smiley15: "(angry)",
17
+    smiley16: "(angel)",
18
+    smiley17: "(sick)",
19
+    smiley18: ";(",
20
+    smiley19: "(bomb)",
21
+    smiley20: "(clap)",
22
+    smiley21: " ;)"
23
+};
24
+
25
+export const regexes = {
26
+    smiley2: /(:-\(\(|:-\(|:\(\(|:\(|\(sad\))/gi,
27
+    smiley3: /(:-\)\)|:\)\)|\(lol\)|:-D|:D)/gi,
28
+    smiley1: /(:-\)|:\))/gi,
29
+    smiley4: /(\(y\)|\(Y\)|\(ok\))/gi,
30
+    smiley5: /(:-P|:P|:-p|:p)/gi,
31
+    smiley6: /(\(wave\))/gi,
32
+    smiley7: /(\(blush\))/gi,
33
+    smiley8: /(\(chuckle\))/gi,
34
+    smiley9: /(:-0|\(shocked\))/gi,
35
+    smiley10: /(:-\*|:\*|\(kiss\))/gi,
36
+    smiley11: /(\(n\))/gi,
37
+    smiley12: /(\(search\))/g,
38
+    smiley13: /(<3|&lt;3|&amp;lt;3|\(L\)|\(l\)|\(H\)|\(h\))/gi,
39
+    smiley14: /(\(oops\))/gi,
40
+    smiley15: /(\(angry\))/gi,
41
+    smiley16: /(\(angel\))/gi,
42
+    smiley17: /(\(sick\))/gi,
43
+    smiley18: /(;-\(\(|;\(\(|;-\(|;\(|:"\(|:"-\(|:~-\(|:~\(|\(upset\))/gi,
44
+    smiley19: /(\(bomb\))/gi,
45
+    smiley20: /(\(clap\))/gi,
46
+    smiley21: /(;-\)|;\)|;-\)\)|;\)\)|;-D|;D|\(wink\))/gi
47
+};

+ 0
- 48
modules/UI/side_pannels/chat/smileys.json 查看文件

1
-{
2
-    "smileys": {
3
-        "smiley1": ":)",
4
-        "smiley2": ":(",
5
-        "smiley3": ":D",
6
-        "smiley4": "(y)",
7
-        "smiley5": " :P",
8
-        "smiley6": "(wave)",
9
-        "smiley7": "(blush)",
10
-        "smiley8": "(chuckle)",
11
-        "smiley9": "(shocked)",
12
-        "smiley10": ":*",
13
-        "smiley11": "(n)",
14
-        "smiley12": "(search)",
15
-        "smiley13": " <3",
16
-        "smiley14": "(oops)",
17
-        "smiley15": "(angry)",
18
-        "smiley16": "(angel)",
19
-        "smiley17": "(sick)",
20
-        "smiley18": ";(",
21
-        "smiley19": "(bomb)",
22
-        "smiley20": "(clap)",
23
-        "smiley21": " ;)"
24
-    },
25
-    "regexs": {
26
-        "smiley2": /(:-\(\(|:-\(|:\(\(|:\(|\(sad\))/gi,
27
-        "smiley3": /(:-\)\)|:\)\)|\(lol\)|:-D|:D)/gi,
28
-        "smiley1": /(:-\)|:\))/gi,
29
-        "smiley4": /(\(y\)|\(Y\)|\(ok\))/gi,
30
-        "smiley5": /(:-P|:P|:-p|:p)/gi,
31
-        "smiley6": /(\(wave\))/gi,
32
-        "smiley7": /(\(blush\))/gi,
33
-        "smiley8": /(\(chuckle\))/gi,
34
-        "smiley9": /(:-0|\(shocked\))/gi,
35
-        "smiley10": /(:-\*|:\*|\(kiss\))/gi,
36
-        "smiley11": /(\(n\))/gi,
37
-        "smiley12": /(\(search\))/g,
38
-        "smiley13": /(<3|&lt;3|&amp;lt;3|\(L\)|\(l\)|\(H\)|\(h\))/gi,
39
-        "smiley14": /(\(oops\))/gi,
40
-        "smiley15": /(\(angry\))/gi,
41
-        "smiley16": /(\(angel\))/gi,
42
-        "smiley17": /(\(sick\))/gi,
43
-        "smiley18": /(;-\(\(|;\(\(|;-\(|;\(|:"\(|:"-\(|:~-\(|:~\(|\(upset\))/gi,
44
-        "smiley19": /(\(bomb\))/gi,
45
-        "smiley20": /(\(clap\))/gi,
46
-        "smiley21": /(;-\)|;\)|;-\)\)|;\)\)|;-D|;D|\(wink\))/gi
47
-    }
48
-}

+ 10
- 18
modules/UI/side_pannels/contactlist/ContactList.js 查看文件

20
         return;
20
         return;
21
     }
21
     }
22
 
22
 
23
-    let buttonIndicatorText = (numberOfContacts === 1) ? '' : numberOfContacts;
24
-    $("#numberOfParticipants").text(buttonIndicatorText);
23
+    $("#numberOfParticipants").text(numberOfContacts);
25
 
24
 
26
     $("#contacts_container>div.title").text(
25
     $("#contacts_container>div.title").text(
27
-        APP.translation.translateString(
28
-            "contactlist", {participants: numberOfContacts}
29
-        ));
26
+        APP.translation.translateString("contactlist")
27
+            + ' (' + numberOfContacts + ')');
30
 }
28
 }
31
 
29
 
32
 /**
30
 /**
59
     return p;
57
     return p;
60
 }
58
 }
61
 
59
 
62
-
63
-function stopGlowing(glower) {
64
-    window.clearInterval(notificationInterval);
65
-    notificationInterval = false;
66
-    glower.removeClass('glowing');
67
-    if (!ContactList.isVisible()) {
68
-        glower.removeClass('active');
69
-    }
70
-}
71
-
72
 function getContactEl (id) {
60
 function getContactEl (id) {
73
     return $(`#contacts>li[id="${id}"]`);
61
     return $(`#contacts>li[id="${id}"]`);
74
 }
62
 }
96
 
84
 
97
     /**
85
     /**
98
      * Adds a contact for the given id.
86
      * Adds a contact for the given id.
99
-     *
87
+     * @param isLocal is an id for the local user.
100
      */
88
      */
101
-    addContact (id) {
89
+    addContact (id, isLocal) {
102
         let contactlist = $('#contacts');
90
         let contactlist = $('#contacts');
103
 
91
 
104
         let newContact = document.createElement('li');
92
         let newContact = document.createElement('li');
112
 
100
 
113
         if (interfaceConfig.SHOW_CONTACTLIST_AVATARS)
101
         if (interfaceConfig.SHOW_CONTACTLIST_AVATARS)
114
             newContact.appendChild(createAvatar(id));
102
             newContact.appendChild(createAvatar(id));
115
-        newContact.appendChild(createDisplayNameParagraph("participant"));
103
+
104
+        newContact.appendChild(
105
+            createDisplayNameParagraph(
106
+                isLocal ? interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME : null,
107
+                isLocal ? null : interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME));
116
 
108
 
117
         if (APP.conference.isLocalId(id)) {
109
         if (APP.conference.isLocalId(id)) {
118
             contactlist.prepend(newContact);
110
             contactlist.prepend(newContact);

+ 2
- 6
modules/UI/side_pannels/settings/SettingsMenu.js 查看文件

74
                     }
74
                     }
75
                 });
75
                 });
76
 
76
 
77
-            // Only show the subtitle if this isn't the only setting section.
78
-            if (interfaceConfig.SETTINGS_SECTIONS.length > 1)
79
-                UIUtil.showElement("deviceOptionsTitle");
80
-
77
+            UIUtil.showElement("deviceOptionsTitle");
81
             UIUtil.showElement("devicesOptions");
78
             UIUtil.showElement("devicesOptions");
82
         }
79
         }
83
 
80
 
150
     showStartMutedOptions (show) {
147
     showStartMutedOptions (show) {
151
         if (show && UIUtil.isSettingEnabled('moderator')) {
148
         if (show && UIUtil.isSettingEnabled('moderator')) {
152
             // Only show the subtitle if this isn't the only setting section.
149
             // Only show the subtitle if this isn't the only setting section.
153
-            if (!$("#moderatorOptionsTitle").is(":visible")
154
-                && interfaceConfig.SETTINGS_SECTIONS.length > 1)
150
+            if (!$("#moderatorOptionsTitle").is(":visible"))
155
                 UIUtil.showElement("moderatorOptionsTitle");
151
                 UIUtil.showElement("moderatorOptionsTitle");
156
 
152
 
157
             UIUtil.showElement("startMutedOptions");
153
             UIUtil.showElement("startMutedOptions");

+ 67
- 19
modules/UI/toolbars/Toolbar.js 查看文件

7
 let roomUrl = null;
7
 let roomUrl = null;
8
 let emitter = null;
8
 let emitter = null;
9
 
9
 
10
-
11
 /**
10
 /**
12
  * Opens the invite link dialog.
11
  * Opens the invite link dialog.
13
  */
12
  */
21
         inviteAttributes = "value=\"" + encodeURI(roomUrl) + "\"";
20
         inviteAttributes = "value=\"" + encodeURI(roomUrl) + "\"";
22
     }
21
     }
23
 
22
 
23
+    let inviteLinkId = "inviteLinkRef";
24
+    let focusInviteLink = function() {
25
+        $('#' + inviteLinkId).focus();
26
+        $('#' + inviteLinkId).select();
27
+    };
28
+
24
     let title = APP.translation.generateTranslationHTML("dialog.shareLink");
29
     let title = APP.translation.generateTranslationHTML("dialog.shareLink");
25
     APP.UI.messageHandler.openTwoButtonDialog(
30
     APP.UI.messageHandler.openTwoButtonDialog(
26
-        null, null, null,
27
-        '<h2>' + title + '</h2>'
28
-        + '<input id="inviteLinkRef" type="text" '
29
-        + inviteAttributes + ' onclick="this.select();" readonly>',
30
-        false, "dialog.Invite",
31
+        null, title, null,
32
+        '<input id="' + inviteLinkId + '" type="text" '
33
+            + inviteAttributes + ' readonly/>',
34
+        false, "dialog.copy",
31
         function (e, v) {
35
         function (e, v) {
32
             if (v && roomUrl) {
36
             if (v && roomUrl) {
33
                 JitsiMeetJS.analytics.sendEvent('toolbar.invite.button');
37
                 JitsiMeetJS.analytics.sendEvent('toolbar.invite.button');
34
-                emitter.emit(UIEvents.USER_INVITED, roomUrl);
38
+
39
+                focusInviteLink();
40
+
41
+                document.execCommand('copy');
35
             }
42
             }
36
             else {
43
             else {
37
                 JitsiMeetJS.analytics.sendEvent('toolbar.invite.cancel');
44
                 JitsiMeetJS.analytics.sendEvent('toolbar.invite.cancel');
38
             }
45
             }
39
         },
46
         },
40
         function (event) {
47
         function (event) {
41
-            if (roomUrl) {
42
-                document.getElementById('inviteLinkRef').select();
43
-            } else {
48
+            if (!roomUrl) {
44
                 if (event && event.target) {
49
                 if (event && event.target) {
45
                     $(event.target).find('button[value=true]')
50
                     $(event.target).find('button[value=true]')
46
                         .prop('disabled', true);
51
                         .prop('disabled', true);
47
                 }
52
                 }
48
             }
53
             }
54
+            else {
55
+                focusInviteLink();
56
+            }
49
         },
57
         },
50
         function (e, v, m, f) {
58
         function (e, v, m, f) {
51
             if(!v && !m && !f)
59
             if(!v && !m && !f)
52
                 JitsiMeetJS.analytics.sendEvent('toolbar.invite.close');
60
                 JitsiMeetJS.analytics.sendEvent('toolbar.invite.close');
53
-        }
61
+        },
62
+        'Copy' // Focus Copy button.
54
     );
63
     );
55
 }
64
 }
56
 
65
 
181
 const defaultToolbarButtons = {
190
 const defaultToolbarButtons = {
182
     'microphone': {
191
     'microphone': {
183
         id: 'toolbar_button_mute',
192
         id: 'toolbar_button_mute',
193
+        tooltipKey: 'toolbar.mute',
184
         className: "button icon-microphone",
194
         className: "button icon-microphone",
185
         shortcut: 'M',
195
         shortcut: 'M',
186
         shortcutAttr: 'mutePopover',
196
         shortcutAttr: 'mutePopover',
211
     },
221
     },
212
     'camera': {
222
     'camera': {
213
         id: 'toolbar_button_camera',
223
         id: 'toolbar_button_camera',
224
+        tooltipKey: 'toolbar.videomute',
214
         className: "button icon-camera",
225
         className: "button icon-camera",
215
         shortcut: 'V',
226
         shortcut: 'V',
216
         shortcutAttr: 'toggleVideoPopover',
227
         shortcutAttr: 'toggleVideoPopover',
224
     },
235
     },
225
     'desktop': {
236
     'desktop': {
226
         id: 'toolbar_button_desktopsharing',
237
         id: 'toolbar_button_desktopsharing',
238
+        tooltipKey: 'toolbar.sharescreen',
227
         className: 'button icon-share-desktop',
239
         className: 'button icon-share-desktop',
228
         shortcut: 'D',
240
         shortcut: 'D',
229
         shortcutAttr: 'toggleDesktopSharingPopover',
241
         shortcutAttr: 'toggleDesktopSharingPopover',
236
         i18n: '[content]toolbar.sharescreen'
248
         i18n: '[content]toolbar.sharescreen'
237
     },
249
     },
238
     'security': {
250
     'security': {
239
-        id: 'toolbar_button_security'
251
+        id: 'toolbar_button_security',
252
+        tooltipKey: 'toolbar.lock'
240
     },
253
     },
241
     'invite': {
254
     'invite': {
242
         id: 'toolbar_button_link',
255
         id: 'toolbar_button_link',
256
+        tooltipKey: 'toolbar.invite',
243
         className: 'button icon-link',
257
         className: 'button icon-link',
244
         content: 'Invite others',
258
         content: 'Invite others',
245
         i18n: '[content]toolbar.invite'
259
         i18n: '[content]toolbar.invite'
246
     },
260
     },
247
     'chat': {
261
     'chat': {
248
         id: 'toolbar_button_chat',
262
         id: 'toolbar_button_chat',
263
+        tooltipKey: 'toolbar.chat',
249
         shortcut: 'C',
264
         shortcut: 'C',
250
         shortcutAttr: 'toggleChatPopover',
265
         shortcutAttr: 'toggleChatPopover',
251
         shortcutFunc: function() {
266
         shortcutFunc: function() {
257
     },
272
     },
258
     'contacts': {
273
     'contacts': {
259
         id: 'toolbar_contact_list',
274
         id: 'toolbar_contact_list',
275
+        tooltipKey: 'bottomtoolbar.contactlist',
260
         sideContainerId: 'contacts_container'
276
         sideContainerId: 'contacts_container'
261
     },
277
     },
262
     'profile': {
278
     'profile': {
263
         id: 'toolbar_button_profile',
279
         id: 'toolbar_button_profile',
280
+        tooltipKey: 'profile.setDisplayNameLabel',
264
         sideContainerId: 'profile_container'
281
         sideContainerId: 'profile_container'
265
     },
282
     },
266
     'etherpad': {
283
     'etherpad': {
267
-        id: 'toolbar_button_etherpad'
284
+        id: 'toolbar_button_etherpad',
285
+        tooltipKey: 'toolbar.etherpad',
268
     },
286
     },
269
     'fullscreen': {
287
     'fullscreen': {
270
         id: 'toolbar_button_fullScreen',
288
         id: 'toolbar_button_fullScreen',
289
+        tooltipKey: 'toolbar.fullscreen',
271
         className: "button icon-full-screen",
290
         className: "button icon-full-screen",
272
-        shortcut: 'F',
291
+        shortcut: 'S',
273
         shortcutAttr: 'toggleFullscreenPopover',
292
         shortcutAttr: 'toggleFullscreenPopover',
274
         shortcutFunc: function() {
293
         shortcutFunc: function() {
275
             JitsiMeetJS.analytics.sendEvent('shortcut.fullscreen.toggled');
294
             JitsiMeetJS.analytics.sendEvent('shortcut.fullscreen.toggled');
276
             APP.UI.toggleFullScreen();
295
             APP.UI.toggleFullScreen();
277
         },
296
         },
278
-        shortcutDescription: "keyboardShortcuts.toggleChat",
297
+        shortcutDescription: "keyboardShortcuts.fullScreen",
279
         content: "Enter / Exit Full Screen",
298
         content: "Enter / Exit Full Screen",
280
         i18n: "[content]toolbar.fullscreen"
299
         i18n: "[content]toolbar.fullscreen"
281
     },
300
     },
282
     'settings': {
301
     'settings': {
283
         id: 'toolbar_button_settings',
302
         id: 'toolbar_button_settings',
303
+        tooltipKey: 'toolbar.Settings',
284
         sideContainerId: "settings_container"
304
         sideContainerId: "settings_container"
285
     },
305
     },
286
     'hangup': {
306
     'hangup': {
287
         id: 'toolbar_button_hangup',
307
         id: 'toolbar_button_hangup',
308
+        tooltipKey: 'toolbar.hangup',
288
         className: "button icon-hangup",
309
         className: "button icon-hangup",
289
         content: "Hang Up",
310
         content: "Hang Up",
290
         i18n: "[content]toolbar.hangup"
311
         i18n: "[content]toolbar.hangup"
291
     },
312
     },
292
     'filmstrip': {
313
     'filmstrip': {
293
         id: 'toolbar_film_strip',
314
         id: 'toolbar_film_strip',
315
+        tooltipKey: 'toolbar.filmstrip',
294
         shortcut: "F",
316
         shortcut: "F",
295
         shortcutAttr: "filmstripPopover",
317
         shortcutAttr: "filmstripPopover",
296
         shortcutFunc: function() {
318
         shortcutFunc: function() {
301
     },
323
     },
302
     'raisehand': {
324
     'raisehand': {
303
         id: "toolbar_button_raisehand",
325
         id: "toolbar_button_raisehand",
326
+        tooltipKey: 'toolbar.raiseHand',
304
         className: "button icon-raised-hand",
327
         className: "button icon-raised-hand",
305
         shortcut: "R",
328
         shortcut: "R",
306
         shortcutAttr: "raiseHandPopover",
329
         shortcutAttr: "raiseHandPopover",
357
         Object.keys(defaultToolbarButtons).forEach(
380
         Object.keys(defaultToolbarButtons).forEach(
358
             id => {
381
             id => {
359
                 if (UIUtil.isButtonEnabled(id)) {
382
                 if (UIUtil.isButtonEnabled(id)) {
360
-                    var button = defaultToolbarButtons[id];
383
+                    let button = defaultToolbarButtons[id];
384
+                    let buttonElement = document.getElementById(button.id);
385
+
386
+                    let tooltipPosition
387
+                        = (interfaceConfig.MAIN_TOOLBAR_BUTTONS
388
+                                .indexOf(id) > -1)
389
+                            ? "bottom" : "right";
390
+
391
+                    UIUtil.setTooltip(  buttonElement,
392
+                                        button.tooltipKey,
393
+                                        tooltipPosition);
361
 
394
 
362
                     if (button.shortcut)
395
                     if (button.shortcut)
363
                         APP.keyboardshortcut.registerShortcut(
396
                         APP.keyboardshortcut.registerShortcut(
382
                                                             isVisible);
415
                                                             isVisible);
383
             });
416
             });
384
 
417
 
418
+        APP.UI.addListener(UIEvents.LOCAL_RAISE_HAND_CHANGED,
419
+            function(isRaisedHand) {
420
+                Toolbar._toggleRaiseHand(isRaisedHand);
421
+            });
422
+
385
         if(!APP.tokenData.isGuest) {
423
         if(!APP.tokenData.isGuest) {
386
             $("#toolbar_button_profile").addClass("unclickable");
424
             $("#toolbar_button_profile").addClass("unclickable");
425
+            UIUtil.removeTooltip(
426
+                document.getElementById('toolbar_button_profile'));
387
         }
427
         }
388
     },
428
     },
389
     /**
429
     /**
458
 
498
 
459
     // Shows or hides the 'shared video' button.
499
     // Shows or hides the 'shared video' button.
460
     showSharedVideoButton () {
500
     showSharedVideoButton () {
501
+        let $element = $('#toolbar_button_sharedvideo');
461
         if (UIUtil.isButtonEnabled('sharedvideo')
502
         if (UIUtil.isButtonEnabled('sharedvideo')
462
                 && config.disableThirdPartyRequests !== true) {
503
                 && config.disableThirdPartyRequests !== true) {
463
-            $('#toolbar_button_sharedvideo').css({display: "inline-block"});
504
+            $element.css({display: "inline-block"});
505
+            UIUtil.setTooltip($element.get(0), 'toolbar.sharedvideo', 'right');
464
         } else {
506
         } else {
465
             $('#toolbar_button_sharedvideo').css({display: "none"});
507
             $('#toolbar_button_sharedvideo').css({display: "none"});
466
         }
508
         }
545
         }
587
         }
546
     },
588
     },
547
 
589
 
590
+    /**
591
+     * Toggles / untoggles the view for raised hand.
592
+     */
593
+    _toggleRaiseHand(isRaisedHand) {
594
+        $('#toolbar_button_raisehand').toggleClass("glow", isRaisedHand);
595
+    },
596
+
548
     /**
597
     /**
549
      * Marks video icon as muted or not.
598
      * Marks video icon as muted or not.
550
      * @param {boolean} muted if icon should look like muted or not
599
      * @param {boolean} muted if icon should look like muted or not
750
             buttonElement.setAttribute("data-i18n", button.i18n);
799
             buttonElement.setAttribute("data-i18n", button.i18n);
751
 
800
 
752
         buttonElement.setAttribute("data-container", "body");
801
         buttonElement.setAttribute("data-container", "body");
753
-        buttonElement.setAttribute("data-toggle", "popover");
754
         buttonElement.setAttribute("data-placement", "bottom");
802
         buttonElement.setAttribute("data-placement", "bottom");
755
         this._addPopups(buttonElement, button.popups);
803
         this._addPopups(buttonElement, button.popups);
756
 
804
 
771
     }
819
     }
772
 };
820
 };
773
 
821
 
774
-export default Toolbar;
822
+export default Toolbar;

+ 100
- 6
modules/UI/util/UIUtil.js 查看文件

1
-/* global $, config, interfaceConfig */
1
+/* global $, APP, config, AJS, interfaceConfig */
2
+
3
+import KeyboardShortcut from '../../keyboardshortcut/keyboardshortcut';
4
+
5
+/**
6
+ * Associates tooltip element position (in the terms of
7
+ * {@link UIUtil#setTooltip} which do not look like CSS <tt>position</tt>) with
8
+ * AUI tooltip <tt>gravity</tt>.
9
+ */
10
+const TOOLTIP_POSITIONS = {
11
+    'bottom': 'n',
12
+    'bottom-left': 'ne',
13
+    'bottom-right': 'nw',
14
+    'left': 'e',
15
+    'right': 'w',
16
+    'top': 's',
17
+    'top-left': 'se',
18
+    'top-right': 'sw'
19
+};
2
 
20
 
3
 /**
21
 /**
4
  * Created by hristo on 12/22/14.
22
  * Created by hristo on 12/22/14.
82
         context.putImageData(imgData, 0, 0);
100
         context.putImageData(imgData, 0, 0);
83
     },
101
     },
84
 
102
 
103
+    /**
104
+     * Sets a global handler for all tooltips. Once invoked, create a new
105
+     * tooltip by merely updating a DOM node with the appropriate class (e.g.
106
+     * <tt>tooltip-n</tt>) and the attribute <tt>content</tt>.
107
+     */
108
+    activateTooltips() {
109
+        AJS.$('[data-tooltip]').tooltip({
110
+            gravity() {
111
+                return this.getAttribute('data-tooltip');
112
+            },
113
+
114
+            title() {
115
+                return this.getAttribute('content');
116
+            },
117
+
118
+            html: true, // Handle multiline tooltips.
119
+
120
+            // The following two prevent tooltips from being stuck:
121
+            hoverable: false, // Make custom tooltips behave like native ones.
122
+            live: true // Attach listener to document element.
123
+        });
124
+    },
125
+
126
+    /**
127
+     * Sets the tooltip to the given element.
128
+     *
129
+     * @param element the element to set the tooltip to
130
+     * @param key the tooltip data-i18n key
131
+     * @param position the position of the tooltip in relation to the element
132
+     */
85
     setTooltip: function (element, key, position) {
133
     setTooltip: function (element, key, position) {
86
-        element.setAttribute("data-i18n", "[data-content]" + key);
87
-        element.setAttribute("data-toggle", "popover");
88
-        element.setAttribute("data-placement", position);
89
-        element.setAttribute("data-html", true);
90
-        element.setAttribute("data-container", "body");
134
+        element.setAttribute('data-tooltip', TOOLTIP_POSITIONS[position]);
135
+        element.setAttribute('data-i18n', '[content]' + key);
136
+
137
+        APP.translation.translateElement($(element));
138
+    },
139
+
140
+    /**
141
+     * Removes the tooltip to the given element.
142
+     *
143
+     * @param element the element to remove the tooltip from
144
+     */
145
+    removeTooltip: function (element) {
146
+        AJS.$(element).tooltip('destroy');
147
+        element.setAttribute('data-tooltip', '');
148
+        element.setAttribute('data-i18n','');
149
+        element.setAttribute('content','');
150
+        element.setAttribute('shortcut','');
151
+    },
152
+
153
+    /**
154
+     * Internal util function for generating tooltip title.
155
+     *
156
+     * @param element
157
+     * @returns {string|*}
158
+     * @private
159
+     */
160
+    _getTooltipText: function (element) {
161
+        let title = element.getAttribute('content');
162
+        let shortcut = element.getAttribute('shortcut');
163
+        if(shortcut) {
164
+            let shortcutString = KeyboardShortcut.getShortcutTooltip(shortcut);
165
+            title += ` ${shortcutString}`;
166
+        }
167
+        return title;
91
     },
168
     },
92
 
169
 
93
     /**
170
     /**
233
      */
310
      */
234
     parseCssInt(cssValue) {
311
     parseCssInt(cssValue) {
235
         return parseInt(cssValue) || 0;
312
         return parseInt(cssValue) || 0;
313
+    },
314
+
315
+    /**
316
+     * Adds href value to 'a' link jquery object. If link value is null,
317
+     * undefined or empty string, disables the link.
318
+     * @param {object} aLinkElement the jquery object
319
+     * @param {string} link the link value
320
+     */
321
+    setLinkHref(aLinkElement, link) {
322
+        if (link) {
323
+            aLinkElement.attr('href', link);
324
+        } else {
325
+            aLinkElement.css({
326
+                "pointer-events": "none",
327
+                "cursor": "default"
328
+            });
329
+        }
236
     }
330
     }
237
 };
331
 };
238
 
332
 

+ 37
- 12
modules/UI/videolayout/ConnectionIndicator.js 查看文件

245
 };
245
 };
246
 
246
 
247
 
247
 
248
-function createIcon(classes) {
248
+function createIcon(classes, iconClass) {
249
     var icon = document.createElement("span");
249
     var icon = document.createElement("span");
250
     for(var i in classes) {
250
     for(var i in classes) {
251
         icon.classList.add(classes[i]);
251
         icon.classList.add(classes[i]);
252
     }
252
     }
253
     icon.appendChild(
253
     icon.appendChild(
254
-        document.createElement("i")).classList.add("icon-connection");
254
+        document.createElement("i")).classList.add(iconClass);
255
     return icon;
255
     return icon;
256
 }
256
 }
257
 
257
 
282
     }.bind(this);
282
     }.bind(this);
283
 
283
 
284
     this.emptyIcon = this.connectionIndicatorContainer.appendChild(
284
     this.emptyIcon = this.connectionIndicatorContainer.appendChild(
285
-        createIcon(["connection", "connection_empty"]));
285
+        createIcon(["connection", "connection_empty"], "icon-connection"));
286
     this.fullIcon = this.connectionIndicatorContainer.appendChild(
286
     this.fullIcon = this.connectionIndicatorContainer.appendChild(
287
-        createIcon(["connection", "connection_full"]));
287
+        createIcon(["connection", "connection_full"], "icon-connection"));
288
+    this.interruptedIndicator = this.connectionIndicatorContainer.appendChild(
289
+        createIcon(["connection", "connection_lost"],"icon-connection-lost"));
290
+    $(this.interruptedIndicator).hide();
288
 };
291
 };
289
 
292
 
290
 /**
293
 /**
298
     this.popover.forceHide();
301
     this.popover.forceHide();
299
 };
302
 };
300
 
303
 
304
+/**
305
+ * Updates the UI which displays warning about user's connectivity problems.
306
+ *
307
+ * @param {boolean} isActive true if the connection is working fine or false if
308
+ * the user is having connectivity issues.
309
+ */
310
+ConnectionIndicator.prototype.updateConnectionStatusIndicator
311
+= function (isActive) {
312
+    this.isConnectionActive = isActive;
313
+    if (this.isConnectionActive) {
314
+        $(this.interruptedIndicator).hide();
315
+        $(this.emptyIcon).show();
316
+        $(this.fullIcon).show();
317
+    } else {
318
+        $(this.interruptedIndicator).show();
319
+        $(this.emptyIcon).hide();
320
+        $(this.fullIcon).hide();
321
+        this.updateConnectionQuality(0 /* zero bars */);
322
+    }
323
+};
324
+
301
 /**
325
 /**
302
  * Updates the data of the indicator
326
  * Updates the data of the indicator
303
  * @param percent the percent of connection quality
327
  * @param percent the percent of connection quality
312
     } else {
336
     } else {
313
         if(this.connectionIndicatorContainer.style.display == "none") {
337
         if(this.connectionIndicatorContainer.style.display == "none") {
314
             this.connectionIndicatorContainer.style.display = "block";
338
             this.connectionIndicatorContainer.style.display = "block";
315
-            this.videoContainer.updateIconPositions();
316
         }
339
         }
317
     }
340
     }
318
-    this.bandwidth = object.bandwidth;
319
-    this.bitrate = object.bitrate;
320
-    this.packetLoss = object.packetLoss;
321
-    this.transport = object.transport;
322
-    if (object.resolution) {
323
-        this.resolution = object.resolution;
341
+    if (object) {
342
+        this.bandwidth = object.bandwidth;
343
+        this.bitrate = object.bitrate;
344
+        this.packetLoss = object.packetLoss;
345
+        this.transport = object.transport;
346
+        if (object.resolution) {
347
+            this.resolution = object.resolution;
348
+        }
324
     }
349
     }
325
     for (var quality in ConnectionIndicator.connectionQualityValues) {
350
     for (var quality in ConnectionIndicator.connectionQualityValues) {
326
         if (percent >= quality) {
351
         if (percent >= quality) {
328
                 ConnectionIndicator.connectionQualityValues[quality];
353
                 ConnectionIndicator.connectionQualityValues[quality];
329
         }
354
         }
330
     }
355
     }
331
-    if (object.isResolutionHD) {
356
+    if (object && typeof object.isResolutionHD === 'boolean') {
332
         this.isResolutionHD = object.isResolutionHD;
357
         this.isResolutionHD = object.isResolutionHD;
333
     }
358
     }
334
     this.updateResolutionIndicator();
359
     this.updateResolutionIndicator();

+ 138
- 42
modules/UI/videolayout/FilmStrip.js 查看文件

3
 import UIEvents from "../../../service/UI/UIEvents";
3
 import UIEvents from "../../../service/UI/UIEvents";
4
 import UIUtil from "../util/UIUtil";
4
 import UIUtil from "../util/UIUtil";
5
 
5
 
6
-const thumbAspectRatio = 1 / 1;
7
-
8
 const FilmStrip = {
6
 const FilmStrip = {
9
     /**
7
     /**
10
      *
8
      *
26
      */
24
      */
27
     toggleFilmStrip (visible) {
25
     toggleFilmStrip (visible) {
28
         if (typeof visible === 'boolean'
26
         if (typeof visible === 'boolean'
29
-                && this.isFilmStripVisible() == visible) {
27
+            && this.isFilmStripVisible() == visible) {
30
             return;
28
             return;
31
         }
29
         }
32
 
30
 
36
         var eventEmitter = this.eventEmitter;
34
         var eventEmitter = this.eventEmitter;
37
         if (eventEmitter) {
35
         if (eventEmitter) {
38
             eventEmitter.emit(
36
             eventEmitter.emit(
39
-                    UIEvents.TOGGLED_FILM_STRIP,
40
-                    this.isFilmStripVisible());
37
+                UIEvents.TOGGLED_FILM_STRIP,
38
+                this.isFilmStripVisible());
41
         }
39
         }
42
     },
40
     },
43
 
41
 
66
             - parseInt(this.filmStrip.css('paddingRight'), 10);
64
             - parseInt(this.filmStrip.css('paddingRight'), 10);
67
     },
65
     },
68
 
66
 
67
+    calculateThumbnailSize() {
68
+        let availableSizes = this.calculateAvailableSize();
69
+        let width = availableSizes.availableWidth;
70
+        let height = availableSizes.availableHeight;
71
+
72
+        return this.calculateThumbnailSizeFromAvailable(width, height);
73
+    },
74
+
69
     /**
75
     /**
70
-     * Calculates the thumbnail size.
76
+     * Normalizes local and remote thumbnail ratios
71
      */
77
      */
72
-     calculateThumbnailSize () {
73
-        let availableHeight = interfaceConfig.FILM_STRIP_MAX_HEIGHT;
78
+    normalizeThumbnailRatio () {
79
+        let remoteHeightRatio = interfaceConfig.REMOTE_THUMBNAIL_RATIO_HEIGHT;
80
+        let remoteWidthRatio = interfaceConfig.REMOTE_THUMBNAIL_RATIO_WIDTH;
81
+
82
+        let localHeightRatio = interfaceConfig.LOCAL_THUMBNAIL_RATIO_HEIGHT;
83
+        let localWidthRatio = interfaceConfig.LOCAL_THUMBNAIL_RATIO_WIDTH;
84
+
85
+        let commonHeightRatio = remoteHeightRatio * localHeightRatio;
74
 
86
 
75
-        let numvids = this.getThumbs(true).length;
87
+        let localRatioCoefficient = localWidthRatio / localHeightRatio;
88
+        let remoteRatioCoefficient = remoteWidthRatio / remoteHeightRatio;
89
+
90
+        remoteWidthRatio = commonHeightRatio * remoteRatioCoefficient;
91
+        remoteHeightRatio = commonHeightRatio;
92
+
93
+        localWidthRatio = commonHeightRatio * localRatioCoefficient;
94
+        localHeightRatio = commonHeightRatio;
95
+
96
+        let localRatio = {
97
+            widthRatio: localWidthRatio,
98
+            heightRatio: localHeightRatio
99
+        };
100
+
101
+        let remoteRatio = {
102
+            widthRatio: remoteWidthRatio,
103
+            heightRatio: remoteHeightRatio
104
+        };
105
+
106
+        return { localRatio, remoteRatio };
107
+    },
108
+
109
+    calculateAvailableSize() {
110
+        let availableHeight = interfaceConfig.FILM_STRIP_MAX_HEIGHT;
111
+        let thumbs = this.getThumbs(true);
112
+        let numvids = thumbs.remoteThumbs.length;
76
 
113
 
77
         let localVideoContainer = $("#localVideoContainer");
114
         let localVideoContainer = $("#localVideoContainer");
78
 
115
 
83
          */
120
          */
84
         let videoAreaAvailableWidth
121
         let videoAreaAvailableWidth
85
             = UIUtil.getAvailableVideoWidth()
122
             = UIUtil.getAvailableVideoWidth()
86
-                - UIUtil.parseCssInt(this.filmStrip.css('right'), 10)
87
-                - UIUtil.parseCssInt(this.filmStrip.css('paddingLeft'), 10)
88
-                - UIUtil.parseCssInt(this.filmStrip.css('paddingRight'), 10)
89
-                - UIUtil.parseCssInt(this.filmStrip.css('borderLeftWidth'), 10)
90
-                - UIUtil.parseCssInt(this.filmStrip.css('borderRightWidth'), 10)
123
+            - UIUtil.parseCssInt(this.filmStrip.css('right'), 10)
124
+            - UIUtil.parseCssInt(this.filmStrip.css('paddingLeft'), 10)
125
+            - UIUtil.parseCssInt(this.filmStrip.css('paddingRight'), 10)
126
+            - UIUtil.parseCssInt(this.filmStrip.css('borderLeftWidth'), 10)
127
+            - UIUtil.parseCssInt(this.filmStrip.css('borderRightWidth'), 10)
91
             - 5;
128
             - 5;
92
 
129
 
93
         let availableWidth = videoAreaAvailableWidth;
130
         let availableWidth = videoAreaAvailableWidth;
94
 
131
 
95
-        // If the number of videos is 0 or undefined we don't need to calculate
96
-        // further.
97
-        if (numvids)
132
+        // If local thumb is not hidden
133
+        if(thumbs.localThumb) {
98
             availableWidth = Math.floor(
134
             availableWidth = Math.floor(
99
-                (videoAreaAvailableWidth - numvids * (
135
+                (videoAreaAvailableWidth - (
100
                 UIUtil.parseCssInt(
136
                 UIUtil.parseCssInt(
101
                     localVideoContainer.css('borderLeftWidth'), 10)
137
                     localVideoContainer.css('borderLeftWidth'), 10)
102
                 + UIUtil.parseCssInt(
138
                 + UIUtil.parseCssInt(
109
                     localVideoContainer.css('marginLeft'), 10)
145
                     localVideoContainer.css('marginLeft'), 10)
110
                 + UIUtil.parseCssInt(
146
                 + UIUtil.parseCssInt(
111
                     localVideoContainer.css('marginRight'), 10)))
147
                     localVideoContainer.css('marginRight'), 10)))
112
-                / numvids);
148
+            );
149
+        }
150
+
151
+        // If the number of videos is 0 or undefined we don't need to calculate
152
+        // further.
153
+        if (numvids) {
154
+            let remoteVideoContainer = thumbs.remoteThumbs.eq(0);
155
+            availableWidth = Math.floor(
156
+                (videoAreaAvailableWidth - numvids * (
157
+                UIUtil.parseCssInt(
158
+                    remoteVideoContainer.css('borderLeftWidth'), 10)
159
+                + UIUtil.parseCssInt(
160
+                    remoteVideoContainer.css('borderRightWidth'), 10)
161
+                + UIUtil.parseCssInt(
162
+                    remoteVideoContainer.css('paddingLeft'), 10)
163
+                + UIUtil.parseCssInt(
164
+                    remoteVideoContainer.css('paddingRight'), 10)
165
+                + UIUtil.parseCssInt(
166
+                    remoteVideoContainer.css('marginLeft'), 10)
167
+                + UIUtil.parseCssInt(
168
+                    remoteVideoContainer.css('marginRight'), 10)))
169
+            );
170
+        }
113
 
171
 
114
         let maxHeight
172
         let maxHeight
115
             // If the MAX_HEIGHT property hasn't been specified
173
             // If the MAX_HEIGHT property hasn't been specified
116
             // we have the static value.
174
             // we have the static value.
117
-            = Math.min( interfaceConfig.FILM_STRIP_MAX_HEIGHT || 120,
118
-                        availableHeight);
175
+            = Math.min(interfaceConfig.FILM_STRIP_MAX_HEIGHT || 120,
176
+            availableHeight);
119
 
177
 
120
         availableHeight
178
         availableHeight
121
-            = Math.min( maxHeight, window.innerHeight - 18);
179
+            = Math.min(maxHeight, window.innerHeight - 18);
180
+
181
+        return { availableWidth, availableHeight };
182
+    },
183
+
184
+    calculateThumbnailSizeFromAvailable(availableWidth, availableHeight) {
185
+        let { localRatio, remoteRatio } = this.normalizeThumbnailRatio();
186
+        let { remoteThumbs } = this.getThumbs(true);
187
+        let remoteProportion = remoteRatio.widthRatio * remoteThumbs.length;
188
+        let widthProportion = remoteProportion + localRatio.widthRatio;
189
+
190
+        let heightUnit = availableHeight / localRatio.heightRatio;
191
+        let widthUnit = availableWidth / widthProportion;
122
 
192
 
123
-        if (availableHeight < availableWidth) {
124
-            availableWidth = availableHeight;
193
+        if (heightUnit < widthUnit) {
194
+            widthUnit = heightUnit;
125
         }
195
         }
126
         else
196
         else
127
-            availableHeight = availableWidth;
197
+            heightUnit = widthUnit;
198
+
199
+        let localVideo = {
200
+            thumbWidth: widthUnit * localRatio.widthRatio,
201
+            thumbHeight: heightUnit * localRatio.heightRatio
202
+        };
203
+        let remoteVideo = {
204
+            thumbWidth: widthUnit * remoteRatio.widthRatio,
205
+            thumbHeight: widthUnit * remoteRatio.heightRatio
206
+        };
128
 
207
 
129
         return {
208
         return {
130
-            thumbWidth: availableWidth,
131
-            thumbHeight: availableHeight
209
+            localVideo,
210
+            remoteVideo
132
         };
211
         };
133
     },
212
     },
134
 
213
 
135
-    resizeThumbnails (thumbWidth, thumbHeight,
214
+    resizeThumbnails (local, remote,
136
                       animate = false, forceUpdate = false) {
215
                       animate = false, forceUpdate = false) {
137
 
216
 
138
         return new Promise(resolve => {
217
         return new Promise(resolve => {
139
-            this.getThumbs(!forceUpdate).animate({
140
-                height: thumbHeight,
141
-                width: thumbWidth
142
-            }, {
143
-                queue: false,
144
-                duration: animate ? 500 : 0,
145
-                complete:  resolve
146
-            });
218
+            let thumbs = this.getThumbs(!forceUpdate);
219
+            if(thumbs.localThumb)
220
+                thumbs.localThumb.animate({
221
+                    height: local.thumbHeight,
222
+                    width: local.thumbWidth
223
+                }, {
224
+                    queue: false,
225
+                    duration: animate ? 500 : 0,
226
+                    complete:  resolve
227
+                });
228
+            if(thumbs.remoteThumbs)
229
+                thumbs.remoteThumbs.animate({
230
+                    height: remote.thumbHeight,
231
+                    width: remote.thumbWidth
232
+                }, {
233
+                    queue: false,
234
+                    duration: animate ? 500 : 0,
235
+                    complete:  resolve
236
+                });
147
 
237
 
148
             this.filmStrip.animate({
238
             this.filmStrip.animate({
149
                 // adds 2 px because of small video 1px border
239
                 // adds 2 px because of small video 1px border
150
-                height: thumbHeight + 2
240
+                height: remote.thumbHeight + 2
151
             }, {
241
             }, {
152
                 queue: false,
242
                 queue: false,
153
                 duration: animate ? 500 : 0
243
                 duration: animate ? 500 : 0
165
             selector += ':visible';
255
             selector += ':visible';
166
         }
256
         }
167
 
257
 
258
+        let localThumb = $("#localVideoContainer");
259
+        let remoteThumbs = this.filmStrip.children(selector)
260
+            .not("#localVideoContainer");
261
+
168
         // Exclude the local video container if it has been hidden.
262
         // Exclude the local video container if it has been hidden.
169
-        if ($("#localVideoContainer").hasClass("hidden"))
170
-            return this.filmStrip.children(selector)
171
-                    .not("#localVideoContainer");
172
-        else
173
-            return this.filmStrip.children(selector);
263
+        if (localThumb.hasClass("hidden")) {
264
+            return { remoteThumbs };
265
+        } else {
266
+            return { remoteThumbs, localThumb };
267
+        }
268
+
174
     }
269
     }
270
+
175
 };
271
 };
176
 
272
 
177
 export default FilmStrip;
273
 export default FilmStrip;

+ 501
- 0
modules/UI/videolayout/LargeVideoManager.js 查看文件

1
+/* global $, APP, interfaceConfig */
2
+/* jshint -W101 */
3
+
4
+import Avatar from "../avatar/Avatar";
5
+import {createDeferred} from '../../util/helpers';
6
+import UIUtil from "../util/UIUtil";
7
+import {VideoContainer, VIDEO_CONTAINER_TYPE} from "./VideoContainer";
8
+
9
+import LargeContainer from "./LargeContainer";
10
+
11
+import AudioLevels from "../audio_levels/AudioLevels";
12
+
13
+/**
14
+ * Manager for all Large containers.
15
+ */
16
+export default class LargeVideoManager {
17
+    constructor (emitter) {
18
+        /**
19
+         * The map of <tt>LargeContainer</tt>s where the key is the video
20
+         * container type.
21
+         * @type {Object.<string, LargeContainer>}
22
+         */
23
+        this.containers = {};
24
+
25
+        this.state = VIDEO_CONTAINER_TYPE;
26
+        this.videoContainer = new VideoContainer(
27
+            () => this.resizeContainer(VIDEO_CONTAINER_TYPE), emitter);
28
+        this.addContainer(VIDEO_CONTAINER_TYPE, this.videoContainer);
29
+
30
+        // use the same video container to handle and desktop tracks
31
+        this.addContainer("desktop", this.videoContainer);
32
+
33
+        this.width = 0;
34
+        this.height = 0;
35
+
36
+        this.$container = $('#largeVideoContainer');
37
+
38
+        this.$container.css({
39
+            display: 'inline-block'
40
+        });
41
+
42
+        if (interfaceConfig.SHOW_JITSI_WATERMARK) {
43
+            let leftWatermarkDiv
44
+                = this.$container.find("div.watermark.leftwatermark");
45
+
46
+            leftWatermarkDiv.css({display: 'block'});
47
+
48
+            UIUtil.setLinkHref(
49
+                leftWatermarkDiv.parent(),
50
+                interfaceConfig.JITSI_WATERMARK_LINK);
51
+        }
52
+
53
+        if (interfaceConfig.SHOW_BRAND_WATERMARK) {
54
+            let rightWatermarkDiv
55
+                = this.$container.find("div.watermark.rightwatermark");
56
+
57
+            rightWatermarkDiv.css({
58
+                display: 'block',
59
+                backgroundImage: 'url(images/rightwatermark.png)'
60
+            });
61
+
62
+            UIUtil.setLinkHref(
63
+                rightWatermarkDiv.parent(),
64
+                interfaceConfig.BRAND_WATERMARK_LINK);
65
+        }
66
+
67
+        if (interfaceConfig.SHOW_POWERED_BY) {
68
+            this.$container.children("a.poweredby").css({display: 'block'});
69
+        }
70
+
71
+        this.$container.hover(
72
+            e => this.onHoverIn(e),
73
+            e => this.onHoverOut(e)
74
+        );
75
+    }
76
+
77
+    onHoverIn (e) {
78
+        if (!this.state) {
79
+            return;
80
+        }
81
+        let container = this.getContainer(this.state);
82
+        container.onHoverIn(e);
83
+    }
84
+
85
+    onHoverOut (e) {
86
+        if (!this.state) {
87
+            return;
88
+        }
89
+        let container = this.getContainer(this.state);
90
+        container.onHoverOut(e);
91
+    }
92
+
93
+    /**
94
+     * Called when the media connection has been interrupted.
95
+     */
96
+    onVideoInterrupted () {
97
+        this.enableLocalConnectionProblemFilter(true);
98
+        this._setLocalConnectionMessage("connection.RECONNECTING");
99
+        // Show the message only if the video is currently being displayed
100
+        this.showLocalConnectionMessage(this.state === VIDEO_CONTAINER_TYPE);
101
+    }
102
+
103
+    /**
104
+     * Called when the media connection has been restored.
105
+     */
106
+    onVideoRestored () {
107
+        this.enableLocalConnectionProblemFilter(false);
108
+        this.showLocalConnectionMessage(false);
109
+    }
110
+
111
+    get id () {
112
+        let container = this.getContainer(this.state);
113
+        return container.id;
114
+    }
115
+
116
+    scheduleLargeVideoUpdate () {
117
+        if (this.updateInProcess || !this.newStreamData) {
118
+            return;
119
+        }
120
+
121
+        this.updateInProcess = true;
122
+
123
+        let container = this.getContainer(this.state);
124
+
125
+        // Include hide()/fadeOut only if we're switching between users
126
+        let preUpdate;
127
+        let isUserSwitch = this.newStreamData.id != this.id;
128
+        if (isUserSwitch) {
129
+            preUpdate = container.hide();
130
+        } else {
131
+            preUpdate = Promise.resolve();
132
+        }
133
+
134
+        preUpdate.then(() => {
135
+            let {id, stream, videoType, resolve} = this.newStreamData;
136
+            this.newStreamData = null;
137
+
138
+            console.info("hover in %s", id);
139
+            this.state = videoType;
140
+            let container = this.getContainer(this.state);
141
+            container.setStream(stream, videoType);
142
+
143
+            // change the avatar url on large
144
+            this.updateAvatar(Avatar.getAvatarUrl(id));
145
+
146
+            // FIXME that does not really make sense, because the videoType
147
+            // (camera or desktop) is a completely different thing than
148
+            // the video container type (Etherpad, SharedVideo, VideoContainer).
149
+            // ----------------------------------------------------------------
150
+            // If we the continer is VIDEO_CONTAINER_TYPE, we need to check
151
+            // its stream whether exist and is muted to set isVideoMuted
152
+            // in rest of the cases it is false
153
+            let showAvatar = false;
154
+            if (videoType == VIDEO_CONTAINER_TYPE)
155
+                showAvatar = stream ? stream.isMuted() : true;
156
+
157
+            // If the user's connection is disrupted then the avatar will be
158
+            // displayed in case we have no video image cached. That is if
159
+            // there was a user switch(image is lost on stream detach) or if
160
+            // the video was not rendered, before the connection has failed.
161
+            let isHavingConnectivityIssues
162
+                = APP.conference.isParticipantConnectionActive(id) === false;
163
+            if (isHavingConnectivityIssues
164
+                    && (isUserSwitch | !container.wasVideoRendered)) {
165
+                showAvatar = true;
166
+            }
167
+
168
+            let promise;
169
+
170
+            // do not show stream if video is muted
171
+            // but we still should show watermark
172
+            if (showAvatar) {
173
+                this.showWatermark(true);
174
+                // If the intention of this switch is to show the avatar
175
+                // we need to make sure that the video is hidden
176
+                promise = container.hide();
177
+            } else {
178
+                promise = container.show();
179
+            }
180
+
181
+            // show the avatar on large if needed
182
+            container.showAvatar(showAvatar);
183
+
184
+            // Make sure no notification about remote failure is shown as
185
+            // it's UI conflicts with the one for local connection interrupted.
186
+            if (APP.conference.isConnectionInterrupted()) {
187
+                this.updateParticipantConnStatusIndication(id, true);
188
+            } else {
189
+                this.updateParticipantConnStatusIndication(
190
+                    id, !isHavingConnectivityIssues);
191
+            }
192
+
193
+            // resolve updateLargeVideo promise after everything is done
194
+            promise.then(resolve);
195
+
196
+            return promise;
197
+        }).then(() => {
198
+            // after everything is done check again if there are any pending
199
+            // new streams.
200
+            this.updateInProcess = false;
201
+            this.scheduleLargeVideoUpdate();
202
+        });
203
+    }
204
+
205
+    /**
206
+     * Shows/hides notification about participant's connectivity issues to be
207
+     * shown on the large video area.
208
+     *
209
+     * @param {string} id the id of remote participant(MUC nickname)
210
+     * @param {boolean} isConnected true if the connection is active or false
211
+     * when the user is having connectivity issues.
212
+     *
213
+     * @private
214
+     */
215
+    updateParticipantConnStatusIndication (id, isConnected) {
216
+
217
+        // Apply grey filter on the large video
218
+        this.videoContainer.showRemoteConnectionProblemIndicator(!isConnected);
219
+
220
+        if (isConnected) {
221
+            // Hide the message
222
+            this.showRemoteConnectionMessage(false);
223
+        } else {
224
+            // Get user's display name
225
+            let displayName
226
+                = APP.conference.getParticipantDisplayName(id);
227
+            this._setRemoteConnectionMessage(
228
+                "connection.USER_CONNECTION_INTERRUPTED",
229
+                { displayName: displayName });
230
+
231
+            // Show it now only if the VideoContainer is on top
232
+            this.showRemoteConnectionMessage(
233
+                this.state === VIDEO_CONTAINER_TYPE);
234
+        }
235
+    }
236
+
237
+    /**
238
+     * Update large video.
239
+     * Switches to large video even if previously other container was visible.
240
+     * @param userID the userID of the participant associated with the stream
241
+     * @param {JitsiTrack?} stream new stream
242
+     * @param {string?} videoType new video type
243
+     * @returns {Promise}
244
+     */
245
+    updateLargeVideo (userID, stream, videoType) {
246
+        if (this.newStreamData) {
247
+            this.newStreamData.reject();
248
+        }
249
+
250
+        this.newStreamData = createDeferred();
251
+        this.newStreamData.id = userID;
252
+        this.newStreamData.stream = stream;
253
+        this.newStreamData.videoType = videoType;
254
+
255
+        this.scheduleLargeVideoUpdate();
256
+
257
+        return this.newStreamData.promise;
258
+    }
259
+
260
+    /**
261
+     * Update container size.
262
+     */
263
+    updateContainerSize () {
264
+        this.width = UIUtil.getAvailableVideoWidth();
265
+        this.height = window.innerHeight;
266
+    }
267
+
268
+    /**
269
+     * Resize Large container of specified type.
270
+     * @param {string} type type of container which should be resized.
271
+     * @param {boolean} [animate=false] if resize process should be animated.
272
+     */
273
+    resizeContainer (type, animate = false) {
274
+        let container = this.getContainer(type);
275
+        container.resize(this.width, this.height, animate);
276
+    }
277
+
278
+    /**
279
+     * Resize all Large containers.
280
+     * @param {boolean} animate if resize process should be animated.
281
+     */
282
+    resize (animate) {
283
+        // resize all containers
284
+        Object.keys(this.containers)
285
+            .forEach(type => this.resizeContainer(type, animate));
286
+
287
+        this.$container.animate({
288
+            width: this.width,
289
+            height: this.height
290
+        }, {
291
+            queue: false,
292
+            duration: animate ? 500 : 0
293
+        });
294
+    }
295
+
296
+    /**
297
+     * Enables/disables the filter indicating a video problem to the user caused
298
+     * by the problems with local media connection.
299
+     *
300
+     * @param enable <tt>true</tt> to enable, <tt>false</tt> to disable
301
+     */
302
+    enableLocalConnectionProblemFilter (enable) {
303
+        this.videoContainer.enableLocalConnectionProblemFilter(enable);
304
+    }
305
+
306
+    /**
307
+     * Updates the src of the dominant speaker avatar
308
+     */
309
+    updateAvatar (avatarUrl) {
310
+        $("#dominantSpeakerAvatar").attr('src', avatarUrl);
311
+    }
312
+
313
+    /**
314
+     * Updates the audio level indicator of the large video.
315
+     *
316
+     * @param lvl the new audio level to set
317
+     */
318
+    updateLargeVideoAudioLevel (lvl) {
319
+        AudioLevels.updateLargeVideoAudioLevel("dominantSpeaker", lvl);
320
+    }
321
+
322
+    /**
323
+     * Show or hide watermark.
324
+     * @param {boolean} show
325
+     */
326
+    showWatermark (show) {
327
+        $('.watermark').css('visibility', show ? 'visible' : 'hidden');
328
+    }
329
+
330
+    /**
331
+     * Shows/hides the message indicating problems with local media connection.
332
+     * @param {boolean|null} show(optional) tells whether the message is to be
333
+     * displayed or not. If missing the condition will be based on the value
334
+     * obtained from {@link APP.conference.isConnectionInterrupted}.
335
+     */
336
+    showLocalConnectionMessage (show) {
337
+        if (typeof show !== 'boolean') {
338
+            show = APP.conference.isConnectionInterrupted();
339
+        }
340
+
341
+        if (show) {
342
+            $('#localConnectionMessage').css({display: "block"});
343
+            // Avatar message conflicts with 'videoConnectionMessage',
344
+            // so it must be hidden
345
+            this.showRemoteConnectionMessage(false);
346
+        } else {
347
+            $('#localConnectionMessage').css({display: "none"});
348
+        }
349
+    }
350
+
351
+    /**
352
+     * Shows hides the "avatar" message which is to be displayed either in
353
+     * the middle of the screen or below the avatar image.
354
+     *
355
+     * @param {null|boolean} show (optional) <tt>true</tt> to show the avatar
356
+     * message or <tt>false</tt> to hide it. If not provided then the connection
357
+     * status of the user currently on the large video will be obtained form
358
+     * "APP.conference" and the message will be displayed if the user's
359
+     * connection is interrupted.
360
+     */
361
+    showRemoteConnectionMessage (show) {
362
+        if (typeof show !== 'boolean') {
363
+            show = APP.conference.isParticipantConnectionActive(this.id);
364
+        }
365
+
366
+        if (show) {
367
+            $('#remoteConnectionMessage').css({display: "block"});
368
+            // 'videoConnectionMessage' message conflicts with 'avatarMessage',
369
+            // so it must be hidden
370
+            this.showLocalConnectionMessage(false);
371
+        } else {
372
+            $('#remoteConnectionMessage').hide();
373
+        }
374
+    }
375
+
376
+    /**
377
+     * Updates the text which describes that the remote user is having
378
+     * connectivity issues.
379
+     *
380
+     * @param {string} msgKey the translation key which will be used to get
381
+     * the message text.
382
+     * @param {object} msgOptions translation options object.
383
+     *
384
+     * @private
385
+     */
386
+    _setRemoteConnectionMessage (msgKey, msgOptions) {
387
+        if (msgKey) {
388
+            let text = APP.translation.translateString(msgKey, msgOptions);
389
+            $('#remoteConnectionMessage')
390
+                .attr("data-i18n", msgKey).text(text);
391
+        }
392
+
393
+        this.videoContainer.positionRemoteConnectionMessage();
394
+    }
395
+
396
+    /**
397
+     * Updated the text which is to be shown on the top of large video, when
398
+     * local media connection is interrupted.
399
+     *
400
+     * @param {string} msgKey the translation key which will be used to get
401
+     * the message text to be displayed on the large video.
402
+     * @param {object} msgOptions translation options object
403
+     *
404
+     * @private
405
+     */
406
+    _setLocalConnectionMessage (msgKey, msgOptions) {
407
+        $('#localConnectionMessage')
408
+            .attr("data-i18n", msgKey)
409
+            .text(APP.translation.translateString(msgKey, msgOptions));
410
+    }
411
+
412
+    /**
413
+     * Add container of specified type.
414
+     * @param {string} type container type
415
+     * @param {LargeContainer} container container to add.
416
+     */
417
+    addContainer (type, container) {
418
+        if (this.containers[type]) {
419
+            throw new Error(`container of type ${type} already exist`);
420
+        }
421
+
422
+        this.containers[type] = container;
423
+        this.resizeContainer(type);
424
+    }
425
+
426
+    /**
427
+     * Get Large container of specified type.
428
+     * @param {string} type container type.
429
+     * @returns {LargeContainer}
430
+     */
431
+    getContainer (type) {
432
+        let container = this.containers[type];
433
+
434
+        if (!container) {
435
+            throw new Error(`container of type ${type} doesn't exist`);
436
+        }
437
+
438
+        return container;
439
+    }
440
+
441
+    /**
442
+     * Remove Large container of specified type.
443
+     * @param {string} type container type.
444
+     */
445
+    removeContainer (type) {
446
+        if (!this.containers[type]) {
447
+            throw new Error(`container of type ${type} doesn't exist`);
448
+        }
449
+
450
+        delete this.containers[type];
451
+    }
452
+
453
+    /**
454
+     * Show Large container of specified type.
455
+     * Does nothing if such container is already visible.
456
+     * @param {string} type container type.
457
+     * @returns {Promise}
458
+     */
459
+    showContainer (type) {
460
+        if (this.state === type) {
461
+            return Promise.resolve();
462
+        }
463
+
464
+        let oldContainer = this.containers[this.state];
465
+        // FIXME when video is being replaced with other content we need to hide
466
+        // companion icons/messages. It would be best if the container would
467
+        // be taking care of it by itself, but that is a bigger refactoring
468
+        if (this.state === VIDEO_CONTAINER_TYPE) {
469
+            this.showWatermark(false);
470
+            this.showLocalConnectionMessage(false);
471
+            this.showRemoteConnectionMessage(false);
472
+        }
473
+        oldContainer.hide();
474
+
475
+        this.state = type;
476
+        let container = this.getContainer(type);
477
+
478
+        return container.show().then(() => {
479
+            if (type === VIDEO_CONTAINER_TYPE) {
480
+                // FIXME when video appears on top of other content we need to
481
+                // show companion icons/messages. It would be best if
482
+                // the container would be taking care of it by itself, but that
483
+                // is a bigger refactoring
484
+                this.showWatermark(true);
485
+                // "avatar" and "video connection" can not be displayed both
486
+                // at the same time, but the latter is of higher priority and it
487
+                // will hide the avatar one if will be displayed.
488
+                this.showRemoteConnectionMessage(/* fet the current state */);
489
+                this.showLocalConnectionMessage(/* fetch the current state */);
490
+            }
491
+        });
492
+    }
493
+
494
+    /**
495
+     * Changes the flipX state of the local video.
496
+     * @param val {boolean} true if flipped.
497
+     */
498
+    onLocalFlipXChange(val) {
499
+        this.videoContainer.setLocalFlipX(val);
500
+    }
501
+}

+ 25
- 19
modules/UI/videolayout/LocalVideo.js 查看文件

11
     this.videoSpanId = "localVideoContainer";
11
     this.videoSpanId = "localVideoContainer";
12
     this.container = $("#localVideoContainer").get(0);
12
     this.container = $("#localVideoContainer").get(0);
13
     this.localVideoId = null;
13
     this.localVideoId = null;
14
-    this.bindHoverHandler();
15
     if(config.enableLocalVideoFlip)
14
     if(config.enableLocalVideoFlip)
16
         this._buildContextMenu();
15
         this._buildContextMenu();
17
     this.isLocal = true;
16
     this.isLocal = true;
29
     this.setDisplayName();
28
     this.setDisplayName();
30
 
29
 
31
     this.createConnectionIndicator();
30
     this.createConnectionIndicator();
31
+    this.addAudioLevelIndicator();
32
 }
32
 }
33
 
33
 
34
 LocalVideo.prototype = Object.create(SmallVideo.prototype);
34
 LocalVideo.prototype = Object.create(SmallVideo.prototype);
44
     editButton.className = 'displayname';
44
     editButton.className = 'displayname';
45
     UIUtil.setTooltip(editButton,
45
     UIUtil.setTooltip(editButton,
46
         "videothumbnail.editnickname",
46
         "videothumbnail.editnickname",
47
-        "top");
47
+        "left");
48
     editButton.innerHTML = '<i class="icon-edit"></i>';
48
     editButton.innerHTML = '<i class="icon-edit"></i>';
49
 
49
 
50
     return editButton;
50
     return editButton;
61
         return;
61
         return;
62
     }
62
     }
63
 
63
 
64
-    var nameSpan = $('#' + this.videoSpanId + '>span.displayname');
64
+    var nameSpan = $('#' + this.videoSpanId + ' .displayname');
65
     var defaultLocalDisplayName = APP.translation.generateTranslationHTML(
65
     var defaultLocalDisplayName = APP.translation.generateTranslationHTML(
66
         interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME);
66
         interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME);
67
 
67
 
72
             if (displayName && displayName.length > 0) {
72
             if (displayName && displayName.length > 0) {
73
                 meHTML = APP.translation.generateTranslationHTML("me");
73
                 meHTML = APP.translation.generateTranslationHTML("me");
74
                 $('#localDisplayName').html(
74
                 $('#localDisplayName').html(
75
-                    UIUtil.escapeHtml(displayName) + ' (' + meHTML + ')'
75
+                    `${UIUtil.escapeHtml(displayName)} (${meHTML})`
76
+                );
77
+                $('#editDisplayName').val(
78
+                    `${UIUtil.escapeHtml(displayName)}`
76
                 );
79
                 );
77
             } else {
80
             } else {
78
                 $('#localDisplayName').html(defaultLocalDisplayName);
81
                 $('#localDisplayName').html(defaultLocalDisplayName);
80
         }
83
         }
81
         this.updateView();
84
         this.updateView();
82
     } else {
85
     } else {
83
-        var editButton = createEditDisplayNameButton();
84
-
85
         nameSpan = document.createElement('span');
86
         nameSpan = document.createElement('span');
86
         nameSpan.className = 'displayname';
87
         nameSpan.className = 'displayname';
87
-        $('#' + this.videoSpanId)[0].appendChild(nameSpan);
88
+        document.getElementById(this.videoSpanId)
89
+            .querySelector('.videocontainer__toolbar')
90
+            .appendChild(nameSpan);
88
 
91
 
89
 
92
 
90
         if (displayName && displayName.length > 0) {
93
         if (displayName && displayName.length > 0) {
97
 
100
 
98
 
101
 
99
         nameSpan.id = 'localDisplayName';
102
         nameSpan.id = 'localDisplayName';
100
-        this.container.appendChild(editButton);
101
         //translates popover of edit button
103
         //translates popover of edit button
102
         APP.translation.translateElement($("a.displayname"));
104
         APP.translation.translateElement($("a.displayname"));
103
 
105
 
104
         var editableText = document.createElement('input');
106
         var editableText = document.createElement('input');
105
-        editableText.className = 'displayname';
107
+        editableText.className = 'editdisplayname';
106
         editableText.type = 'text';
108
         editableText.type = 'text';
107
         editableText.id = 'editDisplayName';
109
         editableText.id = 'editDisplayName';
108
 
110
 
119
             JSON.stringify({name: "Jane Pink"}));
121
             JSON.stringify({name: "Jane Pink"}));
120
         editableText.setAttribute("placeholder", defaultNickname);
122
         editableText.setAttribute("placeholder", defaultNickname);
121
 
123
 
122
-        this.container.appendChild(editableText);
124
+        this.container
125
+            .querySelector('.videocontainer__toolbar')
126
+            .appendChild(editableText);
123
 
127
 
124
         var self = this;
128
         var self = this;
125
         $('#localVideoContainer .displayname')
129
         $('#localVideoContainer .displayname')
126
             .bind("click", function (e) {
130
             .bind("click", function (e) {
131
+                let $editDisplayName = $('#editDisplayName');
132
+                let $localDisplayName = $('#localDisplayName');
127
 
133
 
128
-                var editDisplayName = $('#editDisplayName');
129
                 e.preventDefault();
134
                 e.preventDefault();
130
                 e.stopPropagation();
135
                 e.stopPropagation();
131
-                $('#localDisplayName').hide();
132
-                editDisplayName.show();
133
-                editDisplayName.focus();
134
-                editDisplayName.select();
136
+                $localDisplayName.hide();
137
+                $editDisplayName.show();
138
+                $editDisplayName.focus();
139
+                $editDisplayName.select();
135
 
140
 
136
-                editDisplayName.one("focusout", function (e) {
141
+                $editDisplayName.one("focusout", function (e) {
137
                     self.emitter.emit(UIEvents.NICKNAME_CHANGED, this.value);
142
                     self.emitter.emit(UIEvents.NICKNAME_CHANGED, this.value);
138
-                    $('#editDisplayName').hide();
143
+                    $editDisplayName.hide();
144
+                    $localDisplayName.show();
139
                 });
145
                 });
140
 
146
 
141
-                editDisplayName.on('keydown', function (e) {
147
+                $editDisplayName.on('keydown', function (e) {
142
                     if (e.keyCode === 13) {
148
                     if (e.keyCode === 13) {
143
                         e.preventDefault();
149
                         e.preventDefault();
144
                         $('#editDisplayName').hide();
150
                         $('#editDisplayName').hide();
199
         localVideoContainer.removeChild(localVideo);
205
         localVideoContainer.removeChild(localVideo);
200
         // when removing only the video element and we are on stage
206
         // when removing only the video element and we are on stage
201
         // update the stage
207
         // update the stage
202
-        if(this.VideoLayout.isCurrentlyOnLarge(this.id))
208
+        if(this.isCurrentlyOnLargeVideo())
203
             this.VideoLayout.updateLargeVideo(this.id);
209
             this.VideoLayout.updateLargeVideo(this.id);
204
         stream.off(TrackEvents.LOCAL_TRACK_STOPPED, endedHandler);
210
         stream.off(TrackEvents.LOCAL_TRACK_STOPPED, endedHandler);
205
     };
211
     };

+ 171
- 30
modules/UI/videolayout/RemoteVideo.js 查看文件

8
 import UIEvents from '../../../service/UI/UIEvents';
8
 import UIEvents from '../../../service/UI/UIEvents';
9
 import JitsiPopover from "../util/JitsiPopover";
9
 import JitsiPopover from "../util/JitsiPopover";
10
 
10
 
11
-function RemoteVideo(id, VideoLayout, emitter) {
12
-    this.id = id;
11
+/**
12
+ * Creates new instance of the <tt>RemoteVideo</tt>.
13
+ * @param user {JitsiParticipant} the user for whom remote video instance will
14
+ * be created.
15
+ * @param {VideoLayout} VideoLayout the video layout instance.
16
+ * @param {EventEmitter} emitter the event emitter which will be used by
17
+ * the new instance to emit events.
18
+ * @constructor
19
+ */
20
+function RemoteVideo(user, VideoLayout, emitter) {
21
+    this.user = user;
22
+    this.id = user.getId();
13
     this.emitter = emitter;
23
     this.emitter = emitter;
14
-    this.videoSpanId = `participant_${id}`;
24
+    this.videoSpanId = `participant_${this.id}`;
15
     SmallVideo.call(this, VideoLayout);
25
     SmallVideo.call(this, VideoLayout);
16
     this.hasRemoteVideoMenu = false;
26
     this.hasRemoteVideoMenu = false;
17
     this.addRemoteVideoContainer();
27
     this.addRemoteVideoContainer();
18
-    this.connectionIndicator = new ConnectionIndicator(this, id);
28
+    this.connectionIndicator = new ConnectionIndicator(this, this.id);
19
     this.setDisplayName();
29
     this.setDisplayName();
20
-    this.bindHoverHandler();
21
     this.flipX = false;
30
     this.flipX = false;
22
     this.isLocal = false;
31
     this.isLocal = false;
23
-    this.isMuted = false;
32
+    /**
33
+     * The flag is set to <tt>true</tt> after the 'onplay' event has been
34
+     * triggered on the current video element. It goes back to <tt>false</tt>
35
+     * when the stream is removed. It is used to determine whether the video
36
+     * playback has ever started.
37
+     * @type {boolean}
38
+     */
39
+    this.wasVideoPlayed = false;
40
+    /**
41
+     * The flag is set to <tt>true</tt> if remote participant's video gets muted
42
+     * during his media connection disruption. This is to prevent black video
43
+     * being render on the thumbnail, because even though once the video has
44
+     * been played the image usually remains on the video element it seems that
45
+     * after longer period of the video element being hidden this image can be
46
+     * lost.
47
+     * @type {boolean}
48
+     */
49
+    this.mutedWhileDisconnected = false;
24
 }
50
 }
25
 
51
 
26
 RemoteVideo.prototype = Object.create(SmallVideo.prototype);
52
 RemoteVideo.prototype = Object.create(SmallVideo.prototype);
34
     if (APP.conference.isModerator) {
60
     if (APP.conference.isModerator) {
35
         this.addRemoteVideoMenu();
61
         this.addRemoteVideoMenu();
36
     }
62
     }
37
-    let {thumbWidth, thumbHeight} = this.VideoLayout.resizeThumbnails();
38
-    AudioLevels.updateAudioLevelCanvas(this.id, thumbWidth, thumbHeight);
63
+
64
+    let { remoteVideo } = this.VideoLayout.resizeThumbnails(false, true);
65
+    let { thumbHeight, thumbWidth } = remoteVideo;
66
+
67
+    this.addAudioLevelIndicator();
39
 
68
 
40
     return this.container;
69
     return this.container;
41
 };
70
 };
42
 
71
 
43
-
44
 /**
72
 /**
45
  * Initializes the remote participant popup menu, by specifying previously
73
  * Initializes the remote participant popup menu, by specifying previously
46
  * constructed popupMenuElement, containing all the menu items.
74
  * constructed popupMenuElement, containing all the menu items.
50
  */
78
  */
51
 RemoteVideo.prototype._initPopupMenu = function (popupMenuElement) {
79
 RemoteVideo.prototype._initPopupMenu = function (popupMenuElement) {
52
     this.popover = new JitsiPopover(
80
     this.popover = new JitsiPopover(
53
-        $("#" + this.videoSpanId + " > .remotevideomenu"),
81
+        $("#" + this.videoSpanId + " .remotevideomenu"),
54
         {   content: popupMenuElement.outerHTML,
82
         {   content: popupMenuElement.outerHTML,
55
             skin: "black"});
83
             skin: "black"});
56
 
84
 
60
     this.popover.show = function () {
88
     this.popover.show = function () {
61
         // update content by forcing it, to finish even if popover
89
         // update content by forcing it, to finish even if popover
62
         // is not visible
90
         // is not visible
63
-        this.updateRemoteVideoMenu(this.isMuted, true);
91
+        this.updateRemoteVideoMenu(this.isAudioMuted, true);
64
         // call the original show, passing its actual this
92
         // call the original show, passing its actual this
65
         origShowFunc.call(this.popover);
93
         origShowFunc.call(this.popover);
66
     }.bind(this);
94
     }.bind(this);
96
 
124
 
97
     muteLinkItem.id = "mutelink_" + this.id;
125
     muteLinkItem.id = "mutelink_" + this.id;
98
 
126
 
99
-    if (this.isMuted) {
127
+    if (this.isAudioMuted) {
100
         muteLinkItem.innerHTML = mutedHTML;
128
         muteLinkItem.innerHTML = mutedHTML;
101
         muteLinkItem.className = 'mutelink disabled';
129
         muteLinkItem.className = 'mutelink disabled';
102
     }
130
     }
108
     // Delegate event to the document.
136
     // Delegate event to the document.
109
     $(document).on("click", "#mutelink_" + this.id, function(){
137
     $(document).on("click", "#mutelink_" + this.id, function(){
110
 
138
 
111
-        if (this.isMuted)
139
+        if (this.isAudioMuted)
112
             return;
140
             return;
113
 
141
 
114
         this.emitter.emit(UIEvents.REMOTE_AUDIO_MUTED, this.id);
142
         this.emitter.emit(UIEvents.REMOTE_AUDIO_MUTED, this.id);
152
  */
180
  */
153
 RemoteVideo.prototype.updateRemoteVideoMenu = function (isMuted, force) {
181
 RemoteVideo.prototype.updateRemoteVideoMenu = function (isMuted, force) {
154
 
182
 
155
-    this.isMuted = isMuted;
183
+    this.isAudioMuted = isMuted;
156
 
184
 
157
     // generate content, translate it and add it to document only if
185
     // generate content, translate it and add it to document only if
158
     // popover is visible or we force to do so.
186
     // popover is visible or we force to do so.
161
     }
189
     }
162
 };
190
 };
163
 
191
 
192
+/**
193
+ * @inheritDoc
194
+ */
195
+RemoteVideo.prototype.setMutedView = function(isMuted) {
196
+    SmallVideo.prototype.setMutedView.call(this, isMuted);
197
+    // Update 'mutedWhileDisconnected' flag
198
+    this._figureOutMutedWhileDisconnected(this.isConnectionActive() === false);
199
+};
200
+
201
+/**
202
+ * Figures out the value of {@link #mutedWhileDisconnected} flag by taking into
203
+ * account remote participant's network connectivity and video muted status.
204
+ *
205
+ * @param {boolean} isDisconnected <tt>true</tt> if the remote participant is
206
+ * currently having connectivity issues or <tt>false</tt> otherwise.
207
+ *
208
+ * @private
209
+ */
210
+RemoteVideo.prototype._figureOutMutedWhileDisconnected
211
+= function(isDisconnected) {
212
+    if (isDisconnected && this.isVideoMuted) {
213
+        this.mutedWhileDisconnected = true;
214
+    } else if (!isDisconnected && !this.isVideoMuted) {
215
+        this.mutedWhileDisconnected = false;
216
+    }
217
+};
218
+
164
 /**
219
 /**
165
  * Adds the remote video menu element for the given <tt>id</tt> in the
220
  * Adds the remote video menu element for the given <tt>id</tt> in the
166
  * given <tt>parentElement</tt>.
221
  * given <tt>parentElement</tt>.
170
  */
225
  */
171
 if (!interfaceConfig.filmStripOnly) {
226
 if (!interfaceConfig.filmStripOnly) {
172
     RemoteVideo.prototype.addRemoteVideoMenu = function () {
227
     RemoteVideo.prototype.addRemoteVideoMenu = function () {
173
-        var spanElement = document.createElement('div');
174
-        spanElement.className = 'remotevideomenu';
175
-        this.container.appendChild(spanElement);
228
+
229
+        var spanElement = document.createElement('span');
230
+        spanElement.className = 'remotevideomenu toolbar-icon right';
231
+
232
+        this.container
233
+            .querySelector('.videocontainer__toolbar')
234
+            .appendChild(spanElement);
176
 
235
 
177
         var menuElement = document.createElement('i');
236
         var menuElement = document.createElement('i');
178
-        menuElement.className = 'fa fa-angle-down';
237
+        menuElement.className = 'icon-menu-up';
179
         menuElement.title = 'Remote user controls';
238
         menuElement.title = 'Remote user controls';
180
         spanElement.appendChild(menuElement);
239
         spanElement.appendChild(menuElement);
181
 
240
 
204
     var select = $('#' + elementID);
263
     var select = $('#' + elementID);
205
     select.remove();
264
     select.remove();
206
 
265
 
266
+    if (isVideo) {
267
+        this.wasVideoPlayed = false;
268
+    }
269
+
207
     console.info((isVideo ? "Video" : "Audio") +
270
     console.info((isVideo ? "Video" : "Audio") +
208
                  " removed " + this.id, select);
271
                  " removed " + this.id, select);
209
 
272
 
210
     // when removing only the video element and we are on stage
273
     // when removing only the video element and we are on stage
211
     // update the stage
274
     // update the stage
212
-    if (isVideo && this.VideoLayout.isCurrentlyOnLarge(this.id))
275
+    if (isVideo && this.isCurrentlyOnLargeVideo())
213
         this.VideoLayout.updateLargeVideo(this.id);
276
         this.VideoLayout.updateLargeVideo(this.id);
277
+    else
278
+        // Missing video stream will affect display mode
279
+        this.updateView();
280
+};
281
+
282
+/**
283
+ * Checks whether the remote user associated with this <tt>RemoteVideo</tt>
284
+ * has connectivity issues.
285
+ *
286
+ * @return {boolean} <tt>true</tt> if the user's connection is fine or
287
+ * <tt>false</tt> otherwise.
288
+ */
289
+RemoteVideo.prototype.isConnectionActive = function() {
290
+    return this.user.isConnectionActive();
291
+};
292
+
293
+/**
294
+ * The remote video is considered "playable" once the stream has started
295
+ * according to the {@link #hasVideoStarted} result.
296
+ *
297
+ * @inheritdoc
298
+ * @override
299
+ */
300
+RemoteVideo.prototype.isVideoPlayable = function () {
301
+    return SmallVideo.prototype.isVideoPlayable.call(this)
302
+        && this.hasVideoStarted() && !this.mutedWhileDisconnected;
303
+};
304
+
305
+/**
306
+ * @inheritDoc
307
+ */
308
+RemoteVideo.prototype.updateView = function () {
309
+
310
+    this.updateConnectionStatusIndicator(
311
+        null /* will obtain the status from 'conference' */);
312
+
313
+    // This must be called after 'updateConnectionStatusIndicator' because it
314
+    // affects the display mode by modifying 'mutedWhileDisconnected' flag
315
+    SmallVideo.prototype.updateView.call(this);
316
+};
317
+
318
+/**
319
+ * Updates the UI to reflect user's connectivity status.
320
+ * @param isActive {boolean|null} 'true' if user's connection is active or
321
+ * 'false' when the use is having some connectivity issues and a warning
322
+ * should be displayed. When 'null' is passed then the current value will be
323
+ * obtained from the conference instance.
324
+ */
325
+RemoteVideo.prototype.updateConnectionStatusIndicator = function (isActive) {
326
+    // Check for initial value if 'isActive' is not defined
327
+    if (typeof isActive !== "boolean") {
328
+        isActive = this.isConnectionActive();
329
+        if (isActive === null) {
330
+            // Cancel processing at this point - no update
331
+            return;
332
+        }
333
+    }
334
+
335
+    console.debug(this.id + " thumbnail is connection active ? " + isActive);
336
+
337
+    // Update 'mutedWhileDisconnected' flag
338
+    this._figureOutMutedWhileDisconnected(!isActive);
339
+
340
+    if(this.connectionIndicator)
341
+        this.connectionIndicator.updateConnectionStatusIndicator(isActive);
342
+
343
+    // Toggle thumbnail video problem filter
344
+    this.selectVideoElement().toggleClass(
345
+        "videoThumbnailProblemFilter", !isActive);
346
+    this.$avatar().toggleClass(
347
+        "videoThumbnailProblemFilter", !isActive);
214
 };
348
 };
215
 
349
 
216
 /**
350
 /**
241
     // Register 'onplaying' listener to trigger 'videoactive' on VideoLayout
375
     // Register 'onplaying' listener to trigger 'videoactive' on VideoLayout
242
     // when video playback starts
376
     // when video playback starts
243
     var onPlayingHandler = function () {
377
     var onPlayingHandler = function () {
378
+        self.wasVideoPlayed = true;
244
         self.VideoLayout.videoactive(streamElement, self.id);
379
         self.VideoLayout.videoactive(streamElement, self.id);
245
         streamElement.onplaying = null;
380
         streamElement.onplaying = null;
381
+        // Refresh to show the video
382
+        self.updateView();
246
     };
383
     };
247
     streamElement.onplaying = onPlayingHandler;
384
     streamElement.onplaying = onPlayingHandler;
248
 };
385
 };
249
 
386
 
250
 /**
387
 /**
251
- * Checks whether or not video stream exists and has started for this
252
- * RemoteVideo instance. This is checked by trying to select video element in
253
- * this container and checking if 'currentTime' field's value is greater than 0.
388
+ * Checks whether the video stream has started for this RemoteVideo instance.
254
  *
389
  *
255
- * @returns {*|boolean} true if this RemoteVideo has active video stream running
390
+ * @returns {boolean} true if this RemoteVideo has a video stream for which
391
+ * the playback has been started.
256
  */
392
  */
257
 RemoteVideo.prototype.hasVideoStarted = function () {
393
 RemoteVideo.prototype.hasVideoStarted = function () {
258
-    var videoSelector = this.selectVideoElement();
259
-    return videoSelector.length && videoSelector[0].currentTime > 0;
394
+    return this.wasVideoPlayed;
260
 };
395
 };
261
 
396
 
262
 RemoteVideo.prototype.addRemoteStreamElement = function (stream) {
397
 RemoteVideo.prototype.addRemoteStreamElement = function (stream) {
381
         return;
516
         return;
382
     }
517
     }
383
 
518
 
384
-    var nameSpan = $('#' + this.videoSpanId + '>span.displayname');
519
+    var nameSpan = $('#' + this.videoSpanId + ' .displayname');
385
 
520
 
386
     // If we already have a display name for this video.
521
     // If we already have a display name for this video.
387
     if (nameSpan.length > 0) {
522
     if (nameSpan.length > 0) {
400
     } else {
535
     } else {
401
         nameSpan = document.createElement('span');
536
         nameSpan = document.createElement('span');
402
         nameSpan.className = 'displayname';
537
         nameSpan.className = 'displayname';
403
-        $('#' + this.videoSpanId)[0].appendChild(nameSpan);
538
+        $('#' + this.videoSpanId)[0]
539
+            .querySelector('.videocontainer__toolbar')
540
+            .appendChild(nameSpan);
404
 
541
 
405
         if (displayName && displayName.length > 0) {
542
         if (displayName && displayName.length > 0) {
406
             $(nameSpan).text(displayName);
543
             $(nameSpan).text(displayName);
418
  * @param videoElementId the id of local or remote video element.
555
  * @param videoElementId the id of local or remote video element.
419
  */
556
  */
420
 RemoteVideo.prototype.removeRemoteVideoMenu = function() {
557
 RemoteVideo.prototype.removeRemoteVideoMenu = function() {
421
-    var menuSpan = $('#' + this.videoSpanId + '>span.remotevideomenu');
558
+    var menuSpan = $('#' + this.videoSpanId + '> .remotevideomenu');
422
     if (menuSpan.length) {
559
     if (menuSpan.length) {
423
         this.popover.forceHide();
560
         this.popover.forceHide();
424
         menuSpan.remove();
561
         menuSpan.remove();
427
 };
564
 };
428
 
565
 
429
 RemoteVideo.createContainer = function (spanId) {
566
 RemoteVideo.createContainer = function (spanId) {
430
-    var container = document.createElement('span');
567
+    let container = document.createElement('span');
431
     container.id = spanId;
568
     container.id = spanId;
432
     container.className = 'videocontainer';
569
     container.className = 'videocontainer';
570
+
571
+    let toolbar = document.createElement('div');
572
+    toolbar.className = "videocontainer__toolbar";
573
+    container.appendChild(toolbar);
574
+
433
     var remotes = document.getElementById('remoteVideos');
575
     var remotes = document.getElementById('remoteVideos');
434
     return remotes.appendChild(container);
576
     return remotes.appendChild(container);
435
 };
577
 };
436
 
578
 
437
-
438
 export default RemoteVideo;
579
 export default RemoteVideo;

+ 210
- 136
modules/UI/videolayout/SmallVideo.js 查看文件

1
-/* global $, APP, JitsiMeetJS */
2
-/* jshint -W101 */
1
+/* global $, APP, JitsiMeetJS, interfaceConfig */
3
 import Avatar from "../avatar/Avatar";
2
 import Avatar from "../avatar/Avatar";
4
 import UIUtil from "../util/UIUtil";
3
 import UIUtil from "../util/UIUtil";
5
 import UIEvents from "../../../service/UI/UIEvents";
4
 import UIEvents from "../../../service/UI/UIEvents";
5
+import AudioLevels from "../audio_levels/AudioLevels";
6
 
6
 
7
 const RTCUIHelper = JitsiMeetJS.util.RTCUIHelper;
7
 const RTCUIHelper = JitsiMeetJS.util.RTCUIHelper;
8
 
8
 
9
+/**
10
+ * Display mode constant used when video is being displayed on the small video.
11
+ * @type {number}
12
+ * @constant
13
+ */
14
+const DISPLAY_VIDEO = 0;
15
+/**
16
+ * Display mode constant used when the user's avatar is being displayed on
17
+ * the small video.
18
+ * @type {number}
19
+ * @constant
20
+ */
21
+const DISPLAY_AVATAR = 1;
22
+/**
23
+ * Display mode constant used when neither video nor avatar is being displayed
24
+ * on the small video.
25
+ * @type {number}
26
+ * @constant
27
+ */
28
+const DISPLAY_BLACKNESS = 2;
29
+
9
 function SmallVideo(VideoLayout) {
30
 function SmallVideo(VideoLayout) {
10
-    this.isMuted = false;
31
+    this.isAudioMuted = false;
11
     this.hasAvatar = false;
32
     this.hasAvatar = false;
12
     this.isVideoMuted = false;
33
     this.isVideoMuted = false;
13
     this.videoStream = null;
34
     this.videoStream = null;
40
 };
61
 };
41
 
62
 
42
 SmallVideo.prototype.showDisplayName = function(isShow) {
63
 SmallVideo.prototype.showDisplayName = function(isShow) {
43
-    var nameSpan = $('#' + this.videoSpanId + '>span.displayname').get(0);
64
+    var nameSpan = $('#' + this.videoSpanId + ' .displayname').get(0);
44
     if (isShow) {
65
     if (isShow) {
45
         if (nameSpan && nameSpan.innerHTML && nameSpan.innerHTML.length)
66
         if (nameSpan && nameSpan.innerHTML && nameSpan.innerHTML.length)
46
             nameSpan.setAttribute("style", "display:inline-block;");
67
             nameSpan.setAttribute("style", "display:inline-block;");
171
     return (isVideo ? 'remoteVideo_' : 'remoteAudio_') + stream.getId();
192
     return (isVideo ? 'remoteVideo_' : 'remoteAudio_') + stream.getId();
172
 };
193
 };
173
 
194
 
174
-/**
175
- * Configures hoverIn/hoverOut handlers.
176
- */
177
-SmallVideo.prototype.bindHoverHandler = function () {
178
-    // Add hover handler
179
-    var self = this;
180
-    $(this.container).hover(
181
-        function () {
182
-            self.showDisplayName(true);
183
-        },
184
-        function () {
185
-            // If the video has been "pinned" by the user we want to
186
-            // keep the display name on place.
187
-            if (!self.VideoLayout.isLargeVideoVisible() ||
188
-                !self.VideoLayout.isCurrentlyOnLarge(self.id))
189
-                self.showDisplayName(false);
190
-        }
191
-    );
192
-};
193
-
194
 /**
195
 /**
195
  * Updates the data for the indicator
196
  * Updates the data for the indicator
196
  * @param id the id of the indicator
197
  * @param id the id of the indicator
209
 
210
 
210
 
211
 
211
 /**
212
 /**
212
- * Shows audio muted indicator over small videos.
213
- * @param {string} isMuted
213
+ * Shows / hides the audio muted indicator over small videos.
214
+ *
215
+ * @param {boolean} isMuted indicates if the muted element should be shown
216
+ * or hidden
214
  */
217
  */
215
 SmallVideo.prototype.showAudioIndicator = function(isMuted) {
218
 SmallVideo.prototype.showAudioIndicator = function(isMuted) {
216
-    var audioMutedSpan = $('#' + this.videoSpanId + '>span.audioMuted');
219
+
220
+    var audioMutedIndicator = this.getAudioMutedIndicator();
217
 
221
 
218
     if (!isMuted) {
222
     if (!isMuted) {
219
-        if (audioMutedSpan.length > 0) {
220
-            audioMutedSpan.popover('hide');
221
-            audioMutedSpan.remove();
222
-        }
223
+        audioMutedIndicator.hide();
223
     }
224
     }
224
     else {
225
     else {
225
-        if (!audioMutedSpan.length) {
226
-            audioMutedSpan = document.createElement('span');
227
-            audioMutedSpan.className = 'audioMuted';
228
-            UIUtil.setTooltip(audioMutedSpan,
229
-                "videothumbnail.mute",
230
-                "top");
231
-
232
-            this.container.appendChild(audioMutedSpan);
233
-            APP.translation.translateElement($('#' + this.videoSpanId + " > span"));
234
-            var mutedIndicator = document.createElement('i');
235
-            mutedIndicator.className = 'icon-mic-disabled';
236
-            audioMutedSpan.appendChild(mutedIndicator);
226
+        audioMutedIndicator.show();
227
+    }
228
+    this.isAudioMuted = isMuted;
229
+};
237
 
230
 
238
-        }
239
-        this.updateIconPositions();
231
+/**
232
+ * Returns the audio muted indicator jquery object. If it doesn't exists -
233
+ * creates it.
234
+ *
235
+ * @returns {jQuery|HTMLElement} the audio muted indicator
236
+ */
237
+SmallVideo.prototype.getAudioMutedIndicator = function () {
238
+    var audioMutedSpan = $('#' + this.videoSpanId + ' .audioMuted');
239
+
240
+    if (audioMutedSpan.length) {
241
+        return audioMutedSpan;
240
     }
242
     }
241
-    this.isMuted = isMuted;
243
+
244
+    audioMutedSpan = document.createElement('span');
245
+    audioMutedSpan.className = 'audioMuted toolbar-icon';
246
+
247
+    UIUtil.setTooltip(audioMutedSpan,
248
+        "videothumbnail.mute",
249
+        "top");
250
+
251
+    this.container
252
+        .querySelector('.videocontainer__toolbar')
253
+        .appendChild(audioMutedSpan);
254
+
255
+
256
+    var mutedIndicator = document.createElement('i');
257
+    mutedIndicator.className = 'icon-mic-disabled';
258
+    audioMutedSpan.appendChild(mutedIndicator);
259
+
260
+    return $('#' + this.videoSpanId + ' .audioMuted');
242
 };
261
 };
243
 
262
 
244
 /**
263
 /**
245
  * Shows video muted indicator over small videos and disables/enables avatar
264
  * Shows video muted indicator over small videos and disables/enables avatar
246
  * if video muted.
265
  * if video muted.
266
+ *
267
+ * @param {boolean} isMuted indicates if we should set the view to muted view
268
+ * or not
247
  */
269
  */
248
-SmallVideo.prototype.setMutedView = function(isMuted) {
270
+SmallVideo.prototype.setVideoMutedView = function(isMuted) {
249
     this.isVideoMuted = isMuted;
271
     this.isVideoMuted = isMuted;
250
     this.updateView();
272
     this.updateView();
251
 
273
 
252
-    var videoMutedSpan = $('#' + this.videoSpanId + '>span.videoMuted');
274
+    var videoMutedSpan = this.getVideoMutedIndicator();
253
 
275
 
254
-    if (isMuted === false) {
255
-        if (videoMutedSpan.length > 0) {
256
-            videoMutedSpan.remove();
257
-        }
258
-    }
259
-    else {
260
-        if (!videoMutedSpan.length) {
261
-            videoMutedSpan = document.createElement('span');
262
-            videoMutedSpan.className = 'videoMuted';
263
-
264
-            this.container.appendChild(videoMutedSpan);
265
-
266
-            var mutedIndicator = document.createElement('i');
267
-            mutedIndicator.className = 'icon-camera-disabled';
268
-            UIUtil.setTooltip(mutedIndicator,
269
-                "videothumbnail.videomute",
270
-                "top");
271
-            videoMutedSpan.appendChild(mutedIndicator);
272
-            //translate texts for muted indicator
273
-            APP.translation.translateElement($('#' + this.videoSpanId  + " > span > i"));
274
-        }
275
-
276
-        this.updateIconPositions();
277
-    }
276
+    videoMutedSpan[isMuted ? 'show' : 'hide']();
278
 };
277
 };
279
 
278
 
280
-SmallVideo.prototype.updateIconPositions = function () {
281
-    var audioMutedSpan = $('#' + this.videoSpanId + '>span.audioMuted');
282
-    var connectionIndicator = $('#' + this.videoSpanId + '>div.connectionindicator');
283
-    var videoMutedSpan = $('#' + this.videoSpanId + '>span.videoMuted');
284
-    if(connectionIndicator.length > 0 &&
285
-        connectionIndicator[0].style.display != "none") {
286
-        audioMutedSpan.css({right: "23px"});
287
-        videoMutedSpan.css({right: ((audioMutedSpan.length > 0? 23 : 0) + 30) + "px"});
288
-    } else {
289
-        audioMutedSpan.css({right: "0px"});
290
-        videoMutedSpan.css({right: (audioMutedSpan.length > 0? 30 : 0) + "px"});
279
+/**
280
+ * Returns the video muted indicator jquery object. If it doesn't exists -
281
+ * creates it.
282
+ *
283
+ * @returns {jQuery|HTMLElement} the video muted indicator
284
+ */
285
+SmallVideo.prototype.getVideoMutedIndicator = function () {
286
+    var videoMutedSpan = $('#' + this.videoSpanId + ' .videoMuted');
287
+
288
+    if (videoMutedSpan.length) {
289
+        return videoMutedSpan;
291
     }
290
     }
291
+
292
+    videoMutedSpan = document.createElement('span');
293
+    videoMutedSpan.className = 'videoMuted toolbar-icon';
294
+
295
+    this.container
296
+        .querySelector('.videocontainer__toolbar')
297
+        .appendChild(videoMutedSpan);
298
+
299
+    var mutedIndicator = document.createElement('i');
300
+    mutedIndicator.className = 'icon-camera-disabled';
301
+
302
+    UIUtil.setTooltip(mutedIndicator,
303
+        "videothumbnail.videomute",
304
+        "top");
305
+
306
+    videoMutedSpan.appendChild(mutedIndicator);
307
+
308
+    return $('#' + this.videoSpanId + ' .videoMuted');
292
 };
309
 };
293
 
310
 
294
 /**
311
 /**
295
- * Creates the element indicating the moderator(owner) of the conference.
312
+ * Adds the element indicating the moderator(owner) of the conference.
296
  */
313
  */
297
-SmallVideo.prototype.createModeratorIndicatorElement = function () {
314
+SmallVideo.prototype.addModeratorIndicator = function () {
315
+
316
+    // Don't create moderator indicator if DISABLE_FOCUS_INDICATOR is true
317
+    if (interfaceConfig.DISABLE_FOCUS_INDICATOR)
318
+        return false;
319
+
298
     // Show moderator indicator
320
     // Show moderator indicator
299
     var indicatorSpan = $('#' + this.videoSpanId + ' .focusindicator');
321
     var indicatorSpan = $('#' + this.videoSpanId + ' .focusindicator');
300
 
322
 
301
-    if (!indicatorSpan || indicatorSpan.length === 0) {
302
-        indicatorSpan = document.createElement('span');
303
-        indicatorSpan.className = 'focusindicator';
304
-
305
-        this.container.appendChild(indicatorSpan);
306
-        indicatorSpan = $('#' + this.videoSpanId + ' .focusindicator');
323
+    if (indicatorSpan.length) {
324
+        return;
307
     }
325
     }
308
 
326
 
309
-    if (indicatorSpan.children().length !== 0)
310
-        return;
327
+    indicatorSpan = document.createElement('span');
328
+    indicatorSpan.className = 'focusindicator toolbar-icon right';
329
+
330
+    this.container
331
+        .querySelector('.videocontainer__toolbar')
332
+        .appendChild(indicatorSpan);
333
+
311
     var moderatorIndicator = document.createElement('i');
334
     var moderatorIndicator = document.createElement('i');
312
     moderatorIndicator.className = 'icon-star';
335
     moderatorIndicator.className = 'icon-star';
313
-    indicatorSpan[0].appendChild(moderatorIndicator);
314
 
336
 
315
-    UIUtil.setTooltip(indicatorSpan[0],
337
+    UIUtil.setTooltip(moderatorIndicator,
316
         "videothumbnail.moderator",
338
         "videothumbnail.moderator",
317
-        "top");
339
+        "top-left");
318
 
340
 
319
-    //translates text in focus indicators
320
-    APP.translation.translateElement($('#' + this.videoSpanId + ' .focusindicator'));
341
+    indicatorSpan.appendChild(moderatorIndicator);
342
+};
343
+
344
+/**
345
+ * Adds the element indicating the audio level of the participant.
346
+ */
347
+SmallVideo.prototype.addAudioLevelIndicator = function () {
348
+    var audioSpan = $('#' + this.videoSpanId + ' .audioindicator');
349
+
350
+    if (audioSpan.length) {
351
+        return;
352
+    }
353
+
354
+    this.container.appendChild(
355
+        AudioLevels.createThumbnailAudioLevelIndicator());
356
+};
357
+
358
+/**
359
+ * Updates the audio level for this small video.
360
+ *
361
+ * @param lvl the new audio level to set
362
+ */
363
+SmallVideo.prototype.updateAudioLevelIndicator = function (lvl) {
364
+    AudioLevels.updateThumbnailAudioLevel(this.videoSpanId, lvl);
321
 };
365
 };
322
 
366
 
323
 /**
367
 /**
324
  * Removes the element indicating the moderator(owner) of the conference.
368
  * Removes the element indicating the moderator(owner) of the conference.
325
  */
369
  */
326
-SmallVideo.prototype.removeModeratorIndicatorElement = function () {
370
+SmallVideo.prototype.removeModeratorIndicator = function () {
327
     $('#' + this.videoSpanId + ' .focusindicator').remove();
371
     $('#' + this.videoSpanId + ' .focusindicator').remove();
328
 };
372
 };
329
 
373
 
341
     return $(RTCUIHelper.findVideoElement($('#' + this.videoSpanId)[0]));
385
     return $(RTCUIHelper.findVideoElement($('#' + this.videoSpanId)[0]));
342
 };
386
 };
343
 
387
 
388
+/**
389
+ * Selects the HTML image element which displays user's avatar.
390
+ *
391
+ * @return {jQuery|HTMLElement} a jQuery selector pointing to the HTML image
392
+ * element which displays the user's avatar.
393
+ */
394
+SmallVideo.prototype.$avatar = function () {
395
+    return $('#' + this.videoSpanId + ' .userAvatar');
396
+};
397
+
344
 /**
398
 /**
345
  * Enables / disables the css responsible for focusing/pinning a video
399
  * Enables / disables the css responsible for focusing/pinning a video
346
  * thumbnail.
400
  * thumbnail.
363
     return this.selectVideoElement().length !== 0;
417
     return this.selectVideoElement().length !== 0;
364
 };
418
 };
365
 
419
 
420
+/**
421
+ * Checks whether the user associated with this <tt>SmallVideo</tt> is currently
422
+ * being displayed on the "large video".
423
+ *
424
+ * @return {boolean} <tt>true</tt> if the user is displayed on the large video
425
+ * or <tt>false</tt> otherwise.
426
+ */
427
+SmallVideo.prototype.isCurrentlyOnLargeVideo = function () {
428
+    return this.VideoLayout.isCurrentlyOnLarge(this.id);
429
+};
430
+
431
+/**
432
+ * Checks whether there is a playable video stream available for the user
433
+ * associated with this <tt>SmallVideo</tt>.
434
+ *
435
+ * @return {boolean} <tt>true</tt> if there is a playable video stream available
436
+ * or <tt>false</tt> otherwise.
437
+ */
438
+SmallVideo.prototype.isVideoPlayable = function() {
439
+    return this.videoStream // Is there anything to display ?
440
+        && !this.isVideoMuted && !this.videoStream.isMuted() // Muted ?
441
+        && (this.isLocal || this.VideoLayout.isInLastN(this.id));
442
+};
443
+
444
+/**
445
+ * Determines what should be display on the thumbnail.
446
+ *
447
+ * @return {number} one of <tt>DISPLAY_VIDEO</tt>,<tt>DISPLAY_AVATAR</tt>
448
+ * or <tt>DISPLAY_BLACKNESS</tt>.
449
+ */
450
+SmallVideo.prototype.selectDisplayMode = function() {
451
+    // Display name is always and only displayed when user is on the stage
452
+    if (this.isCurrentlyOnLargeVideo()) {
453
+        return DISPLAY_BLACKNESS;
454
+    } else if (this.isVideoPlayable() && this.selectVideoElement().length) {
455
+        return DISPLAY_VIDEO;
456
+    } else {
457
+        return DISPLAY_AVATAR;
458
+    }
459
+};
460
+
366
 /**
461
 /**
367
  * Hides or shows the user's avatar.
462
  * Hides or shows the user's avatar.
368
  * This update assumes that large video had been updated and we will
463
  * This update assumes that large video had been updated and we will
382
         }
477
         }
383
     }
478
     }
384
 
479
 
385
-    let video = this.selectVideoElement();
386
-
387
-    let avatar = $('#' + this.videoSpanId + ' .userAvatar');
388
-
389
-    var isCurrentlyOnLarge = this.VideoLayout.isCurrentlyOnLarge(this.id);
390
-
391
-    var showVideo = !this.isVideoMuted && !isCurrentlyOnLarge;
392
-    var showAvatar;
393
-    if ((!this.isLocal
394
-            && !this.VideoLayout.isInLastN(this.id))
395
-        || this.isVideoMuted) {
396
-        showAvatar = true;
397
-    } else {
398
-        // We want to show the avatar when the video is muted or not exists
399
-        // that is when 'true' or 'null' is returned
400
-        showAvatar = !this.videoStream || this.videoStream.isMuted();
401
-    }
402
-
403
-    showAvatar = showAvatar && !isCurrentlyOnLarge;
404
-
405
-    if (video && video.length > 0) {
406
-        setVisibility(video, showVideo);
407
-    }
408
-    setVisibility(avatar, showAvatar);
409
-
410
-    this.showDisplayName(!showVideo && !showAvatar);
480
+    // Determine whether video, avatar or blackness should be displayed
481
+    let displayMode = this.selectDisplayMode();
482
+    // Show/hide video
483
+    setVisibility(this.selectVideoElement(), displayMode === DISPLAY_VIDEO);
484
+    // Show/hide the avatar
485
+    setVisibility(this.$avatar(), displayMode === DISPLAY_AVATAR);
411
 };
486
 };
412
 
487
 
413
 SmallVideo.prototype.avatarChanged = function (avatarUrl) {
488
 SmallVideo.prototype.avatarChanged = function (avatarUrl) {
414
     var thumbnail = $('#' + this.videoSpanId);
489
     var thumbnail = $('#' + this.videoSpanId);
415
-    var avatar = $('#' + this.videoSpanId + ' .userAvatar');
490
+    var avatarSel = this.$avatar();
416
     this.hasAvatar = true;
491
     this.hasAvatar = true;
417
 
492
 
418
     // set the avatar in the thumbnail
493
     // set the avatar in the thumbnail
419
-    if (avatar && avatar.length > 0) {
420
-        avatar[0].src = avatarUrl;
494
+    if (avatarSel && avatarSel.length > 0) {
495
+        avatarSel[0].src = avatarUrl;
421
     } else {
496
     } else {
422
         if (thumbnail && thumbnail.length > 0) {
497
         if (thumbnail && thumbnail.length > 0) {
423
-            avatar = document.createElement('img');
424
-            avatar.className = 'userAvatar';
425
-            avatar.src = avatarUrl;
426
-            thumbnail.append(avatar);
498
+            var avatarElement = document.createElement('img');
499
+            avatarElement.className = 'userAvatar';
500
+            avatarElement.src = avatarUrl;
501
+            thumbnail.append(avatarElement);
427
         }
502
         }
428
     }
503
     }
429
 };
504
 };
445
     indicatorSpan.innerHTML
520
     indicatorSpan.innerHTML
446
         = "<i id='indicatoricon' class='fa fa-bullhorn'></i>";
521
         = "<i id='indicatoricon' class='fa fa-bullhorn'></i>";
447
     // adds a tooltip
522
     // adds a tooltip
448
-    UIUtil.setTooltip(indicatorSpan, "speaker", "left");
523
+    UIUtil.setTooltip(indicatorSpan, "speaker", "top");
449
     APP.translation.translateElement($(indicatorSpan));
524
     APP.translation.translateElement($(indicatorSpan));
450
 
525
 
451
     $(indicatorSpan).css("visibility", show ? "visible" : "hidden");
526
     $(indicatorSpan).css("visibility", show ? "visible" : "hidden");
465
     var indicatorSpanId = "raisehandindicator";
540
     var indicatorSpanId = "raisehandindicator";
466
     var indicatorSpan = this.getIndicatorSpan(indicatorSpanId);
541
     var indicatorSpan = this.getIndicatorSpan(indicatorSpanId);
467
 
542
 
468
-    indicatorSpan.style.background = "#D6D61E";
469
     indicatorSpan.innerHTML
543
     indicatorSpan.innerHTML
470
-        = "<i id='indicatoricon' class='fa fa-hand-paper-o'></i>";
544
+        = "<i id='indicatoricon' class='icon-raised-hand'></i>";
471
 
545
 
472
     // adds a tooltip
546
     // adds a tooltip
473
-    UIUtil.setTooltip(indicatorSpan, "raisedHand", "left");
547
+    UIUtil.setTooltip(indicatorSpan, "raisedHand", "top");
474
     APP.translation.translateElement($(indicatorSpan));
548
     APP.translation.translateElement($(indicatorSpan));
475
 
549
 
476
     $(indicatorSpan).css("visibility", show ? "visible" : "hidden");
550
     $(indicatorSpan).css("visibility", show ? "visible" : "hidden");

modules/UI/videolayout/LargeVideo.js → modules/UI/videolayout/VideoContainer.js 查看文件

1
 /* global $, APP, interfaceConfig */
1
 /* global $, APP, interfaceConfig */
2
 /* jshint -W101 */
2
 /* jshint -W101 */
3
 
3
 
4
-import UIUtil from "../util/UIUtil";
5
-import UIEvents from "../../../service/UI/UIEvents";
6
-import LargeContainer from './LargeContainer';
7
 import FilmStrip from './FilmStrip';
4
 import FilmStrip from './FilmStrip';
8
-import Avatar from "../avatar/Avatar";
9
-import {createDeferred} from '../../util/helpers';
10
-
11
-const FADE_DURATION_MS = 300;
5
+import LargeContainer from './LargeContainer';
6
+import UIEvents from "../../../service/UI/UIEvents";
7
+import UIUtil from "../util/UIUtil";
12
 
8
 
9
+// FIXME should be 'video'
13
 export const VIDEO_CONTAINER_TYPE = "camera";
10
 export const VIDEO_CONTAINER_TYPE = "camera";
14
 
11
 
12
+const FADE_DURATION_MS = 300;
13
+
15
 /**
14
 /**
16
  * Get stream id.
15
  * Get stream id.
17
  * @param {JitsiTrack?} stream
16
  * @param {JitsiTrack?} stream
20
     if (!stream) {
19
     if (!stream) {
21
         return;
20
         return;
22
     }
21
     }
23
-    if (stream.isLocal()) { // local stream doesn't have method "getParticipantId"
22
+    // local stream doesn't have method "getParticipantId"
23
+    if (stream.isLocal()) {
24
         return APP.conference.getMyUserId();
24
         return APP.conference.getMyUserId();
25
     } else {
25
     } else {
26
         return stream.getParticipantId();
26
         return stream.getParticipantId();
154
 /**
154
 /**
155
  * Container for user video.
155
  * Container for user video.
156
  */
156
  */
157
-class VideoContainer extends LargeContainer {
157
+export class VideoContainer extends LargeContainer {
158
     // FIXME: With Temasys we have to re-select everytime
158
     // FIXME: With Temasys we have to re-select everytime
159
     get $video () {
159
     get $video () {
160
         return $('#largeVideo');
160
         return $('#largeVideo');
164
         return getStreamOwnerId(this.stream);
164
         return getStreamOwnerId(this.stream);
165
     }
165
     }
166
 
166
 
167
-    constructor (onPlay) {
167
+    constructor (onPlay, emitter) {
168
         super();
168
         super();
169
         this.stream = null;
169
         this.stream = null;
170
         this.videoType = null;
170
         this.videoType = null;
171
         this.localFlipX = true;
171
         this.localFlipX = true;
172
+        this.emitter = emitter;
172
 
173
 
173
         this.isVisible = false;
174
         this.isVisible = false;
174
 
175
 
176
+        /**
177
+         * Flag indicates whether or not the avatar is currently displayed.
178
+         * @type {boolean}
179
+         */
180
+        this.avatarDisplayed = false;
175
         this.$avatar = $('#dominantSpeaker');
181
         this.$avatar = $('#dominantSpeaker');
182
+
183
+        /**
184
+         * A jQuery selector of the remote connection message.
185
+         * @type {jQuery|HTMLElement}
186
+         */
187
+        this.$remoteConnectionMessage = $('#remoteConnectionMessage');
188
+
189
+        /**
190
+         * Indicates whether or not the video stream attached to the video
191
+         * element has started(which means that there is any image rendered
192
+         * even if the video is stalled).
193
+         * @type {boolean}
194
+         */
195
+        this.wasVideoRendered = false;
196
+
176
         this.$wrapper = $('#largeVideoWrapper');
197
         this.$wrapper = $('#largeVideoWrapper');
177
 
198
 
178
         this.avatarHeight = $("#dominantSpeakerAvatar").height();
199
         this.avatarHeight = $("#dominantSpeakerAvatar").height();
179
 
200
 
201
+        var onPlayCallback = function (event) {
202
+            if (typeof onPlay === 'function') {
203
+                onPlay(event);
204
+            }
205
+            this.wasVideoRendered = true;
206
+        }.bind(this);
180
         // This does not work with Temasys plugin - has to be a property to be
207
         // This does not work with Temasys plugin - has to be a property to be
181
         // copied between new <object> elements
208
         // copied between new <object> elements
182
         //this.$video.on('play', onPlay);
209
         //this.$video.on('play', onPlay);
183
-        this.$video[0].onplay = onPlay;
210
+        this.$video[0].onplay = onPlayCallback;
211
+    }
212
+
213
+    /**
214
+     * Enables a filter on the video which indicates that there are some
215
+     * problems with the local media connection.
216
+     *
217
+     * @param {boolean} enable <tt>true</tt> if the filter is to be enabled or
218
+     * <tt>false</tt> otherwise.
219
+     */
220
+    enableLocalConnectionProblemFilter (enable) {
221
+        this.$video.toggleClass("videoProblemFilter", enable);
184
     }
222
     }
185
 
223
 
186
     /**
224
     /**
205
         let { width, height } = this.getStreamSize();
243
         let { width, height } = this.getStreamSize();
206
         if (this.stream && this.isScreenSharing()) {
244
         if (this.stream && this.isScreenSharing()) {
207
             return getDesktopVideoSize( width,
245
             return getDesktopVideoSize( width,
208
-                                        height,
209
-                                        containerWidth,
210
-                                        containerHeight);
246
+                height,
247
+                containerWidth,
248
+                containerHeight);
211
         } else {
249
         } else {
212
             return getCameraVideoSize(  width,
250
             return getCameraVideoSize(  width,
213
-                                        height,
214
-                                        containerWidth,
215
-                                        containerHeight);
251
+                height,
252
+                containerWidth,
253
+                containerHeight);
216
         }
254
         }
217
     }
255
     }
218
 
256
 
228
     getVideoPosition (width, height, containerWidth, containerHeight) {
266
     getVideoPosition (width, height, containerWidth, containerHeight) {
229
         if (this.stream && this.isScreenSharing()) {
267
         if (this.stream && this.isScreenSharing()) {
230
             return getDesktopVideoPosition( width,
268
             return getDesktopVideoPosition( width,
231
-                                            height,
232
-                                            containerWidth,
233
-                                            containerHeight);
269
+                height,
270
+                containerWidth,
271
+                containerHeight);
234
         } else {
272
         } else {
235
             return getCameraVideoPosition(  width,
273
             return getCameraVideoPosition(  width,
236
-                                            height,
237
-                                            containerWidth,
238
-                                            containerHeight);
274
+                height,
275
+                containerWidth,
276
+                containerHeight);
239
         }
277
         }
240
     }
278
     }
241
 
279
 
280
+    /**
281
+     * Update position of the remote connection message which describes that
282
+     * the remote user is having connectivity issues.
283
+     */
284
+    positionRemoteConnectionMessage () {
285
+
286
+        if (this.avatarDisplayed) {
287
+            let $avatarImage = $("#dominantSpeakerAvatar");
288
+            this.$remoteConnectionMessage.css(
289
+                'top',
290
+                $avatarImage.offset().top + $avatarImage.height() + 10);
291
+        } else {
292
+            let height = this.$remoteConnectionMessage.height();
293
+            let parentHeight = this.$remoteConnectionMessage.parent().height();
294
+            this.$remoteConnectionMessage.css(
295
+                'top', (parentHeight/2) - (height/2));
296
+        }
297
+
298
+        let width = this.$remoteConnectionMessage.width();
299
+        let parentWidth = this.$remoteConnectionMessage.parent().width();
300
+        this.$remoteConnectionMessage.css(
301
+            'left', ((parentWidth/2) - (width/2)));
302
+    }
303
+
242
     resize (containerWidth, containerHeight, animate = false) {
304
     resize (containerWidth, containerHeight, animate = false) {
243
         let [width, height]
305
         let [width, height]
244
             = this.getVideoSize(containerWidth, containerHeight);
306
             = this.getVideoSize(containerWidth, containerHeight);
245
         let { horizontalIndent, verticalIndent }
307
         let { horizontalIndent, verticalIndent }
246
             = this.getVideoPosition(width, height,
308
             = this.getVideoPosition(width, height,
247
-                                    containerWidth, containerHeight);
309
+            containerWidth, containerHeight);
248
 
310
 
249
         // update avatar position
311
         // update avatar position
250
         let top = containerHeight / 2 - this.avatarHeight / 4 * 3;
312
         let top = containerHeight / 2 - this.avatarHeight / 4 * 3;
251
 
313
 
252
         this.$avatar.css('top', top);
314
         this.$avatar.css('top', top);
253
 
315
 
316
+        this.positionRemoteConnectionMessage();
317
+
254
         this.$wrapper.animate({
318
         this.$wrapper.animate({
255
             width: width,
319
             width: width,
256
             height: height,
320
             height: height,
272
      * @param {string} videoType video type
336
      * @param {string} videoType video type
273
      */
337
      */
274
     setStream (stream, videoType) {
338
     setStream (stream, videoType) {
339
+
340
+        if (this.stream === stream) {
341
+            return;
342
+        } else {
343
+            // The stream has changed, so the image will be lost on detach
344
+            this.wasVideoRendered = false;
345
+        }
346
+
275
         // detach old stream
347
         // detach old stream
276
         if (this.stream) {
348
         if (this.stream) {
277
             this.stream.detach(this.$video[0]);
349
             this.stream.detach(this.$video[0]);
327
             (show) ? interfaceConfig.DEFAULT_BACKGROUND : "#000");
399
             (show) ? interfaceConfig.DEFAULT_BACKGROUND : "#000");
328
 
400
 
329
         this.$avatar.css("visibility", show ? "visible" : "hidden");
401
         this.$avatar.css("visibility", show ? "visible" : "hidden");
402
+        this.avatarDisplayed = show;
403
+
404
+        this.emitter.emit(UIEvents.LARGE_VIDEO_AVATAR_DISPLAYED, show);
405
+    }
406
+
407
+    /**
408
+     * Indicates that the remote user who is currently displayed by this video
409
+     * container is having connectivity issues.
410
+     *
411
+     * @param {boolean} show <tt>true</tt> to show or <tt>false</tt> to hide
412
+     * the indication.
413
+     */
414
+    showRemoteConnectionProblemIndicator (show) {
415
+        this.$video.toggleClass("remoteVideoProblemFilter", show);
416
+        this.$avatar.toggleClass("remoteVideoProblemFilter", show);
330
     }
417
     }
331
 
418
 
332
     // We are doing fadeOut/fadeIn animations on parent div which wraps
419
     // We are doing fadeOut/fadeIn animations on parent div which wraps
380
         return false;
467
         return false;
381
     }
468
     }
382
 }
469
 }
383
-
384
-/**
385
- * Manager for all Large containers.
386
- */
387
-export default class LargeVideoManager {
388
-    constructor () {
389
-        this.containers = {};
390
-
391
-        this.state = VIDEO_CONTAINER_TYPE;
392
-        this.videoContainer = new VideoContainer(
393
-            () => this.resizeContainer(VIDEO_CONTAINER_TYPE));
394
-        this.addContainer(VIDEO_CONTAINER_TYPE, this.videoContainer);
395
-
396
-        // use the same video container to handle and desktop tracks
397
-        this.addContainer("desktop", this.videoContainer);
398
-
399
-        this.width = 0;
400
-        this.height = 0;
401
-
402
-        this.$container = $('#largeVideoContainer');
403
-
404
-        this.$container.css({
405
-            display: 'inline-block'
406
-        });
407
-
408
-        if (interfaceConfig.SHOW_JITSI_WATERMARK) {
409
-            let leftWatermarkDiv
410
-                = this.$container.find("div.watermark.leftwatermark");
411
-
412
-            leftWatermarkDiv.css({display: 'block'});
413
-
414
-            leftWatermarkDiv.parent().attr(
415
-                'href', interfaceConfig.JITSI_WATERMARK_LINK);
416
-        }
417
-
418
-        if (interfaceConfig.SHOW_BRAND_WATERMARK) {
419
-            let rightWatermarkDiv
420
-                = this.$container.find("div.watermark.rightwatermark");
421
-
422
-            rightWatermarkDiv.css({
423
-                display: 'block',
424
-                backgroundImage: 'url(images/rightwatermark.png)'
425
-            });
426
-
427
-            rightWatermarkDiv.parent().attr(
428
-                'href', interfaceConfig.BRAND_WATERMARK_LINK);
429
-        }
430
-
431
-        if (interfaceConfig.SHOW_POWERED_BY) {
432
-            this.$container.children("a.poweredby").css({display: 'block'});
433
-        }
434
-
435
-        this.$container.hover(
436
-            e => this.onHoverIn(e),
437
-            e => this.onHoverOut(e)
438
-        );
439
-    }
440
-
441
-    onHoverIn (e) {
442
-        if (!this.state) {
443
-            return;
444
-        }
445
-        let container = this.getContainer(this.state);
446
-        container.onHoverIn(e);
447
-    }
448
-
449
-    onHoverOut (e) {
450
-        if (!this.state) {
451
-            return;
452
-        }
453
-        let container = this.getContainer(this.state);
454
-        container.onHoverOut(e);
455
-    }
456
-
457
-    get id () {
458
-        let container = this.getContainer(this.state);
459
-        return container.id;
460
-    }
461
-
462
-    scheduleLargeVideoUpdate () {
463
-        if (this.updateInProcess || !this.newStreamData) {
464
-            return;
465
-        }
466
-
467
-        this.updateInProcess = true;
468
-
469
-        let container = this.getContainer(this.state);
470
-
471
-        // Include hide()/fadeOut only if we're switching between users
472
-        let preUpdate;
473
-        if (this.newStreamData.id != this.id) {
474
-            preUpdate = container.hide();
475
-        } else {
476
-            preUpdate = Promise.resolve();
477
-        }
478
-
479
-        preUpdate.then(() => {
480
-            let {id, stream, videoType, resolve} = this.newStreamData;
481
-            this.newStreamData = null;
482
-
483
-            console.info("hover in %s", id);
484
-            this.state = videoType;
485
-            let container = this.getContainer(this.state);
486
-            container.setStream(stream, videoType);
487
-
488
-            // change the avatar url on large
489
-            this.updateAvatar(Avatar.getAvatarUrl(id));
490
-
491
-            // If we the continer is VIDEO_CONTAINER_TYPE, we need to check
492
-            // its stream whether exist and is muted to set isVideoMuted
493
-            // in rest of the cases it is false
494
-            let isVideoMuted = false;
495
-            if (videoType == VIDEO_CONTAINER_TYPE)
496
-                isVideoMuted = stream ? stream.isMuted() : true;
497
-
498
-            // show the avatar on large if needed
499
-            container.showAvatar(isVideoMuted);
500
-
501
-            let promise;
502
-
503
-            // do not show stream if video is muted
504
-            // but we still should show watermark
505
-            if (isVideoMuted) {
506
-                this.showWatermark(true);
507
-                promise = Promise.resolve();
508
-            } else {
509
-                promise = container.show();
510
-            }
511
-
512
-            // resolve updateLargeVideo promise after everything is done
513
-            promise.then(resolve);
514
-
515
-            return promise;
516
-        }).then(() => {
517
-            // after everything is done check again if there are any pending
518
-            // new streams.
519
-            this.updateInProcess = false;
520
-            this.scheduleLargeVideoUpdate();
521
-        });
522
-    }
523
-
524
-    /**
525
-     * Update large video.
526
-     * Switches to large video even if previously other container was visible.
527
-     * @param userID the userID of the participant associated with the stream
528
-     * @param {JitsiTrack?} stream new stream
529
-     * @param {string?} videoType new video type
530
-     * @returns {Promise}
531
-     */
532
-    updateLargeVideo (userID, stream, videoType) {
533
-        if (this.newStreamData) {
534
-            this.newStreamData.reject();
535
-        }
536
-
537
-        this.newStreamData = createDeferred();
538
-        this.newStreamData.id = userID;
539
-        this.newStreamData.stream = stream;
540
-        this.newStreamData.videoType = videoType;
541
-
542
-        this.scheduleLargeVideoUpdate();
543
-
544
-        return this.newStreamData.promise;
545
-    }
546
-
547
-    /**
548
-     * Update container size.
549
-     */
550
-    updateContainerSize () {
551
-        this.width = UIUtil.getAvailableVideoWidth();
552
-        this.height = window.innerHeight;
553
-    }
554
-
555
-    /**
556
-     * Resize Large container of specified type.
557
-     * @param {string} type type of container which should be resized.
558
-     * @param {boolean} [animate=false] if resize process should be animated.
559
-     */
560
-    resizeContainer (type, animate = false) {
561
-        let container = this.getContainer(type);
562
-        container.resize(this.width, this.height, animate);
563
-    }
564
-
565
-    /**
566
-     * Resize all Large containers.
567
-     * @param {boolean} animate if resize process should be animated.
568
-     */
569
-    resize (animate) {
570
-        // resize all containers
571
-        Object.keys(this.containers)
572
-            .forEach(type => this.resizeContainer(type, animate));
573
-
574
-        this.$container.animate({
575
-            width: this.width,
576
-            height: this.height
577
-        }, {
578
-            queue: false,
579
-            duration: animate ? 500 : 0
580
-        });
581
-    }
582
-
583
-    /**
584
-     * Enables/disables the filter indicating a video problem to the user.
585
-     *
586
-     * @param enable <tt>true</tt> to enable, <tt>false</tt> to disable
587
-     */
588
-    enableVideoProblemFilter (enable) {
589
-        let container = this.getContainer(this.state);
590
-        container.$video.toggleClass("videoProblemFilter", enable);
591
-    }
592
-
593
-    /**
594
-     * Updates the src of the dominant speaker avatar
595
-     */
596
-    updateAvatar (avatarUrl) {
597
-        $("#dominantSpeakerAvatar").attr('src', avatarUrl);
598
-    }
599
-
600
-    /**
601
-     * Show or hide watermark.
602
-     * @param {boolean} show
603
-     */
604
-    showWatermark (show) {
605
-        $('.watermark').css('visibility', show ? 'visible' : 'hidden');
606
-    }
607
-
608
-    /**
609
-     * Add container of specified type.
610
-     * @param {string} type container type
611
-     * @param {LargeContainer} container container to add.
612
-     */
613
-    addContainer (type, container) {
614
-        if (this.containers[type]) {
615
-            throw new Error(`container of type ${type} already exist`);
616
-        }
617
-
618
-        this.containers[type] = container;
619
-        this.resizeContainer(type);
620
-    }
621
-
622
-    /**
623
-     * Get Large container of specified type.
624
-     * @param {string} type container type.
625
-     * @returns {LargeContainer}
626
-     */
627
-    getContainer (type) {
628
-        let container = this.containers[type];
629
-
630
-        if (!container) {
631
-            throw new Error(`container of type ${type} doesn't exist`);
632
-        }
633
-
634
-        return container;
635
-    }
636
-
637
-    /**
638
-     * Remove Large container of specified type.
639
-     * @param {string} type container type.
640
-     */
641
-    removeContainer (type) {
642
-        if (!this.containers[type]) {
643
-            throw new Error(`container of type ${type} doesn't exist`);
644
-        }
645
-
646
-        delete this.containers[type];
647
-    }
648
-
649
-    /**
650
-     * Show Large container of specified type.
651
-     * Does nothing if such container is already visible.
652
-     * @param {string} type container type.
653
-     * @returns {Promise}
654
-     */
655
-    showContainer (type) {
656
-        if (this.state === type) {
657
-            return Promise.resolve();
658
-        }
659
-
660
-        let oldContainer = this.containers[this.state];
661
-        if (this.state === VIDEO_CONTAINER_TYPE) {
662
-            this.showWatermark(false);
663
-        }
664
-        oldContainer.hide();
665
-
666
-        this.state = type;
667
-        let container = this.getContainer(type);
668
-
669
-        return container.show().then(() => {
670
-            if (type === VIDEO_CONTAINER_TYPE) {
671
-                this.showWatermark(true);
672
-            }
673
-        });
674
-    }
675
-
676
-    /**
677
-     * Changes the flipX state of the local video.
678
-     * @param val {boolean} true if flipped.
679
-     */
680
-    onLocalFlipXChange(val) {
681
-        this.videoContainer.setLocalFlipX(val);
682
-    }
683
-}

+ 102
- 49
modules/UI/videolayout/VideoLayout.js 查看文件

1
 /* global config, APP, $, interfaceConfig, JitsiMeetJS */
1
 /* global config, APP, $, interfaceConfig, JitsiMeetJS */
2
 /* jshint -W101 */
2
 /* jshint -W101 */
3
 
3
 
4
-import AudioLevels from "../audio_levels/AudioLevels";
5
 import Avatar from "../avatar/Avatar";
4
 import Avatar from "../avatar/Avatar";
6
 import FilmStrip from "./FilmStrip";
5
 import FilmStrip from "./FilmStrip";
7
 import UIEvents from "../../../service/UI/UIEvents";
6
 import UIEvents from "../../../service/UI/UIEvents";
8
 import UIUtil from "../util/UIUtil";
7
 import UIUtil from "../util/UIUtil";
9
 
8
 
10
 import RemoteVideo from "./RemoteVideo";
9
 import RemoteVideo from "./RemoteVideo";
11
-import LargeVideoManager, {VIDEO_CONTAINER_TYPE} from "./LargeVideo";
10
+import LargeVideoManager  from "./LargeVideoManager";
11
+import {VIDEO_CONTAINER_TYPE} from "./VideoContainer";
12
 import {SHARED_VIDEO_CONTAINER_TYPE} from '../shared_video/SharedVideo';
12
 import {SHARED_VIDEO_CONTAINER_TYPE} from '../shared_video/SharedVideo';
13
 import LocalVideo from "./LocalVideo";
13
 import LocalVideo from "./LocalVideo";
14
 
14
 
102
             });
102
             });
103
         localVideoThumbnail = new LocalVideo(VideoLayout, emitter);
103
         localVideoThumbnail = new LocalVideo(VideoLayout, emitter);
104
         // sets default video type of local video
104
         // sets default video type of local video
105
+        // FIXME container type is totally different thing from the video type
105
         localVideoThumbnail.setVideoType(VIDEO_CONTAINER_TYPE);
106
         localVideoThumbnail.setVideoType(VIDEO_CONTAINER_TYPE);
106
         // if we do not resize the thumbs here, if there is no video device
107
         // if we do not resize the thumbs here, if there is no video device
107
         // the local video thumb maybe one pixel
108
         // the local video thumb maybe one pixel
108
-        let {thumbWidth, thumbHeight} = this.resizeThumbnails(false, true);
109
-        AudioLevels.updateAudioLevelCanvas(null, thumbWidth, thumbHeight);
109
+        let { localVideo } = this.resizeThumbnails(false, true);
110
 
110
 
111
         emitter.addListener(UIEvents.CONTACT_CLICKED, onContactClicked);
111
         emitter.addListener(UIEvents.CONTACT_CLICKED, onContactClicked);
112
         this.lastNCount = config.channelLastN;
112
         this.lastNCount = config.channelLastN;
113
     },
113
     },
114
 
114
 
115
     initLargeVideo () {
115
     initLargeVideo () {
116
-        largeVideo = new LargeVideoManager();
116
+        largeVideo = new LargeVideoManager(eventEmitter);
117
         if(localFlipX) {
117
         if(localFlipX) {
118
             largeVideo.onLocalFlipXChange(localFlipX);
118
             largeVideo.onLocalFlipXChange(localFlipX);
119
         }
119
         }
120
         largeVideo.updateContainerSize();
120
         largeVideo.updateContainerSize();
121
-        AudioLevels.init();
122
     },
121
     },
123
 
122
 
123
+    /**
124
+     * Sets the audio level of the video elements associated to the given id.
125
+     *
126
+     * @param id the video identifier in the form it comes from the library
127
+     * @param lvl the new audio level to update to
128
+     */
124
     setAudioLevel(id, lvl) {
129
     setAudioLevel(id, lvl) {
125
-        if (!largeVideo) {
126
-            return;
127
-        }
128
-        AudioLevels.updateAudioLevel(
129
-            id, lvl, largeVideo.id
130
-        );
130
+        let smallVideo = this.getSmallVideo(id);
131
+        if (smallVideo)
132
+            smallVideo.updateAudioLevelIndicator(lvl);
133
+
134
+        if (largeVideo && id === largeVideo.id)
135
+            largeVideo.updateLargeVideoAudioLevel(lvl);
131
     },
136
     },
132
 
137
 
133
     isInLastN (resource) {
138
     isInLastN (resource) {
254
     electLastVisibleVideo () {
259
     electLastVisibleVideo () {
255
         // pick the last visible video in the row
260
         // pick the last visible video in the row
256
         // if nobody else is left, this picks the local video
261
         // if nobody else is left, this picks the local video
257
-        let thumbs = FilmStrip.getThumbs(true).filter('[id!="mixedstream"]');
262
+        let remoteThumbs = FilmStrip.getThumbs(true).remoteThumbs;
263
+        let thumbs = remoteThumbs.filter('[id!="mixedstream"]');
258
 
264
 
259
         let lastVisible = thumbs.filter(':visible:last');
265
         let lastVisible = thumbs.filter(':visible:last');
260
         if (lastVisible.length) {
266
         if (lastVisible.length) {
268
         }
274
         }
269
 
275
 
270
         console.info("Last visible video no longer exists");
276
         console.info("Last visible video no longer exists");
271
-        thumbs = FilmStrip.getThumbs();
277
+        thumbs = FilmStrip.getThumbs().remoteThumbs;
272
         if (thumbs.length) {
278
         if (thumbs.length) {
273
             let id = getPeerContainerResourceId(thumbs[0]);
279
             let id = getPeerContainerResourceId(thumbs[0]);
274
             if (remoteVideos[id]) {
280
             if (remoteVideos[id]) {
378
     },
384
     },
379
 
385
 
380
     /**
386
     /**
381
-     * Creates a participant container for the given id and smallVideo.
387
+     * Creates or adds a participant container for the given id and smallVideo.
382
      *
388
      *
383
-     * @param id the id of the participant to add
389
+     * @param {JitsiParticipant} user the participant to add
384
      * @param {SmallVideo} smallVideo optional small video instance to add as a
390
      * @param {SmallVideo} smallVideo optional small video instance to add as a
385
-     * remote video, if undefined RemoteVideo will be created
391
+     * remote video, if undefined <tt>RemoteVideo</tt> will be created
386
      */
392
      */
387
-    addParticipantContainer (id, smallVideo) {
393
+    addParticipantContainer (user, smallVideo) {
394
+        let id = user.getId();
388
         let remoteVideo;
395
         let remoteVideo;
389
         if(smallVideo)
396
         if(smallVideo)
390
             remoteVideo = smallVideo;
397
             remoteVideo = smallVideo;
391
         else
398
         else
392
-            remoteVideo = new RemoteVideo(id, VideoLayout, eventEmitter);
399
+            remoteVideo = new RemoteVideo(user, VideoLayout, eventEmitter);
400
+        this.addRemoteVideoContainer(id, remoteVideo);
401
+    },
402
+
403
+    /**
404
+     * Adds remote video container for the given id and <tt>SmallVideo</tt>.
405
+     *
406
+     * @param {string} the id of the video to add
407
+     * @param {SmallVideo} smallVideo the small video instance to add as a
408
+     * remote video
409
+     */
410
+    addRemoteVideoContainer (id, remoteVideo) {
393
         remoteVideos[id] = remoteVideo;
411
         remoteVideos[id] = remoteVideo;
394
 
412
 
395
         let videoType = VideoLayout.getRemoteVideoType(id);
413
         let videoType = VideoLayout.getRemoteVideoType(id);
396
         if (!videoType) {
414
         if (!videoType) {
397
             // make video type the default one (camera)
415
             // make video type the default one (camera)
416
+            // FIXME container type is not a video type
398
             videoType = VIDEO_CONTAINER_TYPE;
417
             videoType = VIDEO_CONTAINER_TYPE;
399
         }
418
         }
400
         remoteVideo.setVideoType(videoType);
419
         remoteVideo.setVideoType(videoType);
401
 
420
 
402
         // In case this is not currently in the last n we don't show it.
421
         // In case this is not currently in the last n we don't show it.
403
         if (localLastNCount && localLastNCount > 0 &&
422
         if (localLastNCount && localLastNCount > 0 &&
404
-            FilmStrip.getThumbs().length >= localLastNCount + 2) {
423
+            FilmStrip.getThumbs().remoteThumbs.length >= localLastNCount + 2) {
405
             remoteVideo.showPeerContainer('hide');
424
             remoteVideo.showPeerContainer('hide');
406
         } else {
425
         } else {
407
             VideoLayout.resizeThumbnails(false, true);
426
             VideoLayout.resizeThumbnails(false, true);
408
         }
427
         }
428
+        // Initialize the view
429
+        remoteVideo.updateView();
409
     },
430
     },
410
 
431
 
411
     videoactive (videoelem, resourceJid) {
432
     videoactive (videoelem, resourceJid) {
448
     showModeratorIndicator () {
469
     showModeratorIndicator () {
449
         let isModerator = APP.conference.isModerator;
470
         let isModerator = APP.conference.isModerator;
450
         if (isModerator) {
471
         if (isModerator) {
451
-            localVideoThumbnail.createModeratorIndicatorElement();
472
+            localVideoThumbnail.addModeratorIndicator();
452
         } else {
473
         } else {
453
-            localVideoThumbnail.removeModeratorIndicatorElement();
474
+            localVideoThumbnail.removeModeratorIndicator();
454
         }
475
         }
455
 
476
 
456
         APP.conference.listMembers().forEach(function (member) {
477
         APP.conference.listMembers().forEach(function (member) {
460
                 return;
481
                 return;
461
 
482
 
462
             if (member.isModerator()) {
483
             if (member.isModerator()) {
463
-                remoteVideo.removeRemoteVideoMenu();
464
-                remoteVideo.createModeratorIndicatorElement();
465
-            } else if (isModerator) {
484
+                remoteVideo.addModeratorIndicator();
485
+            }
486
+
487
+            if (isModerator) {
466
                 // We are moderator, but user is not - add menu
488
                 // We are moderator, but user is not - add menu
467
                 if(!remoteVideo.hasRemoteVideoMenu) {
489
                 if(!remoteVideo.hasRemoteVideoMenu) {
468
                     remoteVideo.addRemoteVideoMenu();
490
                     remoteVideo.addRemoteVideoMenu();
479
         localVideoThumbnail.showAudioIndicator(isMuted);
501
         localVideoThumbnail.showAudioIndicator(isMuted);
480
     },
502
     },
481
 
503
 
504
+    /**
505
+     * Shows/hides the indication about local connection being interrupted.
506
+     *
507
+     * @param {boolean} isInterrupted <tt>true</tt> if local connection is
508
+     * currently in the interrupted state or <tt>false</tt> if the connection
509
+     * is fine.
510
+     */
511
+    showLocalConnectionInterrupted (isInterrupted) {
512
+        localVideoThumbnail.connectionIndicator
513
+            .updateConnectionStatusIndicator(!isInterrupted);
514
+    },
515
+
482
     /**
516
     /**
483
      * Resizes thumbnails.
517
      * Resizes thumbnails.
484
      */
518
      */
486
                         forceUpdate = false,
520
                         forceUpdate = false,
487
                         onComplete = null) {
521
                         onComplete = null) {
488
 
522
 
489
-        let {thumbWidth, thumbHeight}
523
+        let { localVideo, remoteVideo }
490
             = FilmStrip.calculateThumbnailSize();
524
             = FilmStrip.calculateThumbnailSize();
491
 
525
 
492
-        $('.userAvatar').css('left', (thumbWidth - thumbHeight) / 2);
526
+        let {thumbWidth, thumbHeight} = remoteVideo;
493
 
527
 
494
-        FilmStrip.resizeThumbnails(thumbWidth, thumbHeight,
528
+        FilmStrip.resizeThumbnails(localVideo, remoteVideo,
495
             animate, forceUpdate)
529
             animate, forceUpdate)
496
             .then(function () {
530
             .then(function () {
497
-                AudioLevels.updateCanvasSize(thumbWidth, thumbHeight);
498
                 if (onComplete && typeof onComplete === "function")
531
                 if (onComplete && typeof onComplete === "function")
499
                     onComplete();
532
                     onComplete();
500
-        });
501
-        return {thumbWidth, thumbHeight};
533
+            });
534
+        return { localVideo, remoteVideo };
502
     },
535
     },
503
 
536
 
504
     /**
537
     /**
524
      */
557
      */
525
     onVideoMute (id, value) {
558
     onVideoMute (id, value) {
526
         if (APP.conference.isLocalId(id)) {
559
         if (APP.conference.isLocalId(id)) {
527
-            localVideoThumbnail.setMutedView(value);
560
+            localVideoThumbnail.setVideoMutedView(value);
528
         } else {
561
         } else {
529
             let remoteVideo = remoteVideos[id];
562
             let remoteVideo = remoteVideos[id];
530
             if (remoteVideo)
563
             if (remoteVideo)
531
-                remoteVideo.setMutedView(value);
564
+                remoteVideo.setVideoMutedView(value);
532
         }
565
         }
533
 
566
 
534
         if (this.isCurrentlyOnLarge(id)) {
567
         if (this.isCurrentlyOnLarge(id)) {
610
         }
643
         }
611
     },
644
     },
612
 
645
 
646
+    /**
647
+     * Shows/hides warning about remote user's connectivity issues.
648
+     *
649
+     * @param {string} id the ID of the remote participant(MUC nickname)
650
+     * @param {boolean} isActive true if the connection is ok or false when
651
+     * the user is having connectivity issues.
652
+     */
653
+    onParticipantConnectionStatusChanged (id, isActive) {
654
+        // Show/hide warning on the large video
655
+        if (this.isCurrentlyOnLarge(id)) {
656
+            if (largeVideo) {
657
+                // We have to trigger full large video update to transition from
658
+                // avatar to video on connectivity restored.
659
+                this.updateLargeVideo(id, true /* force update */);
660
+            }
661
+        }
662
+        // Show/hide warning on the thumbnail
663
+        let remoteVideo = remoteVideos[id];
664
+        if (remoteVideo) {
665
+            // Updating only connection status indicator is not enough, because
666
+            // when we the connection is restored while the avatar was displayed
667
+            // (due to 'muted while disconnected' condition) we may want to show
668
+            // the video stream again and in order to do that the display mode
669
+            // must be updated.
670
+            //remoteVideo.updateConnectionStatusIndicator(isActive);
671
+            remoteVideo.updateView();
672
+        }
673
+    },
674
+
613
     /**
675
     /**
614
      * On last N change event.
676
      * On last N change event.
615
      *
677
      *
656
         var updateLargeVideo = false;
718
         var updateLargeVideo = false;
657
 
719
 
658
         // Handle LastN/local LastN changes.
720
         // Handle LastN/local LastN changes.
659
-        FilmStrip.getThumbs().each(( index, element ) => {
721
+        FilmStrip.getThumbs().remoteThumbs.each(( index, element ) => {
660
             var resourceJid = getPeerContainerResourceId(element);
722
             var resourceJid = getPeerContainerResourceId(element);
661
             var smallVideo = remoteVideos[resourceJid];
723
             var smallVideo = remoteVideos[resourceJid];
662
 
724
 
945
      * Indicates that the video has been interrupted.
1007
      * Indicates that the video has been interrupted.
946
      */
1008
      */
947
     onVideoInterrupted () {
1009
     onVideoInterrupted () {
948
-        this.enableVideoProblemFilter(true);
949
-        let reconnectingKey = "connection.RECONNECTING";
950
-        $('#videoConnectionMessage')
951
-            .attr("data-i18n", reconnectingKey)
952
-            .text(APP.translation.translateString(reconnectingKey))
953
-            .css({display: "block"});
1010
+        if (largeVideo) {
1011
+            largeVideo.onVideoInterrupted();
1012
+        }
954
     },
1013
     },
955
 
1014
 
956
     /**
1015
     /**
957
      * Indicates that the video has been restored.
1016
      * Indicates that the video has been restored.
958
      */
1017
      */
959
     onVideoRestored () {
1018
     onVideoRestored () {
960
-        this.enableVideoProblemFilter(false);
961
-        $('#videoConnectionMessage').css({display: "none"});
962
-    },
963
-
964
-    enableVideoProblemFilter (enable) {
965
-        if (!largeVideo) {
966
-            return;
1019
+        if (largeVideo) {
1020
+            largeVideo.onVideoRestored();
967
         }
1021
         }
968
-
969
-        largeVideo.enableVideoProblemFilter(enable);
970
     },
1022
     },
971
 
1023
 
972
     isLargeVideoVisible () {
1024
     isLargeVideoVisible () {
994
 
1046
 
995
         if (!isOnLarge || forceUpdate) {
1047
         if (!isOnLarge || forceUpdate) {
996
             let videoType = this.getRemoteVideoType(id);
1048
             let videoType = this.getRemoteVideoType(id);
1049
+            // FIXME video type is not the same thing as container type
997
             if (id !== currentId && videoType === VIDEO_CONTAINER_TYPE) {
1050
             if (id !== currentId && videoType === VIDEO_CONTAINER_TYPE) {
998
                 eventEmitter.emit(UIEvents.SELECTED_ENDPOINT, id);
1051
                 eventEmitter.emit(UIEvents.SELECTED_ENDPOINT, id);
999
             }
1052
             }

+ 7
- 5
modules/UI/welcome_page/WelcomePage.js 查看文件

2
 var animateTimeout, updateTimeout;
2
 var animateTimeout, updateTimeout;
3
 
3
 
4
 var RoomnameGenerator = require("../../util/RoomnameGenerator");
4
 var RoomnameGenerator = require("../../util/RoomnameGenerator");
5
+import UIUtil from "../util/UIUtil";
5
 
6
 
6
 function enter_room() {
7
 function enter_room() {
7
     var val = $("#enter_room_field").val();
8
     var val = $("#enter_room_field").val();
39
             $("#welcome_page_header div[class='watermark leftwatermark']");
40
             $("#welcome_page_header div[class='watermark leftwatermark']");
40
         if(leftWatermarkDiv && leftWatermarkDiv.length > 0) {
41
         if(leftWatermarkDiv && leftWatermarkDiv.length > 0) {
41
             leftWatermarkDiv.css({display: 'block'});
42
             leftWatermarkDiv.css({display: 'block'});
42
-            leftWatermarkDiv.parent().get(0).href =
43
-                interfaceConfig.JITSI_WATERMARK_LINK;
43
+            UIUtil.setLinkHref(
44
+                leftWatermarkDiv.parent(),
45
+                interfaceConfig.JITSI_WATERMARK_LINK);
44
         }
46
         }
45
-
46
     }
47
     }
47
 
48
 
48
     if (interfaceConfig.SHOW_BRAND_WATERMARK) {
49
     if (interfaceConfig.SHOW_BRAND_WATERMARK) {
50
             $("#welcome_page_header div[class='watermark rightwatermark']");
51
             $("#welcome_page_header div[class='watermark rightwatermark']");
51
         if(rightWatermarkDiv && rightWatermarkDiv.length > 0) {
52
         if(rightWatermarkDiv && rightWatermarkDiv.length > 0) {
52
             rightWatermarkDiv.css({display: 'block'});
53
             rightWatermarkDiv.css({display: 'block'});
53
-            rightWatermarkDiv.parent().get(0).href =
54
-                interfaceConfig.BRAND_WATERMARK_LINK;
54
+            UIUtil.setLinkHref(
55
+                rightWatermarkDiv.parent(),
56
+                interfaceConfig.BRAND_WATERMARK_LINK);
55
             rightWatermarkDiv.get(0).style.backgroundImage =
57
             rightWatermarkDiv.get(0).style.backgroundImage =
56
                 "url(images/rightwatermark.png)";
58
                 "url(images/rightwatermark.png)";
57
         }
59
         }

+ 1
- 8
modules/keyboardshortcut/keyboardshortcut.js 查看文件

74
                 }
74
                 }
75
             }
75
             }
76
         };
76
         };
77
-        $('body').popover({ selector: '[data-toggle=popover]',
78
-            trigger: 'click hover',
79
-            content: function() {
80
-                return this.getAttribute("content")
81
-                    + self._getShortcutTooltip(this.getAttribute("shortcut"));
82
-            }
83
-        });
84
     },
77
     },
85
 
78
 
86
     /**
79
     /**
128
      * or an empty string if the shortcutAttr is null, an empty string or not
121
      * or an empty string if the shortcutAttr is null, an empty string or not
129
      * found in the shortcut mapping
122
      * found in the shortcut mapping
130
      */
123
      */
131
-    _getShortcutTooltip: function (shortcutAttr) {
124
+    getShortcutTooltip: function (shortcutAttr) {
132
         if (typeof shortcutAttr === "string" && shortcutAttr.length > 0) {
125
         if (typeof shortcutAttr === "string" && shortcutAttr.length > 0) {
133
             for (var key in _shortcuts) {
126
             for (var key in _shortcuts) {
134
                 if (_shortcuts.hasOwnProperty(key)
127
                 if (_shortcuts.hasOwnProperty(key)

+ 8
- 4
modules/settings/Settings.js 查看文件

174
      * Set device id of the camera which is currently in use.
174
      * Set device id of the camera which is currently in use.
175
      * Empty string stands for default device.
175
      * Empty string stands for default device.
176
      * @param {string} newId new camera device id
176
      * @param {string} newId new camera device id
177
+     * @param {boolean} whether we need to store the value
177
      */
178
      */
178
-    setCameraDeviceId: function (newId = '') {
179
+    setCameraDeviceId: function (newId, store) {
179
         cameraDeviceId = newId;
180
         cameraDeviceId = newId;
180
-        window.localStorage.cameraDeviceId = newId;
181
+        if (store)
182
+            window.localStorage.cameraDeviceId = newId;
181
     },
183
     },
182
 
184
 
183
     /**
185
     /**
192
      * Set device id of the microphone which is currently in use.
194
      * Set device id of the microphone which is currently in use.
193
      * Empty string stands for default device.
195
      * Empty string stands for default device.
194
      * @param {string} newId new microphone device id
196
      * @param {string} newId new microphone device id
197
+     * @param {boolean} whether we need to store the value
195
      */
198
      */
196
-    setMicDeviceId: function (newId = '') {
199
+    setMicDeviceId: function (newId, store) {
197
         micDeviceId = newId;
200
         micDeviceId = newId;
198
-        window.localStorage.micDeviceId = newId;
201
+        if (store)
202
+            window.localStorage.micDeviceId = newId;
199
     },
203
     },
200
 
204
 
201
     /**
205
     /**

+ 14
- 6
package.json 查看文件

16
   "readmeFilename": "README.md",
16
   "readmeFilename": "README.md",
17
   "//": "Callstats.io does not work with recent versions of jsSHA (2.0.1 in particular)",
17
   "//": "Callstats.io does not work with recent versions of jsSHA (2.0.1 in particular)",
18
   "dependencies": {
18
   "dependencies": {
19
+    "@atlassian/aui": "^6.0.0",
19
     "async": "0.9.0",
20
     "async": "0.9.0",
20
     "autosize": "^1.18.13",
21
     "autosize": "^1.18.13",
21
     "bootstrap": "3.1.1",
22
     "bootstrap": "3.1.1",
22
     "events": "*",
23
     "events": "*",
23
     "i18next-client": "1.7.7",
24
     "i18next-client": "1.7.7",
24
-    "jquery": "~2.1.1",
25
     "jQuery-Impromptu": "git+https://github.com/trentrichardson/jQuery-Impromptu.git#v6.0.0",
25
     "jQuery-Impromptu": "git+https://github.com/trentrichardson/jQuery-Impromptu.git#v6.0.0",
26
-    "lib-jitsi-meet": "git+https://github.com/jitsi/lib-jitsi-meet.git",
26
+    "jquery": "~2.1.1",
27
     "jquery-contextmenu": "*",
27
     "jquery-contextmenu": "*",
28
     "jquery-ui": "1.10.5",
28
     "jquery-ui": "1.10.5",
29
     "jssha": "1.5.0",
29
     "jssha": "1.5.0",
30
+    "jws": "*",
31
+    "lib-jitsi-meet": "git+https://github.com/jitsi/lib-jitsi-meet.git",
32
+    "postis": "^2.2.0",
30
     "retry": "0.6.1",
33
     "retry": "0.6.1",
31
     "strophe": "^1.2.2",
34
     "strophe": "^1.2.2",
32
     "strophejs-plugins": "^0.0.6",
35
     "strophejs-plugins": "^0.0.6",
33
-    "toastr": "^2.0.3",
34
-    "postis": "^2.2.0",
35
-    "jws": "*"
36
+    "toastr": "^2.0.3"
36
   },
37
   },
37
   "devDependencies": {
38
   "devDependencies": {
38
     "babel-polyfill": "*",
39
     "babel-polyfill": "*",
83
     "tooltip": "./node_modules/bootstrap/js/tooltip.js",
84
     "tooltip": "./node_modules/bootstrap/js/tooltip.js",
84
     "popover": "./node_modules/bootstrap/js/popover.js",
85
     "popover": "./node_modules/bootstrap/js/popover.js",
85
     "jQuery-Impromptu": "./node_modules/jQuery-Impromptu/dist/jquery-impromptu.js",
86
     "jQuery-Impromptu": "./node_modules/jQuery-Impromptu/dist/jquery-impromptu.js",
86
-    "autosize": "./node_modules/autosize/build/jquery.autosize.js"
87
+    "autosize": "./node_modules/autosize/build/jquery.autosize.js",
88
+    "aui": "./node_modules/@atlassian/aui/dist/aui/js/aui.js",
89
+    "aui-experimental": "./node_modules/@atlassian/aui/dist/aui/js/aui-experimental.js",
90
+    "aui-css": "./node_modules/@atlassian/aui/dist/aui/css/aui.min.css",
91
+    "aui-experimental-css": "./node_modules/@atlassian/aui/dist/aui/css/aui-experimental.min.css"
87
   },
92
   },
88
   "browserify-shim": {
93
   "browserify-shim": {
89
     "jquery": [
94
     "jquery": [
109
     "jQuery-Impromptu": {
114
     "jQuery-Impromptu": {
110
       "depends": "jquery:jQuery"
115
       "depends": "jquery:jQuery"
111
     },
116
     },
117
+    "aui-experimental": {
118
+      "depends": "aui:AJS"
119
+    },
112
     "jquery-contextmenu": {
120
     "jquery-contextmenu": {
113
       "depends": "jquery:jQuery"
121
       "depends": "jquery:jQuery"
114
     },
122
     },

+ 1
- 1
prosody-plugins/mod_token_verification.lua 查看文件

60
 
60
 
61
 	local token = session.auth_token;
61
 	local token = session.auth_token;
62
 	local auth_room = session.jitsi_meet_room;
62
 	local auth_room = session.jitsi_meet_room;
63
-	if room ~= auth_room and disableRoomNameConstraints ~= true then
63
+	if disableRoomNameConstraints ~= true and room ~= string.lower(auth_room) then
64
 		log("error", "Token %s not allowed to join: %s",
64
 		log("error", "Token %s not allowed to join: %s",
65
 			tostring(token), tostring(auth_room));
65
 			tostring(token), tostring(auth_room));
66
 		session.send(
66
 		session.send(

+ 11
- 2
service/UI/UIEvents.js 查看文件

29
      */
29
      */
30
     UPDATE_SHARED_VIDEO: "UI.update_shared_video",
30
     UPDATE_SHARED_VIDEO: "UI.update_shared_video",
31
     ROOM_LOCK_CLICKED: "UI.room_lock_clicked",
31
     ROOM_LOCK_CLICKED: "UI.room_lock_clicked",
32
-    USER_INVITED: "UI.user_invited",
33
     USER_KICKED: "UI.user_kicked",
32
     USER_KICKED: "UI.user_kicked",
34
     REMOTE_AUDIO_MUTED: "UI.remote_audio_muted",
33
     REMOTE_AUDIO_MUTED: "UI.remote_audio_muted",
35
     FULLSCREEN_TOGGLE: "UI.fullscreen_toggle",
34
     FULLSCREEN_TOGGLE: "UI.fullscreen_toggle",
105
      * event must contain the identifier of the container that has been toggled
104
      * event must contain the identifier of the container that has been toggled
106
      * and information about toggle on or off.
105
      * and information about toggle on or off.
107
      */
106
      */
108
-    SIDE_TOOLBAR_CONTAINER_TOGGLED: "UI.side_container_toggled"
107
+    SIDE_TOOLBAR_CONTAINER_TOGGLED: "UI.side_container_toggled",
108
+
109
+    /**
110
+     * Notifies that the raise hand has been changed.
111
+     */
112
+    LOCAL_RAISE_HAND_CHANGED: "UI.local_raise_hand_changed",
113
+
114
+    /**
115
+     * Notifies that the avatar is displayed or not on the largeVideo.
116
+     */
117
+    LARGE_VIDEO_AVATAR_DISPLAYED: "UI.large_video_avatar_displayed"
109
 };
118
 };

+ 6
- 3
service/translation/languages.js 查看文件

9
         return languages;
9
         return languages;
10
     },
10
     },
11
     EN: "en",
11
     EN: "en",
12
+
12
     BG: "bg",
13
     BG: "bg",
13
     DE: "de",
14
     DE: "de",
14
-    TR: "tr",
15
-    FR: "fr",
16
     ES: "es",
15
     ES: "es",
16
+    FR: "fr",
17
     HY: "hy",
17
     HY: "hy",
18
     IT: "it",
18
     IT: "it",
19
     OC: "oc",
19
     OC: "oc",
20
+    PL: "pl",
20
     PTBR: "ptBR",
21
     PTBR: "ptBR",
22
+    RU: "ru",
21
     SK: "sk",
23
     SK: "sk",
22
     SL: "sl",
24
     SL: "sl",
23
-    SV: "sv"
25
+    SV: "sv",
26
+    TR: "tr"
24
 };
27
 };

Loading…
取消
儲存