diff --git a/package-lock.json b/package-lock.json index d000414..c915aae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10181,6 +10181,14 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "react-page-visibility": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/react-page-visibility/-/react-page-visibility-6.3.0.tgz", + "integrity": "sha512-0sZH/i3D+TvVk22PV/xobf6mCBGkr64UXFEPQGMndP21E5d9IX43yCeVHGKIoLuXh8tR4/oO4WYk22jwCr7Asg==", + "requires": { + "prop-types": "^15.7.2" + } + }, "react-scripts": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-3.0.1.tgz", diff --git a/package.json b/package.json index fd221fa..cad118d 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "react-device-detect": "^1.7.5", "react-dom": "^16.8.6", "react-hotkeys": "^2.0.0", + "react-page-visibility": "^6.3.0", "react-scripts": "3.0.1", "store": "^2.0.12" }, diff --git a/src/components/Visualizer.js b/src/components/Visualizer.js index 8aeaf66..054a30e 100644 --- a/src/components/Visualizer.js +++ b/src/components/Visualizer.js @@ -1,7 +1,13 @@ import React from 'react'; import PropTypes from 'prop-types'; +import PageVisibility from 'react-page-visibility'; + +const DELAY = 500; + +export default class Visualizer extends React.PureComponent { + rafId = null; + timerId = null; -export default class Visualizer extends React.Component { constructor(props) { super(props); this.state = { @@ -10,7 +16,8 @@ export default class Visualizer extends React.Component { baseColour: 'rgb(10, 10, 35)', translucent: 'rgba(10, 10, 35, 0.6)', multiplier: 0.7529 - } + }, + isTabVisible: true, }; } @@ -19,10 +26,31 @@ export default class Visualizer extends React.Component { // of the audio context stuff AFTER the audio has been triggered. // We can't see it until // then anyway so it makes no difference to desktop. - componentWillReceiveProps(nextProps) { - if (nextProps.playing && !this.state.eq.context) { - this.initiateEQ(); + componentDidUpdate(prevProps, prevState) { + if (prevProps.playing === this.props.playing && prevState.isTabVisible === this.state.isTabVisible) { + return + } + + // If the player is playing and the tab is being active, + // draw the visualization + if (this.props.playing && this.state.isTabVisible) { + // Create a new audio context if there isn't one available + if (!this.state.eq.context) { + this.initiateEQ(); + } this.createVisualizer(); + this.startDrawing(); + } + // If the player is not playing or the tab is running in the background, + // stop the animation + else { + // Workaround for componentWillUnmount to delay the clean up and achieve fadeout animation + setTimeout(() => { + // Note: Order matters. + // Stop the drawing loop first (using this.rafId), then set the ID to null + this.stopDrawing(); + this.reset(); + }, DELAY); } } @@ -47,7 +75,10 @@ export default class Visualizer extends React.Component { eq.bands = new Uint8Array(eq.analyser.frequencyBinCount - 32); this.setState({ eq }); - this.updateEQBands(); + } + + reset = () => { + this.rafId = null; } /** * @@ -56,11 +87,11 @@ export default class Visualizer extends React.Component { * visualizer is up to date. */ updateEQBands() { + const newEQ = this.state.eq; // Populate the buffer with the audio source’s current data - this.state.eq.analyser.getByteFrequencyData(this.state.eq.bands); + newEQ.analyser.getByteFrequencyData(newEQ.bands); - // Can’t stop, won’t stop - requestAnimationFrame(() => this.updateEQBands()); + this.setState({ eq: { ...newEQ } }); } /** * @@ -77,22 +108,41 @@ export default class Visualizer extends React.Component { width: this._canvas.width, barWidth: this._canvas.width / this.state.eq.bands.length }; + } + + startDrawing = () => { + if (!this.rafId) { + this.rafId = window.requestAnimationFrame(this.drawingLoop); + } + }; + + stopDrawing = () => { + window.cancelAnimationFrame(this.rafId); + clearTimeout(this.timerId); + }; + drawingLoop = () => { + const haveWaveform = this.state.eq.bands.reduce((a, b) => a + b, 0) !== 0; + + this.updateEQBands(); this.drawVisualizer(); - } + + // Because timeupdate events are not triggered at browser speed, + // we use requestanimationframe for higher framerates + if (haveWaveform) { + this.rafId = window.requestAnimationFrame(this.drawingLoop); + } + // If there is no music or audio in the song, then reduce the FPS + else { + this.timerId = setTimeout(this.drawingLoop, 250); + } + }; /** * * As a base visualizer, the equalizer bands are drawn using * canvas in the window directly above the song into. */ drawVisualizer() { - if (this.state.eq.bands.reduce((a, b) => a + b, 0) !== 0) - requestAnimationFrame(() => this.drawVisualizer()); - // Because timeupdate events are not triggered at browser speed, - // we use requestanimationframe for higher framerates - // eslint-disable-next-line no-inline-comments - // If there is no music or audio in the song, then reduce the FPS - else setTimeout(() => this.drawVisualizer(), 250); // Intial bar x coordinate let y, x = 0; @@ -129,11 +179,17 @@ export default class Visualizer extends React.Component { this.visualizer.ctx.fill(); } + handleVisibilityChange = isTabVisible => { + this.setState({ isTabVisible }); + } + render() { return ( - <div id='visualizer'> - <canvas aria-label='visualizer' ref={a => (this._canvas = a)} /> - </div> + <PageVisibility onChange={this.handleVisibilityChange}> + <div id='visualizer'> + <canvas aria-label='visualizer' ref={a => (this._canvas = a)} /> + </div> + </PageVisibility> ); } }