浏览代码

Extends the follow-me feature by adding the possibility to follow the pinned participant, the shared video and the shared document. Adds the possibility to enable disable follow-me from the settings panel. Some other small fixes throughout the UI.

j8
yanas 9 年前
父节点
当前提交
cc761700fe

+ 16
- 2
conference.js 查看文件

31
     SHARED_VIDEO: "shared-video"
31
     SHARED_VIDEO: "shared-video"
32
 };
32
 };
33
 
33
 
34
+import {VIDEO_CONTAINER_TYPE} from "./modules/UI/videolayout/LargeVideo";
35
+
34
 /**
36
 /**
35
  * Open Connection. When authentication failed it shows auth dialog.
37
  * Open Connection. When authentication failed it shows auth dialog.
36
  * @param roomName the room name to use
38
  * @param roomName the room name to use
1030
         APP.UI.addListener(UIEvents.SELECTED_ENDPOINT, (id) => {
1032
         APP.UI.addListener(UIEvents.SELECTED_ENDPOINT, (id) => {
1031
             room.selectParticipant(id);
1033
             room.selectParticipant(id);
1032
         });
1034
         });
1033
-        APP.UI.addListener(UIEvents.PINNED_ENDPOINT, (id) => {
1034
-            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);
1035
         });
1049
         });
1036
 
1050
 
1037
         APP.UI.addListener(
1051
         APP.UI.addListener(

+ 7
- 11
css/settingsmenu.css 查看文件

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

+ 6
- 0
index.html 查看文件

240
                     <select id="selectMic"></select>
240
                     <select id="selectMic"></select>
241
                 </label>
241
                 </label>
242
             </div>
242
             </div>
243
+            <div id="followMeOptions">
244
+                <label class = "followMeLabel">
245
+                    <input type="checkbox" id="followMeCheckBox">
246
+                    <span  data-i18n="settings.followMe"></span>
247
+                </label>
248
+            </div>
243
             <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>
249
             <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>
244
         </div>
250
         </div>
245
         <div class="feedbackButton">
251
         <div class="feedbackButton">

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

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

+ 181
- 49
modules/FollowMe.js 查看文件

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.                                               
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
  */
15
  */
16
 
16
 
17
 import UIEvents from '../service/UI/UIEvents';
17
 import UIEvents from '../service/UI/UIEvents';
18
+import VideoLayout from './UI/videolayout/VideoLayout';
19
+import FilmStrip from './UI/videolayout/FilmStrip';
18
 
20
 
19
 /**
21
 /**
20
  * The (name of the) command which transports the state (represented by
22
  * The (name of the) command which transports the state (represented by
21
  * {State} for the local state at the time of this writing) of a {FollowMe}
23
  * {State} for the local state at the time of this writing) of a {FollowMe}
22
  * (instance) between participants.
24
  * (instance) between participants.
23
  */
25
  */
24
-/* private */ const _COMMAND = "follow-me";
26
+const _COMMAND = "follow-me";
25
 
27
 
26
 /**
28
 /**
27
  * Represents the set of {FollowMe}-related states (properties and their
29
  * Represents the set of {FollowMe}-related states (properties and their
29
  * will send {_COMMAND} whenever a property of {State} changes (if the local
31
  * will send {_COMMAND} whenever a property of {State} changes (if the local
30
  * participant is in her right to issue such a command, of course).
32
  * participant is in her right to issue such a command, of course).
31
  */
33
  */
