2016-08-09 20:04:40 +00:00
|
|
|
/* global $, APP, YT, onPlayerReady, onPlayerStateChange, onPlayerError,
|
|
|
|
JitsiMeetJS */
|
2016-11-11 15:00:54 +00:00
|
|
|
const logger = require("jitsi-meet-logger").getLogger(__filename);
|
2016-03-18 20:00:55 +00:00
|
|
|
|
|
|
|
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';
|
2017-02-16 23:02:40 +00:00
|
|
|
|
2017-04-01 05:52:40 +00:00
|
|
|
import { dockToolbox, showToolbox } from '../../../react/features/toolbox';
|
2016-03-18 20:00:55 +00:00
|
|
|
|
|
|
|
export const SHARED_VIDEO_CONTAINER_TYPE = "sharedvideo";
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Example shared video link.
|
|
|
|
* @type {string}
|
|
|
|
*/
|
|
|
|
const defaultSharedVideoLink = "https://www.youtube.com/watch?v=xNXN7CZk8X0";
|
2016-03-25 20:36:41 +00:00
|
|
|
const updateInterval = 5000; // milliseconds
|
2016-07-06 18:26:27 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The dialog for user input (video link).
|
|
|
|
* @type {null}
|
|
|
|
*/
|
|
|
|
let dialog = null;
|
|
|
|
|
2016-03-18 20:00:55 +00:00
|
|
|
/**
|
|
|
|
* Manager of shared video.
|
|
|
|
*/
|
|
|
|
export default class SharedVideoManager {
|
|
|
|
constructor (emitter) {
|
|
|
|
this.emitter = emitter;
|
|
|
|
this.isSharedVideoShown = false;
|
|
|
|
this.isPlayerAPILoaded = false;
|
2016-04-19 18:07:04 +00:00
|
|
|
this.mutedWithUserInteraction = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2016-04-25 20:39:31 +00:00
|
|
|
* Indicates if the player volume is currently on. This will return true if
|
|
|
|
* we have an available player, which is currently in a PLAYING state,
|
|
|
|
* which isn't muted and has it's volume greater than 0.
|
2016-04-19 18:07:04 +00:00
|
|
|
*
|
2016-04-25 20:39:31 +00:00
|
|
|
* @returns {boolean} indicating if the volume of the shared video is
|
|
|
|
* currently on.
|
2016-04-19 18:07:04 +00:00
|
|
|
*/
|
|
|
|
isSharedVideoVolumeOn() {
|
2016-04-25 20:39:31 +00:00
|
|
|
return (this.player
|
|
|
|
&& this.player.getPlayerState() === YT.PlayerState.PLAYING
|
|
|
|
&& !this.player.isMuted()
|
|
|
|
&& this.player.getVolume() > 0);
|
2016-04-19 18:07:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Indicates if the local user is the owner of the shared video.
|
|
|
|
* @returns {*|boolean}
|
|
|
|
*/
|
|
|
|
isSharedVideoOwner() {
|
|
|
|
return this.from && APP.conference.isLocalId(this.from);
|
2016-03-18 20:00:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Starts shared video by asking user for url, or if its already working
|
|
|
|
* asks whether the user wants to stop sharing the video.
|
|
|
|
*/
|
|
|
|
toggleSharedVideo () {
|
2016-07-06 18:26:27 +00:00
|
|
|
if (dialog)
|
|
|
|
return;
|
|
|
|
|
2016-03-18 20:00:55 +00:00
|
|
|
if(!this.isSharedVideoShown) {
|
|
|
|
requestVideoLink().then(
|
2016-08-04 19:19:09 +00:00
|
|
|
url => {
|
|
|
|
this.emitter.emit(
|
|
|
|
UIEvents.UPDATE_SHARED_VIDEO, url, 'start');
|
2016-08-09 20:04:40 +00:00
|
|
|
JitsiMeetJS.analytics.sendEvent('sharedvideo.started');
|
2016-08-04 19:19:09 +00:00
|
|
|
},
|
|
|
|
err => {
|
2016-11-11 15:00:54 +00:00
|
|
|
logger.log('SHARED VIDEO CANCELED', err);
|
2016-08-09 20:04:40 +00:00
|
|
|
JitsiMeetJS.analytics.sendEvent('sharedvideo.canceled');
|
2016-08-04 19:19:09 +00:00
|
|
|
}
|
2016-03-18 20:00:55 +00:00
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2016-03-26 00:38:56 +00:00
|
|
|
if(APP.conference.isLocalId(this.from)) {
|
2016-08-04 19:19:09 +00:00
|
|
|
showStopVideoPropmpt().then(() => {
|
2016-11-09 18:42:47 +00:00
|
|
|
// make sure we stop updates for playing before we send stop
|
|
|
|
// if we stop it after receiving self presence, we can end
|
|
|
|
// up sending stop playing, and on the other end it will not
|
|
|
|
// stop
|
|
|
|
if(this.intervalId) {
|
|
|
|
clearInterval(this.intervalId);
|
|
|
|
this.intervalId = null;
|
|
|
|
}
|
2016-08-04 19:19:09 +00:00
|
|
|
this.emitter.emit(
|
|
|
|
UIEvents.UPDATE_SHARED_VIDEO, this.url, 'stop');
|
2016-08-09 20:04:40 +00:00
|
|
|
JitsiMeetJS.analytics.sendEvent('sharedvideo.stoped');
|
2016-08-04 19:19:09 +00:00
|
|
|
},
|
2016-07-06 18:26:27 +00:00
|
|
|
() => {});
|
2016-03-26 00:38:56 +00:00
|
|
|
} else {
|
2016-07-06 18:26:27 +00:00
|
|
|
dialog = APP.UI.messageHandler.openMessageDialog(
|
2016-03-26 00:38:56 +00:00
|
|
|
"dialog.shareVideoTitle",
|
2016-07-06 18:26:27 +00:00
|
|
|
"dialog.alreadySharedVideoMsg",
|
2016-10-17 22:33:30 +00:00
|
|
|
null,
|
2016-07-06 18:26:27 +00:00
|
|
|
function () {
|
|
|
|
dialog = null;
|
|
|
|
}
|
2016-03-26 00:38:56 +00:00
|
|
|
);
|
2016-08-09 20:04:40 +00:00
|
|
|
JitsiMeetJS.analytics.sendEvent('sharedvideo.alreadyshared');
|
2016-03-26 00:38:56 +00:00
|
|
|
}
|
2016-03-18 20:00:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2016-04-28 16:54:10 +00:00
|
|
|
* Shows the player component and starts the process that will be sending
|
|
|
|
* updates, if we are the one shared the video.
|
|
|
|
*
|
2016-03-24 20:25:26 +00:00
|
|
|
* @param id the id of the sender of the command
|
2016-03-18 20:00:55 +00:00
|
|
|
* @param url the video url
|
|
|
|
* @param attributes
|
|
|
|
*/
|
2016-04-28 16:54:10 +00:00
|
|
|
onSharedVideoStart (id, url, attributes) {
|
2016-03-18 20:00:55 +00:00
|
|
|
if (this.isSharedVideoShown)
|
|
|
|
return;
|
|
|
|
|
2016-03-31 02:36:05 +00:00
|
|
|
this.isSharedVideoShown = true;
|
|
|
|
|
2016-03-18 20:00:55 +00:00
|
|
|
// the video url
|
|
|
|
this.url = url;
|
|
|
|
|
|
|
|
// the owner of the video
|
2016-03-24 20:25:26 +00:00
|
|
|
this.from = id;
|
2016-03-18 20:00:55 +00:00
|
|
|
|
2016-04-28 22:42:52 +00:00
|
|
|
this.mutedWithUserInteraction = APP.conference.isLocalAudioMuted();
|
|
|
|
|
2016-04-01 19:53:11 +00:00
|
|
|
//listen for local audio mute events
|
2016-04-25 20:39:31 +00:00
|
|
|
this.localAudioMutedListener = this.onLocalAudioMuted.bind(this);
|
2016-04-01 19:53:11 +00:00
|
|
|
this.emitter.on(UIEvents.AUDIO_MUTED, this.localAudioMutedListener);
|
|
|
|
|
2016-03-18 20:00:55 +00:00
|
|
|
// 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);
|
|
|
|
|
2016-03-25 20:36:41 +00:00
|
|
|
// sometimes we receive errors like player not defined
|
|
|
|
// or player.pauseVideo is not a function
|
|
|
|
// we need to operate with player after start playing
|
|
|
|
// self.player will be defined once it start playing
|
|
|
|
// and will process any initial attributes if any
|
2016-03-31 17:14:45 +00:00
|
|
|
this.initialAttributes = attributes;
|
2016-03-25 20:36:41 +00:00
|
|
|
|
2016-03-18 20:00:55 +00:00
|
|
|
var self = this;
|
|
|
|
if(self.isPlayerAPILoaded)
|
|
|
|
window.onYouTubeIframeAPIReady();
|
|
|
|
else
|
|
|
|
window.onYouTubeIframeAPIReady = function() {
|
|
|
|
self.isPlayerAPILoaded = true;
|
|
|
|
let showControls = APP.conference.isLocalId(self.from) ? 1 : 0;
|
2016-04-01 22:08:35 +00:00
|
|
|
let p = new YT.Player('sharedVideoIFrame', {
|
2016-03-18 20:00:55 +00:00
|
|
|
height: '100%',
|
|
|
|
width: '100%',
|
|
|
|
videoId: self.url,
|
|
|
|
playerVars: {
|
|
|
|
'origin': location.origin,
|
|
|
|
'fs': '0',
|
2016-03-25 20:36:41 +00:00
|
|
|
'autoplay': 0,
|
2016-03-18 20:00:55 +00:00
|
|
|
'controls': showControls,
|
|
|
|
'rel' : 0
|
|
|
|
},
|
|
|
|
events: {
|
|
|
|
'onReady': onPlayerReady,
|
|
|
|
'onStateChange': onPlayerStateChange,
|
|
|
|
'onError': onPlayerError
|
|
|
|
}
|
2016-04-01 22:08:35 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
// add listener for volume changes
|
|
|
|
p.addEventListener(
|
2016-04-01 19:53:11 +00:00
|
|
|
"onVolumeChange", "onVolumeChange");
|
2016-04-01 22:08:35 +00:00
|
|
|
|
|
|
|
if (APP.conference.isLocalId(self.from)){
|
|
|
|
// adds progress listener that will be firing events
|
|
|
|
// while we are paused and we change the progress of the
|
|
|
|
// video (seeking forward or backward on the video)
|
|
|
|
p.addEventListener(
|
|
|
|
"onVideoProgress", "onVideoProgress");
|
|
|
|
}
|
2016-03-18 20:00:55 +00:00
|
|
|
};
|
|
|
|
|
2016-04-28 16:54:10 +00:00
|
|
|
/**
|
|
|
|
* Indicates that a change in state has occurred for the shared video.
|
|
|
|
* @param event the event notifying us of the change
|
|
|
|
*/
|
2016-03-18 20:00:55 +00:00
|
|
|
window.onPlayerStateChange = function(event) {
|
|
|
|
if (event.data == YT.PlayerState.PLAYING) {
|
2016-03-24 03:42:17 +00:00
|
|
|
|
2016-03-25 20:36:41 +00:00
|
|
|
self.player = event.target;
|
|
|
|
|
|
|
|
if(self.initialAttributes)
|
|
|
|
{
|
2016-04-28 16:54:10 +00:00
|
|
|
// If a network update has occurred already now is the
|
|
|
|
// time to process it.
|
|
|
|
self.processVideoUpdate(
|
2016-04-25 20:39:31 +00:00
|
|
|
self.player,
|
2016-04-28 16:54:10 +00:00
|
|
|
self.initialAttributes);
|
2016-04-25 20:39:31 +00:00
|
|
|
|
2016-03-25 20:36:41 +00:00
|
|
|
self.initialAttributes = null;
|
2016-03-24 03:42:17 +00:00
|
|
|
}
|
2016-04-28 16:54:10 +00:00
|
|
|
self.smartAudioMute();
|
2016-03-18 20:00:55 +00:00
|
|
|
} else if (event.data == YT.PlayerState.PAUSED) {
|
2016-04-28 16:54:10 +00:00
|
|
|
self.smartAudioUnmute();
|
2016-08-09 20:04:40 +00:00
|
|
|
JitsiMeetJS.analytics.sendEvent('sharedvideo.paused');
|
2016-03-18 20:00:55 +00:00
|
|
|
}
|
2016-04-28 16:54:10 +00:00
|
|
|
self.fireSharedVideoEvent(event.data == YT.PlayerState.PAUSED);
|
2016-03-18 20:00:55 +00:00
|
|
|
};
|
|
|
|
|
2016-04-01 22:08:35 +00:00
|
|
|
/**
|
|
|
|
* Track player progress while paused.
|
|
|
|
* @param event
|
|
|
|
*/
|
|
|
|
window.onVideoProgress = function (event) {
|
|
|
|
let state = event.target.getPlayerState();
|
|
|
|
if (state == YT.PlayerState.PAUSED) {
|
2016-04-28 16:54:10 +00:00
|
|
|
self.fireSharedVideoEvent(true);
|
2016-04-01 22:08:35 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2016-03-31 20:32:13 +00:00
|
|
|
/**
|
|
|
|
* Gets notified for volume state changed.
|
|
|
|
* @param event
|
|
|
|
*/
|
|
|
|
window.onVolumeChange = function (event) {
|
2016-04-28 16:54:10 +00:00
|
|
|
self.fireSharedVideoEvent();
|
2016-04-01 19:53:11 +00:00
|
|
|
|
|
|
|
// let's check, if player is not muted lets mute locally
|
2016-04-25 20:39:31 +00:00
|
|
|
if(event.data.volume > 0 && !event.data.muted) {
|
2016-04-28 16:54:10 +00:00
|
|
|
self.smartAudioMute();
|
2016-04-01 19:53:11 +00:00
|
|
|
}
|
2016-04-25 20:39:31 +00:00
|
|
|
else if (event.data.volume <=0 || event.data.muted) {
|
2016-04-28 16:54:10 +00:00
|
|
|
self.smartAudioUnmute();
|
2016-04-19 18:07:04 +00:00
|
|
|
}
|
2016-08-09 20:04:40 +00:00
|
|
|
JitsiMeetJS.analytics.sendEvent('sharedvideo.volumechanged');
|
2016-03-31 20:32:13 +00:00
|
|
|
};
|
|
|
|
|
2016-03-18 20:00:55 +00:00
|
|
|
window.onPlayerReady = function(event) {
|
|
|
|
let player = event.target;
|
2016-03-25 20:36:41 +00:00
|
|
|
// do not relay on autoplay as it is not sending all of the events
|
|
|
|
// in onPlayerStateChange
|
2016-03-18 20:00:55 +00:00
|
|
|
player.playVideo();
|
|
|
|
|
|
|
|
let thumb = new SharedVideoThumb(self.url);
|
|
|
|
thumb.setDisplayName(player.getVideoData().title);
|
2016-09-16 20:17:00 +00:00
|
|
|
VideoLayout.addRemoteVideoContainer(self.url, thumb);
|
2016-03-18 20:00:55 +00:00
|
|
|
|
|
|
|
let iframe = player.getIframe();
|
|
|
|
self.sharedVideo = new SharedVideoContainer(
|
|
|
|
{url, iframe, player});
|
|
|
|
|
2016-04-01 22:23:30 +00:00
|
|
|
//prevents pausing participants not sharing the video
|
|
|
|
// to pause the video
|
|
|
|
if (!APP.conference.isLocalId(self.from)) {
|
|
|
|
$("#sharedVideo").css("pointer-events","none");
|
|
|
|
}
|
|
|
|
|
2016-03-18 20:00:55 +00:00
|
|
|
VideoLayout.addLargeVideoContainer(
|
|
|
|
SHARED_VIDEO_CONTAINER_TYPE, self.sharedVideo);
|
2016-03-24 01:43:29 +00:00
|
|
|
VideoLayout.handleVideoThumbClicked(self.url);
|
2016-03-18 20:00:55 +00:00
|
|
|
|
2016-03-22 19:49:37 +00:00
|
|
|
// If we are sending the command and we are starting the player
|
|
|
|
// we need to continuously send the player current time position
|
2016-03-18 20:00:55 +00:00
|
|
|
if(APP.conference.isLocalId(self.from)) {
|
|
|
|
self.intervalId = setInterval(
|
2016-04-28 16:54:10 +00:00
|
|
|
self.fireSharedVideoEvent.bind(self),
|
2016-03-25 20:36:41 +00:00
|
|
|
updateInterval);
|
2016-03-18 20:00:55 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
window.onPlayerError = function(event) {
|
2016-11-11 15:00:54 +00:00
|
|
|
logger.error("Error in the player:", event.data);
|
2016-03-31 19:11:33 +00:00
|
|
|
// store the error player, so we can remove it
|
|
|
|
self.errorInPlayer = event.target;
|
2016-03-18 20:00:55 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2016-03-25 20:36:41 +00:00
|
|
|
/**
|
|
|
|
* Process attributes, whether player needs to be paused or seek.
|
|
|
|
* @param player the player to operate over
|
|
|
|
* @param attributes the attributes with the player state we want
|
|
|
|
*/
|
2016-04-28 16:54:10 +00:00
|
|
|
processVideoUpdate (player, attributes)
|
2016-03-25 20:36:41 +00:00
|
|
|
{
|
|
|
|
if(!attributes)
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (attributes.state == 'playing') {
|
|
|
|
|
2016-04-28 16:54:10 +00:00
|
|
|
let isPlayerPaused
|
|
|
|
= (this.player.getPlayerState() === YT.PlayerState.PAUSED);
|
2016-03-25 20:36:41 +00:00
|
|
|
|
2016-04-28 16:54:10 +00:00
|
|
|
// If our player is currently paused force the seek.
|
|
|
|
this.processTime(player, attributes, isPlayerPaused);
|
2016-04-25 20:39:31 +00:00
|
|
|
|
2016-04-28 16:54:10 +00:00
|
|
|
// Process mute.
|
|
|
|
let isAttrMuted = (attributes.muted === "true");
|
|
|
|
if (player.isMuted() !== isAttrMuted) {
|
|
|
|
this.smartPlayerMute(isAttrMuted, true);
|
2016-04-25 20:39:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Process volume
|
|
|
|
if (!isAttrMuted
|
|
|
|
&& attributes.volume !== undefined
|
|
|
|
&& player.getVolume() != attributes.volume) {
|
2016-04-19 18:07:04 +00:00
|
|
|
|
2016-03-25 20:36:41 +00:00
|
|
|
player.setVolume(attributes.volume);
|
2016-11-11 15:00:54 +00:00
|
|
|
logger.info("Player change of volume:" + attributes.volume);
|
2016-04-05 19:17:50 +00:00
|
|
|
this.showSharedVideoMutedPopup(false);
|
2016-03-25 20:36:41 +00:00
|
|
|
}
|
|
|
|
|
2016-04-28 16:54:10 +00:00
|
|
|
if (isPlayerPaused)
|
2016-03-25 20:36:41 +00:00
|
|
|
player.playVideo();
|
|
|
|
|
|
|
|
} else if (attributes.state == 'pause') {
|
|
|
|
// if its not paused, pause it
|
|
|
|
player.pauseVideo();
|
2016-03-27 20:32:45 +00:00
|
|
|
|
2016-04-01 22:08:35 +00:00
|
|
|
this.processTime(player, attributes, true);
|
2016-03-27 20:32:45 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check for time in attributes and if needed seek in current player
|
|
|
|
* @param player the player to operate over
|
|
|
|
* @param attributes the attributes with the player state we want
|
2016-03-31 05:07:12 +00:00
|
|
|
* @param forceSeek whether seek should be forced
|
2016-03-27 20:32:45 +00:00
|
|
|
*/
|
2016-03-31 05:07:12 +00:00
|
|
|
processTime (player, attributes, forceSeek)
|
2016-03-27 20:32:45 +00:00
|
|
|
{
|
2016-03-31 05:07:12 +00:00
|
|
|
if(forceSeek) {
|
2016-11-11 15:00:54 +00:00
|
|
|
logger.info("Player seekTo:", attributes.time);
|
2016-03-31 05:07:12 +00:00
|
|
|
player.seekTo(attributes.time);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2016-03-27 20:32:45 +00:00
|
|
|
// check received time and current time
|
|
|
|
let currentPosition = player.getCurrentTime();
|
|
|
|
let diff = Math.abs(attributes.time - currentPosition);
|
|
|
|
|
|
|
|
// if we drift more than the interval for checking
|
|
|
|
// sync, the interval is in milliseconds
|
|
|
|
if(diff > updateInterval/1000) {
|
2016-11-11 15:00:54 +00:00
|
|
|
logger.info("Player seekTo:", attributes.time,
|
2016-03-27 20:32:45 +00:00
|
|
|
" current time is:", currentPosition, " diff:", diff);
|
|
|
|
player.seekTo(attributes.time);
|
2016-03-25 20:36:41 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-03-18 20:00:55 +00:00
|
|
|
/**
|
|
|
|
* Checks current state of the player and fire an event with the values.
|
|
|
|
*/
|
2016-04-28 16:54:10 +00:00
|
|
|
fireSharedVideoEvent(sendPauseEvent)
|
2016-03-18 20:00:55 +00:00
|
|
|
{
|
|
|
|
// ignore update checks if we are not the owner of the video
|
2016-03-31 17:14:45 +00:00
|
|
|
// or there is still no player defined or we are stopped
|
|
|
|
// (in a process of stopping)
|
|
|
|
if(!APP.conference.isLocalId(this.from) || !this.player
|
|
|
|
|| !this.isSharedVideoShown)
|
2016-03-18 20:00:55 +00:00
|
|
|
return;
|
|
|
|
|
|
|
|
let state = this.player.getPlayerState();
|
|
|
|
// if its paused and haven't been pause - send paused
|
2016-03-18 21:47:47 +00:00
|
|
|
if (state === YT.PlayerState.PAUSED && sendPauseEvent) {
|
2016-03-18 20:00:55 +00:00
|
|
|
this.emitter.emit(UIEvents.UPDATE_SHARED_VIDEO,
|
2016-03-27 20:32:45 +00:00
|
|
|
this.url, 'pause', this.player.getCurrentTime());
|
2016-03-18 20:00:55 +00:00
|
|
|
}
|
|
|
|
// 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(),
|
2016-04-25 20:39:31 +00:00
|
|
|
this.player.isMuted(),
|
|
|
|
this.player.getVolume());
|
2016-03-18 20:00:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Updates video, if its not playing and needs starting or
|
|
|
|
* if its playing and needs to be paysed
|
2016-03-24 20:25:26 +00:00
|
|
|
* @param id the id of the sender of the command
|
2016-03-18 20:00:55 +00:00
|
|
|
* @param url the video url
|
|
|
|
* @param attributes
|
|
|
|
*/
|
2016-04-28 16:54:10 +00:00
|
|
|
onSharedVideoUpdate (id, url, attributes) {
|
2016-03-18 20:00:55 +00:00
|
|
|
// if we are sending the event ignore
|
|
|
|
if(APP.conference.isLocalId(this.from)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2016-03-25 20:36:41 +00:00
|
|
|
if(!this.isSharedVideoShown) {
|
2016-04-28 16:54:10 +00:00
|
|
|
this.onSharedVideoStart(id, url, attributes);
|
2016-03-25 20:36:41 +00:00
|
|
|
return;
|
|
|
|
}
|
2016-03-18 20:00:55 +00:00
|
|
|
|
2016-03-25 20:36:41 +00:00
|
|
|
if(!this.player)
|
|
|
|
this.initialAttributes = attributes;
|
|
|
|
else {
|
2016-04-28 16:54:10 +00:00
|
|
|
this.processVideoUpdate(this.player, attributes);
|
2016-03-18 20:00:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Stop shared video if it is currently showed. If the user started the
|
2016-03-24 20:25:26 +00:00
|
|
|
* shared video is the one in the id (called when user
|
2016-03-18 20:00:55 +00:00
|
|
|
* left and we want to remove video if the user sharing it left).
|
2016-03-24 20:25:26 +00:00
|
|
|
* @param id the id of the sender of the command
|
2016-03-18 20:00:55 +00:00
|
|
|
*/
|
2016-04-28 16:54:10 +00:00
|
|
|
onSharedVideoStop (id, attributes) {
|
2016-03-18 20:00:55 +00:00
|
|
|
if (!this.isSharedVideoShown)
|
|
|
|
return;
|
|
|
|
|
2016-03-24 20:25:26 +00:00
|
|
|
if(this.from !== id)
|
2016-03-18 20:00:55 +00:00
|
|
|
return;
|
|
|
|
|
2016-04-25 20:39:31 +00:00
|
|
|
if(!this.player) {
|
2016-03-31 19:11:33 +00:00
|
|
|
// if there is no error in the player till now,
|
|
|
|
// store the initial attributes
|
|
|
|
if (!this.errorInPlayer) {
|
|
|
|
this.initialAttributes = attributes;
|
|
|
|
return;
|
|
|
|
}
|
2016-03-31 17:14:45 +00:00
|
|
|
}
|
|
|
|
|
2016-04-01 19:53:11 +00:00
|
|
|
this.emitter.removeListener(UIEvents.AUDIO_MUTED,
|
|
|
|
this.localAudioMutedListener);
|
|
|
|
this.localAudioMutedListener = null;
|
|
|
|
|
2016-03-18 20:00:55 +00:00
|
|
|
VideoLayout.removeParticipantContainer(this.url);
|
|
|
|
|
|
|
|
VideoLayout.showLargeVideoContainer(SHARED_VIDEO_CONTAINER_TYPE, false)
|
|
|
|
.then(() => {
|
|
|
|
VideoLayout.removeLargeVideoContainer(
|
|
|
|
SHARED_VIDEO_CONTAINER_TYPE);
|
|
|
|
|
2016-03-31 19:11:33 +00:00
|
|
|
if(this.player) {
|
|
|
|
this.player.destroy();
|
|
|
|
this.player = null;
|
2016-04-01 19:53:11 +00:00
|
|
|
} // if there is an error in player, remove that instance
|
2016-03-31 19:11:33 +00:00
|
|
|
else if (this.errorInPlayer) {
|
|
|
|
this.errorInPlayer.destroy();
|
|
|
|
this.errorInPlayer = null;
|
|
|
|
}
|
2016-04-28 16:54:10 +00:00
|
|
|
this.smartAudioUnmute();
|
2016-04-01 22:23:30 +00:00
|
|
|
// revert to original behavior (prevents pausing
|
|
|
|
// for participants not sharing the video to pause it)
|
|
|
|
$("#sharedVideo").css("pointer-events","auto");
|
2016-03-18 20:00:55 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
this.url = null;
|
|
|
|
this.isSharedVideoShown = false;
|
2016-03-31 19:11:33 +00:00
|
|
|
this.initialAttributes = null;
|
2016-03-18 20:00:55 +00:00
|
|
|
}
|
2016-04-01 19:53:11 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Receives events for local audio mute/unmute by local user.
|
|
|
|
* @param muted boolena whether it is muted or not.
|
2016-04-19 18:07:04 +00:00
|
|
|
* @param {boolean} indicates if this mute was a result of user interaction,
|
|
|
|
* i.e. pressing the mute button or it was programatically triggerred
|
2016-04-01 19:53:11 +00:00
|
|
|
*/
|
2016-04-25 20:39:31 +00:00
|
|
|
onLocalAudioMuted (muted, userInteraction) {
|
2016-04-01 19:53:11 +00:00
|
|
|
if(!this.player)
|
|
|
|
return;
|
|
|
|
|
2016-04-19 18:07:04 +00:00
|
|
|
if (muted) {
|
|
|
|
this.mutedWithUserInteraction = userInteraction;
|
|
|
|
}
|
2016-04-26 07:22:12 +00:00
|
|
|
else if (this.player.getPlayerState() !== YT.PlayerState.PAUSED) {
|
2016-04-28 16:54:10 +00:00
|
|
|
this.smartPlayerMute(true, false);
|
|
|
|
// Check if we need to update other participants
|
|
|
|
this.fireSharedVideoEvent();
|
2016-04-25 20:39:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Mutes / unmutes the player.
|
|
|
|
* @param mute true to mute the shared video, false - otherwise.
|
2016-04-28 16:54:10 +00:00
|
|
|
* @param {boolean} Indicates if this mute is a consequence of a network
|
|
|
|
* video update or is called locally.
|
2016-04-25 20:39:31 +00:00
|
|
|
*/
|
2016-04-28 16:54:10 +00:00
|
|
|
smartPlayerMute(mute, isVideoUpdate) {
|
2016-04-25 20:39:31 +00:00
|
|
|
if (!this.player.isMuted() && mute) {
|
|
|
|
this.player.mute();
|
2016-04-28 16:54:10 +00:00
|
|
|
|
|
|
|
if (isVideoUpdate)
|
|
|
|
this.smartAudioUnmute();
|
2016-04-25 20:39:31 +00:00
|
|
|
}
|
|
|
|
else if (this.player.isMuted() && !mute) {
|
|
|
|
this.player.unMute();
|
2016-04-28 16:54:10 +00:00
|
|
|
if (isVideoUpdate)
|
|
|
|
this.smartAudioMute();
|
2016-04-25 20:39:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
this.showSharedVideoMutedPopup(mute);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Smart mike unmute. If the mike is currently muted and it wasn't muted
|
|
|
|
* by the user via the mike button and the volume of the shared video is on
|
|
|
|
* we're unmuting the mike automatically.
|
|
|
|
*/
|
2016-04-28 16:54:10 +00:00
|
|
|
smartAudioUnmute() {
|
2016-04-25 20:39:31 +00:00
|
|
|
if (APP.conference.isLocalAudioMuted()
|
|
|
|
&& !this.mutedWithUserInteraction
|
|
|
|
&& !this.isSharedVideoVolumeOn()) {
|
|
|
|
|
|
|
|
this.emitter.emit(UIEvents.AUDIO_MUTED, false, false);
|
|
|
|
this.showMicMutedPopup(false);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Smart mike mute. If the mike isn't currently muted and the shared video
|
|
|
|
* volume is on we mute the mike.
|
|
|
|
*/
|
2016-04-28 16:54:10 +00:00
|
|
|
smartAudioMute() {
|
2016-04-25 20:39:31 +00:00
|
|
|
if (!APP.conference.isLocalAudioMuted()
|
|
|
|
&& this.isSharedVideoVolumeOn()) {
|
2016-04-01 19:53:11 +00:00
|
|
|
|
2016-04-25 20:39:31 +00:00
|
|
|
this.emitter.emit(UIEvents.AUDIO_MUTED, true, false);
|
|
|
|
this.showMicMutedPopup(true);
|
2016-04-01 19:53:11 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2016-04-05 19:17:50 +00:00
|
|
|
* Shows a popup under the microphone toolbar icon that notifies the user
|
|
|
|
* of automatic mute after a shared video has started.
|
2016-04-01 19:53:11 +00:00
|
|
|
* @param show boolean, show or hide the notification
|
|
|
|
*/
|
2016-04-05 19:17:50 +00:00
|
|
|
showMicMutedPopup (show) {
|
2016-04-19 18:07:04 +00:00
|
|
|
if(show)
|
2016-04-05 19:17:50 +00:00
|
|
|
this.showSharedVideoMutedPopup(false);
|
|
|
|
|
2017-02-15 08:54:57 +00:00
|
|
|
APP.UI.showCustomToolbarPopup('#micMutedPopup', show, 5000);
|
2016-04-01 19:53:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2016-04-05 19:17:50 +00:00
|
|
|
* Shows a popup under the shared video toolbar icon that notifies the user
|
|
|
|
* of automatic mute of the shared video after the user has unmuted their
|
|
|
|
* mic.
|
2016-04-01 19:53:11 +00:00
|
|
|
* @param show boolean, show or hide the notification
|
|
|
|
*/
|
2016-04-05 19:17:50 +00:00
|
|
|
showSharedVideoMutedPopup (show) {
|
2016-04-19 18:07:04 +00:00
|
|
|
if(show)
|
2016-04-05 19:17:50 +00:00
|
|
|
this.showMicMutedPopup(false);
|
|
|
|
|
2017-02-15 08:54:57 +00:00
|
|
|
APP.UI.showCustomToolbarPopup('#sharedVideoMutedPopup', show, 5000);
|
2016-04-01 19:53:11 +00:00
|
|
|
}
|
2016-03-18 20:00:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Container for shared video iframe.
|
|
|
|
*/
|
|
|
|
class SharedVideoContainer extends LargeContainer {
|
|
|
|
|
|
|
|
constructor ({url, iframe, player}) {
|
|
|
|
super();
|
|
|
|
|
|
|
|
this.$iframe = $(iframe);
|
|
|
|
this.url = url;
|
|
|
|
this.player = player;
|
|
|
|
}
|
|
|
|
|
|
|
|
show () {
|
2016-03-24 19:49:26 +00:00
|
|
|
let self = this;
|
2016-03-18 20:00:55 +00:00
|
|
|
return new Promise(resolve => {
|
|
|
|
this.$iframe.fadeIn(300, () => {
|
2016-03-24 19:49:26 +00:00
|
|
|
self.bodyBackground = document.body.style.background;
|
|
|
|
document.body.style.background = 'black';
|
2016-03-18 20:00:55 +00:00
|
|
|
this.$iframe.css({opacity: 1});
|
2017-04-01 05:52:40 +00:00
|
|
|
APP.store.dispatch(dockToolbox(true));
|
2016-03-18 20:00:55 +00:00
|
|
|
resolve();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
hide () {
|
2016-03-24 19:49:26 +00:00
|
|
|
let self = this;
|
2017-04-01 05:52:40 +00:00
|
|
|
APP.store.dispatch(dockToolbox(false));
|
2016-03-18 20:00:55 +00:00
|
|
|
return new Promise(resolve => {
|
|
|
|
this.$iframe.fadeOut(300, () => {
|
2016-03-24 19:49:26 +00:00
|
|
|
document.body.style.background = self.bodyBackground;
|
2016-03-18 20:00:55 +00:00
|
|
|
this.$iframe.css({opacity: 0});
|
|
|
|
resolve();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
onHoverIn () {
|
2017-04-01 05:52:40 +00:00
|
|
|
APP.store.dispatch(showToolbox());
|
2016-03-18 20:00:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
2016-10-27 19:08:06 +00:00
|
|
|
this.bindHoverHandler();
|
2016-03-18 20:00:55 +00:00
|
|
|
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.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 () {
|
2016-03-24 01:43:29 +00:00
|
|
|
VideoLayout.handleVideoThumbClicked(this.url);
|
2016-03-18 20:00:55 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Removes RemoteVideo from the page.
|
|
|
|
*/
|
|
|
|
SharedVideoThumb.prototype.remove = function () {
|
2016-11-11 15:00:54 +00:00
|
|
|
logger.log("Remove shared video thumb", this.id);
|
2016-03-18 20:00:55 +00:00
|
|
|
|
2016-03-18 22:21:41 +00:00
|
|
|
// Make sure that the large video is updated if are removing its
|
|
|
|
// corresponding small video.
|
2016-03-23 22:45:27 +00:00
|
|
|
this.VideoLayout.updateAfterThumbRemoved(this.id);
|
2016-03-18 22:21:41 +00:00
|
|
|
|
2016-03-18 20:00:55 +00:00
|
|
|
// 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) {
|
2016-11-11 15:00:54 +00:00
|
|
|
logger.warn( "Unable to set displayName - " + this.videoSpanId +
|
2016-03-18 20:00:55 +00:00
|
|
|
" 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;
|
|
|
|
}
|
|
|
|
|
2016-03-18 22:04:13 +00:00
|
|
|
/**
|
|
|
|
* Ask user if he want to close shared video.
|
|
|
|
*/
|
2016-03-22 19:59:03 +00:00
|
|
|
function showStopVideoPropmpt() {
|
2016-03-18 22:04:13 +00:00
|
|
|
return new Promise(function (resolve, reject) {
|
2016-10-12 00:08:24 +00:00
|
|
|
let submitFunction = function(e,v) {
|
|
|
|
if (v) {
|
|
|
|
resolve();
|
|
|
|
} else {
|
|
|
|
reject();
|
2016-03-18 22:04:13 +00:00
|
|
|
}
|
2016-10-12 00:08:24 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
let closeFunction = function () {
|
|
|
|
dialog = null;
|
|
|
|
};
|
2016-03-18 22:04:13 +00:00
|
|
|
|
2016-10-12 00:08:24 +00:00
|
|
|
dialog = APP.UI.messageHandler.openTwoButtonDialog({
|
|
|
|
titleKey: "dialog.removeSharedVideoTitle",
|
|
|
|
msgKey: "dialog.removeSharedVideoMsg",
|
|
|
|
leftButtonKey: "dialog.Remove",
|
|
|
|
submitFunction,
|
|
|
|
closeFunction
|
|
|
|
});
|
2016-03-18 22:04:13 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2016-03-18 20:00:55 +00:00
|
|
|
/**
|
|
|
|
* 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 cancelButton = i18n.generateTranslationHTML("dialog.Cancel");
|
|
|
|
const shareButton = i18n.generateTranslationHTML("dialog.Share");
|
|
|
|
const backButton = i18n.generateTranslationHTML("dialog.Back");
|
|
|
|
const linkError
|
|
|
|
= i18n.generateTranslationHTML("dialog.shareVideoLinkError");
|
|
|
|
|
|
|
|
return new Promise(function (resolve, reject) {
|
2016-07-06 18:26:27 +00:00
|
|
|
dialog = APP.UI.messageHandler.openDialogWithStates({
|
2016-03-18 20:00:55 +00:00
|
|
|
state0: {
|
2016-10-21 17:11:22 +00:00
|
|
|
titleKey: "dialog.shareVideoTitle",
|
2016-03-18 20:00:55 +00:00
|
|
|
html: `
|
|
|
|
<input name="sharedVideoUrl" type="text"
|
2016-11-09 16:46:58 +00:00
|
|
|
class="input-control"
|
2016-03-18 20:00:55 +00:00
|
|
|
data-i18n="[placeholder]defaultLink"
|
|
|
|
autofocus>`,
|
|
|
|
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: {
|
2016-10-21 17:11:22 +00:00
|
|
|
titleKey: "dialog.shareVideoTitle",
|
2016-10-12 00:08:24 +00:00
|
|
|
html: linkError,
|
2016-03-18 20:00:55 +00:00
|
|
|
persistent: false,
|
|
|
|
buttons: [
|
|
|
|
{title: cancelButton, value: false},
|
|
|
|
{title: backButton, value: true}
|
|
|
|
],
|
|
|
|
focus: ':input:first',
|
|
|
|
defaultButton: 1,
|
2016-10-03 16:12:04 +00:00
|
|
|
submit: function (e, v) {
|
2016-03-18 20:00:55 +00:00
|
|
|
e.preventDefault();
|
|
|
|
if (v === 0) {
|
|
|
|
reject();
|
|
|
|
dialog.close();
|
|
|
|
} else {
|
|
|
|
dialog.goToState('state0');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2016-07-06 18:26:27 +00:00
|
|
|
}, {
|
|
|
|
close: function () {
|
|
|
|
dialog = null;
|
|
|
|
}
|
2016-10-21 17:11:22 +00:00
|
|
|
}, {
|
|
|
|
url: defaultSharedVideoLink
|
2016-03-18 20:00:55 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|