Browse Source

feat(recording): frontend logic can support live streaming and recording (#2952)

* feat(recording): frontend logic can support live streaming and recording

Instead of either live streaming or recording, now both can live together. The
changes to facilitate such include the following:
- Killing the state storing in Recording.js. Instead state is stored in the lib
  and updated in redux for labels to display the necessary state updates.
- Creating a new container, Labels, for recording labels. Previously labels were
  manually created and positioned. The container can create a reasonable number
  of labels and only the container itself needs to be positioned with CSS. The
  VideoQualityLabel has been shoved into the container as well because it moves
  along with the recording labels.
- The action for updating recording state has been modified to enable updating
  an array of recording sessions to support having multiple sessions.
- Confirmation dialogs for stopping and starting a file recording session have
  been created, as they previously were jquery modals opened by Recording.js.
- Toolbox.web displays live streaming and recording buttons based on
  configuration instead of recording availability.
- VideoQualityLabel and RecordingLabel have been simplified to remove any
  positioning logic, as the Labels container handles such.
- Previous recording state update logic has been moved into the RecordingLabel
  component. Each RecordingLabel is in charge of displaying state for a
  recording session. The display UX has been left alone.
- Sipgw availability is no longer broadcast so remove logic depending on its
  state. Some moving around of code was necessary to get around linting errors
  about the existing code being too deeply nested (even though I didn't touch
  it).

* work around lib-jitsi-meet circular dependency issues

* refactor labels to use html base

* pass in translation keys to video quality label

* add video quality classnames for torture tests

* break up, rearrange recorder session update listener

* add comment about disabling startup resize animation

* rename session to sessionData

* chore(deps): update to latest lib for recording changes
factor2
virtuacoplenny 7 years ago
parent
commit
ee74f11c3d
No account linked to committer's email address
39 changed files with 1252 additions and 1232 deletions
  1. 81
    34
      conference.js
  2. 2
    10
      css/_vertical_filmstrip_overrides.scss
  3. 39
    79
      css/modals/video-quality/_video-quality.scss
  4. 1
    1
      interface_config.js
  5. 5
    0
      lang/main.json
  6. 24
    16
      modules/UI/UI.js
  7. 0
    462
      modules/UI/recording/Recording.js
  8. 2
    0
      modules/UI/videolayout/VideoLayout.js
  9. 1
    1
      package-lock.json
  10. 1
    1
      package.json
  11. 0
    0
      react/features/base/label/components/CircularLabel.native.js
  12. 60
    0
      react/features/base/label/components/CircularLabel.web.js
  13. 1
    0
      react/features/base/label/components/index.js
  14. 1
    0
      react/features/base/label/index.js
  15. 1
    1
      react/features/base/lib-jitsi-meet/index.js
  16. 7
    1
      react/features/invite/components/InfoDialogButton.web.js
  17. 0
    0
      react/features/large-video/components/Labels.native.js
  18. 139
    0
      react/features/large-video/components/Labels.web.js
  19. 3
    4
      react/features/large-video/components/LargeVideo.web.js
  20. 5
    38
      react/features/recording/actionTypes.js
  21. 15
    57
      react/features/recording/actions.js
  22. 103
    55
      react/features/recording/components/LiveStream/StartLiveStreamDialog.web.js
  23. 56
    41
      react/features/recording/components/LiveStream/StopLiveStreamDialog.web.js
  24. 0
    0
      react/features/recording/components/Recording/StartRecordingDialog.native.js
  25. 103
    0
      react/features/recording/components/Recording/StartRecordingDialog.web.js
  26. 0
    0
      react/features/recording/components/Recording/StopRecordingDialog.native.js
  27. 109
    0
      react/features/recording/components/Recording/StopRecordingDialog.web.js
  28. 2
    0
      react/features/recording/components/Recording/index.js
  29. 180
    136
      react/features/recording/components/RecordingLabel.web.js
  30. 1
    0
      react/features/recording/components/index.js
  31. 18
    0
      react/features/recording/functions.js
  32. 1
    1
      react/features/recording/index.js
  33. 0
    27
      react/features/recording/middleware.js
  34. 51
    25
      react/features/recording/reducer.js
  35. 107
    46
      react/features/toolbox/components/web/Toolbox.js
  36. 1
    2
      react/features/toolbox/reducer.js
  37. 62
    122
      react/features/video-quality/components/VideoQualityLabel.web.js
  38. 70
    70
      react/features/videosipgw/middleware.js
  39. 0
    2
      service/UI/UIEvents.js

+ 81
- 34
conference.js View File

27
     redirectWithStoredParams,
27
     redirectWithStoredParams,
28
     reloadWithStoredParams
28
     reloadWithStoredParams
29
 } from './react/features/app';
29
 } from './react/features/app';
30
-import { updateRecordingState } from './react/features/recording';
30
+import { updateRecordingSessionData } from './react/features/recording';
31
 
31
 
32
 import EventEmitter from 'events';
32
 import EventEmitter from 'events';
33
 
33
 
1100
         return this._room && this._room.myUserId();
1100
         return this._room && this._room.myUserId();
1101
     },
1101
     },
1102
 
1102
 
1103
-    /**
1104
-     * Indicates if recording is supported in this conference.
1105
-     */
1106
-    isRecordingSupported() {
1107
-        return this._room && this._room.isRecordingSupported();
1108
-    },
1109
-
1110
-    /**
1111
-     * Returns the recording state or undefined if the room is not defined.
1112
-     */
1113
-    getRecordingState() {
1114
-        return this._room ? this._room.getRecordingState() : undefined;
1115
-    },
1116
-
1117
     /**
1103
     /**
1118
      * Will be filled with values only when config.debug is enabled.
1104
      * Will be filled with values only when config.debug is enabled.
1119
      * Its used by torture to check audio levels.
1105
      * Its used by torture to check audio levels.
1821
             APP.store.dispatch(dominantSpeakerChanged(id));
1807
             APP.store.dispatch(dominantSpeakerChanged(id));
1822
         });
1808
         });
1823
 
1809
 
1824
-        room.on(JitsiConferenceEvents.LIVE_STREAM_URL_CHANGED,
1825
-            (from, liveStreamViewURL) =>
1826
-                APP.store.dispatch(updateRecordingState({
1827
-                    liveStreamViewURL
1828
-                })));
1829
-
1830
         if (!interfaceConfig.filmStripOnly) {
1810
         if (!interfaceConfig.filmStripOnly) {
1831
             room.on(JitsiConferenceEvents.CONNECTION_INTERRUPTED, () => {
1811
             room.on(JitsiConferenceEvents.CONNECTION_INTERRUPTED, () => {
1832
                 APP.UI.markVideoInterrupted(true);
1812
                 APP.UI.markVideoInterrupted(true);
1951
             });
1931
             });
1952
 
1932
 
1953
         /* eslint-enable max-params */
1933
         /* eslint-enable max-params */
1954
-
1955
         room.on(
1934
         room.on(
1956
             JitsiConferenceEvents.RECORDER_STATE_CHANGED,
1935
             JitsiConferenceEvents.RECORDER_STATE_CHANGED,
1957
-            (status, error) => {
1958
-                logger.log('Received recorder status change: ', status, error);
1959
-                APP.UI.updateRecordingState(status);
1960
-            }
1961
-        );
1936
+            recorderSession => {
1937
+                if (!recorderSession) {
1938
+                    logger.error(
1939
+                        'Received invalid recorder status update',
1940
+                        recorderSession);
1941
+
1942
+                    return;
1943
+                }
1944
+
1945
+                if (recorderSession.getID()) {
1946
+                    APP.store.dispatch(
1947
+                        updateRecordingSessionData(recorderSession));
1948
+
1949
+                    return;
1950
+                }
1951
+
1952
+                // These errors fire when the local participant has requested a
1953
+                // recording but the request itself failed, hence the missing
1954
+                // session ID because the recorder never started.
1955
+                if (recorderSession.getError()) {
1956
+                    this._showRecordingErrorNotification(recorderSession);
1957
+
1958
+                    return;
1959
+                }
1960
+
1961
+                logger.error(
1962
+                    'Received a recorder status update with no ID or error');
1963
+            });
1962
 
1964
 
1963
         room.on(JitsiConferenceEvents.KICKED, () => {
1965
         room.on(JitsiConferenceEvents.KICKED, () => {
1964
             APP.UI.hideStats();
1966
             APP.UI.hideStats();
2093
                     }));
2095
                     }));
2094
             });
2096
             });
2095
 
2097
 
2096
-        /* eslint-enable max-params */
2097
-
2098
-        // Starts or stops the recording for the conference.
2099
-        APP.UI.addListener(UIEvents.RECORDING_TOGGLED, options => {
2100
-            room.toggleRecording(options);
2101
-        });
2102
-
2103
         APP.UI.addListener(UIEvents.AUTH_CLICKED, () => {
2098
         APP.UI.addListener(UIEvents.AUTH_CLICKED, () => {
2104
             AuthHandler.authenticate(room);
2099
             AuthHandler.authenticate(room);
2105
         });
2100
         });
2746
         if (score === -1 || (score >= 1 && score <= 5)) {
2741
         if (score === -1 || (score >= 1 && score <= 5)) {
2747
             APP.store.dispatch(submitFeedback(score, message, room));
2742
             APP.store.dispatch(submitFeedback(score, message, room));
2748
         }
2743
         }
2744
+    },
2745
+
2746
+    /**
2747
+     * Shows a notification about an error in the recording session. A
2748
+     * default notification will display if no error is specified in the passed
2749
+     * in recording session.
2750
+     *
2751
+     * @param {Object} recorderSession - The recorder session model from the
2752
+     * lib.
2753
+     * @private
2754
+     * @returns {void}
2755
+     */
2756
+    _showRecordingErrorNotification(recorderSession) {
2757
+        const isStreamMode
2758
+            = recorderSession.getMode()
2759
+                === JitsiMeetJS.constants.recording.mode.STREAM;
2760
+
2761
+        switch (recorderSession.getError()) {
2762
+        case JitsiMeetJS.constants.recording.error.SERVICE_UNAVAILABLE:
2763
+            APP.UI.messageHandler.showError({
2764
+                descriptionKey: 'recording.unavailable',
2765
+                descriptionArguments: {
2766
+                    serviceName: isStreamMode
2767
+                        ? 'Live Streaming service'
2768
+                        : 'Recording service'
2769
+                },
2770
+                titleKey: isStreamMode
2771
+                    ? 'liveStreaming.unavailableTitle'
2772
+                    : 'recording.unavailableTitle'
2773
+            });
2774
+            break;
2775
+        case JitsiMeetJS.constants.recording.error.RESOURCE_CONSTRAINT:
2776
+            APP.UI.messageHandler.showError({
2777
+                descriptionKey: isStreamMode
2778
+                    ? 'liveStreaming.busy'
2779
+                    : 'recording.busy',
2780
+                titleKey: isStreamMode
2781
+                    ? 'liveStreaming.busyTitle'
2782
+                    : 'recording.busyTitle'
2783
+            });
2784
+            break;
2785
+        default:
2786
+            APP.UI.messageHandler.showError({
2787
+                descriptionKey: isStreamMode
2788
+                    ? 'liveStreaming.error'
2789
+                    : 'recording.error',
2790
+                titleKey: isStreamMode
2791
+                    ? 'liveStreaming.failedToStart'
2792
+                    : 'recording.failedToStart'
2793
+            });
2794
+            break;
2795
+        }
2749
     }
2796
     }
2750
 };
2797
 };

+ 2
- 10
css/_vertical_filmstrip_overrides.scss View File

182
      * The class opening is for when the filmstrip is transitioning from hidden
182
      * The class opening is for when the filmstrip is transitioning from hidden
183
      * to visible.
183
      * to visible.
184
      */
184
      */
185
-    .video-state-indicator.moveToCorner {
186
-        transition: right 0.5s;
187
-
185
+    .large-video-labels {
188
         &.with-filmstrip {
186
         &.with-filmstrip {
189
-            &#recordingLabel {
190
-                right: 200px;
191
-            }
192
-
193
-            &#videoResolutionLabel {
194
-                right: 150px;
195
-            }
187
+            right: 150px;
196
         }
188
         }
197
 
189
 
198
         &.with-filmstrip.opening {
190
         &.with-filmstrip.opening {

+ 39
- 79
css/modals/video-quality/_video-quality.scss View File

141
     }
141
     }
142
 }
142
 }
143
 
143
 
144
-.video-state-indicator {
145
-    background: $videoStateIndicatorBackground;
146
-    cursor: default;
147
-    font-size: 13px;
148
-    height: $videoStateIndicatorSize;
149
-    line-height: 20px;
150
-    text-align: left;
151
-    min-width: $videoStateIndicatorSize;
152
-    border-radius: 50%;
153
-    position: absolute;
154
-    box-sizing: border-box;
155
-
156
-    &.is-recording {
157
-        background: none;
158
-        opacity: 0.9;
159
-        padding: 0;
160
-    }
144
+#videoResolutionLabel {
145
+    z-index: $zindex3 + 1;
146
+}
161
 
147
 
162
-    i {
163
-        line-height: $videoStateIndicatorSize;
164
-    }
148
+.large-video-labels {
149
+    display: flex;
150
+    position: absolute;
151
+    top: 30px;
152
+    right: 30px;
153
+    transition: right 0.5s;
154
+    z-index: $zindex3;
165
 
155
 
166
-    /**
167
-     * Give the label padding so it has more volume and can be easily clicked.
168
-     */
169
-    .video-quality-label-status {
170
-        line-height: $videoStateIndicatorSize;
171
-        min-width: $videoStateIndicatorSize;
172
-        text-align: center;
156
+    .circular-label {
157
+        color: white;
158
+        font-family: -apple-system, BlinkMacSystemFont, $baseFontFamily;
159
+        font-weight: bold;
160
+        margin-left: 8px;
161
+        opacity: 0.8;
173
     }
162
     }
174
 
163
 
175
-    .recording-icon,
176
-    .recording-icon i {
177
-        line-height: $videoStateIndicatorSize;
178
-        font-size: $videoStateIndicatorSize;
179
-        opacity: 0.9;
180
-        position: relative;
164
+    .circular-label {
165
+        background: #B8C7E0;
181
     }
166
     }
182
 
167
 
183
-    .icon-rec {
184
-        color: #FF5630;
168
+    .circular-label.file {
169
+        background: #FF5630;
185
     }
170
     }
186
 
171
 
187
-    .icon-live {
188
-        color: #0065FF;
172
+    .circular-label.stream {
173
+        background: #0065FF;
189
     }
174
     }
190
 
175
 
191
-    .recording-icon-background {
192
-        background: white;
193
-        border-radius: 50%;
194
-        height: calc(#{$videoStateIndicatorSize} - 1px);
176
+    .recording-label.center-message {
177
+        background: $videoStateIndicatorBackground;
178
+        bottom: 50%;
179
+        display: block;
195
         left: 50%;
180
         left: 50%;
196
-        opacity: 0.9;
197
-        position: absolute;
198
-        top: 50%;
181
+        padding: 10px;
182
+        position: fixed;
199
         transform: translate(-50%, -50%);
183
         transform: translate(-50%, -50%);
200
-        width: calc(#{$videoStateIndicatorSize} - 1px);
201
-    }
202
-
203
-    #recordingLabelText {
204
-        display: inline-block;
184
+        z-index: $centeredVideoLabelZ;
205
     }
185
     }
206
 }
186
 }
207
 
187
 
208
-.centeredVideoLabel.moveToCorner {
209
-    z-index: $zindex3;
210
-}
211
-
212
-#videoResolutionLabel {
213
-    z-index: $zindex3 + 1;
214
-}
215
-
216
-.centeredVideoLabel {
217
-    bottom: 45%;
218
-    border-radius: 2px;
219
-    display: none;
220
-    padding: 10px;
221
-    transform: translate(-50%, 0);
222
-    z-index: $centeredVideoLabelZ;
223
-
224
-    &.moveToCorner {
225
-        bottom: auto;
226
-        transform: none;
227
-    }
228
-}
229
-
230
-.moveToCorner {
231
-    position: absolute;
232
-    top: 30px;
233
-    right: 30px;
188
+.circular-label {
189
+    background: $videoStateIndicatorBackground;
190
+    border-radius: 50%;
191
+    box-sizing: border-box;
192
+    cursor: default;
193
+    font-size: 13px;
194
+    height: $videoStateIndicatorSize;
195
+    line-height: $videoStateIndicatorSize;
196
+    text-align: center;
197
+    min-width: $videoStateIndicatorSize;
234
 }
198
 }
235
-
236
-.moveToCorner + .moveToCorner {
237
-    right: 80px;
238
-}

+ 1
- 1
interface_config.js View File