32
-/* private */ class State {
34
+class State {
33
     /**
35
     /**
34
      * Initializes a new {State} instance.
36
      * Initializes a new {State} instance.
35
      *
37
      *
39
      * the property, the old value of the property before the change, and the
41
      * the property, the old value of the property before the change, and the
40
      * new value of the property after the change.
42
      * new value of the property after the change.
41
      */
43
      */
42
-    /* public */ constructor (propertyChangeCallback) {
43
-        /* private*/ this._propertyChangeCallback = propertyChangeCallback;
44
+    constructor (propertyChangeCallback) {
45
+        this._propertyChangeCallback = propertyChangeCallback;
44
     }
46
     }
45
 
47
 
46
-    /* public */ get filmStripVisible () { return this._filmStripVisible; }
48
+    get filmStripVisible () { return this._filmStripVisible; }
47
 
49
 
48
-    /* public */ set filmStripVisible (b) {
50
+    set filmStripVisible (b) {
49
         var oldValue = this._filmStripVisible;
51
         var oldValue = this._filmStripVisible;
50
         if (oldValue !== b) {
52
         if (oldValue !== b) {
51
             this._filmStripVisible = b;
53
             this._filmStripVisible = b;
53
         }
55
         }
54
     }
56
     }
55
 
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
+
56
     /**
78
     /**
57
      * Invokes {_propertyChangeCallback} to notify it that {property} had its
79
      * Invokes {_propertyChangeCallback} to notify it that {property} had its
58
      * value changed from {oldValue} to {newValue}.
80
      * value changed from {oldValue} to {newValue}.
62
      * @param oldValue the value of {property} before the change
84
      * @param oldValue the value of {property} before the change
63
      * @param newValue the value of {property} after the change
85
      * @param newValue the value of {property} after the change
64
      */
86
      */
65
-    /* private */ _firePropertyChange (property, oldValue, newValue) {
87
+    _firePropertyChange (property, oldValue, newValue) {
66
         var propertyChangeCallback = this._propertyChangeCallback;
88
         var propertyChangeCallback = this._propertyChangeCallback;
67
         if (propertyChangeCallback)
89
         if (propertyChangeCallback)
68
             propertyChangeCallback(property, oldValue, newValue);
90
             propertyChangeCallback(property, oldValue, newValue);
74
  * (partially) control the user experience/interface (e.g. film strip
96
  * (partially) control the user experience/interface (e.g. film strip
75
  * visibility) of (other) non-moderator particiapnts.
97
  * visibility) of (other) non-moderator particiapnts.
76
  *
98
  *
77
- * @author Lyubomir Marinov 
99
+ * @author Lyubomir Marinov
78
  */
100
  */
79
-/* public */ class FollowMe {
101
+class FollowMe {
80
     /**
102
     /**
81
      * Initializes a new {FollowMe} instance.
103
      * Initializes a new {FollowMe} instance.
82
      *
104
      *
87
      * destination (model/state) to receive from the remote moderator if the
109
      * destination (model/state) to receive from the remote moderator if the
88
      * local participant is not the moderator
110
      * local participant is not the moderator
89
      */
111
      */
90
-    /* public */ constructor (conference, UI) {
91
-        /* private */ this._conference = conference;
92
-        /* private */ this._UI = UI;
112
+    constructor (conference, UI) {
113
+        this._conference = conference;
114
+        this._UI = UI;
93
 
115
 
94
         // The states of the local participant which are to be followed (by the
116
         // The states of the local participant which are to be followed (by the
95
         // remote participants when the local participant is in her right to
117
         // remote participants when the local participant is in her right to
96
         // issue such commands).
118
         // issue such commands).
97
-        /* private */ this._local
98
-            = new State(this._localPropertyChange.bind(this));
119
+        this._local = new State(this._localPropertyChange.bind(this));
99
 
120
 
100
         // Listen to "Follow Me" commands. I'm not sure whether a moderator can
121
         // Listen to "Follow Me" commands. I'm not sure whether a moderator can
101
         // (in lib-jitsi-meet and/or Meet) become a non-moderator. If that's
122
         // (in lib-jitsi-meet and/or Meet) become a non-moderator. If that's
104
         conference.commands.addCommandListener(
125
         conference.commands.addCommandListener(
105
                 _COMMAND,
126
                 _COMMAND,
106
                 this._onFollowMeCommand.bind(this));
127
                 this._onFollowMeCommand.bind(this));
107
-        // Listen to (user interface) states of the local participant which are
108
-        // to be followed (by the remote participants). A non-moderator (very
109
-        // likely) can become a moderator so it may be easiest to always track
110
-        // the states of interest.
111
-        UI.addListener(
112
-                UIEvents.TOGGLED_FILM_STRIP,
113
-                this._filmStripToggled.bind(this));
114
-        // TODO Listen to changes in the moderator role of the local
115
-        // participant. When the local participant is granted the moderator
116
-        // role, it may need to start sending "Follow Me" commands. Obviously,
117
-        // this depends on how we decide to enable the feature at runtime as
118
-        // well.
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();
119
     }
179
     }
120
 
180
 
121
     /**
181
     /**
125
      * @param filmStripVisible {Boolean} {true} if the film strip was shown (as
185
      * @param filmStripVisible {Boolean} {true} if the film strip was shown (as
126
      * a result of the toggle) or {false} if the film strip was hidden
186
      * a result of the toggle) or {false} if the film strip was hidden
127
      */
187
      */
128
-    /* private */ _filmStripToggled (filmStripVisible) {
188
+    _filmStripToggled (filmStripVisible) {
129
         this._local.filmStripVisible = filmStripVisible;
189
         this._local.filmStripVisible = filmStripVisible;
130
     }
190
     }
131
 
191
 
132
-    /* private */ _localPropertyChange (property, oldValue, newValue) {
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) {
133
         // Only a moderator is allowed to send commands.
231
         // Only a moderator is allowed to send commands.
134
         var conference = this._conference;
232
         var conference = this._conference;
135
         if (!conference.isModerator)
233
         if (!conference.isModerator)
141
         // sendCommand!
239
         // sendCommand!
142
         commands.removeCommand(_COMMAND);
240
         commands.removeCommand(_COMMAND);
143
         var self = this;
241
         var self = this;
144
-        commands.sendCommand(
242
+        commands.sendCommandOnce(
145
                 _COMMAND,
243
                 _COMMAND,
146
                 {
244
                 {
147
                     attributes: {
245
                     attributes: {
148
                         filmStripVisible: self._local.filmStripVisible,
246
                         filmStripVisible: self._local.filmStripVisible,
149
-                    },
247
+                        nextOnStage: self._local.nextOnStage,
248
+                        sharedDocumentVisible: self._local.sharedDocumentVisible
249
+                    }
150
                 });
250
                 });
151
     }
251
     }
152
 
252
 
159
      * notable idiosyncrasy of the Command(s) API to be mindful of here is that
259
      * notable idiosyncrasy of the Command(s) API to be mindful of here is that
160
      * the command may be issued by the local participant.
260
      * the command may be issued by the local participant.
161
      */
261
      */
162
-    /* private */ _onFollowMeCommand ({ attributes }, id) {
262
+    _onFollowMeCommand ({ attributes }, id) {
163
         // We require to know who issued the command because (1) only a
263
         // We require to know who issued the command because (1) only a
164
         // moderator is allowed to send commands and (2) a command MUST be
264
         // moderator is allowed to send commands and (2) a command MUST be
165
         // issued by a defined commander.
265
         // issued by a defined commander.
169
         // to act upon them.
269
         // to act upon them.
170
         if (this._conference.isLocalId(id))
270
         if (this._conference.isLocalId(id))
171
             return;
271
             return;
272
+
172
         // TODO Don't obey commands issued by non-moderators.
273
         // TODO Don't obey commands issued by non-moderators.
173
 
274
 
174
-        // Apply the received/remote command to the user experience/interface
275
+        // Applies the received/remote command to the user experience/interface
175
         // of the local participant.
276
         // of the local participant.
277
+        this._onFilmStripVisible(attributes.filmStripVisible);
278
+        this._onNextOnStage(attributes.nextOnStage);
279
+        this._onSharedDocumentVisible(attributes.sharedDocumentVisible);
280
+    }
176
 
281
 
177
-        // filmStripVisible
178
-        var filmStripVisible = attributes.filmStripVisible;
282
+    _onFilmStripVisible(filmStripVisible) {
179
         if (typeof filmStripVisible !== 'undefined') {
283
         if (typeof filmStripVisible !== 'undefined') {
180
             // XXX The Command(s) API doesn't preserve the types (of
284
             // XXX The Command(s) API doesn't preserve the types (of
181
             // attributes, at least) at the time of this writing so take into
285
             // attributes, at least) at the time of this writing so take into
182
             // account that what originated as a Boolean may be a String on
286
             // account that what originated as a Boolean may be a String on
183
             // receipt.
287
             // receipt.
184
             filmStripVisible = (filmStripVisible == 'true');
288
             filmStripVisible = (filmStripVisible == 'true');
289
+
185
             // FIXME The UI (module) very likely doesn't (want to) expose its
290
             // FIXME The UI (module) very likely doesn't (want to) expose its
186
             // eventEmitter as a public field. I'm not sure at the time of this
291
             // eventEmitter as a public field. I'm not sure at the time of this
187
             // writing whether calling UI.toggleFilmStrip() is acceptable (from
292
             // writing whether calling UI.toggleFilmStrip() is acceptable (from
188
             // a design standpoint) either.
293
             // a design standpoint) either.
189
-            this._UI.eventEmitter.emit(
294
+            if (filmStripVisible !== FilmStrip.isFilmStripVisible())
295
+                this._UI.eventEmitter.emit(
190
                     UIEvents.TOGGLE_FILM_STRIP,
296
                     UIEvents.TOGGLE_FILM_STRIP,
191
                     filmStripVisible);
297
                     filmStripVisible);
192
         }
298
         }
193
     }
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
+    }
194
 }
326
 }
195
 
327
 
196
 export default FollowMe;
328
 export default FollowMe;

+ 35
- 2
modules/UI/UI.js 查看文件

35
 let etherpadManager;
35
 let etherpadManager;
36
 let sharedVideoManager;
36
 let sharedVideoManager;
37
 
37
 
38
+let followMeHandler;
39
+
38
 /**
40
 /**
39
  * Prompt user for nickname.
41
  * Prompt user for nickname.
40
  */
42
  */
252
     // other participants' UI. Consequently, it needs (1) read and write access
254
     // other participants' UI. Consequently, it needs (1) read and write access
253
     // to the UI (depending on the moderator role of the local participant) and
255
     // to the UI (depending on the moderator role of the local participant) and
254
     // (2) APP.conference as means of communication between the participants.
256
     // (2) APP.conference as means of communication between the participants.
255
-    new FollowMe(APP.conference, UI);
257
+    followMeHandler = new FollowMe(APP.conference, UI);
256
 };
258
 };
257
 
259
 
258
 UI.mucJoined = function () {
260
 UI.mucJoined = function () {
290
         UI.toggleFilmStrip();
292
         UI.toggleFilmStrip();
291
         VideoLayout.resizeVideoArea(PanelToggler.isVisible(), true, false);
293
         VideoLayout.resizeVideoArea(PanelToggler.isVisible(), true, false);
292
     });
294
     });
