Browse Source

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 years ago
parent
commit
d8cd3e75b4
28 changed files with 1047 additions and 234 deletions
  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 View File

25
     conferenceFailed,
25
     conferenceFailed,
26
     conferenceJoined,
26
     conferenceJoined,
27
     conferenceLeft,
27
     conferenceLeft,
28
+    dataChannelOpened,
28
     toggleAudioOnly,
29
     toggleAudioOnly,
29
     EMAIL_COMMAND,
30
     EMAIL_COMMAND,
30
-    lockStateChanged
31
+    lockStateChanged,
32
+    p2pStatusChanged
31
 } from './react/features/base/conference';
33
 } from './react/features/base/conference';
32
 import { updateDeviceList } from './react/features/base/devices';
34
 import { updateDeviceList } from './react/features/base/devices';
33
 import {
35
 import {
1704
                 APP.UI.handleLastNEndpoints(leavingIds, enteringIds);
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
         room.on(
1715
         room.on(
1708
             ConferenceEvents.PARTICIPANT_CONN_STATUS_CHANGED,
1716
             ConferenceEvents.PARTICIPANT_CONN_STATUS_CHANGED,
1709
             (id, connectionStatus) => {
1717
             (id, connectionStatus) => {
1952
             }
1960
             }
1953
         );
1961
         );
1954
 
1962
 
1963
+        room.on(
1964
+            ConferenceEvents.DATA_CHANNEL_OPENED, () => {
1965
+                APP.store.dispatch(dataChannelOpened());
1966
+            }
1967
+        );
1968
+
1955
         // call hangup
1969
         // call hangup
1956
         APP.UI.addListener(UIEvents.HANGUP, () => {
1970
         APP.UI.addListener(UIEvents.HANGUP, () => {
1957
             this.hangup(true);
1971
             this.hangup(true);

+ 8
- 4
css/_toolbars.scss View File

42
     position: relative;
42
     position: relative;
43
     z-index: $toolbarZ;
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
      * Toolbar button styles.
54
      * Toolbar button styles.
47
      */
55
      */
94
             &.icon-microphone {
102
             &.icon-microphone {
95
                 @extend .icon-mic-disabled;
103
                 @extend .icon-mic-disabled;
96
             }
104
             }
97
-
98
-            &.icon-visibility {
99
-                @extend .icon-visibility-off;
100
-            }
101
         }
105
         }
102
 
106
 
103
         &.unclickable {
107
         &.unclickable {

+ 0
- 108
css/_videolayout_default.scss View File

535
                     1px 0px 1px rgba(0,0,0,0.3),
535
                     1px 0px 1px rgba(0,0,0,0.3),
536
                     0px 0px 1px rgba(0,0,0,0.3);
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 View File

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

+ 166
- 0
css/modals/video-quality/_video-quality.scss View File

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 View File

35
         //main toolbar
35
         //main toolbar
36
         'microphone', 'camera', 'desktop', 'invite', 'fullscreen', 'fodeviceselection', 'hangup', // jshint ignore:line
36
         'microphone', 'camera', 'desktop', 'invite', 'fullscreen', 'fodeviceselection', 'hangup', // jshint ignore:line
37
         //extended toolbar
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
      * Main Toolbar Buttons
40
      * Main Toolbar Buttons
41
      * All of them should be in TOOLBAR_BUTTONS
41
      * All of them should be in TOOLBAR_BUTTONS

+ 12
- 4
lang/main.json View File

449
         "unlocked": "This call is unlocked. Any new caller with the link may join the call."
449
         "unlocked": "This call is unlocked. Any new caller with the link may join the call."
450
     },
450
     },
451
     "videoStatus": {
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
     "dialOut": {
465
     "dialOut": {
458
         "dial": "Dial",
466
         "dial": "Dial",

+ 10
- 5
modules/UI/videolayout/LargeVideoManager.js View File

1
-/* global $, APP, config, JitsiMeetJS */
1
+/* global $, APP, JitsiMeetJS */
2
 /* eslint-disable no-unused-vars */
2
 /* eslint-disable no-unused-vars */
3
 import React from 'react';
3
 import React from 'react';
4
 import ReactDOM from 'react-dom';
4
 import ReactDOM from 'react-dom';
9
 
9
 
10
 const logger = require("jitsi-meet-logger").getLogger(__filename);
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
 import Avatar from "../avatar/Avatar";
16
 import Avatar from "../avatar/Avatar";
15
 import {createDeferred} from '../../util/helpers';
17
 import {createDeferred} from '../../util/helpers';
659
      */
661
      */
660
     _onVideoResolutionUpdate() {
662
     _onVideoResolutionUpdate() {
661
         const { height, width } = this.videoContainer.getStreamSize();
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
         if (this._videoAspectRatio !== currentAspectRatio) {
672
         if (this._videoAspectRatio !== currentAspectRatio) {
668
             this._videoAspectRatio = currentAspectRatio;
673
             this._videoAspectRatio = currentAspectRatio;

+ 30
- 9
react/features/base/conference/actionTypes.js View File

52
  */
52
  */
53
 export const CONFERENCE_WILL_LEAVE = Symbol('CONFERENCE_WILL_LEAVE');
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
  * The type of (redux) action which signals that the lock state of a specific
66
  * The type of (redux) action which signals that the lock state of a specific
57
  * {@code JitsiConference} changed.
67
  * {@code JitsiConference} changed.
65
 export const LOCK_STATE_CHANGED = Symbol('LOCK_STATE_CHANGED');
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
  * conference.
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
  * The type of (redux) action which sets the video channel's lastN (value).
100
  * The type of (redux) action which sets the video channel's lastN (value).
120
  */
130
  */
121
 export const SET_PASSWORD_FAILED = Symbol('SET_PASSWORD_FAILED');
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
  * The type of (redux) action which sets the name of the room of the
145
  * The type of (redux) action which sets the name of the room of the
125
  * conference to be joined.
146
  * conference to be joined.

+ 47
- 17
react/features/base/conference/actions.js View File

17
     CONFERENCE_LEFT,
17
     CONFERENCE_LEFT,
18
     CONFERENCE_WILL_JOIN,
18
     CONFERENCE_WILL_JOIN,
19
     CONFERENCE_WILL_LEAVE,
19
     CONFERENCE_WILL_LEAVE,
20
+    DATA_CHANNEL_OPENED,
20
     LOCK_STATE_CHANGED,
21
     LOCK_STATE_CHANGED,
22
+    P2P_STATUS_CHANGED,
21
     SET_AUDIO_ONLY,
23
     SET_AUDIO_ONLY,
22
-    SET_LARGE_VIDEO_HD_STATUS,
23
     SET_LASTN,
24
     SET_LASTN,
24
     SET_PASSWORD,
25
     SET_PASSWORD,
25
     SET_PASSWORD_FAILED,
26
     SET_PASSWORD_FAILED,
27
+    SET_RECEIVE_VIDEO_QUALITY,
26
     SET_ROOM
28
     SET_ROOM
27
 } from './actionTypes';
29
 } from './actionTypes';
28
 import {
30
 import {
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
  * Signals that the lock state of a specific JitsiConference changed.
305
  * Signals that the lock state of a specific JitsiConference changed.
291
  *
306
  *
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
  * @returns {{
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
     return {
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
  * @returns {{
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
     return {
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
     };
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
  * Sets (the name of) the room of the conference to be joined.
472
  * Sets (the name of) the room of the conference to be joined.
443
  *
473
  *

+ 12
- 0
react/features/base/conference/constants.js View File

34
  * from the outside is not cool but it should suffice for now.
34
  * from the outside is not cool but it should suffice for now.
35
  */
35
  */
36
 export const JITSI_CONFERENCE_URL_KEY = Symbol('url');
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 View File

14
 import {
14
 import {
15
     createConference,
15
     createConference,
16
     setAudioOnly,
16
     setAudioOnly,
17
-    setLastN
17
+    setLastN,
18
+    toggleAudioOnly
18
 } from './actions';
19
 } from './actions';
19
 import {
20
 import {
20
     CONFERENCE_FAILED,
21
     CONFERENCE_FAILED,
21
     CONFERENCE_JOINED,
22
     CONFERENCE_JOINED,
22
     CONFERENCE_LEFT,
23
     CONFERENCE_LEFT,
24
+    DATA_CHANNEL_OPENED,
23
     SET_AUDIO_ONLY,
25
     SET_AUDIO_ONLY,
24
-    SET_LASTN
26
+    SET_LASTN,
27
+    SET_RECEIVE_VIDEO_QUALITY
25
 } from './actionTypes';
28
 } from './actionTypes';
26
 import {
29
 import {
27
     _addLocalTracksToConference,
30
     _addLocalTracksToConference,
47
     case CONFERENCE_JOINED:
50
     case CONFERENCE_JOINED:
48
         return _conferenceJoined(store, next, action);
51
         return _conferenceJoined(store, next, action);
49
 
52
 
53
+    case DATA_CHANNEL_OPENED:
54
+        return _syncReceiveVideoQuality(store, next, action);
55
+
50
     case PIN_PARTICIPANT:
56
     case PIN_PARTICIPANT:
51
         return _pinParticipant(store, next, action);
57
         return _pinParticipant(store, next, action);
52
 
58
 
56
     case SET_LASTN:
62
     case SET_LASTN:
57
         return _setLastN(store, next, action);
63
         return _setLastN(store, next, action);
58
 
64
 
65
+    case SET_RECEIVE_VIDEO_QUALITY:
66
+        return _setReceiveVideoQuality(store, next, action);
67
+
59
     case TRACK_ADDED:
68
     case TRACK_ADDED:
60
     case TRACK_REMOVED:
69
     case TRACK_REMOVED:
61
         return _trackAddedOrRemoved(store, next, action);
70
         return _trackAddedOrRemoved(store, next, action);
253
     return next(action);
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
  * Synchronizes local tracks from state with local tracks in JitsiConference
293
  * Synchronizes local tracks from state with local tracks in JitsiConference
258
  * instance.
294
  * instance.
282
     return promise || Promise.resolve();
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
  * Notifies the feature base/conference that the action TRACK_ADDED
343
  * Notifies the feature base/conference that the action TRACK_ADDED
287
  * or TRACK_REMOVED is being dispatched within a specific redux store.
344
  * or TRACK_REMOVED is being dispatched within a specific redux store.

+ 43
- 14
react/features/base/conference/reducer.js View File

10
     CONFERENCE_WILL_JOIN,
10
     CONFERENCE_WILL_JOIN,
11
     CONFERENCE_WILL_LEAVE,
11
     CONFERENCE_WILL_LEAVE,
12
     LOCK_STATE_CHANGED,
12
     LOCK_STATE_CHANGED,
13
+    P2P_STATUS_CHANGED,
13
     SET_AUDIO_ONLY,
14
     SET_AUDIO_ONLY,
14
-    SET_LARGE_VIDEO_HD_STATUS,
15
     SET_PASSWORD,
15
     SET_PASSWORD,
16
+    SET_RECEIVE_VIDEO_QUALITY,
16
     SET_ROOM
17
     SET_ROOM
17
 } from './actionTypes';
18
 } from './actionTypes';
19
+import {
20
+    VIDEO_QUALITY_LEVELS
21
+} from './constants';
18
 import { isRoomValid } from './functions';
22
 import { isRoomValid } from './functions';
19
 
23
 
20
 /**
24
 /**
41
     case LOCK_STATE_CHANGED:
45
     case LOCK_STATE_CHANGED:
42
         return _lockStateChanged(state, action);
46
         return _lockStateChanged(state, action);
43
 
47
 
48
+    case P2P_STATUS_CHANGED:
49
+        return _p2pStatusChanged(state, action);
50
+
44
     case SET_AUDIO_ONLY:
51
     case SET_AUDIO_ONLY:
45
         return _setAudioOnly(state, action);
52
         return _setAudioOnly(state, action);
46
 
53
 
47
-    case SET_LARGE_VIDEO_HD_STATUS:
48
-        return _setLargeVideoHDStatus(state, action);
49
-
50
     case SET_PASSWORD:
54
     case SET_PASSWORD:
51
         return _setPassword(state, action);
55
         return _setPassword(state, action);
52
 
56
 
57
+    case SET_RECEIVE_VIDEO_QUALITY:
58
+        return _setReceiveVideoQuality(state, action);
59
+
53
     case SET_ROOM:
60
     case SET_ROOM:
54
         return _setRoom(state, action);
61
         return _setRoom(state, action);
55
     }
62
     }
135
          * @type {boolean}
142
          * @type {boolean}
136
          */
143
          */
137
         locked,
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
 }
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
  * base/conference.
248
  * base/conference.
234
  *
249
  *
235
  * @param {Object} state - The Redux state of the feature base/conference.
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
  * @private
252
  * @private
238
  * @returns {Object} The new state of the feature base/conference after the
253
  * @returns {Object} The new state of the feature base/conference after the
239
  * reduction of the specified action.
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
  * base/conference.
262
  * base/conference.
248
  *
263
  *
249
  * @param {Object} state - The Redux state of the feature base/conference.
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
  * @private
266
  * @private
253
  * @returns {Object} The new state of the feature base/conference after the
267
  * @returns {Object} The new state of the feature base/conference after the
254
  * reduction of the specified action.
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
     return state;
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
  * Reduces a specific Redux action SET_ROOM of the feature base/conference.
327
  * Reduces a specific Redux action SET_ROOM of the feature base/conference.
299
  *
328
  *

+ 11
- 0
react/features/large-video/actionTypes.js View File

8
  */
8
  */
9
 export const SELECT_LARGE_VIDEO_PARTICIPANT
9
 export const SELECT_LARGE_VIDEO_PARTICIPANT
10
     = Symbol('SELECT_LARGE_VIDEO_PARTICIPANT');
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 View File

5
     getTrackByMediaTypeAndParticipant
5
     getTrackByMediaTypeAndParticipant
6
 } from '../base/tracks';
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
  * Signals conference to select a participant.
14
  * Signals conference to select a participant.
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
  * Returns the most recent existing video track. It can be local or remote
87
  * Returns the most recent existing video track. It can be local or remote
69
  * video.
88
  * video.

+ 4
- 2
react/features/large-video/components/LargeVideo.web.js View File

3
 import React, { Component } from 'react';
3
 import React, { Component } from 'react';
4
 
4
 
5
 import { Watermarks } from '../../base/react';
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
  * Implements a React {@link Component} which represents the large video (a.k.a.
11
  * Implements a React {@link Component} which represents the large video (a.k.a.
66
                 </div>
68
                 </div>
67
                 <span id = 'localConnectionMessage' />
69
                 <span id = 'localConnectionMessage' />
68
 
70
 
69
-                <VideoStatusLabel />
71
+                { interfaceConfig.filmStripOnly ? null : <VideoQualityLabel /> }
70
 
72
 
71
                 <span
73
                 <span
72
                     className = 'video-state-indicator centeredVideoLabel'
74
                     className = 'video-state-indicator centeredVideoLabel'

+ 10
- 1
react/features/large-video/reducer.js View File

1
 import { PARTICIPANT_ID_CHANGED } from '../base/participants';
1
 import { PARTICIPANT_ID_CHANGED } from '../base/participants';
2
 import { ReducerRegistry } from '../base/redux';
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
 ReducerRegistry.register('features/large-video', (state = {}, action) => {
9
 ReducerRegistry.register('features/large-video', (state = {}, action) => {
7
     switch (action.type) {
10
     switch (action.type) {
25
             ...state,
28
             ...state,
26
             participantId: action.participantId
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
     return state;
39
     return state;

+ 1
- 0
react/features/toolbox/components/index.js View File

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

+ 6
- 0
react/features/toolbox/defaultToolbarButtons.js View File

7
 import { openAddPeopleDialog, openInviteDialog } from '../invite';
7
 import { openAddPeopleDialog, openInviteDialog } from '../invite';
8
 import UIEvents from '../../../service/UI/UIEvents';
8
 import UIEvents from '../../../service/UI/UIEvents';
9
 
9
 
10
+import { VideoQualityButton } from '../video-quality';
11
+
10
 declare var APP: Object;
12
 declare var APP: Object;
11
 declare var interfaceConfig: Object;
13
 declare var interfaceConfig: Object;
12
 declare var JitsiMeetJS: Object;
14
 declare var JitsiMeetJS: Object;
420
             }
422
             }
421
         ],
423
         ],
422
         tooltipKey: 'toolbar.sharedvideo'
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 View File


+ 153
- 0
react/features/video-quality/components/VideoQualityButton.web.js View File

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 View File


+ 324
- 0
react/features/video-quality/components/VideoQualityDialog.web.js View File

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 View File


react/features/video-status-label/components/VideoStatusLabel.js → react/features/video-quality/components/VideoQualityLabel.web.js View File

1
+import AKInlineDialog from '@atlaskit/inline-dialog';
1
 import React, { Component } from 'react';
2
 import React, { Component } from 'react';
2
 import { connect } from 'react-redux';
3
 import { connect } from 'react-redux';
3
 
4
 
4
-import { toggleAudioOnly } from '../../base/conference';
5
 import { translate } from '../../base/i18n';
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
  * React {@code Component} responsible for displaying a label that indicates
36
  * React {@code Component} responsible for displaying a label that indicates
9
  * the displayed video state of the current conference. {@code AudioOnlyLabel}
37
  * the displayed video state of the current conference. {@code AudioOnlyLabel}
11
  * will display if not in audio only mode and a high-definition large video is
39
  * will display if not in audio only mode and a high-definition large video is
12
  * being displayed.
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
      * @static
46
      * @static
19
      */
47
      */
34
          */
62
          */
35
         _filmstripVisible: React.PropTypes.bool,
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
          * Whether or note remote videos are visible in the filmstrip,
66
          * Whether or note remote videos are visible in the filmstrip,
44
          * regardless of count. Used to determine display classes to set.
67
          * regardless of count. Used to determine display classes to set.
46
         _remoteVideosVisible: React.PropTypes.bool,
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
          * Invoked to obtain translated strings.
77
          * Invoked to obtain translated strings.
57
     };
80
     };
58
 
81
 
59
     /**
82
     /**
60
-     * Initializes a new {@code VideoStatusLabel} instance.
83
+     * Initializes a new {@code VideoQualityLabel} instance.
61
      *
84
      *
62
      * @param {Object} props - The read-only React Component props with which
85
      * @param {Object} props - The read-only React Component props with which
63
      * the new instance is to be initialized.
86
      * the new instance is to be initialized.
66
         super(props);
89
         super(props);
67
 
90
 
68
         this.state = {
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
             togglingToVisible: false
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
             _conferenceStarted,
138
             _conferenceStarted,
104
             _filmstripVisible,
139
             _filmstripVisible,
105
             _remoteVideosVisible,
140
             _remoteVideosVisible,
106
-            _largeVideoHD,
107
-            t
141
+            _resolution
108
         } = this.props;
142
         } = this.props;
109
 
143
 
110
         // FIXME The _conferenceStarted check is used to be defensive against
144
         // FIXME The _conferenceStarted check is used to be defensive against
114
             return null;
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
         // Determine which classes should be set on the component. These classes
151
         // Determine which classes should be set on the component. These classes
127
         // will used to help with animations and setting position.
152
         // will used to help with animations and setting position.
128
         const baseClasses = 'video-state-indicator moveToCorner';
153
         const baseClasses = 'video-state-indicator moveToCorner';
138
         return (
163
         return (
139
             <div
164
             <div
140
                 className = { classNames }
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
             </div>
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
      * @private
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
      * @private
215
      * @private
183
      * @returns {void}
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
  * props.
237
  * props.
193
  *
238
  *
194
  * @param {Object} state - The Redux state.
239
  * @param {Object} state - The Redux state.
197
  *     _audioOnly: boolean,
242
  *     _audioOnly: boolean,
198
  *     _conferenceStarted: boolean,
243
  *     _conferenceStarted: boolean,
199
  *     _filmstripVisible: true,
244
  *     _filmstripVisible: true,
200
- *     _largeVideoHD: (boolean|undefined),
201
- *     _remoteVideosVisible: boolean
245
+ *     _remoteVideosVisible: boolean,
246
+ *     _resolution: number
202
  * }}
247
  * }}
203
  */
248
  */
204
 function _mapStateToProps(state) {
249
 function _mapStateToProps(state) {
205
     const {
250
     const {
206
         audioOnly,
251
         audioOnly,
207
-        conference,
208
-        isLargeVideoHD
252
+        conference
209
     } = state['features/base/conference'];
253
     } = state['features/base/conference'];
210
     const {
254
     const {
211
         remoteVideosVisible,
255
         remoteVideosVisible,
212
         visible
256
         visible
213
     } = state['features/filmstrip'];
257
     } = state['features/filmstrip'];
258
+    const {
259
+        resolution
260
+    } = state['features/large-video'];
214
 
261
 
215
     return {
262
     return {
216
         _audioOnly: audioOnly,
263
         _audioOnly: audioOnly,
217
         _conferenceStarted: Boolean(conference),
264
         _conferenceStarted: Boolean(conference),
218
         _filmstripVisible: visible,
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 View File

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 View File


+ 0
- 1
react/features/video-status-label/components/index.js View File

1
-export { default as VideoStatusLabel } from './VideoStatusLabel';

Loading…
Cancel
Save