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

Merge pull request #548 from jitsi/follow-me

Follow me
master
damencho пре 9 година
родитељ
комит
f788a45bac

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

@@ -31,6 +31,8 @@ const Commands = {
31 31
     SHARED_VIDEO: "shared-video"
32 32
 };
33 33
 
34
+import {VIDEO_CONTAINER_TYPE} from "./modules/UI/videolayout/LargeVideo";
35
+
34 36
 /**
35 37
  * Open Connection. When authentication failed it shows auth dialog.
36 38
  * @param roomName the room name to use
@@ -509,6 +511,51 @@ export default {
509 511
 
510 512
         this._setupListeners();
511 513
     },
514
+
515
+    /**
516
+     * Exposes a Command(s) API on this instance. It is necessitated by (1) the
517
+     * desire to keep room private to this instance and (2) the need of other
518
+     * modules to send and receive commands to and from participants.
519
+     * Eventually, this instance remains in control with respect to the
520
+     * decision whether the Command(s) API of room (i.e. lib-jitsi-meet's
521
+     * JitsiConference) is to be used in the implementation of the Command(s)
522
+     * API of this instance.
523
+     */
524
+    commands: {
525
+        /**
526
+         * Receives notifications from other participants about commands aka
527
+         * custom events (sent by sendCommand or sendCommandOnce methods).
528
+         * @param command {String} the name of the command
529
+         * @param handler {Function} handler for the command
530
+         */
531
+        addCommandListener () {
532
+            room.addCommandListener.apply(room, arguments);
533
+        },
534
+        /**
535
+         * Removes command.
536
+         * @param name {String} the name of the command.
537
+         */
538
+        removeCommand () {
539
+            room.removeCommand.apply(room, arguments);
540
+        },
541
+        /**
542
+         * Sends command.
543
+         * @param name {String} the name of the command.
544
+         * @param values {Object} with keys and values that will be sent.
545
+         */
546
+        sendCommand () {
547
+            room.sendCommand.apply(room, arguments);
548
+        },
549
+        /**
550
+         * Sends command one time.
551
+         * @param name {String} the name of the command.
552
+         * @param values {Object} with keys and values that will be sent.
553
+         */
554
+        sendCommandOnce () {
555
+            room.sendCommandOnce.apply(room, arguments);
556
+        },
557
+    },
558
+
512 559
     _getConferenceOptions() {
513 560
         let options = config;
514 561
         if(config.enableRecording) {
@@ -985,8 +1032,20 @@ export default {
985 1032
         APP.UI.addListener(UIEvents.SELECTED_ENDPOINT, (id) => {
986 1033
             room.selectParticipant(id);
987 1034
         });
988
-        APP.UI.addListener(UIEvents.PINNED_ENDPOINT, (id) => {
989
-            room.pinParticipant(id);
1035
+
1036
+        APP.UI.addListener(UIEvents.PINNED_ENDPOINT, (smallVideo, isPinned) => {
1037
+            var smallVideoId = smallVideo.getId();
1038
+
1039
+            if (smallVideo.getVideoType() === VIDEO_CONTAINER_TYPE
1040
+                && !APP.conference.isLocalId(smallVideoId))
1041
+                if (isPinned)
1042
+                    room.pinParticipant(smallVideoId);
1043
+                // When the library starts supporting multiple pins we would
1044
+                // pass the isPinned parameter together with the identifier,
1045
+                // but currently we send null to indicate that we unpin the
1046
+                // last pinned.
1047
+                else
1048
+                    room.pinParticipant(null);
990 1049
         });
991 1050
 
992 1051
         APP.UI.addListener(

+ 7
- 11
css/settingsmenu.css Прегледај датотеку

@@ -43,29 +43,25 @@
43 43
     cursor: pointer;
44 44
 }
45 45
 
46
-
47
-#startMutedOptions {
46
+#startMutedOptions,
47
+#followMeOptions {
48 48
     padding-left: 10%;
49 49
     text-indent: -10%;
50
-
51 50
     margin-top: 10px;
52
-
53 51
     display: none; /* hide by default */
54
-
55 52
     /* clearfix */
56 53
     overflow: auto;
57 54
     zoom: 1;
58 55
 }
59 56
 
60
-#startAudioMuted {
61
-    width: 13px !important;
62
-}
63
-
64
-#startVideoMuted {
57
+#startAudioMuted,
58
+#startVideoMuted,
59
+#followMeCheckBox {
65 60
     width: 13px !important;
66 61
 }
67 62
 
68
-.startMutedLabel {
63
+.startMutedLabel,
64
+.followMeLabel {
69 65
     width: 94%;
70 66
     float: left;
71 67
     cursor: pointer;

+ 6
- 0
index.html Прегледај датотеку

@@ -241,6 +241,12 @@
241 241
                     <select id="selectMic"></select>
242 242
                 </label>
243 243
             </div>
244
+            <div id="followMeOptions">
245
+                <label class = "followMeLabel">
246
+                    <input type="checkbox" id="followMeCheckBox">
247
+                    <span  data-i18n="settings.followMe"></span>
248
+                </label>
249
+            </div>
244 250
             <a id="downloadlog" data-container="body" data-toggle="popover" data-placement="right" data-i18n="[data-content]downloadlogs" ><i class="fa fa-cloud-download"></i></a>
245 251
         </div>
246 252
         <div class="feedbackButton">

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

@@ -86,7 +86,8 @@
86 86
         "startAudioMuted": "start without audio",
87 87
         "startVideoMuted": "start without video",
88 88
         "selectCamera": "select camera",
89
-        "selectMic": "select microphone"
89
+        "selectMic": "select microphone",
90
+        "followMe": "Enable follow me"
90 91
     },
91 92
     "videothumbnail":
92 93
     {

+ 328
- 0
modules/FollowMe.js Прегледај датотеку

@@ -0,0 +1,328 @@
1
+/*
2
+ * Copyright @ 2015 Atlassian Pty Ltd
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ *     http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+import UIEvents from '../service/UI/UIEvents';
18
+import VideoLayout from './UI/videolayout/VideoLayout';
19
+import FilmStrip from './UI/videolayout/FilmStrip';
20
+
21
+/**
22
+ * The (name of the) command which transports the state (represented by
23
+ * {State} for the local state at the time of this writing) of a {FollowMe}
24
+ * (instance) between participants.
25
+ */
26
+const _COMMAND = "follow-me";
27
+
28
+/**
29
+ * Represents the set of {FollowMe}-related states (properties and their
30
+ * respective values) which are to be followed by a participant. {FollowMe}
31
+ * will send {_COMMAND} whenever a property of {State} changes (if the local
32
+ * participant is in her right to issue such a command, of course).
33
+ */
34
+class State {
35
+    /**
36
+     * Initializes a new {State} instance.
37
+     *
38
+     * @param propertyChangeCallback {Function} which is to be called when a
39
+     * property of the new instance has its value changed from an old value
40
+     * into a (different) new value. The function is supplied with the name of
41
+     * the property, the old value of the property before the change, and the
42
+     * new value of the property after the change.
43
+     */
44
+    constructor (propertyChangeCallback) {
45
+        this._propertyChangeCallback = propertyChangeCallback;
46
+    }
47
+
48
+    get filmStripVisible () { return this._filmStripVisible; }
49
+
50
+    set filmStripVisible (b) {
51
+        var oldValue = this._filmStripVisible;
52
+        if (oldValue !== b) {
53
+            this._filmStripVisible = b;
54
+            this._firePropertyChange('filmStripVisible', oldValue, b);
55
+        }
56
+    }
57
+
58
+    get nextOnStage() { return this._nextOnStage; }
59
+
60
+    set nextOnStage(id) {
61
+        var oldValue = this._nextOnStage;
62
+        if (oldValue !== id) {
63
+            this._nextOnStage = id;
64
+            this._firePropertyChange('nextOnStage', oldValue, id);
65
+        }
66
+    }
67
+
68
+    get sharedDocumentVisible () { return this._sharedDocumentVisible; }
69
+
70
+    set sharedDocumentVisible (b) {
71
+        var oldValue = this._sharedDocumentVisible;
72
+        if (oldValue !== b) {
73
+            this._sharedDocumentVisible = b;
74
+            this._firePropertyChange('sharedDocumentVisible', oldValue, b);
75
+        }
76
+    }
77
+
78
+    /**
79
+     * Invokes {_propertyChangeCallback} to notify it that {property} had its
80
+     * value changed from {oldValue} to {newValue}.
81
+     *
82
+     * @param property the name of the property which had its value changed
83
+     * from {oldValue} to {newValue}
84
+     * @param oldValue the value of {property} before the change
85
+     * @param newValue the value of {property} after the change
86
+     */
87
+    _firePropertyChange (property, oldValue, newValue) {
88
+        var propertyChangeCallback = this._propertyChangeCallback;
89
+        if (propertyChangeCallback)
90
+            propertyChangeCallback(property, oldValue, newValue);
91
+    }
92
+}
93
+
94
+/**
95
+ * Represents the &quot;Follow Me&quot; feature which enables a moderator to
96
+ * (partially) control the user experience/interface (e.g. film strip
97
+ * visibility) of (other) non-moderator particiapnts.
98
+ *
99
+ * @author Lyubomir Marinov
100
+ */
101
+class FollowMe {
102
+    /**
103
+     * Initializes a new {FollowMe} instance.
104
+     *
105
+     * @param conference the {conference} which is to transport
106
+     * {FollowMe}-related information between participants
107
+     * @param UI the {UI} which is the source (model/state) to be sent to
108
+     * remote participants if the local participant is the moderator or the
109
+     * destination (model/state) to receive from the remote moderator if the
110
+     * local participant is not the moderator
111
+     */
112
+    constructor (conference, UI) {
113
+        this._conference = conference;
114
+        this._UI = UI;
115
+
116
+        // The states of the local participant which are to be followed (by the
117
+        // remote participants when the local participant is in her right to
118
+        // issue such commands).
119
+        this._local = new State(this._localPropertyChange.bind(this));
120
+
121
+        // Listen to "Follow Me" commands. I'm not sure whether a moderator can
122
+        // (in lib-jitsi-meet and/or Meet) become a non-moderator. If that's
123
+        // possible, then it may be easiest to always listen to commands. The
124
+        // listener will validate received commands before acting on them.
125
+        conference.commands.addCommandListener(
126
+                _COMMAND,
127
+                this._onFollowMeCommand.bind(this));
128
+    }
129
+
130
+    /**
131
+     * Adds listeners for the UI states of the local participant which are
132
+     * to be followed (by the remote participants). A non-moderator (very
133
+     * likely) can become a moderator so it may be easiest to always track
134
+     * the states of interest.
135
+     * @private
136
+     */
137
+    _addFollowMeListeners () {
138
+        this.filmStripEventHandler = this._filmStripToggled.bind(this);
139
+        this._UI.addListener(UIEvents.TOGGLED_FILM_STRIP,
140
+                            this.filmStripEventHandler);
141
+
142
+        var self = this;
143
+        this.pinnedEndpointEventHandler = function (smallVideo, isPinned) {
144
+            self._nextOnStage(smallVideo, isPinned);
145
+        };
146
+        this._UI.addListener(UIEvents.PINNED_ENDPOINT,
147
+                            this.pinnedEndpointEventHandler);
148
+
149
+        this.sharedDocEventHandler = this._sharedDocumentToggled.bind(this);
150
+        this._UI.addListener( UIEvents.TOGGLED_SHARED_DOCUMENT,
151
+                            this.sharedDocEventHandler);
152
+    }
153
+
154
+    /**
155
+     * Removes all follow me listeners.
156
+     * @private
157
+     */
158
+    _removeFollowMeListeners () {
159
+        this._UI.removeListener(UIEvents.TOGGLED_FILM_STRIP,
160
+                                this.filmStripEventHandler);
161
+        this._UI.removeListener(UIEvents.TOGGLED_SHARED_DOCUMENT,
162
+                                this.sharedDocEventHandler);
163
+        this._UI.removeListener(UIEvents.PINNED_ENDPOINT,
164
+                                this.pinnedEndpointEventHandler);
165
+    }
166
+
167
+    /**
168
+     * Enables or disabled the follow me functionality
169
+     *
170
+     * @param enable {true} to enable the follow me functionality, {false} -
171
+     * to disable it
172
+     */
173
+    enableFollowMe (enable) {
174
+        this.isEnabled = enable;
175
+        if (this.isEnabled)
176
+            this._addFollowMeListeners();
177
+        else
178
+            this._removeFollowMeListeners();
179
+    }
180
+
181
+    /**
182
+     * Notifies this instance that the (visibility of the) film strip was
183
+     * toggled (in the user interface of the local participant).
184
+     *
185
+     * @param filmStripVisible {Boolean} {true} if the film strip was shown (as
186
+     * a result of the toggle) or {false} if the film strip was hidden
187
+     */
188
+    _filmStripToggled (filmStripVisible) {
189
+        this._local.filmStripVisible = filmStripVisible;
190
+    }
191
+
192
+    /**
193
+     * Notifies this instance that the (visibility of the) shared document was
194
+     * toggled (in the user interface of the local participant).
195
+     *
196
+     * @param sharedDocumentVisible {Boolean} {true} if the shared document was
197
+     * shown (as a result of the toggle) or {false} if it was hidden
198
+     */
199
+    _sharedDocumentToggled (sharedDocumentVisible) {
200
+        this._local.sharedDocumentVisible = sharedDocumentVisible;
201
+    }
202
+
203
+    /**
204
+     * Changes the nextOnPage property value.
205
+     *
206
+     * @param smallVideo the {SmallVideo} that was pinned or unpinned
207
+     * @param isPinned indicates if the given {SmallVideo} was pinned or
208
+     * unpinned
209
+     * @private
210
+     */
211
+    _nextOnStage (smallVideo, isPinned) {
212
+        if (!this._conference.isModerator)
213
+            return;
214
+
215
+        var nextOnStage = null;
216
+        if(isPinned)
217
+            nextOnStage = smallVideo.getId();
218
+
219
+        this._local.nextOnStage = nextOnStage;
220
+    }
221
+
222
+    /**
223
+     * Sends the follow-me command, when a local property change occurs.
224
+     *
225
+     * @param property the property name
226
+     * @param oldValue the old value
227
+     * @param newValue the new value
228
+     * @private
229
+     */
230
+    _localPropertyChange (property, oldValue, newValue) {
231
+        // Only a moderator is allowed to send commands.
232
+        var conference = this._conference;
233
+        if (!conference.isModerator)
234
+            return;
235
+
236
+        var commands = conference.commands;
237
+        // XXX The "Follow Me" command represents a snapshot of all states
238
+        // which are to be followed so don't forget to removeCommand before
239
+        // sendCommand!
240
+        commands.removeCommand(_COMMAND);
241
+        var self = this;
242
+        commands.sendCommandOnce(
243
+                _COMMAND,
244
+                {
245
+                    attributes: {
246
+                        filmStripVisible: self._local.filmStripVisible,
247
+                        nextOnStage: self._local.nextOnStage,
248
+                        sharedDocumentVisible: self._local.sharedDocumentVisible
249
+                    }
250
+                });
251
+    }
252
+
253
+    /**
254
+     * Notifies this instance about a &qout;Follow Me&qout; command (delivered
255
+     * by the Command(s) API of {this._conference}).
256
+     *
257
+     * @param attributes the attributes {Object} carried by the command
258
+     * @param id the identifier of the participant who issued the command. A
259
+     * notable idiosyncrasy of the Command(s) API to be mindful of here is that
260
+     * the command may be issued by the local participant.
261
+     */
262
+    _onFollowMeCommand ({ attributes }, id) {
263
+        // We require to know who issued the command because (1) only a
264
+        // moderator is allowed to send commands and (2) a command MUST be
265
+        // issued by a defined commander.
266
+        if (typeof id === 'undefined')
267
+            return;
268
+        // The Command(s) API will send us our own commands and we don't want
269
+        // to act upon them.
270
+        if (this._conference.isLocalId(id))
271
+            return;
272
+
273
+        // TODO Don't obey commands issued by non-moderators.
274
+
275
+        // Applies the received/remote command to the user experience/interface
276
+        // of the local participant.
277
+        this._onFilmStripVisible(attributes.filmStripVisible);
278
+        this._onNextOnStage(attributes.nextOnStage);
279
+        this._onSharedDocumentVisible(attributes.sharedDocumentVisible);
280
+    }
281
+
282
+    _onFilmStripVisible(filmStripVisible) {
283
+        if (typeof filmStripVisible !== 'undefined') {
284
+            // XXX The Command(s) API doesn't preserve the types (of
285
+            // attributes, at least) at the time of this writing so take into
286
+            // account that what originated as a Boolean may be a String on
287
+            // receipt.
288
+            filmStripVisible = (filmStripVisible == 'true');
289
+
290
+            // FIXME The UI (module) very likely doesn't (want to) expose its
291
+            // eventEmitter as a public field. I'm not sure at the time of this
292
+            // writing whether calling UI.toggleFilmStrip() is acceptable (from
293
+            // a design standpoint) either.
294
+            if (filmStripVisible !== FilmStrip.isFilmStripVisible())
295
+                this._UI.eventEmitter.emit(
296
+                    UIEvents.TOGGLE_FILM_STRIP,
297
+                    filmStripVisible);
298
+        }
299
+    }
300
+
301
+    _onNextOnStage(id) {
302
+
303
+        var clickId = null;
304
+        if(typeof id !== 'undefined' && !VideoLayout.isPinned(id))
305
+            clickId = id;
306
+        else if (typeof id == 'undefined')
307
+            clickId = VideoLayout.getPinnedId();
308
+
309
+        if (clickId !== null)
310
+            VideoLayout.handleVideoThumbClicked(clickId);
311
+    }
312
+
313
+    _onSharedDocumentVisible(sharedDocumentVisible) {
314
+        if (typeof sharedDocumentVisible !== 'undefined') {
315
+            // XXX The Command(s) API doesn't preserve the types (of
316
+            // attributes, at least) at the time of this writing so take into
317
+            // account that what originated as a Boolean may be a String on
318
+            // receipt.
319
+            sharedDocumentVisible = (sharedDocumentVisible == 'true');
320
+
321
+            if (sharedDocumentVisible
322
+                !== this._UI.getSharedDocumentManager().isVisible())
323
+                this._UI.getSharedDocumentManager().toggleEtherpad();
324
+        }
325
+    }
326
+}
327
+
328
+export default FollowMe;

+ 45
- 3
modules/UI/UI.js Прегледај датотеку

@@ -27,12 +27,16 @@ var messageHandler = UI.messageHandler;
27 27
 var JitsiPopover = require("./util/JitsiPopover");
28 28
 var Feedback = require("./Feedback");
29 29
 
30
+import FollowMe from "../FollowMe";
31
+
30 32
 var eventEmitter = new EventEmitter();
31 33
 UI.eventEmitter = eventEmitter;
32 34
 
33 35
 let etherpadManager;
34 36
 let sharedVideoManager;
35 37
 
38
+let followMeHandler;
39
+
36 40
 /**
37 41
  * Prompt user for nickname.
38 42
  */
@@ -245,6 +249,12 @@ UI.initConference = function () {
245 249
     if(!interfaceConfig.filmStripOnly) {
246 250
         Feedback.init();
247 251
     }
252
+
253
+    // FollowMe attempts to copy certain aspects of the moderator's UI into the
254
+    // other participants' UI. Consequently, it needs (1) read and write access
255
+    // to the UI (depending on the moderator role of the local participant) and
256
+    // (2) APP.conference as means of communication between the participants.
257
+    followMeHandler = new FollowMe(APP.conference, UI);
248 258
 };
249 259
 
250 260
 UI.mucJoined = function () {
@@ -282,6 +292,11 @@ function registerListeners() {
282 292
         UI.toggleFilmStrip();
283 293
         VideoLayout.resizeVideoArea(PanelToggler.isVisible(), true, false);
284 294
     });
295
+
296
+    UI.addListener(UIEvents.FOLLOW_ME_ENABLED, function (isEnabled) {
297
+        if (followMeHandler)
298
+            followMeHandler.enableFollowMe(isEnabled);
299
+    });
285 300
 }
286 301
 
287 302
 /**
@@ -331,7 +346,7 @@ UI.start = function () {
331 346
     registerListeners();
332 347
 
333 348
     BottomToolbar.init();
334
-    FilmStrip.init();
349
+    FilmStrip.init(eventEmitter);
335 350
 
336 351
     VideoLayout.init(eventEmitter);
337 352
     if (!interfaceConfig.filmStripOnly) {
@@ -470,10 +485,19 @@ UI.initEtherpad = function (name) {
470 485
         return;
471 486
     }
472 487
     console.log('Etherpad is enabled');
473
-    etherpadManager = new EtherpadManager(config.etherpad_base, name);
488
+    etherpadManager
489
+        = new EtherpadManager(config.etherpad_base, name, eventEmitter);
474 490
     Toolbar.showEtherpadButton();
475 491
 };
476 492
 
493
+/**
494
+ * Returns the shared document manager object.
495
+ * @return {EtherpadManager} the shared document manager object
496
+ */
497
+UI.getSharedDocumentManager = function () {
498
+    return etherpadManager;
499
+};
500
+
477 501
 /**
478 502
  * Show user on UI.
479 503
  * @param {string} id user id
@@ -541,6 +565,7 @@ UI.updateLocalRole = function (isModerator) {
541 565
     Toolbar.showRecordingButton(isModerator);
542 566
     Toolbar.showSharedVideoButton(isModerator);
543 567
     SettingsMenu.showStartMutedOptions(isModerator);
568
+    SettingsMenu.showFollowMeOptions(isModerator);
544 569
 
545 570
     if (isModerator) {
546 571
         messageHandler.notify(null, "notify.me", 'connected', "notify.moderator");
@@ -589,7 +614,8 @@ UI.toggleSmileys = function () {
589 614
  * Toggles film strip.
590 615
  */
591 616
 UI.toggleFilmStrip = function () {
592
-    FilmStrip.toggleFilmStrip();
617
+    var self = FilmStrip;
618
+    self.toggleFilmStrip.apply(self, arguments);
593 619
 };
594 620
 
595 621
 /**
@@ -677,10 +703,26 @@ UI.setVideoMuted = function (id, muted) {
677 703
     }
678 704
 };
679 705
 
706
+/**
707
+ * Adds a listener that would be notified on the given type of event.
708
+ *
709
+ * @param type the type of the event we're listening for
710
+ * @param listener a function that would be called when notified
711
+ */
680 712
 UI.addListener = function (type, listener) {
681 713
     eventEmitter.on(type, listener);
682 714
 };
683 715
 
716
+/**
717
+ * Removes the given listener for the given type of event.
718
+ *
719
+ * @param type the type of the event we're listening for
720
+ * @param listener the listener we want to remove
721
+ */
722
+UI.removeListener = function (type, listener) {
723
+    eventEmitter.removeListener(type, listener);
724
+};
725
+
684 726
 UI.clickOnVideo = function (videoNumber) {
685 727
     var remoteVideos = $(".videocontainer:not(#mixedstream)");
686 728
     if (remoteVideos.length > videoNumber) {

+ 12
- 4
modules/UI/etherpad/Etherpad.js Прегледај датотеку

@@ -3,6 +3,7 @@
3 3
 import VideoLayout from "../videolayout/VideoLayout";
4 4
 import LargeContainer from '../videolayout/LargeContainer';
5 5
 import UIUtil from "../util/UIUtil";
6
+import UIEvents from "../../../service/UI/UIEvents";
6 7
 import SidePanelToggler from "../side_pannels/SidePanelToggler";
7 8
 import FilmStrip from '../videolayout/FilmStrip';
8 9
 
@@ -58,6 +59,7 @@ const ETHERPAD_CONTAINER_TYPE = "etherpad";
58 59
  * Container for Etherpad iframe.
59 60
  */
60 61
 class Etherpad extends LargeContainer {
62
+
61 63
     constructor (domain, name) {
62 64
         super();
63 65
 
@@ -149,13 +151,14 @@ class Etherpad extends LargeContainer {
149 151
  * Manager of the Etherpad frame.
150 152
  */
151 153
 export default class EtherpadManager {
152
-    constructor (domain, name) {
154
+    constructor (domain, name, eventEmitter) {
153 155
         if (!domain || !name) {
154 156
             throw new Error("missing domain or name");
155 157
         }
156 158
 
157 159
         this.domain = domain;
158 160
         this.name = name;
161
+        this.eventEmitter = eventEmitter;
159 162
         this.etherpad = null;
160 163
     }
161 164
 
@@ -163,6 +166,10 @@ export default class EtherpadManager {
163 166
         return !!this.etherpad;
164 167
     }
165 168
 
169
+    isVisible() {
170
+        return VideoLayout.isLargeContainerTypeVisible(ETHERPAD_CONTAINER_TYPE);
171
+    }
172
+
166 173
     /**
167 174
      * Create new Etherpad frame.
168 175
      */
@@ -183,11 +190,12 @@ export default class EtherpadManager {
183 190
             this.openEtherpad();
184 191
         }
185 192
 
186
-        let isVisible = VideoLayout.isLargeContainerTypeVisible(
187
-            ETHERPAD_CONTAINER_TYPE
188
-        );
193
+        let isVisible = this.isVisible();
189 194
 
190 195
         VideoLayout.showLargeVideoContainer(
191 196
             ETHERPAD_CONTAINER_TYPE, !isVisible);
197
+
198
+        this.eventEmitter
199
+            .emit(UIEvents.TOGGLED_SHARED_DOCUMENT, !isVisible);
192 200
     }
193 201
 }

+ 2
- 2
modules/UI/shared_video/SharedVideo.js Прегледај датотеку

@@ -120,7 +120,7 @@ export default class SharedVideoManager {
120 120
 
121 121
             VideoLayout.addLargeVideoContainer(
122 122
                 SHARED_VIDEO_CONTAINER_TYPE, self.sharedVideo);
123
-            VideoLayout.handleVideoThumbClicked(true, self.url);
123
+            VideoLayout.handleVideoThumbClicked(self.url);
124 124
 
125 125
             self.isSharedVideoShown = true;
126 126
 
@@ -372,7 +372,7 @@ SharedVideoThumb.prototype.createContainer = function (spanId) {
372 372
  * The thumb click handler.
373 373
  */
374 374
 SharedVideoThumb.prototype.videoClick = function () {
375
-    VideoLayout.handleVideoThumbClicked(true, this.url);
375
+    VideoLayout.handleVideoThumbClicked(this.url);
376 376
 };
377 377
 
378 378
 /**

+ 21
- 0
modules/UI/side_pannels/settings/SettingsMenu.js Прегледај датотеку

@@ -89,6 +89,14 @@ export default {
89 89
             );
90 90
         });
91 91
 
92
+        // FOLLOW ME
93
+        $("#followMeOptions").change(function () {
94
+            let isFollowMeEnabled = $("#followMeCheckBox").is(":checked");
95
+            emitter.emit(
96
+                UIEvents.FOLLOW_ME_ENABLED,
97
+                isFollowMeEnabled
98
+            );
99
+        });
92 100
 
93 101
         // LANGUAGES BOX
94 102
         let languagesBox = $("#languages_selectbox");
@@ -135,6 +143,19 @@ export default {
135 143
         $("#startVideoMuted").attr("checked", startVideoMuted);
136 144
     },
137 145
 
146
+    /**
147
+     * Shows/hides the follow me options in the settings dialog.
148
+     *
149
+     * @param {boolean} show {true} to show those options, {false} to hide them
150
+     */
151
+    showFollowMeOptions (show) {
152
+        if (show) {
153
+            $("#followMeOptions").css("display", "block");
154
+        } else {
155
+            $("#followMeOptions").css("display", "none");
156
+        }
157
+    },
158
+
138 159
     /**
139 160
      * Check if settings menu is visible or not.
140 161
      * @returns {boolean}

+ 30
- 2
modules/UI/videolayout/FilmStrip.js Прегледај датотеку

@@ -1,16 +1,44 @@
1 1
 /* global $, APP, interfaceConfig, config*/
2 2
 
3
+import UIEvents from "../../../service/UI/UIEvents";
3 4
 import UIUtil from "../util/UIUtil";
4 5
 
5 6
 const thumbAspectRatio = 1 / 1;
6 7
 
7 8
 const FilmStrip = {
8
-    init () {
9
+    /**
10
+     *
11
+     * @param eventEmitter the {EventEmitter} through which {FilmStrip} is to
12
+     * emit/fire {UIEvents} (such as {UIEvents.TOGGLED_FILM_STRIP}).
13
+     */
14
+    init (eventEmitter) {
9 15
         this.filmStrip = $('#remoteVideos');
16
+        this.eventEmitter = eventEmitter;
10 17
     },
11 18
 
12
-    toggleFilmStrip () {
19
+    /**
20
+     * Toggles the visibility of the film strip.
21
+     *
22
+     * @param visible optional {Boolean} which specifies the desired visibility
23
+     * of the film strip. If not specified, the visibility will be flipped
24
+     * (i.e. toggled); otherwise, the visibility will be set to the specified
25
+     * value.
26
+     */
27
+    toggleFilmStrip (visible) {
28
+        if (typeof visible === 'boolean'
29
+                && this.isFilmStripVisible() == visible) {
30
+            return;
31
+        }
32
+
13 33
         this.filmStrip.toggleClass("hidden");
34
+
35
+        // Emit/fire UIEvents.TOGGLED_FILM_STRIP.
36
+        var eventEmitter = this.eventEmitter;
37
+        if (eventEmitter) {
38
+            eventEmitter.emit(
39
+                    UIEvents.TOGGLED_FILM_STRIP,
40
+                    this.isFilmStripVisible());
41
+        }
14 42
     },
15 43
 
16 44
     isFilmStripVisible () {

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

@@ -158,7 +158,7 @@ LocalVideo.prototype.changeVideo = function (stream) {
158 158
         if (event.stopPropagation) {
159 159
             event.stopPropagation();
160 160
         }
161
-        this.VideoLayout.handleVideoThumbClicked(true, this.id);
161
+        this.VideoLayout.handleVideoThumbClicked(this.id);
162 162
     };
163 163
 
164 164
     let localVideoContainerSelector = $('#localVideoContainer');

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

@@ -220,7 +220,7 @@ RemoteVideo.prototype.addRemoteStreamElement = function (stream) {
220 220
 
221 221
         // ignore click if it was done in popup menu
222 222
         if ($(source).parents('.popupmenu').length === 0) {
223
-            this.VideoLayout.handleVideoThumbClicked(false, this.id);
223
+            this.VideoLayout.handleVideoThumbClicked(this.id);
224 224
         }
225 225
 
226 226
         // On IE we need to populate this handler on video <object>

+ 8
- 0
modules/UI/videolayout/SmallVideo.js Прегледај датотеку

@@ -20,6 +20,14 @@ function setVisibility(selector, show) {
20 20
     }
21 21
 }
22 22
 
23
+/**
24
+ * Returns the identifier of this small video.
25
+ *
26
+ * @returns the identifier of this small video
27
+ */
28
+SmallVideo.prototype.getId = function () {
29
+    return this.id;
30
+};
23 31
 
24 32
 /* Indicates if this small video is currently visible.
25 33
  *

+ 37
- 31
modules/UI/videolayout/VideoLayout.js Прегледај датотеку

@@ -31,7 +31,7 @@ var eventEmitter = null;
31 31
  * Currently focused video jid
32 32
  * @type {String}
33 33
  */
34
-var focusedVideoResourceJid = null;
34
+var pinnedId = null;
35 35
 
36 36
 /**
37 37
  * On contact list item clicked.
@@ -49,7 +49,7 @@ function onContactClicked (id) {
49 49
         if (remoteVideo.hasVideoStarted()) {
50 50
             // We have a video src, great! Let's update the large video
51 51
             // now.
52
-            VideoLayout.handleVideoThumbClicked(false, id);
52
+            VideoLayout.handleVideoThumbClicked(id);
53 53
         } else {
54 54
 
55 55
             // If we don't have a video src for jid, there's absolutely
@@ -63,7 +63,7 @@ function onContactClicked (id) {
63 63
             // picked up later by the lastN changed event handler.
64 64
 
65 65
             lastNPickupId = id;
66
-            eventEmitter.emit(UIEvents.PINNED_ENDPOINT, id);
66
+            eventEmitter.emit(UIEvents.PINNED_ENDPOINT, remoteVideo, true);
67 67
         }
68 68
     }
69 69
 }
@@ -209,7 +209,7 @@ var VideoLayout = {
209 209
 
210 210
         // We'll show user's avatar if he is the dominant speaker or if
211 211
         // his video thumbnail is pinned
212
-        if (remoteVideos[id] && (id === focusedVideoResourceJid
212
+        if (remoteVideos[id] && (id === pinnedId
213 213
                                 || id === currentDominantSpeaker)) {
214 214
             newId = id;
215 215
         } else {
@@ -289,20 +289,33 @@ var VideoLayout = {
289 289
         return smallVideo ? smallVideo.getVideoType() : null;
290 290
     },
291 291
 
292
-    handleVideoThumbClicked (noPinnedEndpointChangedEvent,
293
-                                          resourceJid) {
294
-        if(focusedVideoResourceJid) {
292
+    isPinned (id) {
293
+        return (pinnedId) ? (id === pinnedId) : false;
294
+    },
295
+
296
+    getPinnedId () {
297
+        return pinnedId;
298
+    },
299
+
300
+    /**
301
+     * Handles the click on a video thumbnail.
302
+     *
303
+     * @param id the identifier of the video thumbnail
304
+     */
305
+    handleVideoThumbClicked (id) {
306
+        if(pinnedId) {
295 307
             var oldSmallVideo
296
-                    = VideoLayout.getSmallVideo(focusedVideoResourceJid);
308
+                    = VideoLayout.getSmallVideo(pinnedId);
297 309
             if (oldSmallVideo && !interfaceConfig.filmStripOnly)
298 310
                 oldSmallVideo.focus(false);
299 311
         }
300 312
 
301
-        var smallVideo = VideoLayout.getSmallVideo(resourceJid);
302
-        // Unlock current focused.
303
-        if (focusedVideoResourceJid === resourceJid)
313
+        var smallVideo = VideoLayout.getSmallVideo(id);
314
+
315
+        // Unpin if currently pinned.
316
+        if (pinnedId === id)
304 317
         {
305
-            focusedVideoResourceJid = null;
318
+            pinnedId = null;
306 319
             // Enable the currently set dominant speaker.
307 320
             if (currentDominantSpeaker) {
308 321
                 if(smallVideo && smallVideo.hasVideo()) {
@@ -310,26 +323,23 @@ var VideoLayout = {
310 323
                 }
311 324
             }
312 325
 
313
-            if (!noPinnedEndpointChangedEvent) {
314
-                eventEmitter.emit(UIEvents.PINNED_ENDPOINT);
315
-            }
326
+            eventEmitter.emit(UIEvents.PINNED_ENDPOINT, smallVideo, false);
327
+
316 328
             return;
317 329
         }
318 330
 
319 331
         // Lock new video
320
-        focusedVideoResourceJid = resourceJid;
332
+        pinnedId = id;
321 333
 
322 334
         // Update focused/pinned interface.
323
-        if (resourceJid) {
335
+        if (id) {
324 336
             if (smallVideo && !interfaceConfig.filmStripOnly)
325 337
                 smallVideo.focus(true);
326 338
 
327
-            if (!noPinnedEndpointChangedEvent) {
328
-                eventEmitter.emit(UIEvents.PINNED_ENDPOINT, resourceJid);
329
-            }
339
+            eventEmitter.emit(UIEvents.PINNED_ENDPOINT, smallVideo, true);
330 340
         }
331 341
 
332
-        this.updateLargeVideo(resourceJid);
342
+        this.updateLargeVideo(id);
333 343
     },
334 344
 
335 345
     /**
@@ -372,10 +382,10 @@ var VideoLayout = {
372 382
         // Update the large video to the last added video only if there's no
373 383
         // current dominant, focused speaker or update it to
374 384
         // the current dominant speaker.
375
-        if ((!focusedVideoResourceJid &&
385
+        if ((!pinnedId &&
376 386
             !currentDominantSpeaker &&
377 387
             this.isLargeContainerTypeVisible(VIDEO_CONTAINER_TYPE)) ||
378
-            focusedVideoResourceJid === resourceJid ||
388
+            pinnedId === resourceJid ||
379 389
             (resourceJid &&
380 390
                 currentDominantSpeaker === resourceJid)) {
381 391
             this.updateLargeVideo(resourceJid, true);
@@ -531,7 +541,7 @@ var VideoLayout = {
531 541
         // since we don't want to switch to local video.
532 542
         // Update the large video if the video source is already available,
533 543
         // otherwise wait for the "videoactive.jingle" event.
534
-        if (!focusedVideoResourceJid
544
+        if (!pinnedId
535 545
             && remoteVideo.hasVideoStarted()
536 546
             && !this.getCurrentlyOnLargeContainer().stayOnStage()) {
537 547
             this.updateLargeVideo(id);
@@ -650,11 +660,7 @@ var VideoLayout = {
650 660
                         // Clean up the lastN pickup id.
651 661
                         lastNPickupId = null;
652 662
 
653
-                        // Don't fire the events again, they've already
654
-                        // been fired in the contact list click handler.
655
-                        VideoLayout.handleVideoThumbClicked(
656
-                            false,
657
-                            resourceJid);
663
+                        VideoLayout.handleVideoThumbClicked(resourceJid);
658 664
 
659 665
                         updateLargeVideo = false;
660 666
                     }
@@ -741,9 +747,9 @@ var VideoLayout = {
741 747
 
742 748
     removeParticipantContainer (id) {
743 749
         // Unlock large video
744
-        if (focusedVideoResourceJid === id) {
750
+        if (pinnedId === id) {
745 751
             console.info("Focused video owner has left the conference");
746
-            focusedVideoResourceJid = null;
752
+            pinnedId = null;
747 753
         }
748 754
 
749 755
         if (currentDominantSpeaker === id) {

+ 27
- 2
service/UI/UIEvents.js Прегледај датотеку

@@ -37,14 +37,39 @@ export default {
37 37
     TOGGLE_CHAT: "UI.toggle_chat",
38 38
     TOGGLE_SETTINGS: "UI.toggle_settings",
39 39
     TOGGLE_CONTACT_LIST: "UI.toggle_contact_list",
40
+    /**
41
+     * Notifies that a command to toggle the film strip has been issued. The
42
+     * event may optionally specify a {Boolean} (primitive) value to assign to
43
+     * the visibility of the film strip (i.e. the event may act as a setter).
44
+     * The very toggling of the film strip may or may not occurred at the time
45
+     * of the receipt of the event depending on the position of the receiving
46
+     * event listener in relation to the event listener which carries out the
47
+     * command to toggle the film strip.
48
+     *
49
+     * @see {TOGGLED_FILM_STRIP}
50
+     */
40 51
     TOGGLE_FILM_STRIP: "UI.toggle_film_strip",
52
+    /**
53
+     * Notifies that the film strip was (actually) toggled. The event supplies
54
+     * a {Boolean} (primitive) value indicating the visibility of the film
55
+     * strip after the toggling (at the time of the event emission).
56
+     *
57
+     * @see {TOGGLE_FILM_STRIP}
58
+     */
59
+    TOGGLED_FILM_STRIP: "UI.toggled_film_strip",
41 60
     TOGGLE_SCREENSHARING: "UI.toggle_screensharing",
61
+    TOGGLED_SHARED_DOCUMENT: "UI.toggled_shared_document",
42 62
     CONTACT_CLICKED: "UI.contact_clicked",
43 63
     HANGUP: "UI.hangup",
44 64
     LOGOUT: "UI.logout",
45 65
     RECORDING_TOGGLE: "UI.recording_toggle",
46 66
     SIP_DIAL: "UI.sip_dial",
47
-    SUBEJCT_CHANGED: "UI.subject_changed",
67
+    SUBJECT_CHANGED: "UI.subject_changed",
48 68
     VIDEO_DEVICE_CHANGED: "UI.video_device_changed",
49
-    AUDIO_DEVICE_CHANGED: "UI.audio_device_changed"
69
+    AUDIO_DEVICE_CHANGED: "UI.audio_device_changed",
70
+    /**
71
+     * Notifies interested listeners that the follow-me feature is enabled or
72
+     * disabled.
73
+     */
74
+    FOLLOW_ME_ENABLED: "UI.follow_me_enabled"
50 75
 };

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