Переглянути джерело

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
master
virtuacoplenny 7 роки тому
джерело
коміт
ee74f11c3d
Аккаунт користувача з таким Email не знайдено
39 змінених файлів з 1252 додано та 1232 видалено
  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 Переглянути файл

@@ -27,7 +27,7 @@ import {
27 27
     redirectWithStoredParams,
28 28
     reloadWithStoredParams
29 29
 } from './react/features/app';
30
-import { updateRecordingState } from './react/features/recording';
30
+import { updateRecordingSessionData } from './react/features/recording';
31 31
 
32 32
 import EventEmitter from 'events';
33 33
 
@@ -1100,20 +1100,6 @@ export default {
1100 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 1104
      * Will be filled with values only when config.debug is enabled.
1119 1105
      * Its used by torture to check audio levels.
@@ -1821,12 +1807,6 @@ export default {
1821 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 1810
         if (!interfaceConfig.filmStripOnly) {
1831 1811
             room.on(JitsiConferenceEvents.CONNECTION_INTERRUPTED, () => {
1832 1812
                 APP.UI.markVideoInterrupted(true);
@@ -1951,14 +1931,36 @@ export default {
1951 1931
             });
1952 1932
 
1953 1933
         /* eslint-enable max-params */
1954
-
1955 1934
         room.on(
1956 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 1965
         room.on(JitsiConferenceEvents.KICKED, () => {
1964 1966
             APP.UI.hideStats();
@@ -2093,13 +2095,6 @@ export default {
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 2098
         APP.UI.addListener(UIEvents.AUTH_CLICKED, () => {
2104 2099
             AuthHandler.authenticate(room);
2105 2100
         });
@@ -2746,5 +2741,57 @@ export default {
2746 2741
         if (score === -1 || (score >= 1 && score <= 5)) {
2747 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 Переглянути файл

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

+ 39
- 79
css/modals/video-quality/_video-quality.scss Переглянути файл

@@ -141,98 +141,58 @@
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 180
         left: 50%;
196
-        opacity: 0.9;
197
-        position: absolute;
198
-        top: 50%;
181
+        padding: 10px;
182
+        position: fixed;
199 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 Переглянути файл

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

+ 5
- 0
lang/main.json Переглянути файл

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

+ 24
- 16
modules/UI/UI.js Переглянути файл

@@ -12,7 +12,6 @@ import UIUtil from './util/UIUtil';
12 12
 import UIEvents from '../../service/UI/UIEvents';
13 13
 import EtherpadManager from './etherpad/Etherpad';
14 14
 import SharedVideoManager from './shared_video/SharedVideo';
15
-import Recording from './recording/Recording';
16 15
 
17 16
 import VideoLayout from './videolayout/VideoLayout';
18 17
 import Filmstrip from './videolayout/Filmstrip';
@@ -38,6 +37,7 @@ import {
38 37
 import { shouldShowOnlyDeviceSelection } from '../../react/features/settings';
39 38
 import {
40 39
     dockToolbox,
40
+    setToolboxEnabled,
41 41
     showToolbox
42 42
 } from '../../react/features/toolbox';
43 43
 
@@ -337,7 +337,12 @@ UI.start = function() {
337 337
     if (!interfaceConfig.filmStripOnly) {
338 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 347
     sharedVideoManager = new SharedVideoManager(eventEmitter);
343 348
 
@@ -346,9 +351,20 @@ UI.start = function() {
346 351
         Filmstrip.setFilmstripOnly();
347 352
         APP.store.dispatch(setNotificationsEnabled(false));
348 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 369
         // Initialize side panels
354 370
         SidePanels.init(eventEmitter);
@@ -520,13 +536,9 @@ UI.onPeerVideoTypeChanged
520 536
 UI.updateLocalRole = isModerator => {
521 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,10 +893,6 @@ UI.addMessage = function(from, displayName, message, stamp) {
881 893
     Chat.updateChatConversation(from, displayName, message, stamp);
882 894
 };
883 895
 
884
-UI.updateRecordingState = function(state) {
885
-    Recording.updateRecordingState(state);
886
-};
887
-
888 896
 UI.notifyTokenAuthFailed = function() {
889 897
     messageHandler.showError({
890 898
         descriptionKey: 'dialog.tokenAuthFailed',

+ 0
- 462
modules/UI/recording/Recording.js Переглянути файл

@@ -1,462 +0,0 @@
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 Переглянути файл

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

+ 1
- 1
package-lock.json Переглянути файл

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

+ 1
- 1
package.json Переглянути файл

@@ -46,7 +46,7 @@
46 46
     "jquery-i18next": "1.2.0",
47 47
     "js-md5": "0.6.1",
48 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 50
     "lodash": "4.17.4",
51 51
     "moment": "2.19.4",
52 52
     "postis": "2.2.0",

+ 0
- 0
react/features/base/label/components/CircularLabel.native.js Переглянути файл


+ 60
- 0
react/features/base/label/components/CircularLabel.web.js Переглянути файл

@@ -0,0 +1,60 @@
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 Переглянути файл

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

+ 1
- 0
react/features/base/label/index.js Переглянути файл

@@ -0,0 +1 @@
1
+export * from './components';

+ 1
- 1
react/features/base/lib-jitsi-meet/index.js Переглянути файл

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

+ 7
- 1
react/features/invite/components/InfoDialogButton.web.js Переглянути файл

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

+ 0
- 0
react/features/large-video/components/Labels.native.js Переглянути файл


+ 139
- 0
react/features/large-video/components/Labels.web.js Переглянути файл

@@ -0,0 +1,139 @@
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 Переглянути файл

@@ -4,8 +4,8 @@ import PropTypes from 'prop-types';
4 4
 import React, { Component } from 'react';
5 5
 
6 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 10
 declare var interfaceConfig: Object;
11 11
 
@@ -72,8 +72,7 @@ export default class LargeVideo extends Component<*> {
72 72
                 </div>
73 73
                 <span id = 'localConnectionMessage' />
74 74
                 { this.props.hideVideoQualityLabel
75
-                    ? null : <VideoQualityLabel /> }
76
-                <RecordingLabel />
75
+                    ? null : <Labels /> }
77 76
             </div>
78 77
         );
79 78
     }

+ 5
- 38
react/features/recording/actionTypes.js Переглянути файл

@@ -1,44 +1,11 @@
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 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 Переглянути файл

@@ -1,66 +1,24 @@
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 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 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 Переглянути файл

@@ -1,12 +1,16 @@
1
-/* globals APP, interfaceConfig */
1
+// @flow
2 2
 
3 3
 import Spinner from '@atlaskit/spinner';
4
-import PropTypes from 'prop-types';
5 4
 import React, { Component } from 'react';
6 5
 import { connect } from 'react-redux';
7 6
 
7
+import {
8
+    createRecordingDialogEvent,
9
+    sendAnalytics
10
+} from '../../../analytics';
8 11
 import { Dialog } from '../../../base/dialog';
9 12
 import { translate } from '../../../base/i18n';
13
+import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
10 14
 
11 15
 import googleApi from '../../googleApi';
12 16
 
@@ -14,6 +18,8 @@ import BroadcastsDropdown from './BroadcastsDropdown';
14 18
 import GoogleSignInButton from './GoogleSignInButton';
15 19
 import StreamKeyForm from './StreamKeyForm';
16 20
 
21
+declare var interfaceConfig: Object;
22
+
17 23
 /**
18 24
  * An enumeration of the different states the Google API can be in while
19 25
  * interacting with {@code StartLiveStreamDialog}.
@@ -45,62 +51,72 @@ const GOOGLE_API_STATES = {
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 122
      * Initializes a new {@code StartLiveStreamDialog} instance.
@@ -108,9 +124,17 @@ class StartLiveStreamDialog extends Component {
108 124
      * @param {Props} props - The React {@code Component} props to initialize
109 125
      * the new {@code StartLiveStreamDialog} instance with.
110 126
      */
111
-    constructor(props) {
127
+    constructor(props: Props) {
112 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 139
          * Instance variable used to flag whether the component is or is not
116 140
          * mounted. Used as a hack to avoid setting state on an unmounted
@@ -186,6 +210,8 @@ class StartLiveStreamDialog extends Component {
186 210
         );
187 211
     }
188 212
 
213
+    _onInitializeGoogleApi: () => Object;
214
+
189 215
     /**
190 216
      * Loads the Google web client application used for fetching stream keys.
191 217
      * If the user is already logged in, then a request for available YouTube
@@ -214,6 +240,8 @@ class StartLiveStreamDialog extends Component {
214 240
             });
215 241
     }
216 242
 
243
+    _onCancel: () => boolean;
244
+
217 245
     /**
218 246
      * Invokes the passed in {@link onCancel} callback and closes
219 247
      * {@code StartLiveStreamDialog}.
@@ -222,11 +250,13 @@ class StartLiveStreamDialog extends Component {
222 250
      * @returns {boolean} True is returned to close the modal.
223 251
      */
224 252
     _onCancel() {
225
-        this.props.onCancel(APP.UI.messageHandler.CANCEL);
253
+        sendAnalytics(createRecordingDialogEvent('start', 'cancel.button'));
226 254
 
227 255
         return true;
228 256
     }
229 257
 
258
+    _onGetYouTubeBroadcasts: () => Object;
259
+
230 260
     /**
231 261
      * Asks the user to sign in, if not already signed in, and then requests a
232 262
      * list of the user's YouTube broadcasts.
@@ -269,6 +299,8 @@ class StartLiveStreamDialog extends Component {
269 299
             });
270 300
     }
271 301
 
302
+    _onRequestGoogleSignIn: () => Object;
303
+
272 304
     /**
273 305
      * Forces the Google web client application to prompt for a sign in, such as
274 306
      * when changing account, and will then fetch available YouTube broadcasts.
@@ -282,6 +314,8 @@ class StartLiveStreamDialog extends Component {
282 314
             .then(() => this._onGetYouTubeBroadcasts());
283 315
     }
284 316
 
317
+    _onStreamKeyChange: () => void;
318
+
285 319
     /**
286 320
      * Callback invoked to update the {@code StartLiveStreamDialog} component's
287 321
      * display of the entered YouTube stream key.
@@ -297,6 +331,8 @@ class StartLiveStreamDialog extends Component {
297 331
         });
298 332
     }
299 333
 
334
+    _onSubmit: () => boolean;
335
+
300 336
     /**
301 337
      * Invokes the passed in {@link onSubmit} callback with the entered stream
302 338
      * key, and then closes {@code StartLiveStreamDialog}.
@@ -306,7 +342,7 @@ class StartLiveStreamDialog extends Component {
306 342
      * closing, true to close the modal.
307 343
      */
308 344
     _onSubmit() {
309
-        const { streamKey, selectedBoundStreamID } = this.state;
345
+        const { broadcasts, streamKey, selectedBoundStreamID } = this.state;
310 346
 
311 347
         if (!streamKey) {
312 348
             return false;
@@ -315,17 +351,25 @@ class StartLiveStreamDialog extends Component {
315 351
         let selectedBroadcastID = null;
316 352
 
317 353
         if (selectedBoundStreamID) {
318
-            const selectedBroadcast = this.state.broadcasts.find(
354
+            const selectedBroadcast = broadcasts && broadcasts.find(
319 355
                 broadcast => broadcast.boundStreamID === selectedBoundStreamID);
320 356
 
321 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 368
         return true;
327 369
     }
328 370
 
371
+    _onYouTubeBroadcastIDSelected: (string) => Object;
372
+
329 373
     /**
330 374
      * Fetches the stream key for a YouTube broadcast and updates the internal
331 375
      * state to display the associated stream key as being entered.
@@ -351,6 +395,8 @@ class StartLiveStreamDialog extends Component {
351 395
             });
352 396
     }
353 397
 
398
+    _parseBroadcasts: (Array<Object>) => Array<Object>;
399
+
354 400
     /**
355 401
      * Takes in a list of broadcasts from the YouTube API, removes dupes,
356 402
      * removes broadcasts that cannot get a stream key, and parses the
@@ -487,13 +533,15 @@ class StartLiveStreamDialog extends Component {
487 533
  * {@code StartLiveStreamDialog}.
488 534
  *
489 535
  * @param {Object} state - The redux state.
490
- * @protected
536
+ * @private
491 537
  * @returns {{
538
+ *     _conference: Object,
492 539
  *     _googleApiApplicationClientID: string
493 540
  * }}
494 541
  */
495 542
 function _mapStateToProps(state) {
496 543
     return {
544
+        _conference: state['features/base/conference'].conference,
497 545
         _googleApiApplicationClientID:
498 546
             state['features/base/config'].googleApiApplicationClientID
499 547
     };

+ 56
- 41
react/features/recording/components/LiveStream/StopLiveStreamDialog.web.js Переглянути файл

@@ -1,8 +1,36 @@
1
-import PropTypes from 'prop-types';
1
+// @flow
2
+
2 3
 import React, { Component } from 'react';
4
+import { connect } from 'react-redux';
3 5
 
4 6
 import { Dialog } from '../../../base/dialog';
5 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 36
  * A React Component for confirming the participant wishes to stop the currently
@@ -10,41 +38,17 @@ import { translate } from '../../../base/i18n';
10 38
  *
11 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 43
      * Initializes a new {@code StopLiveStreamDialog} instance.
39 44
      *
40 45
      * @param {Object} props - The read-only properties with which the new
41 46
      * instance is to be initialized.
42 47
      */
43
-    constructor(props) {
48
+    constructor(props: Props) {
44 49
         super(props);
45 50
 
46 51
         // Bind event handler so it is only bound once for every instance.
47
-        this._onCancel = this._onCancel.bind(this);
48 52
         this._onSubmit = this._onSubmit.bind(this);
49 53
     }
50 54
 
@@ -58,7 +62,6 @@ class StopLiveStreamDialog extends Component {
58 62
         return (
59 63
             <Dialog
60 64
                 okTitleKey = 'dialog.stopLiveStreaming'
61
-                onCancel = { this._onCancel }
62 65
                 onSubmit = { this._onSubmit }
63 66
                 titleKey = 'dialog.liveStreaming'
64 67
                 width = 'small'>
@@ -67,17 +70,7 @@ class StopLiveStreamDialog extends Component {
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 76
      * Callback invoked when stopping of live streaming is confirmed.
@@ -86,10 +79,32 @@ class StopLiveStreamDialog extends Component {
86 79
      * @returns {boolean} True to close the modal.
87 80
      */
88 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 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 Переглянути файл


+ 103
- 0
react/features/recording/components/Recording/StartRecordingDialog.web.js Переглянути файл

@@ -0,0 +1,103 @@
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 Переглянути файл


+ 109
- 0
react/features/recording/components/Recording/StopRecordingDialog.web.js Переглянути файл

@@ -0,0 +1,109 @@
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 Переглянути файл

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

+ 180
- 136
react/features/recording/components/RecordingLabel.web.js Переглянути файл

@@ -1,97 +1,142 @@
1
-import PropTypes from 'prop-types';
1
+// @flow
2
+
2 3
 import React, { Component } from 'react';
3
-import { connect } from 'react-redux';
4 4
 
5
+import { CircularLabel } from '../../base/label';
5 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 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 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 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,78 +146,77 @@ class RecordingLabel extends Component {
101 146
      * @returns {ReactElement}
102 147
      */
103 148
     render() {
149
+        if (this.state.hidden) {
150
+            return null;
151
+        }
152
+
104 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 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 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 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 Переглянути файл

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

+ 18
- 0
react/features/recording/functions.js Переглянути файл

@@ -0,0 +1,18 @@
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 Переглянути файл

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

+ 0
- 27
react/features/recording/middleware.js Переглянути файл

@@ -1,27 +0,0 @@
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 Переглянути файл

@@ -1,34 +1,60 @@
1 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 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 Переглянути файл

@@ -11,6 +11,7 @@ import {
11 11
 } from '../../../analytics';
12 12
 import { openDialog } from '../../../base/dialog';
13 13
 import { translate } from '../../../base/i18n';
14
+import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
14 15
 import {
15 16
     PARTICIPANT_ROLE,
16 17
     getLocalParticipant,
@@ -27,7 +28,13 @@ import {
27 28
     isDialOutEnabled
28 29
 } from '../../../invite';
29 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 38
 import { SettingsButton } from '../../../settings';
32 39
 import { toggleSharedVideo } from '../../../shared-video';
33 40
 import { toggleChat, toggleProfile } from '../../../side-panel';
@@ -95,6 +102,11 @@ type Props = {
95 102
      */
96 103
     _feedbackConfigured: boolean,
97 104
 
105
+    /**
106
+     * The current file recording session, if any.
107
+     */
108
+    _fileRecordingSession: Object,
109
+
98 110
     /**
99 111
      * Whether or not the app is currently in full screen.
100 112
      */
@@ -112,10 +124,9 @@ type Props = {
112 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 132
      * The ID of the local participant.
@@ -137,12 +148,6 @@ type Props = {
137 148
      */
138 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 152
      * Whether or not the local participant is screensharing.
148 153
      */
@@ -214,12 +219,13 @@ class Toolbox extends Component<Props> {
214 219
             = this._onToolbarOpenSpeakerStats.bind(this);
215 220
         this._onToolbarOpenVideoQuality
216 221
             = this._onToolbarOpenVideoQuality.bind(this);
217
-
218 222
         this._onToolbarToggleChat = this._onToolbarToggleChat.bind(this);
219 223
         this._onToolbarToggleEtherpad
220 224
             = this._onToolbarToggleEtherpad.bind(this);
221 225
         this._onToolbarToggleFullScreen
222 226
             = this._onToolbarToggleFullScreen.bind(this);
227
+        this._onToolbarToggleLiveStreaming
228
+            = this._onToolbarToggleLiveStreaming.bind(this);
223 229
         this._onToolbarToggleProfile
224 230
             = this._onToolbarToggleProfile.bind(this);
225 231
         this._onToolbarToggleRaiseHand
@@ -462,6 +468,22 @@ class Toolbox extends Component<Props> {
462 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 488
      * Dispatches an action to show or hide the profile edit panel.
467 489
      *
@@ -495,7 +517,12 @@ class Toolbox extends Component<Props> {
495 517
      * @returns {void}
496 518
      */
497 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,6 +791,25 @@ class Toolbox extends Component<Props> {
764 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 813
     _onToolbarToggleProfile: () => void;
768 814
 
769 815
     /**
@@ -805,8 +851,12 @@ class Toolbox extends Component<Props> {
805 851
      * @returns {void}
806 852
      */
807 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 861
         this._doToggleRecording();
812 862
     }
@@ -891,6 +941,30 @@ class Toolbox extends Component<Props> {
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 969
      * Renders the list elements of the overflow menu.
896 970
      *
@@ -904,6 +978,7 @@ class Toolbox extends Component<Props> {
904 978
             _feedbackConfigured,
905 979
             _fullScreen,
906 980
             _isGuest,
981
+            _recordingEnabled,
907 982
             _sharingVideo,
908 983
             t
909 984
         } = this.props;
@@ -929,7 +1004,12 @@ class Toolbox extends Component<Props> {
929 1004
                     text = { _fullScreen
930 1005
                         ? t('toolbar.exitFullScreen')
931 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 1013
             this._shouldShowButton('sharedvideo')
934 1014
                 && <OverflowMenuItem
935 1015
                     accessibilityLabel = 'Shared video'
@@ -979,42 +1059,23 @@ class Toolbox extends Component<Props> {
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 1065
      * @private
986 1066
      * @returns {ReactElement|null}
987 1067
      */
988 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 1075
         return (
1015 1076
             <OverflowMenuItem
1016 1077
                 accessibilityLabel = 'Record'
1017
-                icon = { iconClass }
1078
+                icon = 'icon-camera-take-picture'
1018 1079
                 key = 'recording'
1019 1080
                 onClick = { this._onToolbarToggleRecording }
1020 1081
                 text = { t(translationKey) } />
@@ -1055,7 +1116,6 @@ function _mapStateToProps(state) {
1055 1116
         enableRecording,
1056 1117
         iAmRecorder
1057 1118
     } = state['features/base/config'];
1058
-    const { isRecording, recordingType } = state['features/recording'];
1059 1119
     const sharedVideoStatus = state['features/shared-video'].status;
1060 1120
     const { current } = state['features/side-panel'];
1061 1121
     const {
@@ -1083,14 +1143,15 @@ function _mapStateToProps(state) {
1083 1143
         _hideInviteButton:
1084 1144
             iAmRecorder || (!addPeopleEnabled && !dialOutEnabled),
1085 1145
         _isGuest: state['features/base/jwt'].isGuest,
1086
-        _isRecording: isRecording,
1146
+        _fileRecordingSession:
1147
+            getActiveSession(state, JitsiRecordingConstants.mode.FILE),
1087 1148
         _fullScreen: fullScreen,
1149
+        _liveStreamingSession:
1150
+             getActiveSession(state, JitsiRecordingConstants.mode.STREAM),
1088 1151
         _localParticipantID: localParticipant.id,
1089 1152
         _overflowMenuVisible: overflowMenuVisible,
1090 1153
         _raisedHand: localParticipant.raisedHand,
1091
-        _recordingEnabled: isModerator && enableRecording
1092
-            && (conference && conference.isRecordingSupported()),
1093
-        _recordingType: recordingType,
1154
+        _recordingEnabled: isModerator && enableRecording,
1094 1155
         _screensharing: localVideo && localVideo.videoType === 'desktop',
1095 1156
         _sharingVideo: sharedVideoStatus === 'playing'
1096 1157
             || sharedVideoStatus === 'start'

+ 1
- 2
react/features/toolbox/reducer.js Переглянути файл

@@ -49,8 +49,7 @@ function _getInitialState() {
49 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 54
          * @type {boolean}
56 55
          */

+ 62
- 122
react/features/video-quality/components/VideoQualityLabel.web.js Переглянути файл

@@ -4,6 +4,7 @@ import React, { Component } from 'react';
4 4
 import { connect } from 'react-redux';
5 5
 
6 6
 import { translate } from '../../base/i18n';
7
+import { CircularLabel } from '../../base/label';
7 8
 import { MEDIA_TYPE } from '../../base/media';
8 9
 import { getTrackByMediaTypeAndParticipant } from '../../base/tracks';
9 10
 
@@ -49,20 +50,14 @@ export class VideoQualityLabel extends Component {
49 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 63
          * The redux representation of the JitsiTrack displayed on large video.
@@ -75,42 +70,6 @@ export class VideoQualityLabel extends Component {
75 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 74
      * Implements React's {@link Component#render()}.
116 75
      *
@@ -120,95 +79,76 @@ export class VideoQualityLabel extends Component {
120 79
     render() {
121 80
         const {
122 81
             _audioOnly,
123
-            _conferenceStarted,
124
-            _filmstripVisible,
125
-            _resolution,
82
+            _labelKey,
83
+            _tooltipKey,
126 84
             _videoTrack,
127 85
             t
128 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 91
         if (_audioOnly) {
150
-            labelContent = <i className = 'icon-visibility-off' />;
92
+            className = 'audio-only';
93
+            labelContent = t('videoStatus.audioOnly');
151 94
             tooltipKey = 'videoStatus.labelTooltipAudioOnly';
152 95
         } else if (!_videoTrack || _videoTrack.muted) {
153
-            labelContent = <i className = 'icon-visibility-off' />;
96
+            className = 'no-video';
97
+            labelContent = t('videoStatus.audioOnly');
154 98
             tooltipKey = 'videoStatus.labelTooiltipNoVideo';
155 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 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,15 +159,13 @@ export class VideoQualityLabel extends Component {
219 159
  * @private
220 160
  * @returns {{
221 161
  *     _audioOnly: boolean,
222
- *     _conferenceStarted: boolean,
223
- *     _filmstripVisible: true,
224
- *     _resolution: number,
162
+ *     _labelKey: string,
163
+ *     _tooltipKey: string,
225 164
  *     _videoTrack: Object
226 165
  * }}
227 166
  */
228 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 169
     const { resolution, participantId } = state['features/large-video'];
232 170
     const videoTrackOnLargeVideo = getTrackByMediaTypeAndParticipant(
233 171
         state['features/base/tracks'],
@@ -235,11 +173,13 @@ function _mapStateToProps(state) {
235 173
         participantId
236 174
     );
237 175
 
176
+    const translationKeys
177
+        = audioOnly ? {} : _mapResolutionToTranslationsKeys(resolution);
178
+
238 179
     return {
239 180
         _audioOnly: audioOnly,
240
-        _conferenceStarted: Boolean(conference),
241
-        _filmstripVisible: visible,
242
-        _resolution: resolution,
181
+        _labelKey: translationKeys.labelKey,
182
+        _tooltipKey: translationKeys.tooltipKey,
243 183
         _videoTrack: videoTrackOnLargeVideo
244 184
     };
245 185
 }

+ 70
- 70
react/features/videosipgw/middleware.js Переглянути файл

@@ -52,76 +52,9 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
52 52
 
53 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 60
     return result;
@@ -144,6 +77,62 @@ function _availabilityChanged(status: string) {
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 137
  * Signals that a session we created has a change in its status.
149 138
  *
@@ -173,6 +162,17 @@ function _sessionStateChanged(
173 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 178
     // nothing to show

+ 0
- 2
service/UI/UIEvents.js Переглянути файл

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

Завантаження…
Відмінити
Зберегти