46
         'microphone', 'camera', 'desktop', 'fullscreen', 'fodeviceselection', 'hangup',
46
         'microphone', 'camera', 'desktop', 'fullscreen', 'fodeviceselection', 'hangup',
47
 
47
 
48
         // extended toolbar
48
         // extended toolbar
49
-        'profile', 'info', 'chat', 'recording', 'etherpad',
49
+        'profile', 'info', 'chat', 'recording', 'livestreaming', 'etherpad',
50
         'sharedvideo', 'settings', 'raisehand', 'videoquality', 'filmstrip',
50
         'sharedvideo', 'settings', 'raisehand', 'videoquality', 'filmstrip',
51
         'invite', 'feedback', 'stats', 'shortcuts'
51
         'invite', 'feedback', 'stats', 'shortcuts'
52
     ],
52
     ],

+ 5
- 0
lang/main.json View File

206
     },
206
     },
207
     "dialog": {
207
     "dialog": {
208
         "allow": "Allow",
208
         "allow": "Allow",
209
+        "confirm": "Confirm",
209
         "kickMessage": "Ouch! You have been kicked out of the meet!",
210
         "kickMessage": "Ouch! You have been kicked out of the meet!",
210
         "popupErrorTitle": "Pop-up blocked",
211
         "popupErrorTitle": "Pop-up blocked",
211
         "popupError": "Your browser is blocking pop-up windows from this site. Please enable pop-ups in your browser's security settings and try again.",
212
         "popupError": "Your browser is blocking pop-up windows from this site. Please enable pop-ups in your browser's security settings and try again.",
389
         "buttonTooltip": "Start / Stop recording",
390
         "buttonTooltip": "Start / Stop recording",
390
         "error": "Recording failed. Please try again.",
391
         "error": "Recording failed. Please try again.",
391
         "failedToStart": "Recording failed to start",
392
         "failedToStart": "Recording failed to start",
393
+        "live": "LIVE",
392
         "off": "Recording stopped",
394
         "off": "Recording stopped",
393
         "on": "Recording",
395
         "on": "Recording",
394
         "pending": "Recording waiting for a member to join...",
396
         "pending": "Recording waiting for a member to join...",
397
+        "rec": "REC",
395
         "serviceName": "Recording service",
398
         "serviceName": "Recording service",
399
+        "startRecordingBody": "Are you sure you would like to start recording?",
396
         "unavailable": "Oops! The __serviceName__ is currently unavailable. We're working on resolving the issue. Please try again later.",
400
         "unavailable": "Oops! The __serviceName__ is currently unavailable. We're working on resolving the issue. Please try again later.",
397
         "unavailableTitle": "Recording unavailable"
401
         "unavailableTitle": "Recording unavailable"
398
     },
402
     },
448
         "testAudio": "Play a test sound"
452
         "testAudio": "Play a test sound"
449
     },
453
     },
450
     "videoStatus": {
454
     "videoStatus": {
455
+        "audioOnly": "AUD",
451
         "callQuality": "Call Quality",
456
         "callQuality": "Call Quality",
452
         "hd": "HD",
457
         "hd": "HD",
453
         "hdTooltip": "Viewing high definition video",
458
         "hdTooltip": "Viewing high definition video",

+ 24
- 16
modules/UI/UI.js View File

12
 import UIEvents from '../../service/UI/UIEvents';
12
 import UIEvents from '../../service/UI/UIEvents';
13
 import EtherpadManager from './etherpad/Etherpad';
13
 import EtherpadManager from './etherpad/Etherpad';
14
 import SharedVideoManager from './shared_video/SharedVideo';
14
 import SharedVideoManager from './shared_video/SharedVideo';
15
-import Recording from './recording/Recording';
16
 
15
 
17
 import VideoLayout from './videolayout/VideoLayout';
16
 import VideoLayout from './videolayout/VideoLayout';
18
 import Filmstrip from './videolayout/Filmstrip';
17
 import Filmstrip from './videolayout/Filmstrip';
38
 import { shouldShowOnlyDeviceSelection } from '../../react/features/settings';
37
 import { shouldShowOnlyDeviceSelection } from '../../react/features/settings';
39
 import {
38
 import {
40
     dockToolbox,
39
     dockToolbox,
40
+    setToolboxEnabled,
41
     showToolbox
41
     showToolbox
42
 } from '../../react/features/toolbox';
42
 } from '../../react/features/toolbox';
43
 
43
 
337
     if (!interfaceConfig.filmStripOnly) {
337
     if (!interfaceConfig.filmStripOnly) {
338
         VideoLayout.initLargeVideo();
338
         VideoLayout.initLargeVideo();
339
     }
339
     }
340
-    VideoLayout.resizeVideoArea(true, true);
340
+
341
+    // Do not animate the video area on UI start (second argument passed into
342
+    // resizeVideoArea) because the animation is not visible anyway. Plus with
343
+    // the current dom layout, the quality label is part of the video layout and
344
+    // will be seen animating in.
345
+    VideoLayout.resizeVideoArea(true, false);
341
 
346
 
342
     sharedVideoManager = new SharedVideoManager(eventEmitter);
347
     sharedVideoManager = new SharedVideoManager(eventEmitter);
343
 
348
 
346
         Filmstrip.setFilmstripOnly();
351
         Filmstrip.setFilmstripOnly();
347
         APP.store.dispatch(setNotificationsEnabled(false));
352
         APP.store.dispatch(setNotificationsEnabled(false));
348
     } else {
353
     } else {
349
-        // Initialise the recording module.
350
-        config.enableRecording
351
-            && Recording.init(eventEmitter, config.recordingType);
354
+        // Initialize recording mode UI.
355
+        if (config.enableRecording && config.iAmRecorder) {
356
+            VideoLayout.enableDeviceAvailabilityIcons(
357
+                APP.conference.getMyUserId(), false);
358
+
359
+            // in case of iAmSipGateway keep local video visible
360
+            if (!config.iAmSipGateway) {
361
+                VideoLayout.setLocalVideoVisible(false);
362
+            }
363
+
364
+            APP.store.dispatch(setToolboxEnabled(false));
365
+            APP.store.dispatch(setNotificationsEnabled(false));
366
+            UI.messageHandler.enablePopups(false);
367
+        }
352
 
368
 
353
         // Initialize side panels
369
         // Initialize side panels
354
         SidePanels.init(eventEmitter);
370
         SidePanels.init(eventEmitter);
520
 UI.updateLocalRole = isModerator => {
536
 UI.updateLocalRole = isModerator => {
521
     VideoLayout.showModeratorIndicator();
537
     VideoLayout.showModeratorIndicator();
522
 
538
 
523
-    if (isModerator) {
524
-        if (!interfaceConfig.DISABLE_FOCUS_INDICATOR) {
525
-            messageHandler.participantNotification(
526
-                null, 'notify.me', 'connected', 'notify.moderator');
527
-        }
528
-
529
-        Recording.checkAutoRecord();
539
+    if (isModerator && !interfaceConfig.DISABLE_FOCUS_INDICATOR) {
540
+        messageHandler.participantNotification(
541
+            null, 'notify.me', 'connected', 'notify.moderator');
530
     }
542
     }
531
 };
543
 };
532
 
544
 
881
     Chat.updateChatConversation(from, displayName, message, stamp);
893
     Chat.updateChatConversation(from, displayName, message, stamp);
882
 };
894
 };
883
 
895
 
