diff --git a/conference.js b/conference.js
index 57a660156..ad08b8a53 100644
--- a/conference.js
+++ b/conference.js
@@ -27,7 +27,8 @@ let room, connection, localAudio, localVideo, roomLocker;
const Commands = {
CONNECTION_QUALITY: "stats",
EMAIL: "email",
- ETHERPAD: "etherpad"
+ ETHERPAD: "etherpad",
+ SHARED_VIDEO: "shared-video"
};
/**
@@ -689,6 +690,7 @@ export default {
console.log('USER %s LEFT', id, user);
APP.API.notifyUserLeft(id);
APP.UI.removeUser(id, user.getDisplayName());
+ APP.UI.stopSharedVideo({from: id});
});
@@ -1012,5 +1014,47 @@ export default {
APP.UI.addListener(
UIEvents.TOGGLE_SCREENSHARING, this.toggleScreenSharing.bind(this)
);
+
+ APP.UI.addListener(UIEvents.UPDATE_SHARED_VIDEO,
+ (url, state, time, volume) => {
+ // send start and stop commands once, and remove any updates
+ // that had left
+ if (state === 'stop' || state === 'start' || state === 'playing') {
+ room.removeCommand(Commands.SHARED_VIDEO);
+ room.sendCommandOnce(Commands.SHARED_VIDEO, {
+ value: url,
+ attributes: {
+ from: APP.conference.localId,
+ state: state,
+ time: time,
+ volume: volume
+ }
+ });
+ }
+ else {
+ // in case of paused, in order to allow late users to join
+ // paused
+ room.sendCommand(Commands.SHARED_VIDEO, {
+ value: url,
+ attributes: {
+ from: APP.conference.localId,
+ state: state,
+ time: time,
+ volume: volume
+ }
+ });
+ }
+ });
+ room.addCommandListener(
+ Commands.SHARED_VIDEO, ({value, attributes}) => {
+ if (attributes.state === 'stop') {
+ APP.UI.stopSharedVideo(attributes);
+ } else if (attributes.state === 'start') {
+ APP.UI.showSharedVideo(value, attributes);
+ } else if (attributes.state === 'playing'
+ || attributes.state === 'pause') {
+ APP.UI.updateSharedVideo(value, attributes);
+ }
+ });
}
};
diff --git a/css/main.css b/css/main.css
index 5a937025f..a79958423 100644
--- a/css/main.css
+++ b/css/main.css
@@ -63,6 +63,7 @@ html, body{
text-align: center;
text-shadow: 0 1px 0 rgba(255,255,255,.3), 0 -1px 0 rgba(0,0,0,.6);
z-index: 1;
+ font-size: 1.22em !important;
}
.toolbar_span>span {
diff --git a/css/videolayout_default.css b/css/videolayout_default.css
index 73668cf56..0469afb9c 100644
--- a/css/videolayout_default.css
+++ b/css/videolayout_default.css
@@ -31,6 +31,7 @@
position: relative;
margin-left: auto;
margin-right: auto;
+ text-align: center;
}
#remoteVideos .videocontainer {
@@ -112,6 +113,7 @@
}
#presentation,
+#sharedVideo,
#etherpad,
#localVideoWrapper>video,
#localVideoWrapper>object,
@@ -436,6 +438,10 @@
border-radius: 200px;
}
+.sharedVideoAvatar {
+ height: 100%;
+}
+
.noMic {
position: absolute;
border-radius: 8px;
diff --git a/index.html b/index.html
index 4154cbdc5..6c8a831db 100644
--- a/index.html
+++ b/index.html
@@ -123,6 +123,7 @@
+
@@ -137,6 +138,7 @@
+
diff --git a/interface_config.js b/interface_config.js
index 7101d6d05..1f6754bd1 100644
--- a/interface_config.js
+++ b/interface_config.js
@@ -16,7 +16,7 @@ var interfaceConfig = {
INVITATION_POWERED_BY: true,
DOMINANT_SPEAKER_AVATAR_SIZE: 100,
TOOLBAR_BUTTONS: ['authentication', 'microphone', 'camera', 'desktop',
- 'recording', 'security', 'invite', 'chat', 'etherpad',
+ 'recording', 'security', 'invite', 'chat', 'etherpad', 'sharedvideo',
'fullscreen', 'sip', 'dialpad', 'settings', 'hangup', 'filmstrip',
'contacts'],
// Determines how the video would fit the screen. 'both' would fit the whole
diff --git a/lang/main.json b/lang/main.json
index 6eaf7e18e..12d579de8 100644
--- a/lang/main.json
+++ b/lang/main.json
@@ -9,6 +9,7 @@
"me": "me",
"speaker": "Speaker",
"defaultNickname": "ex. __name__",
+ "defaultLink": "e.g. __url__",
"welcomepage":{
"go": "GO",
"roomname": "Enter room name",
@@ -55,6 +56,7 @@
"invite": "Invite others",
"chat": "Open / close chat",
"etherpad": "Shared document",
+ "sharedvideo": "Shared video",
"sharescreen": "Share screen",
"fullscreen": "Enter / Exit Full Screen",
"sip": "Call SIP number",
@@ -159,6 +161,8 @@
"passwordRequired": "Password required",
"Ok": "Ok",
"Remove": "Remove",
+ "shareVideoTitle": "Share a video",
+ "shareVideoLinkError": "Please provide a correct youtube link.",
"WaitingForHost": "Waiting for the host ...",
"WaitForHostMsg": "The conference
__room__ has not yet started. If you are the host then please authenticate. Otherwise, please wait for the host to arrive.",
"IamHost": "I am the host",
diff --git a/modules/UI/UI.js b/modules/UI/UI.js
index 8cfa0e067..fcd39c4fc 100644
--- a/modules/UI/UI.js
+++ b/modules/UI/UI.js
@@ -13,6 +13,7 @@ import UIUtil from "./util/UIUtil";
import UIEvents from "../../service/UI/UIEvents";
import CQEvents from '../../service/connectionquality/CQEvents';
import EtherpadManager from './etherpad/Etherpad';
+import SharedVideoManager from './shared_video/SharedVideo';
import VideoLayout from "./videolayout/VideoLayout";
import FilmStrip from "./videolayout/FilmStrip";
@@ -30,6 +31,7 @@ var eventEmitter = new EventEmitter();
UI.eventEmitter = eventEmitter;
let etherpadManager;
+let sharedVideoManager;
/**
* Prompt user for nickname.
@@ -260,6 +262,12 @@ function registerListeners() {
}
});
+ UI.addListener(UIEvents.SHARED_VIDEO_CLICKED, function () {
+ if (sharedVideoManager) {
+ sharedVideoManager.toggleSharedVideo();
+ }
+ });
+
UI.addListener(UIEvents.FULLSCREEN_TOGGLE, toggleFullScreen);
UI.addListener(UIEvents.TOGGLE_CHAT, UI.toggleChat);
@@ -334,6 +342,7 @@ UI.start = function () {
ContactList.init(eventEmitter);
bindEvents();
+ sharedVideoManager = new SharedVideoManager(eventEmitter);
if (!interfaceConfig.filmStripOnly) {
$("#videospace").mousemove(function () {
@@ -530,6 +539,7 @@ UI.updateLocalRole = function (isModerator) {
Toolbar.showSipCallButton(isModerator);
Toolbar.showRecordingButton(isModerator);
+ Toolbar.showSharedVideoButton(isModerator);
SettingsMenu.showStartMutedOptions(isModerator);
if (isModerator) {
@@ -1030,6 +1040,14 @@ UI.getLargeVideoID = function () {
return VideoLayout.getLargeVideoID();
};
+/**
+ * Returns the current video shown on large.
+ * Currently used by tests (torture).
+ */
+UI.getLargeVideo = function () {
+ return VideoLayout.getLargeVideo();
+};
+
/**
* Shows dialog with a link to FF extension.
*/
@@ -1046,4 +1064,33 @@ UI.updateDevicesAvailability = function (id, devices) {
VideoLayout.setDeviceAvailabilityIcons(id, devices);
};
+/**
+* Show shared video.
+* @param {string} url video url
+* @param {string} attributes
+*/
+UI.showSharedVideo = function (url, attributes) {
+ if (sharedVideoManager)
+ sharedVideoManager.showSharedVideo(url, attributes);
+};
+
+/**
+ * Update shared video.
+ * @param {string} url video url
+ * @param {string} attributes
+ */
+UI.updateSharedVideo = function (url, attributes) {
+ if (sharedVideoManager)
+ sharedVideoManager.updateSharedVideo(url, attributes);
+};
+
+/**
+ * Stop showing shared video.
+ * @param {string} attributes
+ */
+UI.stopSharedVideo = function (attributes) {
+ if (sharedVideoManager)
+ sharedVideoManager.stopSharedVideo(attributes);
+};
+
module.exports = UI;
diff --git a/modules/UI/etherpad/Etherpad.js b/modules/UI/etherpad/Etherpad.js
index 01979eb07..7c65016a5 100644
--- a/modules/UI/etherpad/Etherpad.js
+++ b/modules/UI/etherpad/Etherpad.js
@@ -110,9 +110,11 @@ class Etherpad extends LargeContainer {
show () {
const $iframe = $(this.iframe);
const $container = $(this.container);
+ let self = this;
return new Promise(resolve => {
$iframe.fadeIn(300, function () {
+ self.bodyBackground = document.body.style.background;
document.body.style.background = '#eeeeee';
$iframe.css({visibility: 'visible'});
$container.css({zIndex: 2});
@@ -124,6 +126,7 @@ class Etherpad extends LargeContainer {
hide () {
const $iframe = $(this.iframe);
const $container = $(this.container);
+ document.body.style.background = this.bodyBackground;
return new Promise(resolve => {
$iframe.fadeOut(300, function () {
diff --git a/modules/UI/shared_video/SharedVideo.js b/modules/UI/shared_video/SharedVideo.js
new file mode 100644
index 000000000..5b7d16620
--- /dev/null
+++ b/modules/UI/shared_video/SharedVideo.js
@@ -0,0 +1,503 @@
+/* global $, APP, YT, onPlayerReady, onPlayerStateChange, onPlayerError */
+
+import messageHandler from '../util/MessageHandler';
+import UIUtil from '../util/UIUtil';
+import UIEvents from '../../../service/UI/UIEvents';
+
+import VideoLayout from "../videolayout/VideoLayout";
+import LargeContainer from '../videolayout/LargeContainer';
+import SmallVideo from '../videolayout/SmallVideo';
+import FilmStrip from '../videolayout/FilmStrip';
+import ToolbarToggler from "../toolbars/ToolbarToggler";
+
+export const SHARED_VIDEO_CONTAINER_TYPE = "sharedvideo";
+
+/**
+ * Example shared video link.
+ * @type {string}
+ */
+const defaultSharedVideoLink = "https://www.youtube.com/watch?v=xNXN7CZk8X0";
+
+/**
+ * Manager of shared video.
+ */
+export default class SharedVideoManager {
+ constructor (emitter) {
+ this.emitter = emitter;
+ this.isSharedVideoShown = false;
+ this.isPlayerAPILoaded = false;
+ this.updateInterval = 5000; // milliseconds
+ }
+
+ /**
+ * Starts shared video by asking user for url, or if its already working
+ * asks whether the user wants to stop sharing the video.
+ */
+ toggleSharedVideo () {
+ if(!this.isSharedVideoShown) {
+ requestVideoLink().then(
+ url => this.emitter.emit(
+ UIEvents.UPDATE_SHARED_VIDEO, url, 'start'),
+ err => console.error('SHARED VIDEO CANCELED', err)
+ );
+ return;
+ }
+
+ this.emitter.emit(UIEvents.UPDATE_SHARED_VIDEO, null, 'stop');
+ }
+
+ /**
+ * Shows the player component and starts the checking function
+ * that will be sending updates, if we are the one shared the video
+ * @param url the video url
+ * @param attributes
+ */
+ showSharedVideo (url, attributes) {
+ if (this.isSharedVideoShown)
+ return;
+
+ // the video url
+ this.url = url;
+
+ // the owner of the video
+ this.from = attributes.from;
+
+ // This code loads the IFrame Player API code asynchronously.
+ var tag = document.createElement('script');
+
+ tag.src = "https://www.youtube.com/iframe_api";
+ var firstScriptTag = document.getElementsByTagName('script')[0];
+ firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
+
+ var self = this;
+ if(self.isPlayerAPILoaded)
+ window.onYouTubeIframeAPIReady();
+ else
+ window.onYouTubeIframeAPIReady = function() {
+ self.isPlayerAPILoaded = true;
+ let showControls = APP.conference.isLocalId(self.from) ? 1 : 0;
+ self.player = new YT.Player('sharedVideoIFrame', {
+ height: '100%',
+ width: '100%',
+ videoId: self.url,
+ playerVars: {
+ 'origin': location.origin,
+ 'fs': '0',
+ 'autoplay': 1,
+ 'controls': showControls,
+ 'rel' : 0
+ },
+ events: {
+ 'onReady': onPlayerReady,
+ 'onStateChange': onPlayerStateChange,
+ 'onError': onPlayerError
+ }
+ });
+ };
+
+ window.onPlayerStateChange = function(event) {
+ if (event.data == YT.PlayerState.PLAYING) {
+ self.playerPaused = false;
+ self.updateCheck();
+ } else if (event.data == YT.PlayerState.PAUSED) {
+ self.playerPaused = true;
+ self.updateCheck();
+ }
+ };
+
+ window.onPlayerReady = function(event) {
+ let player = event.target;
+ player.playVideo();
+
+ let thumb = new SharedVideoThumb(self.url);
+ thumb.setDisplayName(player.getVideoData().title);
+ VideoLayout.addParticipantContainer(self.url, thumb);
+
+ let iframe = player.getIframe();
+ self.sharedVideo = new SharedVideoContainer(
+ {url, iframe, player});
+
+ VideoLayout.addLargeVideoContainer(
+ SHARED_VIDEO_CONTAINER_TYPE, self.sharedVideo);
+ VideoLayout.handleVideoThumbClicked(true, self.url);
+
+ self.isSharedVideoShown = true;
+
+ if(APP.conference.isLocalId(self.from)) {
+ self.intervalId = setInterval(
+ self.updateCheck.bind(self),
+ self.updateInterval);
+ }
+
+ // set initial state
+ if(attributes.state === 'pause')
+ player.pauseVideo();
+ else if(attributes.time > 0) {
+ console.log("Player seekTo:", attributes.time);
+ player.seekTo(attributes.time);
+ }
+ };
+
+ window.onPlayerError = function(event) {
+ console.error("Error in the player:" + event.data);
+ };
+ }
+
+ /**
+ * Checks current state of the player and fire an event with the values.
+ */
+ updateCheck()
+ {
+ // ignore update checks if we are not the owner of the video
+ if(!APP.conference.isLocalId(this.from))
+ return;
+
+ let state = this.player.getPlayerState();
+ // if its paused and haven't been pause - send paused
+ if (state === YT.PlayerState.PAUSED && !this.playerPaused) {
+ this.emitter.emit(UIEvents.UPDATE_SHARED_VIDEO,
+ this.url, 'pause');
+ }
+ // if its playing and it was paused - send update with time
+ // if its playing and was playing just send update with time
+ else if (state === YT.PlayerState.PLAYING) {
+ this.emitter.emit(UIEvents.UPDATE_SHARED_VIDEO,
+ this.url, 'playing',
+ this.player.getCurrentTime(),
+ this.player.isMuted() ? 0 : this.player.getVolume());
+ }
+ }
+
+ /**
+ * Updates video, if its not playing and needs starting or
+ * if its playing and needs to be paysed
+ * @param url the video url
+ * @param attributes
+ */
+ updateSharedVideo (url, attributes) {
+ // if we are sending the event ignore
+ if(APP.conference.isLocalId(this.from)) {
+ return;
+ }
+
+ if (attributes.state == 'playing') {
+
+ if(!this.isSharedVideoShown) {
+ this.showSharedVideo(url, attributes);
+ return;
+ }
+
+ // ocasionally we get this.player.getCurrentTime is not a function
+ // it seems its that player hasn't really loaded
+ if(!this.player || !this.player.getCurrentTime)
+ return;
+
+ // check received time and current time
+ let currentPosition = this.player.getCurrentTime();
+ let diff = Math.abs(attributes.time - currentPosition);
+
+ // if we drift more than two times of the interval for checking
+ // sync, the interval is in milliseconds
+ if(diff > this.updateInterval*2/1000) {
+ console.log("Player seekTo:", attributes.time,
+ " current time is:", currentPosition, " diff:", diff);
+ this.player.seekTo(attributes.time);
+ }
+
+ // lets check the volume
+ if (attributes.volume !== undefined &&
+ this.player.getVolume() != attributes.volume) {
+ this.player.setVolume(attributes.volume);
+ console.log("Player change of volume:" + attributes.volume);
+ }
+
+
+ if(this.playerPaused)
+ this.player.playVideo();
+ } else if (attributes.state == 'pause') {
+ // if its not paused, pause it
+ if(this.isSharedVideoShown) {
+ this.player.pauseVideo();
+ }
+ else {
+ // if not shown show it, passing attributes so it can
+ // be shown paused
+ this.showSharedVideo(url, attributes);
+ }
+ }
+ }
+
+ /**
+ * Stop shared video if it is currently showed. If the user started the
+ * shared video is the one in the attributes.from (called when user
+ * left and we want to remove video if the user sharing it left).
+ * @param attributes
+ */
+ stopSharedVideo (attributes) {
+ if (!this.isSharedVideoShown)
+ return;
+
+ if(this.from !== attributes.from)
+ return;
+
+ if(this.intervalId) {
+ clearInterval(this.intervalId);
+ this.intervalId = null;
+ }
+
+ VideoLayout.removeParticipantContainer(this.url);
+
+ VideoLayout.showLargeVideoContainer(SHARED_VIDEO_CONTAINER_TYPE, false)
+ .then(() => {
+ VideoLayout.removeLargeVideoContainer(
+ SHARED_VIDEO_CONTAINER_TYPE);
+
+ this.player.destroy();
+ this.player = null;
+ });
+
+ this.url = null;
+ this.isSharedVideoShown = false;
+ }
+}
+
+/**
+ * Container for shared video iframe.
+ */
+class SharedVideoContainer extends LargeContainer {
+
+ constructor ({url, iframe, player}) {
+ super();
+
+ this.$iframe = $(iframe);
+ this.url = url;
+ this.player = player;
+ }
+
+ get $video () {
+ return this.$iframe;
+ }
+
+ show () {
+ return new Promise(resolve => {
+ this.$iframe.fadeIn(300, () => {
+ this.$iframe.css({opacity: 1});
+ resolve();
+ });
+ });
+ }
+
+ hide () {
+ return new Promise(resolve => {
+ this.$iframe.fadeOut(300, () => {
+ this.$iframe.css({opacity: 0});
+ resolve();
+ });
+ });
+ }
+
+ onHoverIn () {
+ ToolbarToggler.showToolbar();
+ }
+
+ get id () {
+ return this.url;
+ }
+
+ resize (containerWidth, containerHeight) {
+ let height = containerHeight - FilmStrip.getFilmStripHeight();
+
+ let width = containerWidth;
+
+ this.$iframe.width(width).height(height);
+ }
+
+ /**
+ * @return {boolean} do not switch on dominant speaker event if on stage.
+ */
+ stayOnStage () {
+ return false;
+ }
+}
+
+function SharedVideoThumb (url)
+{
+ this.id = url;
+
+ this.url = url;
+ this.setVideoType(SHARED_VIDEO_CONTAINER_TYPE);
+ this.videoSpanId = "sharedVideoContainer";
+ this.container = this.createContainer(this.videoSpanId);
+ this.container.onclick = this.videoClick.bind(this);
+ //this.bindHoverHandler();
+
+ SmallVideo.call(this, VideoLayout);
+ this.isVideoMuted = true;
+}
+SharedVideoThumb.prototype = Object.create(SmallVideo.prototype);
+SharedVideoThumb.prototype.constructor = SharedVideoThumb;
+
+/**
+ * hide display name
+ */
+
+SharedVideoThumb.prototype.setDeviceAvailabilityIcons = function () {};
+
+SharedVideoThumb.prototype.avatarChanged = function () {};
+
+SharedVideoThumb.prototype.createContainer = function (spanId) {
+ var container = document.createElement('span');
+ container.id = spanId;
+ container.className = 'videocontainer';
+
+ // add the avatar
+ var avatar = document.createElement('img');
+ avatar.id = 'avatar_' + this.id;
+ avatar.className = 'sharedVideoAvatar';
+ avatar.src = "https://img.youtube.com/vi/" + this.url + "/0.jpg";
+ container.appendChild(avatar);
+
+ var remotes = document.getElementById('remoteVideos');
+ return remotes.appendChild(container);
+};
+
+/**
+ * The thumb click handler.
+ */
+SharedVideoThumb.prototype.videoClick = function () {
+ VideoLayout.handleVideoThumbClicked(true, this.url);
+ VideoLayout.showLargeVideoContainer(this.videoType, true);
+};
+
+/**
+ * Removes RemoteVideo from the page.
+ */
+SharedVideoThumb.prototype.remove = function () {
+ console.log("Remove shared video thumb", this.id);
+
+ // Remove whole container
+ if (this.container.parentNode) {
+ this.container.parentNode.removeChild(this.container);
+ }
+};
+
+/**
+ * Sets the display name for the thumb.
+ */
+SharedVideoThumb.prototype.setDisplayName = function(displayName) {
+ if (!this.container) {
+ console.warn( "Unable to set displayName - " + this.videoSpanId +
+ " does not exist");
+ return;
+ }
+
+ var nameSpan = $('#' + this.videoSpanId + '>span.displayname');
+
+ // If we already have a display name for this video.
+ if (nameSpan.length > 0) {
+ if (displayName && displayName.length > 0) {
+ $('#' + this.videoSpanId + '_name').text(displayName);
+ }
+ } else {
+ nameSpan = document.createElement('span');
+ nameSpan.className = 'displayname';
+ $('#' + this.videoSpanId)[0].appendChild(nameSpan);
+
+ if (displayName && displayName.length > 0)
+ $(nameSpan).text(displayName);
+ nameSpan.id = this.videoSpanId + '_name';
+ }
+
+};
+
+/**
+ * Checks if given string is youtube url.
+ * @param {string} url string to check.
+ * @returns {boolean}
+ */
+function getYoutubeLink(url) {
+ let p = /^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/;//jshint ignore:line
+ return (url.match(p)) ? RegExp.$1 : false;
+}
+
+/**
+ * Ask user for shared video url to share with others.
+ * Dialog validates client input to allow only youtube urls.
+ */
+function requestVideoLink() {
+ let i18n = APP.translation;
+ const title = i18n.generateTranslationHTML("dialog.shareVideoTitle");
+ const cancelButton = i18n.generateTranslationHTML("dialog.Cancel");
+ const shareButton = i18n.generateTranslationHTML("dialog.Share");
+ const backButton = i18n.generateTranslationHTML("dialog.Back");
+ const linkError
+ = i18n.generateTranslationHTML("dialog.shareVideoLinkError");
+ const i18nOptions = {url: defaultSharedVideoLink};
+ const defaultUrl = i18n.translateString("defaultLink", i18nOptions);
+
+ return new Promise(function (resolve, reject) {
+ let dialog = messageHandler.openDialogWithStates({
+ state0: {
+ html: `
+
${title}
+
`,
+ persistent: false,
+ buttons: [
+ {title: cancelButton, value: false},
+ {title: shareButton, value: true}
+ ],
+ focus: ':input:first',
+ defaultButton: 1,
+ submit: function (e, v, m, f) {
+ e.preventDefault();
+ if (!v) {
+ reject('cancelled');
+ dialog.close();
+ return;
+ }
+
+ let sharedVideoUrl = f.sharedVideoUrl;
+ if (!sharedVideoUrl) {
+ return;
+ }
+
+ let urlValue = encodeURI(UIUtil.escapeHtml(sharedVideoUrl));
+ let yVideoId = getYoutubeLink(urlValue);
+ if (!yVideoId) {
+ dialog.goToState('state1');
+ return false;
+ }
+
+ resolve(yVideoId);
+ dialog.close();
+ }
+ },
+
+ state1: {
+ html: `
${title}
${linkError}`,
+ persistent: false,
+ buttons: [
+ {title: cancelButton, value: false},
+ {title: backButton, value: true}
+ ],
+ focus: ':input:first',
+ defaultButton: 1,
+ submit: function (e, v, m, f) {
+ e.preventDefault();
+ if (v === 0) {
+ reject();
+ dialog.close();
+ } else {
+ dialog.goToState('state0');
+ }
+ }
+ }
+ });
+
+ });
+}
+
diff --git a/modules/UI/toolbars/Toolbar.js b/modules/UI/toolbars/Toolbar.js
index 992af2d22..890b13bb2 100644
--- a/modules/UI/toolbars/Toolbar.js
+++ b/modules/UI/toolbars/Toolbar.js
@@ -126,6 +126,10 @@ const buttonHandlers = {
AnalyticsAdapter.sendEvent('toolbar.etherpad.clicked');
emitter.emit(UIEvents.ETHERPAD_CLICKED);
},
+ "toolbar_button_sharedvideo": function () {
+ AnalyticsAdapter.sendEvent('toolbar.sharedvideo.clicked');
+ emitter.emit(UIEvents.SHARED_VIDEO_CLICKED);
+ },
"toolbar_button_desktopsharing": function () {
if (APP.conference.isSharingScreen) {
AnalyticsAdapter.sendEvent('toolbar.screen.disabled');
@@ -284,6 +288,15 @@ const Toolbar = {
}
},
+ // Shows or hides the 'shared video' button.
+ showSharedVideoButton (show) {
+ if (UIUtil.isButtonEnabled('sharedvideo') && show) {
+ $('#toolbar_button_sharedvideo').css({display: "inline-block"});
+ } else {
+ $('#toolbar_button_sharedvideo').css({display: "none"});
+ }
+ },
+
// checks whether recording is enabled and whether we have params
// to start automatically recording
checkAutoRecord () {
diff --git a/modules/UI/videolayout/LargeVideo.js b/modules/UI/videolayout/LargeVideo.js
index 494062af9..61a2d46c4 100644
--- a/modules/UI/videolayout/LargeVideo.js
+++ b/modules/UI/videolayout/LargeVideo.js
@@ -11,6 +11,8 @@ import {createDeferred} from '../../util/helpers';
const avatarSize = interfaceConfig.DOMINANT_SPEAKER_AVATAR_SIZE;
const FADE_DURATION_MS = 300;
+export const VIDEO_CONTAINER_TYPE = "camera";
+
/**
* Get stream id.
* @param {JitsiTrack?} stream
@@ -150,8 +152,6 @@ function getDesktopVideoPosition(videoWidth,
return { horizontalIndent, verticalIndent };
}
-export const VideoContainerType = "camera";
-
/**
* Container for user video.
*/
@@ -365,9 +365,10 @@ export default class LargeVideoManager {
constructor () {
this.containers = {};
- this.state = VideoContainerType;
- this.videoContainer = new VideoContainer(() => this.resizeContainer(VideoContainerType));
- this.addContainer(VideoContainerType, this.videoContainer);
+ this.state = VIDEO_CONTAINER_TYPE;
+ this.videoContainer = new VideoContainer(
+ () => this.resizeContainer(VIDEO_CONTAINER_TYPE));
+ this.addContainer(VIDEO_CONTAINER_TYPE, this.videoContainer);
// use the same video container to handle and desktop tracks
this.addContainer("desktop", this.videoContainer);
@@ -616,7 +617,7 @@ export default class LargeVideoManager {
}
let oldContainer = this.containers[this.state];
- if (this.state === VideoContainerType) {
+ if (this.state === VIDEO_CONTAINER_TYPE) {
this.showWatermark(false);
}
oldContainer.hide();
@@ -625,7 +626,7 @@ export default class LargeVideoManager {
let container = this.getContainer(type);
return container.show().then(() => {
- if (type === VideoContainerType) {
+ if (type === VIDEO_CONTAINER_TYPE) {
this.showWatermark(true);
}
});
diff --git a/modules/UI/videolayout/SmallVideo.js b/modules/UI/videolayout/SmallVideo.js
index fa5bac0bf..055bc460d 100644
--- a/modules/UI/videolayout/SmallVideo.js
+++ b/modules/UI/videolayout/SmallVideo.js
@@ -363,14 +363,7 @@ SmallVideo.prototype.updateView = function () {
}
setVisibility(avatar, showAvatar);
- var showDisplayName = !showVideo && !showAvatar;
-
- if (showDisplayName) {
- this.showDisplayName(this.VideoLayout.isLargeVideoVisible());
- }
- else {
- this.showDisplayName(false);
- }
+ this.showDisplayName(!showVideo && !showAvatar);
};
SmallVideo.prototype.avatarChanged = function (avatarUrl) {
diff --git a/modules/UI/videolayout/VideoLayout.js b/modules/UI/videolayout/VideoLayout.js
index 22e6e16ef..e5b4130b3 100644
--- a/modules/UI/videolayout/VideoLayout.js
+++ b/modules/UI/videolayout/VideoLayout.js
@@ -9,7 +9,8 @@ import UIEvents from "../../../service/UI/UIEvents";
import UIUtil from "../util/UIUtil";
import RemoteVideo from "./RemoteVideo";
-import LargeVideoManager, {VideoContainerType} from "./LargeVideo";
+import LargeVideoManager, {VIDEO_CONTAINER_TYPE} from "./LargeVideo";
+import {SHARED_VIDEO_CONTAINER_TYPE} from '../shared_video/SharedVideo';
import LocalVideo from "./LocalVideo";
import PanelToggler from "../side_pannels/SidePanelToggler";
@@ -343,7 +344,7 @@ var VideoLayout = {
let videoType = VideoLayout.getRemoteVideoType(id);
if (!videoType) {
// make video type the default one (camera)
- videoType = VideoContainerType;
+ videoType = VIDEO_CONTAINER_TYPE;
}
remoteVideo.setVideoType(videoType);
@@ -367,7 +368,8 @@ var VideoLayout = {
// current dominant, focused speaker or update it to
// the current dominant speaker.
if ((!focusedVideoResourceJid &&
- !currentDominantSpeaker) ||
+ !currentDominantSpeaker &&
+ !this.isLargeContainerTypeVisible(SHARED_VIDEO_CONTAINER_TYPE)) ||
focusedVideoResourceJid === resourceJid ||
(resourceJid &&
currentDominantSpeaker === resourceJid)) {
@@ -888,7 +890,7 @@ var VideoLayout = {
},
isLargeVideoVisible () {
- return this.isLargeContainerTypeVisible(VideoContainerType);
+ return this.isLargeContainerTypeVisible(VIDEO_CONTAINER_TYPE);
},
/**
@@ -960,8 +962,17 @@ var VideoLayout = {
return Promise.resolve();
}
+ let currentId = largeVideo.id;
+ if(currentId) {
+ var oldSmallVideo = this.getSmallVideo(currentId);
+ }
+
// if !show then use default type - large video
- return largeVideo.showContainer(show ? type : VideoContainerType);
+ return largeVideo.showContainer(show ? type : VIDEO_CONTAINER_TYPE)
+ .then(() => {
+ if(oldSmallVideo)
+ oldSmallVideo && oldSmallVideo.updateView();
+ });
},
isLargeContainerTypeVisible (type) {
@@ -970,10 +981,18 @@ var VideoLayout = {
/**
* Returns the id of the current video shown on large.
- * Currently used by tests (troture).
+ * Currently used by tests (torture).
*/
getLargeVideoID () {
return largeVideo.id;
+ },
+
+ /**
+ * Returns the the current video shown on large.
+ * Currently used by tests (torture).
+ */
+ getLargeVideo () {
+ return largeVideo;
}
};
diff --git a/service/UI/UIEvents.js b/service/UI/UIEvents.js
index cc507164c..5444ebb0a 100644
--- a/service/UI/UIEvents.js
+++ b/service/UI/UIEvents.js
@@ -21,6 +21,13 @@ export default {
AUDIO_MUTED: "UI.audio_muted",
VIDEO_MUTED: "UI.video_muted",
ETHERPAD_CLICKED: "UI.etherpad_clicked",
+ SHARED_VIDEO_CLICKED: "UI.start-shared-video",
+ /**
+ * Updates shared video with params: url, state, time(optional)
+ * Where url is the video link, state is stop/start/pause and time is the
+ * current video playing time.
+ */
+ UPDATE_SHARED_VIDEO: "UI.update-shared-video",
ROOM_LOCK_CLICKED: "UI.room_lock_clicked",
USER_INVITED: "UI.user_invited",
USER_KICKED: "UI.user_kicked",