Преглед изворни кода

feat(quality-slider): initial implementation (#1817)

* feat(quality-slider): initial implementation

- Add new menu button with an Inline Dialog slider for
  selecting received video quality.
- Place P2P status in redux store for the Inline Dialog
  to display a warning about not respecting video quality
  selection.
- Respond to data channel open events by setting receive
  video quality. This is for lonely call cases where a
  setting is set before the data channel is open.
- Remove dropdown menu from video status label and clean
  up related js and css.

* first pass at addressing feedback

- Move VideoStatusLabel to video-quality directory.
- Rename VideoStatusLabel to VideoQualityLabel.
- Open VideoQualitydialog from VideoQualityLabel.
- New CSS for making VideoQualityLabel display properly.
- Do not render VideoQualityLabel in filmstrip only instead of hiding with css.
- Remove tooltip from VideoQualityLabel.
- Show LD, SD, HD labels in VideoQualityLabel.
- Remove action SET_LARGE_VIDEO_HD_STATUS from conference.
- Create new action UPDATE_KNOWN_LARGE_VIDEO_RESOLUTION in large-video.
- Move VideoQualityButton into video-quality directory.
- General renaming (medium -> standard, menu -> dialog).
- Render P2P message between title and slider.
- Add padding to slider for displacement caused by P2P message's new placement.
- Fix display issue with VideoQualityButton displaying out of line in the
  primary toolbar.

* second pass at addressing feedback

- Fix p2p inline message color
- Force labels to break on words
- Resolve rebase issues, including only dispatching quality
  update on change. Before there was double calling of dispatch
  produced by an IE11 workaround. This breaks now when setting
  audio only mode to true twice.
- Rename some instances of quality to definition

* rename to data channel opened

* do not show p2p in audio only

* stop toggle audio only icon automatically

* remove fixme about toolbar button

* find closest resolution for label

* toggle dialog on button click

* redo last commit for both button and label
master
virtuacoplenny пре 7 година
родитељ
комит
d8cd3e75b4
28 измењених фајлова са 1047 додато и 234 уклоњено
  1. 15
    1
      conference.js
  2. 8
    4
      css/_toolbars.scss
  3. 0
    108
      css/_videolayout_default.scss
  4. 1
    0
      css/main.scss
  5. 166
    0
      css/modals/video-quality/_video-quality.scss
  6. 1
    1
      interface_config.js
  7. 12
    4
      lang/main.json
  8. 10
    5
      modules/UI/videolayout/LargeVideoManager.js
  9. 30
    9
      react/features/base/conference/actionTypes.js
  10. 47
    17
      react/features/base/conference/actions.js
  11. 12
    0
      react/features/base/conference/constants.js
  12. 59
    2
      react/features/base/conference/middleware.js
  13. 43
    14
      react/features/base/conference/reducer.js
  14. 11
    0
      react/features/large-video/actionTypes.js
  15. 20
    1
      react/features/large-video/actions.js
  16. 4
    2
      react/features/large-video/components/LargeVideo.web.js
  17. 10
    1
      react/features/large-video/reducer.js
  18. 1
    0
      react/features/toolbox/components/index.js
  19. 6
    0
      react/features/toolbox/defaultToolbarButtons.js
  20. 0
    0
      react/features/video-quality/components/VideoQualityButton.native.js
  21. 153
    0
      react/features/video-quality/components/VideoQualityButton.web.js
  22. 0
    0
      react/features/video-quality/components/VideoQualityDialog.native.js
  23. 324
    0
      react/features/video-quality/components/VideoQualityDialog.web.js
  24. 0
    0
      react/features/video-quality/components/VideoQualityLabel.native.js
  25. 111
    64
      react/features/video-quality/components/VideoQualityLabel.web.js
  26. 3
    0
      react/features/video-quality/components/index.js
  27. 0
    0
      react/features/video-quality/index.js
  28. 0
    1
      react/features/video-status-label/components/index.js

+ 15
- 1
conference.js Прегледај датотеку

@@ -25,9 +25,11 @@ import {
25 25
     conferenceFailed,
26 26
     conferenceJoined,
27 27
     conferenceLeft,
28
+    dataChannelOpened,
28 29
     toggleAudioOnly,
29 30
     EMAIL_COMMAND,
30
-    lockStateChanged
31
+    lockStateChanged,
32
+    p2pStatusChanged
31 33
 } from './react/features/base/conference';
32 34
 import { updateDeviceList } from './react/features/base/devices';
33 35
 import {
@@ -1704,6 +1706,12 @@ export default {
1704 1706
                 APP.UI.handleLastNEndpoints(leavingIds, enteringIds);
1705 1707
         });
1706 1708
 
1709
+        room.on(
1710
+            ConferenceEvents.P2P_STATUS,
1711
+            (jitsiConference, p2p) => {
1712
+                APP.store.dispatch(p2pStatusChanged(p2p));
1713
+        });
1714
+
1707 1715
         room.on(
1708 1716
             ConferenceEvents.PARTICIPANT_CONN_STATUS_CHANGED,
1709 1717
             (id, connectionStatus) => {
@@ -1952,6 +1960,12 @@ export default {
1952 1960
             }
1953 1961
         );
1954 1962
 
1963
+        room.on(
1964
+            ConferenceEvents.DATA_CHANNEL_OPENED, () => {
1965
+                APP.store.dispatch(dataChannelOpened());
1966
+            }
1967
+        );
1968
+
1955 1969
         // call hangup
1956 1970
         APP.UI.addListener(UIEvents.HANGUP, () => {
1957 1971
             this.hangup(true);

+ 8
- 4
css/_toolbars.scss Прегледај датотеку

@@ -42,6 +42,14 @@
42 42
     position: relative;
43 43
     z-index: $toolbarZ;
44 44
 
45
+    /**
46
+     * Ensure nested elements that don't have a button class, maybe because they
47
+     * are wrapped in a React Element, still display in a line.
48
+     */
49
+    > div {
50
+        display: inline-block;
51
+    }
52
+
45 53
     /**
46 54
      * Toolbar button styles.
47 55
      */
@@ -94,10 +102,6 @@
94 102
             &.icon-microphone {
95 103
                 @extend .icon-mic-disabled;
96 104
             }
97
-
98
-            &.icon-visibility {
99
-                @extend .icon-visibility-off;
100
-            }
101 105
         }
102 106
 
103 107
         &.unclickable {

+ 0
- 108
css/_videolayout_default.scss Прегледај датотеку

@@ -535,111 +535,3 @@
535 535
                     1px 0px 1px rgba(0,0,0,0.3),
536 536
                     0px 0px 1px rgba(0,0,0,0.3);
537 537
 }
538
-
539
-.filmstrip-only {
540
-    #videoResolutionLabel {
541
-        display: none;
542
-    }
543
-}
544
-.video-state-indicator {
545
-    background: $videoStateIndicatorBackground;
546
-    color: $videoStateIndicatorColor;
547
-    cursor: pointer;
548
-    font-size: 13px;
549
-    height: 40px;
550
-    line-height: 20px;
551
-    text-align: center;
552
-    min-width: 40px;
553
-    padding: 10px 5px;
554
-    border-radius: 50%;
555
-    position: absolute;
556
-    box-sizing: border-box;
557
-
558
-    i {
559
-        cursor: pointer;
560
-    }
561
-}
562
-
563
-#videoResolutionLabel,
564
-.centeredVideoLabel.moveToCorner {
565
-    z-index: $tooltipsZ;
566
-}
567
-
568
-.centeredVideoLabel {
569
-    bottom: 45%;
570
-    border-radius: 2px;
571
-    display: none;
572
-    -webkit-transition: all 2s 2s linear;
573
-    transition: all 2s 2s linear;
574
-    z-index: $centeredVideoLabelZ;
575
-
576
-    &.moveToCorner {
577
-        bottom: auto;
578
-    }
579
-}
580
-
581
-.moveToCorner {
582
-    position: absolute;
583
-    top: 30px;
584
-    right: 30px;
585
-}
586
-
587
-.moveToCorner + .moveToCorner {
588
-    right: 80px;
589
-}
590
-
591
-.video-state-indicator-menu {
592
-    display: none;
593
-    padding: 10px;
594
-    position: absolute;
595
-    right: -10px;
596
-    top: 20px;
597
-
598
-    .video-state-indicator-menu-options {
599
-        background: $popoverBg;
600
-        border-radius: 3px;
601
-        color: $popoverFontColor;
602
-        margin-top: 20px;
603
-        padding: 5px 0;
604
-        position: relative;
605
-
606
-        div {
607
-            cursor: pointer;
608
-            padding: 10px;
609
-            padding-right: 30px;
610
-            text-align: left;
611
-            white-space: nowrap;
612
-
613
-            &.active {
614
-                background: $toolbarToggleBackground;
615
-            }
616
-            &:hover:not(.active) {
617
-                background: $popupMenuSelectedItemBackground;
618
-            }
619
-
620
-            i {
621
-                margin-right: 5px;
622
-                vertical-align: middle;
623
-            }
624
-        }
625
-    }
626
-
627
-    .video-state-indicator-menu-options::after {
628
-        content: " ";
629
-        border-color: transparent transparent $popoverBg transparent;
630
-        border-style: solid;
631
-        border-width: 5px;
632
-        position: absolute;
633
-        right: 15px;
634
-        top: -10px;
635
-    }
636
-}
637
-
638
-.video-state-indicator:hover,
639
-.video-state-indicator *:hover {
640
-    background: $toolbarSelectBackground;
641
-
642
-    .video-state-indicator-menu {
643
-        display: block;
644
-    }
645
-}

+ 1
- 0
css/main.scss Прегледај датотеку

@@ -43,6 +43,7 @@
43 43
 @import 'modals/dialog';
44 44
 @import 'modals/feedback/feedback';
45 45
 @import 'modals/speaker_stats/speaker_stats';
46
+@import 'modals/video-quality/video-quality';
46 47
 @import 'videolayout_default';
47 48
 @import 'notice';
48 49
 @import 'popup_menu';

+ 166
- 0
css/modals/video-quality/_video-quality.scss Прегледај датотеку

@@ -0,0 +1,166 @@
1
+.video-quality-dialog {
2
+    color: $modalTextColor;
3
+
4
+    .video-quality-dialog-title {
5
+        margin-bottom: 10px;
6
+    }
7
+
8
+    .video-quality-dialog-contents {
9
+        align-items: center;
10
+        color: $modalTextColor;
11
+        display: flex;
12
+        flex-direction: column;
13
+        padding: 10px;
14
+        min-width: 250px;
15
+
16
+        .video-quality-dialog-slider-container {
17
+            width: 100%;
18
+            text-align: center;
19
+        }
20
+
21
+        .video-quality-dialog-slider {
22
+            width: calc(100% - 5px);
23
+
24
+            @mixin sliderTrackStyles() {
25
+                height: 15px;
26
+                border-radius: 10px;
27
+                background: black;
28
+            }
29
+
30
+            &::-ms-track {
31
+                @include sliderTrackStyles();
32
+            }
33
+
34
+            &::-moz-range-track {
35
+                @include sliderTrackStyles();
36
+            }
37
+
38
+            &::-webkit-slider-runnable-track {
39
+                @include sliderTrackStyles();
40
+            }
41
+
42
+            @mixin sliderThumbStyles() {
43
+                top: 50%;
44
+                border: none;
45
+                position: relative;
46
+                opacity: 0;
47
+            }
48
+
49
+            &::-ms-thumb {
50
+                @include sliderThumbStyles();
51
+            }
52
+
53
+            &::-moz-range-thumb {
54
+                @include sliderThumbStyles();
55
+
56
+            }
57
+
58
+            &::-webkit-slider-thumb {
59
+                @include sliderThumbStyles();
60
+            }
61
+        }
62
+
63
+        .video-quality-dialog-labels {
64
+            box-sizing: border-box;
65
+            display: flex;
66
+            margin-top: 5px;
67
+            position: relative;
68
+            width: 90%;
69
+        }
70
+
71
+        .video-quality-dialog-label-container {
72
+            position: absolute;
73
+            text-align: center;
74
+            transform: translate(-50%, 0%);
75
+
76
+            &::before {
77
+                background: rgb(140, 156, 189);
78
+                content: '';
79
+                border-radius: 50%;
80
+                left: 0;
81
+                height: 6px;
82
+                margin: 0 auto;
83
+                pointer-events: none;
84
+                position: absolute;
85
+                right: 0;
86
+                top: -16px;
87
+                width: 6px;
88
+            }
89
+        }
90
+
91
+        .video-quality-dialog-label-container.active {
92
+            color: $toolbarToggleBackground;
93
+
94
+            &::before {
95
+                background: $toolbarToggleBackground;
96
+                height: 12px;
97
+                top: -19px;
98
+                width: 12px;
99
+            }
100
+        }
101
+
102
+        .video-quality-dialog-label-container:first-child {
103
+            position: relative;
104
+        }
105
+
106
+        .video-quality-dialog-label {
107
+            display: table-caption;
108
+            word-spacing: unset;
109
+        }
110
+    }
111
+}
112
+
113
+.video-state-indicator {
114
+    background: $videoStateIndicatorBackground;
115
+    color: $videoStateIndicatorColor;
116
+    cursor: default;
117
+    font-size: 13px;
118
+    height: 40px;
119
+    line-height: 20px;
120
+    text-align: left;
121
+    min-width: 40px;
122
+    border-radius: 50%;
123
+    position: absolute;
124
+    box-sizing: border-box;
125
+
126
+    i {
127
+        cursor: pointer;
128
+    }
129
+
130
+    /**
131
+     * Give the label padding so it has more volume and can be easily clicked.
132
+     */
133
+    .video-quality-label-status {
134
+        cursor: pointer;
135
+        padding: 10px 5px;
136
+        text-align: center;
137
+    }
138
+}
139
+
140
+#videoResolutionLabel,
141
+.centeredVideoLabel.moveToCorner {
142
+    z-index: $tooltipsZ;
143
+}
144
+
145
+.centeredVideoLabel {
146
+    bottom: 45%;
147
+    border-radius: 2px;
148
+    display: none;
149
+    -webkit-transition: all 2s 2s linear;
150
+    transition: all 2s 2s linear;
151
+    z-index: $centeredVideoLabelZ;
152
+
153
+    &.moveToCorner {
154
+        bottom: auto;
155
+    }
156
+}
157
+
158
+.moveToCorner {
159
+    position: absolute;
160
+    top: 30px;
161
+    right: 30px;
162
+}
163
+
164
+.moveToCorner + .moveToCorner {
165
+    right: 80px;
166
+}

+ 1
- 1
interface_config.js Прегледај датотеку

@@ -35,7 +35,7 @@ var interfaceConfig = { // eslint-disable-line no-unused-vars
35 35
         //main toolbar
36 36
         'microphone', 'camera', 'desktop', 'invite', 'fullscreen', 'fodeviceselection', 'hangup', // jshint ignore:line
37 37
         //extended toolbar
38
-        'profile', 'addtocall', 'contacts', 'chat', 'recording', 'etherpad', 'sharedvideo', 'dialout', 'settings', 'raisehand', 'filmstrip'], // jshint ignore:line
38
+        'profile', 'addtocall', 'contacts', 'chat', 'recording', 'etherpad', 'sharedvideo', 'dialout', 'settings', 'raisehand', 'videoquality', 'filmstrip'], // jshint ignore:line
39 39
     /**
40 40
      * Main Toolbar Buttons
41 41
      * All of them should be in TOOLBAR_BUTTONS

+ 12
- 4
lang/main.json Прегледај датотеку

@@ -449,10 +449,18 @@
449 449
         "unlocked": "This call is unlocked. Any new caller with the link may join the call."
450 450
     },
451 451
     "videoStatus": {
452
-      "hd": "HD",
453
-      "hdVideo": "HD video",
454
-      "sd": "SD",
455
-      "sdVideo": "SD video"
452
+        "callQuality": "Call Quality",
453
+        "changeVideoTip": "Change your video quality from the left toolbar.",
454
+        "hd": "HD",
455
+        "highDefinition": "High definition",
456
+        "ld": "LD",
457
+        "lowDefinition": "Low definition",
458
+        "p2pEnabled": "Peer to Peer Enabled",
459
+        "p2pVideoQualityDescription": "In peer to peer mode, received call quality can only be toggled between high and audio only. Other settings will not be honored until peer to peer is exited.",
460
+        "recHighDefinitionOnly": "Will prefer high definition.",
461
+        "sd": "SD",
462
+        "standardDefinition": "Standard definition",
463
+        "qualityButtonTip": "Change received video quality"
456 464
     },
457 465
     "dialOut": {
458 466
         "dial": "Dial",

+ 10
- 5
modules/UI/videolayout/LargeVideoManager.js Прегледај датотеку

@@ -1,4 +1,4 @@
1
-/* global $, APP, config, JitsiMeetJS */
1
+/* global $, APP, JitsiMeetJS */
2 2
 /* eslint-disable no-unused-vars */
3 3
 import React from 'react';
4 4
 import ReactDOM from 'react-dom';
@@ -9,7 +9,9 @@ import { PresenceLabel } from '../../../react/features/presence-status';
9 9
 
10 10
 const logger = require("jitsi-meet-logger").getLogger(__filename);
11 11
 
12
-import { setLargeVideoHDStatus } from '../../../react/features/base/conference';
12
+import {
13
+    updateKnownLargeVideoResolution
14
+} from '../../../react/features/large-video';
13 15
 
14 16
 import Avatar from "../avatar/Avatar";
15 17
 import {createDeferred} from '../../util/helpers';
@@ -659,10 +661,13 @@ export default class LargeVideoManager {
659 661
      */
660 662
     _onVideoResolutionUpdate() {
661 663
         const { height, width } = this.videoContainer.getStreamSize();
662
-        const currentAspectRatio = width/ height;
663
-        const isCurrentlyHD = Math.min(height, width) >= config.minHDHeight;
664
+        const { resolution } = APP.store.getState()['features/large-video'];
664 665
 
665
-        APP.store.dispatch(setLargeVideoHDStatus(isCurrentlyHD));
666
+        if (height !== resolution) {
667
+            APP.store.dispatch(updateKnownLargeVideoResolution(height));
668
+        }
669
+
670
+        const currentAspectRatio = width / height;
666 671
 
667 672
         if (this._videoAspectRatio !== currentAspectRatio) {
668 673
             this._videoAspectRatio = currentAspectRatio;

+ 30
- 9
react/features/base/conference/actionTypes.js Прегледај датотеку

@@ -52,6 +52,16 @@ export const CONFERENCE_WILL_JOIN = Symbol('CONFERENCE_WILL_JOIN');
52 52
  */
53 53
 export const CONFERENCE_WILL_LEAVE = Symbol('CONFERENCE_WILL_LEAVE');
54 54
 
55
+/**
56
+ * The type of (redux) action which signals that the data channel with the
57
+ * bridge has been established.
58
+ *
59
+ * {
60
+ *     type: DATA_CHANNEL_OPENED
61
+ * }
62
+ */
63
+export const DATA_CHANNEL_OPENED = Symbol('DATA_CHANNEL_OPENED');
64
+
55 65
 /**
56 66
  * The type of (redux) action which signals that the lock state of a specific
57 67
  * {@code JitsiConference} changed.
@@ -65,26 +75,26 @@ export const CONFERENCE_WILL_LEAVE = Symbol('CONFERENCE_WILL_LEAVE');
65 75
 export const LOCK_STATE_CHANGED = Symbol('LOCK_STATE_CHANGED');
66 76
 
67 77
 /**
68
- * The type of (redux) action which sets the audio-only flag for the current
78
+ * The type of (redux) action which sets the peer2peer flag for the current
69 79
  * conference.
70 80
  *
71 81
  * {
72
- *     type: SET_AUDIO_ONLY,
73
- *     audioOnly: boolean
82
+ *     type: P2P_STATUS_CHANGED,
83
+ *     p2p: boolean
74 84
  * }
75 85
  */
76
-export const SET_AUDIO_ONLY = Symbol('SET_AUDIO_ONLY');
86
+export const P2P_STATUS_CHANGED = Symbol('P2P_STATUS_CHANGED');
77 87
 
78 88
 /**
79
- * The type of (redux) action to set whether or not the displayed large video is
80
- * in high-definition.
89
+ * The type of (redux) action which sets the audio-only flag for the current
90
+ * conference.
81 91
  *
82 92
  * {
83
- *     type: SET_LARGE_VIDEO_HD_STATUS,
84
- *     isLargeVideoHD: boolean
93
+ *     type: SET_AUDIO_ONLY,
94
+ *     audioOnly: boolean
85 95
  * }
86 96
  */
87
-export const SET_LARGE_VIDEO_HD_STATUS = Symbol('SET_LARGE_VIDEO_HD_STATUS');
97
+export const SET_AUDIO_ONLY = Symbol('SET_AUDIO_ONLY');
88 98
 
89 99
 /**
90 100
  * The type of (redux) action which sets the video channel's lastN (value).
@@ -120,6 +130,17 @@ export const SET_PASSWORD = Symbol('SET_PASSWORD');
120 130
  */
121 131
 export const SET_PASSWORD_FAILED = Symbol('SET_PASSWORD_FAILED');
122 132
 
133
+/**
134
+ * The type of (redux) action which sets the maximum video size should be
135
+ * received from remote participants.
136
+ *
137
+ * {
138
+ *     type: SET_RECEIVE_VIDEO_QUALITY,
139
+ *     receiveVideoQuality: number
140
+ * }
141
+ */
142
+export const SET_RECEIVE_VIDEO_QUALITY = Symbol('SET_RECEIVE_VIDEO_QUALITY');
143
+
123 144
 /**
124 145
  * The type of (redux) action which sets the name of the room of the
125 146
  * conference to be joined.

+ 47
- 17
react/features/base/conference/actions.js Прегледај датотеку

@@ -17,12 +17,14 @@ import {
17 17
     CONFERENCE_LEFT,
18 18
     CONFERENCE_WILL_JOIN,
19 19
     CONFERENCE_WILL_LEAVE,
20
+    DATA_CHANNEL_OPENED,
20 21
     LOCK_STATE_CHANGED,
22
+    P2P_STATUS_CHANGED,
21 23
     SET_AUDIO_ONLY,
22
-    SET_LARGE_VIDEO_HD_STATUS,
23 24
     SET_LASTN,
24 25
     SET_PASSWORD,
25 26
     SET_PASSWORD_FAILED,
27
+    SET_RECEIVE_VIDEO_QUALITY,
26 28
     SET_ROOM
27 29
 } from './actionTypes';
28 30
 import {
@@ -286,6 +288,19 @@ export function createConference() {
286 288
     };
287 289
 }
288 290
 
291
+/**
292
+ * Signals the data channel with the bridge has successfully opened.
293
+ *
294
+ * @returns {{
295
+ *     type: DATA_CHANNEL_OPENED
296
+ * }}
297
+ */
298
+export function dataChannelOpened() {
299
+    return {
300
+        type: DATA_CHANNEL_OPENED
301
+    };
302
+}
303
+
289 304
 /**
290 305
  * Signals that the lock state of a specific JitsiConference changed.
291 306
  *
@@ -308,36 +323,35 @@ export function lockStateChanged(conference, locked) {
308 323
 }
309 324
 
310 325
 /**
311
- * Sets the audio-only flag for the current JitsiConference.
326
+ * Sets whether or not peer2peer is currently enabled.
312 327
  *
313
- * @param {boolean} audioOnly - True if the conference should be audio only;
314
- * false, otherwise.
328
+ * @param {boolean} p2p - Whether or not peer2peer is currently active.
315 329
  * @returns {{
316
- *     type: SET_AUDIO_ONLY,
317
- *     audioOnly: boolean
330
+ *     type: P2P_STATUS_CHANGED,
331
+ *     p2p: boolean
318 332
  * }}
319 333
  */
320
-export function setAudioOnly(audioOnly) {
334
+export function p2pStatusChanged(p2p) {
321 335
     return {
322
-        type: SET_AUDIO_ONLY,
323
-        audioOnly
336
+        type: P2P_STATUS_CHANGED,
337
+        p2p
324 338
     };
325 339
 }
326 340
 
327 341
 /**
328
- * Action to set whether or not the currently displayed large video is in
329
- * high-definition.
342
+ * Sets the audio-only flag for the current JitsiConference.
330 343
  *
331
- * @param {boolean} isLargeVideoHD - True if the large video is high-definition.
344
+ * @param {boolean} audioOnly - True if the conference should be audio only;
345
+ * false, otherwise.
332 346
  * @returns {{
333
- *     type: SET_LARGE_VIDEO_HD_STATUS,
334
- *     isLargeVideoHD: boolean
347
+ *     type: SET_AUDIO_ONLY,
348
+ *     audioOnly: boolean
335 349
  * }}
336 350
  */
337
-export function setLargeVideoHDStatus(isLargeVideoHD) {
351
+export function setAudioOnly(audioOnly) {
338 352
     return {
339
-        type: SET_LARGE_VIDEO_HD_STATUS,
340
-        isLargeVideoHD
353
+        type: SET_AUDIO_ONLY,
354
+        audioOnly
341 355
     };
342 356
 }
343 357
 
@@ -438,6 +452,22 @@ export function setPassword(conference, method, password) {
438 452
     };
439 453
 }
440 454
 
455
+/**
456
+ * Sets the max frame height to receive from remote participant videos.
457
+ *
458
+ * @param {number} receiveVideoQuality - The max video resolution to receive.
459
+ * @returns {{
460
+ *     type: SET_RECEIVE_VIDEO_QUALITY,
461
+ *     receiveVideoQuality: number
462
+ * }}
463
+ */
464
+export function setReceiveVideoQuality(receiveVideoQuality) {
465
+    return {
466
+        type: SET_RECEIVE_VIDEO_QUALITY,
467
+        receiveVideoQuality
468
+    };
469
+}
470
+
441 471
 /**
442 472
  * Sets (the name of) the room of the conference to be joined.
443 473
  *

+ 12
- 0
react/features/base/conference/constants.js Прегледај датотеку

@@ -34,3 +34,15 @@ export const EMAIL_COMMAND = 'email';
34 34
  * from the outside is not cool but it should suffice for now.
35 35
  */
36 36
 export const JITSI_CONFERENCE_URL_KEY = Symbol('url');
37
+
38
+/**
39
+ * The supported remote video resolutions. The values are currently based on
40
+ * available simulcast layers.
41
+ *
42
+ * @type {object}
43
+ */
44
+export const VIDEO_QUALITY_LEVELS = {
45
+    HIGH: 720,
46
+    STANDARD: 360,
47
+    LOW: 180
48
+};

+ 59
- 2
react/features/base/conference/middleware.js Прегледај датотеку

@@ -14,14 +14,17 @@ import { TRACK_ADDED, TRACK_REMOVED } from '../tracks';
14 14
 import {
15 15
     createConference,
16 16
     setAudioOnly,
17
-    setLastN
17
+    setLastN,
18
+    toggleAudioOnly
18 19
 } from './actions';
19 20
 import {
20 21
     CONFERENCE_FAILED,
21 22
     CONFERENCE_JOINED,
22 23
     CONFERENCE_LEFT,
24
+    DATA_CHANNEL_OPENED,
23 25
     SET_AUDIO_ONLY,
24
-    SET_LASTN
26
+    SET_LASTN,
27
+    SET_RECEIVE_VIDEO_QUALITY
25 28
 } from './actionTypes';
26 29
 import {
27 30
     _addLocalTracksToConference,
@@ -47,6 +50,9 @@ MiddlewareRegistry.register(store => next => action => {
47 50
     case CONFERENCE_JOINED:
48 51
         return _conferenceJoined(store, next, action);
49 52
 
53
+    case DATA_CHANNEL_OPENED:
54
+        return _syncReceiveVideoQuality(store, next, action);
55
+
50 56
     case PIN_PARTICIPANT:
51 57
         return _pinParticipant(store, next, action);
52 58
 
@@ -56,6 +62,9 @@ MiddlewareRegistry.register(store => next => action => {
56 62
     case SET_LASTN:
57 63
         return _setLastN(store, next, action);
58 64
 
65
+    case SET_RECEIVE_VIDEO_QUALITY:
66
+        return _setReceiveVideoQuality(store, next, action);
67
+
59 68
     case TRACK_ADDED:
60 69
     case TRACK_REMOVED:
61 70
         return _trackAddedOrRemoved(store, next, action);
@@ -253,6 +262,33 @@ function _setLastN(store, next, action) {
253 262
     return next(action);
254 263
 }
255 264
 
265
+/**
266
+ * Sets the maximum receive video quality and will turn off audio only mode if
267
+ * enabled.
268
+ *
269
+ * @param {Store} store - The Redux store in which the specified action is being
270
+ * dispatched.
271
+ * @param {Dispatch} next - The Redux dispatch function to dispatch the
272
+ * specified action to the specified store.
273
+ * @param {Action} action - The Redux action SET_RECEIVE_VIDEO_QUALITY which is
274
+ * being dispatched in the specified store.
275
+ * @private
276
+ * @returns {Object} The new state that is the result of the reduction of the
277
+ * specified action.
278
+ */
279
+function _setReceiveVideoQuality(store, next, action) {
280
+    const { audioOnly, conference }
281
+        = store.getState()['features/base/conference'];
282
+
283
+    conference.setReceiverVideoConstraint(action.receiveVideoQuality);
284
+
285
+    if (audioOnly) {
286
+        store.dispatch(toggleAudioOnly());
287
+    }
288
+
289
+    return next(action);
290
+}
291
+
256 292
 /**
257 293
  * Synchronizes local tracks from state with local tracks in JitsiConference
258 294
  * instance.
@@ -282,6 +318,27 @@ function _syncConferenceLocalTracksWithState(store, action) {
282 318
     return promise || Promise.resolve();
283 319
 }
284 320
 
321
+/**
322
+ * Sets the maximum receive video quality.
323
+ *
324
+ * @param {Store} store - The Redux store in which the specified action is being
325
+ * dispatched.
326
+ * @param {Dispatch} next - The Redux dispatch function to dispatch the
327
+ * specified action to the specified store.
328
+ * @param {Action} action - The Redux action DATA_CHANNEL_STATUS_CHANGED which
329
+ * is being dispatched in the specified store.
330
+ * @private
331
+ * @returns {Object} The new state that is the result of the reduction of the
332
+ * specified action.
333
+ */
334
+function _syncReceiveVideoQuality(store, next, action) {
335
+    const state = store.getState()['features/base/conference'];
336
+
337
+    state.conference.setReceiverVideoConstraint(state.receiveVideoQuality);
338
+
339
+    return next(action);
340
+}
341
+
285 342
 /**
286 343
  * Notifies the feature base/conference that the action TRACK_ADDED
287 344
  * or TRACK_REMOVED is being dispatched within a specific redux store.

+ 43
- 14
react/features/base/conference/reducer.js Прегледај датотеку

@@ -10,11 +10,15 @@ import {
10 10
     CONFERENCE_WILL_JOIN,
11 11
     CONFERENCE_WILL_LEAVE,
12 12
     LOCK_STATE_CHANGED,
13
+    P2P_STATUS_CHANGED,
13 14
     SET_AUDIO_ONLY,
14
-    SET_LARGE_VIDEO_HD_STATUS,
15 15
     SET_PASSWORD,
16
+    SET_RECEIVE_VIDEO_QUALITY,
16 17
     SET_ROOM
17 18
 } from './actionTypes';
19
+import {
20
+    VIDEO_QUALITY_LEVELS
21
+} from './constants';
18 22
 import { isRoomValid } from './functions';
19 23
 
20 24
 /**
@@ -41,15 +45,18 @@ ReducerRegistry.register('features/base/conference', (state = {}, action) => {
41 45
     case LOCK_STATE_CHANGED:
42 46
         return _lockStateChanged(state, action);
43 47
 
48
+    case P2P_STATUS_CHANGED:
49
+        return _p2pStatusChanged(state, action);
50
+
44 51
     case SET_AUDIO_ONLY:
45 52
         return _setAudioOnly(state, action);
46 53
 
47
-    case SET_LARGE_VIDEO_HD_STATUS:
48
-        return _setLargeVideoHDStatus(state, action);
49
-
50 54
     case SET_PASSWORD:
51 55
         return _setPassword(state, action);
52 56
 
57
+    case SET_RECEIVE_VIDEO_QUALITY:
58
+        return _setReceiveVideoQuality(state, action);
59
+
53 60
     case SET_ROOM:
54 61
         return _setRoom(state, action);
55 62
     }
@@ -135,7 +142,15 @@ function _conferenceJoined(state, { conference }) {
135 142
          * @type {boolean}
136 143
          */
137 144
         locked,
138
-        passwordRequired: undefined
145
+        passwordRequired: undefined,
146
+
147
+        /**
148
+         * The current resolution restraint on receiving remote video. By
149
+         * default the conference will send the highest level possible.
150
+         *
151
+         * @type number
152
+         */
153
+        receiveVideoQuality: VIDEO_QUALITY_LEVELS.HIGH
139 154
     });
140 155
 }
141 156
 
@@ -229,32 +244,31 @@ function _lockStateChanged(state, { conference, locked }) {
229 244
 }
230 245
 
231 246
 /**
232
- * Reduces a specific Redux action SET_AUDIO_ONLY of the feature
247
+ * Reduces a specific Redux action P2P_STATUS_CHANGED of the feature
233 248
  * base/conference.
234 249
  *
235 250
  * @param {Object} state - The Redux state of the feature base/conference.
236
- * @param {Action} action - The Redux action SET_AUDIO_ONLY to reduce.
251
+ * @param {Action} action - The Redux action P2P_STATUS_CHANGED to reduce.
237 252
  * @private
238 253
  * @returns {Object} The new state of the feature base/conference after the
239 254
  * reduction of the specified action.
240 255
  */
241
-function _setAudioOnly(state, action) {
242
-    return set(state, 'audioOnly', action.audioOnly);
256
+function _p2pStatusChanged(state, action) {
257
+    return set(state, 'p2p', action.p2p);
243 258
 }
244 259
 
245 260
 /**
246
- * Reduces a specific Redux action SET_LARGE_VIDEO_HD_STATUS of the feature
261
+ * Reduces a specific Redux action SET_AUDIO_ONLY of the feature
247 262
  * base/conference.
248 263
  *
249 264
  * @param {Object} state - The Redux state of the feature base/conference.
250
- * @param {Action} action - The Redux action SET_LARGE_VIDEO_HD_STATUS to
251
- * reduce.
265
+ * @param {Action} action - The Redux action SET_AUDIO_ONLY to reduce.
252 266
  * @private
253 267
  * @returns {Object} The new state of the feature base/conference after the
254 268
  * reduction of the specified action.
255 269
  */
256
-function _setLargeVideoHDStatus(state, action) {
257
-    return set(state, 'isLargeVideoHD', action.isLargeVideoHD);
270
+function _setAudioOnly(state, action) {
271
+    return set(state, 'audioOnly', action.audioOnly);
258 272
 }
259 273
 
260 274
 /**
@@ -294,6 +308,21 @@ function _setPassword(state, { conference, method, password }) {
294 308
     return state;
295 309
 }
296 310
 
311
+/**
312
+ * Reduces a specific Redux action SET_RECEIVE_VIDEO_QUALITY of the feature
313
+ * base/conference.
314
+ *
315
+ * @param {Object} state - The Redux state of the feature base/conference.
316
+ * @param {Action} action - The Redux action SET_RECEIVE_VIDEO_QUALITY to
317
+ * reduce.
318
+ * @private
319
+ * @returns {Object} The new state of the feature base/conference after the
320
+ * reduction of the specified action.
321
+ */
322
+function _setReceiveVideoQuality(state, action) {
323
+    return set(state, 'receiveVideoQuality', action.receiveVideoQuality);
324
+}
325
+
297 326
 /**
298 327
  * Reduces a specific Redux action SET_ROOM of the feature base/conference.
299 328
  *

+ 11
- 0
react/features/large-video/actionTypes.js Прегледај датотеку

@@ -8,3 +8,14 @@
8 8
  */
9 9
 export const SELECT_LARGE_VIDEO_PARTICIPANT
10 10
     = Symbol('SELECT_LARGE_VIDEO_PARTICIPANT');
11
+
12
+/**
13
+ * Action to update the redux store with the current resolution of large video.
14
+ *
15
+ * @returns {{
16
+ *     type: UPDATE_KNOWN_LARGE_VIDEO_RESOLUTION,
17
+ *     resolution: number
18
+ * }}
19
+ */
20
+export const UPDATE_KNOWN_LARGE_VIDEO_RESOLUTION
21
+    = Symbol('UPDATE_KNOWN_LARGE_VIDEO_RESOLUTION');

+ 20
- 1
react/features/large-video/actions.js Прегледај датотеку

@@ -5,7 +5,10 @@ import {
5 5
     getTrackByMediaTypeAndParticipant
6 6
 } from '../base/tracks';
7 7
 
8
-import { SELECT_LARGE_VIDEO_PARTICIPANT } from './actionTypes';
8
+import {
9
+    SELECT_LARGE_VIDEO_PARTICIPANT,
10
+    UPDATE_KNOWN_LARGE_VIDEO_RESOLUTION
11
+} from './actionTypes';
9 12
 
10 13
 /**
11 14
  * Signals conference to select a participant.
@@ -64,6 +67,22 @@ export function selectParticipantInLargeVideo() {
64 67
     };
65 68
 }
66 69
 
70
+/**
71
+ * Updates the currently seen resolution of the video displayed on large video.
72
+ *
73
+ * @param {number} resolution - The current resolution (height) of the video.
74
+ * @returns {{
75
+ *     type: UPDATE_KNOWN_LARGE_VIDEO_RESOLUTION,
76
+ *     resolution: number
77
+ * }}
78
+ */
79
+export function updateKnownLargeVideoResolution(resolution) {
80
+    return {
81
+        type: UPDATE_KNOWN_LARGE_VIDEO_RESOLUTION,
82
+        resolution
83
+    };
84
+}
85
+
67 86
 /**
68 87
  * Returns the most recent existing video track. It can be local or remote
69 88
  * video.

+ 4
- 2
react/features/large-video/components/LargeVideo.web.js Прегледај датотеку

@@ -3,7 +3,9 @@
3 3
 import React, { Component } from 'react';
4 4
 
5 5
 import { Watermarks } from '../../base/react';
6
-import { VideoStatusLabel } from '../../video-status-label';
6
+import { VideoQualityLabel } from '../../video-quality';
7
+
8
+declare var interfaceConfig: Object;
7 9
 
8 10
 /**
9 11
  * Implements a React {@link Component} which represents the large video (a.k.a.
@@ -66,7 +68,7 @@ export default class LargeVideo extends Component {
66 68
                 </div>
67 69
                 <span id = 'localConnectionMessage' />
68 70
 
69
-                <VideoStatusLabel />
71
+                { interfaceConfig.filmStripOnly ? null : <VideoQualityLabel /> }
70 72
 
71 73
                 <span
72 74
                     className = 'video-state-indicator centeredVideoLabel'

+ 10
- 1
react/features/large-video/reducer.js Прегледај датотеку

@@ -1,7 +1,10 @@
1 1
 import { PARTICIPANT_ID_CHANGED } from '../base/participants';
2 2
 import { ReducerRegistry } from '../base/redux';
3 3
 
4
-import { SELECT_LARGE_VIDEO_PARTICIPANT } from './actionTypes';
4
+import {
5
+    SELECT_LARGE_VIDEO_PARTICIPANT,
6
+    UPDATE_KNOWN_LARGE_VIDEO_RESOLUTION
7
+} from './actionTypes';
5 8
 
6 9
 ReducerRegistry.register('features/large-video', (state = {}, action) => {
7 10
     switch (action.type) {
@@ -25,6 +28,12 @@ ReducerRegistry.register('features/large-video', (state = {}, action) => {
25 28
             ...state,
26 29
             participantId: action.participantId
27 30
         };
31
+
32
+    case UPDATE_KNOWN_LARGE_VIDEO_RESOLUTION:
33
+        return {
34
+            ...state,
35
+            resolution: action.resolution
36
+        };
28 37
     }
29 38
 
30 39
     return state;

+ 1
- 0
react/features/toolbox/components/index.js Прегледај датотеку

@@ -1 +1,2 @@
1
+export { default as ToolbarButton } from './ToolbarButton';
1 2
 export { default as Toolbox } from './Toolbox';

+ 6
- 0
react/features/toolbox/defaultToolbarButtons.js Прегледај датотеку

@@ -7,6 +7,8 @@ import { openDialOutDialog } from '../dial-out';
7 7
 import { openAddPeopleDialog, openInviteDialog } from '../invite';
8 8
 import UIEvents from '../../../service/UI/UIEvents';
9 9
 
10
+import { VideoQualityButton } from '../video-quality';
11
+
10 12
 declare var APP: Object;
11 13
 declare var interfaceConfig: Object;
12 14
 declare var JitsiMeetJS: Object;
@@ -420,6 +422,10 @@ const buttons: Object = {
420 422
             }
421 423
         ],
422 424
         tooltipKey: 'toolbar.sharedvideo'
425
+    },
426
+
427
+    videoquality: {
428
+        component: VideoQualityButton
423 429
     }
424 430
 };
425 431
 

+ 0
- 0
react/features/video-quality/components/VideoQualityButton.native.js Прегледај датотеку


+ 153
- 0
react/features/video-quality/components/VideoQualityButton.web.js Прегледај датотеку

@@ -0,0 +1,153 @@
1
+import AKInlineDialog from '@atlaskit/inline-dialog';
2
+import React, { Component } from 'react';
3
+import { connect } from 'react-redux';
4
+
5
+import { VideoQualityDialog } from './';
6
+
7
+import { ToolbarButton } from '../../toolbox';
8
+
9
+const DEFAULT_BUTTON_CONFIGURATION = {
10
+    buttonName: 'videoquality',
11
+    classNames: [ 'button', 'icon-visibility' ],
12
+    enabled: true,
13
+    id: 'toolbar_button_videoquality',
14
+    tooltipKey: 'videoStatus.qualityButtonTip'
15
+};
16
+
17
+const TOOLTIP_TO_DIALOG_POSITION = {
18
+    bottom: 'bottom center',
19
+    left: 'left middle',
20
+    right: 'right middle',
21
+    top: 'top center'
22
+};
23
+
24
+/**
25
+ * React {@code Component} for displaying an inline dialog for changing receive
26
+ * video settings.
27
+ *
28
+ * @extends Component
29
+ */
30
+class VideoQualityButton extends Component {
31
+    /**
32
+     * {@code VideoQualityButton}'s property types.
33
+     *
34
+     * @static
35
+     */
36
+    static propTypes = {
37
+        /**
38
+         * Whether or not the button is visible, based on the visibility of the
39
+         * toolbar. Used to automatically hide the inline dialog if not visible.
40
+         */
41
+        _visible: React.PropTypes.bool,
42
+
43
+        /**
44
+         * From which side tooltips should display. Will be re-used for
45
+         * displaying the inline dialog for video quality adjustment.
46
+         */
47
+        tooltipPosition: React.PropTypes.string
48
+    };
49
+
50
+    /**
51
+     * Initializes a new {@code VideoQualityButton} instance.
52
+     *
53
+     * @param {Object} props - The read-only properties with which the new
54
+     * instance is to be initialized.
55
+     */
56
+    constructor(props) {
57
+        super(props);
58
+
59
+        this.state = {
60
+            /**
61
+             * Whether or not the inline dialog for adjusting received video
62
+             * quality is displayed.
63
+             */
64
+            showVideoQualityDialog: false
65
+        };
66
+
67
+        // Bind event handlers so they are only bound once for every instance.
68
+        this._onDialogClose = this._onDialogClose.bind(this);
69
+        this._onDialogToggle = this._onDialogToggle.bind(this);
70
+    }
71
+
72
+    /**
73
+     * Automatically close the inline dialog if the button will not be visible.
74
+     *
75
+     * @inheritdoc
76
+     * @returns {void}
77
+     */
78
+    componentWillReceiveProps(nextProps) {
79
+        if (!nextProps._visible) {
80
+            this._onDialogClose();
81
+        }
82
+    }
83
+
84
+    /**
85
+     * Implements React's {@link Component#render()}.
86
+     *
87
+     * @inheritdoc
88
+     * @returns {ReactElement}
89
+     */
90
+    render() {
91
+        const { _visible, tooltipPosition } = this.props;
92
+        const buttonConfiguration = {
93
+            ...DEFAULT_BUTTON_CONFIGURATION,
94
+            classNames: [
95
+                ...DEFAULT_BUTTON_CONFIGURATION.classNames,
96
+                this.state.showVideoQualityDialog ? 'toggled button-active' : ''
97
+            ]
98
+        };
99
+
100
+        return (
101
+            <AKInlineDialog
102
+                content = { <VideoQualityDialog /> }
103
+                isOpen = { _visible && this.state.showVideoQualityDialog }
104
+                onClose = { this._onDialogClose }
105
+                position = { TOOLTIP_TO_DIALOG_POSITION[tooltipPosition] }>
106
+                <ToolbarButton
107
+                    button = { buttonConfiguration }
108
+                    onClick = { this._onDialogToggle }
109
+                    tooltipPosition = { tooltipPosition } />
110
+            </AKInlineDialog>
111
+        );
112
+    }
113
+
114
+    /**
115
+     * Hides the attached inline dialog.
116
+     *
117
+     * @private
118
+     * @returns {void}
119
+     */
120
+    _onDialogClose() {
121
+        this.setState({ showVideoQualityDialog: false });
122
+    }
123
+
124
+    /**
125
+     * Toggles the display of the dialog.
126
+     *
127
+     * @private
128
+     * @returns {void}
129
+     */
130
+    _onDialogToggle() {
131
+        this.setState({
132
+            showVideoQualityDialog: !this.state.showVideoQualityDialog
133
+        });
134
+    }
135
+}
136
+
137
+/**
138
+ * Maps (parts of) the Redux state to the associated {@code VideoQualityButton}
139
+ * component's props.
140
+ *
141
+ * @param {Object} state - The Redux state.
142
+ * @private
143
+ * @returns {{
144
+ *     _visible: boolean
145
+ * }}
146
+ */
147
+function _mapStateToProps(state) {
148
+    return {
149
+        _visible: state['features/toolbox'].visible
150
+    };
151
+}
152
+
153
+export default connect(_mapStateToProps)(VideoQualityButton);

+ 0
- 0
react/features/video-quality/components/VideoQualityDialog.native.js Прегледај датотеку


+ 324
- 0
react/features/video-quality/components/VideoQualityDialog.web.js Прегледај датотеку

@@ -0,0 +1,324 @@
1
+import InlineMessage from '@atlaskit/inline-message';
2
+import React, { Component } from 'react';
3
+import { connect } from 'react-redux';
4
+
5
+import {
6
+    setAudioOnly,
7
+    setReceiveVideoQuality,
8
+    VIDEO_QUALITY_LEVELS
9
+} from '../../base/conference';
10
+
11
+import { translate } from '../../base/i18n';
12
+
13
+const {
14
+    HIGH,
15
+    STANDARD,
16
+    LOW
17
+} = VIDEO_QUALITY_LEVELS;
18
+
19
+/**
20
+ * Implements a React {@link Component} which displays a dialog with a slider
21
+ * for selecting a new receive video quality.
22
+ *
23
+ * @extends Component
24
+ */
25
+class VideoQualityDialog extends Component {
26
+    /**
27
+     * {@code VideoQualityDialog}'s property types.
28
+     *
29
+     * @static
30
+     */
31
+    static propTypes = {
32
+        /**
33
+         * Whether or not the conference is in audio only mode.
34
+         */
35
+        _audioOnly: React.PropTypes.bool,
36
+
37
+        /**
38
+         * Whether or not the conference is in peer to peer mode.
39
+         */
40
+        _p2p: React.PropTypes.bool,
41
+
42
+        /**
43
+         * The currently configured maximum quality resolution to be received
44
+         * from remote participants.
45
+         */
46
+        _receiveVideoQuality: React.PropTypes.number,
47
+
48
+        /**
49
+         * Invoked to request toggling of audio only mode.
50
+         */
51
+        dispatch: React.PropTypes.func,
52
+
53
+        /**
54
+         * Invoked to obtain translated strings.
55
+         */
56
+        t: React.PropTypes.func
57
+    };
58
+
59
+    /**
60
+     * Initializes a new {@code VideoQualityDialog} instance.
61
+     *
62
+     * @param {Object} props - The read-only React Component props with which
63
+     * the new instance is to be initialized.
64
+     */
65
+    constructor(props) {
66
+        super(props);
67
+
68
+        // Bind event handlers so they are only bound once for every instance.
69
+        this._enableAudioOnly = this._enableAudioOnly.bind(this);
70
+        this._enableHighDefinition = this._enableHighDefinition.bind(this);
71
+        this._enableLowDefinition = this._enableLowDefinition.bind(this);
72
+        this._enableStandardDefinition
73
+            = this._enableStandardDefinition.bind(this);
74
+        this._onSliderChange = this._onSliderChange.bind(this);
75
+
76
+        /**
77
+         * An array of configuration options for displaying a choice in the
78
+         * input. The onSelect callback will be invoked when the option is
79
+         * selected and videoQuality helps determine which choice matches with
80
+         * the currently active quality level.
81
+         *
82
+         * @private
83
+         * @type {Object[]}
84
+         */
85
+        this._sliderOptions = [
86
+            {
87
+                audioOnly: true,
88
+                onSelect: this._enableAudioOnly,
89
+                textKey: 'audioOnly.audioOnly'
90
+            },
91
+            {
92
+                onSelect: this._enableLowDefinition,
93
+                textKey: 'videoStatus.lowDefinition',
94
+                videoQuality: LOW
95
+            },
96
+            {
97
+                onSelect: this._enableStandardDefinition,
98
+                textKey: 'videoStatus.standardDefinition',
99
+                videoQuality: STANDARD
100
+            },
101
+            {
102
+                onSelect: this._enableHighDefinition,
103
+                textKey: 'videoStatus.highDefinition',
104
+                videoQuality: HIGH
105
+            }
106
+        ];
107
+    }
108
+
109
+    /**
110
+     * Implements React's {@link Component#render()}.
111
+     *
112
+     * @inheritdoc
113
+     * @returns {ReactElement}
114
+     */
115
+    render() {
116
+        const { _audioOnly, _p2p, t } = this.props;
117
+        const activeSliderOption = this._mapCurrentQualityToSliderValue();
118
+
119
+        return (
120
+            <div className = 'video-quality-dialog'>
121
+                <h3 className = 'video-quality-dialog-title'>
122
+                    { t('videoStatus.callQuality') }
123
+                </h3>
124
+                { !_audioOnly && _p2p ? this._renderP2PMessage() : null }
125
+                <div className = 'video-quality-dialog-contents'>
126
+                    <div className = 'video-quality-dialog-slider-container'>
127
+                        { /* FIXME: onChange and onMouseUp are both used for
128
+                           * compatibility with IE11. This workaround can be
129
+                           * removed after upgrading to React 16.
130
+                           */ }
131
+                        <input
132
+                            className = 'video-quality-dialog-slider'
133
+                            max = { this._sliderOptions.length - 1 }
134
+                            min = '0'
135
+                            onChange = { this._onSliderChange }
136
+                            onMouseUp = { this._onSliderChange }
137
+                            step = '1'
138
+                            type = 'range'
139
+                            value
140
+                                = { activeSliderOption } />
141
+
142
+                    </div>
143
+                    <div className = 'video-quality-dialog-labels'>
144
+                        { this._createLabels(activeSliderOption) }
145
+                    </div>
146
+                </div>
147
+            </div>
148
+        );
149
+    }
150
+
151
+    /**
152
+     * Creates React Elements for notifying that peer to peer is enabled.
153
+     *
154
+     * @private
155
+     * @returns {ReactElement}
156
+     */
157
+    _renderP2PMessage() {
158
+        const { t } = this.props;
159
+
160
+        return (
161
+            <InlineMessage
162
+                secondaryText = { t('videoStatus.recHighDefinitionOnly') }
163
+                title = { t('videoStatus.p2pEnabled') }>
164
+                { t('videoStatus.p2pVideoQualityDescription') }
165
+            </InlineMessage>
166
+        );
167
+    }
168
+
169
+    /**
170
+     * Creates React Elements to display mock tick marks with associated labels.
171
+     *
172
+     * @param {number} activeLabelIndex - Which of the sliderOptions should
173
+     * display as currently active.
174
+     * @private
175
+     * @returns {ReactElement[]}
176
+     */
177
+    _createLabels(activeLabelIndex) {
178
+        const labelsCount = this._sliderOptions.length;
179
+        const maxWidthOfLabel = `${100 / labelsCount}%`;
180
+
181
+        return this._sliderOptions.map((sliderOption, index) => {
182
+            const style = {
183
+                maxWidth: maxWidthOfLabel,
184
+                left: `${(index * 100) / (labelsCount - 1)}%`
185
+            };
186
+
187
+            const isActiveClass = activeLabelIndex === index ? 'active' : '';
188
+            const className
189
+                = `video-quality-dialog-label-container ${isActiveClass}`;
190
+
191
+            return (
192
+                <div
193
+                    className = { className }
194
+                    key = { index }
195
+                    style = { style }>
196
+                    <div className = 'video-quality-dialog-label'>
197
+                        { this.props.t(sliderOption.textKey) }
198
+                    </div>
199
+                </div>
200
+            );
201
+        });
202
+    }
203
+
204
+    /**
205
+     * Dispatches an action to enable audio only mode.
206
+     *
207
+     * @private
208
+     * @returns {void}
209
+     */
210
+    _enableAudioOnly() {
211
+        this.props.dispatch(setAudioOnly(true));
212
+    }
213
+
214
+    /**
215
+     * Dispatches an action to receive high quality video from remote
216
+     * participants.
217
+     *
218
+     * @private
219
+     * @returns {void}
220
+     */
221
+    _enableHighDefinition() {
222
+        this.props.dispatch(setReceiveVideoQuality(HIGH));
223
+    }
224
+
225
+    /**
226
+     * Dispatches an action to receive low quality video from remote
227
+     * participants.
228
+     *
229
+     * @private
230
+     * @returns {void}
231
+     */
232
+    _enableLowDefinition() {
233
+        this.props.dispatch(setReceiveVideoQuality(LOW));
234
+    }
235
+
236
+    /**
237
+     * Dispatches an action to receive standard quality video from remote
238
+     * participants.
239
+     *
240
+     * @private
241
+     * @returns {void}
242
+     */
243
+    _enableStandardDefinition() {
244
+        this.props.dispatch(setReceiveVideoQuality(STANDARD));
245
+    }
246
+
247
+    /**
248
+     * Matches the current video quality state with corresponding index of the
249
+     * component's slider options.
250
+     *
251
+     * @private
252
+     * @returns {void}
253
+     */
254
+    _mapCurrentQualityToSliderValue() {
255
+        const { _audioOnly, _receiveVideoQuality } = this.props;
256
+        const { _sliderOptions } = this;
257
+
258
+        if (_audioOnly) {
259
+            const audioOnlyOption = _sliderOptions.find(
260
+                ({ audioOnly }) => audioOnly);
261
+
262
+            return _sliderOptions.indexOf(audioOnlyOption);
263
+        }
264
+
265
+        const matchingOption = _sliderOptions.find(
266
+            ({ videoQuality }) => videoQuality === _receiveVideoQuality);
267
+
268
+        return _sliderOptions.indexOf(matchingOption);
269
+    }
270
+
271
+    /**
272
+     * Invokes a callback when the selected video quality changes.
273
+     *
274
+     * @param {Object} event - The slider's change event.
275
+     * @private
276
+     * @returns {void}
277
+     */
278
+    _onSliderChange(event) {
279
+        const { _audioOnly, _receiveVideoQuality } = this.props;
280
+        const {
281
+            audioOnly,
282
+            onSelect,
283
+            videoQuality
284
+        } = this._sliderOptions[event.target.value];
285
+
286
+        // Take no action if the newly chosen option does not change audio only
287
+        // or video quality state.
288
+        if ((_audioOnly && audioOnly)
289
+            || (!_audioOnly && videoQuality === _receiveVideoQuality)) {
290
+            return;
291
+        }
292
+
293
+        onSelect();
294
+    }
295
+}
296
+
297
+/**
298
+ * Maps (parts of) the Redux state to the associated props for the
299
+ * {@code VideoQualityDialog} component.
300
+ *
301
+ * @param {Object} state - The Redux state.
302
+ * @private
303
+ * @returns {{
304
+ *     _audioOnly: boolean,
305
+ *     _p2p: boolean,
306
+ *     _receiveVideoQuality: boolean
307
+ * }}
308
+ */
309
+function _mapStateToProps(state) {
310
+    const {
311
+        audioOnly,
312
+        p2p,
313
+        receiveVideoQuality
314
+    } = state['features/base/conference'];
315
+
316
+    return {
317
+        _audioOnly: audioOnly,
318
+        _p2p: p2p,
319
+        _receiveVideoQuality: receiveVideoQuality
320
+    };
321
+}
322
+
323
+export default translate(connect(_mapStateToProps)(VideoQualityDialog));
324
+

+ 0
- 0
react/features/video-quality/components/VideoQualityLabel.native.js Прегледај датотеку


react/features/video-status-label/components/VideoStatusLabel.js → react/features/video-quality/components/VideoQualityLabel.web.js Прегледај датотеку

@@ -1,9 +1,37 @@
1
+import AKInlineDialog from '@atlaskit/inline-dialog';
1 2
 import React, { Component } from 'react';
2 3
 import { connect } from 'react-redux';
3 4
 
4
-import { toggleAudioOnly } from '../../base/conference';
5 5
 import { translate } from '../../base/i18n';
6 6
 
7
+import { VideoQualityDialog } from './';
8
+
9
+import {
10
+    VIDEO_QUALITY_LEVELS
11
+} from '../../base/conference';
12
+
13
+const { HIGH, STANDARD, LOW } = VIDEO_QUALITY_LEVELS;
14
+
15
+/**
16
+ * Expected video resolutions placed into an array, sorted from lowest to
17
+ * highest resolution.
18
+ *
19
+ * @type {number[]}
20
+ */
21
+const RESOLUTIONS
22
+    = Object.values(VIDEO_QUALITY_LEVELS).sort((a, b) => a - b);
23
+
24
+/**
25
+ * A map of video resolution (number) to translation key.
26
+ *
27
+ * @type {Object}
28
+ */
29
+const RESOLUTION_TO_TRANSLATION_KEY = {
30
+    [HIGH]: 'videoStatus.hd',
31
+    [STANDARD]: 'videoStatus.sd',
32
+    [LOW]: 'videoStatus.ld'
33
+};
34
+
7 35
 /**
8 36
  * React {@code Component} responsible for displaying a label that indicates
9 37
  * the displayed video state of the current conference. {@code AudioOnlyLabel}
@@ -11,9 +39,9 @@ import { translate } from '../../base/i18n';
11 39
  * will display if not in audio only mode and a high-definition large video is
12 40
  * being displayed.
13 41
  */
14
-export class VideoStatusLabel extends Component {
42
+export class VideoQualityLabel extends Component {
15 43
     /**
16
-     * {@code VideoStatusLabel}'s property types.
44
+     * {@code VideoQualityLabel}'s property types.
17 45
      *
18 46
      * @static
19 47
      */
@@ -34,11 +62,6 @@ export class VideoStatusLabel extends Component {
34 62
          */
35 63
         _filmstripVisible: React.PropTypes.bool,
36 64
 
37
-        /**
38
-         * Whether or not a high-definition large video is displayed.
39
-         */
40
-        _largeVideoHD: React.PropTypes.bool,
41
-
42 65
         /**
43 66
          * Whether or note remote videos are visible in the filmstrip,
44 67
          * regardless of count. Used to determine display classes to set.
@@ -46,9 +69,9 @@ export class VideoStatusLabel extends Component {
46 69
         _remoteVideosVisible: React.PropTypes.bool,
47 70
 
48 71
         /**
49
-         * Invoked to request toggling of audio only mode.
72
+         * The current video resolution (height) to display a label for.
50 73
          */
51
-        dispatch: React.PropTypes.func,
74
+        _resolution: React.PropTypes.number,
52 75
 
53 76
         /**
54 77
          * Invoked to obtain translated strings.
@@ -57,7 +80,7 @@ export class VideoStatusLabel extends Component {
57 80
     };
58 81
 
59 82
     /**
60
-     * Initializes a new {@code VideoStatusLabel} instance.
83
+     * Initializes a new {@code VideoQualityLabel} instance.
61 84
      *
62 85
      * @param {Object} props - The read-only React Component props with which
63 86
      * the new instance is to be initialized.
@@ -66,13 +89,25 @@ export class VideoStatusLabel extends Component {
66 89
         super(props);
67 90
 
68 91
         this.state = {
69
-            // Whether or not the filmstrip is transitioning from not visible
70
-            // to visible. Used to set a transition class for animation.
92
+            /**
93
+             * Whether or not the {@code VideoQualityDialog} is displayed.
94
+             *
95
+             * @type {boolean}
96
+             */
97
+            showVideoQualityDialog: false,
98
+
99
+            /**
100
+             * Whether or not the filmstrip is transitioning from not visible
101
+             * to visible. Used to set a transition class for animation.
102
+             *
103
+             * @type {boolean}
104
+             */
71 105
             togglingToVisible: false
72 106
         };
73 107
 
74
-        // Bind event handler so it is only bound once for every instance.
75
-        this._toggleAudioOnly = this._toggleAudioOnly.bind(this);
108
+        // Bind event handlers so they are only bound once for every instance.
109
+        this._onDialogClose = this._onDialogClose.bind(this);
110
+        this._onDialogToggle = this._onDialogToggle.bind(this);
76 111
     }
77 112
 
78 113
     /**
@@ -103,8 +138,7 @@ export class VideoStatusLabel extends Component {
103 138
             _conferenceStarted,
104 139
             _filmstripVisible,
105 140
             _remoteVideosVisible,
106
-            _largeVideoHD,
107
-            t
141
+            _resolution
108 142
         } = this.props;
109 143
 
110 144
         // FIXME The _conferenceStarted check is used to be defensive against
@@ -114,15 +148,6 @@ export class VideoStatusLabel extends Component {
114 148
             return null;
115 149
         }
116 150
 
117
-        let displayedLabel;
118
-
119
-        if (_audioOnly) {
120
-            displayedLabel = <i className = 'icon-visibility-off' />;
121
-        } else {
122
-            displayedLabel = _largeVideoHD
123
-                ? t('videoStatus.hd') : t('videoStatus.sd');
124
-        }
125
-
126 151
         // Determine which classes should be set on the component. These classes
127 152
         // will used to help with animations and setting position.
128 153
         const baseClasses = 'video-state-indicator moveToCorner';
@@ -138,57 +163,77 @@ export class VideoStatusLabel extends Component {
138 163
         return (
139 164
             <div
140 165
                 className = { classNames }
141
-                id = 'videoResolutionLabel' >
142
-                { displayedLabel }
143
-                { this._renderVideonMenu() }
166
+                id = 'videoResolutionLabel'
167
+                onClick = { this._onDialogToggle }>
168
+                <AKInlineDialog
169
+                    content = { <VideoQualityDialog /> }
170
+                    isOpen = { this.state.showVideoQualityDialog }
171
+                    onClose = { this._onDialogClose }
172
+                    position = { 'left top' }>
173
+                    <div className = 'video-quality-label-status'>
174
+                        { _audioOnly
175
+                            ? <i className = 'icon-visibility-off' />
176
+                            : this._mapResolutionToTranslation(_resolution) }
177
+                    </div>
178
+                </AKInlineDialog>
144 179
             </div>
145 180
         );
146 181
     }
147 182
 
148 183
     /**
149
-     * Renders a dropdown menu for changing video modes.
184
+     * Matches the passed in resolution with a translation key for describing
185
+     * the resolution. The passed in resolution will be matched with a known
186
+     * resolution that it is at least greater than or equal to.
150 187
      *
188
+     * @param {number} resolution - The video height to match with a
189
+     * translation.
151 190
      * @private
152
-     * @returns {ReactElement}
191
+     * @returns {string}
153 192
      */
154
-    _renderVideonMenu() {
155
-        const { _audioOnly, t } = this.props;
156
-        const audioOnlyAttributes = _audioOnly ? { className: 'active' }
157
-            : { onClick: this._toggleAudioOnly };
158
-        const videoAttributes = _audioOnly ? { onClick: this._toggleAudioOnly }
159
-            : { className: 'active' };
193
+    _mapResolutionToTranslation(resolution) {
194
+        // Set the default matching resolution of the lowest just in case a
195
+        // match is not found.
196
+        let highestMatchingResolution = RESOLUTIONS[0];
160 197
 
161
-        return (
162
-            <div className = 'video-state-indicator-menu'>
163
-                <div className = 'video-state-indicator-menu-options'>
164
-                    <div { ...audioOnlyAttributes }>
165
-                        <i className = 'icon-visibility' />
166
-                        { t('audioOnly.audioOnly') }
167
-                    </div>
168
-                    <div { ...videoAttributes }>
169
-                        <i className = 'icon-camera' />
170
-                        { this.props._largeVideoHD
171
-                            ? t('videoStatus.hdVideo')
172
-                            : t('videoStatus.sdVideo') }
173
-                    </div>
174
-                </div>
175
-            </div>
176
-        );
198
+        for (let i = 0; i < RESOLUTIONS.length; i++) {
199
+            const knownResolution = RESOLUTIONS[i];
200
+
201
+            if (resolution >= knownResolution) {
202
+                highestMatchingResolution = knownResolution;
203
+            } else {
204
+                break;
205
+            }
206
+        }
207
+
208
+        return this.props.t(
209
+            RESOLUTION_TO_TRANSLATION_KEY[highestMatchingResolution]);
177 210
     }
178 211
 
179 212
     /**
180
-     * Dispatches an action to toggle the state of audio only mode.
213
+     * Toggles the display of the {@code VideoQualityDialog}.
181 214
      *
182 215
      * @private
183 216
      * @returns {void}
184 217
      */
185
-    _toggleAudioOnly() {
186
-        this.props.dispatch(toggleAudioOnly());
218
+    _onDialogToggle() {
219
+        this.setState({
220
+            showVideoQualityDialog: !this.state.showVideoQualityDialog
221
+        });
222
+    }
223
+
224
+    /**
225
+     * Hides the attached inline dialog.
226
+     *
227
+     * @private
228
+     * @returns {void}
229
+     */
230
+    _onDialogClose() {
231
+        this.setState({ showVideoQualityDialog: false });
187 232
     }
188 233
 }
189 234
 
190 235
 /**
191
- * Maps (parts of) the Redux state to the associated {@code VideoStatusLabel}'s
236
+ * Maps (parts of) the Redux state to the associated {@code VideoQualityLabel}'s
192 237
  * props.
193 238
  *
194 239
  * @param {Object} state - The Redux state.
@@ -197,28 +242,30 @@ export class VideoStatusLabel extends Component {
197 242
  *     _audioOnly: boolean,
198 243
  *     _conferenceStarted: boolean,
199 244
  *     _filmstripVisible: true,
200
- *     _largeVideoHD: (boolean|undefined),
201
- *     _remoteVideosVisible: boolean
245
+ *     _remoteVideosVisible: boolean,
246
+ *     _resolution: number
202 247
  * }}
203 248
  */
204 249
 function _mapStateToProps(state) {
205 250
     const {
206 251
         audioOnly,
207
-        conference,
208
-        isLargeVideoHD
252
+        conference
209 253
     } = state['features/base/conference'];
210 254
     const {
211 255
         remoteVideosVisible,
212 256
         visible
213 257
     } = state['features/filmstrip'];
258
+    const {
259
+        resolution
260
+    } = state['features/large-video'];
214 261
 
215 262
     return {
216 263
         _audioOnly: audioOnly,
217 264
         _conferenceStarted: Boolean(conference),
218 265
         _filmstripVisible: visible,
219
-        _largeVideoHD: isLargeVideoHD,
220
-        _remoteVideosVisible: remoteVideosVisible
266
+        _remoteVideosVisible: remoteVideosVisible,
267
+        _resolution: resolution
221 268
     };
222 269
 }
223 270
 
224
-export default translate(connect(_mapStateToProps)(VideoStatusLabel));
271
+export default translate(connect(_mapStateToProps)(VideoQualityLabel));

+ 3
- 0
react/features/video-quality/components/index.js Прегледај датотеку

@@ -0,0 +1,3 @@
1
+export { default as VideoQualityButton } from './VideoQualityButton';
2
+export { default as VideoQualityDialog } from './VideoQualityDialog';
3
+export { default as VideoQualityLabel } from './VideoQualityLabel';

react/features/video-status-label/index.js → react/features/video-quality/index.js Прегледај датотеку


+ 0
- 1
react/features/video-status-label/components/index.js Прегледај датотеку

@@ -1 +0,0 @@
1
-export { default as VideoStatusLabel } from './VideoStatusLabel';

Loading…
Откажи
Сачувај