Add player poster
This commit is contained in:
@@ -1,15 +1,26 @@
|
||||
import React from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import VideoJS from './player';
|
||||
import ViewerPing from './viewer-ping';
|
||||
import VideoPoster from './VideoPoster';
|
||||
import { getLocalStorage, setLocalStorage } from '../../utils/helpers';
|
||||
import { videoStateAtom } from '../stores/ClientConfigStore';
|
||||
import { VideoState } from '../../interfaces/application-state';
|
||||
|
||||
const PLAYER_VOLUME = 'owncast_volume';
|
||||
|
||||
const ping = new ViewerPing();
|
||||
|
||||
export default function OwncastPlayer(props) {
|
||||
interface Props {
|
||||
source: string;
|
||||
online: boolean;
|
||||
}
|
||||
|
||||
export default function OwncastPlayer(props: Props) {
|
||||
const playerRef = React.useRef(null);
|
||||
const { source } = props;
|
||||
const { source, online } = props;
|
||||
|
||||
const setVideoState = useSetRecoilState<VideoState>(videoStateAtom);
|
||||
|
||||
const setSavedVolume = () => {
|
||||
try {
|
||||
@@ -51,7 +62,7 @@ export default function OwncastPlayer(props) {
|
||||
},
|
||||
sources: [
|
||||
{
|
||||
src: `${source}/hls/stream.m3u8`,
|
||||
src: source,
|
||||
type: 'application/x-mpegURL',
|
||||
},
|
||||
],
|
||||
@@ -75,6 +86,7 @@ export default function OwncastPlayer(props) {
|
||||
player.on('playing', () => {
|
||||
player.log('player is playing');
|
||||
ping.start();
|
||||
setVideoState(VideoState.Playing);
|
||||
});
|
||||
|
||||
player.on('pause', () => {
|
||||
@@ -85,10 +97,26 @@ export default function OwncastPlayer(props) {
|
||||
player.on('ended', () => {
|
||||
player.log('player is ended');
|
||||
ping.stop();
|
||||
setVideoState(VideoState.Unavailable);
|
||||
});
|
||||
|
||||
player.on('volumechange', handleVolume);
|
||||
};
|
||||
|
||||
return <VideoJS options={videoJsOptions} onReady={handlePlayerReady} />;
|
||||
return (
|
||||
<div style={{ display: 'grid' }}>
|
||||
{online && (
|
||||
<div style={{ gridColumn: 1, gridRow: 1 }}>
|
||||
<VideoJS
|
||||
style={{ gridColumn: 1, gridRow: 1 }}
|
||||
options={videoJsOptions}
|
||||
onReady={handlePlayerReady}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div style={{ gridColumn: 1, gridRow: 1 }}>
|
||||
<VideoPoster online={online} initialSrc="/logo" src="/thumbnail.jpg" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
.player {
|
||||
height: 80vh;
|
||||
width: 100%;
|
||||
background-color: green;
|
||||
background-color: black;
|
||||
|
||||
.vjs-big-play-centered .vjs-big-play-button {
|
||||
z-index: 99999 !important;
|
||||
}
|
||||
}
|
||||
5
web/components/video/VideoPoster.module.scss
Normal file
5
web/components/video/VideoPoster.module.scss
Normal file
@@ -0,0 +1,5 @@
|
||||
.poster {
|
||||
background-color: black;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -1,109 +1,46 @@
|
||||
/*
|
||||
VideoPoster is the image that covers up the video component and shows a
|
||||
preview of the video, refreshing every N seconds.
|
||||
It's more complex than it needs to be, using the "double buffer" approach to
|
||||
cross-fade the two images. Now that we've moved to React we may be able to
|
||||
simply use some simple cross-fading component.
|
||||
*/
|
||||
import { useEffect, useState } from 'react';
|
||||
import CrossfadeImage from '../ui/CrossfadeImage/CrossfadeImage';
|
||||
import s from './VideoPoster.module.scss';
|
||||
|
||||
import { useEffect, useLayoutEffect, useState } from 'react';
|
||||
import { ReactElement } from 'react-markdown/lib/react-markdown';
|
||||
const REFRESH_INTERVAL = 20_000;
|
||||
|
||||
const REFRESH_INTERVAL = 15000;
|
||||
const TEMP_IMAGE = 'http://localhost:8080/logo';
|
||||
const POSTER_BASE_URL = 'http://localhost:8080/';
|
||||
interface Props {
|
||||
initialSrc: string;
|
||||
src: string;
|
||||
online: boolean;
|
||||
}
|
||||
|
||||
export default function VideoPoster(props): ReactElement {
|
||||
const { active } = props;
|
||||
const [flipped, setFlipped] = useState(false);
|
||||
const [oldUrl, setOldUrl] = useState(TEMP_IMAGE);
|
||||
const [url, setUrl] = useState(props.url);
|
||||
const [currentUrl, setCurrentUrl] = useState(TEMP_IMAGE);
|
||||
const [loadingImage, setLoadingImage] = useState(TEMP_IMAGE);
|
||||
const [offlineImage, setOfflineImage] = useState(TEMP_IMAGE);
|
||||
export default function VideoPoster(props: Props) {
|
||||
const { online, initialSrc, src: base } = props;
|
||||
|
||||
let refreshTimer = null;
|
||||
|
||||
const setLoaded = () => {
|
||||
setFlipped(!flipped);
|
||||
setUrl(loadingImage);
|
||||
setOldUrl(currentUrl);
|
||||
};
|
||||
|
||||
const fire = () => {
|
||||
const cachebuster = Math.round(new Date().getTime() / 1000);
|
||||
setLoadingImage(`${POSTER_BASE_URL}?cb=${cachebuster}`);
|
||||
const img = new Image();
|
||||
img.onload = setLoaded;
|
||||
img.src = loadingImage;
|
||||
};
|
||||
|
||||
const stopRefreshTimer = () => {
|
||||
clearInterval(refreshTimer);
|
||||
refreshTimer = null;
|
||||
};
|
||||
|
||||
const startRefreshTimer = () => {
|
||||
stopRefreshTimer();
|
||||
fire();
|
||||
// Load a new copy of the image every n seconds
|
||||
refreshTimer = setInterval(fire, REFRESH_INTERVAL);
|
||||
};
|
||||
let timer: ReturnType<typeof setInterval>;
|
||||
const [src, setSrc] = useState(initialSrc);
|
||||
const [duration, setDuration] = useState('0s');
|
||||
|
||||
useEffect(() => {
|
||||
if (active) {
|
||||
fire();
|
||||
startRefreshTimer();
|
||||
} else {
|
||||
stopRefreshTimer();
|
||||
}
|
||||
}, [active]);
|
||||
clearInterval(timer);
|
||||
timer = setInterval(() => {
|
||||
if (duration === '0s') {
|
||||
setDuration('3s');
|
||||
}
|
||||
|
||||
// On component unmount.
|
||||
useLayoutEffect(
|
||||
() => () => {
|
||||
stopRefreshTimer();
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// TODO: Replace this with React memo logic.
|
||||
// shouldComponentUpdate(prevProps, prevState) {
|
||||
// return (
|
||||
// this.props.active !== prevProps.active ||
|
||||
// this.props.offlineImage !== prevProps.offlineImage ||
|
||||
// this.state.url !== prevState.url ||
|
||||
// this.state.oldUrl !== prevState.oldUrl
|
||||
// );
|
||||
// }
|
||||
|
||||
if (!active) {
|
||||
return (
|
||||
<div id="oc-custom-poster">
|
||||
<ThumbImage url={offlineImage} visible />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
setSrc(`${base}?${Date.now()}`);
|
||||
}, REFRESH_INTERVAL);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div id="oc-custom-poster">
|
||||
<ThumbImage url={!flipped ? oldUrl : url} visible />
|
||||
<ThumbImage url={flipped ? oldUrl : url} visible={!flipped} />
|
||||
<div className={s.poster}>
|
||||
{!online && <img src={initialSrc} alt="logo" height="500vh" />}
|
||||
|
||||
{online && (
|
||||
<CrossfadeImage
|
||||
src={src}
|
||||
duration={duration}
|
||||
objectFit="contain"
|
||||
width="100%"
|
||||
height="500px"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ThumbImage({ url, visible }) {
|
||||
if (!url) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className="custom-thumbnail-image"
|
||||
style={{
|
||||
opacity: visible ? 1 : 0,
|
||||
backgroundImage: `url(${url})`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user