4 changed files with 125 additions and 142 deletions
@ -0,0 +1,109 @@
@@ -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 @@
@@ -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 @@
@@ -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