Parcourir la source

Merge pull request #543 from damencho/shared-video

Shared video, synchronized play/pause/seek/muting/volume, initial commit.
j8
yanas il y a 9 ans
Parent
révision
32c2d912be

+ 45
- 1
conference.js Voir le fichier

@@ -27,7 +27,8 @@ let room, connection, localAudio, localVideo, roomLocker;
27 27
 const Commands = {
28 28
     CONNECTION_QUALITY: "stats",
29 29
     EMAIL: "email",
30
-    ETHERPAD: "etherpad"
30
+    ETHERPAD: "etherpad",
31
+    SHARED_VIDEO: "shared-video"
31 32
 };
32 33
 
33 34
 /**
@@ -689,6 +690,7 @@ export default {
689 690
             console.log('USER %s LEFT', id, user);
690 691
             APP.API.notifyUserLeft(id);
691 692
             APP.UI.removeUser(id, user.getDisplayName());
693
+            APP.UI.stopSharedVideo({from: id});
692 694
         });
693 695
 
694 696
 
@@ -1012,5 +1014,47 @@ export default {
1012 1014
         APP.UI.addListener(
1013 1015
             UIEvents.TOGGLE_SCREENSHARING, this.toggleScreenSharing.bind(this)
1014 1016
         );
1017
+
1018
+        APP.UI.addListener(UIEvents.UPDATE_SHARED_VIDEO,
1019
+            (url, state, time, volume) => {
1020
+            // send start and stop commands once, and remove any updates
1021
+            // that had left
1022
+            if (state === 'stop' || state === 'start' || state === 'playing') {
1023
+                room.removeCommand(Commands.SHARED_VIDEO);
1024
+                room.sendCommandOnce(Commands.SHARED_VIDEO, {
1025
+                    value: url,
1026
+                    attributes: {
1027
+                        from: APP.conference.localId,
1028
+                        state: state,
1029
+                        time: time,
1030
+                        volume: volume
1031
+                    }
1032
+                });
1033
+            }
1034
+            else {
1035
+                // in case of paused, in order to allow late users to join
1036
+                // paused
1037
+                room.sendCommand(Commands.SHARED_VIDEO, {
1038
+                    value: url,
1039
+                    attributes: {
1040
+                        from: APP.conference.localId,
1041
+                        state: state,
1042
+                        time: time,
1043
+                        volume: volume
1044
+                    }
1045
+                });
1046
+            }
1047
+        });
1048
+        room.addCommandListener(
1049
+            Commands.SHARED_VIDEO, ({value, attributes}) => {
1050
+                if (attributes.state === 'stop') {
1051
+                    APP.UI.stopSharedVideo(attributes);
1052
+                } else if (attributes.state === 'start') {
1053
+                    APP.UI.showSharedVideo(value, attributes);
1054
+                } else if (attributes.state === 'playing'
1055
+                    || attributes.state === 'pause') {
1056
+                    APP.UI.updateSharedVideo(value, attributes);
1057
+                }
1058
+            });
1015 1059
     }
1016 1060
 };

+ 1
- 0
css/main.css Voir le fichier

@@ -63,6 +63,7 @@ html, body{
63 63
     text-align: center;
64 64
     text-shadow: 0 1px 0 rgba(255,255,255,.3), 0 -1px 0 rgba(0,0,0,.6);
65 65
     z-index: 1;
66
+    font-size: 1.22em !important;
66 67
 }
67 68
 
68 69
 .toolbar_span>span {

+ 6
- 0
css/videolayout_default.css Voir le fichier

@@ -31,6 +31,7 @@
31 31
     position: relative;
32 32
     margin-left: auto;
33 33
     margin-right: auto;
34
+    text-align: center;
34 35
 }
35 36
 
36 37
 #remoteVideos .videocontainer {
@@ -112,6 +113,7 @@
112 113
 }
113 114
 
114 115
 #presentation,
116
+#sharedVideo,
115 117
 #etherpad,
116 118
 #localVideoWrapper>video,
117 119
 #localVideoWrapper>object,
@@ -436,6 +438,10 @@
436 438
     border-radius: 200px;
437 439
 }
438 440
 
441
+.sharedVideoAvatar {
442
+    height: 100%;
443
+}
444
+
439 445
 .noMic {
440 446
     position: absolute;
441 447
     border-radius: 8px;

+ 2
- 0
index.html Voir le fichier

@@ -123,6 +123,7 @@
123 123
                         <span id="unreadMessages"></span>
124 124
                     </a>
125 125
                     <a class="button icon-share-doc" id="toolbar_button_etherpad" data-container="body" data-toggle="popover" data-placement="bottom" content="Shared document" data-i18n="[content]toolbar.etherpad"></a>
126
+                    <a class="button fa fa-share-alt-square" id="toolbar_button_sharedvideo" data-container="body" data-toggle="popover" data-placement="bottom" content="Shared Video" data-i18n="[content]toolbar.sharedvideo" style="display: none"></a>
126 127
                     <a class="button icon-share-desktop" id="toolbar_button_desktopsharing" data-container="body" data-toggle="popover" data-placement="bottom" shortcut="toggleDesktopSharingPopover" content="Share screen" data-i18n="[content]toolbar.sharescreen" style="display: none"></a>
127 128
                     <a class="button icon-full-screen" id="toolbar_button_fullScreen" data-container="body" data-toggle="popover" data-placement="bottom" content="Enter / Exit Full Screen" data-i18n="[content]toolbar.fullscreen"></a>
128 129
                     <a class="button icon-telephone" id="toolbar_button_sip" data-container="body" data-toggle="popover" data-placement="bottom" content="Call SIP number" data-i18n="[content]toolbar.sip" style="display: none"></a>
@@ -137,6 +138,7 @@
137 138
 
138 139
             <div id="largeVideoContainer" class="videocontainer">
139 140
                 <div id="presentation"></div>
141
+                <div id="sharedVideo"><div id="sharedVideoIFrame"></div></div>
140 142
                 <div id="etherpad"></div>
141 143
                 <a target="_new"><div class="watermark leftwatermark"></div></a>
142 144
                 <a target="_new"><div class="watermark rightwatermark"></div></a>

+ 1
- 1
interface_config.js Voir le fichier

@@ -16,7 +16,7 @@ var interfaceConfig = {
16 16
     INVITATION_POWERED_BY: true,
17 17
     DOMINANT_SPEAKER_AVATAR_SIZE: 100,
18 18
     TOOLBAR_BUTTONS: ['authentication', 'microphone', 'camera', 'desktop',
19
-        'recording', 'security', 'invite', 'chat', 'etherpad',
19
+        'recording', 'security', 'invite', 'chat', 'etherpad', 'sharedvideo',
20 20
         'fullscreen', 'sip', 'dialpad', 'settings', 'hangup', 'filmstrip',
21 21
         'contacts'],
22 22
     // Determines how the video would fit the screen. 'both' would fit the whole

+ 6
- 0
lang/main.json Voir le fichier

@@ -9,6 +9,7 @@
9 9
     "me": "me",
10 10
     "speaker": "Speaker",
11 11
     "defaultNickname": "ex. __name__",
12
+    "defaultLink": "e.g. __url__",
12 13
     "welcomepage":{
13 14
         "go": "GO",
14 15
         "roomname": "Enter room name",
@@ -55,6 +56,7 @@
55 56
         "invite": "Invite others",
56 57
         "chat": "Open / close chat",
57 58
         "etherpad": "Shared document",
59
+        "sharedvideo": "Shared video",
58 60
         "sharescreen": "Share screen",
59 61
         "fullscreen": "Enter / Exit Full Screen",
60 62
         "sip": "Call SIP number",
@@ -159,6 +161,10 @@
159 161
         "passwordRequired": "Password required",
160 162
         "Ok": "Ok",
161 163
         "Remove": "Remove",
164
+        "shareVideoTitle": "Share a video",
165
+        "shareVideoLinkError": "Please provide a correct youtube link.",
166
+        "removeSharedVideoTitle": "Remove shared video",
167
+        "removeSharedVideoMsg": "Are you sure you would like to remove your shared video?",
162 168
         "WaitingForHost": "Waiting for the host ...",
163 169
         "WaitForHostMsg": "The conference <b>__room__ </b> has not yet started. If you are the host then please authenticate. Otherwise, please wait for the host to arrive.",
164 170
         "IamHost": "I am the host",

+ 50
- 3
modules/UI/UI.js Voir le fichier

@@ -13,6 +13,7 @@ import UIUtil from "./util/UIUtil";
13 13
 import UIEvents from "../../service/UI/UIEvents";
14 14
 import CQEvents from '../../service/connectionquality/CQEvents';
15 15
 import EtherpadManager from './etherpad/Etherpad';
16
+import SharedVideoManager from './shared_video/SharedVideo';
16 17
 
17 18
 import VideoLayout from "./videolayout/VideoLayout";
18 19
 import FilmStrip from "./videolayout/FilmStrip";
@@ -30,6 +31,7 @@ var eventEmitter = new EventEmitter();
30 31
 UI.eventEmitter = eventEmitter;
31 32
 
32 33
 let etherpadManager;
34
+let sharedVideoManager;
33 35
 
34 36
 /**
35 37
  * Prompt user for nickname.
@@ -260,6 +262,12 @@ function registerListeners() {
260 262
         }
261 263
     });
262 264
 
265
+    UI.addListener(UIEvents.SHARED_VIDEO_CLICKED, function () {
266
+        if (sharedVideoManager) {
267
+            sharedVideoManager.toggleSharedVideo();
268
+        }
269
+    });
270
+
263 271
     UI.addListener(UIEvents.FULLSCREEN_TOGGLE, toggleFullScreen);
264 272
 
265 273
     UI.addListener(UIEvents.TOGGLE_CHAT, UI.toggleChat);
@@ -334,6 +342,7 @@ UI.start = function () {
334 342
     ContactList.init(eventEmitter);
335 343
 
336 344
     bindEvents();
345
+    sharedVideoManager = new SharedVideoManager(eventEmitter);
337 346
     if (!interfaceConfig.filmStripOnly) {
338 347
 
339 348
         $("#videospace").mousemove(function () {
@@ -481,11 +490,11 @@ UI.addUser = function (id, displayName) {
481 490
         config.startAudioMuted > APP.conference.membersCount)
482 491
         UIUtil.playSoundNotification('userJoined');
483 492
 
484
-    // Configure avatar
485
-    UI.setUserAvatar(id);
486
-
487 493
     // Add Peer's container
488 494
     VideoLayout.addParticipantContainer(id);
495
+
496
+    // Configure avatar
497
+    UI.setUserAvatar(id);
489 498
 };
490 499
 
491 500
 /**
@@ -530,6 +539,7 @@ UI.updateLocalRole = function (isModerator) {
530 539
 
531 540
     Toolbar.showSipCallButton(isModerator);
532 541
     Toolbar.showRecordingButton(isModerator);
542
+    Toolbar.showSharedVideoButton(isModerator);
533 543
     SettingsMenu.showStartMutedOptions(isModerator);
534 544
 
535 545
     if (isModerator) {
@@ -1030,6 +1040,14 @@ UI.getLargeVideoID = function () {
1030 1040
     return VideoLayout.getLargeVideoID();
1031 1041
 };
1032 1042
 
1043
+/**
1044
+ * Returns the current video shown on large.
1045
+ * Currently used by tests (torture).
1046
+ */
1047
+UI.getLargeVideo = function () {
1048
+    return VideoLayout.getLargeVideo();
1049
+};
1050
+
1033 1051
 /**
1034 1052
  * Shows dialog with a link to FF extension.
1035 1053
  */