295
+
296
+    UI.addListener(UIEvents.FOLLOW_ME_ENABLED, function (isEnabled) {
297
+        if (followMeHandler)
298
+            followMeHandler.enableFollowMe(isEnabled);
299
+    });
293
 }
300
 }
294
 
301
 
295
 /**
302
 /**
478
         return;
485
         return;
479
     }
486
     }
480
     console.log('Etherpad is enabled');
487
     console.log('Etherpad is enabled');
481
-    etherpadManager = new EtherpadManager(config.etherpad_base, name);
488
+    etherpadManager
489
+        = new EtherpadManager(config.etherpad_base, name, eventEmitter);
482
     Toolbar.showEtherpadButton();
490
     Toolbar.showEtherpadButton();
483
 };
491
 };
484
 
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
+
485
 /**
501
 /**
486
  * Show user on UI.
502
  * Show user on UI.
487
  * @param {string} id user id
503
  * @param {string} id user id
549
     Toolbar.showRecordingButton(isModerator);
565
     Toolbar.showRecordingButton(isModerator);
550
     Toolbar.showSharedVideoButton(isModerator);
566
     Toolbar.showSharedVideoButton(isModerator);
551
     SettingsMenu.showStartMutedOptions(isModerator);
567
     SettingsMenu.showStartMutedOptions(isModerator);
568
+    SettingsMenu.showFollowMeOptions(isModerator);
552
 
569
 
553
     if (isModerator) {
570
     if (isModerator) {
554
         messageHandler.notify(null, "notify.me", 'connected', "notify.moderator");
571
         messageHandler.notify(null, "notify.me", 'connected', "notify.moderator");
686
     }
703
     }
687
 };
704
 };
688
 
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
+ */
689
 UI.addListener = function (type, listener) {
712
 UI.addListener = function (type, listener) {
690
     eventEmitter.on(type, listener);
713
     eventEmitter.on(type, listener);
691
 };
714
 };