884
-UI.updateRecordingState = function(state) {
885
-    Recording.updateRecordingState(state);
886
-};
887
-
888
 UI.notifyTokenAuthFailed = function() {
896
 UI.notifyTokenAuthFailed = function() {
889
     messageHandler.showError({
897
     messageHandler.showError({
890
         descriptionKey: 'dialog.tokenAuthFailed',
898
         descriptionKey: 'dialog.tokenAuthFailed',

+ 0
- 462
modules/UI/recording/Recording.js View File

1
-/* global APP, config, interfaceConfig */
2
-/*
3
- * Copyright @ 2015 Atlassian Pty Ltd
4
- *
5
- * Licensed under the Apache License, Version 2.0 (the "License");
6
- * you may not use this file except in compliance with the License.
7
- * You may obtain a copy of the License at
8
- *
9
- *     http://www.apache.org/licenses/LICENSE-2.0
10
- *
11
- * Unless required by applicable law or agreed to in writing, software
12
- * distributed under the License is distributed on an "AS IS" BASIS,
13
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
- * See the License for the specific language governing permissions and
15
- * limitations under the License.
16
- */
17
-const logger = require('jitsi-meet-logger').getLogger(__filename);
18
-
19
-import UIEvents from '../../../service/UI/UIEvents';
20
-import UIUtil from '../util/UIUtil';
21
-import VideoLayout from '../videolayout/VideoLayout';
22
-
23
-import { openDialog } from '../../../react/features/base/dialog';
24
-import {
25
-    JitsiRecordingStatus
26
-} from '../../../react/features/base/lib-jitsi-meet';
27
-import {
28
-    createToolbarEvent,
29
-    createRecordingDialogEvent,
30
-    sendAnalytics
31
-} from '../../../react/features/analytics';
32
-import { setToolboxEnabled } from '../../../react/features/toolbox';
33
-import { setNotificationsEnabled } from '../../../react/features/notifications';
34
-import {
35
-    StartLiveStreamDialog,
36
-    StopLiveStreamDialog,
37
-    hideRecordingLabel,
38
-    setRecordingType,
39
-    updateRecordingState
40
-} from '../../../react/features/recording';
41
-
42
-/**
43
- * Translation keys to use for display in the UI when recording the conference
44
- * but not streaming live.
45
- *
46
- * @private
47
- * @type {Object}
48
- */
49
-export const RECORDING_TRANSLATION_KEYS = {
50
-    failedToStartKey: 'recording.failedToStart',
51
-    recordingBusy: 'recording.busy',
52
-    recordingBusyTitle: 'recording.busyTitle',
53
-    recordingButtonTooltip: 'recording.buttonTooltip',
54
-    recordingErrorKey: 'recording.error',
55
-    recordingOffKey: 'recording.off',
56
-    recordingOnKey: 'recording.on',
57
-    recordingPendingKey: 'recording.pending',
58
-    recordingTitle: 'dialog.recording',
59
-    recordingUnavailable: 'recording.unavailable',
60
-    recordingUnavailableParams: '$t(recording.serviceName)',
61
-    recordingUnavailableTitle: 'recording.unavailableTitle'
62
-};
63
-
64
-/**
65
- * Translation keys to use for display in the UI when the recording mode is
66
- * currently streaming live.
67
- *
68
- * @private
69
- * @type {Object}
70
- */
71
-export const STREAMING_TRANSLATION_KEYS = {
72
-    failedToStartKey: 'liveStreaming.failedToStart',
73
-    recordingBusy: 'liveStreaming.busy',
74
-    recordingBusyTitle: 'liveStreaming.busyTitle',
75
-    recordingButtonTooltip: 'liveStreaming.buttonTooltip',
76
-    recordingErrorKey: 'liveStreaming.error',
77
-    recordingOffKey: 'liveStreaming.off',
78
-    recordingOnKey: 'liveStreaming.on',
79
-    recordingPendingKey: 'liveStreaming.pending',
80
-    recordingTitle: 'dialog.liveStreaming',
81
-    recordingUnavailable: 'recording.unavailable',
82
-    recordingUnavailableParams: '$t(liveStreaming.serviceName)',
83
-    recordingUnavailableTitle: 'liveStreaming.unavailableTitle'
84
-};
85
-
86
-/**
87
- * The dialog for user input.
88
- */
89
-let dialog = null;
90
-
91
-/**
92
- * Indicates if the recording button should be enabled.
93
- *
94
- * @returns {boolean} {true} if the
95
- * @private
96
- */
97
-function _isRecordingButtonEnabled() {
98
-    return (
99
-        interfaceConfig.TOOLBAR_BUTTONS.indexOf('recording') !== -1
100
-            && config.enableRecording
101
-            && APP.conference.isRecordingSupported());
102
-}
103
-
104
-/**
105
- * Request live stream token from the user.
106
- * @returns {Promise}
107
- */
108
-function _requestLiveStreamId() {
109
-    return new Promise((resolve, reject) =>
110
-        APP.store.dispatch(openDialog(StartLiveStreamDialog, {
111
-            onCancel: reject,
112
-            onSubmit: (streamId, broadcastId) => resolve({
113
-                broadcastId,
114
-                streamId
115
-            })
116
-        })));
117
-}
118
-
119
-/**
120
- * Request recording token from the user.
121
- * @returns {Promise}
122
- */
123
-function _requestRecordingToken() {
124
-    const titleKey = 'dialog.recordingToken';
125
-    const msgString
126
-        = `<input name="recordingToken" type="text"
127
-                data-i18n="[placeholder]dialog.token"
128
-                class="input-control"
129
-                autofocus>`
130
-
131
-    ;
132
-
133
-
134
-    return new Promise((resolve, reject) => {
135
-        dialog = APP.UI.messageHandler.openTwoButtonDialog({
136
-            titleKey,
137
-            msgString,
138
-            leftButtonKey: 'dialog.Save',
139
-            submitFunction(e, v, m, f) { // eslint-disable-line max-params
140
-                if (v && f.recordingToken) {
141
-                    resolve(UIUtil.escapeHtml(f.recordingToken));
142
-                } else {
143
-                    reject(APP.UI.messageHandler.CANCEL);
144
-                }
145
-            },
146
-            closeFunction() {
147
-                dialog = null;
148
-            },
149
-            focus: ':input:first'
150
-        });
151
-    });
152
-}
153
-
154
-/**
155
- * Shows a prompt dialog to the user when they have toggled off the recording.
156
- *
157
- * @param recordingType the recording type
158
- * @returns {Promise}
159
- * @private
160
- */
161
-function _showStopRecordingPrompt(recordingType) {
162
-    if (recordingType === 'jibri') {
163
-        return new Promise((resolve, reject) => {
164
-            APP.store.dispatch(openDialog(StopLiveStreamDialog, {
165
-                onCancel: reject,
166
-                onSubmit: resolve
167
-            }));
168
-        });
169
-    }
170
-
171
-    return new Promise((resolve, reject) => {
172
-        dialog = APP.UI.messageHandler.openTwoButtonDialog({
173
-            titleKey: 'dialog.recording',
174
-            msgKey: 'dialog.stopRecordingWarning',
175
-            leftButtonKey: 'dialog.stopRecording',
176
-            submitFunction: (e, v) => (v ? resolve : reject)(),
177
-            closeFunction: () => {
178
-                dialog = null;
179
-            }
180
-        });
181
-    });
182
-}
183
-
184
-/**
185
- * Checks whether if the given status is either PENDING or RETRYING
186
- * @param status {JitsiRecordingStatus} Jibri status to be checked
187
- * @returns {boolean} true if the condition is met or false otherwise.
188
- */
189
-function isStartingStatus(status) {
190
-    return (
191
-        status === JitsiRecordingStatus.PENDING
192
-            || status === JitsiRecordingStatus.RETRYING
193
-    );
194
-}
195
-
196
-/**
197
- * Manages the recording user interface and user experience.
198
- * @type {{init, updateRecordingState, updateRecordingUI, checkAutoRecord}}
199
- */
200
-const Recording = {
201
-    /**
202
-     * Initializes the recording UI.
203
-     */
204
-    init(eventEmitter, recordingType) {
205
-        this.eventEmitter = eventEmitter;
206
-        this.recordingType = recordingType;
207
-
208
-        APP.store.dispatch(setRecordingType(recordingType));
209
-
210
-        this.updateRecordingState(APP.conference.getRecordingState());
211
-
212
-        if (recordingType === 'jibri') {
213
-            this.baseClass = 'fa fa-play-circle';
214
-            Object.assign(this, STREAMING_TRANSLATION_KEYS);
215
-        } else {
216
-            this.baseClass = 'icon-recEnable';
217
-            Object.assign(this, RECORDING_TRANSLATION_KEYS);
218
-        }
219
-
220
-        this.eventEmitter.on(UIEvents.TOGGLE_RECORDING,
221
-            () => this._onToolbarButtonClick());
222
-
223
-        // If I am a recorder then I publish my recorder custom role to notify
224
-        // everyone.
225
-        if (config.iAmRecorder) {
226
-            VideoLayout.enableDeviceAvailabilityIcons(
227
-                APP.conference.getMyUserId(), false);
228
-
229
-            // in case of iAmSipGateway keep local video visible
230
-            if (!config.iAmSipGateway) {
231
-                VideoLayout.setLocalVideoVisible(false);
232
-            }
233
-
234
-            APP.store.dispatch(setToolboxEnabled(false));
235
-            APP.store.dispatch(setNotificationsEnabled(false));
236
-            APP.UI.messageHandler.enablePopups(false);
237
-        }
238
-    },
239
-
240
-    /**
241
-     * Updates the recording state UI.
242
-     * @param recordingState gives us the current recording state
243
-     */
244
-    updateRecordingState(recordingState) {
245
-        // I'm the recorder, so I don't want to see any UI related to states.
246
-        if (config.iAmRecorder) {
247
-            return;
248
-        }
249
-
250
-        // If there's no state change, we ignore the update.
251
-        if (!recordingState || this.currentState === recordingState) {
252
-            return;
253
-        }
254
-
255
-        this.updateRecordingUI(recordingState);
256
-    },
257
-
258
-    /**
259
-     * Sets the state of the recording button.
260
-     * @param recordingState gives us the current recording state
261
-     */
262
-    updateRecordingUI(recordingState) {
263
-        const oldState = this.currentState;
264
-
265
-        this.currentState = recordingState;
266
-
267
-        let labelDisplayConfiguration;
268
-        let isRecording = false;
269
-
270
-        switch (recordingState) {
271
-        case JitsiRecordingStatus.ON:
272
-        case JitsiRecordingStatus.RETRYING: {
273
-            labelDisplayConfiguration = {
274
-                centered: false,
275
-                key: this.recordingOnKey,
276
-                showSpinner: recordingState === JitsiRecordingStatus.RETRYING
277
-            };
278
-
279
-            isRecording = true;
280
-
281
-            break;
282
-        }
283
-
284
-        case JitsiRecordingStatus.OFF:
285
-        case JitsiRecordingStatus.BUSY:
286
-        case JitsiRecordingStatus.FAILED:
287
-        case JitsiRecordingStatus.UNAVAILABLE: {
288
-            const wasInStartingStatus = isStartingStatus(oldState);
289
-
290
-            // We don't want UI changes if this is an availability change.
291
-            if (oldState !== JitsiRecordingStatus.ON && !wasInStartingStatus) {
292
-                APP.store.dispatch(updateRecordingState({ recordingState }));
293
-
294
-                return;
295
-            }
296
-
297
-            labelDisplayConfiguration = {
298
-                centered: true,
299
-                key: wasInStartingStatus
300
-                    ? this.failedToStartKey
301
-                    : this.recordingOffKey
302
-            };
303
-
304
-            setTimeout(() => {
305
-                APP.store.dispatch(hideRecordingLabel());
306
-            }, 5000);
307
-
308
-            break;
309
-        }
310
-
311
-        case JitsiRecordingStatus.PENDING: {
312
-            labelDisplayConfiguration = {
313
-                centered: true,
314
-                key: this.recordingPendingKey
315
-            };
316
-
317
-            break;
318
-        }
319
-
320
-        case JitsiRecordingStatus.ERROR: {
321
-            labelDisplayConfiguration = {
322
-                centered: true,
323
-                key: this.recordingErrorKey
324
-            };
325
-
326
-            break;
327
-        }
328
-
329
-        // Return an empty label display configuration to indicate no label
330
-        // should be displayed. The JitsiRecordingStatus.AVAIABLE case is
331
-        // handled here.
332
-        default: {
333
-            labelDisplayConfiguration = null;
334
-        }
335
-        }
336
-
337
-        APP.store.dispatch(updateRecordingState({
338
-            isRecording,
339
-            labelDisplayConfiguration,
340
-            recordingState
341
-        }));
342
-    },
343
-
344
-    // checks whether recording is enabled and whether we have params
345
-    // to start automatically recording (XXX: No, it doesn't do that).
346
-    checkAutoRecord() {
347
-        if (_isRecordingButtonEnabled && config.autoRecord) {
348
-            this.predefinedToken = UIUtil.escapeHtml(config.autoRecordToken);
349
-            this.eventEmitter.emit(
350
-                UIEvents.RECORDING_TOGGLED,
351
-                { token: this.predefinedToken });
352
-        }
353
-    },
354
-
355
-    /**
356
-     * Handles {@code click} on {@code toolbar_button_record}.
357
-     *
358
-     * @returns {void}
359
-     */
360
-    _onToolbarButtonClick() {
361
-        sendAnalytics(createToolbarEvent(
362
-            'recording.button',
363
-            {
364
-                'dialog_present': Boolean(dialog)
365
-            }));
366
-
367
-        if (dialog) {
368
-            return;
369
-        }
370
-
371
-        switch (this.currentState) {
372
-        case JitsiRecordingStatus.ON:
373
-        case JitsiRecordingStatus.RETRYING:
374
-        case JitsiRecordingStatus.PENDING: {
375
-            _showStopRecordingPrompt(this.recordingType).then(
376
-                () => {
377
-                    this.eventEmitter.emit(UIEvents.RECORDING_TOGGLED);
378
-
379
-                    // The confirm button on the stop recording dialog was
380
-                    // clicked
381
-                    sendAnalytics(
382
-                        createRecordingDialogEvent(
383
-                            'stop',
384
-                            'confirm.button'));
385
-                },
386
-                () => {}); // eslint-disable-line no-empty-function
387
-            break;
388
-        }
389
-        case JitsiRecordingStatus.AVAILABLE:
390
-        case JitsiRecordingStatus.OFF: {
391
-            if (this.recordingType === 'jibri') {
392
-                _requestLiveStreamId()
393
-                .then(({ broadcastId, streamId }) => {
394
-                    this.eventEmitter.emit(
395
-                        UIEvents.RECORDING_TOGGLED,
396
-                        {
397
-                            broadcastId,
398
-                            streamId
399
-                        });
400
-
401
-                    // The confirm button on the start recording dialog was
402
-                    // clicked
403
-                    sendAnalytics(
404
-                        createRecordingDialogEvent(
405
-                            'start',
406
-                            'confirm.button'));
407
-                })
408
-                .catch(reason => {
409
-                    if (reason === APP.UI.messageHandler.CANCEL) {
410
-                        // The cancel button on the start recording dialog was
411
-                        // clicked
412
-                        sendAnalytics(
413
-                            createRecordingDialogEvent(
414
-                                'start',
415
-                                'cancel.button'));
416
-                    } else {
417
-                        logger.error(reason);
418
-                    }
419
-                });
420
-            } else {
421
-                // Note that we only fire analytics events for Jibri.
422
-                if (this.predefinedToken) {
423
-                    this.eventEmitter.emit(
424
-                        UIEvents.RECORDING_TOGGLED,
425
-                        { token: this.predefinedToken });
426
-
427
-                    return;
428
-                }
429
-
430
-                _requestRecordingToken().then(token => {
431
-                    this.eventEmitter.emit(
432
-                        UIEvents.RECORDING_TOGGLED,
433
-                        { token });
434
-                })
435
-                .catch(reason => {
436
-                    if (reason !== APP.UI.messageHandler.CANCEL) {
437
-                        logger.error(reason);
438
-                    }
439
-                });
440
-            }
441
-            break;
442
-        }
443
-        case JitsiRecordingStatus.BUSY: {
444
-            APP.UI.messageHandler.showWarning({
445
-                descriptionKey: this.recordingBusy,
446
-                titleKey: this.recordingBusyTitle
447
-            });
448
-            break;
449
-        }
450
-        default: {
451
-            APP.UI.messageHandler.showError({
452
-                descriptionKey: this.recordingUnavailable,
453
-                descriptionArguments: {
454
-                    serviceName: this.recordingUnavailableParams },
455
-                titleKey: this.recordingUnavailableTitle
456
-            });
457
-        }
458
-        }
459
-    }
460
-};
461
-
462
-export default Recording;

+ 2
- 0
modules/UI/videolayout/VideoLayout.js View File

854
     /**
854
     /**
855
      * Resizes the video area.
855
      * Resizes the video area.
856
      *
856
      *
857
+     * TODO: Remove the "animate" param as it is no longer passed in as true.
858
+     *
857
      * @param forceUpdate indicates that hidden thumbnails will be shown
859
      * @param forceUpdate indicates that hidden thumbnails will be shown
858
      * @param completeFunction a function to be called when the video area is
860
      * @param completeFunction a function to be called when the video area is
859
      * resized.
861
      * resized.

+ 1
- 1
package-lock.json View File

7608
       }
7608
       }
7609
     },
7609
     },
7610
     "lib-jitsi-meet": {
7610
     "lib-jitsi-meet": {
7611
-      "version": "github:jitsi/lib-jitsi-meet#fa24ac5289c5e73b2f5d4fe005cef8f9cfff8268",
7611
+      "version": "github:jitsi/lib-jitsi-meet#fefd96e0e8968c553aab6952bc7cf2116b53362e",
7612
       "requires": {
7612
       "requires": {
7613
         "async": "0.9.0",
7613
         "async": "0.9.0",
7614
         "current-executing-script": "0.1.3",
7614
         "current-executing-script": "0.1.3",

+ 1
- 1
package.json View File

46
     "jquery-i18next": "1.2.0",
46
     "jquery-i18next": "1.2.0",
47
     "js-md5": "0.6.1",
47
     "js-md5": "0.6.1",
48
     "jwt-decode": "2.2.0",
48
     "jwt-decode": "2.2.0",
49
-    "lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#fa24ac5289c5e73b2f5d4fe005cef8f9cfff8268",
49
+    "lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#fefd96e0e8968c553aab6952bc7cf2116b53362e",
50
     "lodash": "4.17.4",
50
     "lodash": "4.17.4",
51
     "moment": "2.19.4",
51
     "moment": "2.19.4",
52
     "postis": "2.2.0",
52
     "postis": "2.2.0",

+ 0
- 0
react/features/base/label/components/CircularLabel.native.js View File


+ 60
- 0
react/features/base/label/components/CircularLabel.web.js View File

1
+// @flow
2
+
3
+import React, { Component } from 'react';
4
+
5
+type Props = {
6
+
7
+    /**
8
+     * The children to be displayed within {@code CircularLabel}.
9
+     */
10
+    children: React$Node,
11
+
12
+    /**
13
+     * Additional CSS class names to add to the root of {@code CircularLabel}.
14
+     */
15
+    className: string,
16
+
17
+    /**
18
+     * HTML ID attribute to add to the root of {@code CircularLabel}.
19
+     */
20
+    id: string
21
+
22
+};
23
+
24
+/**
25
+ * React Component for showing short text in a circle.
26
+ *
27
+ * @extends Component
28
+ */
29
+export default class CircularLabel extends Component<Props> {
30
+    /**
31
+     * Default values for {@code CircularLabel} component's properties.
32
+     *
33
+     * @static
34
+     */
35
+    static defaultProps = {
36
+        className: ''
37
+    };
38
+
39
+    /**
40
+     * Implements React's {@link Component#render()}.
41
+     *
42
+     * @inheritdoc
43
+     * @returns {ReactElement}
44
+     */
45
+    render() {
46
+        const {
47
+            children,
48
+            className,
49
+            id
50
+        } = this.props;
51
+
52
+        return (
53
+            <div
54
+                className = { `circular-label ${className}` }
55
+                id = { id }>
56
+                { children }
57
+            </div>
58
+        );
59
+    }
60
+}

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

1
+export { default as CircularLabel } from './CircularLabel';

+ 1
- 0
react/features/base/label/index.js View File

1
+export * from './components';

+ 1
- 1
react/features/base/lib-jitsi-meet/index.js View File

17
 export const JitsiMediaDevicesEvents = JitsiMeetJS.events.mediaDevices;
17
 export const JitsiMediaDevicesEvents = JitsiMeetJS.events.mediaDevices;
18
 export const JitsiParticipantConnectionStatus
18
 export const JitsiParticipantConnectionStatus
19
     = JitsiMeetJS.constants.participantConnectionStatus;
19
     = JitsiMeetJS.constants.participantConnectionStatus;
20
-export const JitsiRecordingStatus = JitsiMeetJS.constants.recordingStatus;
20
+export const JitsiRecordingConstants = JitsiMeetJS.constants.recording;
21
 export const JitsiSIPVideoGWStatus = JitsiMeetJS.constants.sipVideoGW;
21
 export const JitsiSIPVideoGWStatus = JitsiMeetJS.constants.sipVideoGW;
22
 export const JitsiTrackErrors = JitsiMeetJS.errors.track;
22
 export const JitsiTrackErrors = JitsiMeetJS.errors.track;
23
 export const JitsiTrackEvents = JitsiMeetJS.events.track;
23
 export const JitsiTrackEvents = JitsiMeetJS.events.track;

+ 7
- 1
react/features/invite/components/InfoDialogButton.web.js View File

5
 
5
 
6
 import { createToolbarEvent, sendAnalytics } from '../../analytics';
6
 import { createToolbarEvent, sendAnalytics } from '../../analytics';
7
 import { translate } from '../../base/i18n';
7
 import { translate } from '../../base/i18n';
8
+import { JitsiRecordingConstants } from '../../base/lib-jitsi-meet';
8
 import { getParticipantCount } from '../../base/participants';
9
 import { getParticipantCount } from '../../base/participants';
10
+import { getActiveSession } from '../../recording';
9
 import { ToolbarButton } from '../../toolbox';
11
 import { ToolbarButton } from '../../toolbox';
10
 
12
 
11
 import { updateDialInNumbers } from '../actions';
13
 import { updateDialInNumbers } from '../actions';
228
  * }}
230
  * }}
229
  */
231
  */
230
 function _mapStateToProps(state) {
232
 function _mapStateToProps(state) {
233
+    const currentLiveStreamingSession
234
+        = getActiveSession(state, JitsiRecordingConstants.mode.STREAM);
235
+
231
     return {
236
     return {
232
         _dialIn: state['features/invite'],
237
         _dialIn: state['features/invite'],
233
         _disableAutoShow: state['features/base/config'].iAmRecorder,
238
         _disableAutoShow: state['features/base/config'].iAmRecorder,
234
-        _liveStreamViewURL: state['features/recording'].liveStreamViewURL,
239
+        _liveStreamViewURL: currentLiveStreamingSession
240
+            && currentLiveStreamingSession.liveStreamViewURL,
235
         _participantCount:
241
         _participantCount:
236
             getParticipantCount(state['features/base/participants']),
242
             getParticipantCount(state['features/base/participants']),
237
         _toolboxVisible: state['features/toolbox'].visible
243
         _toolboxVisible: state['features/toolbox'].visible

+ 0
- 0
react/features/large-video/components/Labels.native.js View File


+ 139
- 0
react/features/large-video/components/Labels.web.js View File

1
+// @flow
2
+
3
+import React, { Component } from 'react';
4
+import { connect } from 'react-redux';
5
+
6
+import { RecordingLabel } from '../../recording';
7
+import { VideoQualityLabel } from '../../video-quality';
8
+
9
+/**
10
+ * The type of the React {@code Component} props of {@link Labels}.
11
+ */
12
+type Props = {
13
+
14
+    /**
15
+    * Whether or not the filmstrip is displayed with remote videos. Used to
16
+    * determine display classes to set.
17
+    */
18
+    _filmstripVisible: boolean,
19
+
20
+
21
+    /**
22
+     * The redux state for all known recording sessions.
23
+     */
24
+    _recordingSessions: Array<Object>
25
+};
26
+
27
+/**
28
+ * The type of the React {@code Component} state of {@link Labels}.
29
+ */
30
+type State = {
31
+
32
+    /**
33
+     * Whether or not the filmstrip was not visible but has transitioned in the
34
+     * latest component update to visible. This boolean is used  to set a class
35
+     * for position animations.
36
+     *
37
+     * @type {boolean}
38
+     */
39
+    filmstripBecomingVisible: boolean
40
+}
41
+
42
+/**
43
+ * A container to hold video status labels, including recording status and
44
+ * current large video quality.
45
+ *
46
+ * @extends Component
47
+ */
48
+class Labels extends Component<Props, State> {
49
+    /**
50
+     * Initializes a new {@code Labels} instance.
51
+     *
52
+     * @param {Object} props - The read-only properties with which the new
53
+     * instance is to be initialized.
54
+     */
55
+    constructor(props: Props) {
56
+        super(props);
57
+
58
+        this.state = {
59
+            filmstripBecomingVisible: false
60
+        };
61
+
62
+        // Bind event handler so it is only bound once for every instance.
63
+        this._renderRecordingLabel = this._renderRecordingLabel.bind(this);
64
+    }
65
+
66
+    /**
67
+     * Updates the state for whether or not the filmstrip is being toggled to
68
+     * display after having being hidden.
69
+     *
70
+     * @inheritdoc
71
+     * @param {Object} nextProps - The read-only props which this Component will
72
+     * receive.
73
+     * @returns {void}
74
+     */
75
+    componentWillReceiveProps(nextProps) {
76
+        this.setState({
77
+            filmstripBecomingVisible: nextProps._filmstripVisible
78
+                && !this.props._filmstripVisible
79
+        });
80
+    }
81
+
82
+    /**
83
+     * Implements React's {@link Component#render()}.
84
+     *
85
+     * @inheritdoc
86
+     * @returns {ReactElement}
87
+     */
88
+    render() {
89
+        const { _filmstripVisible, _recordingSessions } = this.props;
90
+        const { filmstripBecomingVisible } = this.state;
91
+        const className = `large-video-labels ${
92
+            filmstripBecomingVisible ? 'opening' : ''} ${
93
+            _filmstripVisible ? 'with-filmstrip' : 'without-filmstrip'}`;
94
+
95
+        return (
96
+            <div className = { className } >
97
+                { _recordingSessions.map(this._renderRecordingLabel) }
98
+                <VideoQualityLabel />
99
+            </div>
100
+        );
101
+    }
102
+
103
+    _renderRecordingLabel: (Object) => React$Node;
104
+
105
+    /**
106
+     * Renders a recording label.
107
+     *
108
+     * @param {Object} recordingSession - The recording session to render.
109
+     * @private
110
+     * @returns {ReactElement}
111
+     */
112
+    _renderRecordingLabel(recordingSession) {
113
+        return (
114
+            <RecordingLabel
115
+                key = { recordingSession.id }
116
+                session = { recordingSession } />
117
+        );
118
+    }
119
+}
120
+
121
+/**
122
+ * Maps (parts of) the Redux state to the associated props for the
123
+ * {@code Labels} component.
124
+ *
125
+ * @param {Object} state - The Redux state.
126
+ * @private
127
+ * @returns {{
128
+ *     _filmstripVisible: boolean,
129
+ *     _recordingSessions: Array<Object>
130
+ * }}
131
+ */
132
+function _mapStateToProps(state) {
133
+    return {
134
+        _filmstripVisible: state['features/filmstrip'].visible,
135
+        _recordingSessions: state['features/recording'].sessionDatas
136
+    };
137
+}
138
+
139
+export default connect(_mapStateToProps)(Labels);

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

4
 import React, { Component } from 'react';
4
 import React, { Component } from 'react';
5
 
5
 
6
 import { Watermarks } from '../../base/react';
6
 import { Watermarks } from '../../base/react';
7
-import { VideoQualityLabel } from '../../video-quality';
8
-import { RecordingLabel } from '../../recording';
7
+
8
+import Labels from './Labels';
9
 
9
 
10
 declare var interfaceConfig: Object;
10
 declare var interfaceConfig: Object;
11
 
11
 
72
                 </div>
72
                 </div>
73
                 <span id = 'localConnectionMessage' />
73
                 <span id = 'localConnectionMessage' />
74
                 { this.props.hideVideoQualityLabel
74
                 { this.props.hideVideoQualityLabel
75
-                    ? null : <VideoQualityLabel /> }
76
-                <RecordingLabel />
75
+                    ? null : <Labels /> }
77
             </div>
76
             </div>
78
         );
77
         );
79
     }
78
     }

+ 5
- 38
react/features/recording/actionTypes.js View File

1
 /**
1
 /**
2
- * The type of Redux action which signals for the label indicating current
3
- * recording state to stop displaying.
2
+ * The type of Redux action which updates the current known state of a recording
3
+ * session.
4
  *
4
  *
5
  * {
5
  * {
6
- *     type: HIDE_RECORDING_LABEL
6
+ *     type: RECORDING_SESSION_UPDATED,
7
+ *     sessionData: Object
7
  * }
8
  * }
8
  * @public
9
  * @public
9
  */
10
  */
10
-export const HIDE_RECORDING_LABEL = Symbol('HIDE_RECORDING_LABEL');
11
-
12
-/**
13
- * The type of Redux action which updates the current known state of the
14
- * recording feature.
15
- *
16
- * {
17
- *     type: RECORDING_STATE_UPDATED,
18
- *     recordingState: string
19
- * }
20
- * @public
21
- */
22
-export const RECORDING_STATE_UPDATED = Symbol('RECORDING_STATE_UPDATED');
23
-
24
-/**
25
- * The type of Redux action which updates the current known type of configured
26
- * recording. For example, type "jibri" is used for live streaming.
27
- *
28
- * {
29
- *     type: RECORDING_STATE_UPDATED,
30
- *     recordingType: string
31
- * }
32
- * @public
33
- */
34
-export const SET_RECORDING_TYPE = Symbol('SET_RECORDING_TYPE');
35
-
36
-/**
37
- * The type of Redux action triggers the flow to start or stop recording.
38
- *
39
- * {
40
- *     type: TOGGLE_RECORDING
41
- * }
42
- * @public
43
- */
44
-export const TOGGLE_RECORDING = Symbol('TOGGLE_RECORDING');
11
+export const RECORDING_SESSION_UPDATED = Symbol('RECORDING_SESSION_UPDATED');

+ 15
- 57
react/features/recording/actions.js View File

1
-import {
2
-    HIDE_RECORDING_LABEL,
3
-    RECORDING_STATE_UPDATED,
4
-    SET_RECORDING_TYPE,
5
-    TOGGLE_RECORDING
6
-} from './actionTypes';
1
+import { RECORDING_SESSION_UPDATED } from './actionTypes';
7
 
2
 
8
 /**
3
 /**
9
- * Hides any displayed recording label, regardless of current recording state.
4
+ * Updates the known state for a given recording session.
10
  *
5
  *
6
+ * @param {Object} session - The new state to merge with the existing state in
7
+ * redux.
11
  * @returns {{
8
  * @returns {{
12
- *     type: HIDE_RECORDING_LABEL
9
+ *     type: RECORDING_SESSION_UPDATED,
10
+ *     sessionData: Object
13
  * }}
11
  * }}
14
  */
12
  */
15
-export function hideRecordingLabel() {
13
+export function updateRecordingSessionData(session) {
16
     return {
14
     return {
17
-        type: HIDE_RECORDING_LABEL
18
-    };
19
-}
20
-
21
-/**
22
- * Sets what type of recording service will be used.
23
- *
24
- * @param {string} recordingType - The type of recording service to be used.
25
- * Should be one of the enumerated types in {@link RECORDING_TYPES}.
26
- * @returns {{
27
- *     type: SET_RECORDING_TYPE,
28
- *     recordingType: string
29
- * }}
30
- */
31
-export function setRecordingType(recordingType) {
32
-    return {
33
-        type: SET_RECORDING_TYPE,
34
-        recordingType
35
-    };
36
-}
37
-
38
-/**
39
- * Start or stop recording.
40
- *
41
- * @returns {{
42
- *     type: TOGGLE_RECORDING
43
- * }}
44
- */
45
-export function toggleRecording() {
46
-    return {
47
-        type: TOGGLE_RECORDING
48
-    };
49
-}
50
-
51
-/**
52
- * Updates the redux state for the recording feature.
53
- *
54
- * @param {Object} recordingState - The new state to merge with the existing
55
- * state in redux.
56
- * @returns {{
57
- *     type: RECORDING_STATE_UPDATED,
58
- *     recordingState: Object
59
- * }}
60
- */
61
-export function updateRecordingState(recordingState = {}) {
62
-    return {
63
-        type: RECORDING_STATE_UPDATED,
64
-        recordingState
15
+        type: RECORDING_SESSION_UPDATED,
16
+        sessionData: {
17
+            error: session.getError(),
18
+            id: session.getID(),
19
+            liveStreamViewURL: session.getLiveStreamViewURL(),
20
+            mode: session.getMode(),
21
+            status: session.getStatus()
22
+        }
65
     };
23
     };
66
 }
24
 }

+ 103
- 55
react/features/recording/components/LiveStream/StartLiveStreamDialog.web.js View File

1
-/* globals APP, interfaceConfig */
1
+// @flow
2
 
2
 
3
 import Spinner from '@atlaskit/spinner';
3
 import Spinner from '@atlaskit/spinner';
4
-import PropTypes from 'prop-types';
5
 import React, { Component } from 'react';
4
 import React, { Component } from 'react';
6
 import { connect } from 'react-redux';
5
 import { connect } from 'react-redux';
7
 
6
 
7
+import {
8
+    createRecordingDialogEvent,
9
+    sendAnalytics
10
+} from '../../../analytics';
8
 import { Dialog } from '../../../base/dialog';
11
 import { Dialog } from '../../../base/dialog';
9
 import { translate } from '../../../base/i18n';
12
 import { translate } from '../../../base/i18n';
13
+import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
10
 
14
 
11
 import googleApi from '../../googleApi';
15
 import googleApi from '../../googleApi';
12
 
16
 
14
 import GoogleSignInButton from './GoogleSignInButton';
18
 import GoogleSignInButton from './GoogleSignInButton';
15
 import StreamKeyForm from './StreamKeyForm';
19
 import StreamKeyForm from './StreamKeyForm';
16
 
20
 
21
+declare var interfaceConfig: Object;
22
+
17
 /**
23
 /**
18
  * An enumeration of the different states the Google API can be in while
24
  * An enumeration of the different states the Google API can be in while
19
  * interacting with {@code StartLiveStreamDialog}.
25
  * interacting with {@code StartLiveStreamDialog}.
45
 };
51
 };
46
 
52
 
47
 /**
53
 /**
48
- * A React Component for requesting a YouTube stream key to use for live
49
- * streaming of the current conference.
50
- *
51
- * @extends Component
54
+ * The type of the React {@code Component} props of
55
+ * {@link StartLiveStreamDialog}.
52
  */
56
  */
53
-class StartLiveStreamDialog extends Component {
57
+type Props = {
58
+
54
     /**
59
     /**
55
-     * {@code StartLiveStreamDialog} component's property types.
56
-     *
57
-     * @static
60
+     * The {@code JitsiConference} for the current conference.
58
      */
61
      */
59
-    static propTypes = {
60
-        /**
61
-         * The ID for the Google web client application used for making stream
62
-         * key related requests.
63
-         */
64
-        _googleApiApplicationClientID: PropTypes.string,
62
+    _conference: Object,
65
 
63
 
66
-        /**
67
-         * Callback to invoke when the dialog is dismissed without submitting a
68
-         * stream key.
69
-         */
70
-        onCancel: PropTypes.func,
64
+    /**
65
+     * The ID for the Google web client application used for making stream key
66
+     * related requests.
67
+     */
68
+    _googleApiApplicationClientID: string,
71
 
69
 
72
-        /**
73
-         * Callback to invoke when a stream key is submitted for use.
74
-         */
75
-        onSubmit: PropTypes.func,
70
+    /**
71
+     * Invoked to obtain translated strings.
72
+     */
73
+    t: Function
74
+};
76
 
75
 
77
-        /**
78
-         * Invoked to obtain translated strings.
79
-         */
80
-        t: PropTypes.func
81
-    };
76
+/**
77
+ * The type of the React {@code Component} state of
78
+ * {@link StartLiveStreamDialog}.
79
+ */
80
+type State = {
82
 
81
 
83
     /**
82
     /**
84
-     * {@code StartLiveStreamDialog} component's local state.
85
-     *
86
-     * @property {boolean} googleAPIState - The current state of interactions
87
-     * with the Google API. Determines what Google related UI should display.
88
-     * @property {Object[]|undefined} broadcasts - Details about the broadcasts
89
-     * available for use for the logged in Google user's YouTube account.
90
-     * @property {string} googleProfileEmail - The email of the user currently
91
-     * logged in to the Google web client application.
92
-     * @property {string} selectedBoundStreamID - The boundStreamID of the
93
-     * broadcast currently selected in the broadcast dropdown.
94
-     * @property {string} streamKey - The selected or entered stream key to use
95
-     * for YouTube live streaming.
83
+     * Details about the broadcasts available for use for the logged in Google
84
+     * user's YouTube account.
96
      */
85
      */
97
-    state = {
98
-        broadcasts: undefined,
99
-        googleAPIState: GOOGLE_API_STATES.NEEDS_LOADING,
100
-        googleProfileEmail: '',
101
-        selectedBoundStreamID: undefined,
102
-        streamKey: ''
103
-    };
86
+    broadcasts: ?Array<Object>,
87
+
88
+    /**
89
+     * The current state of interactions with the Google API. Determines what
90
+     * Google related UI should display.
91
+     */
92
+    googleAPIState: number,
93
+
94
+    /**
95
+     * The email of the user currently logged in to the Google web client
96
+     * application.
97
+     */
98
+    googleProfileEmail: string,
99
+
100
+    /**
101
+     * The boundStreamID of the broadcast currently selected in the broadcast
102
+     * dropdown.
103
+     */
104
+    selectedBoundStreamID: ?string,
105
+
106
+    /**
107
+     * The selected or entered stream key to use for YouTube live streaming.
108
+     */
109
+    streamKey: string
110
+};
111
+
112
+/**
113
+ * A React Component for requesting a YouTube stream key to use for live
114
+ * streaming of the current conference.
115
+ *
116
+ * @extends Component
117
+ */
118
+class StartLiveStreamDialog extends Component<Props, State> {
119
+    _isMounted: boolean;
104
 
120
 
105
     /**
121
     /**
106
      * Initializes a new {@code StartLiveStreamDialog} instance.
122
      * Initializes a new {@code StartLiveStreamDialog} instance.
108
      * @param {Props} props - The React {@code Component} props to initialize
124
      * @param {Props} props - The React {@code Component} props to initialize
109
      * the new {@code StartLiveStreamDialog} instance with.
125
      * the new {@code StartLiveStreamDialog} instance with.
110
      */
126
      */
111
-    constructor(props) {
127
+    constructor(props: Props) {
112
         super(props);
128
         super(props);
113
 
129
 
130
+        this.state = {
131
+            broadcasts: undefined,
132
+            googleAPIState: GOOGLE_API_STATES.NEEDS_LOADING,
133
+            googleProfileEmail: '',
134
+            selectedBoundStreamID: undefined,
135
+            streamKey: ''
136
+        };
137
+
114
         /**
138
         /**
115
          * Instance variable used to flag whether the component is or is not
139
          * Instance variable used to flag whether the component is or is not
116
          * mounted. Used as a hack to avoid setting state on an unmounted
140
          * mounted. Used as a hack to avoid setting state on an unmounted
186
         );
210
         );
187
     }
211
     }
188
 
212
 
213
+    _onInitializeGoogleApi: () => Object;
214
+
189
     /**
215
     /**
190
      * Loads the Google web client application used for fetching stream keys.
216
      * Loads the Google web client application used for fetching stream keys.
191
      * If the user is already logged in, then a request for available YouTube
217
      * If the user is already logged in, then a request for available YouTube
214
             });
240
             });
215
     }
241
     }
216
 
242
 
243
+    _onCancel: () => boolean;
244
+
217
     /**
245
     /**
218
      * Invokes the passed in {@link onCancel} callback and closes
246
      * Invokes the passed in {@link onCancel} callback and closes
219
      * {@code StartLiveStreamDialog}.
247
      * {@code StartLiveStreamDialog}.
222
      * @returns {boolean} True is returned to close the modal.
250
      * @returns {boolean} True is returned to close the modal.
223
      */
251
      */
224
     _onCancel() {
252
     _onCancel() {
225
-        this.props.onCancel(APP.UI.messageHandler.CANCEL);
253
+        sendAnalytics(createRecordingDialogEvent('start', 'cancel.button'));
226
 
254
 
227
         return true;
255
         return true;
228
     }
256
     }
229
 
257
 
258
+    _onGetYouTubeBroadcasts: () => Object;
259
+
230
     /**
260
     /**
231
      * Asks the user to sign in, if not already signed in, and then requests a
261
      * Asks the user to sign in, if not already signed in, and then requests a
232
      * list of the user's YouTube broadcasts.
262
      * list of the user's YouTube broadcasts.
269
             });
299
             });
270
     }
300
     }
271
 
301
 
302
+    _onRequestGoogleSignIn: () => Object;
303
+
272
     /**
304
     /**
273
      * Forces the Google web client application to prompt for a sign in, such as
305
      * Forces the Google web client application to prompt for a sign in, such as
274
      * when changing account, and will then fetch available YouTube broadcasts.
306
      * when changing account, and will then fetch available YouTube broadcasts.
282
             .then(() => this._onGetYouTubeBroadcasts());
314
             .then(() => this._onGetYouTubeBroadcasts());
283
     }
315
     }
284
 
316
 
317
+    _onStreamKeyChange: () => void;
318
+
285
     /**
319
     /**
286
      * Callback invoked to update the {@code StartLiveStreamDialog} component's
320
      * Callback invoked to update the {@code StartLiveStreamDialog} component's
287
      * display of the entered YouTube stream key.
321
      * display of the entered YouTube stream key.
297
         });
331
         });
298
     }
332
     }
299
 
333
 
334
+    _onSubmit: () => boolean;
335
+
300
     /**
336
     /**
301
      * Invokes the passed in {@link onSubmit} callback with the entered stream
337
      * Invokes the passed in {@link onSubmit} callback with the entered stream
302
      * key, and then closes {@code StartLiveStreamDialog}.
338
      * key, and then closes {@code StartLiveStreamDialog}.
306
      * closing, true to close the modal.
342
      * closing, true to close the modal.
307
      */
343
      */
308
     _onSubmit() {
344
     _onSubmit() {
309
-        const { streamKey, selectedBoundStreamID } = this.state;
345
+        const { broadcasts, streamKey, selectedBoundStreamID } = this.state;
310
 
346
 
311
         if (!streamKey) {
347
         if (!streamKey) {
312
             return false;
348
             return false;
315
         let selectedBroadcastID = null;
351
         let selectedBroadcastID = null;
316
 
352
 
317
         if (selectedBoundStreamID) {
353
         if (selectedBoundStreamID) {
318
-            const selectedBroadcast = this.state.broadcasts.find(
354
+            const selectedBroadcast = broadcasts && broadcasts.find(
319
                 broadcast => broadcast.boundStreamID === selectedBoundStreamID);
355
                 broadcast => broadcast.boundStreamID === selectedBoundStreamID);
320
 
356
 
321
             selectedBroadcastID = selectedBroadcast && selectedBroadcast.id;
357
             selectedBroadcastID = selectedBroadcast && selectedBroadcast.id;
322
         }
358
         }
323
 
359
 
324
-        this.props.onSubmit(streamKey, selectedBroadcastID);
360
+        sendAnalytics(createRecordingDialogEvent('start', 'confirm.button'));
361
+
362
+        this.props._conference.startRecording({
363
+            broadcastId: selectedBroadcastID,
364
+            mode: JitsiRecordingConstants.mode.STREAM,
365
+            streamId: streamKey
366
+        });
325
 
367
 
326
         return true;
368
         return true;
327
     }
369
     }
328
 
370
 
371
+    _onYouTubeBroadcastIDSelected: (string) => Object;
372
+
329
     /**
373
     /**
330
      * Fetches the stream key for a YouTube broadcast and updates the internal
374
      * Fetches the stream key for a YouTube broadcast and updates the internal
331
      * state to display the associated stream key as being entered.
375
      * state to display the associated stream key as being entered.
351
             });
395
             });
352
     }
396
     }
353
 
397
 
398
+    _parseBroadcasts: (Array<Object>) => Array<Object>;
399
+
354
     /**
400
     /**
355
      * Takes in a list of broadcasts from the YouTube API, removes dupes,
401
      * Takes in a list of broadcasts from the YouTube API, removes dupes,
356
      * removes broadcasts that cannot get a stream key, and parses the
402
      * removes broadcasts that cannot get a stream key, and parses the
487
  * {@code StartLiveStreamDialog}.
533
  * {@code StartLiveStreamDialog}.
488
  *
534
  *
489
  * @param {Object} state - The redux state.
535
  * @param {Object} state - The redux state.
490
- * @protected
536
+ * @private
491
  * @returns {{
537
  * @returns {{
538
+ *     _conference: Object,
492
  *     _googleApiApplicationClientID: string
539
  *     _googleApiApplicationClientID: string
493
  * }}
540
  * }}
494
  */
541
  */
495
 function _mapStateToProps(state) {
542
 function _mapStateToProps(state) {
496
     return {
543
     return {
544
+        _conference: state['features/base/conference'].conference,
497
         _googleApiApplicationClientID:
545
         _googleApiApplicationClientID:
498
             state['features/base/config'].googleApiApplicationClientID
546
             state['features/base/config'].googleApiApplicationClientID
499
     };
547
     };

+ 56
- 41
react/features/recording/components/LiveStream/StopLiveStreamDialog.web.js View File

1
-import PropTypes from 'prop-types';
1
+// @flow
2
+
2
 import React, { Component } from 'react';
3
 import React, { Component } from 'react';
4
+import { connect } from 'react-redux';
3
 
5
 
4
 import { Dialog } from '../../../base/dialog';
6
 import { Dialog } from '../../../base/dialog';
5
 import { translate } from '../../../base/i18n';
7
 import { translate } from '../../../base/i18n';
8
+import {
9
+    createRecordingDialogEvent,
10
+    sendAnalytics
11
+} from '../../../analytics';
12
+
13
+/**
14
+ * The type of the React {@code Component} props of
15
+ * {@link StopLiveStreamDialog}.
16
+ */
17
+type Props = {
18
+
19
+    /**
20
+     * The {@code JitsiConference} for the current conference.
21
+     */
22
+    _conference: Object,
23
+
24
+    /**
25
+     * The redux representation of the live stremaing to be stopped.
26
+     */
27
+    session: Object,
28
+
29
+    /**
30
+     * Invoked to obtain translated strings.
31
+     */
32
+    t: Function
33
+};
6
 
34
 
7
 /**
35
 /**
8
  * A React Component for confirming the participant wishes to stop the currently
36
  * A React Component for confirming the participant wishes to stop the currently
10
  *
38
  *
11
  * @extends Component
39
  * @extends Component
12
  */
40
  */
13
-class StopLiveStreamDialog extends Component {
14
-    /**
15
-     * {@code StopLiveStreamDialog} component's property types.
16
-     *
17
-     * @static
18
-     */
19
-    static propTypes = {
20
-        /**
21
-         * Callback to invoke when the dialog is dismissed without confirming
22
-         * the live stream should be stopped.
23
-         */
24
-        onCancel: PropTypes.func,
25
-
26
-        /**
27
-         * Callback to invoke when confirming the live stream should be stopped.
28
-         */
29
-        onSubmit: PropTypes.func,
30
-
31
-        /**
32
-         * Invoked to obtain translated strings.
33
-         */
34
-        t: PropTypes.func
35
-    };
36
-
41
+class StopLiveStreamDialog extends Component<Props> {
37
     /**
42
     /**
38
      * Initializes a new {@code StopLiveStreamDialog} instance.
43
      * Initializes a new {@code StopLiveStreamDialog} instance.
39
      *
44
      *
40
      * @param {Object} props - The read-only properties with which the new
45
      * @param {Object} props - The read-only properties with which the new
41
      * instance is to be initialized.
46
      * instance is to be initialized.
42
      */
47
      */
43
-    constructor(props) {
48
+    constructor(props: Props) {
44
         super(props);
49
         super(props);
45
 
50
 
46
         // Bind event handler so it is only bound once for every instance.
51
         // Bind event handler so it is only bound once for every instance.
47
-        this._onCancel = this._onCancel.bind(this);
48
         this._onSubmit = this._onSubmit.bind(this);
52
         this._onSubmit = this._onSubmit.bind(this);
49
     }
53
     }
50
 
54
 
58
         return (
62
         return (
59
             <Dialog
63
             <Dialog
60
                 okTitleKey = 'dialog.stopLiveStreaming'
64
                 okTitleKey = 'dialog.stopLiveStreaming'
61
-                onCancel = { this._onCancel }
62
                 onSubmit = { this._onSubmit }
65
                 onSubmit = { this._onSubmit }
63
                 titleKey = 'dialog.liveStreaming'
66
                 titleKey = 'dialog.liveStreaming'
64
                 width = 'small'>
67
                 width = 'small'>
67
         );
70
         );
68
     }
71
     }
69
 
72
 
70
-    /**
71
-     * Callback invoked when stopping of live streaming is canceled.
72
-     *
73
-     * @private
74
-     * @returns {boolean} True to close the modal.
75
-     */
76
-    _onCancel() {
77
-        this.props.onCancel();
78
-
79
-        return true;
80
-    }
73
+    _onSubmit: () => boolean;
81
 
74
 
82
     /**
75
     /**
83
      * Callback invoked when stopping of live streaming is confirmed.
76
      * Callback invoked when stopping of live streaming is confirmed.
86
      * @returns {boolean} True to close the modal.
79
      * @returns {boolean} True to close the modal.
87
      */
80
      */
88
     _onSubmit() {
81
     _onSubmit() {
89
-        this.props.onSubmit();
82
+        sendAnalytics(createRecordingDialogEvent('stop', 'confirm.button'));
83
+
84
+        const { session } = this.props;
85
+
86
+        if (session) {
87
+            this.props._conference.stopRecording(session.id);
88
+        }
90
 
89
 
91
         return true;
90
         return true;
92
     }
91
     }
93
 }
92
 }
94
 
93
 
95
-export default translate(StopLiveStreamDialog);
94
+/**
95
+ * Maps (parts of) the redux state to the React {@code Component} props of
96
+ * {@code StopLiveStreamDialog}.
97
+ *
98
+ * @param {Object} state - The redux state.
99
+ * @private
100
+ * @returns {{
101
+ *     _conference: Object
102
+ * }}
103
+ */
104
+function _mapStateToProps(state) {
105
+    return {
106
+        _conference: state['features/base/conference'].conference
107
+    };
108
+}
109
+
110
+export default translate(connect(_mapStateToProps)(StopLiveStreamDialog));

+ 0
- 0
react/features/recording/components/Recording/StartRecordingDialog.native.js View File


+ 103
- 0
react/features/recording/components/Recording/StartRecordingDialog.web.js View File

1
+// @flow
2
+
3
+import React, { Component } from 'react';
4
+import { connect } from 'react-redux';
5
+
6
+import {
7
+    createRecordingDialogEvent,
8
+    sendAnalytics
9
+} from '../../../analytics';
10
+import { Dialog } from '../../../base/dialog';
11
+import { translate } from '../../../base/i18n';
12
+import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
13
+
14
+/**
15
+ * The type of the React {@code Component} props of
16
+ * {@link StartRecordingDialog}.
17
+ */
18
+type Props = {
19
+
20
+    /**
21
+     * The {@code JitsiConference} for the current conference.
22
+     */
23
+    _conference: Object,
24
+
25
+    /**
26
+     * Invoked to obtain translated strings.
27
+     */
28
+    t: Function
29
+};
30
+
31
+/**
32
+ * React Component for getting confirmation to start a file recording session.
33
+ *
34
+ * @extends Component
35
+ */
36
+class StartRecordingDialog extends Component<Props> {
37
+    /**
38
+     * Initializes a new {@code StartRecordingDialog} instance.
39
+     *
40
+     * @param {Props} props - The read-only properties with which the new
41
+     * instance is to be initialized.
42
+     */
43
+    constructor(props: Props) {
44
+        super(props);
45
+
46
+        // Bind event handler so it is only bound once for every instance.
47
+        this._onSubmit = this._onSubmit.bind(this);
48
+    }
49
+
50
+    /**
51
+     * Implements React's {@link Component#render()}.
52
+     *
53
+     * @inheritdoc
54
+     * @returns {ReactElement}
55
+     */
56
+    render() {
57
+        return (
58
+            <Dialog
59
+                okTitleKey = 'dialog.confirm'
60
+                onSubmit = { this._onSubmit }
61
+                titleKey = 'dialog.recording'
62
+                width = 'small'>
63
+                { this.props.t('recording.startRecordingBody') }
64
+            </Dialog>
65
+        );
66
+    }
67
+
68
+    _onSubmit: () => boolean;
69
+
70
+    /**
71
+     * Starts a file recording session.
72
+     *
73
+     * @private
74
+     * @returns {boolean} - True (to note that the modal should be closed).
75
+     */
76
+    _onSubmit() {
77
+        sendAnalytics(createRecordingDialogEvent('start', 'confirm.button'));
78
+
79
+        this.props._conference.startRecording({
80
+            mode: JitsiRecordingConstants.mode.FILE
81
+        });
82
+
83
+        return true;
84
+    }
85
+}
86
+
87
+/**
88
+ * Maps (parts of) the Redux state to the associated props for the
89
+ * {@code StartRecordingDialog} component.
90
+ *
91
+ * @param {Object} state - The Redux state.
92
+ * @private
93
+ * @returns {{
94
+ *     _conference: JitsiConference
95
+ * }}
96
+ */
97
+function _mapStateToProps(state) {
98
+    return {
99
+        _conference: state['features/base/conference'].conference
100
+    };
101
+}
102
+
103
+export default translate(connect(_mapStateToProps)(StartRecordingDialog));

+ 0
- 0
react/features/recording/components/Recording/StopRecordingDialog.native.js View File


+ 109
- 0
react/features/recording/components/Recording/StopRecordingDialog.web.js View File

1
+// @flow
2
+
3
+import React, { Component } from 'react';
4
+import { connect } from 'react-redux';
5
+
6
+import {
7
+    createRecordingDialogEvent,
8
+    sendAnalytics
9
+} from '../../../analytics';
10
+import { Dialog } from '../../../base/dialog';
11
+import { translate } from '../../../base/i18n';
12
+
13
+/**
14
+ * The type of the React {@code Component} props of {@link StopRecordingDialog}.
15
+ */
16
+type Props = {
17
+
18
+    /**
19
+     * The {@code JitsiConference} for the current conference.
20
+     */
21
+    _conference: Object,
22
+
23
+    /**
24
+     * The redux representation of the recording session to be stopped.
25
+     */
26
+    session: Object,
27
+
28
+    /**
29
+     * Invoked to obtain translated strings.
30
+     */
31
+    t: Function
32
+};
33
+
34
+/**
35
+ * React Component for getting confirmation to stop a file recording session in
36
+ * progress.
37
+ *
38
+ * @extends Component
39
+ */
40
+class StopRecordingDialog extends Component<Props> {
41
+    /**
42
+     * Initializes a new {@code StopRecordingDialog} instance.
43
+     *
44
+     * @param {Props} props - The read-only properties with which the new
45
+     * instance is to be initialized.
46
+     */
47
+    constructor(props) {
48
+        super(props);
49
+
50
+        // Bind event handler so it is only bound once for every instance.
51
+        this._onSubmit = this._onSubmit.bind(this);
52
+    }
53
+
54
+    /**
55
+     * Implements React's {@link Component#render()}.
56
+     *
57
+     * @inheritdoc
58
+     * @returns {ReactElement}
59
+     */
60
+    render() {
61
+        return (
62
+            <Dialog
63
+                okTitleKey = 'dialog.stopRecording'
64
+                onSubmit = { this._onSubmit }
65
+                titleKey = 'dialog.recording'
66
+                width = 'small'>
67
+                { this.props.t('dialog.stopRecordingWarning') }
68
+            </Dialog>
69
+        );
70
+    }
71
+
72
+    _onSubmit: () => boolean;
73
+
74
+    /**
75
+     * Stops the recording session.
76
+     *
77
+     * @private
78
+     * @returns {boolean} - True (to note that the modal should be closed).
79
+     */
80
+    _onSubmit() {
81
+        sendAnalytics(createRecordingDialogEvent('stop', 'confirm.button'));
82
+
83
+        const { session } = this.props;
84
+
85
+        if (session) {
86
+            this.props._conference.stopRecording(session.id);
87
+        }
88
+
89
+        return true;
90
+    }
91
+}
92
+
93
+/**
94
+ * Maps (parts of) the Redux state to the associated props for the
95
+ * {@code StopRecordingDialog} component.
96
+ *
97
+ * @param {Object} state - The Redux state.
98
+ * @private
99
+ * @returns {{
100
+ *     _conference: JitsiConference
101
+ * }}
102
+ */
103
+function _mapStateToProps(state) {
104
+    return {
105
+        _conference: state['features/base/conference'].conference
106
+    };
107
+}
108
+
109
+export default translate(connect(_mapStateToProps)(StopRecordingDialog));

+ 2
- 0
react/features/recording/components/Recording/index.js View File

1
+export { default as StartRecordingDialog } from './StartRecordingDialog';
2
+export { default as StopRecordingDialog } from './StopRecordingDialog';

+ 180
- 136
react/features/recording/components/RecordingLabel.web.js View File

1
-import PropTypes from 'prop-types';
1
+// @flow
2
+
2
 import React, { Component } from 'react';
3
 import React, { Component } from 'react';
3
-import { connect } from 'react-redux';
4
 
4
 
5
+import { CircularLabel } from '../../base/label';
5
 import { translate } from '../../base/i18n';
6
 import { translate } from '../../base/i18n';
6
-import { JitsiRecordingStatus } from '../../base/lib-jitsi-meet';
7
+import { JitsiRecordingConstants } from '../../base/lib-jitsi-meet';
8
+
9
+/**
10
+ * The translation keys to use when displaying messages. The values are set
11
+ * lazily to work around circular dependency issues with lib-jitsi-meet causing
12
+ * undefined imports.
13
+ *
14
+ * @private
15
+ * @type {Object}
16
+ */
17
+let TRANSLATION_KEYS_BY_MODE = null;
18
+
19
+/**
20
+ * Lazily initializes TRANSLATION_KEYS_BY_MODE with translation keys to be used
21
+ * by the {@code RecordingLabel} for messaging recording session state.
22
+ *
23
+ * @private
24
+ * @returns {Object}
25
+ */
26
+function _getTranslationKeysByMode() {
27
+    if (!TRANSLATION_KEYS_BY_MODE) {
28
+        const {
29
+            error: errorConstants,
30
+            mode: modeConstants,
31
+            status: statusConstants
32
+        } = JitsiRecordingConstants;
33
+
34
+        TRANSLATION_KEYS_BY_MODE = {
35
+            [modeConstants.FILE]: {
36
+                status: {
37
+                    [statusConstants.PENDING]: 'recording.pending',
38
+                    [statusConstants.OFF]: 'recording.off'
39
+                },
40
+                errors: {
41
+                    [errorConstants.BUSY]: 'recording.failedToStart',
42
+                    [errorConstants.ERROR]: 'recording.error'
43
+                }
44
+            },
45
+            [modeConstants.STREAM]: {
46
+                status: {
47
+                    [statusConstants.PENDING]: 'liveStreaming.pending',
48
+                    [statusConstants.OFF]: 'liveStreaming.off'
49
+                },
50
+                errors: {
51
+                    [errorConstants.BUSY]: 'liveStreaming.busy',
52
+                    [errorConstants.ERROR]: 'liveStreaming.error'
53
+                }
54
+            }
55
+        };
56
+    }
7
 
57
 
8
-import { RECORDING_TYPES } from '../constants';
58
+    return TRANSLATION_KEYS_BY_MODE;
59
+}
60
+
61
+/**
62
+ * The type of the React {@code Component} props of {@link RecordingLabel}.
63
+ */
64
+type Props = {
65
+
66
+    /**
67
+     * The redux representation of a recording session.
68
+     */
69
+    session: Object,
70
+
71
+    /**
72
+     * Invoked to obtain translated strings.
73
+     */
74
+    t: Function
75
+};
76
+
77
+/**
78
+ * The type of the React {@code Component} state of {@link RecordingLabel}.
79
+ */
80
+type State = {
81
+
82
+    /**
83
+     * Whether or not the {@link RecordingLabel} should be invisible.
84
+     */
85
+    hidden: boolean
86
+};
9
 
87
 
10
 /**
88
 /**
11
  * Implements a React {@link Component} which displays the current state of
89
  * Implements a React {@link Component} which displays the current state of
12
- * conference recording. Currently it uses CSS to display itself automatically
13
- * when there is a recording state update.
90
+ * conference recording.
14
  *
91
  *
15
  * @extends {Component}
92
  * @extends {Component}
16
  */
93
  */
17
-class RecordingLabel extends Component {
94
+class RecordingLabel extends Component<Props, State> {
95
+    _autohideTimeout: number;
96
+
97
+    state = {
98
+        hidden: false
99
+    };
100
+
101
+    static defaultProps = {
102
+        session: {}
103
+    };
104
+
18
     /**
105
     /**
19
-     * {@code RecordingLabel} component's property types.
106
+     * Sets a timeout to automatically hide the {@link RecordingLabel} if the
107
+     * recording session started as failed.
20
      *
108
      *
21
-     * @static
109
+     * @inheritdoc
22
      */
110
      */
23
-    static propTypes = {
24
-        /**
25
-         * Whether or not the filmstrip is currently visible or toggled to
26
-         * hidden. Depending on the filmstrip state, different CSS classes will
27
-         * be set to allow for adjusting of {@code RecordingLabel} positioning.
28
-         */
29
-        _filmstripVisible: PropTypes.bool,
30
-
31
-        /**
32
-         * Whether or not the conference is currently being recorded.
33
-         */
34
-        _isRecording: PropTypes.bool,
35
-
36
-        /**
37
-         * An object to describe the {@code RecordingLabel} content. If no
38
-         * translation key to display is specified, the label will apply CSS to
39
-         * itself so it can be made invisible.
40
-         * {{
41
-         *     centered: boolean,
42
-         *     key: string,
43
-         *     showSpinner: boolean
44
-         * }}
45
-         */
46
-        _labelDisplayConfiguration: PropTypes.object,
47
-
48
-        /**
49
-         * Whether the recording feature is live streaming (jibri) or is file
50
-         * recording (jirecon).
51
-         */
52
-        _recordingType: PropTypes.string,
53
-
54
-        /**
55
-         * Invoked to obtain translated string.
56
-         */
57
-        t: PropTypes.func
58
-    };
111
+    componentDidMount() {
112
+        if (this.props.session.status === JitsiRecordingConstants.status.OFF) {
113
+            this._setHideTimeout();
114
+        }
115
+    }
59
 
116
 
60
     /**
117
     /**
61
-     * Initializes a new {@code RecordingLabel} instance.
118
+     * Sets a timeout to automatically hide {the @link RecordingLabel} if it has
119
+     * transitioned to off.
62
      *
120
      *
63
-     * @param {Object} props - The read-only properties with which the new
64
-     * instance is to be initialized.
121
+     * @inheritdoc
65
      */
122
      */
66
-    constructor(props) {
67
-        super(props);
68
-
69
-        this.state = {
70
-            /**
71
-             * Whether or not the filmstrip was not visible but has transitioned
72
-             * in the latest component update to visible. This boolean is used
73
-             * to set a class for position animations.
74
-             *
75
-             * @type {boolean}
76
-             */
77
-            filmstripBecomingVisible: false
78
-        };
123
+    componentWillReceiveProps(nextProps) {
124
+        const { status } = this.props.session;
125
+        const nextStatus = nextProps.session.status;
126
+
127
+        if (status !== JitsiRecordingConstants.status.OFF
128
+            && nextStatus === JitsiRecordingConstants.status.OFF) {
129
+            this._setHideTimeout();
130
+        }
79
     }
131
     }
80
 
132
 
81
     /**
133
     /**
82
-     * Updates the state for whether or not the filmstrip is being toggled to
83
-     * display after having being hidden.
134
+     * Clears the timeout for automatically hiding the {@link RecordingLabel}.
84
      *
135
      *
85
      * @inheritdoc
136
      * @inheritdoc
86
-     * @param {Object} nextProps - The read-only props which this Component will
87
-     * receive.
88
-     * @returns {void}
89
      */
137
      */
90
-    componentWillReceiveProps(nextProps) {
91
-        this.setState({
92
-            filmstripBecomingVisible: nextProps._filmstripVisible
93
-                && !this.props._filmstripVisible
94
-        });
138
+    componentWillUnmount() {
139
+        this._clearAutoHideTimeout();
95
     }
140
     }
96
 
141
 
97
     /**
142
     /**
101
      * @returns {ReactElement}
146
      * @returns {ReactElement}
102
      */
147
      */
103
     render() {
148
     render() {
149
+        if (this.state.hidden) {
150
+            return null;
151
+        }
152
+
104
         const {
153
         const {
105
-            _isRecording,
106
-            _labelDisplayConfiguration,
107
-            _recordingType
108
-        } = this.props;
109
-        const { centered, key, showSpinner } = _labelDisplayConfiguration || {};
110
-
111
-        const isVisible = Boolean(key);
112
-        const rootClassName = [
113
-            'video-state-indicator centeredVideoLabel',
114
-            _isRecording ? 'is-recording' : '',
115
-            isVisible ? 'show-inline' : '',
116
-            centered ? '' : 'moveToCorner',
117
-            this.state.filmstripBecomingVisible ? 'opening' : '',
118
-            this.props._filmstripVisible
119
-                ? 'with-filmstrip' : 'without-filmstrip'
120
-        ].join(' ');
154
+            error: errorConstants,
155
+            mode: modeConstants,
156
+            status: statusConstants
157
+        } = JitsiRecordingConstants;
158
+        const { session } = this.props;
159
+        const allTranslationKeys = _getTranslationKeysByMode();
160
+        const translationKeys = allTranslationKeys[session.mode];
161
+        let circularLabelClass, circularLabelKey, messageKey;
162
+
163
+        switch (session.status) {
164
+        case statusConstants.OFF: {
165
+            if (session.error) {
166
+                messageKey = translationKeys.errors[session.error]
167
+                    || translationKeys.errors[errorConstants.ERROR];
168
+            } else {
169
+                messageKey = translationKeys.status[statusConstants.OFF];
170
+            }
171
+            break;
172
+        }
173
+        case statusConstants.ON:
174
+            circularLabelClass = session.mode;
175
+            circularLabelKey = session.mode === modeConstants.STREAM
176
+                ? 'recording.live' : 'recording.rec';
177
+            break;
178
+        case statusConstants.PENDING:
179
+            messageKey = translationKeys.status[statusConstants.PENDING];
180
+            break;
181
+        }
182
+
183
+        const className = `recording-label ${
184
+            messageKey ? 'center-message' : ''}`;
121
 
185
 
122
         return (
186
         return (
123
-            <div
124
-                className = { rootClassName }
125
-                id = 'recordingLabel'>
126
-                { _isRecording
127
-                    ? <div className = 'recording-icon'>
128
-                        <div className = 'recording-icon-background' />
129
-                        <i
130
-                            className = {
131
-                                _recordingType === RECORDING_TYPES.JIBRI
132
-                                    ? 'icon-live'
133
-                                    : 'icon-rec' } />
187
+            <div className = { className }>
188
+                { messageKey
189
+                    ? <div>
190
+                        { this.props.t(messageKey) }
134
                     </div>
191
                     </div>
135
-                    : <div id = 'recordingLabelText'>
136
-                        { this.props.t(key) }
137
-                    </div> }
138
-                { !_isRecording
139
-                    && showSpinner
140
-                    && <img
141
-                        className = 'recordingSpinner'
142
-                        id = 'recordingSpinner'
143
-                        src = 'images/spin.svg' /> }
192
+                    : <CircularLabel className = { circularLabelClass }>
193
+                        { this.props.t(circularLabelKey) }
194
+                    </CircularLabel> }
144
             </div>
195
             </div>
145
         );
196
         );
146
     }
197
     }
147
-}
148
 
198
 
149
-/**
150
- * Maps (parts of) the Redux state to the associated {@code RecordingLabel}
151
- * component's props.
152
- *
153
- * @param {Object} state - The Redux state.
154
- * @private
155
- * @returns {{
156
- *     _filmstripVisible: boolean,
157
- *     _isRecording: boolean,
158
- *     _labelDisplayConfiguration: Object,
159
- *     _recordingType: string
160
- * }}
161
- */
162
-function _mapStateToProps(state) {
163
-    const { visible } = state['features/filmstrip'];
164
-    const {
165
-        labelDisplayConfiguration,
166
-        recordingState,
167
-        recordingType
168
-    } = state['features/recording'];
169
-
170
-    return {
171
-        _filmstripVisible: visible,
172
-        _isRecording: recordingState === JitsiRecordingStatus.ON,
173
-        _labelDisplayConfiguration: labelDisplayConfiguration,
174
-        _recordingType: recordingType
175
-    };
199
+    /**
200
+     * Clears the timeout for automatically hiding {@link RecordingLabel}.
201
+     *
202
+     * @private
203
+     * @returns {void}
204
+     */
205
+    _clearAutoHideTimeout() {
206
+        clearTimeout(this._autohideTimeout);
207
+    }
208
+
209
+    /**
210
+     * Sets a timeout to automatically hide {@link RecordingLabel}.
211
+     *
212
+     * @private
213
+     * @returns {void}
214
+     */
215
+    _setHideTimeout() {
216
+        this._autohideTimeout = setTimeout(() => {
217
+            this.setState({ hidden: true });
218
+        }, 5000);
219
+    }
176
 }
220
 }
177
 
221
 
178
-export default translate(connect(_mapStateToProps)(RecordingLabel));
222
+export default translate(RecordingLabel);

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

1
 export { StartLiveStreamDialog, StopLiveStreamDialog } from './LiveStream';
1
 export { StartLiveStreamDialog, StopLiveStreamDialog } from './LiveStream';
2
+export { StartRecordingDialog, StopRecordingDialog } from './Recording';
2
 export { default as RecordingLabel } from './RecordingLabel';
3
 export { default as RecordingLabel } from './RecordingLabel';

+ 18
- 0
react/features/recording/functions.js View File

1
+import { JitsiRecordingConstants } from '../base/lib-jitsi-meet';
2
+
3
+/**
4
+ * Searches in the passed in redux state for an active recording session of the
5
+ * passed in mode.
6
+ *
7
+ * @param {Object} state - The redux state to search in.
8
+ * @param {string} mode - Find an active recording session of the given mode.
9
+ * @returns {Object|undefined}
10
+ */
11
+export function getActiveSession(state, mode) {
12
+    const { sessionDatas } = state['features/recording'];
13
+    const { status: statusConstants } = JitsiRecordingConstants;
14
+
15
+    return sessionDatas.find(sessionData => sessionData.mode === mode
16
+        && (sessionData.status === statusConstants.ON
17
+            || sessionData.status === statusConstants.PENDING));
18
+}

+ 1
- 1
react/features/recording/index.js View File

1
 export * from './actions';
1
 export * from './actions';
2
 export * from './components';
2
 export * from './components';
3
 export * from './constants';
3
 export * from './constants';
4
+export * from './functions';
4
 
5
 
5
-import './middleware';
6
 import './reducer';
6
 import './reducer';

+ 0
- 27
react/features/recording/middleware.js View File

1
-// @flow
2
-
3
-import { MiddlewareRegistry } from '../base/redux';
4
-import UIEvents from '../../../service/UI/UIEvents';
5
-
6
-import { TOGGLE_RECORDING } from './actionTypes';
7
-
8
-declare var APP: Object;
9
-
10
-/**
11
- * Implements the middleware of the feature recording.
12
- *
13
- * @param {Store} store - The redux store.
14
- * @returns {Function}
15
- */
16
-// eslint-disable-next-line no-unused-vars
17
-MiddlewareRegistry.register(store => next => action => {
18
-    switch (action.type) {
19
-    case TOGGLE_RECORDING:
20
-        if (typeof APP === 'object') {
21
-            APP.UI.emitEvent(UIEvents.TOGGLE_RECORDING);
22
-        }
23
-        break;
24
-    }
25
-
26
-    return next(action);
27
-});

+ 51
- 25
react/features/recording/reducer.js View File

1
 import { ReducerRegistry } from '../base/redux';
1
 import { ReducerRegistry } from '../base/redux';
2
-import {
3
-    HIDE_RECORDING_LABEL,
4
-    RECORDING_STATE_UPDATED,
5
-    SET_RECORDING_TYPE
6
-} from './actionTypes';
2
+import { RECORDING_SESSION_UPDATED } from './actionTypes';
3
+
4
+const DEFAULT_STATE = {
5
+    sessionDatas: []
6
+};
7
 
7
 
8
 /**
8
 /**
9
  * Reduces the Redux actions of the feature features/recording.
9
  * Reduces the Redux actions of the feature features/recording.
10
  */
10
  */
11
-ReducerRegistry.register('features/recording', (state = {}, action) => {
12
-    switch (action.type) {
13
-    case HIDE_RECORDING_LABEL:
14
-        return {
15
-            ...state,
16
-            labelDisplayConfiguration: null
17
-        };
11
+ReducerRegistry.register('features/recording',
12
+    (state = DEFAULT_STATE, action) => {
13
+        switch (action.type) {
14
+        case RECORDING_SESSION_UPDATED:
15
+            return {
16
+                ...state,
17
+                sessionDatas:
18
+                    _updateSessionDatas(state.sessionDatas, action.sessionData)
19
+            };
18
 
20
 
19
-    case RECORDING_STATE_UPDATED:
20
-        return {
21
-            ...state,
22
-            ...action.recordingState
23
-        };
21
+        default:
22
+            return state;
23
+        }
24
+    });
24
 
25
 
25
-    case SET_RECORDING_TYPE:
26
-        return {
27
-            ...state,
28
-            recordingType: action.recordingType
29
-        };
26
+/**
27
+ * Updates the known information on recording sessions.
28
+ *
29
+ * @param {Array} sessionDatas - The current sessions in the redux store.
30
+ * @param {Object} newSessionData - The updated session data.
31
+ * @private
32
+ * @returns {Array} The session datas with the updated session data added.
33
+ */
34
+function _updateSessionDatas(sessionDatas, newSessionData) {
35
+    const hasExistingSessionData = sessionDatas.find(
36
+        sessionData => sessionData.id === newSessionData.id);
37
+    let newSessionDatas;
30
 
38
 
31
-    default:
32
-        return state;
39
+    if (hasExistingSessionData) {
40
+        newSessionDatas = sessionDatas.map(sessionData => {
41
+            if (sessionData.id === newSessionData.id) {
42
+                return {
43
+                    ...newSessionData
44
+                };
45
+            }
46
+
47
+            // Nothing to update for this session data so pass it back in.
48
+            return sessionData;
49
+        });
50
+    } else {
51
+        // If the session data is not present, then there is nothing to update
52
+        // and instead it needs to be added to the known session datas.
53
+        newSessionDatas = [
54
+            ...sessionDatas,
55
+            { ...newSessionData }
56
+        ];
33
     }
57
     }
34
-});
58
+
59
+    return newSessionDatas;
60
+}

+ 107
- 46
react/features/toolbox/components/web/Toolbox.js View File

11
 } from '../../../analytics';
11
 } from '../../../analytics';
12
 import { openDialog } from '../../../base/dialog';
12
 import { openDialog } from '../../../base/dialog';
13
 import { translate } from '../../../base/i18n';
13
 import { translate } from '../../../base/i18n';
14
+import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
14
 import {
15
 import {
15
     PARTICIPANT_ROLE,
16
     PARTICIPANT_ROLE,
16
     getLocalParticipant,
17
     getLocalParticipant,
27
     isDialOutEnabled
28
     isDialOutEnabled
28
 } from '../../../invite';
29
 } from '../../../invite';
29
 import { openKeyboardShortcutsDialog } from '../../../keyboard-shortcuts';
30
 import { openKeyboardShortcutsDialog } from '../../../keyboard-shortcuts';
30
-import { RECORDING_TYPES, toggleRecording } from '../../../recording';
31
+import {
32
+    StartLiveStreamDialog,
33
+    StartRecordingDialog,
34
+    StopLiveStreamDialog,
35
+    StopRecordingDialog,
36
+    getActiveSession
37
+} from '../../../recording';
31
 import { SettingsButton } from '../../../settings';
38
 import { SettingsButton } from '../../../settings';
32
 import { toggleSharedVideo } from '../../../shared-video';
39
 import { toggleSharedVideo } from '../../../shared-video';
33
 import { toggleChat, toggleProfile } from '../../../side-panel';
40
 import { toggleChat, toggleProfile } from '../../../side-panel';
95
      */
102
      */
96
     _feedbackConfigured: boolean,
103
     _feedbackConfigured: boolean,
97
 
104
 
105
+    /**
106
+     * The current file recording session, if any.
107
+     */
108
+    _fileRecordingSession: Object,
109
+
98
     /**
110
     /**
99
      * Whether or not the app is currently in full screen.
111
      * Whether or not the app is currently in full screen.
100
      */
112
      */
112
     _isGuest: boolean,
124
     _isGuest: boolean,
113
 
125
 
114
     /**
126
     /**
115
-     * Whether or not the conference is currently being recorded by the local
116
-     * participant.
127
+     * The current live streaming session, if any.
117
      */
128
      */
118
-    _isRecording: boolean,
129
+    _liveStreamingSession: ?Object,
119
 
130
 
120
     /**
131
     /**
121
      * The ID of the local participant.
132
      * The ID of the local participant.
137
      */
148
      */
138
     _recordingEnabled: boolean,
149
     _recordingEnabled: boolean,
139
 
150
 
140
-    /**
141
-     * Whether the recording feature is live streaming (jibri) or is file
142
-     * recording (jirecon).
143
-     */
144
-    _recordingType: String,
145
-
146
     /**
151
     /**
147
      * Whether or not the local participant is screensharing.
152
      * Whether or not the local participant is screensharing.
148
      */
153
      */
214
             = this._onToolbarOpenSpeakerStats.bind(this);
219
             = this._onToolbarOpenSpeakerStats.bind(this);
215
         this._onToolbarOpenVideoQuality
220
         this._onToolbarOpenVideoQuality
216
             = this._onToolbarOpenVideoQuality.bind(this);
221
             = this._onToolbarOpenVideoQuality.bind(this);
217
-
218
         this._onToolbarToggleChat = this._onToolbarToggleChat.bind(this);
222
         this._onToolbarToggleChat = this._onToolbarToggleChat.bind(this);
219
         this._onToolbarToggleEtherpad
223
         this._onToolbarToggleEtherpad
220
             = this._onToolbarToggleEtherpad.bind(this);
224
             = this._onToolbarToggleEtherpad.bind(this);
221
         this._onToolbarToggleFullScreen
225
         this._onToolbarToggleFullScreen
222
             = this._onToolbarToggleFullScreen.bind(this);
226
             = this._onToolbarToggleFullScreen.bind(this);
227
+        this._onToolbarToggleLiveStreaming
228
+            = this._onToolbarToggleLiveStreaming.bind(this);
223
         this._onToolbarToggleProfile
229
         this._onToolbarToggleProfile
224
             = this._onToolbarToggleProfile.bind(this);
230
             = this._onToolbarToggleProfile.bind(this);
225
         this._onToolbarToggleRaiseHand
231
         this._onToolbarToggleRaiseHand
462
         this.props.dispatch(setFullScreen(fullScreen));
468
         this.props.dispatch(setFullScreen(fullScreen));
463
     }
469
     }
464
 
470
 
471
+    /**
472
+     * Dispatches an action to show a dialog for starting or stopping a live
473
+     * streaming session.
474
+     *
475
+     * @private
476
+     * @returns {void}
477
+     */
478
+    _doToggleLiveStreaming() {
479
+        const { _liveStreamingSession } = this.props;
480
+        const dialogToDisplay = _liveStreamingSession
481
+            ? StopLiveStreamDialog : StartLiveStreamDialog;
482
+
483
+        this.props.dispatch(
484
+            openDialog(dialogToDisplay, { session: _liveStreamingSession }));
485
+    }
486
+
465
     /**
487
     /**
466
      * Dispatches an action to show or hide the profile edit panel.
488
      * Dispatches an action to show or hide the profile edit panel.
467
      *
489
      *
495
      * @returns {void}
517
      * @returns {void}
496
      */
518
      */
497
     _doToggleRecording() {
519
     _doToggleRecording() {
498
-        this.props.dispatch(toggleRecording());
520
+        const { _fileRecordingSession } = this.props;
521
+        const dialog = _fileRecordingSession
522
+            ? StopRecordingDialog : StartRecordingDialog;
523
+
524
+        this.props.dispatch(
525
+            openDialog(dialog, { session: _fileRecordingSession }));
499
     }
526
     }
500
 
527
 
501
     /**
528
     /**
764
         this._doToggleFullScreen();
791
         this._doToggleFullScreen();
765
     }
792
     }
766
 
793
 
794
+    _onToolbarToggleLiveStreaming: () => void;
795
+
796
+    /**
797
+     * Starts the process for enabling or disabling live streaming.
798
+     *
799
+     * @private
800
+     * @returns {void}
801
+     */
802
+    _onToolbarToggleLiveStreaming() {
803
+        sendAnalytics(createToolbarEvent(
804
+            'livestreaming.button',
805
+            {
806
+                'is_streaming': Boolean(this.props._liveStreamingSession),
807
+                type: JitsiRecordingConstants.mode.STREAM
808
+            }));
809
+
810
+        this._doToggleLiveStreaming();
811
+    }
812
+
767
     _onToolbarToggleProfile: () => void;
813
     _onToolbarToggleProfile: () => void;
768
 
814
 
769
     /**
815
     /**
805
      * @returns {void}
851
      * @returns {void}
806
      */
852
      */
807
     _onToolbarToggleRecording() {
853
     _onToolbarToggleRecording() {
808
-        // No analytics handling is added here for the click as this action will
809
-        // exercise the old toolbar UI flow, which includes analytics handling.
854
+        sendAnalytics(createToolbarEvent(
855
+            'recording.button',
856
+            {
857
+                'is_recording': Boolean(this.props._fileRecordingSession),
858
+                type: JitsiRecordingConstants.mode.FILE
859
+            }));
810
 
860
 
811
         this._doToggleRecording();
861
         this._doToggleRecording();
812
     }
862
     }
891
         );
941
         );
892
     }
