Skip to content

fix: prevent memory leak in Visualizer component #114

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jan 23, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -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"
},
96 changes: 76 additions & 20 deletions src/components/Visualizer.js
Original file line number Diff line number Diff line change
@@ -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;
Comment on lines +8 to +9
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since they are changing, should we set the to states?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure what rafId stands for.


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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we do (prevProps.playing !== this.props.playing && prevState.isTabVisible !== this.state.isTabVisible) so all of the conditions can stay in one block.

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