4 changed files with 125 additions and 142 deletions
@ -0,0 +1,109 @@ |
|||||||
|
/* |
||||||
|
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, useLayoutEffect, useState } from 'react'; |
||||||
|
import { ReactElement } from 'react-markdown/lib/react-markdown'; |
||||||
|
|
||||||
|
const REFRESH_INTERVAL = 15000; |
||||||
|
const TEMP_IMAGE = 'http://localhost:8080/logo'; |
||||||
|
const POSTER_BASE_URL = 'http://localhost:8080/'; |
||||||
|
|
||||||
|
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); |
||||||
|
|
||||||
|
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); |
||||||
|
}; |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (active) { |
||||||
|
fire(); |
||||||
|
startRefreshTimer(); |
||||||
|
} else { |
||||||
|
stopRefreshTimer(); |
||||||
|
} |
||||||
|
}, [active]); |
||||||
|
|
||||||
|
// 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> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<div id="oc-custom-poster"> |
||||||
|
<ThumbImage url={!flipped ? oldUrl : url} visible /> |
||||||
|
<ThumbImage url={flipped ? oldUrl : url} visible={!flipped} /> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
function ThumbImage({ url, visible }) { |
||||||
|
if (!url) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
return ( |
||||||
|
<div |
||||||
|
className="custom-thumbnail-image" |
||||||
|
style={{ |
||||||
|
opacity: visible ? 1 : 0, |
||||||
|
backgroundImage: `url(${url})`, |
||||||
|
}} |
||||||
|
/> |
||||||
|
); |
||||||
|
} |
||||||
@ -0,0 +1,16 @@ |
|||||||
|
import React from 'react'; |
||||||
|
import { ComponentStory, ComponentMeta } from '@storybook/react'; |
||||||
|
import VideoPoster from '../components/video/VideoPoster'; |
||||||
|
|
||||||
|
export default { |
||||||
|
title: 'owncast/VideoPoster', |
||||||
|
component: VideoPoster, |
||||||
|
parameters: {}, |
||||||
|
} as ComponentMeta<typeof VideoPoster>; |
||||||
|
|
||||||
|
const VideoPosterExample = () => <VideoPoster />; |
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const Template: ComponentStory<typeof VideoPoster> = args => <VideoPosterExample />; |
||||||
|
|
||||||
|
export const Basic = Template.bind({}); |
||||||
@ -1,114 +0,0 @@ |
|||||||
import { h, Component } from '/js/web_modules/preact.js'; |
|
||||||
import htm from '/js/web_modules/htm.js'; |
|
||||||
const html = htm.bind(h); |
|
||||||
|
|
||||||
import { TEMP_IMAGE } from '../utils/constants.js'; |
|
||||||
|
|
||||||
const REFRESH_INTERVAL = 15000; |
|
||||||
const POSTER_BASE_URL = '/thumbnail.jpg'; |
|
||||||
|
|
||||||
export default class VideoPoster extends Component { |
|
||||||
constructor(props) { |
|
||||||
super(props); |
|
||||||
|
|
||||||
this.state = { |
|
||||||
// flipped is the state of showing primary/secondary image views
|
|
||||||
flipped: false, |
|
||||||
oldUrl: TEMP_IMAGE, |
|
||||||
url: TEMP_IMAGE, |
|
||||||
}; |
|
||||||
|
|
||||||
this.refreshTimer = null; |
|
||||||
this.startRefreshTimer = this.startRefreshTimer.bind(this); |
|
||||||
this.fire = this.fire.bind(this); |
|
||||||
this.setLoaded = this.setLoaded.bind(this); |
|
||||||
} |
|
||||||
componentDidMount() { |
|
||||||
if (this.props.active) { |
|
||||||
this.fire(); |
|
||||||
this.startRefreshTimer(); |
|
||||||
} |
|
||||||
} |
|
||||||
shouldComponentUpdate(prevProps, prevState) { |
|
||||||
return this.props.active !== prevProps.active || |
|
||||||
this.props.offlineImage !== prevProps.offlineImage || |
|
||||||
this.state.url !== prevState.url || |
|
||||||
this.state.oldUrl !== prevState.oldUrl; |
|
||||||
} |
|
||||||
componentDidUpdate(prevProps) { |
|
||||||
const { active } = this.props; |
|
||||||
const { active: prevActive } = prevProps; |
|
||||||
|
|
||||||
if (active && !prevActive) { |
|
||||||
this.startRefreshTimer(); |
|
||||||
} else if (!active && prevActive) { |
|
||||||
this.stopRefreshTimer(); |
|
||||||
} |
|
||||||
} |
|
||||||
componentWillUnmount() { |
|
||||||
this.stopRefreshTimer(); |
|
||||||
} |
|
||||||
|
|
||||||
startRefreshTimer() { |
|
||||||
this.stopRefreshTimer(); |
|
||||||
this.fire(); |
|
||||||
// Load a new copy of the image every n seconds
|
|
||||||
this.refreshTimer = setInterval(this.fire, REFRESH_INTERVAL); |
|
||||||
} |
|
||||||
|
|
||||||
// load new img
|
|
||||||
fire() { |
|
||||||
const cachebuster = Math.round(new Date().getTime() / 1000); |
|
||||||
this.loadingImage = POSTER_BASE_URL + '?cb=' + cachebuster; |
|
||||||
const img = new Image(); |
|
||||||
img.onload = this.setLoaded; |
|
||||||
img.src = this.loadingImage; |
|
||||||
} |
|
||||||
|
|
||||||
setLoaded() { |
|
||||||
const { url: currentUrl, flipped } = this.state; |
|
||||||
this.setState({ |
|
||||||
flipped: !flipped, |
|
||||||
url: this.loadingImage, |
|
||||||
oldUrl: currentUrl, |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
stopRefreshTimer() { |
|
||||||
clearInterval(this.refreshTimer); |
|
||||||
this.refreshTimer = null; |
|
||||||
} |
|
||||||
|
|
||||||
render() { |
|
||||||
const { active, offlineImage } = this.props; |
|
||||||
const { url, oldUrl, flipped } = this.state; |
|
||||||
if (!active) { |
|
||||||
return html` |
|
||||||
<div id="oc-custom-poster"> |
|
||||||
<${ThumbImage} url=${offlineImage} visible=${true} /> |
|
||||||
</div> |
|
||||||
`;
|
|
||||||
} |
|
||||||
return html` |
|
||||||
<div id="oc-custom-poster"> |
|
||||||
<${ThumbImage} url=${!flipped ? oldUrl : url } visible=${true} /> |
|
||||||
<${ThumbImage} url=${flipped ? oldUrl : url } visible=${!flipped} /> |
|
||||||
</div> |
|
||||||
`;
|
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
function ThumbImage({ url, visible }) { |
|
||||||
if (!url) { |
|
||||||
return null; |
|
||||||
} |
|
||||||
return html` |
|
||||||
<div |
|
||||||
class="custom-thumbnail-image" |
|
||||||
style=${{ |
|
||||||
opacity: visible ? 1 : 0, |
|
||||||
backgroundImage: `url(${url})`, |
|
||||||
}} |
|
||||||
/> |
|
||||||
`;
|
|
||||||
} |
|
||||||
Loading…
Reference in new issue