feat(share-video) Add capability for sharing any direct link video
This commit is contained in:
parent
9657bd9b6d
commit
4e4ff0f60f
|
@ -129,7 +129,6 @@ import {
|
|||
import { disableReceiver, stopReceiver } from './react/features/remote-control';
|
||||
import { setScreenAudioShareState, isScreenAudioShared } from './react/features/screen-share/';
|
||||
import { toggleScreenshotCaptureEffect } from './react/features/screenshot-capture';
|
||||
import { setSharedVideoStatus } from './react/features/shared-video/actions';
|
||||
import { AudioMixerEffect } from './react/features/stream-effects/audio-mixer/AudioMixerEffect';
|
||||
import { createPresenterEffect } from './react/features/stream-effects/presenter';
|
||||
import { endpointMessageReceived } from './react/features/subtitles';
|
||||
|
@ -177,8 +176,7 @@ const commands = {
|
|||
AVATAR_URL: AVATAR_URL_COMMAND,
|
||||
CUSTOM_ROLE: 'custom-role',
|
||||
EMAIL: EMAIL_COMMAND,
|
||||
ETHERPAD: 'etherpad',
|
||||
SHARED_VIDEO: 'shared-video'
|
||||
ETHERPAD: 'etherpad'
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -2017,8 +2015,6 @@ export default {
|
|||
}
|
||||
|
||||
logger.log(`USER ${id} LEFT:`, user);
|
||||
|
||||
APP.UI.onSharedVideoStop(id);
|
||||
});
|
||||
|
||||
room.on(JitsiConferenceEvents.USER_STATUS_CHANGED, (id, status) => {
|
||||
|
@ -2454,59 +2450,6 @@ export default {
|
|||
this.toggleScreenSharing(undefined, { audioOnly });
|
||||
}
|
||||
);
|
||||
|
||||
/* eslint-disable max-params */
|
||||
APP.UI.addListener(
|
||||
UIEvents.UPDATE_SHARED_VIDEO,
|
||||
(url, state, time, isMuted, volume) => {
|
||||
/* eslint-enable max-params */
|
||||
// send start and stop commands once, and remove any updates
|
||||
// that had left
|
||||
if (state === 'stop'
|
||||
|| state === 'start'
|
||||
|| state === 'playing') {
|
||||
const localParticipant = getLocalParticipant(APP.store.getState());
|
||||
|
||||
room.removeCommand(this.commands.defaults.SHARED_VIDEO);
|
||||
room.sendCommandOnce(this.commands.defaults.SHARED_VIDEO, {
|
||||
value: url,
|
||||
attributes: {
|
||||
state,
|
||||
time,
|
||||
muted: isMuted,
|
||||
volume,
|
||||
from: localParticipant.id
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// in case of paused, in order to allow late users to join
|
||||
// paused
|
||||
room.removeCommand(this.commands.defaults.SHARED_VIDEO);
|
||||
room.sendCommand(this.commands.defaults.SHARED_VIDEO, {
|
||||
value: url,
|
||||
attributes: {
|
||||
state,
|
||||
time,
|
||||
muted: isMuted,
|
||||
volume
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
APP.store.dispatch(setSharedVideoStatus(state));
|
||||
});
|
||||
room.addCommandListener(
|
||||
this.commands.defaults.SHARED_VIDEO,
|
||||
({ value, attributes }, id) => {
|
||||
if (attributes.state === 'stop') {
|
||||
APP.UI.onSharedVideoStop(id, attributes);
|
||||
} else if (attributes.state === 'start') {
|
||||
APP.UI.onSharedVideoStart(id, value, attributes);
|
||||
} else if (attributes.state === 'playing'
|
||||
|| attributes.state === 'pause') {
|
||||
APP.UI.onSharedVideoUpdate(id, value, attributes);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -240,6 +240,15 @@
|
|||
object-fit: cover;
|
||||
}
|
||||
|
||||
#sharedVideo video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#sharedVideo.disable-pointer {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#sharedVideo,
|
||||
#etherpad,
|
||||
#localVideoWrapper video,
|
||||
|
|
|
@ -828,7 +828,7 @@
|
|||
"security": "Security options",
|
||||
"Settings": "Settings",
|
||||
"shareaudio": "Share audio",
|
||||
"sharedvideo": "Share a YouTube video",
|
||||
"sharedvideo": "Share a video",
|
||||
"shareRoom": "Invite someone",
|
||||
"shortcuts": "View shortcuts",
|
||||
"speakerStats": "Speaker stats",
|
||||
|
|
|
@ -44,6 +44,7 @@ import {
|
|||
import { toggleLobbyMode } from '../../react/features/lobby/actions';
|
||||
import { RECORDING_TYPES } from '../../react/features/recording/constants';
|
||||
import { getActiveSession } from '../../react/features/recording/functions';
|
||||
import { playSharedVideo, stopSharedVideo } from '../../react/features/shared-video/actions.any';
|
||||
import { toggleTileView, setTileView } from '../../react/features/video-layout';
|
||||
import { muteAllParticipants } from '../../react/features/video-menu/actions';
|
||||
import { setVideoQuality } from '../../react/features/video-quality';
|
||||
|
@ -263,6 +264,18 @@ function initCommands() {
|
|||
APP.store.dispatch(setVideoQuality(frameHeight));
|
||||
},
|
||||
|
||||
'start-share-video': url => {
|
||||
logger.debug('Share video command received');
|
||||
sendAnalytics(createApiEvent('share.video.start'));
|
||||
APP.store.dispatch(playSharedVideo(url));
|
||||
},
|
||||
|
||||
'stop-share-video': () => {
|
||||
logger.debug('Share video command received');
|
||||
sendAnalytics(createApiEvent('share.video.start'));
|
||||
APP.store.dispatch(stopSharedVideo());
|
||||
},
|
||||
|
||||
/**
|
||||
* Starts a file recording or streaming session depending on the passed on params.
|
||||
* For RTMP streams, `rtmpStreamKey` must be passed on. `rtmpBroadcastID` is optional.
|
||||
|
|
|
@ -47,7 +47,9 @@ const commands = {
|
|||
setTileView: 'set-tile-view',
|
||||
setVideoQuality: 'set-video-quality',
|
||||
startRecording: 'start-recording',
|
||||
startShareVideo: 'start-share-video',
|
||||
stopRecording: 'stop-recording',
|
||||
stopShareVideo: 'stop-share-video',
|
||||
subject: 'subject',
|
||||
submitFeedback: 'submit-feedback',
|
||||
toggleAudio: 'toggle-audio',
|
||||
|
|
|
@ -19,7 +19,6 @@ import {
|
|||
import UIEvents from '../../service/UI/UIEvents';
|
||||
|
||||
import EtherpadManager from './etherpad/Etherpad';
|
||||
import SharedVideoManager from './shared_video/SharedVideo';
|
||||
import messageHandler from './util/MessageHandler';
|
||||
import UIUtil from './util/UIUtil';
|
||||
import VideoLayout from './videolayout/VideoLayout';
|
||||
|
@ -33,15 +32,11 @@ const eventEmitter = new EventEmitter();
|
|||
UI.eventEmitter = eventEmitter;
|
||||
|
||||
let etherpadManager;
|
||||
let sharedVideoManager;
|
||||
|
||||
const UIListeners = new Map([
|
||||
[
|
||||
UIEvents.ETHERPAD_CLICKED,
|
||||
() => etherpadManager && etherpadManager.toggleEtherpad()
|
||||
], [
|
||||
UIEvents.SHARED_VIDEO_CLICKED,
|
||||
() => sharedVideoManager && sharedVideoManager.toggleSharedVideo()
|
||||
], [
|
||||
UIEvents.TOGGLE_FILMSTRIP,
|
||||
() => UI.toggleFilmstrip()
|
||||
|
@ -58,14 +53,6 @@ UI.isFullScreen = function() {
|
|||
return UIUtil.isFullScreen();
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if there is a shared video which is being shown (?).
|
||||
* @returns {boolean} - true if there is a shared video which is being shown.
|
||||
*/
|
||||
UI.isSharedVideoShown = function() {
|
||||
return Boolean(sharedVideoManager && sharedVideoManager.isSharedVideoShown);
|
||||
};
|
||||
|
||||
/**
|
||||
* Notify user that server has shut down.
|
||||
*/
|
||||
|
@ -112,8 +99,6 @@ UI.start = function() {
|
|||
// will be seen animating in.
|
||||
VideoLayout.resizeVideoArea();
|
||||
|
||||
sharedVideoManager = new SharedVideoManager(eventEmitter);
|
||||
|
||||
if (isMobileBrowser()) {
|
||||
$('body').addClass('mobile-browser');
|
||||
} else {
|
||||
|
@ -411,60 +396,6 @@ UI.getLargeVideo = function() {
|
|||
return VideoLayout.getLargeVideo();
|
||||
};
|
||||
|
||||
/**
|
||||
* Show shared video.
|
||||
* @param {string} id the id of the sender of the command
|
||||
* @param {string} url video url
|
||||
* @param {string} attributes
|
||||
*/
|
||||
UI.onSharedVideoStart = function(id, url, attributes) {
|
||||
if (sharedVideoManager) {
|
||||
sharedVideoManager.onSharedVideoStart(id, url, attributes);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update shared video.
|
||||
* @param {string} id the id of the sender of the command
|
||||
* @param {string} url video url
|
||||
* @param {string} attributes
|
||||
*/
|
||||
UI.onSharedVideoUpdate = function(id, url, attributes) {
|
||||
if (sharedVideoManager) {
|
||||
sharedVideoManager.onSharedVideoUpdate(id, url, attributes);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Stop showing shared video.
|
||||
* @param {string} id the id of the sender of the command
|
||||
* @param {string} attributes
|
||||
*/
|
||||
UI.onSharedVideoStop = function(id, attributes) {
|
||||
if (sharedVideoManager) {
|
||||
sharedVideoManager.onSharedVideoStop(id, attributes);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Show shared video.
|
||||
* @param {string} url video url
|
||||
*/
|
||||
UI.startSharedVideoEmitter = function(url) {
|
||||
if (sharedVideoManager) {
|
||||
sharedVideoManager.startSharedVideoEmitter(url);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Stop shared video.
|
||||
*/
|
||||
UI.stopSharedVideoEmitter = function() {
|
||||
if (sharedVideoManager) {
|
||||
sharedVideoManager.stopSharedVideoEmitter();
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: Export every function separately. For now there is no point of doing
|
||||
// this because we are importing everything.
|
||||
export default UI;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -8,7 +8,6 @@ import {
|
|||
getParticipantById
|
||||
} from '../../../react/features/base/participants';
|
||||
import { getTrackByMediaTypeAndParticipant } from '../../../react/features/base/tracks';
|
||||
import { SHARED_VIDEO_CONTAINER_TYPE } from '../shared_video/SharedVideo';
|
||||
|
||||
import LargeVideoManager from './LargeVideoManager';
|
||||
import { VIDEO_CONTAINER_TYPE } from './VideoContainer';
|
||||
|
@ -89,7 +88,7 @@ const VideoLayout = {
|
|||
const participant = getParticipantById(state, id);
|
||||
|
||||
if (participant?.isFakeParticipant) {
|
||||
return SHARED_VIDEO_CONTAINER_TYPE;
|
||||
return VIDEO_TYPE.CAMERA;
|
||||
}
|
||||
|
||||
const videoTrack = getTrackByMediaTypeAndParticipant(state['features/base/tracks'], MEDIA_TYPE.VIDEO, id);
|
||||
|
|
|
@ -11145,6 +11145,11 @@
|
|||
"strip-bom": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"load-script": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz",
|
||||
"integrity": "sha1-BJGTngvuVkPuSUp+PaPSuscMbKQ="
|
||||
},
|
||||
"loader-runner": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz",
|
||||
|
@ -15305,6 +15310,23 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"react-youtube": {
|
||||
"version": "7.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-youtube/-/react-youtube-7.13.1.tgz",
|
||||
"integrity": "sha512-b++TLHmHDpd0ZBS1wcbYabbuchU+W4jtx5A2MUQX0BINNKKsaIQX29sn/aLvZ9v5luwAoceia3VGtyz9blaB9w==",
|
||||
"requires": {
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"prop-types": "15.7.2",
|
||||
"youtube-player": "5.5.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"read-pkg": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz",
|
||||
|
@ -16105,6 +16127,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"sister": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/sister/-/sister-3.0.2.tgz",
|
||||
"integrity": "sha512-p19rtTs+NksBRKW9qn0UhZ8/TUI9BPw9lmtHny+Y3TinWlOa9jWh9xB0AtPSdmOy49NJJJSSe0Ey4C7h0TrcYA=="
|
||||
},
|
||||
"slash": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
|
||||
|
@ -19806,6 +19833,16 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"youtube-player": {
|
||||
"version": "5.5.2",
|
||||
"resolved": "https://registry.npmjs.org/youtube-player/-/youtube-player-5.5.2.tgz",
|
||||
"integrity": "sha512-ZGtsemSpXnDky2AUYWgxjaopgB+shFHgXVpiJFeNB5nWEugpW1KWYDaHKuLqh2b67r24GtP6HoSW5swvf0fFIQ==",
|
||||
"requires": {
|
||||
"debug": "^2.6.6",
|
||||
"load-script": "^1.0.0",
|
||||
"sister": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"zxcvbn": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/zxcvbn/-/zxcvbn-4.4.2.tgz",
|
||||
|
|
|
@ -92,6 +92,7 @@
|
|||
"react-redux": "7.1.0",
|
||||
"react-textarea-autosize": "7.1.0",
|
||||
"react-transition-group": "2.4.0",
|
||||
"react-youtube": "7.13.1",
|
||||
"redux": "4.0.4",
|
||||
"redux-thunk": "2.2.0",
|
||||
"rnnoise-wasm": "github:jitsi/rnnoise-wasm#566a16885897704d6e6d67a1d5ac5d39781db2af",
|
||||
|
|
|
@ -4,6 +4,7 @@ import React, { Component } from 'react';
|
|||
import { Text, View } from 'react-native';
|
||||
|
||||
import { YoutubeLargeVideo } from '../../../shared-video/components';
|
||||
import { getYoutubeId } from '../../../shared-video/functions';
|
||||
import { Avatar } from '../../avatar';
|
||||
import { translate } from '../../i18n';
|
||||
import { JitsiParticipantConnectionStatus } from '../../lib-jitsi-meet';
|
||||
|
@ -208,7 +209,8 @@ class ParticipantView extends Component<Props> {
|
|||
? this.props.testHintId
|
||||
: `org.jitsi.meet.Participant#${this.props.participantId}`;
|
||||
|
||||
const renderYoutubeLargeVideo = _isFakeParticipant && !disableVideo;
|
||||
const youtubeId = getYoutubeId(this.props.participantId);
|
||||
const renderYoutubeLargeVideo = _isFakeParticipant && !disableVideo && Boolean(youtubeId);
|
||||
|
||||
return (
|
||||
<Container
|
||||
|
@ -224,7 +226,7 @@ class ParticipantView extends Component<Props> {
|
|||
onPress = { renderYoutubeLargeVideo ? undefined : onPress }
|
||||
value = '' />
|
||||
|
||||
{ renderYoutubeLargeVideo && <YoutubeLargeVideo youtubeId = { this.props.participantId } /> }
|
||||
{ renderYoutubeLargeVideo && <YoutubeLargeVideo youtubeId = { youtubeId } /> }
|
||||
|
||||
{ !_isFakeParticipant && renderVideo
|
||||
&& <VideoTrack
|
||||
|
|
|
@ -550,7 +550,7 @@ class Thumbnail extends Component<Props, State> {
|
|||
*/
|
||||
_renderFakeParticipant() {
|
||||
const { _participant } = this.props;
|
||||
const { id } = _participant;
|
||||
const { id, avatarURL } = _participant;
|
||||
const styles = this._getStyles();
|
||||
const containerClassName = this._getContainerClassName();
|
||||
|
||||
|
@ -562,9 +562,12 @@ class Thumbnail extends Component<Props, State> {
|
|||
onMouseEnter = { this._onMouseEnter }
|
||||
onMouseLeave = { this._onMouseLeave }
|
||||
style = { styles.thumbnail }>
|
||||
<img
|
||||
className = 'sharedVideoAvatar'
|
||||
src = { `https://img.youtube.com/vi/${id}/0.jpg` } />
|
||||
{avatarURL ? (
|
||||
<img
|
||||
className = 'sharedVideoAvatar'
|
||||
src = { avatarURL } />
|
||||
)
|
||||
: this._renderAvatar(styles.avatar)}
|
||||
<div className = 'displayNameContainer'>
|
||||
<DisplayName
|
||||
elementID = 'sharedVideoContainer_name'
|
||||
|
|
|
@ -6,6 +6,7 @@ import { Watermarks } from '../../base/react';
|
|||
import { connect } from '../../base/redux';
|
||||
import { setColorAlpha } from '../../base/util';
|
||||
import { fetchCustomBrandingData } from '../../dynamic-branding';
|
||||
import { SharedVideo } from '../../shared-video/components/web';
|
||||
import { Captions } from '../../subtitles/';
|
||||
|
||||
declare var interfaceConfig: Object;
|
||||
|
@ -67,17 +68,19 @@ class LargeVideo extends Component<Props> {
|
|||
* @returns {React$Element}
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
_isChatOpen,
|
||||
_noAutoPlayVideo
|
||||
} = this.props;
|
||||
const style = this._getCustomSyles();
|
||||
const className = `videocontainer${this.props._isChatOpen ? ' shift-right' : ''}`;
|
||||
const className = `videocontainer${_isChatOpen ? ' shift-right' : ''}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
className = { className }
|
||||
id = 'largeVideoContainer'
|
||||
style = { style }>
|
||||
<div id = 'sharedVideo'>
|
||||
<div id = 'sharedVideoIFrame' />
|
||||
</div>
|
||||
<SharedVideo />
|
||||
<div id = 'etherpad' />
|
||||
|
||||
<Watermarks />
|
||||
|
@ -101,7 +104,7 @@ class LargeVideo extends Component<Props> {
|
|||
*/}
|
||||
<div id = 'largeVideoWrapper'>
|
||||
<video
|
||||
autoPlay = { !this.props._noAutoPlayVideo }
|
||||
autoPlay = { !_noAutoPlayVideo }
|
||||
id = 'largeVideo'
|
||||
muted = { true }
|
||||
playsInline = { true } /* for Safari on iOS to work */ />
|
||||
|
|
|
@ -12,15 +12,14 @@
|
|||
export const SET_SHARED_VIDEO_STATUS = 'SET_SHARED_VIDEO_STATUS';
|
||||
|
||||
/**
|
||||
* The type of the action which signals to start the flow for starting or
|
||||
* stopping a shared video.
|
||||
* The type of the action which signals to reset the current known state of the
|
||||
* shared video.
|
||||
*
|
||||
* {
|
||||
* type: TOGGLE_SHARED_VIDEO
|
||||
* type: RESET_SHARED_VIDEO_STATUS,
|
||||
* }
|
||||
*/
|
||||
export const TOGGLE_SHARED_VIDEO = 'TOGGLE_SHARED_VIDEO';
|
||||
|
||||
export const RESET_SHARED_VIDEO_STATUS = 'RESET_SHARED_VIDEO_STATUS';
|
||||
|
||||
/**
|
||||
* The type of the action which signals to disable or enable the shared video
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -1,26 +1,8 @@
|
|||
// @flow
|
||||
|
||||
import { openDialog } from '../base/dialog/actions';
|
||||
import { SharedVideoDialog } from '../shared-video/components';
|
||||
|
||||
import { SET_SHARED_VIDEO_STATUS, TOGGLE_SHARED_VIDEO, SET_DISABLE_BUTTON } from './actionTypes';
|
||||
|
||||
/**
|
||||
* Updates the current known status of the shared video.
|
||||
*
|
||||
* @param {string} status - The current status of the video being shared.
|
||||
* @returns {{
|
||||
* type: SET_SHARED_VIDEO_STATUS,
|
||||
* status: string
|
||||
* }}
|
||||
*/
|
||||
export function setSharedVideoStatus(status: string) {
|
||||
return {
|
||||
type: SET_SHARED_VIDEO_STATUS,
|
||||
status
|
||||
};
|
||||
}
|
||||
import { SET_DISABLE_BUTTON } from './actionTypes';
|
||||
|
||||
export * from './actions.any';
|
||||
|
||||
/**
|
||||
* Disabled share video button.
|
||||
|
@ -37,26 +19,3 @@ export function setDisableButton(disabled: boolean) {
|
|||
disabled
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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: ?Function) {
|
||||
return openDialog(SharedVideoDialog, { onPostSubmit });
|
||||
}
|
||||
|
|
|
@ -3,9 +3,6 @@
|
|||
import { Component } from 'react';
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
import { getYoutubeLink } from '../functions';
|
||||
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of
|
||||
* {@link AbstractSharedVideoDialog}.
|
||||
|
@ -20,7 +17,7 @@ export type Props = {
|
|||
/**
|
||||
* Function to be invoked after typing a valid video.
|
||||
*/
|
||||
onPostSubmit: ?Function,
|
||||
onPostSubmit: Function,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
|
@ -60,18 +57,10 @@ export default class AbstractSharedVideoDialog<S: *> extends Component < Props,
|
|||
return false;
|
||||
}
|
||||
|
||||
const videoId = getYoutubeLink(link);
|
||||
const { onPostSubmit } = this.props;
|
||||
|
||||
if (videoId) {
|
||||
const { onPostSubmit } = this.props;
|
||||
onPostSubmit(link);
|
||||
|
||||
onPostSubmit && onPostSubmit(videoId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -292,32 +292,42 @@ class YoutubeLargeVideo extends Component<Props, *> {
|
|||
/**
|
||||
* Dispatches the video status, time and ownerId if the status is playing or paused.
|
||||
*
|
||||
* @param {string} videoId - The youtube id of the video.
|
||||
* @param {string} videoUrl - The youtube id of the video.
|
||||
* @param {string} status - The status of the player.
|
||||
* @param {number} time - The seek time.
|
||||
* @param {string} ownerId - The id of the participant sharing the video.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
onVideoChangeEvent(videoId, status, time, ownerId) {
|
||||
onVideoChangeEvent(videoUrl, status, time, ownerId) {
|
||||
if (![ 'playing', 'paused' ].includes(status)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.dispatch(setSharedVideoStatus(videoId, translateStatus(status), time, ownerId));
|
||||
this.props.dispatch(setSharedVideoStatus({
|
||||
videoUrl,
|
||||
status: translateStatus(status),
|
||||
time,
|
||||
ownerId
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches the 'playing' as video status, time and ownerId.
|
||||
*
|
||||
* @param {string} videoId - The youtube id of the video.
|
||||
* @param {string} videoUrl - The youtube id of the video.
|
||||
* @param {number} time - The seek time.
|
||||
* @param {string} ownerId - The id of the participant sharing the video.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
onVideoReady(videoId, time, ownerId) {
|
||||
time.then(t => this.props.dispatch(setSharedVideoStatus(videoId, 'playing', t, ownerId)));
|
||||
onVideoReady(videoUrl, time, ownerId) {
|
||||
time.then(t => this.props.dispatch(setSharedVideoStatus({
|
||||
videoUrl,
|
||||
status: 'playing',
|
||||
time: t,
|
||||
ownerId
|
||||
})));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
|
@ -9,11 +9,9 @@ import {
|
|||
AbstractButton,
|
||||
type AbstractButtonProps
|
||||
} from '../../../base/toolbox/components';
|
||||
import { showSharedVideoDialog } from '../../actions.web';
|
||||
import { toggleSharedVideo } from '../../actions.any';
|
||||
import { isSharingStatus } from '../../functions';
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
type Props = AbstractButtonProps & {
|
||||
|
||||
/**
|
||||
|
@ -49,7 +47,7 @@ class SharedVideoButton extends AbstractButton<Props, *> {
|
|||
* @returns {void}
|
||||
*/
|
||||
_handleClick() {
|
||||
this._doToggleSharedVideoDialog();
|
||||
this._doToggleSharedVideo();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -80,12 +78,8 @@ class SharedVideoButton extends AbstractButton<Props, *> {
|
|||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_doToggleSharedVideoDialog() {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
return this._isToggled()
|
||||
? APP.UI.stopSharedVideoEmitter()
|
||||
: dispatch(showSharedVideoDialog(id => APP.UI.startSharedVideoEmitter(id)));
|
||||
_doToggleSharedVideo() {
|
||||
this.props.dispatch(toggleSharedVideo());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@ import { translate } from '../../../base/i18n';
|
|||
import { getFieldValue } from '../../../base/react';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { defaultSharedVideoLink } from '../../constants';
|
||||
import { getYoutubeLink } from '../../functions';
|
||||
import AbstractSharedVideoDialog from '../AbstractSharedVideoDialog';
|
||||
|
||||
/**
|
||||
|
@ -48,7 +47,7 @@ class SharedVideoDialog extends AbstractSharedVideoDialog<*> {
|
|||
|
||||
this.setState({
|
||||
value: linkValue,
|
||||
okDisabled: !getYoutubeLink(linkValue)
|
||||
okDisabled: !linkValue
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
|
@ -1,5 +1,5 @@
|
|||
// @flow
|
||||
|
||||
export { default as SharedVideo } from './SharedVideo';
|
||||
export { default as SharedVideoButton } from './SharedVideoButton';
|
||||
export { default as SharedVideoDialog } from './SharedVideoDialog';
|
||||
|
||||
|
|
|
@ -4,13 +4,20 @@
|
|||
* Example shared video link.
|
||||
* @type {string}
|
||||
*/
|
||||
export const defaultSharedVideoLink = 'https://youtu.be/TB7LlM4erx8';
|
||||
export const defaultSharedVideoLink = 'Youtube link or direct video link';
|
||||
|
||||
/**
|
||||
* Fixed name of the video player fake participant.
|
||||
* @type {string}
|
||||
*/
|
||||
export const VIDEO_PLAYER_PARTICIPANT_NAME = 'YouTube';
|
||||
export const VIDEO_PLAYER_PARTICIPANT_NAME = 'Video';
|
||||
|
||||
/**
|
||||
* Fixed name of the youtube player fake participant.
|
||||
* @type {string}
|
||||
*/
|
||||
export const YOUTUBE_PLAYER_PARTICIPANT_NAME = 'YouTube';
|
||||
|
||||
|
||||
/**
|
||||
* Shared video command.
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { getParticipants } from '../base/participants';
|
||||
|
||||
import { VIDEO_PLAYER_PARTICIPANT_NAME } from './constants';
|
||||
import { VIDEO_PLAYER_PARTICIPANT_NAME, YOUTUBE_PLAYER_PARTICIPANT_NAME } from './constants';
|
||||
|
||||
/**
|
||||
* Validates the entered video url.
|
||||
|
@ -10,16 +10,19 @@ import { VIDEO_PLAYER_PARTICIPANT_NAME } from './constants';
|
|||
* It returns a boolean to reflect whether the url matches the youtube regex.
|
||||
*
|
||||
* @param {string} url - The entered video link.
|
||||
* @returns {boolean}
|
||||
* @returns {string} The youtube video id if matched.
|
||||
*/
|
||||
export function getYoutubeLink(url: string) {
|
||||
export function getYoutubeId(url: string) {
|
||||
if (!url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const p = /^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|(?:m\.)?youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/;// eslint-disable-line max-len
|
||||
const result = url.match(p);
|
||||
|
||||
return result ? result[1] : false;
|
||||
return result ? result[1] : null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if the status is one that is actually sharing the video - playing, pause or start.
|
||||
*
|
||||
|
@ -39,6 +42,6 @@ export function isSharingStatus(status: string) {
|
|||
*/
|
||||
export function isVideoPlaying(stateful: Object | Function): boolean {
|
||||
return Boolean(getParticipants(stateful).find(p => p.isFakeParticipant
|
||||
&& p.name === VIDEO_PLAYER_PARTICIPANT_NAME)
|
||||
&& (p.name === VIDEO_PLAYER_PARTICIPANT_NAME || p.name === YOUTUBE_PLAYER_PARTICIPANT_NAME))
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -1,37 +1,13 @@
|
|||
// @flow
|
||||
|
||||
import UIEvents from '../../../service/UI/UIEvents';
|
||||
import { getCurrentConference } from '../base/conference';
|
||||
import { getLocalParticipant } from '../base/participants';
|
||||
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
|
||||
import { StateListenerRegistry } from '../base/redux';
|
||||
|
||||
import { TOGGLE_SHARED_VIDEO } from './actionTypes';
|
||||
import { setDisableButton } from './actions.web';
|
||||
import { SHARED_VIDEO } from './constants';
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
/**
|
||||
* Middleware that captures actions related to video sharing and updates
|
||||
* components not hooked into redux.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
if (typeof APP === 'undefined') {
|
||||
return next(action);
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
case TOGGLE_SHARED_VIDEO:
|
||||
APP.UI.emitEvent(UIEvents.SHARED_VIDEO_CLICKED);
|
||||
break;
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
import './middleware.any';
|
||||
|
||||
/**
|
||||
* Set up state change listener to disable or enable the share video button in
|
||||
|
@ -43,7 +19,6 @@ StateListenerRegistry.register(
|
|||
if (conference && conference !== previousConference) {
|
||||
conference.addCommandListener(SHARED_VIDEO,
|
||||
({ attributes }) => {
|
||||
|
||||
const { dispatch, getState } = store;
|
||||
const { from } = attributes;
|
||||
const localParticipantId = getLocalParticipant(getState()).id;
|
||||
|
|
|
@ -2,22 +2,28 @@
|
|||
|
||||
import { ReducerRegistry } from '../base/redux';
|
||||
|
||||
import { SET_SHARED_VIDEO_STATUS } from './actionTypes';
|
||||
import { RESET_SHARED_VIDEO_STATUS, SET_SHARED_VIDEO_STATUS } from './actionTypes';
|
||||
|
||||
const initialState = {};
|
||||
|
||||
/**
|
||||
* Reduces the Redux actions of the feature features/shared-video.
|
||||
*/
|
||||
ReducerRegistry.register('features/shared-video', (state = {}, action) => {
|
||||
const { videoId, status, time, ownerId } = action;
|
||||
ReducerRegistry.register('features/shared-video', (state = initialState, action) => {
|
||||
const { videoUrl, status, time, ownerId, muted, volume } = action;
|
||||
|
||||
switch (action.type) {
|
||||
case RESET_SHARED_VIDEO_STATUS:
|
||||
return initialState;
|
||||
case SET_SHARED_VIDEO_STATUS:
|
||||
return {
|
||||
...state,
|
||||
videoId,
|
||||
muted,
|
||||
ownerId,
|
||||
status,
|
||||
time,
|
||||
ownerId
|
||||
videoUrl,
|
||||
volume
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
|
|
|
@ -2,19 +2,27 @@
|
|||
|
||||
import { ReducerRegistry } from '../base/redux';
|
||||
|
||||
import { SET_SHARED_VIDEO_STATUS, SET_DISABLE_BUTTON } from './actionTypes';
|
||||
import { RESET_SHARED_VIDEO_STATUS, SET_SHARED_VIDEO_STATUS, SET_DISABLE_BUTTON } from './actionTypes';
|
||||
|
||||
const initialState = {};
|
||||
|
||||
/**
|
||||
* Reduces the Redux actions of the feature features/shared-video.
|
||||
*/
|
||||
ReducerRegistry.register('features/shared-video', (state = {}, action) => {
|
||||
const { status, disabled } = action;
|
||||
ReducerRegistry.register('features/shared-video', (state = initialState, action) => {
|
||||
const { videoUrl, status, time, ownerId, disabled, muted } = action;
|
||||
|
||||
switch (action.type) {
|
||||
case RESET_SHARED_VIDEO_STATUS:
|
||||
return initialState;
|
||||
case SET_SHARED_VIDEO_STATUS:
|
||||
return {
|
||||
...state,
|
||||
status
|
||||
muted,
|
||||
ownerId,
|
||||
status,
|
||||
time,
|
||||
videoUrl
|
||||
};
|
||||
|
||||
case SET_DISABLE_BUTTON:
|
||||
|
|
|
@ -45,10 +45,9 @@ export function muteLocal(enable: boolean, mediaType: MEDIA_TYPE) {
|
|||
dispatch(isAudio ? setAudioMuted(enable, /* ensureTrack */ true)
|
||||
: setVideoMuted(enable, mediaType, VIDEO_MUTISM_AUTHORITY.USER, /* ensureTrack */ true));
|
||||
|
||||
// FIXME: The old conference logic as well as the shared video feature
|
||||
// still rely on this event being emitted.
|
||||
// FIXME: The old conference logic still relies on this event being emitted.
|
||||
typeof APP === 'undefined'
|
||||
|| APP.UI.emitEvent(isAudio ? UIEvents.AUDIO_MUTED : UIEvents.VIDEO_MUTED, enable, true);
|
||||
|| APP.UI.emitEvent(isAudio ? UIEvents.AUDIO_MUTED : UIEvents.VIDEO_MUTED, enable);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -12,14 +12,12 @@ export default {
|
|||
AUDIO_MUTED: 'UI.audio_muted',
|
||||
VIDEO_MUTED: 'UI.video_muted',
|
||||
ETHERPAD_CLICKED: 'UI.etherpad_clicked',
|
||||
SHARED_VIDEO_CLICKED: 'UI.start_shared_video',
|
||||
|
||||
/**
|
||||
* Updates shared video with params: url, state, time(optional)
|
||||
* Where url is the video link, state is stop/start/pause and time is the
|
||||
* current video playing time.
|
||||
*/
|
||||
UPDATE_SHARED_VIDEO: 'UI.update_shared_video',
|
||||
TOGGLE_FULLSCREEN: 'UI.toogle_fullscreen',
|
||||
FULLSCREEN_TOGGLED: 'UI.fullscreen_toggled',
|
||||
AUTH_CLICKED: 'UI.auth_clicked',
|
||||
|
|
Loading…
Reference in New Issue