浏览代码

Show the YouTube live stream URL (#2837)

* feat(recording): show the YouTube live stream URL

- From the start live stream dialog, push up the broadcast ID
  of the chosen broadcast. It is assumed the ID can be used to
  create the YouTube link.
- Listen for lib-jitsi-meet to emit updates of the known live
  stream URL, shove it into redux, and have InfoDialog display
  it.

* ref(info): pass in dial in and live stream url

Passing these values in should trigger AtlasKit InlineDialog
to re-render and reposition itself.

* ref(info): use conference existence as trigger for autoshowing dialog

* feat(info): add live stream link to invite copy

* Revert "ref(info): use conference existence as trigger for autoshowing dialog"

This reverts commit 1072102267.

* hidden -> url

* _onClickHiddenURL -> _onClickURLText
j8
virtuacoplenny 7 年前
父节点
当前提交
2c4a3b0f60

+ 7
- 0
conference.js 查看文件

@@ -28,6 +28,7 @@ import {
28 28
     redirectWithStoredParams,
29 29
     reloadWithStoredParams
30 30
 } from './react/features/app';
31
+import { updateRecordingState } from './react/features/recording';
31 32
 
32 33
 import EventEmitter from 'events';
33 34
 
@@ -1851,6 +1852,12 @@ export default {
1851 1852
             APP.store.dispatch(dominantSpeakerChanged(id));
1852 1853
         });
1853 1854
 