942
     }
893
 
943
 
944
+    /**
945
+     * Renders an {@code OverflowMenuItem} for starting or stopping a live
946
+     * streaming of the current conference.
947
+     *
948
+     * @private
949
+     * @returns {ReactElement}
950
+     */
951
+    _renderLiveStreamingButton() {
952
+        const { _liveStreamingSession, t } = this.props;
953
+
954
+        const translationKey = _liveStreamingSession
955
+            ? 'dialog.stopLiveStreaming'
956
+            : 'dialog.startLiveStreaming';
957
+
958
+        return (
959
+            <OverflowMenuItem
960
+                accessibilityLabel = 'Live stream'
961
+                icon = 'icon-public'
962
+                key = 'liveStreaming'
963
+                onClick = { this._onToolbarToggleLiveStreaming }
964
+                text = { t(translationKey) } />
965
+        );
966
+    }
967
+
894
     /**
968
     /**
895
      * Renders the list elements of the overflow menu.
969
      * Renders the list elements of the overflow menu.
896
      *
970
      *
904
             _feedbackConfigured,
978
             _feedbackConfigured,
905
             _fullScreen,
979
             _fullScreen,
906
             _isGuest,
980
             _isGuest,
981
+            _recordingEnabled,
907
             _sharingVideo,
982
             _sharingVideo,
908
             t
983
             t
909
         } = this.props;
984
         } = this.props;
929
                     text = { _fullScreen
1004
                     text = { _fullScreen
930
                         ? t('toolbar.exitFullScreen')
1005
                         ? t('toolbar.exitFullScreen')
931
                         : t('toolbar.enterFullScreen') } />,
1006
                         : t('toolbar.enterFullScreen') } />,
932
-            this._renderRecordingButton(),
1007
+            _recordingEnabled
1008
+                && this._shouldShowButton('livestreaming')
1009
+                && this._renderLiveStreamingButton(),
1010
+            _recordingEnabled
1011
+                && this._shouldShowButton('recording')
1012
+                && this._renderRecordingButton(),
933
             this._shouldShowButton('sharedvideo')
1013
             this._shouldShowButton('sharedvideo')
934
                 && <OverflowMenuItem
1014
                 && <OverflowMenuItem
935
                     accessibilityLabel = 'Shared video'
1015
                     accessibilityLabel = 'Shared video'
979
     }
1059
     }
980
 
1060
 
981
     /**
1061
     /**
982
-     * Renders an {@code OverflowMenuItem} depending on the current recording
983
-     * state.
1062
+     * Renders an {@code OverflowMenuItem} to start or stop recording of the
1063
+     * current conference.
984
      *
1064
      *
985
      * @private
1065
      * @private
986
      * @returns {ReactElement|null}
1066
      * @returns {ReactElement|null}
987
      */
