107 lines
3.1 KiB
TypeScript
107 lines
3.1 KiB
TypeScript
import React from "react";
|
|
import { connect } from "react-redux";
|
|
|
|
import Controls from "./components/controls";
|
|
import Visualizer from "./components/visualizer";
|
|
import { State } from "./store";
|
|
|
|
type AudioState = {
|
|
audioContext: AudioContext;
|
|
audioSource: HTMLAudioElement;
|
|
audioSourceNode: MediaElementAudioSourceNode;
|
|
audioVolume: GainNode;
|
|
};
|
|
|
|
type MusicPlayerProps = {
|
|
playing: boolean;
|
|
muted: boolean;
|
|
source?: string;
|
|
};
|
|
|
|
class MusicPlayer extends React.Component<MusicPlayerProps, State> {
|
|
private audioState: AudioState;
|
|
|
|
constructor(props: MusicPlayerProps) {
|
|
super(props);
|
|
|
|
const context = new AudioContext();
|
|
const source = new Audio();
|
|
const sourceNode = context.createMediaElementSource(source);
|
|
const volume = context.createGain();
|
|
|
|
sourceNode.connect(volume);
|
|
volume.connect(context.destination);
|
|
|
|
this.audioState = {
|
|
audioContext: context,
|
|
audioSourceNode: sourceNode,
|
|
audioSource: source,
|
|
audioVolume: volume,
|
|
};
|
|
}
|
|
|
|
render() {
|
|
return (
|
|
<div className="is-flex-grow-1 is-flex is-flex-direction-column">
|
|
<div className="is-flex-grow-1 is-overflow-hidden">
|
|
<Visualizer
|
|
audioContext={this.audioState.audioContext}
|
|
audioSource={this.audioState.audioSourceNode}
|
|
/>
|
|
</div>
|
|
<div className="is-flex-grow-0">
|
|
<Controls />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
async componentDidUpdate() {
|
|
const context = this.audioState.audioContext;
|
|
const source = this.audioState.audioSource;
|
|
const volume = this.audioState.audioVolume;
|
|
|
|
// First, set the audio source (if it changed)
|
|
if (this.props.source && source.src != this.props.source) {
|
|
source.src = this.props.source;
|
|
}
|
|
|
|
if (this.props.playing) {
|
|
// Chrome is super awkward about AudioContext, and won't
|
|
// even allow creating one without complaining about
|
|
// wanting user input, so we need to resume the context
|
|
// before we can actually play.
|
|
//
|
|
// Luckily, this has no adverse effects on Firefox.
|
|
try {
|
|
await context.resume();
|
|
await source.play();
|
|
} catch (error) {
|
|
if (error instanceof DOMException) {
|
|
console.error(`Could not play audio: ${error.message}`);
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
} else {
|
|
source.pause();
|
|
}
|
|
|
|
if (!this.props.muted) {
|
|
volume.gain.setValueAtTime(1, context.currentTime);
|
|
} else {
|
|
volume.gain.setValueAtTime(0, context.currentTime);
|
|
}
|
|
}
|
|
}
|
|
|
|
function mapStateToProps(state: State): MusicPlayerProps {
|
|
return {
|
|
playing: state.musicState.playing,
|
|
muted: state.musicState.muted,
|
|
source: state.musicState.source,
|
|
};
|
|
}
|
|
|
|
export default connect(mapStateToProps)(MusicPlayer);
|