1855
+        room.on(JitsiConferenceEvents.LIVE_STREAM_URL_CHANGED,
1856
+            (from, liveStreamViewURL) =>
1857
+                APP.store.dispatch(updateRecordingState({
1858
+                    liveStreamViewURL
1859
+                })));
1860
+
1854 1861
         if (!interfaceConfig.filmStripOnly) {
1855 1862
             room.on(JitsiConferenceEvents.CONNECTION_INTERRUPTED, () => {
1856 1863
                 APP.UI.markVideoInterrupted(true);

+ 4
- 3
css/modals/invite/_info.scss 查看文件

@@ -59,7 +59,8 @@
59 59
         }
60 60
     }
61 61
 
62
-    .info-dialog-conference-url {
62
+    .info-dialog-conference-url,
63
+    .info-dialog-live-stream-url {
63 64
         width: max-content;
64 65
         width: -moz-max-content;
65 66
         width: -webkit-max-content;
@@ -81,8 +82,8 @@
81 82
         font-size: 16px;
82 83
     }
83 84
 
84
-    .info-dialog-invite-link,
85
-    .info-dialog-invite-link:hover {
85
+    .info-dialog-url-text,
86
+    .info-dialog-url-text:hover {
86 87
         color: inherit;
87 88
         cursor: inherit;
88 89
     }

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

@@ -516,9 +516,11 @@
516 516
         "dialInConferenceID": "PIN:",
517 517
         "dialInNotSupported": "Sorry, dialing in is currently not suppported.",
518 518
         "genericError": "Whoops, something went wrong.",
519
+        "inviteLiveStream": "To view the live stream of this meeting, click this link: __url__",
519 520
         "invitePhone": "To join by phone, dial __number__ and enter this PIN: __conferenceID__#",
520 521
         "invitePhoneAlternatives": "To view more phone numbers, click this link: __url__",
521 522
         "inviteURL": "To join the video meeting, click this link: __url__",
523
+        "liveStreamURL": "Live stream:",
522 524
         "moreNumbers": "More numbers",
523 525
         "noNumbers": "No dial-in numbers.",
524 526
         "noPassword": "None",

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

@@ -109,7 +109,10 @@ function _requestLiveStreamId() {
109 109
     return new Promise((resolve, reject) =>
110 110
         APP.store.dispatch(openDialog(StartLiveStreamDialog, {
111 111
             onCancel: reject,
112
-            onSubmit: resolve
112
+            onSubmit: (streamId, broadcastId) => resolve({
113
+                broadcastId,
114
+                streamId
115
+            })
113 116
         })));
114 117
 }
115 118
 
@@ -257,7 +260,6 @@ const Recording = {
257 260
      * @param recordingState gives us the current recording state
258 261
      */
259 262
     updateRecordingUI(recordingState) {
260
-
261 263
         const oldState = this.currentState;
262 264
 
263 265
         this.currentState = recordingState;
@@ -388,10 +390,13 @@ const Recording = {
388 390
         case JitsiRecordingStatus.OFF: {
389 391
             if (this.recordingType === 'jibri') {
390 392
                 _requestLiveStreamId()
391
-                .then(streamId => {
393
+                .then(({ broadcastId, streamId }) => {
392 394
                     this.eventEmitter.emit(
393 395
                         UIEvents.RECORDING_TOGGLED,
394
-                        { streamId });
396
+                        {
397
+                            broadcastId,
398
+                            streamId
399
+                        });
395 400
 
396 401
                     // The confirm button on the start recording dialog was
397 402
                     // clicked

+ 18
- 11
react/features/invite/components/InfoDialogButton.web.js 查看文件

@@ -35,12 +35,9 @@ class InfoDialogButton extends Component {
35 35
     static propTypes = {
36 36
 
37 37
         /**
38
-         * Phone numbers for dialing into the conference.
38
+         * The redux state representing the dial-in numbers feature.
39 39
          */
40
-        _dialInNumbers: PropTypes.oneOfType([
41
-            PropTypes.object,
42
-            PropTypes.array
43
-        ]),
40
+        _dialIn: PropTypes.object,
44 41
 
45 42
         /**
46 43
          * Whether or not the {@code InfoDialog} should display automatically
@@ -48,6 +45,11 @@ class InfoDialogButton extends Component {
48 45
          */
49 46
         _disableAutoShow: PropTypes.bool,
50 47
 
48
+        /**
49
+         * The URL for a currently active live broadcast
50
+         */
51
+        _liveStreamViewURL: PropTypes.string,
52
+
51 53
         /**
52 54
          * The number of real participants in the call. If in a lonely call,
53 55
          * the {@code InfoDialog} will be automatically shown.
@@ -117,7 +119,7 @@ class InfoDialogButton extends Component {
117 119
             this._maybeAutoShowDialog();
118 120
         }, INFO_DIALOG_AUTO_SHOW_TIMEOUT);
119 121
 
120
-        if (!this.props._dialInNumbers) {
122
+        if (!this.props._dialIn.numbers) {
121 123
             this.props.dispatch(updateDialInNumbers());
122 124
         }
123 125
     }
@@ -150,7 +152,7 @@ class InfoDialogButton extends Component {
150 152
      * @returns {ReactElement}
151 153
      */
152 154
     render() {
153
-        const { t } = this.props;
155
+        const { _dialIn, _liveStreamViewURL, t } = this.props;
154 156
         const { showDialog } = this.state;
155 157
         const iconClass = `icon-info ${showDialog ? 'toggled' : ''}`;
156 158
 
@@ -158,7 +160,10 @@ class InfoDialogButton extends Component {
158 160
             <div className = 'toolbox-button-wth-dialog'>
159 161
                 <InlineDialog
160 162
                     content = {
161
-                        <InfoDialog onClose = { this._onDialogClose } /> }
163
+                        <InfoDialog
164
+                            dialIn = { _dialIn }
165
+                            liveStreamViewURL = { _liveStreamViewURL }
166
+                            onClose = { this._onDialogClose } /> }
162 167
                     isOpen = { showDialog }
163 168
                     onClose = { this._onDialogClose }
164 169
                     position = { 'top right' }>
@@ -215,16 +220,18 @@ class InfoDialogButton extends Component {
215 220
  * @param {Object} state - The Redux state.
216 221
  * @private
217 222
  * @returns {{
218
- *     _dialInNumbers: Array,
219
- *     _disableAutoShow: bolean,
223
+ *     _dialIn: Object,
224
+ *     _disableAutoShow: boolean,
225
+ *     _liveStreamViewURL: string,
220 226
  *     _participantCount: number,
221 227
  *     _toolboxVisible: boolean
222 228
  * }}
223 229
  */
224 230
 function _mapStateToProps(state) {
225 231
     return {
226
-        _dialInNumbers: state['features/invite'].numbers,
232
+        _dialIn: state['features/invite'],
227 233
         _disableAutoShow: state['features/base/config'].iAmRecorder,
234
+        _liveStreamViewURL: state['features/recording'].liveStreamViewURL,
228 235
         _participantCount:
229 236
             getParticipantCount(state['features/base/participants']),
230 237
         _toolboxVisible: state['features/toolbox'].visible

+ 60
- 52
react/features/invite/components/info-dialog/InfoDialog.web.js 查看文件

@@ -10,8 +10,6 @@ import {
10 10
     getLocalParticipant
11 11
 } from '../../../base/participants';
12 12
 
13
-import { updateDialInNumbers } from '../../actions';
14
-
15 13
 import DialInNumber from './DialInNumber';
16 14
 import PasswordForm from './PasswordForm';
17 15
 
@@ -24,15 +22,6 @@ const logger = require('jitsi-meet-logger').getLogger(__filename);
24 22
  * @extends Component
25 23
  */
26 24
 class InfoDialog extends Component {
27
-    /**
28
-     * Default values for {@code InfoDialog} component's properties.
29
-     *
30
-     * @static
31
-     */
32
-    static defaultProps = {
33
-        autoUpdateNumbers: true
34
-    };
35
-
36 25
     /**
37 26
      * {@code InfoDialog} component's property types.
38 27
      *
@@ -57,11 +46,6 @@ class InfoDialog extends Component {
57 46
          */
58 47
         _conferenceName: PropTypes.string,
59 48
 
60
-        /**
61
-         * The redux state representing the dial-in numbers feature.
62
-         */
63
-        _dialIn: PropTypes.object,
64
-
65 49
         /**
66 50
          * The current url of the conference to be copied onto the clipboard.
67 51
          */
@@ -79,17 +63,20 @@ class InfoDialog extends Component {
79 63
         _password: PropTypes.string,
80 64
 
81 65
         /**
82
-         * Whether or not this component should make a request for dial-in
83
-         * numbers. If false, this component will rely on an outside source
84
-         * updating and passing in numbers through the _dialIn prop.
66
+         * The object representing the dialIn feature.
85 67
          */
86
-        autoUpdateNumbers: PropTypes.bool,
68
+        dialIn: PropTypes.object,
87 69
 
88 70
         /**
89 71
          * Invoked to open a dialog for adding participants to the conference.
90 72
          */
91 73
         dispatch: PropTypes.func,
92 74
 
75
+        /**
76
+         * The current known URL for a live stream in progress.
77
+         */
78
+        liveStreamViewURL: PropTypes.string,
79
+
93 80
         /**
94 81
          * Callback invoked when the dialog should be closed.
95 82
          */
@@ -129,7 +116,7 @@ class InfoDialog extends Component {
129 116
     constructor(props) {
130 117
         super(props);
131 118
 
132
-        const { defaultCountry, numbers } = props._dialIn;
119
+        const { defaultCountry, numbers } = props.dialIn;
133 120
 
134 121
         if (numbers) {
135 122
             this.state.phoneNumber
@@ -147,7 +134,7 @@ class InfoDialog extends Component {
147 134
         this._copyElement = null;
148 135
 
149 136
         // Bind event handlers so they are only bound once for every instance.
150
-        this._onClickInviteURL = this._onClickInviteURL.bind(this);
137
+        this._onClickURLText = this._onClickURLText.bind(this);
151 138
         this._onCopyInviteURL = this._onCopyInviteURL.bind(this);
152 139
         this._onPasswordRemove = this._onPasswordRemove.bind(this);
153 140
         this._onPasswordSubmit = this._onPasswordSubmit.bind(this);
@@ -156,20 +143,6 @@ class InfoDialog extends Component {
156 143
         this._setCopyElement = this._setCopyElement.bind(this);
157 144
     }
158 145
 
159
-    /**
160
-     * Implements {@link Component#componentDidMount()}. Invoked immediately
161
-     * after this component is mounted. Requests dial-in numbers if not
162
-     * already known.
163
-     *
164
-     * @inheritdoc
165
-     * @returns {void}
166
-     */
167
-    componentDidMount() {
168
-        if (!this.state.phoneNumber && this.props.autoUpdateNumbers) {
169
-            this.props.dispatch(updateDialInNumbers());
170
-        }
171
-    }
172
-
173 146
     /**
174 147
      * Implements React's {@link Component#componentWillReceiveProps()}. Invoked
175 148
      * before this mounted component receives new props.
@@ -182,8 +155,8 @@ class InfoDialog extends Component {
182 155
             this.setState({ passwordEditEnabled: false });
183 156
         }
184 157
 
185
-        if (!this.state.phoneNumber && nextProps._dialIn.numbers) {
186
-            const { defaultCountry, numbers } = nextProps._dialIn;
158
+        if (!this.state.phoneNumber && nextProps.dialIn.numbers) {
159
+            const { defaultCountry, numbers } = nextProps.dialIn;
187 160
 
188 161
             this.setState({
189 162
                 phoneNumber:
@@ -199,7 +172,7 @@ class InfoDialog extends Component {
199 172
      * @returns {ReactElement}
200 173
      */
201 174
     render() {
202
-        const { onMouseOver, t } = this.props;
175
+        const { liveStreamViewURL, onMouseOver, t } = this.props;
203 176
 
204 177
         return (
205 178
             <div
@@ -221,9 +194,9 @@ class InfoDialog extends Component {
221 194
                         <span className = 'spacer'>&nbsp;</span>
222 195
                         <span className = 'info-value'>
223 196
                             <a
224
-                                className = 'info-dialog-invite-link'
197
+                                className = 'info-dialog-url-text'
225 198
                                 href = { this.props._inviteURL }
226
-                                onClick = { this._onClickInviteURL } >
199
+                                onClick = { this._onClickURLText } >
227 200
                                 { this._getURLToDisplay() }
228 201
                             </a>
229 202
                         </span>
@@ -231,6 +204,7 @@ class InfoDialog extends Component {
231 204
                     <div className = 'info-dialog-dial-in'>
232 205
                         { this._renderDialInDisplay() }
233 206
                     </div>
207
+                    { liveStreamViewURL && this._renderLiveStreamURL() }
234 208
                     <div className = 'info-dialog-password'>
235 209
                         <PasswordForm
236 210
                             editEnabled = { this.state.passwordEditEnabled }
@@ -321,16 +295,24 @@ class InfoDialog extends Component {
321 295
      * @returns {string}
322 296
      */
323 297
     _getTextToCopy() {
324
-        const { t } = this.props;
298
+        const { liveStreamViewURL, t } = this.props;
325 299
 
326 300
         let invite = t('info.inviteURL', {
327 301
             url: this.props._inviteURL
328 302
         });
329 303
 
304
+        if (liveStreamViewURL) {
305
+            const liveStream = t('info.inviteLiveStream', {
306
+                url: liveStreamViewURL
307
+            });
308
+
309
+            invite = `${invite}\n${liveStream}`;
310
+        }
311
+
330 312
         if (this._shouldDisplayDialIn()) {
331 313
             const dial = t('info.invitePhone', {
332 314
                 number: this.state.phoneNumber,
333
-                conferenceID: this.props._dialIn.conferenceID
315
+                conferenceID: this.props.dialIn.conferenceID
334 316
             });
335 317
             const moreNumbers = t('info.invitePhoneAlternatives', {
336 318
                 url: this._getDialInfoPageURL()
@@ -353,16 +335,16 @@ class InfoDialog extends Component {
353 335
     }
354 336
 
355 337
     /**
356
-     * Callback invoked when the displayed invite URL link is clicked to prevent
357
-     * actual navigation from happening. The invite URL link has an href to
358
-     * display "Copy Link Address" in the context menu but otherwise it should
359
-     * not behave like a link.
338
+     * Callback invoked when a displayed URL link is clicked to prevent actual
339
+     * navigation from happening. The URL links have an href to display "Copy
340
+     * Link Address" in the context menu but otherwise it should not behave like
341
+     * links.
360 342
      *
361 343
      * @param {Object} event - The click event from clicking on the link.
362 344
      * @private
363 345
      * @returns {void}
364 346
      */
365
-    _onClickInviteURL(event) {
347
+    _onClickURLText(event) {
366 348
         event.preventDefault();
367 349
     }
368 350
 
@@ -439,7 +421,7 @@ class InfoDialog extends Component {
439 421
         return (
440 422
             <div>
441 423
                 <DialInNumber
442
-                    conferenceID = { this.props._dialIn.conferenceID }
424
+                    conferenceID = { this.props.dialIn.conferenceID }
443 425
                     phoneNumber = { this.state.phoneNumber } />
444 426
                 <a
445 427
                     className = 'more-numbers'
@@ -490,6 +472,34 @@ class InfoDialog extends Component {
490 472
             : null;
491 473
     }
492 474
 
475
+    /**
476
+     * Returns a ReactElement for display a link to the current url of a
477
+     * live stream in progress.
478
+     *
479
+     * @private
480
+     * @returns {null|ReactElement}
481
+     */
482
+    _renderLiveStreamURL() {
483
+        const { liveStreamViewURL, t } = this.props;
484
+
485
+        return (
486
+            <div className = 'info-dialog-live-stream-url'>
487
+                <span className = 'info-label'>
488
+                    { t('info.liveStreamURL') }
489
+                </span>
490
+                <span className = 'spacer'>&nbsp;</span>
491
+                <span className = 'info-value'>
492
+                    <a
493
+                        className = 'info-dialog-url-text'
494
+                        href = { liveStreamViewURL }
495
+                        onClick = { this._onClickURLText } >
496
+                        { liveStreamViewURL }
497
+                    </a>
498
+                </span>
499
+            </div>
500
+        );
501
+    }
502
+
493 503
     /**
494 504
      * Returns whether or not dial-in related UI should be displayed.
495 505
      *
@@ -497,7 +507,7 @@ class InfoDialog extends Component {
497 507
      * @returns {boolean}
498 508
      */
499 509
     _shouldDisplayDialIn() {
500
-        const { conferenceID, numbers, numbersEnabled } = this.props._dialIn;
510
+        const { conferenceID, numbers, numbersEnabled } = this.props.dialIn;
501 511
         const { phoneNumber } = this.state;
502 512
 
503 513
         return Boolean(
@@ -531,7 +541,6 @@ class InfoDialog extends Component {
531 541
  *     _canEditPassword: boolean,
532 542
  *     _conference: Object,
533 543
  *     _conferenceName: string,
534
- *     _dialIn: Object,
535 544
  *     _inviteURL: string,
536 545
  *     _locked: string,
537 546
  *     _password: string
@@ -558,7 +567,6 @@ function _mapStateToProps(state) {
558 567
         _canEditPassword: canEditPassword,
559 568
         _conference: conference,
560 569
         _conferenceName: room,
561
-        _dialIn: state['features/invite'],
562 570
         _inviteURL: getInviteURL(state),
563 571
         _locked: locked,
564 572
         _password: password

+ 3
- 3
react/features/recording/components/LiveStream/BroadcastsDropdown.web.js 查看文件

@@ -44,7 +44,7 @@ class BroadcastsDropdown extends PureComponent {
44 44
          * The boundStreamID of the broadcast that should display as selected in
45 45
          * the dropdown.
46 46
          */
47
-        selectedBroadcastID: PropTypes.string,
47
+        selectedBoundStreamID: PropTypes.string,
48 48
 
49 49
         /**
50 50
          * Invoked to obtain translated strings.
@@ -84,7 +84,7 @@ class BroadcastsDropdown extends PureComponent {
84 84
      * @returns {ReactElement}
85 85
      */
86 86
     render() {
87
-        const { broadcasts, selectedBroadcastID, t } = this.props;
87
+        const { broadcasts, selectedBoundStreamID, t } = this.props;
88 88
 
89 89
         const dropdownItems = broadcasts.map(broadcast =>
90 90
             // eslint-disable-next-line react/jsx-wrap-multilines
@@ -96,7 +96,7 @@ class BroadcastsDropdown extends PureComponent {
96 96
             </DropdownItem>
97 97
         );
98 98
         const selected = this.props.broadcasts.find(
99
-            broadcast => broadcast.boundStreamID === selectedBroadcastID);
99
+            broadcast => broadcast.boundStreamID === selectedBoundStreamID);
100 100
         const triggerText = (selected && selected.title)
101 101
             || t('liveStreaming.choose');
102 102
 

+ 21
- 7
react/features/recording/components/LiveStream/StartLiveStreamDialog.web.js 查看文件

@@ -89,6 +89,8 @@ class StartLiveStreamDialog extends Component {
89 89
      * available for use for the logged in Google user's YouTube account.
90 90
      * @property {string} googleProfileEmail - The email of the user currently
91 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.
92 94
      * @property {string} streamKey - The selected or entered stream key to use
93 95
      * for YouTube live streaming.
94 96
      */
@@ -96,7 +98,7 @@ class StartLiveStreamDialog extends Component {
96 98
         broadcasts: undefined,
97 99
         googleAPIState: GOOGLE_API_STATES.NEEDS_LOADING,
98 100
         googleProfileEmail: '',
99
-        selectedBroadcastID: undefined,
101
+        selectedBoundStreamID: undefined,
100 102
         streamKey: ''
101 103
     };
102 104
 
@@ -291,7 +293,7 @@ class StartLiveStreamDialog extends Component {
291 293
     _onStreamKeyChange(event) {
292 294
         this._setStateIfMounted({
293 295
             streamKey: event.target.value,
294
-            selectedBroadcastID: undefined
296
+            selectedBoundStreamID: undefined
295 297
         });
296 298
     }
297 299
 
@@ -304,11 +306,22 @@ class StartLiveStreamDialog extends Component {
304 306
      * closing, true to close the modal.
305 307
      */
306 308
     _onSubmit() {
307
-        if (!this.state.streamKey) {
309
+        const { streamKey, selectedBoundStreamID } = this.state;
310
+
311
+        if (!streamKey) {
308 312
             return false;
309 313
         }
310 314
 
311
-        this.props.onSubmit(this.state.streamKey);
315
+        let selectedBroadcastID = null;
316
+
317
+        if (selectedBoundStreamID) {
318
+            const selectedBroadcast = this.state.broadcasts.find(
319
+                broadcast => broadcast.boundStreamID === selectedBoundStreamID);
320
+
321
+            selectedBroadcastID = selectedBroadcast && selectedBroadcast.id;
322
+        }
323
+
324
+        this.props.onSubmit(streamKey, selectedBroadcastID);
312 325
 
313 326
         return true;
314 327
     }
@@ -333,7 +346,7 @@ class StartLiveStreamDialog extends Component {
333 346
 
334 347
                 this._setStateIfMounted({
335 348
                     streamKey,
336
-                    selectedBroadcastID: boundStreamID
349
+                    selectedBoundStreamID: boundStreamID
337 350
                 });
338 351
             });
339 352
     }
@@ -358,6 +371,7 @@ class StartLiveStreamDialog extends Component {
358 371
             if (boundStreamID && !parsedBroadcasts[boundStreamID]) {
359 372
                 parsedBroadcasts[boundStreamID] = {
360 373
                     boundStreamID,
374
+                    id: broadcast.id,
361 375
                     status: broadcast.status.lifeCycleStatus,
362 376
                     title: broadcast.snippet.title
363 377
                 };
@@ -378,7 +392,7 @@ class StartLiveStreamDialog extends Component {
378 392
         const {
379 393
             broadcasts,
380 394
             googleProfileEmail,
381
-            selectedBroadcastID
395
+            selectedBoundStreamID
382 396
         } = this.state;
383 397
 
384 398
         let googleContent, helpText;
@@ -399,7 +413,7 @@ class StartLiveStreamDialog extends Component {
399 413
                 <BroadcastsDropdown
400 414
                     broadcasts = { broadcasts }
401 415
                     onBroadcastSelected = { this._onYouTubeBroadcastIDSelected }
402
-                    selectedBroadcastID = { selectedBroadcastID } />
416
+                    selectedBoundStreamID = { selectedBoundStreamID } />
403 417
             );
404 418
 
405 419
             /**

正在加载...
取消
保存