1067
      */
988
     _renderRecordingButton() {
1068
     _renderRecordingButton() {
989
-        const {
990
-            _isRecording,
991
-            _recordingEnabled,
992
-            _recordingType,
993
-            t
994
-        } = this.props;
995
-
996
-        if (!_recordingEnabled || !this._shouldShowButton('recording')) {
997
-            return null;
998
-        }
1069
+        const { _fileRecordingSession, t } = this.props;
999
 
1070
 
1000
-        let iconClass, translationKey;
1001
-
1002
-        if (_recordingType === RECORDING_TYPES.JIBRI) {
1003
-            iconClass = 'icon-public';
1004
-            translationKey = _isRecording
1005
-                ? 'dialog.stopLiveStreaming'
1006
-                : 'dialog.startLiveStreaming';
1007
-        } else {
1008
-            iconClass = 'icon-camera-take-picture';
1009
-            translationKey = _isRecording
1010
-                ? 'dialog.stopRecording'
1011
-                : 'dialog.startRecording';
1012
-        }
1071
+        const translationKey = _fileRecordingSession
1072
+            ? 'dialog.stopRecording'
1073
+            : 'dialog.startRecording';
1013
 
1074
 
1014
         return (
1075
         return (
1015
             <OverflowMenuItem
1076
             <OverflowMenuItem
1016
                 accessibilityLabel = 'Record'
1077
                 accessibilityLabel = 'Record'
1017
-                icon = { iconClass }
1078
+                icon = 'icon-camera-take-picture'
1018
                 key = 'recording'
1079
                 key = 'recording'
1019
                 onClick = { this._onToolbarToggleRecording }
1080
                 onClick = { this._onToolbarToggleRecording }
1020
                 text = { t(translationKey) } />
1081
                 text = { t(translationKey) } />
1055
         enableRecording,
1116
         enableRecording,
1056
         iAmRecorder
1117
         iAmRecorder
1057
     } = state['features/base/config'];
1118
     } = state['features/base/config'];
1058
-    const { isRecording, recordingType } = state['features/recording'];
1059
     const sharedVideoStatus = state['features/shared-video'].status;
1119
     const sharedVideoStatus = state['features/shared-video'].status;
1060
     const { current } = state['features/side-panel'];
1120
     const { current } = state['features/side-panel'];
1061
     const {
1121
     const {
1083
         _hideInviteButton:
1143
         _hideInviteButton:
1084
             iAmRecorder || (!addPeopleEnabled && !dialOutEnabled),
1144
             iAmRecorder || (!addPeopleEnabled && !dialOutEnabled),
1085
         _isGuest: state['features/base/jwt'].isGuest,
1145
         _isGuest: state['features/base/jwt'].isGuest,
1086
-        _isRecording: isRecording,
1146
+        _fileRecordingSession:
1147
+            getActiveSession(state, JitsiRecordingConstants.mode.FILE),
1087
         _fullScreen: fullScreen,
1148
         _fullScreen: fullScreen,
1149
+        _liveStreamingSession:
1150
+             getActiveSession(state, JitsiRecordingConstants.mode.STREAM),
1088
         _localParticipantID: localParticipant.id,
1151
         _localParticipantID: localParticipant.id,
1089
         _overflowMenuVisible: overflowMenuVisible,
1152
         _overflowMenuVisible: overflowMenuVisible,
1090
         _raisedHand: localParticipant.raisedHand,
1153
         _raisedHand: localParticipant.raisedHand,
1091
-        _recordingEnabled: isModerator && enableRecording
1092
-            && (conference && conference.isRecordingSupported()),
1093
-        _recordingType: recordingType,
1154
+        _recordingEnabled: isModerator && enableRecording,
1094
         _screensharing: localVideo && localVideo.videoType === 'desktop',
1155
         _screensharing: localVideo && localVideo.videoType === 'desktop',
1095
         _sharingVideo: sharedVideoStatus === 'playing'
1156
         _sharingVideo: sharedVideoStatus === 'playing'
1096
             || sharedVideoStatus === 'start'
1157
             || sharedVideoStatus === 'start'

+ 1
- 2
react/features/toolbox/reducer.js View File

49
         alwaysVisible: false,
49
         alwaysVisible: false,
50
 
50
 
51
         /**
51
         /**
52
-         * The indicator which determines whether the Toolbox is enabled. For
53
-         * example, modules/UI/recording/Recording.js disables the Toolbox.
52
+         * The indicator which determines whether the Toolbox is enabled.
54
          *
53
          *
55
          * @type {boolean}
54
          * @type {boolean}
56
          */
55
          */

+ 62
- 122
react/features/video-quality/components/VideoQualityLabel.web.js View File

4
 import { connect } from 'react-redux';
4
 import { connect } from 'react-redux';
5
 
5
 
6
 import { translate } from '../../base/i18n';
6
 import { translate } from '../../base/i18n';
7
+import { CircularLabel } from '../../base/label';
7
 import { MEDIA_TYPE } from '../../base/media';
8
 import { MEDIA_TYPE } from '../../base/media';
8
 import { getTrackByMediaTypeAndParticipant } from '../../base/tracks';
9
 import { getTrackByMediaTypeAndParticipant } from '../../base/tracks';
9
 
10
 
49
         _audioOnly: PropTypes.bool,
50
         _audioOnly: PropTypes.bool,
50
 
51
 
51
         /**
52
         /**
52
-         * Whether or not a connection to a conference has been established.
53
+         * The message to show within the label.
53
          */
54
          */
54
-        _conferenceStarted: PropTypes.bool,
55
+        _labelKey: PropTypes.string,
55
 
56
 
56
         /**
57
         /**
57
-         * Whether or not the filmstrip is displayed with remote videos. Used to
58
-         * determine display classes to set.
58
+         * The message to show within the label's tooltip.
59
          */
59
          */
60
-        _filmstripVisible: PropTypes.bool,
61
-
62
-        /**
63
-         * The current video resolution (height) to display a label for.
64
-         */
65
-        _resolution: PropTypes.number,
60
+        _tooltipKey: PropTypes.string,
66
 
61
 
67
         /**
62
         /**
68
          * The redux representation of the JitsiTrack displayed on large video.
63
          * The redux representation of the JitsiTrack displayed on large video.
75
         t: PropTypes.func
70
         t: PropTypes.func
76
     };
71
     };
77
 
72
 
78
-    /**
79
-     * Initializes a new {@code VideoQualityLabel} instance.
80
-     *
81
-     * @param {Object} props - The read-only React Component props with which
82
-     * the new instance is to be initialized.
83
-     */
84
-    constructor(props) {
85
-        super(props);
86
-
87
-        this.state = {
88
-            /**
89
-             * Whether or not the filmstrip is transitioning from not visible
90
-             * to visible. Used to set a transition class for animation.
91
-             *
92
-             * @type {boolean}
93
-             */
94
-            togglingToVisible: false
95
-        };
96
-    }
97
-
98
-    /**
99
-     * Updates the state for whether or not the filmstrip is being toggled to
100
-     * display after having being hidden.
101
-     *
102
-     * @inheritdoc
103
-     * @param {Object} nextProps - The read-only props which this Component will
104
-     * receive.
105
-     * @returns {void}
106
-     */
107
-    componentWillReceiveProps(nextProps) {
108
-        this.setState({
109
-            togglingToVisible: nextProps._filmstripVisible
110
-                && !this.props._filmstripVisible
111
-        });
112
-    }
113
-
114
     /**
73
     /**
115
      * Implements React's {@link Component#render()}.
74
      * Implements React's {@link Component#render()}.
116
      *
75
      *
120
     render() {
79
     render() {
121
         const {
80
         const {
122
             _audioOnly,
81
             _audioOnly,
123
-            _conferenceStarted,
124
-            _filmstripVisible,
125
-            _resolution,
82
+            _labelKey,
83
+            _tooltipKey,
126
             _videoTrack,
84
             _videoTrack,
127
             t
85
             t
128
         } = this.props;
86
         } = this.props;
129
 
87
 
130
-        // FIXME The _conferenceStarted check is used to be defensive against
131
-        // toggling audio only mode while there is no conference and hides the
132
-        // need for error handling around audio only mode toggling.
133
-        if (!_conferenceStarted) {
134
-            return null;
135
-        }
136
-
137
-        // Determine which classes should be set on the component. These classes
138
-        // will used to help with animations and setting position.
139
-        const baseClasses = 'video-state-indicator moveToCorner';
140
-        const filmstrip
141
-            = _filmstripVisible ? 'with-filmstrip' : 'without-filmstrip';
142
-        const opening = this.state.togglingToVisible ? 'opening' : '';
143
-        const classNames
144
-            = `${baseClasses} ${filmstrip} ${opening}`;
145
 
88
 
146
-        let labelContent;
147
-        let tooltipKey;
89
+        let className, labelContent, tooltipKey;
148
 
90
 
149
         if (_audioOnly) {
91
         if (_audioOnly) {
150
-            labelContent = <i className = 'icon-visibility-off' />;
92
+            className = 'audio-only';
93
+            labelContent = t('videoStatus.audioOnly');
151
             tooltipKey = 'videoStatus.labelTooltipAudioOnly';
94
             tooltipKey = 'videoStatus.labelTooltipAudioOnly';
152
         } else if (!_videoTrack || _videoTrack.muted) {
95
         } else if (!_videoTrack || _videoTrack.muted) {
153
-            labelContent = <i className = 'icon-visibility-off' />;
96
+            className = 'no-video';
97
+            labelContent = t('videoStatus.audioOnly');
154
             tooltipKey = 'videoStatus.labelTooiltipNoVideo';
98
             tooltipKey = 'videoStatus.labelTooiltipNoVideo';
155
         } else {
99
         } else {
156
-            const translationKeys
157
-                = this._mapResolutionToTranslationsKeys(_resolution);
158
-
159
-            labelContent = t(translationKeys.labelKey);
160
-            tooltipKey = translationKeys.tooltipKey;
100
+            className = 'current-video-quality';
101
+            labelContent = t(_labelKey);
102
+            tooltipKey = _tooltipKey;
161
         }
103
         }
162
 
104
 
163
 
105
 
164
         return (
106
         return (
165
-            <div
166
-                className = { classNames }
167
-                id = 'videoResolutionLabel'>
168
-                <Tooltip
169
-                    content = { t(tooltipKey) }
170
-                    position = { 'left' }>
171
-                    <div className = 'video-quality-label-status'>
172
-                        { labelContent }
173
-                    </div>
174
-                </Tooltip>
175
-            </div>
107
+            <Tooltip
108
+                content = { t(tooltipKey) }
109
+                position = { 'left' }>
110
+                <CircularLabel
111
+                    className = { className }
112
+                    id = 'videoResolutionLabel'>
113
+                    { labelContent }
114
+                </CircularLabel>
115
+            </Tooltip>
176
         );
116
         );
177
     }
117
     }
118
+}
178
 
119
 
179
-    /**
180
-     * Matches the passed in resolution with a translation keys for describing
181
-     * the resolution. The passed in resolution will be matched with a known
182
-     * resolution that it is at least greater than or equal to.
183
-     *
184
-     * @param {number} resolution - The video height to match with a
185
-     * translation.
186
-     * @private
187
-     * @returns {Object}
188
-     */
189
-    _mapResolutionToTranslationsKeys(resolution) {
190
-        // Set the default matching resolution of the lowest just in case a
191
-        // match is not found.
192
-        let highestMatchingResolution = RESOLUTIONS[0];
193
-
194
-        for (let i = 0; i < RESOLUTIONS.length; i++) {
195
-            const knownResolution = RESOLUTIONS[i];
196
-
197
-            if (resolution >= knownResolution) {
198
-                highestMatchingResolution = knownResolution;
199
-            } else {
200
-                break;
201
-            }
202
-        }
120
+/**
121
+ * Matches the passed in resolution with a translation keys for describing
122
+ * the resolution. The passed in resolution will be matched with a known
123
+ * resolution that it is at least greater than or equal to.
124
+ *
125
+ * @param {number} resolution - The video height to match with a
126
+ * translation.
127
+ * @private
128
+ * @returns {Object}
129
+ */
130
+function _mapResolutionToTranslationsKeys(resolution) {
131
+    // Set the default matching resolution of the lowest just in case a match is
132
+    // not found.
133
+    let highestMatchingResolution = RESOLUTIONS[0];
203
 
134
 
204
-        const labelKey
205
-            = RESOLUTION_TO_TRANSLATION_KEY[highestMatchingResolution];
135
+    for (let i = 0; i < RESOLUTIONS.length; i++) {
136
+        const knownResolution = RESOLUTIONS[i];
206
 
137
 
207
-        return {
208
-            labelKey,
209
-            tooltipKey: `${labelKey}Tooltip`
210
-        };
138
+        if (resolution >= knownResolution) {
139
+            highestMatchingResolution = knownResolution;
140
+        } else {
141
+            break;
142
+        }
211
     }
143
     }
144
+
145
+    const labelKey
146
+        = RESOLUTION_TO_TRANSLATION_KEY[highestMatchingResolution];
147
+
148
+    return {
149
+        labelKey,
150
+        tooltipKey: `${labelKey}Tooltip`
151
+    };
212
 }
152
 }
213
 
153
 
214
 /**
154
 /**
219
  * @private
159
  * @private
220
  * @returns {{
160
  * @returns {{
221
  *     _audioOnly: boolean,
161
  *     _audioOnly: boolean,
222
- *     _conferenceStarted: boolean,
223
- *     _filmstripVisible: true,
224
- *     _resolution: number,
162
+ *     _labelKey: string,
163
+ *     _tooltipKey: string,
225
  *     _videoTrack: Object
164
  *     _videoTrack: Object
226
  * }}
165
  * }}
227
  */
166
  */
228
 function _mapStateToProps(state) {
167
 function _mapStateToProps(state) {
229
-    const { audioOnly, conference } = state['features/base/conference'];
230
-    const { visible } = state['features/filmstrip'];
168
+    const { audioOnly } = state['features/base/conference'];
231
     const { resolution, participantId } = state['features/large-video'];
169
     const { resolution, participantId } = state['features/large-video'];
232
     const videoTrackOnLargeVideo = getTrackByMediaTypeAndParticipant(
170
     const videoTrackOnLargeVideo = getTrackByMediaTypeAndParticipant(
233
         state['features/base/tracks'],
171
         state['features/base/tracks'],
235
         participantId
173
         participantId
236
     );
174
     );
237
 
175
 
176
+    const translationKeys
177
+        = audioOnly ? {} : _mapResolutionToTranslationsKeys(resolution);
178
+
238
     return {
179
     return {
239
         _audioOnly: audioOnly,
180
         _audioOnly: audioOnly,
240
-        _conferenceStarted: Boolean(conference),
241
-        _filmstripVisible: visible,
242
-        _resolution: resolution,
181
+        _labelKey: translationKeys.labelKey,
182
+        _tooltipKey: translationKeys.tooltipKey,
243
         _videoTrack: videoTrackOnLargeVideo
183
         _videoTrack: videoTrackOnLargeVideo
244
     };
184
     };
245
 }
185
 }

+ 70
- 70
react/features/videosipgw/middleware.js View File

52
 
52
 
53
         break;
53
         break;
54
     }
54
     }
55
-    case SIP_GW_INVITE_ROOMS: {
56
-        const { status } = getState()['features/videosipgw'];
57
-
58
-        if (status === JitsiSIPVideoGWStatus.STATUS_UNDEFINED) {
59
-            dispatch(showErrorNotification({
60
-                descriptionKey: 'recording.unavailable',
61
-                descriptionArguments: {
62
-                    serviceName: '$t(videoSIPGW.serviceName)'
63
-                },
64
-                titleKey: 'videoSIPGW.unavailableTitle'
65
-            }));
66
-
67
-            return;
68
-        } else if (status === JitsiSIPVideoGWStatus.STATUS_BUSY) {
69
-            dispatch(showWarningNotification({
70
-                descriptionKey: 'videoSIPGW.busy',
71
-                titleKey: 'videoSIPGW.busyTitle'
72
-            }));
73
-
74
-            return;
75
-        } else if (status !== JitsiSIPVideoGWStatus.STATUS_AVAILABLE) {
76
-            logger.error(`Unknown sip videogw status ${status}`);
77
-
78
-            return;
79
-        }
80
-
81
-        for (const room of action.rooms) {
82
-            const { id: sipAddress, name: displayName } = room;
83
-
84
-            if (sipAddress && displayName) {
85
-                const newSession = action.conference
86
-                    .createVideoSIPGWSession(sipAddress, displayName);
87
-
88
-                if (newSession instanceof Error) {
89
-                    const e = newSession;
90
-
91
-                    if (e) {
92
-                        switch (e.message) {
93
-                        case JitsiSIPVideoGWStatus.ERROR_NO_CONNECTION: {
94
-                            dispatch(showErrorNotification({
95
-                                descriptionKey: 'videoSIPGW.errorInvite',
96
-                                titleKey: 'videoSIPGW.errorInviteTitle'
97
-                            }));
98
-
99
-                            return;
100
-                        }
101
-                        case JitsiSIPVideoGWStatus.ERROR_SESSION_EXISTS: {
102
-                            dispatch(showWarningNotification({
103
-                                titleKey: 'videoSIPGW.errorAlreadyInvited',
104
-                                titleArguments: { displayName }
105
-                            }));
106
-
107
-                            return;
108
-                        }
109
-                        }
110
-                    }
111
-                    logger.error(
112
-                        'Unknown error trying to create sip videogw session',
113
-                        e);
114
-
115
-                    return;
116
-                }
117
-
118
-                newSession.start();
119
-            } else {
120
-                logger.error(`No display name or sip number for ${
121
-                    JSON.stringify(room)}`);
122
-            }
123
-        }
124
-    }
55
+    case SIP_GW_INVITE_ROOMS:
56
+        _inviteRooms(action.rooms, action.conference, dispatch);
57
+        break;
125
     }
