Rework music player with bulma
This commit is contained in:
parent
aa721767fa
commit
9ffc145834
|
@ -6,81 +6,85 @@ import Visualizer from "./components/visualizer";
|
||||||
import { State } from "./store";
|
import { State } from "./store";
|
||||||
|
|
||||||
type AudioState = {
|
type AudioState = {
|
||||||
audioContext: AudioContext;
|
audioContext: AudioContext;
|
||||||
audioSource: HTMLAudioElement;
|
audioSource: HTMLAudioElement;
|
||||||
audioSourceNode: MediaElementAudioSourceNode;
|
audioSourceNode: MediaElementAudioSourceNode;
|
||||||
audioVolume: GainNode;
|
audioVolume: GainNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
type MusicPlayerProps = {
|
type MusicPlayerProps = {
|
||||||
playing: boolean;
|
playing: boolean;
|
||||||
muted: boolean;
|
muted: boolean;
|
||||||
source?: string;
|
source?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
class MusicPlayer extends React.Component<MusicPlayerProps, State> {
|
class MusicPlayer extends React.Component<MusicPlayerProps, State> {
|
||||||
private audioState: AudioState;
|
private audioState: AudioState;
|
||||||
|
|
||||||
constructor(props: MusicPlayerProps) {
|
constructor(props: MusicPlayerProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const context = new AudioContext();
|
const context = new AudioContext();
|
||||||
const source = new Audio();
|
const source = new Audio();
|
||||||
const sourceNode = context.createMediaElementSource(source);
|
const sourceNode = context.createMediaElementSource(source);
|
||||||
const volume = context.createGain();
|
const volume = context.createGain();
|
||||||
|
|
||||||
sourceNode.connect(volume);
|
sourceNode.connect(volume);
|
||||||
volume.connect(context.destination);
|
volume.connect(context.destination);
|
||||||
|
|
||||||
this.audioState = {
|
this.audioState = {
|
||||||
audioContext: context,
|
audioContext: context,
|
||||||
audioSourceNode: sourceNode,
|
audioSourceNode: sourceNode,
|
||||||
audioSource: source,
|
audioSource: source,
|
||||||
audioVolume: volume,
|
audioVolume: volume,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div id="player" style={{ height: "100%", width: "100%" }}>
|
<div className="is-flex-grow-1 is-flex is-flex-direction-column">
|
||||||
<Visualizer
|
<div className="is-flex-grow-1 is-overflow-hidden">
|
||||||
audioContext={this.audioState.audioContext}
|
<Visualizer
|
||||||
audioSource={this.audioState.audioSourceNode}
|
audioContext={this.audioState.audioContext}
|
||||||
/>
|
audioSource={this.audioState.audioSourceNode}
|
||||||
<Controls />
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
<div className="is-flex-grow-0">
|
||||||
}
|
<Controls />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
componentDidUpdate() {
|
componentDidUpdate() {
|
||||||
const context = this.audioState.audioContext;
|
const context = this.audioState.audioContext;
|
||||||
const source = this.audioState.audioSource;
|
const source = this.audioState.audioSource;
|
||||||
const volume = this.audioState.audioVolume;
|
const volume = this.audioState.audioVolume;
|
||||||
|
|
||||||
// First, set the audio source (if it changed)
|
// First, set the audio source (if it changed)
|
||||||
if (this.props.source && source.src != this.props.source) {
|
if (this.props.source && source.src != this.props.source) {
|
||||||
source.src = this.props.source;
|
source.src = this.props.source;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.playing) {
|
if (this.props.playing) {
|
||||||
source
|
source
|
||||||
.play()
|
.play()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.info("Started playing audio");
|
console.info("Started playing audio");
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error(`Could not play audio: ${error}`);
|
console.error(`Could not play audio: ${error}`);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
source.pause();
|
source.pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.props.muted) {
|
if (!this.props.muted) {
|
||||||
volume.gain.setValueAtTime(1, context.currentTime);
|
volume.gain.setValueAtTime(1, context.currentTime);
|
||||||
} else {
|
} else {
|
||||||
volume.gain.setValueAtTime(0, context.currentTime);
|
volume.gain.setValueAtTime(0, context.currentTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapStateToProps(state: State): MusicPlayerProps {
|
function mapStateToProps(state: State): MusicPlayerProps {
|
||||||
|
|
|
@ -11,29 +11,45 @@ type ControlProps = {
|
||||||
|
|
||||||
class Controls extends React.Component<ControlProps, State> {
|
class Controls extends React.Component<ControlProps, State> {
|
||||||
render() {
|
render() {
|
||||||
return (
|
let title = (
|
||||||
<div id="playerControls" className="container-fluid fixed-bottom">
|
<div className="notification is-primary">
|
||||||
<div className="align-items-center row p-2">
|
<div className="level-item">{this.props.title.name}</div>
|
||||||
<Indicator></Indicator>
|
</div>
|
||||||
<div
|
);
|
||||||
id="playerText"
|
|
||||||
className="text-justify text-truncate col-6 playerControlsContent"
|
|
||||||
>
|
|
||||||
{this.props.title.name} - {this.props.title.album}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{this.props.title.name === "Journey" &&
|
if (
|
||||||
this.props.title.artist === "Mseq" ? (
|
this.props.title.name == "Journey" &&
|
||||||
<div id="copyrightNotice" className="col text-center">
|
this.props.title.artist == "Mseq"
|
||||||
<a href="http://dig.ccmixter.org/files/Mseq/54702">Journey</a>
|
) {
|
||||||
by Mseq (c) copyright 2016 Licensed under a Creative
|
title = (
|
||||||
Commons
|
<div className="notification is-primary">
|
||||||
<a href="http://creativecommons.org/licenses/by-nc/3.0/">
|
<div className="level-item">
|
||||||
Attribution Noncommercial (3.0)
|
<a href="http://dig.ccmixter.org/files/Mseq/54702">
|
||||||
</a>
|
Journey
|
||||||
license. Ft: Admiral Bob,Texas Radio Fish
|
</a>
|
||||||
</div>
|
by Mseq (c) copyright 2016 Licensed under a
|
||||||
) : null}
|
Creative Commons
|
||||||
|
<a href="http://creativecommons.org/licenses/by-nc/3.0/">
|
||||||
|
Attribution Noncommercial (3.0)
|
||||||
|
</a>
|
||||||
|
license. Ft: Admiral Bob,Texas Radio Fish
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="notification is-primary">
|
||||||
|
<div className="level">
|
||||||
|
<div className="level-left">
|
||||||
|
<Indicator></Indicator>
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
<div className="level-right">
|
||||||
|
<div className="level-item">
|
||||||
|
{this.props.title.artist}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -22,22 +22,30 @@ class Indicator extends React.Component<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const classes = classNames({
|
const button_classes = classNames({
|
||||||
btn: true,
|
button: true,
|
||||||
"col-auto": true,
|
"is-primary": true,
|
||||||
|
"level-item": true,
|
||||||
|
// TODO(tlater): Add loading logic here
|
||||||
|
});
|
||||||
|
|
||||||
|
const icon_classes = classNames({
|
||||||
fas: true,
|
fas: true,
|
||||||
|
"fa-2x": true,
|
||||||
"fa-muted": this.props.muted,
|
"fa-muted": this.props.muted,
|
||||||
"fa-play": this.props.playing,
|
"fa-play": !this.props.playing,
|
||||||
"fa-pause": !this.props.playing,
|
"fa-pause": this.props.playing,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
id="playerIndicator"
|
|
||||||
onClick={this.click.bind(this)}
|
onClick={this.click.bind(this)}
|
||||||
className={classes}
|
className={button_classes}>
|
||||||
></button>
|
<span className="icon is-medium">
|
||||||
|
<i className={icon_classes}></i>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,7 +80,7 @@ class CanvasDrawer {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.renderer.setClearColor(new three.Color(0x0f0f0f));
|
this.renderer.setClearColor(new three.Color(0x0f0f0f));
|
||||||
this.renderer.setSize(canvas.width, canvas.height);
|
this.renderer.setSize(canvas.width, canvas.height, false);
|
||||||
|
|
||||||
// Set up canvas resizing
|
// Set up canvas resizing
|
||||||
window.addEventListener("resize", this.resize.bind(this));
|
window.addEventListener("resize", this.resize.bind(this));
|
||||||
|
@ -147,28 +147,31 @@ class CanvasDrawer {
|
||||||
|
|
||||||
resize() {
|
resize() {
|
||||||
const canvas = this.canvas;
|
const canvas = this.canvas;
|
||||||
|
|
||||||
if (canvas.parentElement === null) {
|
if (canvas.parentElement === null) {
|
||||||
throw Error("Could not access canvas parent for size calculation");
|
throw Error("Could not access canvas parent for size calculation");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute the height of all our siblings
|
// This is stupid, but by setting the canvas proportions to 0
|
||||||
let combinedHeight = 0;
|
// for a split second the browser can actually figure out the
|
||||||
for (let i = 0; i < canvas.parentElement.children.length; i++) {
|
// height of the parentElement.
|
||||||
const child = canvas.parentElement.children[i];
|
//
|
||||||
|
// If the canvas is allowed to keep its height, it will
|
||||||
|
// prevent the parentElement from being able to shrink because
|
||||||
|
// its contents are filling it completely.
|
||||||
|
//
|
||||||
|
// I've seen others use `overflow: hidden` to achieve this
|
||||||
|
// (and in fact seen it render in my browser on jsfiddle), but
|
||||||
|
// that doesn't work here somehow.
|
||||||
|
canvas.height = 0;
|
||||||
|
canvas.width = 0;
|
||||||
|
|
||||||
if (child != canvas) {
|
let height = canvas.parentElement.clientHeight;
|
||||||
combinedHeight += child.clientHeight;
|
let width = canvas.parentElement.clientWidth;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The remaining space we want to fill
|
this.camera.aspect = width / height;
|
||||||
const remainingHeight = canvas.parentElement.clientHeight - combinedHeight;
|
|
||||||
canvas.height = remainingHeight;
|
|
||||||
canvas.width = canvas.parentElement.clientWidth;
|
|
||||||
|
|
||||||
this.camera.aspect = canvas.width / remainingHeight;
|
|
||||||
this.camera.updateProjectionMatrix();
|
this.camera.updateProjectionMatrix();
|
||||||
this.renderer.setSize(canvas.width, remainingHeight);
|
this.renderer.setSize(width, height, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
|
@ -179,9 +182,9 @@ class CanvasDrawer {
|
||||||
}
|
}
|
||||||
|
|
||||||
class Visualizer extends React.Component<VisualizerProps, State> {
|
class Visualizer extends React.Component<VisualizerProps, State> {
|
||||||
private analyser: AnalyserNode;
|
private analyser?: AnalyserNode;
|
||||||
private canvas: React.RefObject<HTMLCanvasElement>;
|
private canvas: React.RefObject<HTMLCanvasElement>;
|
||||||
private drawer: CanvasDrawer;
|
private drawer?: CanvasDrawer;
|
||||||
|
|
||||||
constructor(props: VisualizerProps) {
|
constructor(props: VisualizerProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -191,10 +194,10 @@ class Visualizer extends React.Component<VisualizerProps, State> {
|
||||||
render(): React.ReactNode {
|
render(): React.ReactNode {
|
||||||
return (
|
return (
|
||||||
<canvas
|
<canvas
|
||||||
id="visualizer"
|
|
||||||
ref={this.canvas}
|
ref={this.canvas}
|
||||||
style={{ width: "100%", height: "100%" }}
|
style={{
|
||||||
></canvas>
|
display: "block",
|
||||||
|
}}></canvas>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -207,11 +210,14 @@ class Visualizer extends React.Component<VisualizerProps, State> {
|
||||||
this.analyser.fftSize = 2048;
|
this.analyser.fftSize = 2048;
|
||||||
this.analyser.smoothingTimeConstant = 0.8;
|
this.analyser.smoothingTimeConstant = 0.8;
|
||||||
this.props.audioSource.connect(this.analyser);
|
this.props.audioSource.connect(this.analyser);
|
||||||
|
|
||||||
this.drawer = new CanvasDrawer(this.analyser, this.canvas.current);
|
this.drawer = new CanvasDrawer(this.analyser, this.canvas.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount(): void {
|
componentWillUnmount(): void {
|
||||||
|
if (!this.drawer || !this.analyser) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.drawer.stop();
|
this.drawer.stop();
|
||||||
this.props.audioSource.disconnect(this.analyser);
|
this.props.audioSource.disconnect(this.analyser);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import React from "react";
|
import { createRoot } from "react-dom/client";
|
||||||
import ReactDOM from "react-dom";
|
|
||||||
import { Provider } from "react-redux";
|
import { Provider } from "react-redux";
|
||||||
|
|
||||||
import { store } from "./store";
|
import { store } from "./store";
|
||||||
|
@ -10,11 +9,11 @@ import mseq from "./Mseq_-_Journey.mp3";
|
||||||
|
|
||||||
const rootElement = document.getElementById("playerUI");
|
const rootElement = document.getElementById("playerUI");
|
||||||
|
|
||||||
ReactDOM.render(
|
const root = createRoot(rootElement!);
|
||||||
|
root.render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<MusicPlayer />
|
<MusicPlayer />
|
||||||
</Provider>,
|
</Provider>
|
||||||
rootElement
|
|
||||||
);
|
);
|
||||||
|
|
||||||
store.dispatch(setSource(mseq));
|
store.dispatch(setSource(mseq));
|
||||||
|
|
|
@ -3,6 +3,6 @@ $fa-font-path: "npm:@fortawesome/fontawesome-free/webfonts";
|
||||||
@import "~/node_modules/@fortawesome/fontawesome-free/scss/fontawesome";
|
@import "~/node_modules/@fortawesome/fontawesome-free/scss/fontawesome";
|
||||||
@import "~/node_modules/@fortawesome/fontawesome-free/scss/solid";
|
@import "~/node_modules/@fortawesome/fontawesome-free/scss/solid";
|
||||||
|
|
||||||
#playerControls {
|
.is-overflow-hidden {
|
||||||
background-color: #11151c;
|
overflow: hidden !important;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
<link rel="stylesheet" , href="music/music.scss" />
|
<link rel="stylesheet" , href="music/music.scss" />
|
||||||
</block>
|
</block>
|
||||||
|
|
||||||
<block name="footer">
|
<block name="content">
|
||||||
<div id="playerUI" class="container-fluid flex-grow-1"></div>
|
<div id="playerUI" class="is-flex-grow-1 is-flex"></div>
|
||||||
<script type="module" src="./music/index.tsx"></script>
|
<script type="module" src="./music/index.tsx"></script>
|
||||||
</block>
|
</block>
|
||||||
</extends>
|
</extends>
|
||||||
|
|
Reference in a new issue