692
 
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
+
693
 UI.clickOnVideo = function (videoNumber) {
726
 UI.clickOnVideo = function (videoNumber) {
694
     var remoteVideos = $(".videocontainer:not(#mixedstream)");
727
     var remoteVideos = $(".videocontainer:not(#mixedstream)");
695
     if (remoteVideos.length > videoNumber) {
728
     if (remoteVideos.length > videoNumber) {

+ 12
- 4
modules/UI/etherpad/Etherpad.js 查看文件

3
 import VideoLayout from "../videolayout/VideoLayout";
3
 import VideoLayout from "../videolayout/VideoLayout";
4
 import LargeContainer from '../videolayout/LargeContainer';
4
 import LargeContainer from '../videolayout/LargeContainer';
5
 import UIUtil from "../util/UIUtil";
5
 import UIUtil from "../util/UIUtil";
6
+import UIEvents from "../../../service/UI/UIEvents";
6
 import SidePanelToggler from "../side_pannels/SidePanelToggler";
7
 import SidePanelToggler from "../side_pannels/SidePanelToggler";
7
 import FilmStrip from '../videolayout/FilmStrip';
8
 import FilmStrip from '../videolayout/FilmStrip';
8
 
9
 
58
  * Container for Etherpad iframe.
59
  * Container for Etherpad iframe.
59
  */
60
  */
60
 class Etherpad extends LargeContainer {
61
 class Etherpad extends LargeContainer {
62
+
61
     constructor (domain, name) {
63
     constructor (domain, name) {
62
         super();
64
         super();
63
 
65
 
149
  * Manager of the Etherpad frame.
151
  * Manager of the Etherpad frame.
150
  */
152
  */
151
 export default class EtherpadManager {
153
 export default class EtherpadManager {
152
-    constructor (domain, name) {
154
+    constructor (domain, name, eventEmitter) {
153
         if (!domain || !name) {
155
         if (!domain || !name) {
154
             throw new Error("missing domain or name");
156
             throw new Error("missing domain or name");
155
         }
157
         }
156
 
158
 
157
         this.domain = domain;
159
         this.domain = domain;
158
         this.name = name;
160
         this.name = name;
161
+        this.eventEmitter = eventEmitter;
159
         this.etherpad = null;
162
         this.etherpad = null;
160
     }
163
     }
161
 
164
 
163
         return !!this.etherpad;
166
         return !!this.etherpad;
164
     }
167
     }
165
 
168
 
169
+    isVisible() {
170
+        return VideoLayout.isLargeContainerTypeVisible(ETHERPAD_CONTAINER_TYPE);
171
+    }
172
+
166
     /**
173
     /**
167
      * Create new Etherpad frame.
174
      * Create new Etherpad frame.
168
      */
175
      */
183
             this.openEtherpad();
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
         VideoLayout.showLargeVideoContainer(
195
         VideoLayout.showLargeVideoContainer(
191
             ETHERPAD_CONTAINER_TYPE, !isVisible);
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
 
120
 
121
             VideoLayout.addLargeVideoContainer(
121
             VideoLayout.addLargeVideoContainer(
122
                 SHARED_VIDEO_CONTAINER_TYPE, self.sharedVideo);
122
                 SHARED_VIDEO_CONTAINER_TYPE, self.sharedVideo);
123
-            VideoLayout.handleVideoThumbClicked(true, self.url);
123
+            VideoLayout.handleVideoThumbClicked(self.url);
124
 
124
 
125
             self.isSharedVideoShown = true;
125
             self.isSharedVideoShown = true;
126
 
126
 
372
  * The thumb click handler.
372
  * The thumb click handler.
373
  */
373
  */
374
 SharedVideoThumb.prototype.videoClick = function () {
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
             );
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
         // LANGUAGES BOX
101
         // LANGUAGES BOX
94
         let languagesBox = $("#languages_selectbox");
102
         let languagesBox = $("#languages_selectbox");
135
         $("#startVideoMuted").attr("checked", startVideoMuted);
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
      * Check if settings menu is visible or not.
160
      * Check if settings menu is visible or not.
140
      * @returns {boolean}
161
      * @returns {boolean}

+ 1
- 1
modules/UI/videolayout/LocalVideo.js 查看文件

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

+ 1
- 1
modules/UI/videolayout/RemoteVideo.js 查看文件

220
 
220
 
221
         // ignore click if it was done in popup menu
221
         // ignore click if it was done in popup menu
222
         if ($(source).parents('.popupmenu').length === 0) {
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
         // On IE we need to populate this handler on video <object>
226
         // On IE we need to populate this handler on video <object>

+ 8
- 0
modules/UI/videolayout/SmallVideo.js 查看文件

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
 /* Indicates if this small video is currently visible.
32
 /* Indicates if this small video is currently visible.
25
  *
33
  *

+ 37
- 31
modules/UI/videolayout/VideoLayout.js 查看文件

31
  * Currently focused video jid
31
  * Currently focused video jid
32
  * @type {String}
32
  * @type {String}
33
  */
33
  */
34
-var focusedVideoResourceJid = null;
34
+var pinnedId = null;
35
 
35
 
36
 /**
36
 /**
37
  * On contact list item clicked.
37
  * On contact list item clicked.
49
         if (remoteVideo.hasVideoStarted()) {
49
         if (remoteVideo.hasVideoStarted()) {
50
             // We have a video src, great! Let's update the large video
50
             // We have a video src, great! Let's update the large video
51
             // now.
51
             // now.
52
-            VideoLayout.handleVideoThumbClicked(false, id);
52
+            VideoLayout.handleVideoThumbClicked(id);
53
         } else {
53
         } else {
54
 
54
 
55
             // If we don't have a video src for jid, there's absolutely
55
             // If we don't have a video src for jid, there's absolutely
63
             // picked up later by the lastN changed event handler.
63
             // picked up later by the lastN changed event handler.
64
 
64
 
65
             lastNPickupId = id;
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
 
209
 
210
         // We'll show user's avatar if he is the dominant speaker or if
210
         // We'll show user's avatar if he is the dominant speaker or if
211
         // his video thumbnail is pinned
211
         // his video thumbnail is pinned
212
-        if (remoteVideos[id] && (id === focusedVideoResourceJid
212
+        if (remoteVideos[id] && (id === pinnedId
213
                                 || id === currentDominantSpeaker)) {
213
                                 || id === currentDominantSpeaker)) {
214
             newId = id;
214
             newId = id;
215
         } else {
215
         } else {
289
         return smallVideo ? smallVideo.getVideoType() : null;
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
             var oldSmallVideo
307
             var oldSmallVideo
296
-                    = VideoLayout.getSmallVideo(focusedVideoResourceJid);
308
+                    = VideoLayout.getSmallVideo(pinnedId);
297
             if (oldSmallVideo && !interfaceConfig.filmStripOnly)
309
             if (oldSmallVideo && !interfaceConfig.filmStripOnly)
298
                 oldSmallVideo.focus(false);
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
             // Enable the currently set dominant speaker.
319
             // Enable the currently set dominant speaker.
307
             if (currentDominantSpeaker) {
320
             if (currentDominantSpeaker) {
308
                 if(smallVideo && smallVideo.hasVideo()) {
321
                 if(smallVideo && smallVideo.hasVideo()) {
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
             return;
328
             return;
317
         }
329
         }
318
 
330
 
319
         // Lock new video
331
         // Lock new video
320
-        focusedVideoResourceJid = resourceJid;
332
+        pinnedId = id;
321
 
333
 
322
         // Update focused/pinned interface.
334
         // Update focused/pinned interface.
323
-        if (resourceJid) {
335
+        if (id) {
324
             if (smallVideo && !interfaceConfig.filmStripOnly)
336
             if (smallVideo && !interfaceConfig.filmStripOnly)
325
                 smallVideo.focus(true);
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
         // Update the large video to the last added video only if there's no
382
         // Update the large video to the last added video only if there's no
373
         // current dominant, focused speaker or update it to
383
         // current dominant, focused speaker or update it to
374
         // the current dominant speaker.
384
         // the current dominant speaker.
375
-        if ((!focusedVideoResourceJid &&
385
+        if ((!pinnedId &&
376
             !currentDominantSpeaker &&
386
             !currentDominantSpeaker &&
377
             this.isLargeContainerTypeVisible(VIDEO_CONTAINER_TYPE)) ||
387
             this.isLargeContainerTypeVisible(VIDEO_CONTAINER_TYPE)) ||
378
-            focusedVideoResourceJid === resourceJid ||
388
+            pinnedId === resourceJid ||
379
             (resourceJid &&
389
             (resourceJid &&
380
                 currentDominantSpeaker === resourceJid)) {
390
                 currentDominantSpeaker === resourceJid)) {
381
             this.updateLargeVideo(resourceJid, true);
391
             this.updateLargeVideo(resourceJid, true);
531
         // since we don't want to switch to local video.
541
         // since we don't want to switch to local video.
532
         // Update the large video if the video source is already available,
542
         // Update the large video if the video source is already available,
533
         // otherwise wait for the "videoactive.jingle" event.
543
         // otherwise wait for the "videoactive.jingle" event.
534
-        if (!focusedVideoResourceJid
544
+        if (!pinnedId
535
             && remoteVideo.hasVideoStarted()
545
             && remoteVideo.hasVideoStarted()
536
             && !this.getCurrentlyOnLargeContainer().stayOnStage()) {
546
             && !this.getCurrentlyOnLargeContainer().stayOnStage()) {
537
             this.updateLargeVideo(id);
547
             this.updateLargeVideo(id);
650
                         // Clean up the lastN pickup id.
660
                         // Clean up the lastN pickup id.
651
                         lastNPickupId = null;
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
                         updateLargeVideo = false;
665
                         updateLargeVideo = false;
660
                     }
666
                     }
741
 
747
 
742
     removeParticipantContainer (id) {
748
     removeParticipantContainer (id) {
743
         // Unlock large video
749
         // Unlock large video
744
-        if (focusedVideoResourceJid === id) {
750
+        if (pinnedId === id) {
745
             console.info("Focused video owner has left the conference");
751
             console.info("Focused video owner has left the conference");
746
-            focusedVideoResourceJid = null;
752
+            pinnedId = null;
747
         }
753
         }
748
 
754
 
749
         if (currentDominantSpeaker === id) {
755
         if (currentDominantSpeaker === id) {

+ 9
- 3
service/UI/UIEvents.js 查看文件

56
      *
56
      *
57
      * @see {TOGGLE_FILM_STRIP}
57
      * @see {TOGGLE_FILM_STRIP}
58
      */
58
      */
59
-    TOGGLED_FILM_STRIP: "UI.toggled_fim_strip",
59
+    TOGGLED_FILM_STRIP: "UI.toggled_film_strip",
60
     TOGGLE_SCREENSHARING: "UI.toggle_screensharing",
60
     TOGGLE_SCREENSHARING: "UI.toggle_screensharing",
61
+    TOGGLED_SHARED_DOCUMENT: "UI.toggled_shared_document",
61
     CONTACT_CLICKED: "UI.contact_clicked",
62
     CONTACT_CLICKED: "UI.contact_clicked",
62
     HANGUP: "UI.hangup",
63
     HANGUP: "UI.hangup",
63
     LOGOUT: "UI.logout",
64
     LOGOUT: "UI.logout",
64
     RECORDING_TOGGLE: "UI.recording_toggle",
65
     RECORDING_TOGGLE: "UI.recording_toggle",
65
     SIP_DIAL: "UI.sip_dial",
66
     SIP_DIAL: "UI.sip_dial",
66
-    SUBEJCT_CHANGED: "UI.subject_changed",
67
+    SUBJECT_CHANGED: "UI.subject_changed",
67
     VIDEO_DEVICE_CHANGED: "UI.video_device_changed",
68
     VIDEO_DEVICE_CHANGED: "UI.video_device_changed",
68
-    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"
69
 };
75
 };

正在加载...
取消
保存