58
     }
126
 
59
 
127
     return result;
60
     return result;
144
     };
77
     };
145
 }
78
 }
146
 
79
 
80
+/**
81
+ * Processes the action from the actionType {@code SIP_GW_INVITE_ROOMS} by
82
+ * inviting rooms into the conference or showing an error message.
83
+ *
84
+ * @param {Array} rooms - The conference rooms to invite.
85
+ * @param {Object} conference - The JitsiConference to invite the rooms to.
86
+ * @param {Function} dispatch - The redux dispatch function for emitting state
87
+ * changes (queuing error notifications).
88
+ * @private
89
+ * @returns {void}
90
+ */
91
+function _inviteRooms(rooms, conference, dispatch) {
92
+    for (const room of rooms) {
93
+        const { id: sipAddress, name: displayName } = room;
94
+
95
+        if (sipAddress && displayName) {
96
+            const newSession = conference
97
+                .createVideoSIPGWSession(sipAddress, displayName);
98
+
99
+            if (newSession instanceof Error) {
100
+                const e = newSession;
101
+
102
+                switch (e.message) {
103
+                case JitsiSIPVideoGWStatus.ERROR_NO_CONNECTION: {
104
+                    dispatch(showErrorNotification({
105
+                        descriptionKey: 'videoSIPGW.errorInvite',
106
+                        titleKey: 'videoSIPGW.errorInviteTitle'
107
+                    }));
108
+
109
+                    return;
110
+                }
111
+                case JitsiSIPVideoGWStatus.ERROR_SESSION_EXISTS: {
112
+                    dispatch(showWarningNotification({
113
+                        titleKey: 'videoSIPGW.errorAlreadyInvited',
114
+                        titleArguments: { displayName }
115
+                    }));
116
+
117
+                    return;
118
+                }
119
+                }
120
+
121
+                logger.error(
122
+                    'Unknown error trying to create sip videogw session',
123
+                    e);
124
+
125
+                return;
126
+            }
127
+
128
+            newSession.start();
129
+        } else {
130
+            logger.error(`No display name or sip number for ${
131
+                JSON.stringify(room)}`);
132
+        }
133
+    }
134
+}
135
+
147
 /**
136
 /**
148
  * Signals that a session we created has a change in its status.
137
  * Signals that a session we created has a change in its status.
149
  *
138
  *
173
             descriptionKey: 'videoSIPGW.errorInviteFailed'
162
             descriptionKey: 'videoSIPGW.errorInviteFailed'
174
         });
163
         });
175
     }
164
     }
165
+    case JitsiSIPVideoGWStatus.STATE_OFF: {
166
+        if (event.failureReason === JitsiSIPVideoGWStatus.STATUS_BUSY) {
167
+            return showErrorNotification({
168
+                descriptionKey: 'videoSIPGW.busy',
169
+                titleKey: 'videoSIPGW.busyTitle'
170
+            });
171
+        } else if (event.failureReason) {
172
+            logger.error(`Unknown sip videogw error ${event.newState} ${
173
+                event.failureReason}`);
174
+        }
175
+    }
176
     }
176
     }
177
 
177
 
178
     // nothing to show
178
     // nothing to show

+ 0
- 2
service/UI/UIEvents.js View File

64
      * @see {TOGGLE_FILMSTRIP}
64
      * @see {TOGGLE_FILMSTRIP}
65
      */