@@ -1046,4 +1064,33 @@ UI.updateDevicesAvailability = function (id, devices) {
1046 1064
     VideoLayout.setDeviceAvailabilityIcons(id, devices);
1047 1065
 };
1048 1066
 
1067
+/**
1068
+* Show shared video.
1069
+* @param {string} url video url
1070
+* @param {string} attributes
1071
+*/
1072
+UI.showSharedVideo = function (url, attributes) {
1073
+    if (sharedVideoManager)
1074
+        sharedVideoManager.showSharedVideo(url, attributes);
1075
+};
1076
+
1077
+/**
1078
+ * Update shared video.
1079
+ * @param {string} url video url
1080
+ * @param {string} attributes
1081
+ */
1082
+UI.updateSharedVideo = function (url, attributes) {
1083
+    if (sharedVideoManager)
1084
+        sharedVideoManager.updateSharedVideo(url, attributes);
1085
+};
1086
+
1087
+/**
1088
+ * Stop showing shared video.
1089
+ * @param {string} attributes
1090
+ */
1091
+UI.stopSharedVideo = function (attributes) {
1092
+    if (sharedVideoManager)
1093
+        sharedVideoManager.stopSharedVideo(attributes);
1094
+};
1095
+
1049 1096
 module.exports = UI;

+ 3
- 0
modules/UI/etherpad/Etherpad.js Voir le fichier

