mirror of https://github.com/jitsi/jitsi-meet
parent
9657bd9b6d
commit
4e4ff0f60f
@ -1,655 +0,0 @@ |
||||
/* global $, APP, YT, interfaceConfig, onPlayerReady, onPlayerStateChange, |
||||
onPlayerError */ |
||||
|
||||
import Logger from 'jitsi-meet-logger'; |
||||
|
||||
import { |
||||
createSharedVideoEvent as createEvent, |
||||
sendAnalytics |
||||
} from '../../../react/features/analytics'; |
||||
import { |
||||
participantJoined, |
||||
participantLeft, |
||||
pinParticipant |
||||
} from '../../../react/features/base/participants'; |
||||
import { VIDEO_PLAYER_PARTICIPANT_NAME } from '../../../react/features/shared-video/constants'; |
||||
import { dockToolbox, showToolbox } from '../../../react/features/toolbox/actions.web'; |
||||
import { getToolboxHeight } from '../../../react/features/toolbox/functions.web'; |
||||
import UIEvents from '../../../service/UI/UIEvents'; |
||||
import Filmstrip from '../videolayout/Filmstrip'; |
||||
import LargeContainer from '../videolayout/LargeContainer'; |
||||
import VideoLayout from '../videolayout/VideoLayout'; |
||||
|
||||
const logger = Logger.getLogger(__filename); |
||||
|
||||
export const SHARED_VIDEO_CONTAINER_TYPE = 'sharedvideo'; |
||||
|
||||
/** |
||||
* Example shared video link. |
||||
* @type {string} |
||||
*/ |
||||
const updateInterval = 5000; // milliseconds
|
||||
|
||||
|
||||
/** |
||||
* 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); |
||||
} |
||||
|
||||
/** |
||||
* Start shared video event emitter if a video is not shown. |
||||
* |
||||
* @param url of the video |
||||
*/ |
||||
startSharedVideoEmitter(url) { |
||||
|
||||
if (!this.isSharedVideoShown) { |
||||
if (url) { |
||||
this.emitter.emit( |
||||
UIEvents.UPDATE_SHARED_VIDEO, url, 'start'); |
||||
sendAnalytics(createEvent('started')); |
||||
} |
||||
|
||||
logger.log('SHARED VIDEO CANCELED'); |
||||
sendAnalytics(createEvent('canceled')); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Stop shared video event emitter done by the one who shared the video. |
||||
*/ |
||||
stopSharedVideoEmitter() { |
||||
|
||||
if (APP.conference.isLocalId(this.from)) { |
||||
if (this.intervalId) { |
||||
clearInterval(this.intervalId); |
||||
this.intervalId = null; |
||||
} |
||||
this.emitter.emit( |
||||
UIEvents.UPDATE_SHARED_VIDEO, this.url, 'stop'); |
||||
sendAnalytics(createEvent('stopped')); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 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 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({ |
||||
|
||||
// FIXME The cat is out of the bag already or rather _room is
|
||||
// not private because it is used in multiple other places
|
||||
// already such as AbstractPageReloadOverlay.
|
||||
conference: APP.conference._room, |
||||
id: self.url, |
||||
isFakeParticipant: true, |
||||
name: VIDEO_PLAYER_PARTICIPANT_NAME |
||||
})); |
||||
|
||||
APP.store.dispatch(pinParticipant(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; |
||||
|
||||
APP.store.dispatch(participantLeft(this.url, APP.conference._room)); |
||||
|
||||
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'); |
||||
}); |
||||
|
||||
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 programmatically triggered |
||||
*/ |
||||
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.getVerticalFilmstripWidth(); |
||||
} 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; |
||||
} |
||||
} |
@ -0,0 +1,122 @@ |
||||
import { getCurrentConference } from '../base/conference'; |
||||
import { openDialog } from '../base/dialog/actions'; |
||||
import { getLocalParticipant } from '../base/participants'; |
||||
import { SharedVideoDialog } from '../shared-video/components'; |
||||
|
||||
import { RESET_SHARED_VIDEO_STATUS, SET_SHARED_VIDEO_STATUS } from './actionTypes'; |
||||
|
||||
/** |
||||
* Resets the status of the shared video. |
||||
* |
||||
* @returns {{ |
||||
* type: SET_SHARED_VIDEO_STATUS, |
||||
* }} |
||||
*/ |
||||
export function resetSharedVideoStatus() { |
||||
return { |
||||
type: RESET_SHARED_VIDEO_STATUS |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* Updates the current known status of the shared video. |
||||
* |
||||
* @param {{ |
||||
* muted: boolean, |
||||
* ownerId: string, |
||||
* status: string, |
||||
* time: number, |
||||
* videoUrl: string |
||||
* }} options - The options. |
||||
* |
||||
* @returns {{ |
||||
* type: SET_SHARED_VIDEO_STATUS, |
||||
* muted: boolean, |
||||
* ownerId: string, |
||||
* status: string, |
||||
* time: number, |
||||
* videoUrl: string, |
||||
* }} |
||||
*/ |
||||
export function setSharedVideoStatus({ videoUrl, status, time, ownerId, muted }) { |
||||
return { |
||||
type: SET_SHARED_VIDEO_STATUS, |
||||
ownerId, |
||||
status, |
||||
time, |
||||
videoUrl, |
||||
muted |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* Displays the dialog for entering the video link. |
||||
* |
||||
* @param {Function} onPostSubmit - The function to be invoked when a valid link is entered. |
||||
* @returns {Function} |
||||
*/ |
||||
export function showSharedVideoDialog(onPostSubmit) { |
||||
return openDialog(SharedVideoDialog, { onPostSubmit }); |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* Stops playing a shared video. |
||||
* |
||||
* @returns {Function} |
||||
*/ |
||||
export function stopSharedVideo() { |
||||
return (dispatch, getState) => { |
||||
const state = getState(); |
||||
const { ownerId } = state['features/shared-video']; |
||||
const localParticipant = getLocalParticipant(state); |
||||
|
||||
if (ownerId === localParticipant.id) { |
||||
dispatch(resetSharedVideoStatus()); |
||||
} |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* Plays a shared video. |
||||
* |
||||
* @param {string} videoUrl - The video url to be played. |
||||
* |
||||
* @returns {Function} |
||||
*/ |
||||
export function playSharedVideo(videoUrl) { |
||||
return (dispatch, getState) => { |
||||
const conference = getCurrentConference(getState()); |
||||
|
||||
if (conference) { |
||||
const localParticipant = getLocalParticipant(getState()); |
||||
|
||||
dispatch(setSharedVideoStatus({ |
||||
videoUrl, |
||||
status: 'start', |
||||
time: 0, |
||||
ownerId: localParticipant.id |
||||
})); |
||||
} |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* Stops playing a shared video. |
||||
* |
||||
* @returns {Function} |
||||
*/ |
||||
export function toggleSharedVideo() { |
||||
return (dispatch, getState) => { |
||||
const state = getState(); |
||||
const { status } = state['features/shared-video']; |
||||
|
||||
if ([ 'playing', 'start', 'pause' ].includes(status)) { |
||||
dispatch(stopSharedVideo()); |
||||
} else { |
||||
dispatch(showSharedVideoDialog(id => dispatch(playSharedVideo(id)))); |
||||
} |
||||
}; |
||||
} |
@ -1,54 +1 @@ |
||||
// @flow
|
||||
|
||||
import { openDialog } from '../base/dialog'; |
||||
|
||||
import { SET_SHARED_VIDEO_STATUS, TOGGLE_SHARED_VIDEO } from './actionTypes'; |
||||
import { SharedVideoDialog } from './components/native'; |
||||
|
||||
/** |
||||
* Updates the current known status of the shared video. |
||||
* |
||||
* @param {string} videoId - The id of the video to be shared. |
||||
* @param {string} status - The current status of the video being shared. |
||||
* @param {number} time - The current position of the video being shared. |
||||
* @param {string} ownerId - The participantId of the user sharing the video. |
||||
* @returns {{ |
||||
* type: SET_SHARED_VIDEO_STATUS, |
||||
* ownerId: string, |
||||
* status: string, |
||||
* time: number, |
||||
* videoId: string |
||||
* }} |
||||
*/ |
||||
export function setSharedVideoStatus(videoId: string, status: string, time: number, ownerId: string) { |
||||
return { |
||||
type: SET_SHARED_VIDEO_STATUS, |
||||
ownerId, |
||||
status, |
||||
time, |
||||
videoId |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* Starts the flow for starting or stopping a shared video. |
||||
* |
||||
* @returns {{ |
||||
* type: TOGGLE_SHARED_VIDEO |
||||
* }} |
||||
*/ |
||||
export function toggleSharedVideo() { |
||||
return { |
||||
type: TOGGLE_SHARED_VIDEO |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* Displays the prompt for entering the video link. |
||||
* |
||||
* @param {Function} onPostSubmit - The function to be invoked when a valid link is entered. |
||||
* @returns {Function} |
||||
*/ |
||||
export function showSharedVideoDialog(onPostSubmit: ?Function) { |
||||
return openDialog(SharedVideoDialog, { onPostSubmit }); |
||||
} |
||||
export * from './actions.any'; |
||||
|
@ -0,0 +1,425 @@ |
||||
/* @flow */ |
||||
/* eslint-disable no-invalid-this */ |
||||
|
||||
import throttle from 'lodash/throttle'; |
||||
import { Component } from 'react'; |
||||
|
||||
import { sendAnalytics, createSharedVideoEvent as createEvent } from '../../../analytics'; |
||||
import { getCurrentConference } from '../../../base/conference'; |
||||
import { MEDIA_TYPE } from '../../../base/media'; |
||||
import { getLocalParticipant } from '../../../base/participants'; |
||||
import { isLocalTrackMuted } from '../../../base/tracks'; |
||||
import { dockToolbox } from '../../../toolbox/actions.web'; |
||||
import { muteLocal } from '../../../video-menu/actions.any'; |
||||
import { setSharedVideoStatus } from '../../actions.any'; |
||||
|
||||
export const PLAYBACK_STATES = { |
||||
PLAYING: 'playing', |
||||
PAUSED: 'pause', |
||||
STOPPED: 'stop' |
||||
}; |
||||
|
||||
/** |
||||
* Return true if the diffenrece between the two timees is larger than 5. |
||||
* |
||||
* @param {number} newTime - The current time. |
||||
* @param {number} previousTime - The previous time. |
||||
* @private |
||||
* @returns {boolean} |
||||
*/ |
||||
function shouldSeekToPosition(newTime, previousTime) { |
||||
return Math.abs(newTime - previousTime) > 5; |
||||
} |
||||
|
||||
/** |
||||
* The type of the React {@link Component} props of {@link YoutubeLargeVideo}. |
||||
*/ |
||||
export type Props = { |
||||
|
||||
/** |
||||
* The current coference |
||||
*/ |
||||
_conference: Object, |
||||
|
||||
/** |
||||
* Docks the toolbox |
||||
*/ |
||||
_dockToolbox: Function, |
||||
|
||||
/** |
||||
* Indicates whether the local audio is muted |
||||
*/ |
||||
_isLocalAudioMuted: boolean, |
||||
|
||||
/** |
||||
* Is the video shared by the local user. |
||||
* |
||||
* @private |
||||
*/ |
||||
_isOwner: boolean, |
||||
|
||||
/** |
||||
* Store flag for muted state |
||||
*/ |
||||
_muted: boolean, |
||||
|
||||
/** |
||||
* Mutes local audio track |
||||
*/ |
||||
_muteLocal: Function, |
||||
|
||||
/** |
||||
* The shared video owner id |
||||
*/ |
||||
_ownerId: string, |
||||
|
||||
/** |
||||
* Updates the shared video status |
||||
*/ |
||||
_setSharedVideoStatus: Function, |
||||
|
||||
/** |
||||
* The shared video status |
||||
*/ |
||||
_status: string, |
||||
|
||||
/** |
||||
* Seek time in seconds. |
||||
* |
||||
*/ |
||||
_time: number, |
||||
|
||||
/** |
||||
* The video url |
||||
*/ |
||||
_videoUrl: string, |
||||
|
||||
/** |
||||
* The video id |
||||
*/ |
||||
videoId: string |
||||
} |
||||
|
||||
/** |
||||
* Manager of shared video. |
||||
*/ |
||||
class AbstractVideoManager extends Component<Props> { |
||||
throttledFireUpdateSharedVideoEvent: Function; |
||||
|
||||
/** |
||||
* Initializes a new instance of AbstractVideoManager. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
constructor() { |
||||
super(); |
||||
|
||||
this.throttledFireUpdateSharedVideoEvent = throttle(this.fireUpdateSharedVideoEvent.bind(this), 5000); |
||||
|
||||
// selenium tests handler
|
||||
window._sharedVideoPlayer = this; |
||||
} |
||||
|
||||
/** |
||||
* Implements React Component's componentDidMount. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
componentDidMount() { |
||||
this.props._dockToolbox(true); |
||||
this.processUpdatedProps(); |
||||
} |
||||
|
||||
/** |
||||
* Implements React Component's componentDidUpdate. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
componentDidUpdate(prevProps: Props) { |
||||
const { _videoUrl } = this.props; |
||||
|
||||
if (prevProps._videoUrl !== _videoUrl) { |
||||
sendAnalytics(createEvent('started')); |
||||
} |
||||
|
||||
this.processUpdatedProps(); |
||||
} |
||||
|
||||
/** |
||||
* Implements React Component's componentWillUnmount. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
componentWillUnmount() { |
||||
sendAnalytics(createEvent('stopped')); |
||||
|
||||
if (this.dispose) { |
||||
this.dispose(); |
||||
} |
||||
|
||||
this.props._dockToolbox(false); |
||||
} |
||||
|
||||
/** |
||||
* Processes new properties. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
processUpdatedProps() { |
||||
const { _status, _time, _isOwner, _muted } = this.props; |
||||
|
||||
if (_isOwner) { |
||||
return; |
||||
} |
||||
|
||||
const playerTime = this.getTime(); |
||||
|
||||
if (shouldSeekToPosition(_time, playerTime)) { |
||||
this.seek(_time); |
||||
} |
||||
|
||||
if (this.getPlaybackState() !== _status) { |
||||
if (_status === PLAYBACK_STATES.PLAYING) { |
||||
this.play(); |
||||
} |
||||
|
||||
if (_status === PLAYBACK_STATES.PAUSED) { |
||||
this.pause(); |
||||
} |
||||
} |
||||
|
||||
if (this.isMuted() !== _muted) { |
||||
if (_muted) { |
||||
this.mute(); |
||||
} else { |
||||
this.unMute(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Handle video playing. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
onPlay() { |
||||
this.smartAudioMute(); |
||||
sendAnalytics(createEvent('play')); |
||||
this.fireUpdateSharedVideoEvent(); |
||||
} |
||||
|
||||
/** |
||||
* Handle video paused. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
onPause() { |
||||
sendAnalytics(createEvent('paused')); |
||||
this.fireUpdateSharedVideoEvent(); |
||||
} |
||||
|
||||
/** |
||||
* Handle volume changed. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
onVolumeChange() { |
||||
const volume = this.getVolume(); |
||||
const muted = this.isMuted(); |
||||
|
||||
if (volume > 0 && !muted) { |
||||
this.smartAudioMute(); |
||||
} |
||||
|
||||
sendAnalytics(createEvent( |
||||
'volume.changed', |
||||
{ |
||||
volume, |
||||
muted |
||||
})); |
||||
|
||||
this.fireUpdatePlayingVideoEvent(); |
||||
} |
||||
|
||||
/** |
||||
* Handle changes to the shared playing video. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
fireUpdatePlayingVideoEvent() { |
||||
if (this.getPlaybackState() === PLAYBACK_STATES.PLAYING) { |
||||
this.fireUpdateSharedVideoEvent(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Dispatches an update action for the shared video. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
fireUpdateSharedVideoEvent() { |
||||
const { _isOwner } = this.props; |
||||
|
||||
if (!_isOwner) { |
||||
return; |
||||
} |
||||
|
||||
const status = this.getPlaybackState(); |
||||
|
||||
if (!Object.values(PLAYBACK_STATES).includes(status)) { |
||||
return; |
||||
} |
||||
|
||||
const { |
||||
_ownerId, |
||||
_setSharedVideoStatus, |
||||
_videoUrl |
||||
} = this.props; |
||||
|
||||
_setSharedVideoStatus({ |
||||
videoUrl: _videoUrl, |
||||
status, |
||||
time: this.getTime(), |
||||
ownerId: _ownerId, |
||||
muted: this.isMuted() |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* 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.getPlaybackState() === PLAYBACK_STATES.PLAYING |
||||
&& !this.isMuted() |
||||
&& this.getVolume() > 0; |
||||
} |
||||
|
||||
/** |
||||
* Smart mike mute. If the mike isn't currently muted and the shared video |
||||
* volume is on we mute the mike. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
smartAudioMute() { |
||||
const { _isLocalAudioMuted, _muteLocal } = this.props; |
||||
|
||||
if (!_isLocalAudioMuted |
||||
&& this.isSharedVideoVolumeOn()) { |
||||
sendAnalytics(createEvent('audio.muted')); |
||||
_muteLocal(true); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Seeks video to provided time |
||||
* @param {number} time |
||||
*/ |
||||
seek: (time: number) => void; |
||||
|
||||
/** |
||||
* Indicates the playback state of the video |
||||
*/ |
||||
getPlaybackState: () => boolean; |
||||
|
||||
/** |
||||
* Indicates whether the video is muted |
||||
*/ |
||||
isMuted: () => boolean; |
||||
|
||||
/** |
||||
* Retrieves current volume |
||||
*/ |
||||
getVolume: () => number; |
||||
|
||||
/** |
||||
* Sets current volume |
||||
*/ |
||||
setVolume: (value: number) => void; |
||||
|
||||
/** |
||||
* Plays video |
||||
*/ |
||||
play: () => void; |
||||
|
||||
/** |
||||
* Pauses video |
||||
*/ |
||||
pause: () => void; |
||||
|
||||
/** |
||||
* Mutes video |
||||
*/ |
||||
mute: () => void; |
||||
|
||||
/** |
||||
* Unmutes video |
||||
*/ |
||||
unMute: () => void; |
||||
|
||||
/** |
||||
* Retrieves current time |
||||
*/ |
||||
getTime: () => number; |
||||
|
||||
/** |
||||
* Disposes current video player |
||||
*/ |
||||
dispose: () => void; |
||||
} |
||||
|
||||
|
||||
export default AbstractVideoManager; |
||||
|
||||
/** |
||||
* Maps part of the Redux store to the props of this component. |
||||
* |
||||
* @param {Object} state - The Redux state. |
||||
* @returns {Props} |
||||
*/ |
||||
export function _mapStateToProps(state: Object): $Shape<Props> { |
||||
const { ownerId, status, time, videoUrl, muted } = state['features/shared-video']; |
||||
const localParticipant = getLocalParticipant(state); |
||||
const _isLocalAudioMuted = isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.AUDIO); |
||||
|
||||
return { |
||||
_conference: getCurrentConference(state), |
||||
_isLocalAudioMuted, |
||||
_isOwner: ownerId === localParticipant.id, |
||||
_muted: muted, |
||||
_ownerId: ownerId, |
||||
_status: status, |
||||
_time: time, |
||||
_videoUrl: videoUrl |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* Maps part of the props of this component to Redux actions. |
||||
* |
||||
* @param {Function} dispatch - The Redux dispatch function. |
||||
* @returns {Props} |
||||
*/ |
||||
export function _mapDispatchToProps(dispatch: Function): $Shape<Props> { |
||||
return { |
||||
_dockToolbox: value => { |
||||
dispatch(dockToolbox(value)); |
||||
}, |
||||
_muteLocal: value => { |
||||
dispatch(muteLocal(value, MEDIA_TYPE.AUDIO)); |
||||
}, |
||||
_setSharedVideoStatus: ({ videoUrl, status, time, ownerId, muted }) => { |
||||
dispatch(setSharedVideoStatus({ |
||||
videoUrl, |
||||
status, |
||||
time, |
||||
ownerId, |
||||
muted |
||||
})); |
||||
} |
||||
}; |
||||
} |
@ -0,0 +1,147 @@ |
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react'; |
||||
|
||||
import Filmstrip from '../../../../../modules/UI/videolayout/Filmstrip'; |
||||
import { getLocalParticipant } from '../../../base/participants'; |
||||
import { connect } from '../../../base/redux'; |
||||
import { getToolboxHeight } from '../../../toolbox/functions.web'; |
||||
import { getYoutubeId } from '../../functions'; |
||||
|
||||
import VideoManager from './VideoManager'; |
||||
import YoutubeVideoManager from './YoutubeVideoManager'; |
||||
|
||||
declare var interfaceConfig: Object; |
||||
|
||||
type Props = { |
||||
|
||||
/** |
||||
* The available client width |
||||
*/ |
||||
clientHeight: number, |
||||
|
||||
/** |
||||
* The available client width |
||||
*/ |
||||
clientWidth: number, |
||||
|
||||
/** |
||||
* Is the video shared by the local user. |
||||
* |
||||
* @private |
||||
*/ |
||||
isOwner: boolean, |
||||
|
||||
/** |
||||
* The shared video id |
||||
*/ |
||||
sharedVideoId: string, |
||||
|
||||
/** |
||||
* The shared youtube video id |
||||
*/ |
||||
sharedYoutubeVideoId: string, |
||||
} |
||||
|
||||
/** |
||||
* Implements a React {@link Component} which represents the large video (a.k.a. |
||||
* the conference participant who is on the local stage) on Web/React. |
||||
* |
||||
* @extends Component |
||||
*/ |
||||
class SharedVideo extends Component<Props> { |
||||
/** |
||||
* Computes the width and the height of the component. |
||||
* |
||||
* @returns {{ |
||||
* height: number, |
||||
* width: number |
||||
* }} |
||||
*/ |
||||
getDimmensions() { |
||||
const { clientHeight, clientWidth } = this.props; |
||||
|
||||
let width; |
||||
let height; |
||||
|
||||
if (interfaceConfig.VERTICAL_FILMSTRIP) { |
||||
height = `${clientHeight - getToolboxHeight()}px`; |
||||
width = `${clientWidth - Filmstrip.getVerticalFilmstripWidth()}px`; |
||||
} else { |
||||
height = `${clientHeight - Filmstrip.getFilmstripHeight()}px`; |
||||
width = `${clientWidth}px`; |
||||
} |
||||
|
||||
return { |
||||
width, |
||||
height |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* Retrieves the manager to be used for playing the shared video. |
||||
* |
||||
* @returns {Component} |
||||
*/ |
||||
getManager() { |
||||
const { |
||||
sharedVideoId, |
||||
sharedYoutubeVideoId |
||||
} = this.props; |
||||
|
||||
if (!sharedVideoId) { |
||||
return null; |
||||
} |
||||
|
||||
if (sharedYoutubeVideoId) { |
||||
return <YoutubeVideoManager videoId = { sharedYoutubeVideoId } />; |
||||
} |
||||
|
||||
return <VideoManager videoId = { sharedVideoId } />; |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {React$Element} |
||||
*/ |
||||
render() { |
||||
const { isOwner } = this.props; |
||||
const className = isOwner ? '' : 'disable-pointer'; |
||||
|
||||
return ( |
||||
<div |
||||
className = { className } |
||||
id = 'sharedVideo' |
||||
style = { this.getDimmensions() }> |
||||
{this.getManager()} |
||||
</div> |
||||
); |
||||
} |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Maps (parts of) the Redux state to the associated LargeVideo props. |
||||
* |
||||
* @param {Object} state - The Redux state. |
||||
* @private |
||||
* @returns {Props} |
||||
*/ |
||||
function _mapStateToProps(state) { |
||||
const { ownerId, videoUrl } = state['features/shared-video']; |
||||
const { clientHeight, clientWidth } = state['features/base/responsive-ui']; |
||||
|
||||
const localParticipant = getLocalParticipant(state); |
||||
|
||||
return { |
||||
clientHeight, |
||||
clientWidth, |
||||
isOwner: ownerId === localParticipant.id, |
||||
sharedVideoId: videoUrl, |
||||
sharedYoutubeVideoId: getYoutubeId(videoUrl) |
||||
}; |
||||
} |
||||
|
||||
export default connect(_mapStateToProps)(SharedVideo); |
@ -0,0 +1,197 @@ |
||||
import Logger from 'jitsi-meet-logger'; |
||||
import React from 'react'; |
||||
|
||||
import { connect } from '../../../base/redux'; |
||||
|
||||
import AbstractVideoManager, { |
||||
_mapDispatchToProps, |
||||
_mapStateToProps, |
||||
PLAYBACK_STATES, |
||||
Props |
||||
} from './AbstractVideoManager'; |
||||
|
||||
const logger = Logger.getLogger(__filename); |
||||
|
||||
/** |
||||
* Manager of shared video. |
||||
*/ |
||||
class VideoManager extends AbstractVideoManager<Props> { |
||||
/** |
||||
* Initializes a new VideoManager instance. |
||||
* |
||||
* @param {Object} props - This component's props. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
constructor(props) { |
||||
super(props); |
||||
|
||||
this.playerRef = React.createRef(); |
||||
} |
||||
|
||||
/** |
||||
* Retrieves the current player ref. |
||||
*/ |
||||
get player() { |
||||
return this.playerRef.current; |
||||
} |
||||
|
||||
/** |
||||
* Indicates the playback state of the video. |
||||
* |
||||
* @returns {string} |
||||
*/ |
||||
getPlaybackState() { |
||||
let state; |
||||
|
||||
if (!this.player) { |
||||
return; |
||||
} |
||||
|
||||
if (this.player.paused) { |
||||
state = PLAYBACK_STATES.PAUSED; |
||||
} else { |
||||
state = PLAYBACK_STATES.PLAYING; |
||||
} |
||||
|
||||
return state; |
||||
} |
||||
|
||||
/** |
||||
* Indicates whether the video is muted. |
||||
* |
||||
* @returns {boolean} |
||||
*/ |
||||
isMuted() { |
||||
return this.player?.muted; |
||||
} |
||||
|
||||
/** |
||||
* Retrieves current volume. |
||||
* |
||||
* @returns {number} |
||||
*/ |
||||
getVolume() { |
||||
return this.player?.volume; |
||||
} |
||||
|
||||
/** |
||||
* Sets player volume. |
||||
* |
||||
* @param {number} value - The volume. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
setVolume(value) { |
||||
if (this.player) { |
||||
this.player.volume = value; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Retrieves current time. |
||||
* |
||||
* @returns {number} |
||||
*/ |
||||
getTime() { |
||||
return this.player?.currentTime; |
||||
} |
||||
|
||||
/** |
||||
* Seeks video to provided time. |
||||
* |
||||
* @param {number} time - The time to seek to. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
seek(time) { |
||||
if (this.player) { |
||||
this.player.currentTime = time; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Plays video. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
play() { |
||||
return this.player?.play(); |
||||
} |
||||
|
||||
/** |
||||
* Pauses video. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
pause() { |
||||
return this.player?.pause(); |
||||
} |
||||
|
||||
/** |
||||
* Mutes video. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
mute() { |
||||
if (this.player) { |
||||
this.player.muted = true; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Unmutes video. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
unMute() { |
||||
if (this.player) { |
||||
this.player.muted = false; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Retrieves video tag params. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
getPlayerOptions() { |
||||
const { _isOwner, videoId } = this.props; |
||||
|
||||
let options = { |
||||
autoPlay: true, |
||||
src: videoId, |
||||
controls: _isOwner, |
||||
onError: event => { |
||||
logger.error('Error in the player:', event); |
||||
}, |
||||
onPlay: () => this.onPlay(), |
||||
onVolumeChange: () => this.onVolumeChange() |
||||
}; |
||||
|
||||
if (_isOwner) { |
||||
options = { |
||||
...options, |
||||
onPause: () => this.onPause(), |
||||
onTimeUpdate: this.throttledFireUpdateSharedVideoEvent |
||||
}; |
||||
|
||||
} |
||||
|
||||
return options; |
||||
} |
||||
|
||||
/** |
||||
* Implements React Component's render. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
render() { |
||||
return (<video |
||||
id = 'sharedVideoPlayer' |
||||
ref = { this.playerRef } |
||||
{ ...this.getPlayerOptions() } />); |
||||
} |
||||
} |
||||
|
||||
export default connect(_mapStateToProps, _mapDispatchToProps)(VideoManager); |
@ -0,0 +1,251 @@ |
||||
/* eslint-disable no-invalid-this */ |
||||
import Logger from 'jitsi-meet-logger'; |
||||
import React from 'react'; |
||||
import YouTube from 'react-youtube'; |
||||
|
||||
import { connect } from '../../../base/redux'; |
||||
|
||||
import AbstractVideoManager, { |
||||
_mapDispatchToProps, |
||||
_mapStateToProps, |
||||
PLAYBACK_STATES |
||||
} from './AbstractVideoManager'; |
||||
|
||||
const logger = Logger.getLogger(__filename); |
||||
|
||||
/** |
||||
* Manager of shared video. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
class YoutubeVideoManager extends AbstractVideoManager<Props> { |
||||
/** |
||||
* Initializes a new YoutubeVideoManager instance. |
||||
* |
||||
* @param {Object} props - This component's props. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
constructor(props) { |
||||
super(props); |
||||
|
||||
this.isPlayerAPILoaded = false; |
||||
} |
||||
|
||||
/** |
||||
* Indicates the playback state of the video. |
||||
* |
||||
* @returns {string} |
||||
*/ |
||||
getPlaybackState() { |
||||
let state; |
||||
|
||||
if (!this.player) { |
||||
return; |
||||
} |
||||
|
||||
const playerState = this.player.getPlayerState(); |
||||
|
||||
if (playerState === YouTube.PlayerState.PLAYING) { |
||||
state = PLAYBACK_STATES.PLAYING; |
||||
} |
||||
|
||||
if (playerState === YouTube.PlayerState.PAUSED) { |
||||
state = PLAYBACK_STATES.PAUSED; |
||||
} |
||||
|
||||
return state; |
||||
} |
||||
|
||||
/** |
||||
* Indicates whether the video is muted. |
||||
* |
||||
* @returns {boolean} |
||||
*/ |
||||
isMuted() { |
||||
return this.player?.isMuted(); |
||||
} |
||||
|
||||
/** |
||||
* Retrieves current volume. |
||||
* |
||||
* @returns {number} |
||||
*/ |
||||
getVolume() { |
||||
return this.player?.getVolume(); |
||||
} |
||||
|
||||
/** |
||||
* Sets player volume. |
||||
* |
||||
* @param {number} value - The volume. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
setVolume(value) { |
||||
return this.player?.setVolume(value); |
||||
} |
||||
|
||||
/** |
||||
* Retrieves current time. |
||||
* |
||||
* @returns {number} |
||||
*/ |
||||
getTime() { |
||||
return this.player?.getCurrentTime(); |
||||
} |
||||
|
||||
/** |
||||
* Seeks video to provided time. |
||||
* |
||||
* @param {number} time - The time to seek to. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
seek(time) { |
||||
return this.player?.seekTo(time); |
||||
} |
||||
|
||||
/** |
||||
* Plays video. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
play() { |
||||
return this.player?.playVideo(); |
||||
} |
||||
|
||||
/** |
||||
* Pauses video. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
pause() { |
||||
return this.player?.pauseVideo(); |
||||
} |
||||
|
||||
/** |
||||
* Mutes video. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
mute() { |
||||
return this.player?.mute(); |
||||
} |
||||
|
||||
/** |
||||
* Unmutes video. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
unMute() { |
||||
return this.player?.unMute(); |
||||
} |
||||
|
||||
/** |
||||
* Disposes of the current video player. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
dispose() { |
||||
if (this.player) { |
||||
this.player.destroy(); |
||||
this.player = null; |
||||
} |
||||
|
||||
if (this.errorInPlayer) { |
||||
this.errorInPlayer.destroy(); |
||||
this.errorInPlayer = null; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Fired on play state toggle. |
||||
* |
||||
* @param {Object} event - The yt player stateChange event. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
onPlayerStateChange = event => { |
||||
if (event.data === YouTube.PlayerState.PLAYING) { |
||||
this.onPlay(); |
||||
} else if (event.data === YouTube.PlayerState.PAUSED) { |
||||
this.onPause(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Fired when youtube player is ready. |
||||
* |
||||
* @param {Object} event - The youtube player event. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
onPlayerReady = event => { |
||||
const { _isOwner } = this.props; |
||||
|
||||
this.player = event.target; |
||||
|
||||
this.player.addEventListener('onVolumeChange', () => { |
||||
this.onVolumeChange(); |
||||
}); |
||||
|
||||
if (_isOwner) { |
||||
this.player.addEventListener('onVideoProgress', this.throttledFireUpdateSharedVideoEvent); |
||||
} |
||||
|
||||
this.play(); |
||||
}; |
||||
|
||||
/** |
||||
* Fired when youtube player throws an error. |
||||
* |
||||
* @param {Object} event - Youtube player error event. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
onPlayerError = event => { |
||||
logger.error('Error in the player:', event.data); |
||||
|
||||
// store the error player, so we can remove it
|
||||
this.errorInPlayer = event.target; |
||||
}; |
||||
|
||||
getPlayerOptions = () => { |
||||
const { _isOwner, videoId } = this.props; |
||||
const showControls = _isOwner ? 1 : 0; |
||||
|
||||
const options = { |
||||
id: 'sharedVideoPlayer', |
||||
opts: { |
||||
height: '100%', |
||||
width: '100%', |
||||
playerVars: { |
||||
'origin': location.origin, |
||||
'fs': '0', |
||||
'autoplay': 0, |
||||
'controls': showControls, |
||||
'rel': 0 |
||||
} |
||||
}, |
||||
onError: this.onPlayerError, |
||||
onReady: this.onPlayerReady, |
||||
onStateChange: this.onPlayerStateChange, |
||||
videoId |
||||
}; |
||||
|
||||
return options; |
||||
} |
||||
|
||||
/** |
||||
* Implements React Component's render. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
render() { |
||||
return (<YouTube |
||||
{ ...this.getPlayerOptions() } />); |
||||
} |
||||
} |
||||
|
||||
export default connect(_mapStateToProps, _mapDispatchToProps)(YoutubeVideoManager); |
@ -0,0 +1,177 @@ |
||||
// @flow
|
||||
|
||||
import { batch } from 'react-redux'; |
||||
|
||||
import { CONFERENCE_LEFT, getCurrentConference } from '../base/conference'; |
||||
import { |
||||
PARTICIPANT_LEFT, |
||||
getLocalParticipant, |
||||
participantJoined, |
||||
participantLeft, |
||||
pinParticipant |
||||
} from '../base/participants'; |
||||
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux'; |
||||
|
||||
import { SET_SHARED_VIDEO_STATUS, RESET_SHARED_VIDEO_STATUS } from './actionTypes'; |
||||
import { |
||||
resetSharedVideoStatus, |
||||
setSharedVideoStatus |
||||
} from './actions.any'; |
||||
import { SHARED_VIDEO, VIDEO_PLAYER_PARTICIPANT_NAME } from './constants'; |
||||
import { getYoutubeId, isSharingStatus } from './functions'; |
||||
|
||||
/** |
||||
* Middleware that captures actions related to video sharing and updates |
||||
* components not hooked into redux. |
||||
* |
||||
* @param {Store} store - The redux store. |
||||
* @returns {Function} |
||||
*/ |
||||
MiddlewareRegistry.register(store => next => action => { |
||||
const { dispatch, getState } = store; |
||||
const state = getState(); |
||||
const conference = getCurrentConference(state); |
||||
const localParticipantId = getLocalParticipant(state)?.id; |
||||
const { videoUrl, status, ownerId, time, muted, volume } = action; |
||||
const { ownerId: stateOwnerId, videoUrl: statevideoUrl } = state['features/shared-video']; |
||||
|
||||
switch (action.type) { |
||||
case CONFERENCE_LEFT: |
||||
dispatch(resetSharedVideoStatus()); |
||||
break; |
||||
case PARTICIPANT_LEFT: |
||||
if (action.participant.id === stateOwnerId) { |
||||
batch(() => { |
||||
dispatch(resetSharedVideoStatus()); |
||||
dispatch(participantLeft(statevideoUrl, conference)); |
||||
}); |
||||
} |
||||
break; |
||||
case SET_SHARED_VIDEO_STATUS: |
||||
if (localParticipantId === ownerId) { |
||||
sendShareVideoCommand({ |
||||
conference, |
||||
localParticipantId, |
||||
muted, |
||||
status, |
||||
time, |
||||
id: videoUrl, |
||||
volume |
||||
}); |
||||
} |
||||
break; |
||||
case RESET_SHARED_VIDEO_STATUS: |
||||
if (localParticipantId === stateOwnerId) { |
||||
sendShareVideoCommand({ |
||||
conference, |
||||
id: statevideoUrl, |
||||
localParticipantId, |
||||
muted: true, |
||||
status: 'stop', |
||||
time: 0, |
||||
volume: 0 |
||||
}); |
||||
} |
||||
break; |
||||
} |
||||
|
||||
return next(action); |
||||
}); |
||||
|
||||
/** |
||||
* Set up state change listener to perform maintenance tasks when the conference |
||||
* is left or failed, e.g. clear messages or close the chat modal if it's left |
||||
* open. |
||||
*/ |
||||
StateListenerRegistry.register( |
||||
state => getCurrentConference(state), |
||||
(conference, store, previousConference) => { |
||||
if (conference && conference !== previousConference) { |
||||
conference.addCommandListener(SHARED_VIDEO, |
||||
({ value, attributes }) => { |
||||
|
||||
const { dispatch, getState } = store; |
||||
const { from } = attributes; |
||||
const localParticipantId = getLocalParticipant(getState()).id; |
||||
const status = attributes.state; |
||||
|
||||
if (isSharingStatus(status)) { |
||||
handleSharingVideoStatus(store, value, attributes, conference); |
||||
} else if (status === 'stop') { |
||||
dispatch(participantLeft(value, conference)); |
||||
if (localParticipantId !== from) { |
||||
dispatch(resetSharedVideoStatus()); |
||||
} |
||||
} |
||||
} |
||||
); |
||||
} |
||||
} |
||||
); |
||||
|
||||
/** |
||||
* Handles the playing, pause and start statuses for the shared video. |
||||
* Dispatches participantJoined event and, if necessary, pins it. |
||||
* Sets the SharedVideoStatus if the event was triggered by the local user. |
||||
* |
||||
* @param {Store} store - The redux store. |
||||
* @param {string} videoUrl - The id of the video to the shared. |
||||
* @param {Object} attributes - The attributes received from the share video command. |
||||
* @param {JitsiConference} conference - The current conference. |
||||
* @returns {void} |
||||
*/ |
||||
function handleSharingVideoStatus(store, videoUrl, { state, time, from, muted }, conference) { |
||||
const { dispatch, getState } = store; |
||||
const localParticipantId = getLocalParticipant(getState()).id; |
||||
const oldStatus = getState()['features/shared-video']?.status; |
||||
|
||||
if (state === 'start' || ![ 'playing', 'pause', 'start' ].includes(oldStatus)) { |
||||
const youtubeId = getYoutubeId(videoUrl); |
||||
const avatarURL = youtubeId ? `https://img.youtube.com/vi/${youtubeId}/0.jpg` : ''; |
||||
|
||||
dispatch(participantJoined({ |
||||
conference, |
||||
id: videoUrl, |
||||
isFakeParticipant: true, |
||||
avatarURL, |
||||
name: VIDEO_PLAYER_PARTICIPANT_NAME |
||||
})); |
||||
|
||||
dispatch(pinParticipant(videoUrl)); |
||||
} |
||||
|
||||
if (localParticipantId !== from) { |
||||
dispatch(setSharedVideoStatus({ |
||||
muted: muted === 'true', |
||||
ownerId: from, |
||||
status: state, |
||||
time: Number(time), |
||||
videoUrl |
||||
})); |
||||
} |
||||
} |
||||
|
||||
/* eslint-disable max-params */ |
||||
|
||||
/** |
||||
* Sends SHARED_VIDEO command. |
||||
* |
||||
* @param {string} id - The id of the video. |
||||
* @param {string} status - The status of the shared video. |
||||
* @param {JitsiConference} conference - The current conference. |
||||
* @param {string} localParticipantId - The id of the local participant. |
||||
* @param {string} time - The seek position of the video. |
||||
* @returns {void} |
||||
*/ |
||||
function sendShareVideoCommand({ id, status, conference, localParticipantId, time, muted, volume }) { |
||||
conference.sendCommandOnce(SHARED_VIDEO, { |
||||
value: id, |
||||
attributes: { |
||||
from: localParticipantId, |
||||
muted, |
||||
state: status, |
||||
time, |
||||
volume |
||||
} |
||||
}); |
||||
} |
@ -1,186 +1 @@ |
||||
// @flow
|
||||
|
||||
import { CONFERENCE_LEFT, getCurrentConference } from '../base/conference'; |
||||
import { |
||||
PARTICIPANT_LEFT, |
||||
getLocalParticipant, |
||||
participantJoined, |
||||
participantLeft, |
||||
pinParticipant |
||||
} from '../base/participants'; |
||||
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux'; |
||||
|
||||
import { TOGGLE_SHARED_VIDEO, SET_SHARED_VIDEO_STATUS } from './actionTypes'; |
||||
import { setSharedVideoStatus, showSharedVideoDialog } from './actions.native'; |
||||
import { SHARED_VIDEO, VIDEO_PLAYER_PARTICIPANT_NAME } from './constants'; |
||||
import { isSharingStatus } from './functions'; |
||||
|
||||
/** |
||||
* Middleware that captures actions related to video sharing and updates |
||||
* components not hooked into redux. |
||||
* |
||||
* @param {Store} store - The redux store. |
||||
* @returns {Function} |
||||
*/ |
||||
MiddlewareRegistry.register(store => next => action => { |
||||
const { dispatch, getState } = store; |
||||
const state = getState(); |
||||
const conference = getCurrentConference(state); |
||||
const localParticipantId = getLocalParticipant(state)?.id; |
||||
const { videoId, status, ownerId, time } = action; |
||||
const { ownerId: stateOwnerId, videoId: stateVideoId } = state['features/shared-video']; |
||||
|
||||
switch (action.type) { |
||||
case TOGGLE_SHARED_VIDEO: |
||||
_toggleSharedVideo(store, next, action); |
||||
break; |
||||
case CONFERENCE_LEFT: |
||||
dispatch(setSharedVideoStatus('', 'stop', 0, '')); |
||||
break; |
||||
case PARTICIPANT_LEFT: |
||||
if (action.participant.id === stateOwnerId) { |
||||
dispatch(setSharedVideoStatus('', 'stop', 0, '')); |
||||
dispatch(participantLeft(stateVideoId, conference)); |
||||
} |
||||
break; |
||||
case SET_SHARED_VIDEO_STATUS: |
||||
if (localParticipantId === ownerId) { |
||||
sendShareVideoCommand(videoId, status, conference, localParticipantId, time); |
||||
} |
||||
break; |
||||
} |
||||
|
||||
return next(action); |
||||
}); |
||||
|
||||
/** |
||||
* Set up state change listener to perform maintenance tasks when the conference |
||||
* is left or failed, e.g. clear messages or close the chat modal if it's left |
||||
* open. |
||||
*/ |
||||
StateListenerRegistry.register( |
||||
state => getCurrentConference(state), |
||||
(conference, store, previousConference) => { |
||||
if (conference && conference !== previousConference) { |
||||
conference.addCommandListener(SHARED_VIDEO, |
||||
({ value, attributes }) => { |
||||
|
||||
const { dispatch, getState } = store; |
||||
const { from } = attributes; |
||||
const localParticipantId = getLocalParticipant(getState()).id; |
||||
const status = attributes.state; |
||||
|
||||
if (isSharingStatus(status)) { |
||||
handleSharingVideoStatus(store, value, attributes, conference); |
||||
} else if (status === 'stop') { |
||||
dispatch(participantLeft(value, conference)); |
||||
if (localParticipantId !== from) { |
||||
dispatch(setSharedVideoStatus(value, 'stop', 0, from)); |
||||
} |
||||
} |
||||
} |
||||
); |
||||
} |
||||
} |
||||
); |
||||
|
||||
/** |
||||
* Handles the playing, pause and start statuses for the shared video. |
||||
* Dispatches participantJoined event and, if necessary, pins it. |
||||
* Sets the SharedVideoStatus if the event was triggered by the local user. |
||||
* |
||||
* @param {Store} store - The redux store. |
||||
* @param {string} videoId - The id of the video to the shared. |
||||
* @param {Object} attributes - The attributes received from the share video command. |
||||
* @param {JitsiConference} conference - The current conference. |
||||
* @returns {void} |
||||
*/ |
||||
function handleSharingVideoStatus(store, videoId, { state, time, from }, conference) { |
||||
const { dispatch, getState } = store; |
||||
const localParticipantId = getLocalParticipant(getState()).id; |
||||
const oldStatus = getState()['features/shared-video']?.status; |
||||
|
||||
if (state === 'start' || ![ 'playing', 'pause', 'start' ].includes(oldStatus)) { |
||||
dispatch(participantJoined({ |
||||
conference, |
||||
id: videoId, |
||||
isFakeParticipant: true, |
||||
avatarURL: `https://img.youtube.com/vi/${videoId}/0.jpg`, |
||||
name: VIDEO_PLAYER_PARTICIPANT_NAME |
||||
})); |
||||
|
||||
dispatch(pinParticipant(videoId)); |
||||
} |
||||
|
||||
if (localParticipantId !== from) { |
||||
dispatch(setSharedVideoStatus(videoId, state, time, from)); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Dispatches shared video status. |
||||
* |
||||
* @param {Store} store - The redux store. |
||||
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the |
||||
* specified {@code action} in the specified {@code store}. |
||||
* @param {Action} action - The redux action which is |
||||
* being dispatched in the specified {@code store}. |
||||
* @returns {Function} |
||||
*/ |
||||
function _toggleSharedVideo(store, next, action) { |
||||
const { dispatch, getState } = store; |
||||
const state = getState(); |
||||
const { videoId, ownerId, status } = state['features/shared-video']; |
||||
const localParticipant = getLocalParticipant(state); |
||||
|
||||
if (status === 'playing' || status === 'start' || status === 'pause') { |
||||
if (ownerId === localParticipant.id) { |
||||
dispatch(setSharedVideoStatus(videoId, 'stop', 0, localParticipant.id)); |
||||
} |
||||
} else { |
||||
dispatch(showSharedVideoDialog(id => _onVideoLinkEntered(store, id))); |
||||
} |
||||
|
||||
return next(action); |
||||
} |
||||
|
||||
/** |
||||
* Sends SHARED_VIDEO start command. |
||||
* |
||||
* @param {Store} store - The redux store. |
||||
* @param {string} id - The id of the video to be shared. |
||||
* @returns {void} |
||||
*/ |
||||
function _onVideoLinkEntered(store, id) { |
||||
const { dispatch, getState } = store; |
||||
const conference = getCurrentConference(getState()); |
||||
|
||||
if (conference) { |
||||
const localParticipant = getLocalParticipant(getState()); |
||||
|
||||
dispatch(setSharedVideoStatus(id, 'start', 0, localParticipant.id)); |
||||
} |
||||
} |
||||
|
||||
/* eslint-disable max-params */ |
||||
|
||||
/** |
||||
* Sends SHARED_VIDEO command. |
||||
* |
||||
* @param {string} id - The id of the video. |
||||
* @param {string} status - The status of the shared video. |
||||
* @param {JitsiConference} conference - The current conference. |
||||
* @param {string} localParticipantId - The id of the local participant. |
||||
* @param {string} time - The seek position of the video. |
||||
* @returns {void} |
||||
*/ |
||||
function sendShareVideoCommand(id, status, conference, localParticipantId, time) { |
||||
conference.sendCommandOnce(SHARED_VIDEO, { |
||||
value: id, |
||||
attributes: { |
||||
from: localParticipantId, |
||||
state: status, |
||||
time |
||||
} |
||||
}); |
||||
} |
||||
import './middleware.any'; |
||||
|
Loading…
Reference in new issue