65
      */
66
     TOGGLED_FILMSTRIP: 'UI.toggled_filmstrip',
66
     TOGGLED_FILMSTRIP: 'UI.toggled_filmstrip',
67
-    TOGGLE_RECORDING: 'UI.toggle_recording',
68
     TOGGLE_SCREENSHARING: 'UI.toggle_screensharing',
67
     TOGGLE_SCREENSHARING: 'UI.toggle_screensharing',
69
     TOGGLED_SHARED_DOCUMENT: 'UI.toggled_shared_document',
68
     TOGGLED_SHARED_DOCUMENT: 'UI.toggled_shared_document',
70
     HANGUP: 'UI.hangup',
69
     HANGUP: 'UI.hangup',
71
     LOGOUT: 'UI.logout',
70
     LOGOUT: 'UI.logout',
72
-    RECORDING_TOGGLED: 'UI.recording_toggled',
73
     VIDEO_DEVICE_CHANGED: 'UI.video_device_changed',
71
     VIDEO_DEVICE_CHANGED: 'UI.video_device_changed',
74
     AUDIO_DEVICE_CHANGED: 'UI.audio_device_changed',
72
     AUDIO_DEVICE_CHANGED: 'UI.audio_device_changed',
75
     AUDIO_OUTPUT_DEVICE_CHANGED: 'UI.audio_output_device_changed',
73
     AUDIO_OUTPUT_DEVICE_CHANGED: 'UI.audio_output_device_changed',

Loading…
Cancel
Save