2023-11-06 23:02:12 +08:00
|
|
|
import React, { useEffect, useMemo, useRef } from 'react';
|
|
|
|
import ReactPlayer from 'react-player';
|
|
|
|
import { defineMessages, useIntl } from 'react-intl';
|
|
|
|
import audioManager from '/imports/ui/services/audio-manager';
|
2023-11-15 21:38:50 +08:00
|
|
|
import { Session } from 'meteor/session';
|
2024-01-19 22:46:11 +08:00
|
|
|
import { useReactiveVar, useMutation } from '@apollo/client';
|
2023-11-06 23:02:12 +08:00
|
|
|
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
|
|
|
|
import { OnProgressProps } from 'react-player/base';
|
|
|
|
|
|
|
|
import useMeeting from '/imports/ui/core/hooks/useMeeting';
|
|
|
|
import {
|
|
|
|
layoutDispatch,
|
|
|
|
layoutSelect,
|
|
|
|
layoutSelectInput,
|
|
|
|
layoutSelectOutput,
|
|
|
|
} from '../../layout/context';
|
|
|
|
import Styled from './styles';
|
|
|
|
import {
|
|
|
|
ExternalVideo,
|
|
|
|
Input,
|
|
|
|
Layout,
|
|
|
|
Output,
|
|
|
|
} from '../../layout/layoutTypes';
|
|
|
|
import { uniqueId } from '/imports/utils/string-utils';
|
|
|
|
import useTimeSync from '/imports/ui/core/local-states/useTimeSync';
|
|
|
|
import ExternalVideoPlayerToolbar from './toolbar/component';
|
|
|
|
import deviceInfo from '/imports/utils/deviceInfo';
|
|
|
|
import { ACTIONS } from '../../layout/enums';
|
2024-01-19 22:46:11 +08:00
|
|
|
import { EXTERNAL_VIDEO_UPDATE } from '../mutations';
|
2023-11-06 23:02:12 +08:00
|
|
|
|
|
|
|
import PeerTube from '../custom-players/peertube';
|
|
|
|
import { ArcPlayer } from '../custom-players/arc-player';
|
|
|
|
|
|
|
|
const AUTO_PLAY_BLOCK_DETECTION_TIMEOUT_SECONDS = 5;
|
|
|
|
|
|
|
|
const intlMessages = defineMessages({
|
|
|
|
autoPlayWarning: {
|
|
|
|
id: 'app.externalVideo.autoPlayWarning',
|
|
|
|
description: 'Shown when user needs to interact with player to make it work',
|
|
|
|
},
|
|
|
|
refreshLabel: {
|
|
|
|
id: 'app.externalVideo.refreshLabel',
|
|
|
|
},
|
|
|
|
fullscreenLabel: {
|
|
|
|
id: 'app.externalVideo.fullscreenLabel',
|
|
|
|
},
|
|
|
|
subtitlesOn: {
|
|
|
|
id: 'app.externalVideo.subtitlesOn',
|
|
|
|
},
|
|
|
|
subtitlesOff: {
|
|
|
|
id: 'app.externalVideo.subtitlesOff',
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
interface ExternalVideoPlayerProps {
|
|
|
|
isEchoTest: boolean;
|
|
|
|
isPresenter: boolean;
|
|
|
|
videoUrl: string;
|
|
|
|
isResizing: boolean;
|
|
|
|
fullscreenContext: boolean;
|
|
|
|
externalVideo: ExternalVideo;
|
|
|
|
playing: boolean;
|
|
|
|
playerPlaybackRate: number;
|
|
|
|
currentTime: number;
|
|
|
|
layoutContextDispatch: ReturnType<typeof layoutDispatch>;
|
|
|
|
key: string;
|
|
|
|
setKey: (key: string) => void;
|
|
|
|
}
|
|
|
|
|
|
|
|
// @ts-ignore - PeerTubePlayer is not typed
|
|
|
|
Styled.VideoPlayer.addCustomPlayer(PeerTube);
|
|
|
|
// @ts-ignore - ArcPlayer is not typed
|
|
|
|
Styled.VideoPlayer.addCustomPlayer(ArcPlayer);
|
|
|
|
|
|
|
|
const ExternalVideoPlayer: React.FC<ExternalVideoPlayerProps> = ({
|
|
|
|
isResizing,
|
|
|
|
externalVideo,
|
|
|
|
fullscreenContext,
|
|
|
|
videoUrl,
|
|
|
|
isPresenter,
|
|
|
|
playing,
|
|
|
|
playerPlaybackRate,
|
|
|
|
currentTime,
|
|
|
|
isEchoTest,
|
|
|
|
layoutContextDispatch,
|
|
|
|
key,
|
|
|
|
setKey,
|
|
|
|
}) => {
|
|
|
|
const intl = useIntl();
|
|
|
|
|
|
|
|
const {
|
|
|
|
height,
|
|
|
|
width,
|
|
|
|
top,
|
|
|
|
left,
|
|
|
|
right,
|
|
|
|
} = externalVideo;
|
|
|
|
|
|
|
|
const hideVolume = useMemo(() => ({
|
|
|
|
Vimeo: true,
|
|
|
|
Facebook: true,
|
|
|
|
ArcPlayer: true,
|
|
|
|
// YouTube: true,
|
|
|
|
}), []);
|
|
|
|
|
|
|
|
const videoPlayConfig = useMemo(() => {
|
|
|
|
return {
|
|
|
|
// default option for all players, can be overwritten
|
|
|
|
playerOptions: {
|
|
|
|
autoplay: true,
|
|
|
|
playsinline: true,
|
|
|
|
controls: isPresenter,
|
|
|
|
},
|
|
|
|
file: {
|
|
|
|
attributes: {
|
|
|
|
controls: isPresenter ? 'controls' : '',
|
|
|
|
autoplay: 'autoplay',
|
|
|
|
playsinline: 'playsinline',
|
|
|
|
},
|
|
|
|
},
|
|
|
|
facebook: {
|
|
|
|
controls: isPresenter,
|
|
|
|
},
|
|
|
|
dailymotion: {
|
|
|
|
params: {
|
|
|
|
controls: isPresenter,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
youtube: {
|
|
|
|
playerVars: {
|
|
|
|
autoplay: 1,
|
|
|
|
modestbranding: 1,
|
|
|
|
autohide: 1,
|
|
|
|
rel: 0,
|
|
|
|
ecver: 2,
|
|
|
|
controls: isPresenter ? 1 : 0,
|
|
|
|
cc_lang_pref: document.getElementsByTagName('html')[0].lang.substring(0, 2),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
peertube: {
|
|
|
|
isPresenter,
|
|
|
|
},
|
|
|
|
twitch: {
|
|
|
|
options: {
|
|
|
|
controls: isPresenter,
|
|
|
|
},
|
|
|
|
playerId: 'externalVideoPlayerTwitch',
|
|
|
|
},
|
|
|
|
preload: true,
|
|
|
|
showHoverToolBar: false,
|
|
|
|
};
|
|
|
|
}, [isPresenter]);
|
|
|
|
|
|
|
|
const [autoPlayBlocked, setAutoPlayBlocked] = React.useState(false);
|
|
|
|
const [showHoverToolBar, setShowHoverToolBar] = React.useState(false);
|
|
|
|
const [mute, setMute] = React.useState(false);
|
|
|
|
const [volume, setVolume] = React.useState(1);
|
|
|
|
const [subtitlesOn, setSubtitlesOn] = React.useState(false);
|
|
|
|
const [played, setPlayed] = React.useState(0);
|
|
|
|
const [loaded, setLoaded] = React.useState(0);
|
|
|
|
const playerRef = useRef<ReactPlayer>();
|
|
|
|
const playerParentRef = useRef<HTMLDivElement| null>(null);
|
|
|
|
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
2023-12-13 04:47:29 +08:00
|
|
|
const presenterRef = useRef(isPresenter);
|
2024-01-19 22:46:11 +08:00
|
|
|
|
|
|
|
const [updateExternalVideo] = useMutation(EXTERNAL_VIDEO_UPDATE);
|
|
|
|
|
|
|
|
let lastMessage: {
|
|
|
|
event: string;
|
|
|
|
rate: number;
|
|
|
|
time: number;
|
|
|
|
state?: string;
|
|
|
|
} = { event: '', rate: 0, time: 0 };
|
|
|
|
|
|
|
|
const sendMessage = (event: string, data: { rate: number; time: number; state?: string}) => {
|
|
|
|
// don't re-send repeated update messages
|
|
|
|
if (
|
|
|
|
lastMessage.event === event
|
|
|
|
&& lastMessage.time === data.time
|
|
|
|
) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// don't register to redis a viewer joined message
|
|
|
|
if (event === 'viewerJoined') {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
lastMessage = { ...data, event };
|
|
|
|
|
|
|
|
// Use an integer for playing state
|
|
|
|
// 0: stopped 1: playing
|
|
|
|
// We might use more states in the future
|
|
|
|
const state = data.state ? 1 : 0;
|
|
|
|
|
|
|
|
updateExternalVideo({
|
|
|
|
variables: {
|
|
|
|
status: event,
|
|
|
|
rate: data?.rate,
|
|
|
|
time: data?.time,
|
|
|
|
state,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2023-11-06 23:02:12 +08:00
|
|
|
useEffect(() => {
|
|
|
|
timeoutRef.current = setTimeout(() => {
|
|
|
|
setAutoPlayBlocked(true);
|
|
|
|
}, AUTO_PLAY_BLOCK_DETECTION_TIMEOUT_SECONDS * 1000);
|
|
|
|
|
|
|
|
layoutContextDispatch({
|
|
|
|
type: ACTIONS.SET_HAS_EXTERNAL_VIDEO,
|
|
|
|
value: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
layoutContextDispatch({
|
|
|
|
type: ACTIONS.SET_PRESENTATION_IS_OPEN,
|
|
|
|
value: true,
|
|
|
|
});
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (playerRef.current && !isPresenter) {
|
|
|
|
playerRef.current.seekTo(currentTime, 'seconds');
|
|
|
|
}
|
|
|
|
}, [currentTime]);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (playerRef.current) {
|
|
|
|
playerRef.current.seekTo(currentTime, 'seconds');
|
|
|
|
}
|
|
|
|
}, [playerRef.current]);
|
|
|
|
|
2023-12-13 04:47:29 +08:00
|
|
|
useEffect(() => {
|
|
|
|
if (isPresenter !== presenterRef.current) {
|
|
|
|
setKey(uniqueId('react-player'));
|
|
|
|
presenterRef.current = isPresenter;
|
|
|
|
}
|
|
|
|
}, [isPresenter]);
|
|
|
|
|
2023-11-06 23:02:12 +08:00
|
|
|
const handleOnPlay = () => {
|
|
|
|
setAutoPlayBlocked(false);
|
2023-11-08 04:55:17 +08:00
|
|
|
clearTimeout(timeoutRef.current);
|
2023-11-06 23:02:12 +08:00
|
|
|
if (isPresenter) {
|
|
|
|
if (timeoutRef.current) {
|
|
|
|
clearTimeout(timeoutRef.current);
|
|
|
|
}
|
|
|
|
const rate = playerRef.current?.getInternalPlayer()?.getPlaybackRate() as number ?? 1;
|
|
|
|
const currentTime = playerRef.current?.getCurrentTime() ?? 0;
|
|
|
|
sendMessage('play', {
|
|
|
|
rate,
|
|
|
|
time: currentTime,
|
|
|
|
});
|
|
|
|
} else if (!playing && !isPresenter) {
|
|
|
|
playerRef.current?.getInternalPlayer().pauseVideo();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleOnPause = () => {
|
|
|
|
if (isPresenter) {
|
|
|
|
const rate = playerRef.current?.getInternalPlayer()?.getPlaybackRate() as number ?? 1;
|
|
|
|
const currentTime = playerRef.current?.getCurrentTime() ?? 0;
|
|
|
|
sendMessage('stop', {
|
|
|
|
rate,
|
|
|
|
time: currentTime,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!isPresenter && playing) {
|
|
|
|
playerRef.current?.getInternalPlayer().playVideo();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const isMinimized = width === 0 && height === 0;
|
|
|
|
|
|
|
|
// @ts-ignore accessing lib private property
|
|
|
|
const playerName = playerRef.current && playerRef.current.player
|
|
|
|
// @ts-ignore accessing lib private property
|
|
|
|
&& playerRef.current.player.player && playerRef.current.player.player.constructor.name as string;
|
|
|
|
let toolbarStyle = 'hoverToolbar';
|
|
|
|
|
|
|
|
if (deviceInfo.isMobile && !showHoverToolBar) {
|
|
|
|
toolbarStyle = 'dontShowMobileHoverToolbar';
|
|
|
|
}
|
|
|
|
|
|
|
|
if (deviceInfo.isMobile && showHoverToolBar) {
|
|
|
|
toolbarStyle = 'showMobileHoverToolbar';
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<Styled.Container
|
|
|
|
style={{
|
|
|
|
height,
|
|
|
|
width,
|
|
|
|
top,
|
|
|
|
left,
|
|
|
|
right,
|
|
|
|
}}
|
|
|
|
isResizing={isResizing}
|
|
|
|
isMinimized={isMinimized}
|
|
|
|
>
|
|
|
|
<Styled.VideoPlayerWrapper
|
|
|
|
fullscreen={fullscreenContext}
|
|
|
|
ref={playerParentRef}
|
|
|
|
data-test="videoPlayer"
|
|
|
|
>
|
|
|
|
|
|
|
|
{
|
|
|
|
autoPlayBlocked
|
|
|
|
? (
|
|
|
|
<Styled.AutoPlayWarning>
|
|
|
|
{intl.formatMessage(intlMessages.autoPlayWarning)}
|
|
|
|
</Styled.AutoPlayWarning>
|
|
|
|
)
|
|
|
|
: ''
|
|
|
|
}
|
|
|
|
<Styled.VideoPlayer
|
|
|
|
config={videoPlayConfig}
|
|
|
|
autoPlay
|
|
|
|
playsInline
|
|
|
|
url={videoUrl}
|
|
|
|
playing={playing}
|
|
|
|
playbackRate={playerPlaybackRate}
|
|
|
|
key={key}
|
|
|
|
height="100%"
|
|
|
|
width="100%"
|
|
|
|
ref={playerRef}
|
|
|
|
volume={volume}
|
|
|
|
onPlay={handleOnPlay}
|
|
|
|
onProgress={(state: OnProgressProps) => {
|
|
|
|
setPlayed(state.played);
|
|
|
|
setLoaded(state.loaded);
|
|
|
|
}}
|
|
|
|
onPause={handleOnPause}
|
2023-12-13 04:47:29 +08:00
|
|
|
muted={mute}
|
2023-11-06 23:02:12 +08:00
|
|
|
/>
|
|
|
|
{
|
|
|
|
!isPresenter ? (
|
|
|
|
<ExternalVideoPlayerToolbar
|
|
|
|
handleOnMuted={(m: boolean) => { setMute(m); }}
|
|
|
|
handleReload={() => setKey(uniqueId('react-player'))}
|
|
|
|
setShowHoverToolBar={setShowHoverToolBar}
|
|
|
|
toolbarStyle={toolbarStyle}
|
|
|
|
handleVolumeChanged={setVolume}
|
|
|
|
volume={volume}
|
|
|
|
muted={mute || isEchoTest}
|
|
|
|
mutedByEchoTest={isEchoTest}
|
|
|
|
playing={playing}
|
|
|
|
playerName={playerName}
|
|
|
|
toggleSubtitle={() => setSubtitlesOn(!subtitlesOn)}
|
|
|
|
playerParent={playerParentRef.current}
|
|
|
|
played={played}
|
|
|
|
loaded={loaded}
|
|
|
|
subtitlesOn={subtitlesOn}
|
|
|
|
hideVolume={hideVolume[playerName as keyof typeof hideVolume]}
|
|
|
|
/>
|
|
|
|
) : null
|
|
|
|
}
|
|
|
|
</Styled.VideoPlayerWrapper>
|
|
|
|
</Styled.Container>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
const ExternalVideoPlayerContainer: React.FC = () => {
|
|
|
|
/* eslint no-underscore-dangle: "off" */
|
|
|
|
// @ts-ignore - temporary while hybrid (meteor+GraphQl)
|
|
|
|
const isEchoTest = useReactiveVar(audioManager._isEchoTest.value) as boolean;
|
2023-11-15 21:38:50 +08:00
|
|
|
const theresNoExternalVideo = useRef(true);
|
2023-11-14 02:10:41 +08:00
|
|
|
const { data: currentUser } = useCurrentUser((user) => ({
|
2023-11-06 23:02:12 +08:00
|
|
|
presenter: user.presenter,
|
|
|
|
}));
|
|
|
|
|
2023-11-14 02:10:41 +08:00
|
|
|
const { data: currentMeeting } = useMeeting((m) => ({
|
2023-11-06 23:02:12 +08:00
|
|
|
externalVideo: m.externalVideo,
|
|
|
|
}));
|
|
|
|
|
2023-11-15 21:38:50 +08:00
|
|
|
useEffect(() => {
|
|
|
|
if (!currentMeeting?.externalVideo?.externalVideoUrl && !theresNoExternalVideo.current) {
|
|
|
|
layoutContextDispatch({
|
|
|
|
type: ACTIONS.SET_HAS_EXTERNAL_VIDEO,
|
|
|
|
value: false,
|
|
|
|
});
|
|
|
|
layoutContextDispatch({
|
|
|
|
type: ACTIONS.SET_PRESENTATION_IS_OPEN,
|
|
|
|
value: Session.get('presentationLastState'),
|
|
|
|
});
|
|
|
|
} else if (currentMeeting?.externalVideo?.externalVideoUrl) {
|
|
|
|
layoutContextDispatch({
|
|
|
|
type: ACTIONS.SET_HAS_EXTERNAL_VIDEO,
|
|
|
|
value: true,
|
|
|
|
});
|
|
|
|
layoutContextDispatch({
|
|
|
|
type: ACTIONS.SET_PRESENTATION_IS_OPEN,
|
|
|
|
value: true,
|
|
|
|
});
|
|
|
|
theresNoExternalVideo.current = false;
|
|
|
|
}
|
|
|
|
}, [currentMeeting]);
|
|
|
|
|
2023-11-06 23:02:12 +08:00
|
|
|
const [timeSync] = useTimeSync();
|
|
|
|
|
|
|
|
const fullscreenElementId = 'ExternalVideo';
|
|
|
|
const externalVideo: ExternalVideo = layoutSelectOutput((i: Output) => i.externalVideo);
|
2023-11-15 21:38:50 +08:00
|
|
|
const hasExternalVideoOnLayout: boolean = layoutSelectInput((i: Input) => i.externalVideo.hasExternalVideo);
|
2023-11-06 23:02:12 +08:00
|
|
|
const cameraDock = layoutSelectInput((i: Input) => i.cameraDock);
|
|
|
|
const { isResizing } = cameraDock;
|
|
|
|
const layoutContextDispatch = layoutDispatch();
|
|
|
|
const fullscreen = layoutSelect((i: Layout) => i.fullscreen);
|
|
|
|
const { element } = fullscreen;
|
|
|
|
const fullscreenContext = (element === fullscreenElementId);
|
|
|
|
const [key, setKey] = React.useState(uniqueId('react-player'));
|
|
|
|
if (!currentUser || !currentMeeting) return null;
|
2023-11-15 21:38:50 +08:00
|
|
|
if (!hasExternalVideoOnLayout) return null;
|
2023-11-06 23:02:12 +08:00
|
|
|
const playerCurrentTime = currentMeeting.externalVideo?.playerCurrentTime ?? 0;
|
|
|
|
const playerPlaybackRate = currentMeeting.externalVideo?.playerPlaybackRate ?? 1;
|
|
|
|
const playerUpdatedAt = currentMeeting.externalVideo?.updatedAt ?? Date.now();
|
|
|
|
const playerUpdatedAtDate = new Date(playerUpdatedAt);
|
|
|
|
const currentDate = new Date(Date.now() + timeSync);
|
|
|
|
const currentTime = (((currentDate.getTime() - playerUpdatedAtDate.getTime()) / 1000)
|
|
|
|
+ playerCurrentTime) * playerPlaybackRate;
|
|
|
|
|
|
|
|
return (
|
|
|
|
<ExternalVideoPlayer
|
|
|
|
isEchoTest={isEchoTest}
|
|
|
|
isPresenter={currentUser.presenter ?? false}
|
|
|
|
videoUrl={currentMeeting.externalVideo?.externalVideoUrl ?? ''}
|
|
|
|
playing={currentMeeting.externalVideo?.playerPlaying ?? false}
|
|
|
|
playerPlaybackRate={currentMeeting.externalVideo?.playerPlaybackRate ?? 1}
|
|
|
|
isResizing={isResizing}
|
|
|
|
layoutContextDispatch={layoutContextDispatch}
|
|
|
|
fullscreenContext={fullscreenContext}
|
|
|
|
externalVideo={externalVideo}
|
|
|
|
currentTime={currentTime}
|
|
|
|
key={key}
|
|
|
|
setKey={setKey}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
export default ExternalVideoPlayerContainer;
|