Add player poster

This commit is contained in:
Gabe Kangas
2022-05-10 15:36:09 -07:00
parent 9bb37679c0
commit ff6886575f
14 changed files with 234 additions and 111 deletions

View File

@@ -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>
);
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,5 @@
.poster {
background-color: black;
display: flex;
justify-content: center;
}

View File

@@ -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})`,
}}
/>
);
}