Преглед на файлове

Shared video, synchronized playing/seek/muting/volume initial commit.

j8
damencho преди 9 години
родител
ревизия
38275ce045

+ 45
- 1
conference.js Целия файл

@@ -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 Целия файл

@@ -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 Целия файл

@@ -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 Целия файл

@@ -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 Целия файл

@@ -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

+ 4
- 0
lang/main.json Целия файл

@@ -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,8 @@
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.",
162 166
         "WaitingForHost": "Waiting for the host ...",
163 167
         "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 168
         "IamHost": "I am the host",

+ 47
- 0
modules/UI/UI.js Целия файл

@@ -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 () {
@@ -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 Целия файл

@@ -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 () {

+ 503
- 0
modules/UI/shared_video/SharedVideo.js Целия файл

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

+ 13
- 0
modules/UI/toolbars/Toolbar.js Целия файл

@@ -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 () {

+ 8
- 7
modules/UI/videolayout/LargeVideo.js Целия файл

@@ -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
 
@@ -616,7 +617,7 @@ export default class LargeVideoManager {
616 617
         }
617 618
 
618 619
         let oldContainer = this.containers[this.state];
619
-        if (this.state === VideoContainerType) {
620
+        if (this.state === VIDEO_CONTAINER_TYPE) {
620 621
             this.showWatermark(false);
621 622
         }
622 623
         oldContainer.hide();
@@ -625,7 +626,7 @@ export default class LargeVideoManager {
625 626
         let container = this.getContainer(type);
626 627
 
627 628
         return container.show().then(() => {
628
-            if (type === VideoContainerType) {
629
+            if (type === VIDEO_CONTAINER_TYPE) {
629 630
                 this.showWatermark(true);
630 631
             }
631 632
         });

+ 1
- 8
modules/UI/videolayout/SmallVideo.js Целия файл

@@ -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) {

+ 25
- 6
modules/UI/videolayout/VideoLayout.js Целия файл

@@ -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
 
@@ -343,7 +344,7 @@ var VideoLayout = {
343 344
         let videoType = VideoLayout.getRemoteVideoType(id);
344 345
         if (!videoType) {
345 346
             // make video type the default one (camera)
346
-            videoType = VideoContainerType;
347
+            videoType = VIDEO_CONTAINER_TYPE;
347 348
         }
348 349
         remoteVideo.setVideoType(videoType);
349 350
 
@@ -367,7 +368,8 @@ var VideoLayout = {
367 368
         // current dominant, focused speaker or update it to
368 369
         // the current dominant speaker.
369 370
         if ((!focusedVideoResourceJid &&
370
-            !currentDominantSpeaker) ||
371
+            !currentDominantSpeaker &&
372
+            !this.isLargeContainerTypeVisible(SHARED_VIDEO_CONTAINER_TYPE)) ||
371 373
             focusedVideoResourceJid === resourceJid ||
372 374
             (resourceJid &&
373 375
                 currentDominantSpeaker === resourceJid)) {
@@ -888,7 +890,7 @@ var VideoLayout = {
888 890
     },
889 891
 
890 892
     isLargeVideoVisible () {
891
-        return this.isLargeContainerTypeVisible(VideoContainerType);
893
+        return this.isLargeContainerTypeVisible(VIDEO_CONTAINER_TYPE);
892 894
     },
893 895
 
894 896
     /**
@@ -960,8 +962,17 @@ var VideoLayout = {
960 962
             return Promise.resolve();
961 963
         }
962 964
 
965
+        let currentId = largeVideo.id;
966
+        if(currentId) {
967
+            var oldSmallVideo = this.getSmallVideo(currentId);
968
+        }
969
+
963 970
         // if !show then use default type - large video
964
-        return largeVideo.showContainer(show ? type : VideoContainerType);
971
+        return largeVideo.showContainer(show ? type : VIDEO_CONTAINER_TYPE)
972
+            .then(() => {
973
+                if(oldSmallVideo)
974
+                    oldSmallVideo && oldSmallVideo.updateView();
975
+            });
965 976
     },
966 977
 
967 978
     isLargeContainerTypeVisible (type) {
@@ -970,10 +981,18 @@ var VideoLayout = {
970 981
 
971 982
     /**
972 983
      * Returns the id of the current video shown on large.
973
-     * Currently used by tests (troture).
984
+     * Currently used by tests (torture).
974 985
      */
975 986
     getLargeVideoID () {
976 987
         return largeVideo.id;
988
+    },
989
+
990
+    /**
991
+     * Returns the the current video shown on large.
992
+     * Currently used by tests (torture).
993
+     */
994
+    getLargeVideo () {
995
+        return largeVideo;
977 996
     }
978 997
 };
979 998
 

+ 7
- 0
service/UI/UIEvents.js Целия файл

@@ -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",

Loading…
Отказ
Запис