Leonard Kim abee3331aa ref(toolbar): remove remnant of custom tooltip display on demand
The feature was not ported to the new toolbar. Arguable these
can all be moved into notification but for now simply the
logic will be removed and worked on again as demand arised.
2018-04-11 10:35:01 +02:00

816 lines
26 KiB
JavaScript

/* global $, APP, YT, interfaceConfig, onPlayerReady, onPlayerStateChange,
onPlayerError */
const logger = require('jitsi-meet-logger').getLogger(__filename);
import UIUtil from '../util/UIUtil';
import UIEvents from '../../../service/UI/UIEvents';
import VideoLayout from '../videolayout/VideoLayout';
import LargeContainer from '../videolayout/LargeContainer';
import Filmstrip from '../videolayout/Filmstrip';
import {
createSharedVideoEvent as createEvent,
sendAnalytics
} from '../../../react/features/analytics';
import {
participantJoined,
participantLeft
} from '../../../react/features/base/participants';
import {
dockToolbox,
getToolboxHeight,
showToolbox
} from '../../../react/features/toolbox';
import SharedVideoThumb from './SharedVideoThumb';
export const SHARED_VIDEO_CONTAINER_TYPE = 'sharedvideo';
/**
* Example shared video link.
* @type {string}
*/
const defaultSharedVideoLink = 'https://www.youtube.com/watch?v=xNXN7CZk8X0';
const updateInterval = 5000; // milliseconds
/**
* The dialog for user input (video link).
* @type {null}
*/
let dialog = null;
/**
* Manager of shared video.
*/
export default class SharedVideoManager {
/**
*
*/
constructor(emitter) {
this.emitter = emitter;
this.isSharedVideoShown = false;
this.isPlayerAPILoaded = false;
this.mutedWithUserInteraction = false;
}
/**
* 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.
*
* @returns {boolean} indicating if the volume of the shared video is
* currently on.
*/
isSharedVideoVolumeOn() {
return this.player
&& this.player.getPlayerState() === YT.PlayerState.PLAYING
&& !this.player.isMuted()
&& this.player.getVolume() > 0;
}
/**
* Indicates if the local user is the owner of the shared video.
* @returns {*|boolean}
*/
isSharedVideoOwner() {
return this.from && APP.conference.isLocalId(this.from);
}
/**
* 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 (dialog) {
return;
}
if (!this.isSharedVideoShown) {
requestVideoLink().then(
url => {
this.emitter.emit(
UIEvents.UPDATE_SHARED_VIDEO, url, 'start');
sendAnalytics(createEvent('started'));
},
err => {
logger.log('SHARED VIDEO CANCELED', err);
sendAnalytics(createEvent('canceled'));
}
);
return;
}
if (APP.conference.isLocalId(this.from)) {
showStopVideoPropmpt().then(
() => {
// 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;
}
this.emitter.emit(
UIEvents.UPDATE_SHARED_VIDEO, this.url, 'stop');
sendAnalytics(createEvent('stopped'));
},
() => {}); // eslint-disable-line no-empty-function
} else {
APP.UI.messageHandler.showWarning({
descriptionKey: 'dialog.alreadySharedVideoMsg',
titleKey: 'dialog.alreadySharedVideoTitle'
});
sendAnalytics(createEvent('already.shared'));
}
}
/**
* Shows the player component and starts the process that will be sending
* updates, if we are the one shared the video.
*
* @param id the id of the sender of the command
* @param url the video url
* @param attributes
*/
onSharedVideoStart(id, url, attributes) {
if (this.isSharedVideoShown) {
return;
}
this.isSharedVideoShown = true;
// the video url
this.url = url;
// the owner of the video
this.from = id;
this.mutedWithUserInteraction = APP.conference.isLocalAudioMuted();
// listen for local audio mute events
this.localAudioMutedListener = this.onLocalAudioMuted.bind(this);
this.emitter.on(UIEvents.AUDIO_MUTED, this.localAudioMutedListener);
// This code loads the IFrame Player API code asynchronously.
const tag = document.createElement('script');
tag.src = 'https://www.youtube.com/iframe_api';
const firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
// 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
this.initialAttributes = attributes;
const self = this;
if (self.isPlayerAPILoaded) {
window.onYouTubeIframeAPIReady();
} else {
window.onYouTubeIframeAPIReady = function() {
self.isPlayerAPILoaded = true;
const showControls
= APP.conference.isLocalId(self.from) ? 1 : 0;
const p = new YT.Player('sharedVideoIFrame', {
height: '100%',
width: '100%',
videoId: self.url,
playerVars: {
'origin': location.origin,
'fs': '0',
'autoplay': 0,
'controls': showControls,
'rel': 0
},
events: {
'onReady': onPlayerReady,
'onStateChange': onPlayerStateChange,
'onError': onPlayerError
}
});
// add listener for volume changes
p.addEventListener(
'onVolumeChange', 'onVolumeChange');
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');
}
};
}
/**
* Indicates that a change in state has occurred for the shared video.
* @param event the event notifying us of the change
*/
window.onPlayerStateChange = function(event) {
// eslint-disable-next-line eqeqeq
if (event.data == YT.PlayerState.PLAYING) {
self.player = event.target;
if (self.initialAttributes) {
// If a network update has occurred already now is the
// time to process it.
self.processVideoUpdate(
self.player,
self.initialAttributes);
self.initialAttributes = null;
}
self.smartAudioMute();
// eslint-disable-next-line eqeqeq
} else if (event.data == YT.PlayerState.PAUSED) {
self.smartAudioUnmute();
sendAnalytics(createEvent('paused'));
}
// eslint-disable-next-line eqeqeq
self.fireSharedVideoEvent(event.data == YT.PlayerState.PAUSED);
};
/**
* Track player progress while paused.
* @param event
*/
window.onVideoProgress = function(event) {
const state = event.target.getPlayerState();
// eslint-disable-next-line eqeqeq
if (state == YT.PlayerState.PAUSED) {
self.fireSharedVideoEvent(true);
}
};
/**
* Gets notified for volume state changed.
* @param event
*/
window.onVolumeChange = function(event) {
self.fireSharedVideoEvent();
// let's check, if player is not muted lets mute locally
if (event.data.volume > 0 && !event.data.muted) {
self.smartAudioMute();
} else if (event.data.volume <= 0 || event.data.muted) {
self.smartAudioUnmute();
}
sendAnalytics(createEvent(
'volume.changed',
{
volume: event.data.volume,
muted: event.data.muted
}));
};
window.onPlayerReady = function(event) {
const player = event.target;
// do not relay on autoplay as it is not sending all of the events
// in onPlayerStateChange
player.playVideo();
const thumb = new SharedVideoThumb(
self.url, SHARED_VIDEO_CONTAINER_TYPE, VideoLayout);
thumb.setDisplayName('YouTube');
VideoLayout.addRemoteVideoContainer(self.url, thumb);
VideoLayout.resizeThumbnails(true);
const iframe = player.getIframe();
// eslint-disable-next-line no-use-before-define
self.sharedVideo = new SharedVideoContainer(
{ url,
iframe,
player });
// prevents pausing participants not sharing the video
// to pause the video
if (!APP.conference.isLocalId(self.from)) {
$('#sharedVideo').css('pointer-events', 'none');
}
VideoLayout.addLargeVideoContainer(
SHARED_VIDEO_CONTAINER_TYPE, self.sharedVideo);
APP.store.dispatch(participantJoined({
id: self.url,
isBot: true,
name: 'YouTube'
}));
VideoLayout.handleVideoThumbClicked(self.url);
// If we are sending the command and we are starting the player
// we need to continuously send the player current time position
if (APP.conference.isLocalId(self.from)) {
self.intervalId = setInterval(
self.fireSharedVideoEvent.bind(self),
updateInterval);
}
};
window.onPlayerError = function(event) {
logger.error('Error in the player:', event.data);
// store the error player, so we can remove it
self.errorInPlayer = event.target;
};
}
/**
* 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
*/
processVideoUpdate(player, attributes) {
if (!attributes) {
return;
}
// eslint-disable-next-line eqeqeq
if (attributes.state == 'playing') {
const isPlayerPaused
= this.player.getPlayerState() === YT.PlayerState.PAUSED;
// If our player is currently paused force the seek.
this.processTime(player, attributes, isPlayerPaused);
// Process mute.
const isAttrMuted = attributes.muted === 'true';
if (player.isMuted() !== isAttrMuted) {
this.smartPlayerMute(isAttrMuted, true);
}
// Process volume
if (!isAttrMuted
&& attributes.volume !== undefined
// eslint-disable-next-line eqeqeq
&& player.getVolume() != attributes.volume) {
player.setVolume(attributes.volume);
logger.info(`Player change of volume:${attributes.volume}`);
}
if (isPlayerPaused) {
player.playVideo();
}
// eslint-disable-next-line eqeqeq
} else if (attributes.state == 'pause') {
// if its not paused, pause it
player.pauseVideo();
this.processTime(player, attributes, true);
}
}
/**
* 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
* @param forceSeek whether seek should be forced
*/
processTime(player, attributes, forceSeek) {
if (forceSeek) {
logger.info('Player seekTo:', attributes.time);
player.seekTo(attributes.time);
return;
}
// check received time and current time
const currentPosition = player.getCurrentTime();
const 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) {
logger.info('Player seekTo:', attributes.time,
' current time is:', currentPosition, ' diff:', diff);
player.seekTo(attributes.time);
}
}
/**
* Checks current state of the player and fire an event with the values.
*/
fireSharedVideoEvent(sendPauseEvent) {
// ignore update checks if we are not the owner of the video
// 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) {
return;
}
const state = this.player.getPlayerState();
// if its paused and haven't been pause - send paused
if (state === YT.PlayerState.PAUSED && sendPauseEvent) {
this.emitter.emit(UIEvents.UPDATE_SHARED_VIDEO,
this.url, 'pause', this.player.getCurrentTime());
} else if (state === YT.PlayerState.PLAYING) {
// if its playing and it was paused - send update with time
// if its playing and was playing just send update with time
this.emitter.emit(UIEvents.UPDATE_SHARED_VIDEO,
this.url, 'playing',
this.player.getCurrentTime(),
this.player.isMuted(),
this.player.getVolume());
}
}
/**
* Updates video, if it's not playing and needs starting or if it's playing
* and needs to be paused.
* @param id the id of the sender of the command
* @param url the video url
* @param attributes
*/
onSharedVideoUpdate(id, url, attributes) {
// if we are sending the event ignore
if (APP.conference.isLocalId(this.from)) {
return;
}
if (!this.isSharedVideoShown) {
this.onSharedVideoStart(id, url, attributes);
return;
}
// eslint-disable-next-line no-negated-condition
if (!this.player) {
this.initialAttributes = attributes;
} else {
this.processVideoUpdate(this.player, attributes);
}
}
/**
* Stop shared video if it is currently showed. If the user started the
* shared video is the one in the id (called when user
* left and we want to remove video if the user sharing it left).
* @param id the id of the sender of the command
*/
onSharedVideoStop(id, attributes) {
if (!this.isSharedVideoShown) {
return;
}
if (this.from !== id) {
return;
}
if (!this.player) {
// if there is no error in the player till now,
// store the initial attributes
if (!this.errorInPlayer) {
this.initialAttributes = attributes;
return;
}
}
this.emitter.removeListener(UIEvents.AUDIO_MUTED,
this.localAudioMutedListener);
this.localAudioMutedListener = null;
VideoLayout.removeParticipantContainer(this.url);
VideoLayout.showLargeVideoContainer(SHARED_VIDEO_CONTAINER_TYPE, false)
.then(() => {
VideoLayout.removeLargeVideoContainer(
SHARED_VIDEO_CONTAINER_TYPE);
if (this.player) {
this.player.destroy();
this.player = null;
} else if (this.errorInPlayer) {
// if there is an error in player, remove that instance
this.errorInPlayer.destroy();
this.errorInPlayer = null;
}
this.smartAudioUnmute();
// revert to original behavior (prevents pausing
// for participants not sharing the video to pause it)
$('#sharedVideo').css('pointer-events', 'auto');
this.emitter.emit(
UIEvents.UPDATE_SHARED_VIDEO, null, 'removed');
});
APP.store.dispatch(participantLeft(this.url));
this.url = null;
this.isSharedVideoShown = false;
this.initialAttributes = null;
}
/**
* Receives events for local audio mute/unmute by local user.
* @param muted boolena whether it is muted or not.
* @param {boolean} indicates if this mute was a result of user interaction,
* i.e. pressing the mute button or it was programatically triggerred
*/
onLocalAudioMuted(muted, userInteraction) {
if (!this.player) {
return;
}
if (muted) {
this.mutedWithUserInteraction = userInteraction;
} else if (this.player.getPlayerState() !== YT.PlayerState.PAUSED) {
this.smartPlayerMute(true, false);
// Check if we need to update other participants
this.fireSharedVideoEvent();
}
}
/**
* Mutes / unmutes the player.
* @param mute true to mute the shared video, false - otherwise.
* @param {boolean} Indicates if this mute is a consequence of a network
* video update or is called locally.
*/
smartPlayerMute(mute, isVideoUpdate) {
if (!this.player.isMuted() && mute) {
this.player.mute();
if (isVideoUpdate) {
this.smartAudioUnmute();
}
} else if (this.player.isMuted() && !mute) {
this.player.unMute();
if (isVideoUpdate) {
this.smartAudioMute();
}
}
}
/**
* 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.
*/
smartAudioUnmute() {
if (APP.conference.isLocalAudioMuted()
&& !this.mutedWithUserInteraction
&& !this.isSharedVideoVolumeOn()) {
sendAnalytics(createEvent('audio.unmuted'));
logger.log('Shared video: audio unmuted');
this.emitter.emit(UIEvents.AUDIO_MUTED, false, false);
}
}
/**
* Smart mike mute. If the mike isn't currently muted and the shared video
* volume is on we mute the mike.
*/
smartAudioMute() {
if (!APP.conference.isLocalAudioMuted()
&& this.isSharedVideoVolumeOn()) {
sendAnalytics(createEvent('audio.muted'));
logger.log('Shared video: audio muted');
this.emitter.emit(UIEvents.AUDIO_MUTED, true, false);
}
}
}
/**
* Container for shared video iframe.
*/
class SharedVideoContainer extends LargeContainer {
/**
*
*/
constructor({ url, iframe, player }) {
super();
this.$iframe = $(iframe);
this.url = url;
this.player = player;
}
/**
*
*/
show() {
const self = this;
return new Promise(resolve => {
this.$iframe.fadeIn(300, () => {
self.bodyBackground = document.body.style.background;
document.body.style.background = 'black';
this.$iframe.css({ opacity: 1 });
APP.store.dispatch(dockToolbox(true));
resolve();
});
});
}
/**
*
*/
hide() {
const self = this;
APP.store.dispatch(dockToolbox(false));
return new Promise(resolve => {
this.$iframe.fadeOut(300, () => {
document.body.style.background = self.bodyBackground;
this.$iframe.css({ opacity: 0 });
resolve();
});
});
}
/**
*
*/
onHoverIn() {
APP.store.dispatch(showToolbox());
}
/**
*
*/
get id() {
return this.url;
}
/**
*
*/
resize(containerWidth, containerHeight) {
let height, width;
if (interfaceConfig.VERTICAL_FILMSTRIP) {
height = containerHeight - getToolboxHeight();
width = containerWidth - Filmstrip.getFilmstripWidth();
} else {
height = containerHeight - Filmstrip.getFilmstripHeight();
width = containerWidth;
}
this.$iframe.width(width).height(height);
}
/**
* @return {boolean} do not switch on dominant speaker event if on stage.
*/
stayOnStage() {
return false;
}
}
/**
* Checks if given string is youtube url.
* @param {string} url string to check.
* @returns {boolean}
*/
function getYoutubeLink(url) {
const p = /^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/;// eslint-disable-line max-len
return url.match(p) ? RegExp.$1 : false;
}
/**
* Ask user if he want to close shared video.
*/
function showStopVideoPropmpt() {
return new Promise((resolve, reject) => {
const submitFunction = function(e, v) {
if (v) {
resolve();
} else {
reject();
}
};
const closeFunction = function() {
dialog = null;
};
dialog = APP.UI.messageHandler.openTwoButtonDialog({
titleKey: 'dialog.removeSharedVideoTitle',
msgKey: 'dialog.removeSharedVideoMsg',
leftButtonKey: 'dialog.Remove',
submitFunction,
closeFunction
});
});
}
/**
* Ask user for shared video url to share with others.
* Dialog validates client input to allow only youtube urls.
*/
function requestVideoLink() {
const 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((resolve, reject) => {
dialog = APP.UI.messageHandler.openDialogWithStates({
state0: {
titleKey: 'dialog.shareVideoTitle',
html: `
<input name='sharedVideoUrl' type='text'
class='input-control'
data-i18n='[placeholder]defaultLink'
autofocus>`,
persistent: false,
buttons: [
{ title: cancelButton,
value: false },
{ title: shareButton,
value: true }
],
focus: ':input:first',
defaultButton: 1,
submit(e, v, m, f) { // eslint-disable-line max-params
e.preventDefault();
if (!v) {
reject('cancelled');
dialog.close();
return;
}
const sharedVideoUrl = f.sharedVideoUrl;
if (!sharedVideoUrl) {
return;
}
const urlValue
= encodeURI(UIUtil.escapeHtml(sharedVideoUrl));
const yVideoId = getYoutubeLink(urlValue);
if (!yVideoId) {
dialog.goToState('state1');
return false;
}
resolve(yVideoId);
dialog.close();
}
},
state1: {
titleKey: 'dialog.shareVideoTitle',
html: linkError,
persistent: false,
buttons: [
{ title: cancelButton,
value: false },
{ title: backButton,
value: true }
],
focus: ':input:first',
defaultButton: 1,
submit(e, v) {
e.preventDefault();
if (v === 0) {
reject();
dialog.close();
} else {
dialog.goToState('state0');
}
}
}
}, {
close() {
dialog = null;
}
}, {
url: defaultSharedVideoLink
});
});
}