@@ -110,9 +110,11 @@ class Etherpad extends LargeContainer {
110 110
     show () {
111 111
         const $iframe = $(this.iframe);
112 112
         const $container = $(this.container);
113
+        let self = this;
113 114
 
114 115
         return new Promise(resolve => {
115 116
             $iframe.fadeIn(300, function () {
117
+                self.bodyBackground = document.body.style.background;
116 118
                 document.body.style.background = '#eeeeee';
117 119
                 $iframe.css({visibility: 'visible'});
118 120
                 $container.css({zIndex: 2});
@@ -124,6 +126,7 @@ class Etherpad extends LargeContainer {
124 126
     hide () {
125 127
         const $iframe = $(this.iframe);
126 128
         const $container = $(this.container);
129
+        document.body.style.background = this.bodyBackground;
127 130
 
128 131
         return new Promise(resolve => {
129 132
             $iframe.fadeOut(300, function () {

+ 537
- 0
modules/UI/shared_video/SharedVideo.js Voir le fichier

@@ -0,0 +1,537 @@
1
+/* global $, APP, YT, onPlayerReady, onPlayerStateChange, onPlayerError */
2
+
3
+import messageHandler from '../util/MessageHandler';
4
+import UIUtil from '../util/UIUtil';
5
+import UIEvents from '../../../service/UI/UIEvents';
6
+
7
+import VideoLayout from "../videolayout/VideoLayout";
8
+import LargeContainer from '../videolayout/LargeContainer';
9
+import SmallVideo from '../videolayout/SmallVideo';
10
+import FilmStrip from '../videolayout/FilmStrip';
11
+import ToolbarToggler from "../toolbars/ToolbarToggler";
12
+
13
+export const SHARED_VIDEO_CONTAINER_TYPE = "sharedvideo";
14
+
15
+/**
16
+ * Example shared video link.
17
+ * @type {string}
18
+ */
19
+const defaultSharedVideoLink = "https://www.youtube.com/watch?v=xNXN7CZk8X0";
20
+
21
+/**
22
+ * Manager of shared video.
23
+ */
24
+export default class SharedVideoManager {
25
+    constructor (emitter) {
26
+        this.emitter = emitter;
27
+        this.isSharedVideoShown = false;
28
+        this.isPlayerAPILoaded = false;
29
+        this.updateInterval = 5000; // milliseconds
30
+    }
31
+
32
+    /**
33
+     * Starts shared video by asking user for url, or if its already working
34
+     * asks whether the user wants to stop sharing the video.
35
+     */
36
+    toggleSharedVideo () {
37
+        if(!this.isSharedVideoShown) {
38
+            requestVideoLink().then(
39
+                    url => this.emitter.emit(
40
+                                UIEvents.UPDATE_SHARED_VIDEO, url, 'start'),
41
+                    err => console.error('SHARED VIDEO CANCELED', err)
42
+            );
43
+            return;
44
+        }
45
+
46
+        showStopVideoPropmpt().then(() =>
47
+            this.emitter.emit(UIEvents.UPDATE_SHARED_VIDEO, null, 'stop'));
48
+    }
49
+
50
+    /**
51
+     * Shows the player component and starts the checking function
52
+     * that will be sending updates, if we are the one shared the video
53
+     * @param url the video url
54
+     * @param attributes
55
+     */
56
+    showSharedVideo (url, attributes) {
57
+        if (this.isSharedVideoShown)
58
+            return;
59
+
60
+        // the video url
61
+        this.url = url;
62
+
63
+        // the owner of the video
64
+        this.from = attributes.from;
65
+
66
+        // This code loads the IFrame Player API code asynchronously.
67
+        var tag = document.createElement('script');
68
+
69
+        tag.src = "https://www.youtube.com/iframe_api";
70
+        var firstScriptTag = document.getElementsByTagName('script')[0];
71
+        firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
72
+
73
+        var self = this;
74
+        if(self.isPlayerAPILoaded)
75
+            window.onYouTubeIframeAPIReady();
76
+        else
77
+            window.onYouTubeIframeAPIReady = function() {
78
+                self.isPlayerAPILoaded = true;
79
+                let showControls = APP.conference.isLocalId(self.from) ? 1 : 0;
80
+                self.player = new YT.Player('sharedVideoIFrame', {
81
+                    height: '100%',
82
+                    width: '100%',
83
+                    videoId: self.url,
84
+                    playerVars: {
85
+                        'origin': location.origin,
86
+                        'fs': '0',
87
+                        'autoplay': 1,
88
+                        'controls': showControls,
89
+                        'rel' : 0
90
+                    },
91
+                    events: {
92
+                        'onReady': onPlayerReady,
93
+                        'onStateChange': onPlayerStateChange,
94
+                        'onError': onPlayerError
95
+                    }
96
+                });
97
+            };
98
+
99
+        window.onPlayerStateChange = function(event) {
100
+            if (event.data == YT.PlayerState.PLAYING) {
101
+                self.playerPaused = false;
102
+                self.updateCheck();
103
+            } else if (event.data == YT.PlayerState.PAUSED) {
104
+                self.playerPaused = true;
105
+                self.updateCheck(true);
106
+            }
107
+        };
108
+
109
+        window.onPlayerReady = function(event) {
110
+            let player = event.target;
111
+            player.playVideo();
112
+
113
+            let thumb = new SharedVideoThumb(self.url);
114
+            thumb.setDisplayName(player.getVideoData().title);
115
+            VideoLayout.addParticipantContainer(self.url, thumb);
116
+
117
+            let iframe = player.getIframe();
118
+            self.sharedVideo = new SharedVideoContainer(
119
+                {url, iframe, player});
120
+
121
+            VideoLayout.addLargeVideoContainer(
122
+                SHARED_VIDEO_CONTAINER_TYPE, self.sharedVideo);
123
+            VideoLayout.handleVideoThumbClicked(true, self.url);
124
+
125
+            self.isSharedVideoShown = true;
126
+
127
+            // If we are sending the command and we are starting the player
128
+            // we need to continuously send the player current time position
129
+            if(APP.conference.isLocalId(self.from)) {
130
+                self.intervalId = setInterval(
131
+                    self.updateCheck.bind(self),
132
+                    self.updateInterval);
133
+            }
134
+
135
+            // set initial state of the player if there is enough information
136
+            if(attributes.state === 'pause')
137
+                player.pauseVideo();
138
+            else if(attributes.time > 0) {
139
+                console.log("Player seekTo:", attributes.time);
140
+                player.seekTo(attributes.time);
141
+            }
142
+        };
143
+
144
+        window.onPlayerError = function(event) {
145
+            console.error("Error in the player:" + event.data);
146
+        };
147
+    }
148
+
149
+    /**
150
+     * Checks current state of the player and fire an event with the values.
151
+     */
152
+    updateCheck(sendPauseEvent)
153
+    {
154
+        // ignore update checks if we are not the owner of the video
155
+        if(!APP.conference.isLocalId(this.from))
156
+            return;
157
+
158
+        let state = this.player.getPlayerState();
159
+        // if its paused and haven't been pause - send paused
160
+        if (state === YT.PlayerState.PAUSED && sendPauseEvent) {
161
+            this.emitter.emit(UIEvents.UPDATE_SHARED_VIDEO,
162
+                this.url, 'pause');
163
+        }
164
+        // if its playing and it was paused - send update with time
165
+        // if its playing and was playing just send update with time
166
+        else if (state === YT.PlayerState.PLAYING) {
167
+            this.emitter.emit(UIEvents.UPDATE_SHARED_VIDEO,
168
+                this.url, 'playing',
169
+                this.player.getCurrentTime(),
170
+                this.player.isMuted() ? 0 : this.player.getVolume());
171
+        }
172
+    }
173
+
174
+    /**
175
+     * Updates video, if its not playing and needs starting or
176
+     * if its playing and needs to be paysed
177
+     * @param url the video url
178
+     * @param attributes
179
+     */
180
+    updateSharedVideo (url, attributes) {
181
+        // if we are sending the event ignore
182
+        if(APP.conference.isLocalId(this.from)) {
183
+            return;
184
+        }
185
+
186
+        if (attributes.state == 'playing') {
187
+
188
+            if(!this.isSharedVideoShown) {
189
+                this.showSharedVideo(url, attributes);
190
+                return;
191
+            }
192
+
193
+            // ocasionally we get this.player.getCurrentTime is not a function
194
+            // it seems its that player hasn't really loaded
195
+            if(!this.player || !this.player.getCurrentTime
196
+                || !this.player.pauseVideo
197
+                || !this.player.playVideo
198
+                || !this.player.getVolume
199
+                || !this.player.seekTo
200
+                || !this.player.getVolume)
201
+                return;
202
+
203
+            // check received time and current time
204
+            let currentPosition = this.player.getCurrentTime();
205
+            let diff = Math.abs(attributes.time - currentPosition);
206
+
207
+            // if we drift more than two times of the interval for checking
208
+            // sync, the interval is in milliseconds
209
+            if(diff > this.updateInterval*2/1000) {
210
+                console.log("Player seekTo:", attributes.time,
211
+                    " current time is:", currentPosition, " diff:", diff);
212
+                this.player.seekTo(attributes.time);
213
+            }
214
+
215
+            // lets check the volume
216
+            if (attributes.volume !== undefined &&
217
+                this.player.getVolume() != attributes.volume) {
218
+                this.player.setVolume(attributes.volume);
219
+                console.log("Player change of volume:" + attributes.volume);
220
+            }
221
+
222
+            if(this.playerPaused)
223
+                this.player.playVideo();
224
+        } else if (attributes.state == 'pause') {
225
+            // if its not paused, pause it
226
+            if(this.isSharedVideoShown) {
227
+                this.player.pauseVideo();
228
+            }
229
+            else {
230
+                // if not shown show it, passing attributes so it can
231
+                // be shown paused
232
+                this.showSharedVideo(url, attributes);
233
+            }
234
+        }
235
+    }
236
+
237
+    /**
238
+     * Stop shared video if it is currently showed. If the user started the
239
+     * shared video is the one in the attributes.from (called when user
240
+     * left and we want to remove video if the user sharing it left).
241
+     * @param attributes
242
+     */
243
+    stopSharedVideo (attributes) {
244
+        if (!this.isSharedVideoShown)
245
+            return;
246
+
247
+        if(this.from !== attributes.from)
248
+            return;
249
+
250
+        if(this.intervalId) {
251
+            clearInterval(this.intervalId);
252
+            this.intervalId = null;
253
+        }
254
+
255
+        VideoLayout.removeParticipantContainer(this.url);
256
+
257
+        VideoLayout.showLargeVideoContainer(SHARED_VIDEO_CONTAINER_TYPE, false)
258
+            .then(() => {
259
+                VideoLayout.removeLargeVideoContainer(
260
+                    SHARED_VIDEO_CONTAINER_TYPE);
261
+
262
+                this.player.destroy();
263
+                this.player = null;
264
+        });
265
+
266
+        this.url = null;
267
+        this.isSharedVideoShown = false;
268
+    }
269
+}
270
+
271
+/**
272
+ * Container for shared video iframe.
273
+ */
274
+class SharedVideoContainer extends LargeContainer {
275
+
276
+    constructor ({url, iframe, player}) {
277
+        super();
278
+
279
+        this.$iframe = $(iframe);
280
+        this.url = url;
281
+        this.player = player;
282
+    }
283
+
284
+    get $video () {
285
+        return this.$iframe;
286
+    }
287
+
288
+    show () {
289
+        return new Promise(resolve => {
290
+            this.$iframe.fadeIn(300, () => {
291
+                this.$iframe.css({opacity: 1});
292
+                resolve();
293
+            });
294
+        });
295
+    }
296
+
297
+    hide () {
298
+        return new Promise(resolve => {
299
+            this.$iframe.fadeOut(300, () => {
300
+                this.$iframe.css({opacity: 0});
301
+                resolve();
302
+            });
303
+        });
304
+    }
305
+
306
+    onHoverIn () {
307
+        ToolbarToggler.showToolbar();
308
+    }
309
+
310
+    get id () {
311
+        return this.url;
312
+    }
313
+
314
+    resize (containerWidth, containerHeight) {
315
+        let height = containerHeight - FilmStrip.getFilmStripHeight();
316
+
317
+        let width = containerWidth;
318
+
319
+        this.$iframe.width(width).height(height);
320
+    }
321
+
322
+    /**
323
+     * @return {boolean} do not switch on dominant speaker event if on stage.
324
+     */
325
+    stayOnStage () {
326
+        return false;
327
+    }
328
+}
329
+
330
+function SharedVideoThumb (url)
331
+{
332
+    this.id = url;
333
+
334
+    this.url = url;
335
+    this.setVideoType(SHARED_VIDEO_CONTAINER_TYPE);
336
+    this.videoSpanId = "sharedVideoContainer";
337
+    this.container = this.createContainer(this.videoSpanId);
338
+    this.container.onclick = this.videoClick.bind(this);
339
+    this.bindHoverHandler();
340
+
341
+    SmallVideo.call(this, VideoLayout);
342
+    this.isVideoMuted = true;
343
+}
344
+SharedVideoThumb.prototype = Object.create(SmallVideo.prototype);
345
+SharedVideoThumb.prototype.constructor = SharedVideoThumb;
346
+
347
+/**
348
+ * hide display name
349
+ */
350
+
351
+SharedVideoThumb.prototype.setDeviceAvailabilityIcons = function () {};
352
+
353
+SharedVideoThumb.prototype.avatarChanged = function () {};
354
+
355
+SharedVideoThumb.prototype.createContainer = function (spanId) {
356
+    var container = document.createElement('span');
357
+    container.id = spanId;
358
+    container.className = 'videocontainer';
359
+
360
+    // add the avatar
361
+    var avatar = document.createElement('img');
362
+    avatar.id = 'avatar_' + this.id;
363
+    avatar.className = 'sharedVideoAvatar';
364
+    avatar.src = "https://img.youtube.com/vi/" + this.url + "/0.jpg";
365
+    container.appendChild(avatar);
366
+
367
+    var remotes = document.getElementById('remoteVideos');
368
+    return remotes.appendChild(container);
369
+};
370
+
371
+/**
372
+ * The thumb click handler.
373
+ */
374
+SharedVideoThumb.prototype.videoClick = function () {
375
+    VideoLayout.handleVideoThumbClicked(true, this.url);
376
+};
377
+
378
+/**
379
+ * Removes RemoteVideo from the page.
380
+ */
381
+SharedVideoThumb.prototype.remove = function () {
382
+    console.log("Remove shared video thumb", this.id);
383
+
384
+    // Make sure that the large video is updated if are removing its
385
+    // corresponding small video.
386
+    this.VideoLayout.updateRemovedVideo(this.id);
387
+
388
+    // Remove whole container
389
+    if (this.container.parentNode) {
390
+        this.container.parentNode.removeChild(this.container);
391
+    }
392
+};
393
+
394
+/**
395
+ * Sets the display name for the thumb.
396
+ */
397
+SharedVideoThumb.prototype.setDisplayName = function(displayName) {
398
+    if (!this.container) {
399
+        console.warn( "Unable to set displayName - " + this.videoSpanId +
400
+            " does not exist");
401
+        return;
402
+    }
403
+
404
+    var nameSpan = $('#' + this.videoSpanId + '>span.displayname');
405
+
406
+    // If we already have a display name for this video.
407
+    if (nameSpan.length > 0) {
408
+        if (displayName && displayName.length > 0) {
409
+            $('#' + this.videoSpanId + '_name').text(displayName);
410
+        }
411
+    } else {
412
+        nameSpan = document.createElement('span');
413
+        nameSpan.className = 'displayname';
414
+        $('#' + this.videoSpanId)[0].appendChild(nameSpan);
415
+
416
+        if (displayName && displayName.length > 0)
417
+            $(nameSpan).text(displayName);
418
+        nameSpan.id = this.videoSpanId + '_name';
419
+    }
420
+
421
+};
422
+
423
+/**
424
+ * Checks if given string is youtube url.
425
+ * @param {string} url string to check.
426
+ * @returns {boolean}
427
+ */
428
+function getYoutubeLink(url) {
429
+    let p = /^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/;//jshint ignore:line
430
+    return (url.match(p)) ? RegExp.$1 : false;
431
+}
432
+
433
+/**
434
+ * Ask user if he want to close shared video.
435
+ */
436
+function showStopVideoPropmpt() {
437
+    return new Promise(function (resolve, reject) {
438
+        messageHandler.openTwoButtonDialog(
439
+            "dialog.removeSharedVideoTitle",
440
+            null,
441
+            "dialog.removeSharedVideoMsg",
442
+            null,
443
+            false,
444
+            "dialog.Remove",
445
+            function(e,v,m,f) {
446
+                if (v) {
447
+                    resolve();
448
+                } else {
449
+                    reject();
450
+                }
451
+            }
452
+        );
453
+
454
+    });
455
+}
456
+
457
+/**
458
+ * Ask user for shared video url to share with others.
459
+ * Dialog validates client input to allow only youtube urls.
460
+ */
461
+function requestVideoLink() {
462
+    let i18n = APP.translation;
463
+    const title = i18n.generateTranslationHTML("dialog.shareVideoTitle");
464
+    const cancelButton = i18n.generateTranslationHTML("dialog.Cancel");
465
+    const shareButton = i18n.generateTranslationHTML("dialog.Share");
466
+    const backButton = i18n.generateTranslationHTML("dialog.Back");
467
+    const linkError
468
+        = i18n.generateTranslationHTML("dialog.shareVideoLinkError");
469
+    const i18nOptions = {url: defaultSharedVideoLink};
470
+    const defaultUrl = i18n.translateString("defaultLink", i18nOptions);
471
+
472
+    return new Promise(function (resolve, reject) {
473
+        let dialog = messageHandler.openDialogWithStates({
474
+            state0: {
475
+                html:  `
476
+                    <h2>${title}</h2>
477
+                    <input name="sharedVideoUrl" type="text"
478
+                           data-i18n="[placeholder]defaultLink"
479
+                           data-i18n-options="${JSON.stringify(i18nOptions)}"
480
+                           placeholder="${defaultUrl}"
481
+                           autofocus>`,
482
+                persistent: false,
483
+                buttons: [
484
+                    {title: cancelButton, value: false},
485
+                    {title: shareButton, value: true}
486
+                ],
487
+                focus: ':input:first',
488
+                defaultButton: 1,
489
+                submit: function (e, v, m, f) {
490
+                    e.preventDefault();
491
+                    if (!v) {
492
+                        reject('cancelled');
493
+                        dialog.close();
494
+                        return;
495
+                    }
496
+
497
+                    let sharedVideoUrl = f.sharedVideoUrl;
498
+                    if (!sharedVideoUrl) {
499
+                        return;
500
+                    }
501
+
502
+                    let urlValue = encodeURI(UIUtil.escapeHtml(sharedVideoUrl));
503
+                    let yVideoId = getYoutubeLink(urlValue);
504
+                    if (!yVideoId) {
505
+                        dialog.goToState('state1');
506
+                        return false;
507
+                    }
508
+
509
+                    resolve(yVideoId);
510
+                    dialog.close();
511
+                }
512
+            },
513
+
514
+            state1: {
515
+                html: `<h2>${title}</h2> ${linkError}`,
516
+                persistent: false,
517
+                buttons: [
518
+                    {title: cancelButton, value: false},
519
+                    {title: backButton, value: true}
520
+                ],
521
+                focus: ':input:first',
522
+                defaultButton: 1,
523
+                submit: function (e, v, m, f) {
524
+                    e.preventDefault();
525
+                    if (v === 0) {
526
+                        reject();
527
+                        dialog.close();
528
+                    } else {
529
+                        dialog.goToState('state0');
530
+                    }
531
+                }
532
+            }
533
+        });
534
+
535
+    });
536
+}
537
+

+ 13
- 0
modules/UI/toolbars/Toolbar.js Voir le fichier

@@ -126,6 +126,10 @@ const buttonHandlers = {
126 126
         AnalyticsAdapter.sendEvent('toolbar.etherpad.clicked');
127 127
         emitter.emit(UIEvents.ETHERPAD_CLICKED);
128 128
     },
129
+    "toolbar_button_sharedvideo": function () {
130
+        AnalyticsAdapter.sendEvent('toolbar.sharedvideo.clicked');
131
+        emitter.emit(UIEvents.SHARED_VIDEO_CLICKED);
132
+    },
129 133
     "toolbar_button_desktopsharing": function () {
130 134
         if (APP.conference.isSharingScreen) {
131 135
             AnalyticsAdapter.sendEvent('toolbar.screen.disabled');
@@ -284,6 +288,15 @@ const Toolbar = {
284 288
         }
285 289
     },
286 290
 
291
+    // Shows or hides the 'shared video' button.
292
+    showSharedVideoButton (show) {
293
+        if (UIUtil.isButtonEnabled('sharedvideo') && show) {
294
+            $('#toolbar_button_sharedvideo').css({display: "inline-block"});
295
+        } else {
296
+            $('#toolbar_button_sharedvideo').css({display: "none"});
297
+        }
298
+    },
299
+
287 300
     // checks whether recording is enabled and whether we have params
288 301
     // to start automatically recording
289 302
     checkAutoRecord () {

+ 14
- 8
modules/UI/videolayout/LargeVideo.js Voir le fichier

@@ -11,6 +11,8 @@ import {createDeferred} from '../../util/helpers';
11 11
 const avatarSize = interfaceConfig.DOMINANT_SPEAKER_AVATAR_SIZE;
12 12
 const FADE_DURATION_MS = 300;
13 13
 
14
+export const VIDEO_CONTAINER_TYPE = "camera";
15
+
14 16
 /**
15 17
  * Get stream id.
16 18
  * @param {JitsiTrack?} stream
@@ -150,8 +152,6 @@ function getDesktopVideoPosition(videoWidth,
150 152
     return { horizontalIndent, verticalIndent };
151 153
 }
152 154
 
153
-export const VideoContainerType = "camera";
154
-
155 155
 /**
156 156
  * Container for user video.
157 157
  */
@@ -365,9 +365,10 @@ export default class LargeVideoManager {
365 365
     constructor () {
366 366
         this.containers = {};
367 367
 
368
-        this.state = VideoContainerType;
369
-        this.videoContainer = new VideoContainer(() => this.resizeContainer(VideoContainerType));
370
-        this.addContainer(VideoContainerType, this.videoContainer);
368
+        this.state = VIDEO_CONTAINER_TYPE;
369
+        this.videoContainer = new VideoContainer(
370
+            () => this.resizeContainer(VIDEO_CONTAINER_TYPE));
371
+        this.addContainer(VIDEO_CONTAINER_TYPE, this.videoContainer);
371 372
         // use the same video container to handle and desktop tracks
372 373
         this.addContainer("desktop", this.videoContainer);
373 374
 
@@ -451,7 +452,12 @@ export default class LargeVideoManager {
451 452
             // change the avatar url on large
452 453
             this.updateAvatar(Avatar.getAvatarUrl(id));
453 454
 
454
-            let isVideoMuted = stream ? stream.isMuted() : true;
455
+            // If we the continer is VIDEO_CONTAINER_TYPE, we need to check
456
+            // its stream whether exist and is muted to set isVideoMuted
457
+            // in rest of the cases it is false
458
+            let isVideoMuted = false;
459
+            if (videoType == VIDEO_CONTAINER_TYPE)
460
+                isVideoMuted = stream ? stream.isMuted() : true;
455 461
 
456 462
             // show the avatar on large if needed
457 463
             container.showAvatar(isVideoMuted);
@@ -616,7 +622,7 @@ export default class LargeVideoManager {
616 622
         }
617 623
 
618 624
         let oldContainer = this.containers[this.state];
619
-        if (this.state === VideoContainerType) {
625
+        if (this.state === VIDEO_CONTAINER_TYPE) {
620 626
             this.showWatermark(false);
621 627
         }
622 628
         oldContainer.hide();
@@ -625,7 +631,7 @@ export default class LargeVideoManager {
625 631
         let container = this.getContainer(type);
626 632
 
627 633
         return container.show().then(() => {
628
-            if (type === VideoContainerType) {
634
+            if (type === VIDEO_CONTAINER_TYPE) {
629 635
                 this.showWatermark(true);
630 636
             }
631 637
         });

+ 1
- 8
modules/UI/videolayout/SmallVideo.js Voir le fichier

@@ -363,14 +363,7 @@ SmallVideo.prototype.updateView = function () {
363 363
     }
364 364
     setVisibility(avatar, showAvatar);
365 365
 
366
-    var showDisplayName = !showVideo && !showAvatar;
367
-
368
-    if (showDisplayName) {
369
-        this.showDisplayName(this.VideoLayout.isLargeVideoVisible());
370
-    }
371
-    else {
372
-        this.showDisplayName(false);
373
-    }
366
+    this.showDisplayName(!showVideo && !showAvatar);
374 367
 };
375 368
 
376 369
 SmallVideo.prototype.avatarChanged = function (avatarUrl) {

+ 30
- 6
modules/UI/videolayout/VideoLayout.js Voir le fichier

@@ -9,7 +9,8 @@ import UIEvents from "../../../service/UI/UIEvents";
9 9
 import UIUtil from "../util/UIUtil";
10 10
 
11 11
 import RemoteVideo from "./RemoteVideo";
12
-import LargeVideoManager, {VideoContainerType} from "./LargeVideo";
12
+import LargeVideoManager, {VIDEO_CONTAINER_TYPE} from "./LargeVideo";
13
+import {SHARED_VIDEO_CONTAINER_TYPE} from '../shared_video/SharedVideo';
13 14
 import LocalVideo from "./LocalVideo";
14 15
 import PanelToggler from "../side_pannels/SidePanelToggler";
15 16
 
@@ -92,6 +93,11 @@ var VideoLayout = {
92 93
     init (emitter) {
93 94
         eventEmitter = emitter;
94 95
         localVideoThumbnail = new LocalVideo(VideoLayout, emitter);
96
+        // sets default video type of local video
97
+        localVideoThumbnail.setVideoType(VIDEO_CONTAINER_TYPE);
98
+        // if we do not resize the thumbs here, if there is no video device
99
+        // the local video thumb maybe one pixel
100
+        this.resizeThumbnails(false, true, false);
95 101
 
96 102
         emitter.addListener(UIEvents.CONTACT_CLICKED, onContactClicked);
97 103
         this.lastNCount = config.channelLastN;
@@ -343,7 +349,7 @@ var VideoLayout = {
343 349
         let videoType = VideoLayout.getRemoteVideoType(id);
344 350
         if (!videoType) {
345 351
             // make video type the default one (camera)
346
-            videoType = VideoContainerType;
352
+            videoType = VIDEO_CONTAINER_TYPE;
347 353
         }
348 354
         remoteVideo.setVideoType(videoType);
349 355
 
@@ -367,7 +373,8 @@ var VideoLayout = {
367 373
         // current dominant, focused speaker or update it to
368 374
         // the current dominant speaker.
369 375
         if ((!focusedVideoResourceJid &&
370
-            !currentDominantSpeaker) ||
376
+            !currentDominantSpeaker &&
377
+            this.isLargeContainerTypeVisible(VIDEO_CONTAINER_TYPE)) ||
371 378
             focusedVideoResourceJid === resourceJid ||
372 379
             (resourceJid &&
373 380
                 currentDominantSpeaker === resourceJid)) {
@@ -888,7 +895,7 @@ var VideoLayout = {
888 895
     },
889 896
 
890 897
     isLargeVideoVisible () {
891
-        return this.isLargeContainerTypeVisible(VideoContainerType);
898
+        return this.isLargeContainerTypeVisible(VIDEO_CONTAINER_TYPE);
892 899
     },
893 900
 
894 901
     /**
@@ -960,8 +967,17 @@ var VideoLayout = {
960 967
             return Promise.resolve();
961 968
         }
962 969
 
970
+        let currentId = largeVideo.id;
971
+        if(currentId) {
972
+            var oldSmallVideo = this.getSmallVideo(currentId);
973
+        }
974
+
963 975
         // if !show then use default type - large video
964
-        return largeVideo.showContainer(show ? type : VideoContainerType);
976
+        return largeVideo.showContainer(show ? type : VIDEO_CONTAINER_TYPE)
977
+            .then(() => {
978
+                if(oldSmallVideo)
979
+                    oldSmallVideo && oldSmallVideo.updateView();
980
+            });
965 981
     },
966 982
 
967 983
     isLargeContainerTypeVisible (type) {
@@ -970,10 +986,18 @@ var VideoLayout = {
970 986
 
971 987
     /**
972 988
      * Returns the id of the current video shown on large.
973
-     * Currently used by tests (troture).
989
+     * Currently used by tests (torture).
974 990
      */
975 991
     getLargeVideoID () {
976 992
         return largeVideo.id;
993
+    },
994
+
995
+    /**
996
+     * Returns the the current video shown on large.
997
+     * Currently used by tests (torture).
998
+     */
999
+    getLargeVideo () {
1000
+        return largeVideo;
977 1001
     }
978 1002
 };
979 1003
 

+ 7
- 0
service/UI/UIEvents.js Voir le fichier

@@ -21,6 +21,13 @@ export default {
21 21
     AUDIO_MUTED: "UI.audio_muted",
22 22
     VIDEO_MUTED: "UI.video_muted",
23 23
     ETHERPAD_CLICKED: "UI.etherpad_clicked",
24
+    SHARED_VIDEO_CLICKED: "UI.start_shared_video",
25
+    /**
26
+     * Updates shared video with params: url, state, time(optional)
27
+     * Where url is the video link, state is stop/start/pause and time is the
28
+     * current video playing time.
29
+     */
30
+    UPDATE_SHARED_VIDEO: "UI.update_shared_video",
24 31
     ROOM_LOCK_CLICKED: "UI.room_lock_clicked",
25 32
     USER_INVITED: "UI.user_invited",
26 33
     USER_KICKED: "UI.user_kicked",

Chargement…
Annuler
Enregistrer